diff --git a/podman/scripts/install_rust.sh b/podman/scripts/install_rust.sh index 0d515eb1..067e610c 100755 --- a/podman/scripts/install_rust.sh +++ b/podman/scripts/install_rust.sh @@ -6,4 +6,4 @@ RUST_VERSION=1.84.0 echo "<<< Install Rust $RUST_VERSION >>>" curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- --default-toolchain=$RUST_VERSION -y . "$HOME/.cargo/env" -cargo install cargo-edit --locked \ No newline at end of file +cargo install cargo-edit cargo-bundle --locked \ No newline at end of file diff --git a/src/Tools/Tools.code-workspace b/src/Tools/Tools.code-workspace index 825be1e4..49672a22 100644 --- a/src/Tools/Tools.code-workspace +++ b/src/Tools/Tools.code-workspace @@ -13,6 +13,7 @@ "psxcdread all!psxcdread", "psxfileconv all!psxfileconv", "psxreadmap all!psxreadmap", + "tim_tool all!tim_tool", "wslpath all!wslpath", ] }, @@ -49,7 +50,7 @@ { "id": "cargo cmd", "type":"pickString", - "options": ["build", "check", "upgrade", "clean", "run", "tree"], + "options": ["build", "check", "run", "upgrade", "clean", "bundle"], "default": "build", "description": "cargo command to run" } diff --git a/src/Tools/tim_tool/Cargo.toml b/src/Tools/tim_tool/Cargo.toml new file mode 100644 index 00000000..a5253132 --- /dev/null +++ b/src/Tools/tim_tool/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "tim_tool" +version = "0.1.0" +edition = "2021" + +[profile.release] +panic = "abort" + +[dependencies] +png = "0.17.16" +rfd = "0.15.2" +slint = "1.9.2" +tiny-skia = "0.11.4" +tool_helper = {path = "../tool_helper"} + +[build-dependencies] +slint-build = "1.9.2" + +[package.metadata.bundle] +name = "TIMTOOL" +identifier = "zone.jabyengine.timtool" +icon = ["ui/assets/TimTool64x64.png"] +copyright = "Copyright (c) 2025 Jaby MIT License" +category = "Developer Tool" +short_description = "TIM Tool for JabyEngine" +long_description = """ +A new interpetation of the TIM Tool from Sony, for use with PSYQ and the JabyEngine +""" \ No newline at end of file diff --git a/src/Tools/tim_tool/Makefile b/src/Tools/tim_tool/Makefile new file mode 100644 index 00000000..f1694818 --- /dev/null +++ b/src/Tools/tim_tool/Makefile @@ -0,0 +1,13 @@ +include ../Common.mk + +ARTIFACT = tim_tool + +.PHONY: $(WINDOWS_ARTIFACT) $(UNIX_ARTIFACT) +$(WINDOWS_ARTIFACT): + $(call cargo_windows_default) + +$(UNIX_ARTIFACT): + $(call cargo_unix_default) + +all-windows: $(WINDOWS_ARTIFACT) +all: $(UNIX_ARTIFACT) \ No newline at end of file diff --git a/src/Tools/tim_tool/build.rs b/src/Tools/tim_tool/build.rs new file mode 100644 index 00000000..9bc30378 --- /dev/null +++ b/src/Tools/tim_tool/build.rs @@ -0,0 +1,3 @@ +fn main() { + slint_build::compile("ui/app-window.slint").expect("Slint build failed"); +} diff --git a/src/Tools/tim_tool/src/gui/file_tab.rs b/src/Tools/tim_tool/src/gui/file_tab.rs new file mode 100644 index 00000000..02b1c9c3 --- /dev/null +++ b/src/Tools/tim_tool/src/gui/file_tab.rs @@ -0,0 +1,63 @@ +use crate::MainWindow; +use super::{GUIElements, GUIElementsRef, MainWindowRef, display_error}; +use slint::Image; +use tool_helper::Error; + +pub struct FileTab { + main_window: MainWindowRef +} + +impl FileTab { + pub fn new(main_window: MainWindowRef) -> FileTab { + FileTab{main_window} + } + + pub fn clear_load(&self) { + let main_window = self.main_window.borrow(); + + main_window.set_file_tab_browse_path("".into()); + main_window.set_file_tab_image_data(Image::default()); + main_window.set_file_tab_image_name("".into()); + main_window.set_file_tab_enable(false); + } + + pub fn update_new_load(&self, file_name: Option, image: Image) { + let main_window = self.main_window.borrow(); + + main_window.set_file_tab_image_data(image); + if let Some(file_name) = file_name { + main_window.set_file_tab_image_name(file_name.into()); + } + + else { + main_window.set_file_tab_image_name("".into()); + } + main_window.set_file_tab_enable(true); + } + + pub fn get_file_name(&self) -> String { + self.main_window.borrow().get_file_tab_image_name().to_string() + } + + pub fn on_browse_file(&self, gui_elements: GUIElementsRef, mut function: impl FnMut(&mut GUIElements, &MainWindow) -> Result<(), Error> + 'static) { + let main_window_cloned = self.main_window.clone(); + let gui_cloned = gui_elements.clone(); + + self.main_window.borrow().on_file_tab_browse_convert_image(move || { + if let Err(error) = function(&mut gui_cloned.borrow_mut(), &main_window_cloned.borrow()) { + display_error("Loadind file failed", &error.to_string()); + } + }); + } + + pub fn on_add_image(&self, gui_elements: GUIElementsRef, mut function: impl FnMut(&mut GUIElements, &MainWindow) -> Result<(), Error> + 'static) { + let main_window_cloned = self.main_window.clone(); + let gui_cloned = gui_elements.clone(); + + self.main_window.borrow().on_file_tab_add_convert_image(move || { + if let Err(error) = function(&mut gui_cloned.borrow_mut(), &main_window_cloned.borrow()) { + display_error("Adding file failed", &error.to_string()); + } + }); + } +} \ No newline at end of file diff --git a/src/Tools/tim_tool/src/gui/gui_elements.rs b/src/Tools/tim_tool/src/gui/gui_elements.rs new file mode 100644 index 00000000..da0036e2 --- /dev/null +++ b/src/Tools/tim_tool/src/gui/gui_elements.rs @@ -0,0 +1,20 @@ +use super::create_vram_bg; + +use super::MainWindowRef; +use super::{file_tab::FileTab, main_tab::MainTab}; + +pub struct GUIElements { + pub file_tab: FileTab, + pub main_tab: MainTab, +} + +impl GUIElements { + pub fn new(main_window: MainWindowRef) -> Result { + main_window.borrow().set_main_tab_vram_bg(GUIElements::create_bg()?); + Ok(GUIElements{file_tab: FileTab::new(main_window.clone()), main_tab: MainTab::new(main_window.clone())}) + } + + fn create_bg() -> Result { + Ok(slint::Image::from_rgba8_premultiplied(create_vram_bg(320, 256).ok_or(slint::PlatformError::Other("Failed creating VRAM background image".to_string()))?)) + } +} \ No newline at end of file diff --git a/src/Tools/tim_tool/src/gui/main_tab.rs b/src/Tools/tim_tool/src/gui/main_tab.rs new file mode 100644 index 00000000..3647dfe3 --- /dev/null +++ b/src/Tools/tim_tool/src/gui/main_tab.rs @@ -0,0 +1,99 @@ +use crate::{gui::{VRAM_HEIGHT, VRAM_WIDTH}, MainWindow, VRAMImage}; +use super::{GUIElementsRef, MainWindowRef}; + +use slint::Model; +use std::{rc::Rc, sync::{Arc, Mutex}}; + +pub struct MainTab { + main_window: MainWindowRef, + mtx: Arc>, + vram_file_list: Rc>, + vram_image_list: Rc>, +} + +impl MainTab { + pub fn new(main_window: MainWindowRef) -> MainTab { + let vram_file_list:Vec = main_window.borrow().get_main_tab_vram_file_list().iter().collect(); + let vram_file_list = Rc::new(slint::VecModel::from(vram_file_list)); + let vram_image_list = Rc::new(slint::VecModel::from(Vec::::new())); + + main_window.borrow().set_main_tab_vram_file_list(vram_file_list.clone().into()); + main_window.borrow().set_main_tab_vram_images(vram_image_list.clone().into()); + + MainTab{main_window, mtx: Arc::new(Mutex::new(0)), vram_file_list, vram_image_list} + } + + pub fn add_new_vram_file(&mut self, file_name: &String, image: slint::Image, image_palette: Option) { + let add_new_image = |file_name: &String, image: slint::Image| { + let vram_image = VRAMImage{ + img: image, + x: 0, + y: 0, + palette_count: 0, + is_palette: false, + }; + + self.vram_file_list.push(slint::StandardListViewItem::from(file_name.as_str())); + self.vram_image_list.push(vram_image); + }; + + let _lock = self.mtx.lock().unwrap(); + add_new_image(file_name, image); + } + + pub fn remove_vram_file(&mut self, idx: usize) -> bool { + let _lock = self.mtx.lock().unwrap(); + + if let Some(element) = self.vram_image_list.iter().skip(idx).next() { + if element.is_palette { + return false; + } + } + + self.vram_file_list.remove(idx); + self.vram_image_list.remove(idx); + return true; + } + + pub fn move_vram_image(&mut self, idx: usize, dx: i32, dy: i32) { + if let Some(mut vram_info) = self.vram_image_list.row_data(idx) { + vram_info.x += dx; + vram_info.y += dy; + + if vram_info.x < 0 { + vram_info.x = 0; + } + + if vram_info.y < 0 { + vram_info.y = 0; + } + + let (vram_img_width, vram_img_height) = (vram_info.img.size().width as i32, vram_info.img.size().height as i32); + if (vram_info.x + vram_img_width) > VRAM_WIDTH as i32 { + vram_info.x = VRAM_WIDTH as i32 - vram_img_width; + } + + if (vram_info.y + vram_img_height) > VRAM_HEIGHT as i32 { + vram_info.y = VRAM_HEIGHT as i32 - vram_img_height; + } + + self.vram_image_list.set_row_data(idx, vram_info); + } + } + + pub fn on_move_vram_image(&self, gui_elements: GUIElementsRef, mut function: impl FnMut(&mut MainTab, &MainWindow, i32, i32, i32) + 'static) { + let main_window_cloned = self.main_window.clone(); + let gui_cloned = gui_elements.clone(); + self.main_window.borrow().on_move_vram_image(move |idx, dx, dy| { + function(&mut gui_cloned.borrow_mut().main_tab, &main_window_cloned.borrow(), idx, dx, dy); + }); + } + + pub fn on_remove_file(&self, gui_elements: GUIElementsRef, mut function: impl FnMut(&mut MainTab, &MainWindow, i32) + 'static) { + let main_window_cloned = self.main_window.clone(); + let gui_cloned = gui_elements.clone(); + self.main_window.borrow().on_main_tab_remove_file_clicked(move |idx| { + function(&mut gui_cloned.borrow_mut().main_tab, &main_window_cloned.borrow(), idx); + }); + } +} \ No newline at end of file diff --git a/src/Tools/tim_tool/src/gui/mod.rs b/src/Tools/tim_tool/src/gui/mod.rs new file mode 100644 index 00000000..438d75fd --- /dev/null +++ b/src/Tools/tim_tool/src/gui/mod.rs @@ -0,0 +1,51 @@ +mod file_tab; +mod gui_elements; +mod main_tab; + +use crate::MainWindow; +use rfd::MessageDialog; +use std::{cell::RefCell, rc::Rc}; +use slint::{Rgba8Pixel, SharedPixelBuffer}; +use tiny_skia::{Rect, Transform}; + +pub use gui_elements::GUIElements; + +pub const VRAM_WIDTH:usize = 1024; +pub const VRAM_HEIGHT:usize = 512; + +type MainWindowRef = Rc>; +type GUIElementsRef = Rc>; + +pub fn display_information(title: impl Into, text: impl Into) { + MessageDialog::new().set_title(title).set_level(rfd::MessageLevel::Info).set_description(text).show(); +} + +pub fn display_error(title: impl Into, text: impl Into) { + MessageDialog::new().set_title(title).set_level(rfd::MessageLevel::Error).set_description(text).show(); +} + +fn create_vram_bg(rect_width: u32, rect_height: u32) -> Option> { + let mut pixel_buffer = SharedPixelBuffer::::new(VRAM_WIDTH as u32, VRAM_HEIGHT as u32); + let width = pixel_buffer.width(); + let height = pixel_buffer.height(); + + if let Some(mut pixmap) = tiny_skia::PixmapMut::from_bytes(pixel_buffer.make_mut_bytes(), width, height) { + let vram_color = tiny_skia::Color::from_rgba8(0x0, 0x80, 0x80, 0xFF); + let display_buffer_color = tiny_skia::Color::from_rgba8(0x0, 0x80, 0x0, 0xFF); + let draw_area_color = tiny_skia::Color::from_rgba8(0x80, 0x80, 0x0, 0xFF); + + let mut paint = tiny_skia::Paint::default(); + + pixmap.fill(vram_color); + + paint.set_color(display_buffer_color); + pixmap.fill_rect(Rect::from_xywh(0.0, 0.0, rect_width as f32, rect_height as f32).unwrap(), &paint, Transform::identity(), None); + paint.set_color(draw_area_color); + pixmap.fill_rect(Rect::from_xywh(0.0, rect_height as f32, rect_width as f32, rect_height as f32).unwrap(), &paint, Transform::identity(), None); + Some(pixel_buffer) + } + + else { + None + } +} \ No newline at end of file diff --git a/src/Tools/tim_tool/src/lib.rs b/src/Tools/tim_tool/src/lib.rs new file mode 100644 index 00000000..45a2c471 --- /dev/null +++ b/src/Tools/tim_tool/src/lib.rs @@ -0,0 +1 @@ +pub mod logic; \ No newline at end of file diff --git a/src/Tools/tim_tool/src/logic/mod.rs b/src/Tools/tim_tool/src/logic/mod.rs new file mode 100644 index 00000000..c938513f --- /dev/null +++ b/src/Tools/tim_tool/src/logic/mod.rs @@ -0,0 +1,37 @@ +pub mod tim; + +use std::path::PathBuf; +use slint::Image; +use tim::TIMInfo; +use tool_helper::Error; + +pub struct Logic { + unadded_tim: Option, +} + +impl Logic { + pub fn new() -> Logic { + Logic{unadded_tim: None} + } + + pub fn set_unadded_tim(&mut self, path: &PathBuf) -> Result<(Image, Option), Error> { + let tim_info = TIMInfo::from_image(path)?; + let image = tim_info.get_slint_images(); + + self.unadded_tim = Some(tim_info); + Ok(image) + } + + pub fn add_unadded_tim_as(&mut self, _name: &String) -> Result<(Image, Option), Error> { + if let Some(unadded_tim) = &self.unadded_tim { + let image = unadded_tim.get_slint_images(); + + self.unadded_tim = None; + Ok(image) + } + + else { + Err(Error::from_str("No data found for loaded image")) + } + } +} \ No newline at end of file diff --git a/src/Tools/tim_tool/src/logic/tim.rs b/src/Tools/tim_tool/src/logic/tim.rs new file mode 100644 index 00000000..025eed4e --- /dev/null +++ b/src/Tools/tim_tool/src/logic/tim.rs @@ -0,0 +1,76 @@ +use std::{fs::File, path::PathBuf}; +use slint::{Rgba8Pixel, SharedPixelBuffer}; +use tool_helper::Error; + +pub struct TIMInfo { + image_data: SharedPixelBuffer, + palette: Option>, +} + +impl TIMInfo { + pub fn from_image(path: &PathBuf) -> Result { + fn make_color(iter: &mut dyn std::iter::Iterator, load_alpha: bool) -> Option { + Some(Rgba8Pixel::new( + *iter.next()?, *iter.next()?, *iter.next()?, + if load_alpha {*iter.next()?} else {0xFF})) + } + + let mut reader = png::Decoder::new(File::open(path)?).read_info().or_else(|error| {Err(Error::from_error(error))})?; + let info = reader.info().clone(); + + let mut buffer = vec![0; reader.output_buffer_size()]; + let frame_info = reader.next_frame(&mut buffer).or_else(|error| {Err(Error::from_error(error))})?; + let bytes_per_pixel = info.bytes_per_pixel(); + let bit_depth = frame_info.bit_depth; + let mut image_data = SharedPixelBuffer::new(frame_info.width, frame_info.height); + let mut dst_pixels = image_data.make_mut_slice(); + + if info.color_type == png::ColorType::Indexed { + let palette = info.palette.ok_or(Error::from_str("Found indexed PNG without palette"))?; + let mut palette_colors = Vec::new(); + let mut iter = palette.iter(); + + while let Some(color) = make_color(&mut iter, false) { + palette_colors.push(color); + } + + for byte in buffer.into_iter() { + match bit_depth { + png::BitDepth::Four => { + dst_pixels[0] = palette_colors[(byte >> 4) as usize]; + dst_pixels[1] = palette_colors[(byte & 0xF) as usize]; + dst_pixels = &mut dst_pixels[2..]; + }, + png::BitDepth::Eight => { + dst_pixels[0] = palette_colors[byte as usize]; + dst_pixels = &mut dst_pixels[1..]; + }, + _ => {return Err(Error::from_str("Only 4 and 8bit color depth are supported for indexed color images"));} + } + } + Ok(TIMInfo{image_data, palette: Some(palette_colors)}) + } + + else { + if bytes_per_pixel != 3 && bytes_per_pixel != 4 { + return Err(Error::from_text(format!("Image has {} bytes per pixel, but only 3 and 4 bytes per pixel are supported", bytes_per_pixel))); + } + + let mut byte_iter = buffer.iter(); + while let Some(color) = make_color(&mut byte_iter, bytes_per_pixel == 4) { + match bit_depth { + png::BitDepth::Eight => { + dst_pixels[0] = color; + dst_pixels = &mut dst_pixels[1..]; + } + _ => {return Err(Error::from_str("Only 8bit color depth are supported for direct color images"));} + } + } + Ok(TIMInfo{image_data, palette: None}) + } + } + + pub fn get_slint_images(&self) -> (slint::Image, Option) { + (slint::Image::from_rgba8_premultiplied(self.image_data.clone()), None) + } +} \ No newline at end of file diff --git a/src/Tools/tim_tool/src/main.rs b/src/Tools/tim_tool/src/main.rs new file mode 100644 index 00000000..3506d5a5 --- /dev/null +++ b/src/Tools/tim_tool/src/main.rs @@ -0,0 +1,86 @@ +// Prevent console window in addition to Slint window in Windows release builds when, e.g., starting the app via file manager. Ignored on other platforms. +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +mod gui; +use gui::{GUIElements, VRAM_WIDTH, VRAM_HEIGHT, display_information}; +use rfd::FileDialog; +use std::{cell::RefCell, rc::Rc}; +use slint::SharedString; +use tim_tool::logic::Logic; +use tool_helper::Error; + +slint::include_modules!(); + + +fn main() -> Result<(), slint::PlatformError> { + let logic_ref = Rc::new(RefCell::new(Logic::new())); + let main_window_ref = Rc::new(RefCell::new(MainWindow::new()?)); + let gui_elements_ref = Rc::new(RefCell::new(GUIElements::new(main_window_ref.clone())?)); + + setup_main_tab(gui_elements_ref.clone()); + setup_file_tab(gui_elements_ref.clone(), logic_ref); + + let main_window = main_window_ref.borrow(); + main_window.run() +} + +fn setup_main_tab(gui_elements_ref: Rc>) { + let gui_elements = gui_elements_ref.borrow(); + + gui_elements.main_tab.on_move_vram_image(gui_elements_ref.clone(), move |main_tab, _main_window, idx, dx, dy| { + main_tab.move_vram_image(idx as usize, dx, dy); + }); + + gui_elements.main_tab.on_remove_file(gui_elements_ref.clone(), move |main_tab, _main_window, idx| { + if idx >= 0 { + if !main_tab.remove_vram_file(idx as usize) { + display_information("Removing VRAM file", "Can not remove palette. Delete image instead"); + } + } + }); +} + +fn setup_file_tab(gui_elements_ref: Rc>, logic_ref: Rc>) { + let gui_elements = gui_elements_ref.borrow(); + + let logic = logic_ref.clone(); + gui_elements.file_tab.on_browse_file(gui_elements_ref.clone(), move |gui_elements, main_window| -> Result<(), Error> { + let file = FileDialog::new() + .add_filter("PNG image (.png)", &["png"]) + .set_title("PNG image file") + .pick_file(); + + if let Some(file) = file { + if let Some(file_path) = file.to_str() { + main_window.set_file_tab_browse_path(SharedString::from(file_path)); + } + + let file_tab = &gui_elements.file_tab; + let file_name = if let Some(name) = file.file_name() {Some(name.to_string_lossy().to_string())} else {None}; + + let (image, palette_image) = logic.borrow_mut().set_unadded_tim(&file)?; + + let img_size = image.size(); + if img_size.width > VRAM_WIDTH as u32 || img_size.height > VRAM_HEIGHT as u32 { + return Err(Error::from_text(format!("Image size ({}; {}) is to big for VRAM ({}, {})", img_size.width, img_size.height, VRAM_WIDTH, VRAM_HEIGHT))); + } + file_tab.update_new_load(file_name, image); + } + + Ok(()) + }); + + let logic = logic_ref.clone(); + gui_elements.file_tab.on_add_image(gui_elements_ref.clone(), move |gui_elements, main_window| { + let main_tab = &mut gui_elements.main_tab; + let file_tab = &gui_elements.file_tab; + + let file_name = file_tab.get_file_name(); + let (image, palette_image) = logic.borrow_mut().add_unadded_tim_as(&file_name)?; + + main_tab.add_new_vram_file(&file_name, image, palette_image); + file_tab.clear_load(); + main_window.invoke_change_to_main(); + Ok(()) + }); +} \ No newline at end of file diff --git a/src/Tools/tim_tool/ui/app-window.slint b/src/Tools/tim_tool/ui/app-window.slint new file mode 100644 index 00000000..a556f9dc --- /dev/null +++ b/src/Tools/tim_tool/ui/app-window.slint @@ -0,0 +1,63 @@ +import { AboutTab } from "./tab/about-tab.slint"; +import { FileTab, State } from "./tab/file-tab.slint"; +import { MainTab } from "./tab/main-tab.slint"; +import { TabWidget } from "std-widgets.slint"; + +export component MainWindow inherits Window { + // Main Tab values + in-out property main_tab_vram_bg <=> main_tab.vram_bg; + in-out property main_tab_vram_file_list <=> main_tab.vram_files; + in-out property main_tab_vram_images <=> main_tab.vram_images; + + callback main_tab_remove_file_clicked <=> main_tab.remove_file_clicked; + callback move_vram_image <=> main_tab.move_vram_image; + + // Convert Image values + in-out property file_tab_browse_path <=> file_tab.conv_image_path; + in-out property file_tab_image_data <=> file_tab.conv_image_data; + in-out property file_tab_image_name <=> file_tab.conv_image_name; + in-out property file_tab_enable <=> file_tab.conv_image_enable; + callback file_tab_browse_convert_image <=> file_tab.conv_image_browse_clicked; + callback file_tab_add_convert_image <=> file_tab.conv_image_add_clicked; + + title: "TIM Tool 0.1.0"; + width: tab_widget.width; + height: tab_widget.height; + + tab_widget := TabWidget { + x: 0px; + y: 0px; + width: main_tab.width; + height: main_tab.height; + + current-index: 1; + Tab { + title: "File"; + file_tab := FileTab { + x: 0px; + y: 0px; + } + } + Tab { + title: "VRAM Layout"; + main_tab := MainTab { + x: 0px; + y: 0px; + add_file_clicked => {root.change_to_load_file()} + } + } + Tab { + title: "About"; + AboutTab {} + } + } + + public function change_to_load_file() { + file_tab.state = State.ConvertImage; + tab_widget.current-index = 0; + } + + public function change_to_main() { + tab_widget.current-index = 1; + } +} \ No newline at end of file diff --git a/src/Tools/tim_tool/ui/assets/TimTool64x64.png b/src/Tools/tim_tool/ui/assets/TimTool64x64.png new file mode 100644 index 00000000..826eeb6f --- /dev/null +++ b/src/Tools/tim_tool/ui/assets/TimTool64x64.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ee6873c7e18715367d9eec6da7fa56532ed23985cea21905acdf5f20a52007f +size 1703 diff --git a/src/Tools/tim_tool/ui/tab/about-tab.slint b/src/Tools/tim_tool/ui/tab/about-tab.slint new file mode 100644 index 00000000..6f470cf6 --- /dev/null +++ b/src/Tools/tim_tool/ui/tab/about-tab.slint @@ -0,0 +1,28 @@ +import { AboutSlint } from "std-widgets.slint"; +export component AboutTab { + y: 0px; + VerticalLayout { + padding: 8px; + alignment: start; + Text { + font-size: 24pt; + text: "TIM_Tool Version 0.1.0"; + horizontal-alignment: center; + } + Text { + font-size: 20pt; + text: "Part of JabyEngine"; + horizontal-alignment: center; + } + Text { + font-size: 16pt; + text: "MIT License"; + horizontal-alignment: center; + } + Text { + font-size: 16pt; + text: " "; + } + AboutSlint {} + } +} \ No newline at end of file diff --git a/src/Tools/tim_tool/ui/tab/file-tab.slint b/src/Tools/tim_tool/ui/tab/file-tab.slint new file mode 100644 index 00000000..58a39df2 --- /dev/null +++ b/src/Tools/tim_tool/ui/tab/file-tab.slint @@ -0,0 +1,150 @@ +import { Button, TabWidget, LineEdit, GroupBox } from "std-widgets.slint"; + +export enum State { + Project, + ConvertImage, +} + +component ProjectWidget inherits Rectangle { + Text { + text: "!!Planschbecken!!"; + } +} + +component ConvertImageWidget inherits Rectangle { + in-out property image_path; + in-out property image_data; + in-out property image_name; + in-out property enable_button: false; + + callback browse_clicked(); + callback add_clicked(); + + background: #D0D0D0; + + VerticalLayout { + alignment: start; + GroupBox { + title: "Add image file"; + VerticalLayout { + alignment: start; + padding: 4px; + Text { + text: "Select image file to convert to be used with TIM Tool"; + } + HorizontalLayout { + alignment: start; + padding: 4px; + LineEdit { + width: 300pt; + text: image_path; + } + Button { + text: "Browse"; + clicked => {browse_clicked();} + } + } + } + } + + GroupBox { + title: "Loaded image"; + VerticalLayout { + alignment: start; + padding: 4px; + HorizontalLayout { + alignment: center; + Rectangle { + width: 256px; + height: 256px; + background: #000000; + Image { + width: 256px; + height: 256px; + source: root.image_data; + image-fit: contain; + } + } + } + HorizontalLayout { + alignment: start; + padding: 4px; + VerticalLayout { + alignment: center; + Text { + text: "Name: "; + } + } + LineEdit { + text: root.image_name; + } + } + HorizontalLayout { + alignment: start; + padding: 4px; + Button { + text: "Add Image"; + enabled: root.enable_button; + clicked => {root.add_clicked();} + } + } + } + } + } +} + +export component FileTab inherits Rectangle { + in-out property conv_image_path; + in-out property conv_image_data; + in-out property conv_image_name; + in-out property conv_image_enable; + in-out property state; + callback conv_image_browse_clicked; + callback conv_image_add_clicked; + + x: 0px; + y: 0px; + + HorizontalLayout { + padding: 4px; + VerticalLayout { + padding: 4px; + width: 20%; + alignment: start; + + Button { + text: "Projects"; + clicked => { + root.state = State.Project; + } + } + Button { + text: "Add Image"; + clicked => { + root.state = State.ConvertImage; + } + } + } + + VerticalLayout { + padding: 4px; + alignment: start; + if root.state == State.Project : ProjectWidget { + } + if root.state == State.ConvertImage : ConvertImageWidget { + image_path <=> root.conv_image_path; + image_data <=> root.conv_image_data; + image_name <=> root.conv_image_name; + enable_button <=> root.conv_image_enable; + + browse_clicked => { + root.conv_image_browse_clicked(); + } + + add_clicked => { + root.conv_image_add_clicked(); + } + } + } + } +} \ No newline at end of file diff --git a/src/Tools/tim_tool/ui/tab/main-tab.slint b/src/Tools/tim_tool/ui/tab/main-tab.slint new file mode 100644 index 00000000..dc3b1a0f --- /dev/null +++ b/src/Tools/tim_tool/ui/tab/main-tab.slint @@ -0,0 +1,156 @@ +import { VRAMArea } from "../vram-components.slint"; +import { Button, ComboBox, GroupBox, StandardListView } from "std-widgets.slint"; + +struct VRAMImage { + img: image, + x: int, + y: int, + palette_count: int, + is_palette: bool, +} + +export component MainTab inherits Rectangle { + in-out property vram_bg; + in-out property <[StandardListViewItem]> vram_files: []; + in-out property <[VRAMImage]> vram_images: []; + + callback add_file_clicked(); + callback remove_file_clicked(int); + callback move_vram_image(int, int, int); + + width: group.width + group.x*2; + height: group.height + group.y*2 + 32px; + + group := GroupBox { + title: "VRAM Layout"; + x: 4px; + y: 4px; + + VerticalLayout { + background_rect := Rectangle { + width: background_image.width + root.get_border_width()*2px; + height: background_image.height + root.get_border_width()*2px; + border-width: root.get_border_width()*1px; + border-color: #404040; + background: #A0A0A0; + background_image := VRAMArea { + x: root.get_border_width()*1px; + y: root.get_border_width()*1px; + img: vram_bg; + img_x: 0; + img_y: 0; + } + + for vram_image[i] in root.vram_images: VRAMArea { + x: root.get_border_width()*1px; + y: root.get_border_width()*1px; + img: vram-image.img; + img_x: vram-image.x; + img_y: vram-image.y; + TouchArea { + x: (parent.img_x + self.lines_crossed_x())*1px; + y: (parent.img_y + self.lines_crossed_y())*1px; + width: (parent.img.width + self.lines_crossed_width())*1px; + height: (parent.img.height + self.lines_crossed_height())*1px; + mouse-cursor: grab; + moved => { + self.mouse-cursor = MouseCursor.grabbing; + root.move_vram_image(i, (self.mouse-x - self.pressed-x)/1px, (self.mouse-y - self.pressed-y)/1px); + cur_sel_x.display_value = parent.img_x; + cur_sel_y.display_value = parent.img_y; + } + pointer-event(event) => { + if event.kind == PointerEventKind.up { + self.mouse-cursor = MouseCursor.grab; + } + + if event.kind == PointerEventKind.down { + cur_sel_x.display_value = parent.img_x; + cur_sel_y.display_value = parent.img_y; + cur_sel_img.source = parent.img; + } + } + + // Thanks to Cody the white tiger + function lines_crossed_x() -> int { + return parent.img_x/64; + } + + function lines_crossed_y() -> int { + return parent.img_y/256; + } + + function lines_crossed_width() -> int { + return ((parent.img_x + parent.img.width)/64) - self.lines_crossed_x(); + } + + function lines_crossed_height() -> int { + return ((parent.img_y + parent.img.height)/256) - self.lines_crossed_y(); + } + } + } + } + HorizontalLayout { + padding: 4px; + GroupBox { + title: "Added files"; + VerticalLayout { + alignment: start; + padding: 4px; + vram_files_list := StandardListView { + width: background_image.width/2; + height: 128px; + model: root.vram_files; + } + HorizontalLayout { + padding: 4px; + Button { + text: "Add file"; + clicked => {root.add_file_clicked();} + } + Button { + text: "Remove file"; + clicked => { + root.remove_file_clicked(vram_files_list.current_item); + vram_files_list.current-item = -1; + } + } + } + } + } + GroupBox { + title: "Current File"; + VerticalLayout { + padding: 4px; + Rectangle { + width: 128px; + height: 128px; + background: #A0A0A0; + cur_sel_img := Image { + width: 128px; + height: 128px; + image-fit: contain; + } + } + cur_sel_x := Text { + in-out property display_value; + text: "X: " + display_value; + } + cur_sel_y :=Text { + in-out property display_value; + text: "Y: " + display_value; + } + ComboBox { + model: ["4-bit", "16-bit", "24-bit"]; + current-value: "4-bit"; + } + } + } + } + } + } + + function get_border_width() -> int { + return 4; + } +} \ No newline at end of file diff --git a/src/Tools/tim_tool/ui/vram-components.slint b/src/Tools/tim_tool/ui/vram-components.slint new file mode 100644 index 00000000..b7b869df --- /dev/null +++ b/src/Tools/tim_tool/ui/vram-components.slint @@ -0,0 +1,41 @@ +// TODO: Maybe make them inherit Windows...? +component VRAMSegment inherits Rectangle { + in property img; + in property clip_x; + in property clip_y; + + width: 64px; + height: 256px; + clip: true; + Image { + source: img; + x: -root.clip_x*1px; + y: -root.clip_y*1px; + } +} + +export component VRAMArea inherits Rectangle { + in property img; + in property img_x; + in property img_y; + + width: (64*16+15)*1px; + height: (256*2+1)*1px; + + for idx in 32 : VRAMSegment { + x: root.get_x(idx)*(64px + 1px); + y: root.get_y(idx)*(256px + 1px); + + img: img; + clip_x: (root.get_x(idx)*64) - root.img_x; + clip_y: (root.get_y(idx)*256) - root.img_y; + } + + function get_x(idx: int) -> int { + return mod(idx, 16); + } + + function get_y(idx: int) -> int { + return floor(idx/16); + } +} \ No newline at end of file