diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/app.rs b/hcie-iced-app/crates/hcie-iced-gui/src/app.rs index 81dc67a..eec8f79 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/app.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/app.rs @@ -70,6 +70,9 @@ pub enum ActiveDialog { CloseConfirm, SelectionOp(&'static str), LayerStyleDialog, + ImageSize, + CanvasSize, + About, } /// Top-level application state. @@ -111,6 +114,15 @@ pub struct HcieIcedApp { pub dialog_lightness: f32, /// Selection operation dialog state. pub dialog_selection_value: f32, + /// Image Size dialog state. + pub dialog_image_size_width: u32, + pub dialog_image_size_height: u32, + pub dialog_image_size_constrain: bool, + pub dialog_image_size_original_ratio: f32, + /// Canvas Size dialog state. + pub dialog_canvas_size_width: u32, + pub dialog_canvas_size_height: u32, + pub dialog_canvas_size_anchor: (usize, usize), /// Selected filter. pub selected_filter: Option, /// Filter parameters (JSON) for the currently selected filter. @@ -429,6 +441,18 @@ pub enum Message { SelectionOpValue(f32), SelectionOpApply, + // ── Image Size Dialog ────────────────────────────── + DialogImageSizeWidth(u32), + DialogImageSizeHeight(u32), + DialogImageSizeConstrain(bool), + DialogImageSizeApply, + + // ── Canvas Size Dialog ───────────────────────────── + DialogCanvasSizeWidth(u32), + DialogCanvasSizeHeight(u32), + DialogCanvasSizeAnchor(usize, usize), + DialogCanvasSizeApply, + // ── Selection tools ───────────────────────────────── SelectionDragStart(f32, f32), SelectionDragMove(f32, f32), @@ -706,6 +730,13 @@ impl HcieIcedApp { dialog_saturation: 0.0, dialog_lightness: 0.0, dialog_selection_value: 5.0, + dialog_image_size_width: 800, + dialog_image_size_height: 600, + dialog_image_size_constrain: true, + dialog_image_size_original_ratio: 800.0 / 600.0, + dialog_canvas_size_width: 800, + dialog_canvas_size_height: 600, + dialog_canvas_size_anchor: (1, 1), selected_filter: None, filter_params: serde_json::json!({}), filter_preview_active: false, @@ -2187,6 +2218,54 @@ impl HcieIcedApp { return Task::perform(async {}, |_| Message::CompositeRefresh); } + // ── Image Size Dialog ──────────────────────── + Message::DialogImageSizeWidth(w) => { + self.dialog_image_size_width = w; + if self.dialog_image_size_constrain && self.dialog_image_size_original_ratio > 0.0 { + self.dialog_image_size_height = (w as f32 / self.dialog_image_size_original_ratio) as u32; + } + } + Message::DialogImageSizeHeight(h) => { + self.dialog_image_size_height = h; + if self.dialog_image_size_constrain && self.dialog_image_size_original_ratio > 0.0 { + self.dialog_image_size_width = (h as f32 * self.dialog_image_size_original_ratio) as u32; + } + } + Message::DialogImageSizeConstrain(c) => { + self.dialog_image_size_constrain = c; + } + Message::DialogImageSizeApply => { + let w = self.dialog_image_size_width.max(1); + let h = self.dialog_image_size_height.max(1); + self.documents[self.active_doc].engine.resize_canvas(w, h); + self.documents[self.active_doc].engine.pre_tile_all_layers(); + self.refresh_composite_if_needed(); + self.documents[self.active_doc].full_upload.replace(true); + self.active_dialog = ActiveDialog::None; + return Task::perform(async {}, |_| Message::CompositeRefresh); + } + + // ── Canvas Size Dialog ─────────────────────── + Message::DialogCanvasSizeWidth(w) => { + self.dialog_canvas_size_width = w; + } + Message::DialogCanvasSizeHeight(h) => { + self.dialog_canvas_size_height = h; + } + Message::DialogCanvasSizeAnchor(r, c) => { + self.dialog_canvas_size_anchor = (r, c); + } + Message::DialogCanvasSizeApply => { + let new_w = self.dialog_canvas_size_width.max(1); + let new_h = self.dialog_canvas_size_height.max(1); + self.documents[self.active_doc].engine.resize_canvas(new_w, new_h); + self.documents[self.active_doc].engine.pre_tile_all_layers(); + self.refresh_composite_if_needed(); + self.documents[self.active_doc].full_upload.replace(true); + self.active_dialog = ActiveDialog::None; + return Task::perform(async {}, |_| Message::CompositeRefresh); + } + // ── Selection tools ─────────────────────────── Message::SelectionDragStart(x, y) => { self.documents[self.active_doc].selection_rect = Some((x, y, x, y)); @@ -3288,8 +3367,23 @@ impl HcieIcedApp { // (2, 7) = Vectorize Bitmap... // (2, 8) = Wavelet Decompose... // (2, 9) = separator - // (2, 10) = Canvas Size... - // (2, 11) = Image Size... + (2, 10) => { + // Canvas Size... + let (w, h) = self.documents[self.active_doc].engine.get_canvas_size(); + self.dialog_canvas_size_width = w; + self.dialog_canvas_size_height = h; + self.dialog_canvas_size_anchor = (1, 1); + self.active_dialog = ActiveDialog::CanvasSize; + } + (2, 11) => { + // Image Size... + let (w, h) = self.documents[self.active_doc].engine.get_canvas_size(); + self.dialog_image_size_width = w; + self.dialog_image_size_height = h; + self.dialog_image_size_original_ratio = w as f32 / h as f32; + self.dialog_image_size_constrain = true; + self.active_dialog = ActiveDialog::ImageSize; + } // (2, 12) = Transform (submenu) // (2, 13) = Crop // (2, 14) = Trim... @@ -3731,6 +3825,18 @@ impl HcieIcedApp { self.layer_style_offset, ) } + ActiveDialog::ImageSize => dialogs::image_size::view( + self.dialog_image_size_width, + self.dialog_image_size_height, + self.dialog_image_size_constrain, + ), + ActiveDialog::CanvasSize => dialogs::canvas_size::view( + self.dialog_canvas_size_width, + self.dialog_canvas_size_height, + self.dialog_canvas_size_anchor.0, + self.dialog_canvas_size_anchor.1, + ), + ActiveDialog::About => dialogs::about::view(), }; // Stack main content with overlays (dialog + menu dropdown + dock profile dialog). diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/dialogs/about.rs b/hcie-iced-app/crates/hcie-iced-gui/src/dialogs/about.rs new file mode 100644 index 0000000..7d5af2b --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/dialogs/about.rs @@ -0,0 +1,38 @@ +//! About dialog — displays application name, version, and credits. + +use crate::app::Message; +use iced::widget::{button, column, container, horizontal_rule, text}; +use iced::{Element, Length}; + +/// Build the About dialog. +pub fn view() -> Element<'static, Message> { + let ok_btn = button(text("OK").size(12)) + .on_press(Message::DialogClose) + .padding([8, 16]); + + let dialog = column![ + text("HCIE-Rust").size(20).font(iced::Font::MONOSPACE), + text("Version 3.05").size(12), + horizontal_rule(1), + text("A pixel-grade image editor built with Rust.").size(11), + text("Powered by ICED and the HCIE engine.").size(11), + horizontal_rule(1), + text("© 2025 HCIE-Rust Contributors").size(10), + ok_btn, + ] + .spacing(8) + .padding(20) + .width(320) + .align_x(iced::Alignment::Center); + + container(dialog) + .center_x(Length::Fill) + .center_y(Length::Fill) + .style(|_theme| iced::widget::container::Style { + background: Some(iced::Background::Color(iced::Color::from_rgba(0.0, 0.0, 0.0, 0.5))), + ..Default::default() + }) + .width(Length::Fill) + .height(Length::Fill) + .into() +} diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/dialogs/canvas_size.rs b/hcie-iced-app/crates/hcie-iced-gui/src/dialogs/canvas_size.rs new file mode 100644 index 0000000..f7133eb --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/dialogs/canvas_size.rs @@ -0,0 +1,99 @@ +//! Canvas Size dialog — extend or crop the canvas around an anchor point. +//! +//! Shows width/height inputs and a 9-point anchor grid. The anchor +//! determines which corner/edge of the existing content stays fixed +//! when the canvas is resized. + +use crate::app::Message; +use iced::widget::{button, column, container, horizontal_rule, row, text, text_input}; +use iced::{Element, Length}; + +/// Anchor position labels for the 9-point grid (row-major). +const ANCHOR_LABELS: [[&str; 3]; 3] = [ + ["TL", "TC", "TR"], + ["ML", "MC", "MR"], + ["BL", "BC", "BR"], +]; + +/// Build the Canvas Size dialog. +pub fn view( + width: u32, + height: u32, + anchor_row: usize, + anchor_col: usize, +) -> Element<'static, Message> { + let width_str = width.to_string(); + let height_str = height.to_string(); + + let width_input = text_input("Width", &width_str) + .on_input(|s| { + let v: u32 = s.parse().unwrap_or(0); + Message::DialogCanvasSizeWidth(v) + }) + .width(120); + + let height_input = text_input("Height", &height_str) + .on_input(|s| { + let v: u32 = s.parse().unwrap_or(0); + Message::DialogCanvasSizeHeight(v) + }) + .width(120); + + // 3×3 anchor grid + let mut anchor_grid = column![].spacing(2); + for r in 0..3 { + let mut row_widgets = row![].spacing(2); + for c in 0..3 { + let is_selected = r == anchor_row && c == anchor_col; + let label = ANCHOR_LABELS[r][c]; + let btn = button(text(label).size(10)) + .on_press(Message::DialogCanvasSizeAnchor(r, c)) + .padding([4, 6]); + let btn = if is_selected { + btn.style(iced::widget::button::primary) + } else { + btn.style(iced::widget::button::secondary) + }; + row_widgets = row_widgets.push(btn); + } + anchor_grid = anchor_grid.push(row_widgets); + } + + let apply_btn = button(text("Apply").size(12)) + .on_press(Message::DialogCanvasSizeApply) + .padding([8, 16]); + + let cancel_btn = button(text("Cancel").size(12)) + .on_press(Message::DialogClose) + .padding([8, 16]); + + let dialog = column![ + text("Canvas Size").size(16).font(iced::Font::MONOSPACE), + horizontal_rule(1), + row![ + column![text("Width:").size(11), width_input].spacing(4), + text("×").size(12), + column![text("Height:").size(11), height_input].spacing(4), + ] + .spacing(8) + .align_y(iced::Alignment::Center), + text("Anchor:").size(11), + anchor_grid, + horizontal_rule(1), + row![cancel_btn, apply_btn].spacing(12), + ] + .spacing(8) + .padding(16) + .width(360); + + container(dialog) + .center_x(Length::Fill) + .center_y(Length::Fill) + .style(|_theme| iced::widget::container::Style { + background: Some(iced::Background::Color(iced::Color::from_rgba(0.0, 0.0, 0.0, 0.5))), + ..Default::default() + }) + .width(Length::Fill) + .height(Length::Fill) + .into() +} diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/dialogs/image_size.rs b/hcie-iced-app/crates/hcie-iced-gui/src/dialogs/image_size.rs new file mode 100644 index 0000000..a84d6dc --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/dialogs/image_size.rs @@ -0,0 +1,73 @@ +//! Image Size dialog — resize the image (all layers). +//! +//! Provides width/height inputs with a constrain-proportions checkbox. +//! When the checkbox is checked, changing one dimension automatically +//! updates the other to maintain the current aspect ratio. + +use crate::app::Message; +use iced::widget::{button, checkbox, column, container, horizontal_rule, row, text, text_input}; +use iced::{Element, Length}; + +/// Build the Image Size dialog. +pub fn view( + width: u32, + height: u32, + constrain: bool, +) -> Element<'static, Message> { + let width_str = width.to_string(); + let height_str = height.to_string(); + + let width_input = text_input("Width", &width_str) + .on_input(|s| { + let v: u32 = s.parse().unwrap_or(0); + Message::DialogImageSizeWidth(v) + }) + .width(120); + + let height_input = text_input("Height", &height_str) + .on_input(|s| { + let v: u32 = s.parse().unwrap_or(0); + Message::DialogImageSizeHeight(v) + }) + .width(120); + + let constrain_check = checkbox("Constrain proportions", constrain) + .on_toggle(|v| Message::DialogImageSizeConstrain(v)); + + let apply_btn = button(text("Apply").size(12)) + .on_press(Message::DialogImageSizeApply) + .padding([8, 16]); + + let cancel_btn = button(text("Cancel").size(12)) + .on_press(Message::DialogClose) + .padding([8, 16]); + + let dialog = column![ + text("Image Size").size(16).font(iced::Font::MONOSPACE), + horizontal_rule(1), + row![ + column![text("Width:").size(11), width_input].spacing(4), + text("×").size(12), + column![text("Height:").size(11), height_input].spacing(4), + ] + .spacing(8) + .align_y(iced::Alignment::Center), + constrain_check, + horizontal_rule(1), + row![cancel_btn, apply_btn].spacing(12), + ] + .spacing(8) + .padding(16) + .width(360); + + container(dialog) + .center_x(Length::Fill) + .center_y(Length::Fill) + .style(|_theme| iced::widget::container::Style { + background: Some(iced::Background::Color(iced::Color::from_rgba(0.0, 0.0, 0.0, 0.5))), + ..Default::default() + }) + .width(Length::Fill) + .height(Length::Fill) + .into() +} diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/dialogs/mod.rs b/hcie-iced-app/crates/hcie-iced-gui/src/dialogs/mod.rs index 5c50453..f3de249 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/dialogs/mod.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/dialogs/mod.rs @@ -4,3 +4,6 @@ pub mod new_image; pub mod adjustments; pub mod confirm; pub mod dock_profile; +pub mod image_size; +pub mod canvas_size; +pub mod about;