From fca9524184da5b36de8c0b14201d725665479f0b Mon Sep 17 00:00:00 2001 From: Maxime Coirault Date: Tue, 29 Jul 2025 10:53:47 +0300 Subject: [PATCH 1/9] make editors content scrollable and make uniform the size of toolbars --- crates/suitcase/src/tabs/icn_viewer.rs | 74 +++++++++++++++----- crates/suitcase/src/tabs/icon_sys_viewer.rs | 16 +++-- crates/suitcase/src/tabs/tab.rs | 3 + crates/suitcase/src/tabs/title_cfg_viewer.rs | 18 ++--- 4 files changed, 78 insertions(+), 33 deletions(-) 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..a787b22 100644 --- a/crates/suitcase/src/tabs/icon_sys_viewer.rs +++ b/crates/suitcase/src/tabs/icon_sys_viewer.rs @@ -2,7 +2,7 @@ use crate::tabs::Tab; use crate::{AppState, VirtualFile}; use eframe::egui; use eframe::egui::{ - vec2, Color32, CornerRadius, Grid, Id, PopupCloseBehavior, Response, Rgba, TextEdit, Ui, + menu, vec2, Color32, CornerRadius, Grid, Id, PopupCloseBehavior, Response, Rgba, TextEdit, Ui, }; use ps2_filetypes::color::Color; use ps2_filetypes::{ColorF, IconSys, Vector}; @@ -150,10 +150,15 @@ impl IconSysViewer { }) .collect(); - ui.vertical(|ui| { - // eframe::egui::Grid::new(Id::from("IconSysEditor")) - // .num_columns(2) - // .show(ui, |ui| { + 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.separator(); + + egui::ScrollArea::vertical().show(ui, |ui| { ui.heading("Icon Configuration"); ui.add_space(4.0); ui.horizontal(|ui| { @@ -255,7 +260,6 @@ impl IconSysViewer { ui.end_row(); } - // }); ui.button("Save") .on_hover_text("Save changes") .clicked() 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..594fd96 100644 --- a/crates/suitcase/src/tabs/title_cfg_viewer.rs +++ b/crates/suitcase/src/tabs/title_cfg_viewer.rs @@ -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) @@ -138,6 +139,7 @@ impl TitleCfgViewer { } }); } + ui.separator(); // cheap trick to force to take the entire width }); } From b79b171756b2ccd2b3556c4eedde61a5c53b9f1a Mon Sep 17 00:00:00 2001 From: Maxime Coirault Date: Tue, 29 Jul 2025 12:04:41 +0300 Subject: [PATCH 2/9] handle space and period properly during sjis encoding/decoding --- crates/ps2-filetypes/src/common/sjis.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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')', From 06c432f5f594fd851bd705efa14d0a2bbc9125f1 Mon Sep 17 00:00:00 2001 From: Maxime Coirault Date: Tue, 29 Jul 2025 13:37:21 +0300 Subject: [PATCH 3/9] add linebreak logic to icon.sys title --- crates/suitcase/src/tabs/icon_sys_viewer.rs | 30 ++++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/crates/suitcase/src/tabs/icon_sys_viewer.rs b/crates/suitcase/src/tabs/icon_sys_viewer.rs index a787b22..900b4a7 100644 --- a/crates/suitcase/src/tabs/icon_sys_viewer.rs +++ b/crates/suitcase/src/tabs/icon_sys_viewer.rs @@ -2,7 +2,8 @@ use crate::tabs::Tab; use crate::{AppState, VirtualFile}; use eframe::egui; use eframe::egui::{ - menu, vec2, Color32, CornerRadius, Grid, Id, PopupCloseBehavior, Response, Rgba, TextEdit, Ui, + menu, vec2, Color32, CornerRadius, Frame, Grid, Id, PopupCloseBehavior, Response, Rgba, + RichText, TextEdit, Ui, }; use ps2_filetypes::color::Color; use ps2_filetypes::{ColorF, IconSys, Vector}; @@ -83,6 +84,7 @@ impl Light { pub struct IconSysViewer { title: String, + linebreak_pos: u16, file: String, pub icon_file: String, pub icon_copy_file: String, @@ -103,6 +105,7 @@ impl IconSysViewer { Self { title: sys.title.clone(), + linebreak_pos: sys.linebreak_pos, icon_file: sys.icon_file.clone(), icon_copy_file: sys.icon_copy_file.clone(), icon_delete_file: sys.icon_delete_file.clone(), @@ -162,8 +165,26 @@ impl IconSysViewer { ui.heading("Icon Configuration"); ui.add_space(4.0); ui.horizontal(|ui| { - ui.label("Title"); - ui.add(TextEdit::singleline(&mut self.title)); + Grid::new("title").num_columns(2).show(ui, |ui| { + ui.label("Title"); + ui.add(TextEdit::singleline(&mut self.title)); + ui.end_row(); + ui.label("Linebreak Position"); + ui.add(egui::Slider::new( + &mut self.linebreak_pos, + 0..=self.title.len().try_into().unwrap(), + )); + }); + + ui.add_space(20.0); + let mut output_title = self.title.clone(); + output_title.insert(self.linebreak_pos as usize, '\n'); + Frame::new() + .inner_margin(10.0) + .fill(Color32::BLACK) + .show(ui, |ui| { + ui.label(RichText::new(output_title).color(Color32::WHITE)); + }); }); ui.heading("Icons"); @@ -289,10 +310,11 @@ impl Tab for IconSysViewer { fn save(&mut self) { let new_sys = IconSys { title: self.title.clone(), + linebreak_pos: self.linebreak_pos, 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(), From 5a7efb12d09030c356e58b2cb9dc076a0b9f6b2f Mon Sep 17 00:00:00 2001 From: Maxime Coirault Date: Tue, 29 Jul 2025 14:40:42 +0300 Subject: [PATCH 4/9] rework the icon.sys layout --- crates/suitcase/src/tabs/icon_sys_viewer.rs | 174 +++++++++++--------- 1 file changed, 95 insertions(+), 79 deletions(-) diff --git a/crates/suitcase/src/tabs/icon_sys_viewer.rs b/crates/suitcase/src/tabs/icon_sys_viewer.rs index 900b4a7..dd12266 100644 --- a/crates/suitcase/src/tabs/icon_sys_viewer.rs +++ b/crates/suitcase/src/tabs/icon_sys_viewer.rs @@ -133,6 +133,7 @@ impl IconSysViewer { } pub fn show(&mut self, ui: &mut Ui, app: &mut AppState) { + const SPACE_AROUND_HEADING: f32 = 10.0; let files: Vec = app .files .iter() @@ -163,7 +164,7 @@ impl IconSysViewer { egui::ScrollArea::vertical().show(ui, |ui| { ui.heading("Icon Configuration"); - ui.add_space(4.0); + ui.add_space(SPACE_AROUND_HEADING); ui.horizontal(|ui| { Grid::new("title").num_columns(2).show(ui, |ui| { ui.label("Title"); @@ -187,8 +188,10 @@ impl IconSysViewer { }); }); + ui.add_space(SPACE_AROUND_HEADING); + ui.separator(); ui.heading("Icons"); - ui.add_space(4.0); + ui.add_space(SPACE_AROUND_HEADING); Grid::new("icons").num_columns(2).show(ui, |ui| { ui.label("List"); @@ -201,91 +204,104 @@ impl IconSysViewer { file_select(ui, "delete_icon", &mut self.icon_delete_file, &files); }); + ui.add_space(SPACE_AROUND_HEADING); + ui.separator(); ui.heading("Background"); - ui.add_space(4.0); + ui.add_space(SPACE_AROUND_HEADING); - const SPACING: f32 = 40.0; - - 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.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, - ); - - 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, - ); - cols[1].add_space(SPACING); - egui::widgets::color_picker::color_edit_button_rgb( - &mut cols[2], - &mut self.background_colors[3].rgb, - ); - }); - 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 \ + ui.horizontal(|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, + ); + cols[1].add_space(GRADIENT_BOX_SPACING); + egui::widgets::color_picker::color_edit_button_rgb( + &mut cols[2], + &mut self.background_colors[1].rgb, + ); + + 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, + ); + cols[1].add_space(GRADIENT_BOX_SPACING); + egui::widgets::color_picker::color_edit_button_rgb( + &mut cols[2], + &mut self.background_colors[3].rgb, + ); + }); + 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, + )); + 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(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(40.0) + .striped(true) + .show(ui, |ui| { + for (index, light) in self.lights.iter_mut().enumerate() { + Grid::new(format!("light{index}")) + .num_columns(2) + .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, + ); + 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)); + }); + } }); }); } From 90fea483d26a10705fa58b91880936e989862dcf Mon Sep 17 00:00:00 2001 From: Maxime Coirault Date: Tue, 29 Jul 2025 15:59:31 +0300 Subject: [PATCH 5/9] add warning when value exceeds maximum length --- crates/ps2-filetypes/src/parser/icon_sys.rs | 38 +++++++++++++++------ crates/suitcase/src/tabs/icon_sys_viewer.rs | 38 +++++++++++++++++++-- 2 files changed, 64 insertions(+), 12 deletions(-) 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/tabs/icon_sys_viewer.rs b/crates/suitcase/src/tabs/icon_sys_viewer.rs index dd12266..c7842f2 100644 --- a/crates/suitcase/src/tabs/icon_sys_viewer.rs +++ b/crates/suitcase/src/tabs/icon_sys_viewer.rs @@ -165,7 +165,7 @@ impl IconSysViewer { egui::ScrollArea::vertical().show(ui, |ui| { ui.heading("Icon Configuration"); ui.add_space(SPACE_AROUND_HEADING); - ui.horizontal(|ui| { + Grid::new("title").num_columns(3).show(ui, |ui| { Grid::new("title").num_columns(2).show(ui, |ui| { ui.label("Title"); ui.add(TextEdit::singleline(&mut self.title)); @@ -177,7 +177,6 @@ impl IconSysViewer { )); }); - ui.add_space(20.0); let mut output_title = self.title.clone(); output_title.insert(self.linebreak_pos as usize, '\n'); Frame::new() @@ -186,6 +185,13 @@ impl IconSysViewer { .show(ui, |ui| { ui.label(RichText::new(output_title).color(Color32::WHITE)); }); + + length_warning( + ui, + self.title.len(), + IconSys::MAXIMUM_TITLE_BYTE_LENGTH / 2, + "Title too long!", + ); }); ui.add_space(SPACE_AROUND_HEADING); @@ -196,12 +202,32 @@ impl IconSysViewer { Grid::new("icons").num_columns(2).show(ui, |ui| { ui.label("List"); file_select(ui, "list_icon", &mut self.icon_file, &files); + length_warning( + ui, + self.icon_file.len(), + IconSys::MAXIMUM_FILENAME_BYTE_LENGTH / 2, + "Filename too long!", + ); ui.end_row(); + ui.label("Copy"); file_select(ui, "copy_icon", &mut self.icon_copy_file, &files); + length_warning( + ui, + self.icon_copy_file.len(), + IconSys::MAXIMUM_FILENAME_BYTE_LENGTH / 2, + "Filename too long!", + ); ui.end_row(); + ui.label("Delete"); file_select(ui, "delete_icon", &mut self.icon_delete_file, &files); + length_warning( + ui, + self.icon_delete_file.len(), + IconSys::MAXIMUM_FILENAME_BYTE_LENGTH / 2, + "Filename too long!", + ); }); ui.add_space(SPACE_AROUND_HEADING); @@ -450,3 +476,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}")); + } +} From b95b32249c46894feba97867808f139259ea9fdd Mon Sep 17 00:00:00 2001 From: Maxime Coirault Date: Tue, 29 Jul 2025 18:22:36 +0300 Subject: [PATCH 6/9] use 2 title fields to handle icon.sys linebreak position --- crates/suitcase/src/tabs/icon_sys_viewer.rs | 48 ++++++++------------- 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/crates/suitcase/src/tabs/icon_sys_viewer.rs b/crates/suitcase/src/tabs/icon_sys_viewer.rs index c7842f2..0cc9087 100644 --- a/crates/suitcase/src/tabs/icon_sys_viewer.rs +++ b/crates/suitcase/src/tabs/icon_sys_viewer.rs @@ -2,8 +2,7 @@ use crate::tabs::Tab; use crate::{AppState, VirtualFile}; use eframe::egui; use eframe::egui::{ - menu, vec2, Color32, CornerRadius, Frame, Grid, Id, PopupCloseBehavior, Response, Rgba, - RichText, TextEdit, Ui, + menu, vec2, Color32, CornerRadius, Grid, Id, PopupCloseBehavior, Response, Rgba, TextEdit, Ui, }; use ps2_filetypes::color::Color; use ps2_filetypes::{ColorF, IconSys, Vector}; @@ -83,8 +82,8 @@ impl Light { } pub struct IconSysViewer { - title: String, - linebreak_pos: u16, + title_first_line: String, + title_second_line: String, file: String, pub icon_file: String, pub icon_copy_file: String, @@ -103,9 +102,12 @@ impl IconSysViewer { let sys = IconSys::new(buf); + let (title_first_line, title_second_line) = + sys.title.split_at(sys.linebreak_pos as usize).to_owned(); + Self { - title: sys.title.clone(), - linebreak_pos: sys.linebreak_pos, + 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(), @@ -165,30 +167,16 @@ impl IconSysViewer { egui::ScrollArea::vertical().show(ui, |ui| { ui.heading("Icon Configuration"); ui.add_space(SPACE_AROUND_HEADING); - Grid::new("title").num_columns(3).show(ui, |ui| { - Grid::new("title").num_columns(2).show(ui, |ui| { - ui.label("Title"); - ui.add(TextEdit::singleline(&mut self.title)); - ui.end_row(); - ui.label("Linebreak Position"); - ui.add(egui::Slider::new( - &mut self.linebreak_pos, - 0..=self.title.len().try_into().unwrap(), - )); - }); - - let mut output_title = self.title.clone(); - output_title.insert(self.linebreak_pos as usize, '\n'); - Frame::new() - .inner_margin(10.0) - .fill(Color32::BLACK) - .show(ui, |ui| { - ui.label(RichText::new(output_title).color(Color32::WHITE)); - }); + Grid::new("title").num_columns(2).show(ui, |ui| { + ui.label("Title first line"); + ui.add(TextEdit::singleline(&mut self.title_first_line)); + ui.end_row(); + ui.label("Title second line"); + ui.add(TextEdit::singleline(&mut self.title_second_line)); length_warning( ui, - self.title.len(), + self.title_first_line.len() + self.title_second_line.len(), IconSys::MAXIMUM_TITLE_BYTE_LENGTH / 2, "Title too long!", ); @@ -343,7 +331,7 @@ impl Tab for IconSysViewer { } fn get_modified(&self) -> bool { - self.sys.title != self.title + self.sys.title != format!("{}{}", self.title_first_line, self.title_second_line) || 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 @@ -351,8 +339,8 @@ impl Tab for IconSysViewer { fn save(&mut self) { let new_sys = IconSys { - title: self.title.clone(), - linebreak_pos: self.linebreak_pos, + 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(), From b815dee584ed71d0a865541f09ccf57d593064ba Mon Sep 17 00:00:00 2001 From: Maxime Coirault Date: Tue, 29 Jul 2025 18:44:12 +0300 Subject: [PATCH 7/9] align ui elements in icon.sys tab --- crates/suitcase/src/tabs/icon_sys_viewer.rs | 207 +++++++++++--------- 1 file changed, 109 insertions(+), 98 deletions(-) diff --git a/crates/suitcase/src/tabs/icon_sys_viewer.rs b/crates/suitcase/src/tabs/icon_sys_viewer.rs index 0cc9087..7c7c320 100644 --- a/crates/suitcase/src/tabs/icon_sys_viewer.rs +++ b/crates/suitcase/src/tabs/icon_sys_viewer.rs @@ -136,6 +136,7 @@ impl IconSysViewer { 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() @@ -167,122 +168,131 @@ impl IconSysViewer { egui::ScrollArea::vertical().show(ui, |ui| { ui.heading("Icon Configuration"); ui.add_space(SPACE_AROUND_HEADING); - Grid::new("title").num_columns(2).show(ui, |ui| { - ui.label("Title first line"); - ui.add(TextEdit::singleline(&mut self.title_first_line)); - ui.end_row(); - ui.label("Title second line"); - ui.add(TextEdit::singleline(&mut self.title_second_line)); - - length_warning( - ui, - self.title_first_line.len() + self.title_second_line.len(), - IconSys::MAXIMUM_TITLE_BYTE_LENGTH / 2, - "Title too long!", - ); - }); + Grid::new("title") + .num_columns(2) + .min_col_width(LABEL_COLUMN_WIDTH) + .show(ui, |ui| { + ui.label("Title first line"); + ui.add(TextEdit::singleline(&mut self.title_first_line)); + ui.end_row(); + ui.label("Title second line"); + ui.add(TextEdit::singleline(&mut self.title_second_line)); + + length_warning( + ui, + self.title_first_line.len() + self.title_second_line.len(), + IconSys::MAXIMUM_TITLE_BYTE_LENGTH / 2, + "Title too long!", + ); + }); ui.add_space(SPACE_AROUND_HEADING); ui.separator(); ui.heading("Icons"); ui.add_space(SPACE_AROUND_HEADING); - Grid::new("icons").num_columns(2).show(ui, |ui| { - ui.label("List"); - file_select(ui, "list_icon", &mut self.icon_file, &files); - length_warning( - ui, - self.icon_file.len(), - IconSys::MAXIMUM_FILENAME_BYTE_LENGTH / 2, - "Filename too long!", - ); - ui.end_row(); - - ui.label("Copy"); - file_select(ui, "copy_icon", &mut self.icon_copy_file, &files); - length_warning( - ui, - self.icon_copy_file.len(), - IconSys::MAXIMUM_FILENAME_BYTE_LENGTH / 2, - "Filename too long!", - ); - ui.end_row(); - - ui.label("Delete"); - file_select(ui, "delete_icon", &mut self.icon_delete_file, &files); - length_warning( - ui, - self.icon_delete_file.len(), - IconSys::MAXIMUM_FILENAME_BYTE_LENGTH / 2, - "Filename too long!", - ); - }); + Grid::new("icons") + .num_columns(2) + .min_col_width(LABEL_COLUMN_WIDTH) + .show(ui, |ui| { + ui.label("List"); + file_select(ui, "list_icon", &mut self.icon_file, &files); + length_warning( + ui, + self.icon_file.len(), + IconSys::MAXIMUM_FILENAME_BYTE_LENGTH / 2, + "Filename too long!", + ); + ui.end_row(); + + ui.label("Copy"); + file_select(ui, "copy_icon", &mut self.icon_copy_file, &files); + length_warning( + ui, + self.icon_copy_file.len(), + IconSys::MAXIMUM_FILENAME_BYTE_LENGTH / 2, + "Filename too long!", + ); + ui.end_row(); + + ui.label("Delete"); + file_select(ui, "delete_icon", &mut self.icon_delete_file, &files); + length_warning( + ui, + self.icon_delete_file.len(), + IconSys::MAXIMUM_FILENAME_BYTE_LENGTH / 2, + "Filename too long!", + ); + }); ui.add_space(SPACE_AROUND_HEADING); ui.separator(); ui.heading("Background"); ui.add_space(SPACE_AROUND_HEADING); - ui.horizontal(|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, - ); - cols[1].add_space(GRADIENT_BOX_SPACING); - egui::widgets::color_picker::color_edit_button_rgb( - &mut cols[2], - &mut self.background_colors[1].rgb, - ); + Grid::new("background") + .num_columns(2) + .min_col_width(LABEL_COLUMN_WIDTH) + .show(ui, |ui| { + const GRADIENT_BOX_SPACING: f32 = 40.0; - cols[0].add_space(GRADIENT_BOX_SPACING); - cols[1].add_space(GRADIENT_BOX_SPACING); - cols[2].add_space(GRADIENT_BOX_SPACING); + 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); - egui::widgets::color_picker::color_edit_button_rgb( - &mut cols[0], - &mut self.background_colors[2].rgb, - ); - cols[1].add_space(GRADIENT_BOX_SPACING); - egui::widgets::color_picker::color_edit_button_rgb( - &mut cols[2], - &mut self.background_colors[3].rgb, + 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(GRADIENT_BOX_SPACING); + egui::widgets::color_picker::color_edit_button_rgb( + &mut cols[2], + &mut self.background_colors[1].rgb, + ); + + 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, + ); + cols[1].add_space(GRADIENT_BOX_SPACING); + egui::widgets::color_picker::color_edit_button_rgb( + &mut cols[2], + &mut self.background_colors[3].rgb, + ); + }); + 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.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, + )); + 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(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(); @@ -291,12 +301,13 @@ impl IconSysViewer { Grid::new("lights") .num_columns(3) .spacing([50.0, 50.0]) - .min_col_width(40.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(); From 5e972773305dade307b9ceac89994b8e434a6c84 Mon Sep 17 00:00:00 2001 From: Maxime Coirault Date: Tue, 29 Jul 2025 21:59:49 +0300 Subject: [PATCH 8/9] factorize value_select and add 'modified' logic to all icon.sys fields --- crates/suitcase/src/components/mod.rs | 5 +- .../suitcase/src/components/value_select.rs | 67 +++++++++ crates/suitcase/src/tabs/icon_sys_viewer.rs | 133 ++++++++---------- crates/suitcase/src/tabs/title_cfg_viewer.rs | 73 +--------- 4 files changed, 129 insertions(+), 149 deletions(-) create mode 100644 crates/suitcase/src/components/value_select.rs 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/icon_sys_viewer.rs b/crates/suitcase/src/tabs/icon_sys_viewer.rs index 7c7c320..3a519ed 100644 --- a/crates/suitcase/src/tabs/icon_sys_viewer.rs +++ b/crates/suitcase/src/tabs/icon_sys_viewer.rs @@ -1,8 +1,9 @@ +use crate::components::value_select::value_select; use crate::tabs::Tab; use crate::{AppState, VirtualFile}; use eframe::egui; use eframe::egui::{ - menu, vec2, Color32, CornerRadius, Grid, Id, PopupCloseBehavior, Response, Rgba, TextEdit, Ui, + menu, vec2, Color32, Grid, Rgba, Ui, }; use ps2_filetypes::color::Color; use ps2_filetypes::{ColorF, IconSys, Vector}; @@ -94,6 +95,7 @@ pub struct IconSysViewer { pub lights: [Light; 3], pub sys: IconSys, pub file_path: PathBuf, + modified: bool, } impl IconSysViewer { @@ -131,6 +133,7 @@ impl IconSysViewer { .relative_to(state.opened_folder.clone().unwrap()) .unwrap() .to_string(), + modified: false, } } @@ -173,10 +176,14 @@ impl IconSysViewer { .min_col_width(LABEL_COLUMN_WIDTH) .show(ui, |ui| { ui.label("Title first line"); - ui.add(TextEdit::singleline(&mut self.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.add(TextEdit::singleline(&mut self.title_second_line)); + ui.text_edit_singleline(&mut self.title_second_line) + .changed() + .then(|| self.modified = true); length_warning( ui, @@ -196,7 +203,9 @@ impl IconSysViewer { .min_col_width(LABEL_COLUMN_WIDTH) .show(ui, |ui| { ui.label("List"); - file_select(ui, "list_icon", &mut self.icon_file, &files); + value_select(ui, "list_icon", &mut self.icon_file, &files) + .changed() + .then(|| self.modified = true); length_warning( ui, self.icon_file.len(), @@ -206,7 +215,9 @@ impl IconSysViewer { ui.end_row(); ui.label("Copy"); - file_select(ui, "copy_icon", &mut self.icon_copy_file, &files); + value_select(ui, "copy_icon", &mut self.icon_copy_file, &files) + .changed() + .then(|| self.modified = true); length_warning( ui, self.icon_copy_file.len(), @@ -216,7 +227,9 @@ impl IconSysViewer { ui.end_row(); ui.label("Delete"); - file_select(ui, "delete_icon", &mut self.icon_delete_file, &files); + value_select(ui, "delete_icon", &mut self.icon_delete_file, &files) + .changed() + .then(|| self.modified = true); length_warning( ui, self.icon_delete_file.len(), @@ -248,12 +261,16 @@ impl IconSysViewer { 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); @@ -262,12 +279,16 @@ impl IconSysViewer { 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() }, @@ -283,13 +304,18 @@ impl IconSysViewer { 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(); }); }); @@ -311,20 +337,32 @@ impl IconSysViewer { .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)); + 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)); + 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)); + ui.add(egui::Slider::new(&mut light.direction.z, 0.0..=1.0)) + .changed() + .then(|| self.modified = true); }); } }); @@ -342,10 +380,7 @@ impl Tab for IconSysViewer { } fn get_modified(&self) -> bool { - self.sys.title != format!("{}{}", self.title_first_line, self.title_second_line) - || 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) { @@ -377,66 +412,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); diff --git a/crates/suitcase/src/tabs/title_cfg_viewer.rs b/crates/suitcase/src/tabs/title_cfg_viewer.rs index 594fd96..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; @@ -125,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); @@ -176,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 From 613f487dcab8507d807fbd0ea3e910cfdd9208b6 Mon Sep 17 00:00:00 2001 From: Maxime Coirault Date: Wed, 30 Jul 2025 10:49:27 +0300 Subject: [PATCH 9/9] prevent panic if the linebreak position is greater thant the title length --- crates/suitcase/src/tabs/icon_sys_viewer.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/suitcase/src/tabs/icon_sys_viewer.rs b/crates/suitcase/src/tabs/icon_sys_viewer.rs index 3a519ed..212af46 100644 --- a/crates/suitcase/src/tabs/icon_sys_viewer.rs +++ b/crates/suitcase/src/tabs/icon_sys_viewer.rs @@ -2,12 +2,11 @@ use crate::components::value_select::value_select; use crate::tabs::Tab; use crate::{AppState, VirtualFile}; use eframe::egui; -use eframe::egui::{ - menu, vec2, Color32, Grid, Rgba, 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; @@ -104,8 +103,8 @@ impl IconSysViewer { let sys = IconSys::new(buf); - let (title_first_line, title_second_line) = - sys.title.split_at(sys.linebreak_pos as usize).to_owned(); + 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_first_line: title_first_line.to_string(),