From 3c5a9e32a1d60ef2a945e04ebee8333351f05d23 Mon Sep 17 00:00:00 2001 From: Halit Can Date: Wed, 15 Jul 2026 17:35:34 +0300 Subject: [PATCH] feat(iced): add dock profile management --- hcie-iced-app/crates/hcie-iced-gui/src/app.rs | 182 +++++++++++++++++- .../hcie-iced-gui/src/dialogs/dock_profile.rs | 107 ++++++++++ .../crates/hcie-iced-gui/src/dialogs/mod.rs | 1 + .../crates/hcie-iced-gui/src/settings.rs | 22 +++ 4 files changed, 310 insertions(+), 2 deletions(-) create mode 100644 hcie-iced-app/crates/hcie-iced-gui/src/dialogs/dock_profile.rs 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 2b46ce9..3e93c75 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/app.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/app.rs @@ -181,6 +181,16 @@ pub struct HcieIcedApp { pub viewer_active: bool, /// Viewer state for directory browsing and thumbnail grid. pub viewer_state: crate::viewer::ViewerState, + /// Name of the currently active dock profile (if any). + pub active_dock_profile: Option, + /// Text input for new dock profile name. + pub dock_profile_new_name: String, + /// Index of the dock profile being renamed (if any). + pub dock_profile_rename_idx: Option, + /// Text input for renaming a dock profile. + pub dock_profile_rename_buf: String, + /// Whether the dock profile dialog is visible. + pub show_dock_profile_dialog: bool, } /// A recently opened file entry. @@ -509,6 +519,19 @@ pub enum Message { WindowClose, WindowId(iced::window::Id), + // ── Dock Profiles ─────────────────────────────────── + DockProfileSave(String), + DockProfileLoad(String), + DockProfileRename(String, String), + DockProfileDelete(String), + DockProfileRenameStart(String), + DockProfileRenameConfirm, + DockProfileRenameCancel, + DockProfileNewNameChanged(String), + DockProfileRenameChanged(String), + DockProfileDialogShow, + DockProfileDialogHide, + // ── Misc ──────────────────────────────────────────── NoOp, @@ -570,6 +593,27 @@ fn build_brush_tip(state: &ToolState) -> BrushTip { } } +/// Convert a display label back to a PaneType for dock profile loading. +fn pane_type_from_label(label: &str) -> Option { + use crate::dock::state::PaneType; + match label { + "Canvas" => Some(PaneType::Canvas), + "Layers" => Some(PaneType::Layers), + "History" => Some(PaneType::History), + "Brushes & Tips" => Some(PaneType::Brushes), + "Filters" => Some(PaneType::Filters), + "Color Palette" => Some(PaneType::ColorPicker), + "Properties" => Some(PaneType::Properties), + "Script" => Some(PaneType::Script), + "AI Assistant" => Some(PaneType::AiChat), + "Layer Details" => Some(PaneType::LayerDetails), + "Geometry" => Some(PaneType::Geometry), + "Tool Settings" => Some(PaneType::ToolSettings), + "AI Script Generator" => Some(PaneType::AiScript), + _ => None, + } +} + /// Helper to combine two bounding boxes of dirty regions. fn union_regions(r1: Option<[u32; 4]>, r2: [u32; 4]) -> [u32; 4] { match r1 { @@ -691,6 +735,11 @@ impl HcieIcedApp { ai_script_url: "http://localhost:11434".to_string(), viewer_active: false, viewer_state: crate::viewer::ViewerState::default(), + active_dock_profile: None, + dock_profile_new_name: String::new(), + dock_profile_rename_idx: None, + dock_profile_rename_buf: String::new(), + show_dock_profile_dialog: false, }; // Apply saved settings to tool state @@ -2809,6 +2858,127 @@ impl HcieIcedApp { self.dock.toggle_maximize(pane); } + // ── Dock Profiles ───────────────────────────── + Message::DockProfileSave(name) => { + let name = name.trim().to_string(); + if name.is_empty() { + return Task::none(); + } + // Collect the list of panel types currently open in the dock + let visible_panels: Vec = self.dock.pane_grid.iter() + .map(|(_, pane_type)| pane_type.label().to_string()) + .collect(); + let profile = crate::settings::DockProfile { + name: name.clone(), + visible_panels, + left_col1_w: 0.0, + left_col2_w: 0.0, + right_col1_w: 0.0, + right_col2_w: 0.0, + last_window_width: 0.0, + }; + self.settings.dock_profiles.retain(|p| p.name != name); + self.settings.dock_profiles.push(profile); + self.active_dock_profile = Some(name.clone()); + self.dock_profile_new_name.clear(); + log::info!("Dock profile saved: {}", name); + let _ = self.settings.save(); + self.active_dialog = ActiveDialog::None; + } + Message::DockProfileLoad(name) => { + if let Some(profile) = self + .settings + .dock_profiles + .iter() + .find(|p| p.name == *name) + { + // Reopen all panels listed in the saved profile + let panel_names = profile.visible_panels.clone(); + self.dock = crate::dock::state::DockState::new(); + for panel_name in &panel_names { + if let Some(pane_type) = pane_type_from_label(panel_name) { + self.dock.reopen_pane(pane_type); + } + } + self.active_dock_profile = Some(name.clone()); + log::info!("Dock profile applied: {}", name); + } + } + Message::DockProfileRename(old_name, new_name) => { + let new_name = new_name.trim().to_string(); + if new_name.is_empty() { + return Task::none(); + } + if let Some(profile) = self + .settings + .dock_profiles + .iter_mut() + .find(|p| p.name == old_name) + { + profile.name = new_name.clone(); + if self.active_dock_profile.as_deref() == Some(&old_name) { + self.active_dock_profile = Some(new_name); + } + log::info!("Dock profile renamed: {} -> {}", old_name, profile.name); + let _ = self.settings.save(); + } + self.dock_profile_rename_idx = None; + self.dock_profile_rename_buf.clear(); + } + Message::DockProfileDelete(name) => { + let before = self.settings.dock_profiles.len(); + self.settings.dock_profiles.retain(|p| p.name != *name); + if self.settings.dock_profiles.len() < before { + if self.active_dock_profile.as_deref() == Some(&name) { + self.active_dock_profile = None; + } + log::info!("Dock profile deleted: {}", name); + let _ = self.settings.save(); + } + } + Message::DockProfileRenameStart(name) => { + if let Some(idx) = self.settings.dock_profiles.iter().position(|p| p.name == name) { + self.dock_profile_rename_idx = Some(idx); + self.dock_profile_rename_buf = name; + } + } + Message::DockProfileRenameConfirm => { + if let Some(idx) = self.dock_profile_rename_idx { + if idx < self.settings.dock_profiles.len() { + let old_name = self.settings.dock_profiles[idx].name.clone(); + let new_name = self.dock_profile_rename_buf.trim().to_string(); + if !new_name.is_empty() { + self.settings.dock_profiles[idx].name = new_name.clone(); + if self.active_dock_profile.as_deref() == Some(&old_name) { + self.active_dock_profile = Some(new_name); + } + log::info!("Dock profile renamed: {} -> {}", old_name, self.settings.dock_profiles[idx].name); + let _ = self.settings.save(); + } + } + } + self.dock_profile_rename_idx = None; + self.dock_profile_rename_buf.clear(); + } + Message::DockProfileRenameCancel => { + self.dock_profile_rename_idx = None; + self.dock_profile_rename_buf.clear(); + } + Message::DockProfileNewNameChanged(name) => { + self.dock_profile_new_name = name; + } + Message::DockProfileRenameChanged(name) => { + self.dock_profile_rename_buf = name; + } + Message::DockProfileDialogShow => { + self.show_dock_profile_dialog = true; + } + Message::DockProfileDialogHide => { + self.show_dock_profile_dialog = false; + self.dock_profile_rename_idx = None; + self.dock_profile_rename_buf.clear(); + } + // ── File I/O ───────────────────────────────── Message::NewDocument(w, h) => { let mut engine = Engine::new(w, h); @@ -3511,17 +3681,25 @@ impl HcieIcedApp { } }; - // Stack main content with overlays (dialog + menu dropdown). + // Stack main content with overlays (dialog + menu dropdown + dock profile dialog). // `content` is already wrapped in a styled container with the app // background, so no extra wrapper is needed. let has_dialog = self.active_dialog != ActiveDialog::None; let has_menu = self.active_menu.is_some(); + let has_dock_profile_dialog = self.show_dock_profile_dialog; - if has_dialog || has_menu { + if has_dialog || has_menu || has_dock_profile_dialog { let mut stack = iced::widget::Stack::new().push(content); if has_dialog { stack = stack.push(dialog_overlay); } + if has_dock_profile_dialog { + stack = stack.push(dialogs::dock_profile::view( + &self.settings.dock_profiles, + self.active_dock_profile.as_deref(), + &self.dock_profile_new_name, + )); + } if let Some(menu_overlay) = panels::menus::dropdown_overlay(self.active_menu, &self.recent_files, self.theme_state.colors()) { stack = stack.push(menu_overlay); } diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/dialogs/dock_profile.rs b/hcie-iced-app/crates/hcie-iced-gui/src/dialogs/dock_profile.rs new file mode 100644 index 0000000..6559bde --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/dialogs/dock_profile.rs @@ -0,0 +1,107 @@ +//! Dock Profile dialog — save, load, rename, and delete dock layout profiles. + +use crate::app::Message; +use iced::widget::{button, column, container, horizontal_rule, row, scrollable, text, text_input}; +use iced::{Element, Length}; + +/// Build the dock profile management dialog. +/// +/// Displays a list of saved dock profiles with apply/rename/delete actions, +/// plus a text input and save button for creating new profiles. +/// +/// # Arguments +/// * `profiles` — serialized list of saved dock profiles +/// * `active_profile` — name of the currently active profile (if any) +/// * `new_name` — current text in the "new profile name" input field +pub fn view( + profiles: &[crate::settings::DockProfile], + active_profile: Option<&str>, + new_name: &str, +) -> Element<'static, Message> { + let new_name_owned = new_name.to_string(); + + // Profile list + let mut profile_list = column![].spacing(4); + + if profiles.is_empty() { + profile_list = profile_list.push( + text("No saved profiles yet.").size(11), + ); + } else { + for profile in profiles { + let name = profile.name.clone(); + let is_active = active_profile == Some(&name); + + let apply_label = if is_active { "✓ Applied" } else { "Apply" }; + let apply_btn = button(text(apply_label).size(11)) + .on_press(Message::DockProfileLoad(name.clone())) + .padding([4, 10]); + + let rename_btn = button(text("✎").size(11)) + .on_press(Message::DockProfileRenameStart(name.clone())) + .padding([4, 8]); + + let delete_btn = button(text("✕").size(11)) + .on_press(Message::DockProfileDelete(name.clone())) + .padding([4, 8]); + + let name_text = text(name.clone()) + .size(12) + .width(Length::Fill); + + profile_list = profile_list.push( + row![apply_btn, name_text, rename_btn, delete_btn] + .spacing(8) + .align_y(iced::Alignment::Center), + ); + } + } + + // New profile name input + save + let name_input = text_input("My Layout", &new_name_owned) + .on_input(Message::DockProfileNewNameChanged) + .width(Length::Fill); + + let can_save = !new_name.trim().is_empty(); + let save_btn = if can_save { + button(text("Save Current").size(11)) + .on_press(Message::DockProfileSave(new_name_owned.trim().to_string())) + .padding([6, 12]) + } else { + button(text("Save Current").size(11)) + .padding([6, 12]) + }; + + // Scrollable profile list with fixed max height + let scrollable_list = scrollable(profile_list) + .height(iced::Length::Fixed(200.0)) + .width(Length::Fill); + + let dialog = column![ + text("Dock Layout Profiles").size(16), + horizontal_rule(1), + scrollable_list, + horizontal_rule(1), + text("New profile name:").size(11), + row![name_input, save_btn].spacing(8).width(Length::Fill), + horizontal_rule(1), + row![button(text("Close").size(11)) + .on_press(Message::DockProfileDialogHide) + .padding([6, 12])] + .width(Length::Fill), + ] + .spacing(8) + .padding(16) + .width(420); + + 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 65bc9cd..5c50453 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 @@ -3,3 +3,4 @@ pub mod new_image; pub mod adjustments; pub mod confirm; +pub mod dock_profile; diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/settings.rs b/hcie-iced-app/crates/hcie-iced-gui/src/settings.rs index fe0e25b..93c6649 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/settings.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/settings.rs @@ -6,6 +6,24 @@ use serde::{Deserialize, Serialize}; use std::path::PathBuf; +/// A saved dock layout profile. +/// +/// Stores a simplified pane layout description along with layout dimensions +/// so the user can save, load, rename, and delete dock configurations. +/// The full iced PaneGrid State cannot be serialized directly, so we store +/// the essential layout info: which panels are open and their relative positions. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DockProfile { + pub name: String, + /// List of panel names currently visible in the dock (e.g. "Layers", "History"). + pub visible_panels: Vec, + pub left_col1_w: f32, + pub left_col2_w: f32, + pub right_col1_w: f32, + pub right_col2_w: f32, + pub last_window_width: f32, +} + /// Main settings structure saved to disk. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AppSettings { @@ -15,6 +33,9 @@ pub struct AppSettings { pub panel_layout: PanelLayout, /// Window settings. pub window: WindowSettings, + /// Saved dock layout profiles. + #[serde(default)] + pub dock_profiles: Vec, } /// Tool-specific settings that persist between sessions. @@ -68,6 +89,7 @@ impl Default for AppSettings { tool_settings: ToolSettings::default(), panel_layout: PanelLayout::default(), window: WindowSettings::default(), + dock_profiles: Vec::new(), } } }