feat(iced): add dock profile management

This commit is contained in:
2026-07-15 17:35:34 +03:00
parent 6d51ee528a
commit 3c5a9e32a1
4 changed files with 310 additions and 2 deletions
+180 -2
View File
@@ -181,6 +181,16 @@ pub struct HcieIcedApp {
pub viewer_active: bool, pub viewer_active: bool,
/// Viewer state for directory browsing and thumbnail grid. /// Viewer state for directory browsing and thumbnail grid.
pub viewer_state: crate::viewer::ViewerState, pub viewer_state: crate::viewer::ViewerState,
/// Name of the currently active dock profile (if any).
pub active_dock_profile: Option<String>,
/// 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<usize>,
/// 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. /// A recently opened file entry.
@@ -509,6 +519,19 @@ pub enum Message {
WindowClose, WindowClose,
WindowId(iced::window::Id), 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 ──────────────────────────────────────────── // ── Misc ────────────────────────────────────────────
NoOp, 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<crate::dock::state::PaneType> {
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. /// Helper to combine two bounding boxes of dirty regions.
fn union_regions(r1: Option<[u32; 4]>, r2: [u32; 4]) -> [u32; 4] { fn union_regions(r1: Option<[u32; 4]>, r2: [u32; 4]) -> [u32; 4] {
match r1 { match r1 {
@@ -691,6 +735,11 @@ impl HcieIcedApp {
ai_script_url: "http://localhost:11434".to_string(), ai_script_url: "http://localhost:11434".to_string(),
viewer_active: false, viewer_active: false,
viewer_state: crate::viewer::ViewerState::default(), 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 // Apply saved settings to tool state
@@ -2809,6 +2858,127 @@ impl HcieIcedApp {
self.dock.toggle_maximize(pane); 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<String> = 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 ───────────────────────────────── // ── File I/O ─────────────────────────────────
Message::NewDocument(w, h) => { Message::NewDocument(w, h) => {
let mut engine = Engine::new(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 // `content` is already wrapped in a styled container with the app
// background, so no extra wrapper is needed. // background, so no extra wrapper is needed.
let has_dialog = self.active_dialog != ActiveDialog::None; let has_dialog = self.active_dialog != ActiveDialog::None;
let has_menu = self.active_menu.is_some(); 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); let mut stack = iced::widget::Stack::new().push(content);
if has_dialog { if has_dialog {
stack = stack.push(dialog_overlay); 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()) { if let Some(menu_overlay) = panels::menus::dropdown_overlay(self.active_menu, &self.recent_files, self.theme_state.colors()) {
stack = stack.push(menu_overlay); stack = stack.push(menu_overlay);
} }
@@ -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()
}
@@ -3,3 +3,4 @@
pub mod new_image; pub mod new_image;
pub mod adjustments; pub mod adjustments;
pub mod confirm; pub mod confirm;
pub mod dock_profile;
@@ -6,6 +6,24 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::PathBuf; 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<String>,
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. /// Main settings structure saved to disk.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppSettings { pub struct AppSettings {
@@ -15,6 +33,9 @@ pub struct AppSettings {
pub panel_layout: PanelLayout, pub panel_layout: PanelLayout,
/// Window settings. /// Window settings.
pub window: WindowSettings, pub window: WindowSettings,
/// Saved dock layout profiles.
#[serde(default)]
pub dock_profiles: Vec<DockProfile>,
} }
/// Tool-specific settings that persist between sessions. /// Tool-specific settings that persist between sessions.
@@ -68,6 +89,7 @@ impl Default for AppSettings {
tool_settings: ToolSettings::default(), tool_settings: ToolSettings::default(),
panel_layout: PanelLayout::default(), panel_layout: PanelLayout::default(),
window: WindowSettings::default(), window: WindowSettings::default(),
dock_profiles: Vec::new(),
} }
} }
} }