Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/ps2-filetypes/src/common/sjis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ pub fn encode_sjis(input: &str) -> Vec<u8> {
.as_bytes()
.iter()
.flat_map(|b| match *b {
b' ' => [0x80, 0x3F],
b' ' => [0x81, 0x40],
b':' => [0x81, 0x46],
b'/' => [0x81, 0x5E],
b'(' => [0x81, 0x69],
Expand Down
46 changes: 40 additions & 6 deletions crates/ps2-filetypes/src/parser/icon_sys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,14 @@ impl Vector {
#[derive(Clone, Debug)]
pub struct IconSys {
pub flags: u16,
pub linebreak_pos: u16,
pub linebreak_pos: u8,
pub background_transparency: u32,
pub background_colors: [Color; 4],
pub light_directions: [Vector; 3],
pub light_colors: [ColorF; 3],
pub ambient_color: ColorF,
pub title: String,
pub title_line1: String,
pub title_line2: String,
pub icon_file: String,
pub icon_copy_file: String,
pub icon_delete_file: String,
Expand All @@ -84,7 +85,8 @@ impl IconSys {
bytes.extend_from_slice(b"PS2D");

bytes.extend(self.flags.to_le_bytes());
bytes.extend(self.linebreak_pos.to_le_bytes());
bytes.extend(convert_linepos(self.linebreak_pos).to_le_bytes());
bytes.extend(0u8.to_le_bytes()); // Reserved
bytes.extend(0u32.to_le_bytes()); // Reserved
bytes.extend(self.background_transparency.to_le_bytes());

Expand All @@ -102,7 +104,7 @@ impl IconSys {

bytes.extend_from_slice(&self.ambient_color.to_bytes());

let title_bytes = encode_sjis(&self.title);
let title_bytes = encode_sjis(&join_title_lines(self.title_line1.clone(), self.title_line2.clone()));
let title_len = title_bytes.len();
if title_len > 68 {
return Err(std::io::Error::new(
Expand Down Expand Up @@ -153,7 +155,8 @@ fn parse_icon_sys(bytes: Vec<u8>) -> Result<IconSys> {
c.read_exact(&mut magic)?;

let flags = c.read_u16::<LE>()?;
let linebreak_pos = c.read_u16::<LE>()?;
let linebreak_pos = convert_linepos(c.read_u8()?);
_ = c.read_u8(); // Reserved, always 0
_ = c.read_u32::<LE>(); // Reserved, always 0
let background_transparency = c.read_u32::<LE>()?;

Expand Down Expand Up @@ -188,6 +191,9 @@ fn parse_icon_sys(bytes: Vec<u8>) -> Result<IconSys> {
let mut icon_delete_file_buf = vec![0u8; 64];
c.read_exact(&mut icon_delete_file_buf)?;

let title = parse_sjis_string(&title_buf);
let (title_line1, title_line2) = split_title(linebreak_pos, title);

Ok(IconSys {
flags,
linebreak_pos,
Expand All @@ -196,7 +202,8 @@ fn parse_icon_sys(bytes: Vec<u8>) -> Result<IconSys> {
light_directions,
light_colors,
ambient_color,
title: parse_sjis_string(&title_buf),
title_line1,
title_line2,
icon_file: parse_cstring(&icon_file_buf),
icon_copy_file: parse_cstring(&icon_copy_file_buf),
icon_delete_file: parse_cstring(&icon_delete_file_buf),
Expand Down Expand Up @@ -235,3 +242,30 @@ fn parse_direction(c: &mut Cursor<Vec<u8>>) -> Result<Vector> {

Ok(Vector { x, y, z, w })
}

/**
* The linebreak is stored at byte 0x06 in the 4 lower bits, but with the least significant bit first.
* For example a linebreak on the 5rd (0b 0000 0101) character will be represented as 0b 0000 1010.
* The 4 higher bits are seemingly ignored in the context of OSDMENU.
*/
fn convert_linepos(source: u8) -> u8 {
// zero out the 4 most significant bits
let low_half = source & 0b00001111;
low_half.reverse_bits() >> 4
}

fn split_title(linebreak_pos: u8, title: String) -> (String, String) {
if linebreak_pos >= title.len() as u8 {
return (title, "".to_string());
}
let (title_line1, title_line2) = title.split_at(linebreak_pos as usize);
// trim the leading space on line2 necessary for proper linebreak
(title_line1.into(), title_line2.trim_start().into())
}

fn join_title_lines(title_line1: String, title_line2: String) -> String {
if title_line2.len() == 0 {
return title_line1;
}
format!("{} {}", title_line1, title_line2)
}
228 changes: 132 additions & 96 deletions crates/suitcase/src/tabs/icon_sys_viewer.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,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,
};
use eframe::egui::{menu, vec2, Color32, CornerRadius, Grid, Id, PopupCloseBehavior, Response, Rgba, RichText, TextEdit, Ui};
use ps2_filetypes::color::Color;
use ps2_filetypes::{ColorF, IconSys, Vector};
use relative_path::PathExt;
Expand Down Expand Up @@ -82,7 +80,8 @@ impl Light {
}

pub struct IconSysViewer {
title: String,
title_line1: String,
title_line2: String,
file: String,
pub icon_file: String,
pub icon_copy_file: String,
Expand All @@ -102,7 +101,8 @@ impl IconSysViewer {
let sys = IconSys::new(buf);

Self {
title: sys.title.clone(),
title_line1: sys.title_line1.clone(),
title_line2: sys.title_line2.clone(),
icon_file: sys.icon_file.clone(),
icon_copy_file: sys.icon_copy_file.clone(),
icon_delete_file: sys.icon_delete_file.clone(),
Expand Down Expand Up @@ -150,21 +150,45 @@ impl IconSysViewer {
})
.collect();

const MIN_COL_WIDTH: f32 = 160.0;
const SEPARATOR_MARGIN: f32 = 10.0;

ui.vertical(|ui| {
// eframe::egui::Grid::new(Id::from("IconSysEditor"))
// .num_columns(2)
// .show(ui, |ui| {
menu::bar(ui, |ui| {
ui.set_height(25.0);
ui.add_space(10.0);
ui.button("Save").clicked().then(|| self.save());
});
ui.separator();
ui.add_space(SEPARATOR_MARGIN);

ui.heading("Icon Configuration");
ui.add_space(4.0);
ui.horizontal(|ui| {
ui.label("Title");
ui.add(TextEdit::singleline(&mut self.title));
ui.add_space(SEPARATOR_MARGIN);

Grid::new(Id::from("IconSysEditor")).num_columns(2).min_col_width(MIN_COL_WIDTH).show(ui, |ui| {
ui.label("Title first line");
ui.add(TextEdit::singleline(&mut self.title_line1));
if (self.title_line1.len() > 15) {
ui.label(format!("OSDMENU can only handle linebreak before 15 characters. {}/15", self.title_line1.len()));
}
ui.end_row();
ui.label("Title second line");
ui.add(TextEdit::singleline(&mut self.title_line2));
if self.title_len() > 34 {
ui.end_row();
ui.end_row();
ui.label("");
ui.label(RichText::new(format!("Title too long {}/34", self.title_len())).color(Color32::RED));
}
});
ui.add_space(SEPARATOR_MARGIN);
ui.separator();
ui.add_space(SEPARATOR_MARGIN);

ui.heading("Icons");
ui.add_space(4.0);
ui.add_space(SEPARATOR_MARGIN);

Grid::new("icons").num_columns(2).show(ui, |ui| {
Grid::new("icons").num_columns(2).min_col_width(MIN_COL_WIDTH).show(ui, |ui| {
ui.label("List");
file_select(ui, "list_icon", &mut self.icon_file, &files);
ui.end_row();
Expand All @@ -174,96 +198,105 @@ impl IconSysViewer {
ui.label("Delete");
file_select(ui, "delete_icon", &mut self.icon_delete_file, &files);
});
ui.add_space(SEPARATOR_MARGIN);
ui.separator();
ui.add_space(SEPARATOR_MARGIN);

ui.heading("Background");
ui.add_space(4.0);

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.add_space(SEPARATOR_MARGIN);

Grid::new("background").num_columns(2).min_col_width(MIN_COL_WIDTH).show(ui, |ui| {
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()
});
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",
);
Grid::new("background_ambient").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(SEPARATOR_MARGIN);
ui.separator();
ui.add_space(SEPARATOR_MARGIN);

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(SEPARATOR_MARGIN);

Grid::new("lights").num_columns(3).min_col_width(250.0).show(ui, |ui| {
for (index, light) in self.lights.iter_mut().enumerate() {
let human_readable_index = index + 1;
Grid::new(format!("light {index}")).num_columns(2).min_col_width(50.0).show(ui, |ui| {
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));
});
}
});
});
}

fn title_len(&self) -> usize {
if self.title_line2.len() == 0 {
return self.title_line1.len();
}
self.title_line1.len() + self.title_line2.len() + 1 // 1 extra char for the linebreak space
}
}

impl Tab for IconSysViewer {
Expand All @@ -276,15 +309,18 @@ impl Tab for IconSysViewer {
}

fn get_modified(&self) -> bool {
self.sys.title != self.title
self.sys.title_line1 != self.title_line1
|| self.sys.title_line2 != self.title_line2
|| 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
}

fn save(&mut self) {
let new_sys = IconSys {
title: self.title.clone(),
title_line1: self.title_line1.clone(),
title_line2: self.title_line2.clone(),
linebreak_pos: self.title_line1.len() as u8,
icon_file: self.icon_file.clone(),
icon_copy_file: self.icon_copy_file.clone(),
icon_delete_file: self.icon_delete_file.clone(),
Expand Down
Loading
Loading