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,
/// 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<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.
@@ -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<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.
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<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 ─────────────────────────────────
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);
}