diff --git a/crates/ps2-filetypes/src/common/sjis.rs b/crates/ps2-filetypes/src/common/sjis.rs index d4c6300..117e18b 100644 --- a/crates/ps2-filetypes/src/common/sjis.rs +++ b/crates/ps2-filetypes/src/common/sjis.rs @@ -3,7 +3,8 @@ pub fn encode_sjis(input: &str) -> Vec { .as_bytes() .iter() .flat_map(|b| match *b { - b' ' => [0x80, 0x3F], + b' ' => [0x81, 0x40], + b'.' => [0x81, 0x44], b':' => [0x81, 0x46], b'/' => [0x81, 0x5E], b'(' => [0x81, 0x69], @@ -42,6 +43,7 @@ pub fn decode_sjis(input: &[u8]) -> String { 0x81 => match pair[1] { 0x40 => b' ', 0x46 => b':', + 0x44 => b'.', 0x5E => b'/', 0x69 => b'(', 0x6A => b')', diff --git a/crates/ps2-filetypes/src/parser/icon_sys.rs b/crates/ps2-filetypes/src/parser/icon_sys.rs index 1d0ae1b..32d21d9 100644 --- a/crates/ps2-filetypes/src/parser/icon_sys.rs +++ b/crates/ps2-filetypes/src/parser/icon_sys.rs @@ -1,8 +1,8 @@ -use std::io::{Cursor, Read, Result}; use crate::color::Color; use crate::sjis::{decode_sjis, encode_sjis}; use crate::util::parse_cstring; use byteorder::{ReadBytesExt, LE}; +use std::io::{Cursor, Read, Result}; #[derive(Clone, Copy, Debug)] pub struct ColorF { @@ -75,6 +75,12 @@ pub struct IconSys { } impl IconSys { + // Maximum length of a title the icon.sys format can hold in bytes (2 bytes per character) + pub const MAXIMUM_TITLE_BYTE_LENGTH: usize = 68; + + // Maximum length of a filename (for the .icn files) the icon.sys format can hold in bytes (2 bytes per character) + pub const MAXIMUM_FILENAME_BYTE_LENGTH: usize = 64; + pub fn new(bytes: Vec) -> Self { parse_icon_sys(bytes).unwrap() } @@ -104,7 +110,7 @@ impl IconSys { let title_bytes = encode_sjis(&self.title); let title_len = title_bytes.len(); - if title_len > 68 { + if title_len > Self::MAXIMUM_TITLE_BYTE_LENGTH { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, "Title length exceeds 68 bytes", @@ -112,26 +118,38 @@ impl IconSys { } bytes.extend_from_slice(&title_bytes); - if title_len < 68 { - bytes.extend(vec![0; 68 - title_len]); + if title_len < Self::MAXIMUM_TITLE_BYTE_LENGTH { + bytes.extend(vec![0; Self::MAXIMUM_TITLE_BYTE_LENGTH - title_len]); } bytes.extend_from_slice(self.icon_file.as_bytes()); - if self.icon_file.len() < 64 { - bytes.extend(vec![0; 64 - self.icon_file.len()]); + if self.icon_file.len() < Self::MAXIMUM_FILENAME_BYTE_LENGTH { + bytes.extend(vec![ + 0; + Self::MAXIMUM_FILENAME_BYTE_LENGTH + - self.icon_file.len() + ]); } bytes.extend_from_slice(self.icon_copy_file.as_bytes()); - if self.icon_copy_file.len() < 64 { - bytes.extend(vec![0; 64 - self.icon_copy_file.len()]); + if self.icon_copy_file.len() < Self::MAXIMUM_FILENAME_BYTE_LENGTH { + bytes.extend(vec![ + 0; + Self::MAXIMUM_FILENAME_BYTE_LENGTH + - self.icon_copy_file.len() + ]); } bytes.extend_from_slice(self.icon_delete_file.as_bytes()); - if self.icon_delete_file.len() < 64 { - bytes.extend(vec![0; 64 - self.icon_delete_file.len()]); + if self.icon_delete_file.len() < Self::MAXIMUM_FILENAME_BYTE_LENGTH { + bytes.extend(vec![ + 0; + Self::MAXIMUM_FILENAME_BYTE_LENGTH + - self.icon_delete_file.len() + ]); } bytes.extend(vec![0; 512]); diff --git a/crates/suitcase/src/components/mod.rs b/crates/suitcase/src/components/mod.rs index 7621ed8..0690c2a 100644 --- a/crates/suitcase/src/components/mod.rs +++ b/crates/suitcase/src/components/mod.rs @@ -1,10 +1,11 @@ pub mod bottom_bar; pub mod buttons; pub mod dialogs; +pub(crate) mod file_picker; pub mod file_tree; +pub mod greeting; pub mod menu_bar; pub mod menu_item; pub mod tab_viewer; pub mod toolbar; -pub mod greeting; -pub(crate) mod file_picker; +pub mod value_select; diff --git a/crates/suitcase/src/components/value_select.rs b/crates/suitcase/src/components/value_select.rs new file mode 100644 index 0000000..eabb2b2 --- /dev/null +++ b/crates/suitcase/src/components/value_select.rs @@ -0,0 +1,67 @@ +use eframe::egui::{CornerRadius, Id, PopupCloseBehavior, Response, Ui}; +use std::ops::Add; + +pub fn value_select( + ui: &mut Ui, + name: impl Into, + selected_value: &mut String, + values: &[String], +) -> Response { + let id = Id::from(name.into()); + let mut layout_response = ui.horizontal(|ui| { + ui.style_mut().spacing.item_spacing.x = 1.0; + + set_border_radius( + ui, + CornerRadius { + nw: 2, + sw: 2, + ne: 0, + se: 0, + }, + ); + let edit_response = ui.text_edit_singleline(selected_value); + + set_border_radius( + ui, + CornerRadius { + nw: 0, + sw: 0, + ne: 2, + se: 2, + }, + ); + let button_response = ui.button("🔽"); + button_response.clicked().then(|| { + ui.memory_mut(|mem| { + mem.toggle_popup(id); + }); + }); + + (edit_response, button_response) + }); + + // Small hack to ensure the popup is positioned correctly + let res = Response { + rect: layout_response.response.rect, + ..layout_response.inner.1 + }; + + eframe::egui::popup_below_widget(ui, id, &res, PopupCloseBehavior::CloseOnClick, |ui| { + ui.set_min_width(200.0); + values.iter().for_each(|value| { + if ui.selectable_label(false, value.clone()).clicked() { + *selected_value = value.clone(); + layout_response.inner.0.mark_changed(); + } + }); + }); + + layout_response.inner.0 +} + +fn set_border_radius(ui: &mut Ui, radius: CornerRadius) { + ui.style_mut().visuals.widgets.hovered.corner_radius = radius.add(CornerRadius::same(1)); + ui.style_mut().visuals.widgets.inactive.corner_radius = radius; + ui.style_mut().visuals.widgets.active.corner_radius = radius; +} diff --git a/crates/suitcase/src/tabs/icn_viewer.rs b/crates/suitcase/src/tabs/icn_viewer.rs index 04735e6..4d5f575 100644 --- a/crates/suitcase/src/tabs/icn_viewer.rs +++ b/crates/suitcase/src/tabs/icn_viewer.rs @@ -9,7 +9,10 @@ use crate::{ }; use cgmath::Vector3; use eframe::egui::load::SizedTexture; -use eframe::egui::{vec2, ColorImage, ComboBox, Grid, Id, ImageData, ImageSource, Stroke, TextureId, TextureOptions, WidgetText}; +use eframe::egui::{ + vec2, ColorImage, ComboBox, Grid, Id, ImageData, ImageSource, Stroke, TextureId, + TextureOptions, WidgetText, +}; use eframe::{ egui, egui::{include_image, menu, Color32, Ui}, @@ -64,9 +67,11 @@ impl<'a> TabViewer for ICNTabViewer<'a> { } ui.end_row(); ui.label("Compression"); - ComboBox::from_id_salt("compression").selected_text("On").show_ui(ui, |ui| { - ui.selectable_label(true, "On"); - }) + ComboBox::from_id_salt("compression") + .selected_text("On") + .show_ui(ui, |ui| { + ui.selectable_label(true, "On"); + }) }); } ICNTab::IconSysProperties => { @@ -139,25 +144,45 @@ impl ICNViewer { impl ICNViewer { pub fn new(file: &VirtualFile, state: &AppState) -> Self { let mut background_colors = [Color32::DARK_GRAY; 4]; - let mut light_colors = [ColorF{r: 0.0, g: 0.0, b: 0.0, a: 0.0}; 3]; - let mut light_positions = [Vector{x: 0.0, y: 0.0, z: 0.0, w: 0.0}; 3]; - let mut ambient_color = ColorF{r: 0.1, g: 0.1, b: 0.1, a: 0.0}; + let mut light_colors = [ColorF { + r: 0.0, + g: 0.0, + b: 0.0, + a: 0.0, + }; 3]; + let mut light_positions = [Vector { + x: 0.0, + y: 0.0, + z: 0.0, + w: 0.0, + }; 3]; + let mut ambient_color = ColorF { + r: 0.1, + g: 0.1, + b: 0.1, + a: 0.0, + }; let buf = std::fs::read(&file.file_path).expect("File not found"); let icon_sys = file.file_path.clone().parent().unwrap().join("icon.sys"); if icon_sys.exists() { let icon_sys = IconSys::new(std::fs::read(icon_sys).unwrap()); - background_colors = icon_sys.background_colors.map(|c| PS2RgbaInterface::build_from_color(c).into()); + background_colors = icon_sys + .background_colors + .map(|c| PS2RgbaInterface::build_from_color(c).into()); light_colors = icon_sys.light_colors; light_positions = icon_sys.light_directions; } let icn = ps2_filetypes::ICNParser::read(&buf.clone()).unwrap(); - let mut dock_state = - DockState::new(vec![ICNTab::IconProperties]); + let mut dock_state = DockState::new(vec![ICNTab::IconProperties]); - dock_state.main_surface_mut().split_below(NodeIndex::root(), 0.5, vec![ICNTab::IconSysProperties]); + dock_state.main_surface_mut().split_below( + NodeIndex::root(), + 0.5, + vec![ICNTab::IconSysProperties], + ); Self { dock_state, @@ -218,8 +243,7 @@ impl ICNViewer { let mut delta_pitch = 0.0; let mut delta_zoom = 0.0; - if response.dragged() - { + if response.dragged() { let delta = response.drag_delta(); delta_yaw -= delta.x * 0.01; delta_pitch += delta.y * 0.01; @@ -257,7 +281,15 @@ impl ICNViewer { if closing { renderer.drop(painter.gl()) } else { - renderer.paint(painter.gl(), aspect_ratio, camera, frame, light_colors, light_positions, ambient_color); + renderer.paint( + painter.gl(), + aspect_ratio, + camera, + frame, + light_colors, + light_positions, + ambient_color, + ); } })), }; @@ -300,7 +332,8 @@ impl ICNViewer { }); ui.vertical(|ui| { menu::bar(ui, |ui| { - ui.set_height(50.0); + ui.set_height(Self::TOOLBAR_HEIGHT); + ui.add_space(Self::TOOLBAR_LEFT_MARGIN); if ui .icon_text_button( include_image!("../../assets/icons/file-arrow-right.svg"), @@ -358,10 +391,13 @@ impl ICNViewer { ui.vertical(|ui| { ui.set_height(ui.available_size_before_wrap().y - 28.0); - egui::Frame::canvas(ui.style()).stroke(Stroke::NONE).corner_radius(0).show(ui, |ui| { - draw_background(ui, &self.background_colors); - self.custom_painting(ui); - }); + egui::Frame::canvas(ui.style()) + .stroke(Stroke::NONE) + .corner_radius(0) + .show(ui, |ui| { + draw_background(ui, &self.background_colors); + self.custom_painting(ui); + }); }); if self.icn.animation_header.frame_length > 1 { diff --git a/crates/suitcase/src/tabs/icon_sys_viewer.rs b/crates/suitcase/src/tabs/icon_sys_viewer.rs index dfdb78d..212af46 100644 --- a/crates/suitcase/src/tabs/icon_sys_viewer.rs +++ b/crates/suitcase/src/tabs/icon_sys_viewer.rs @@ -1,12 +1,12 @@ +use crate::components::value_select::value_select; use crate::tabs::Tab; use crate::{AppState, VirtualFile}; use eframe::egui; -use eframe::egui::{ - vec2, Color32, CornerRadius, Grid, Id, PopupCloseBehavior, Response, Rgba, TextEdit, Ui, -}; +use eframe::egui::{menu, vec2, Color32, Grid, Rgba, Ui}; use ps2_filetypes::color::Color; use ps2_filetypes::{ColorF, IconSys, Vector}; use relative_path::PathExt; +use std::cmp::min; use std::ops::Add; use std::path::PathBuf; @@ -82,7 +82,8 @@ impl Light { } pub struct IconSysViewer { - title: String, + title_first_line: String, + title_second_line: String, file: String, pub icon_file: String, pub icon_copy_file: String, @@ -93,6 +94,7 @@ pub struct IconSysViewer { pub lights: [Light; 3], pub sys: IconSys, pub file_path: PathBuf, + modified: bool, } impl IconSysViewer { @@ -101,8 +103,12 @@ impl IconSysViewer { let sys = IconSys::new(buf); + let split_position: usize = min(sys.linebreak_pos as usize, sys.title.len()); + let (title_first_line, title_second_line) = sys.title.split_at(split_position).to_owned(); + Self { - title: sys.title.clone(), + title_first_line: title_first_line.to_string(), + title_second_line: title_second_line.to_string(), icon_file: sys.icon_file.clone(), icon_copy_file: sys.icon_copy_file.clone(), icon_delete_file: sys.icon_delete_file.clone(), @@ -126,10 +132,13 @@ impl IconSysViewer { .relative_to(state.opened_folder.clone().unwrap()) .unwrap() .to_string(), + modified: false, } } pub fn show(&mut self, ui: &mut Ui, app: &mut AppState) { + const SPACE_AROUND_HEADING: f32 = 10.0; + const LABEL_COLUMN_WIDTH: f32 = 135.0; let files: Vec = app .files .iter() @@ -150,117 +159,211 @@ impl IconSysViewer { }) .collect(); - ui.vertical(|ui| { - // eframe::egui::Grid::new(Id::from("IconSysEditor")) - // .num_columns(2) - // .show(ui, |ui| { - ui.heading("Icon Configuration"); - ui.add_space(4.0); - ui.horizontal(|ui| { - ui.label("Title"); - ui.add(TextEdit::singleline(&mut self.title)); - }); - - ui.heading("Icons"); - ui.add_space(4.0); - - Grid::new("icons").num_columns(2).show(ui, |ui| { - ui.label("List"); - file_select(ui, "list_icon", &mut self.icon_file, &files); - ui.end_row(); - ui.label("Copy"); - file_select(ui, "copy_icon", &mut self.icon_copy_file, &files); - ui.end_row(); - ui.label("Delete"); - file_select(ui, "delete_icon", &mut self.icon_delete_file, &files); - }); - - ui.heading("Background"); - ui.add_space(4.0); - - const SPACING: f32 = 40.0; + menu::bar(ui, |ui| { + ui.set_height(Self::TOOLBAR_HEIGHT); + ui.add_space(Self::TOOLBAR_LEFT_MARGIN); + ui.button("Save").clicked().then(|| self.save()); + }); - ui.add_sized(vec2(SPACING * 3.0, SPACING * 3.0), |ui: &mut Ui| { - draw_background(ui, &self.background_colors); - ui.spacing_mut().interact_size = vec2(SPACING, SPACING); - ui.spacing_mut().item_spacing = vec2(0.0, 0.0); + ui.separator(); - ui.columns(3, |cols| { - egui::widgets::color_picker::color_edit_button_rgb( - &mut cols[0], - &mut self.background_colors[0].rgb, - ); - cols[1].add_space(SPACING); - egui::widgets::color_picker::color_edit_button_rgb( - &mut cols[2], - &mut self.background_colors[1].rgb, + egui::ScrollArea::vertical().show(ui, |ui| { + ui.heading("Icon Configuration"); + ui.add_space(SPACE_AROUND_HEADING); + Grid::new("title") + .num_columns(2) + .min_col_width(LABEL_COLUMN_WIDTH) + .show(ui, |ui| { + ui.label("Title first line"); + ui.text_edit_singleline(&mut self.title_first_line) + .changed() + .then(|| self.modified = true); + ui.end_row(); + ui.label("Title second line"); + ui.text_edit_singleline(&mut self.title_second_line) + .changed() + .then(|| self.modified = true); + + length_warning( + ui, + self.title_first_line.len() + self.title_second_line.len(), + IconSys::MAXIMUM_TITLE_BYTE_LENGTH / 2, + "Title too long!", ); + }); - cols[0].add_space(SPACING); - cols[1].add_space(SPACING); - cols[2].add_space(SPACING); - - egui::widgets::color_picker::color_edit_button_rgb( - &mut cols[0], - &mut self.background_colors[2].rgb, + ui.add_space(SPACE_AROUND_HEADING); + ui.separator(); + ui.heading("Icons"); + ui.add_space(SPACE_AROUND_HEADING); + + Grid::new("icons") + .num_columns(2) + .min_col_width(LABEL_COLUMN_WIDTH) + .show(ui, |ui| { + ui.label("List"); + value_select(ui, "list_icon", &mut self.icon_file, &files) + .changed() + .then(|| self.modified = true); + length_warning( + ui, + self.icon_file.len(), + IconSys::MAXIMUM_FILENAME_BYTE_LENGTH / 2, + "Filename too long!", + ); + ui.end_row(); + + ui.label("Copy"); + value_select(ui, "copy_icon", &mut self.icon_copy_file, &files) + .changed() + .then(|| self.modified = true); + length_warning( + ui, + self.icon_copy_file.len(), + IconSys::MAXIMUM_FILENAME_BYTE_LENGTH / 2, + "Filename too long!", ); - cols[1].add_space(SPACING); - egui::widgets::color_picker::color_edit_button_rgb( - &mut cols[2], - &mut self.background_colors[3].rgb, + ui.end_row(); + + ui.label("Delete"); + value_select(ui, "delete_icon", &mut self.icon_delete_file, &files) + .changed() + .then(|| self.modified = true); + length_warning( + ui, + self.icon_delete_file.len(), + IconSys::MAXIMUM_FILENAME_BYTE_LENGTH / 2, + "Filename too long!", ); }); - ui.response() - }); - Grid::new("background").num_columns(2).show(ui, |ui| { - ui.label("Background Transparency").on_hover_ui(|ui| { - ui.label( - "This is the opposite of opacity, so a value of 100 will make \ - the background completely transparent", + ui.add_space(SPACE_AROUND_HEADING); + ui.separator(); + ui.heading("Background"); + ui.add_space(SPACE_AROUND_HEADING); + + Grid::new("background") + .num_columns(2) + .min_col_width(LABEL_COLUMN_WIDTH) + .show(ui, |ui| { + const GRADIENT_BOX_SPACING: f32 = 40.0; + + ui.add_sized( + vec2(GRADIENT_BOX_SPACING * 3.0, GRADIENT_BOX_SPACING * 3.0), + |ui: &mut Ui| { + draw_background(ui, &self.background_colors); + ui.spacing_mut().interact_size = + vec2(GRADIENT_BOX_SPACING, GRADIENT_BOX_SPACING); + ui.spacing_mut().item_spacing = vec2(0.0, 0.0); + + ui.columns(3, |cols| { + egui::widgets::color_picker::color_edit_button_rgb( + &mut cols[0], + &mut self.background_colors[0].rgb, + ) + .changed() + .then(|| self.modified = true); + cols[1].add_space(GRADIENT_BOX_SPACING); + egui::widgets::color_picker::color_edit_button_rgb( + &mut cols[2], + &mut self.background_colors[1].rgb, + ) + .changed() + .then(|| self.modified = true); + + cols[0].add_space(GRADIENT_BOX_SPACING); + cols[1].add_space(GRADIENT_BOX_SPACING); + cols[2].add_space(GRADIENT_BOX_SPACING); + + egui::widgets::color_picker::color_edit_button_rgb( + &mut cols[0], + &mut self.background_colors[2].rgb, + ) + .changed() + .then(|| self.modified = true); + cols[1].add_space(GRADIENT_BOX_SPACING); + egui::widgets::color_picker::color_edit_button_rgb( + &mut cols[2], + &mut self.background_colors[3].rgb, + ) + .changed() + .then(|| self.modified = true); + }); + ui.response() + }, ); + + Grid::new("background").num_columns(2).show(ui, |ui| { + ui.label("Background Transparency").on_hover_ui(|ui| { + ui.label( + "This is the opposite of opacity, so a value of 100 will make \ + the background completely transparent", + ); + }); + ui.add(egui::Slider::new( + &mut self.background_transparency, + 0..=100, + )) + .changed() + .then(|| self.modified = true); + ui.end_row(); + + ui.label("Ambient Color"); + egui::widgets::color_picker::color_edit_button_rgb( + ui, + &mut self.ambient_color.rgb, + ) + .changed() + .then(|| self.modified = true); + ui.end_row(); + }); }); - ui.add(egui::Slider::new( - &mut self.background_transparency, - 0..=100, - )); - ui.end_row(); - ui.label("Ambient Color"); - egui::widgets::color_picker::color_edit_button_rgb(ui, &mut self.ambient_color.rgb); - ui.end_row(); - }); + ui.add_space(SPACE_AROUND_HEADING); + ui.separator(); ui.heading("Lights"); - ui.add_space(4.0); - - for (index, light) in self.lights.iter_mut().enumerate() { - let human_readable_index = index + 1; - ui.label(format!("Light {human_readable_index}")); - ui.end_row(); - ui.label("Color"); - egui::widgets::color_picker::color_edit_button_rgb(ui, &mut light.color.rgb); - ui.end_row(); - - ui.label("X"); - ui.add(egui::Slider::new(&mut light.direction.x, 0.0..=1.0)); - ui.end_row(); - ui.label("Y"); - ui.add(egui::Slider::new(&mut light.direction.y, 0.0..=1.0)); - ui.end_row(); - ui.label("Z"); - ui.add(egui::Slider::new(&mut light.direction.z, 0.0..=1.0)); - ui.end_row(); - - Ui::separator(ui); - ui.end_row(); - } - - // }); - ui.button("Save") - .on_hover_text("Save changes") - .clicked() - .then(|| { - self.save(); + ui.add_space(SPACE_AROUND_HEADING); + Grid::new("lights") + .num_columns(3) + .spacing([50.0, 50.0]) + .min_col_width(LABEL_COLUMN_WIDTH) + .striped(true) + .show(ui, |ui| { + for (index, light) in self.lights.iter_mut().enumerate() { + Grid::new(format!("light{index}")) + .num_columns(2) + .min_col_width(50.0) + .show(ui, |ui| { + ui.label(format!("Light {}", index + 1)); + ui.end_row(); + + ui.label("Color"); + egui::widgets::color_picker::color_edit_button_rgb( + ui, + &mut light.color.rgb, + ) + .changed() + .then(|| self.modified = true); + ui.end_row(); + + ui.label("X"); + ui.add(egui::Slider::new(&mut light.direction.x, 0.0..=1.0)) + .changed() + .then(|| self.modified = true); + ui.end_row(); + + ui.label("Y"); + ui.add(egui::Slider::new(&mut light.direction.y, 0.0..=1.0)) + .changed() + .then(|| self.modified = true); + ui.end_row(); + + ui.label("Z"); + ui.add(egui::Slider::new(&mut light.direction.z, 0.0..=1.0)) + .changed() + .then(|| self.modified = true); + }); + } }); }); } @@ -276,19 +379,17 @@ impl Tab for IconSysViewer { } fn get_modified(&self) -> bool { - self.sys.title != self.title - || self.sys.icon_file != self.icon_file - || self.sys.icon_copy_file != self.icon_copy_file - || self.sys.icon_delete_file != self.icon_delete_file + self.modified } fn save(&mut self) { let new_sys = IconSys { - title: self.title.clone(), + title: format!("{}{}", self.title_first_line, self.title_second_line), + linebreak_pos: self.title_first_line.len() as u16, icon_file: self.icon_file.clone(), icon_copy_file: self.icon_copy_file.clone(), icon_delete_file: self.icon_delete_file.clone(), - background_transparency: self.background_transparency.clone(), + background_transparency: self.background_transparency, ambient_color: self.ambient_color.to_color_f(), background_colors: [ self.background_colors[0].to_color(), @@ -310,66 +411,10 @@ impl Tab for IconSysViewer { }; std::fs::write(&self.file_path, new_sys.to_bytes().unwrap()).expect("Failed to save icon"); self.sys = new_sys; + self.modified = false; } } -fn set_border_radius(ui: &mut Ui, radius: CornerRadius) { - ui.style_mut().visuals.widgets.hovered.corner_radius = radius.add(CornerRadius::same(1)); - ui.style_mut().visuals.widgets.inactive.corner_radius = radius; - ui.style_mut().visuals.widgets.active.corner_radius = radius; -} - -fn file_select(ui: &mut Ui, name: impl Into, value: &mut String, files: &[String]) { - let id = Id::from(name.into()); - let layout_response = ui.horizontal(|ui| { - ui.style_mut().spacing.item_spacing.x = 1.0; - - set_border_radius( - ui, - CornerRadius { - nw: 2, - sw: 2, - ne: 0, - se: 0, - }, - ); - ui.text_edit_singleline(value); - - set_border_radius( - ui, - CornerRadius { - nw: 0, - sw: 0, - ne: 2, - se: 2, - }, - ); - let response = ui.button("🔽"); - if response.clicked() { - ui.memory_mut(|mem| { - mem.toggle_popup(id); - }); - } - - response - }); - - // Small hack to ensure the popup is positioned correctly - let res = Response { - rect: layout_response.response.rect, - ..layout_response.inner - }; - - egui::popup_below_widget(ui, id, &res, PopupCloseBehavior::CloseOnClick, |ui| { - ui.set_min_width(200.0); - files.iter().for_each(|file| { - if ui.selectable_label(false, file.clone()).clicked() { - *value = file.clone(); - } - }); - }); -} - fn draw_background(ui: &mut Ui, colors: &[PS2RgbaInterface; 4]) { let rect = ui.available_rect_before_wrap(); let painter = ui.painter_at(rect); @@ -408,3 +453,11 @@ fn draw_background(ui: &mut Ui, colors: &[PS2RgbaInterface; 4]) { painter.add(egui::Shape::mesh(mesh)); } + +fn length_warning(ui: &mut Ui, length: usize, maximum_length: usize, message: &str) { + if length > maximum_length { + ui.end_row(); + ui.label(""); + ui.colored_label(Color32::RED, format!("{message} {length}/{maximum_length}")); + } +} diff --git a/crates/suitcase/src/tabs/tab.rs b/crates/suitcase/src/tabs/tab.rs index 48486bb..faf8dac 100644 --- a/crates/suitcase/src/tabs/tab.rs +++ b/crates/suitcase/src/tabs/tab.rs @@ -1,4 +1,7 @@ pub trait Tab { + const TOOLBAR_HEIGHT: f32 = 25.0; + const TOOLBAR_LEFT_MARGIN: f32 = 10.0; + fn get_id(&self) -> &str; fn get_title(&self) -> String; fn get_modified(&self) -> bool; diff --git a/crates/suitcase/src/tabs/title_cfg_viewer.rs b/crates/suitcase/src/tabs/title_cfg_viewer.rs index a577f82..353e76f 100644 --- a/crates/suitcase/src/tabs/title_cfg_viewer.rs +++ b/crates/suitcase/src/tabs/title_cfg_viewer.rs @@ -1,10 +1,10 @@ +use crate::components::value_select::value_select; use crate::data::state::AppState; use crate::tabs::Tab; use crate::VirtualFile; -use eframe::egui::{menu, CornerRadius, Id, PopupCloseBehavior, Response, TextEdit, Ui}; +use eframe::egui::{menu, Id, TextEdit, Ui}; use ps2_filetypes::TitleCfg; use relative_path::PathExt; -use std::ops::Add; use std::path::PathBuf; use toml::Value; @@ -41,16 +41,17 @@ impl TitleCfgViewer { } pub fn show(&mut self, ui: &mut Ui) { - ui.vertical(|ui| { - menu::bar(ui, |ui| { - ui.set_height(25.0); - ui.button("Save").clicked().then(|| self.save()); - ui.button("Toggle Raw Editor").clicked().then(|| { - self.toggle_editors(); - }); + menu::bar(ui, |ui| { + ui.set_height(Self::TOOLBAR_HEIGHT); + ui.add_space(Self::TOOLBAR_LEFT_MARGIN); + ui.button("Save").clicked().then(|| self.save()); + ui.button("Toggle Raw Editor").clicked().then(|| { + self.toggle_editors(); }); - ui.separator(); + }); + ui.separator(); + eframe::egui::ScrollArea::vertical().show(ui, |ui| { if self.is_raw_editor { eframe::egui::Grid::new(Id::from("TitleCfgEditor")) .num_columns(1) @@ -124,7 +125,7 @@ impl TitleCfgViewer { ui, key, value, - key_helper.unwrap().get("values").unwrap(), + &parse_values(key_helper.unwrap().get("values").unwrap()).unwrap(), ) .changed() .then(|| self.modified = true); @@ -138,6 +139,7 @@ impl TitleCfgViewer { } }); } + ui.separator(); // cheap trick to force to take the entire width }); } @@ -174,73 +176,6 @@ impl Tab for TitleCfgViewer { } } -fn set_border_radius(ui: &mut Ui, radius: CornerRadius) { - ui.style_mut().visuals.widgets.hovered.corner_radius = radius.add(CornerRadius::same(1)); - ui.style_mut().visuals.widgets.inactive.corner_radius = radius; - ui.style_mut().visuals.widgets.active.corner_radius = radius; -} - -fn value_select( - ui: &mut Ui, - name: impl Into, - selected_value: &mut String, - values: &Value, -) -> Response { - let id = Id::from(name.into()); - let mut layout_response = ui.horizontal(|ui| { - ui.style_mut().spacing.item_spacing.x = 1.0; - - set_border_radius( - ui, - CornerRadius { - nw: 2, - sw: 2, - ne: 0, - se: 0, - }, - ); - let edit_response = ui.text_edit_singleline(selected_value); - - set_border_radius( - ui, - CornerRadius { - nw: 0, - sw: 0, - ne: 2, - se: 2, - }, - ); - let button_response = ui.button("🔽"); - button_response.clicked().then(|| { - ui.memory_mut(|mem| { - mem.toggle_popup(id); - }); - }); - - (edit_response, button_response) - }); - - // Small hack to ensure the popup is positioned correctly - let res = Response { - rect: layout_response.response.rect, - ..layout_response.inner.1 - }; - - let values = parse_values(values).unwrap_or_default(); - - eframe::egui::popup_below_widget(ui, id, &res, PopupCloseBehavior::CloseOnClick, |ui| { - ui.set_min_width(200.0); - for value in values { - ui.selectable_label(false, &value).clicked().then(|| { - *selected_value = value; - layout_response.inner.0.mark_changed(); - }); - } - }); - - layout_response.inner.0 -} - fn parse_values(value: &Value) -> Option> { Some( value