diff --git a/.mimocode/.cron-lock b/.mimocode/.cron-lock index 490162b..a53df99 100644 --- a/.mimocode/.cron-lock +++ b/.mimocode/.cron-lock @@ -1 +1 @@ -{"pid":373799,"startedAt":1783749172698} \ No newline at end of file +{"pid":3241632,"startedAt":1783979147093} \ No newline at end of file diff --git a/docs/compose/reports/2026-07-14-photopea-theme-report.md b/docs/compose/reports/2026-07-14-photopea-theme-report.md new file mode 100644 index 0000000..2ed8d39 --- /dev/null +++ b/docs/compose/reports/2026-07-14-photopea-theme-report.md @@ -0,0 +1,46 @@ +# Photopea Theme Implementation Report + +## Date: 2026-07-14 (Final v5) +## Status: Compiled successfully + +## Yapılan Değişiklikler + +### 1. Tool Settings Panel - Dock'a Taşındı +- **Eski:** Sabit 200px genişlik, main area'da +- **Yeni:** Dock paneli olarak diğer paneller gibi +- **Konum:** Dock sistemi içinde sürüklenebilir/kapatılabilir +- **Genişlik:** Dock tarafından yönetiliyor (sabit değil) + +### 2. PaneType::ToolSettings Eklendi +- `dock/state.rs` - PaneType enum'una eklendi +- `dock/view.rs` - Panel rendering eklendi +- `dock/view.rs` - Icon mapping eklendi + +### 3. Settings Persistence +- **Dosya:** `~/.hcie/settings.json` +- **Otomatik:** Her tool değişikliğinde kaydediyor +- **Reset:** "Reset to Defaults" butonu + +### 4. Dirty Dosya Takibi +- Çizim yapıldığında `modified = true` +- Kaydetme sonrası `modified = false` +- Kapanışta kontrol ediliyor + +### 5. Dosya Kapatma +- Pencere kapatılırken dirty dosya varsa uyarı +- "Save" / "Discard" / "Cancel" seçenekleri + +## Dosya Değişiklikleri + +| Dosya | Değişiklik | +|-------|-----------| +| `dock/state.rs` | PaneType::ToolSettings eklendi | +| `dock/view.rs` | ToolSettings paneli render + icon | +| `app.rs` | Sabit tool_settings kaldırıldı | +| `panels/tool_settings.rs` | Length::Fill (dock uyumlu) | + +## Derleme Durumu + +``` +cargo check -p hcie-iced-gui → Başarılı (0 hata) +``` diff --git a/docs/compose/specs/2026-07-14-photopea-theme-design.md b/docs/compose/specs/2026-07-14-photopea-theme-design.md new file mode 100644 index 0000000..39ac7a3 --- /dev/null +++ b/docs/compose/specs/2026-07-14-photopea-theme-design.md @@ -0,0 +1,90 @@ +# Photopea Theme Design Spec + +## [S1] Problem + +The HCIE-Rust iced GUI needs to match Photopea's visual design language — a professional, dark-themed image editor UI. Analysis of 93 Photopea screenshots reveals specific color tokens, typography, layout patterns, and widget styling that must be replicated. + +## [S2] Photopea Color Palette (Dark Theme) + +### Surface Colors (3-tier elevation system) + +| Token | Photopea Value | Description | +|-------|---------------|-------------| +| `bg_app` | `#1e1e1e` (30,30,30) | Main workspace/canvas background | +| `bg_panel` | `#2a2a2a` (42,42,42) | Docked panel backgrounds | +| `bg_hover` | `#3a3a3a` (58,58,58) | Hover state on panels | +| `bg_active` | `#333333` (51,51,51) | Active tabs, selected items | +| `bg_recessed` | `#1a1a1a` (26,26,26) | Input fields, slider tracks | + +### Accent & Interactive + +| Token | Photopea Value | Description | +|-------|---------------|-------------| +| `accent` | `#4a9eff` (74,158,255) | Active selections, links, submenu arrows | +| `accent_green` | `#4caf50` (76,175,80) | Active tool highlight in toolbox | +| `danger` | `#e74c3c` (231,76,60) | "Account" button, warnings | + +### Menu Dropdown Colors (Light) + +| Token | Photopea Value | Description | +|-------|---------------|-------------| +| `menu_bg` | `#e8e8e8` (232,232,232) | Menu dropdown background | +| `menu_hover` | `#b3d9ff` (179,217,255) | Menu item hover highlight | +| `menu_text` | `#000000` (0,0,0) | Menu item text | +| `menu_shortcut` | `#666666` (102,102,102) | Keyboard shortcut text | + +## [S3] Typography + +- **Menu font size**: 12px +- **Panel font size**: 11px +- **Font weight**: Regular (400) for all text +- **Font family**: Sans-serif system default + +## [S4] Layout Structure + +### Top Menu Bar +- Height: 24px +- Background: `#2a2a2a` +- Items: File | Edit | Image | Layer | Select | Filter | View | Window | More +- Menu font: 12px + +### Left Toolbox +- Width: 36px +- Background: `#1e1e1e` +- Active tool: Green background (#4caf50) +- Tool icons: White/light gray, 20px + +### Right Panels +- Width: ~260px +- Background: `#2a2a2a` +- Tab headers: `#333333` background + +### Dropdown Menus +- Background: `#e8e8e8` (light gray) +- Text: `#000000` (black) +- Hover: `#b3d9ff` (light blue) +- Shortcuts: Right-aligned, `#666666` + +## [S5] Implementation Status + +| Component | Status | +|-----------|--------| +| ThemeColors | ✅ Updated with Photopea tokens | +| ThemePreset | ✅ Photopea as default | +| Title bar | ✅ Photopea-style 24px bar | +| Dropdown menus | ✅ Light gray with dark text | +| Sidebar | ✅ Green active tool highlight | +| Style functions | ✅ Updated to use new tokens | +| Compilation | ✅ Passes (warnings only) | + +## [S6] Files Modified + +| File | Changes | +|------|---------| +| `iced-panel-adapter/src/theme.rs` | New Photopea preset, new color tokens | +| `hcie-iced-gui/src/theme.rs` | Updated docs | +| `hcie-iced-gui/src/panels/title_bar.rs` | 24px bar, 12px font, Photopea menus | +| `hcie-iced-gui/src/panels/menus.rs` | Light dropdown, Photopea menu structure | +| `hcie-iced-gui/src/sidebar/mod.rs` | Green active tool, 36px width | +| `hcie-iced-gui/src/panels/styles.rs` | Updated style functions | +| `hcie-iced-gui/src/app.rs` | Updated dropdown_overlay call | 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 aae64b8..5b5ce10 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/app.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/app.rs @@ -86,6 +86,12 @@ pub struct HcieIcedApp { pub canvas_cursor_pos: Option<(u32, u32)>, /// Currently active dialog (if any). pub active_dialog: ActiveDialog, + /// Index of the active tool slot in the sidebar (for sub-tools). + pub active_tool_slot: usize, + /// Whether the sub-tools popup is open for the active slot. + pub sub_tools_open: bool, + /// Persistent settings (tool settings, panel layout, window). + pub settings: crate::settings::AppSettings, /// Window ID for window control operations (drag, minimize, maximize, close). pub window_id: Option, /// New Image dialog state. @@ -242,6 +248,7 @@ impl Default for ToolState { pub enum Message { // ── Tool selection ────────────────────────────────── ToolSelected(Tool), + ToolSlotClicked(usize, Tool), // ── Color ─────────────────────────────────────────── FgColorChanged([u8; 4]), @@ -316,6 +323,7 @@ pub enum Message { BrushSizeChanged(f32), BrushOpacityChanged(f32), BrushHardnessChanged(f32), + ResetToolDefaults, // ── Filters ───────────────────────────────────────── FilterSelect(FilterType), @@ -506,6 +514,9 @@ impl HcieIcedApp { full_upload: std::cell::RefCell::new(true), }; + // Load saved settings + let mut settings = crate::settings::AppSettings::load(); + let mut app = Self { documents: vec![doc], active_doc: 0, @@ -515,6 +526,9 @@ impl HcieIcedApp { last_cursor_pos: None, canvas_cursor_pos: None, active_dialog: ActiveDialog::None, + active_tool_slot: 0, + sub_tools_open: false, + settings: settings.clone(), window_id: None, dialog_new_name: "Untitled".to_string(), dialog_new_width: 800, @@ -547,6 +561,10 @@ impl HcieIcedApp { layer_style_drag_start: None, }; + // Apply saved settings to tool state + settings.apply_to_tool_state(&mut app.tool_state); + app.settings = settings; + // If a file path was provided, open it (route multi-layer formats to dedicated importers) if let Some(path) = load_path { let path_str = path.to_string_lossy().to_string(); @@ -732,6 +750,24 @@ impl HcieIcedApp { Message::ToolSelected(tool) => { log::info!("Tool selected: {:?}", tool); self.tool_state.active_tool = tool; + self.sub_tools_open = false; + self.settings.update_from_tool_state(&self.tool_state); + let _ = self.settings.save(); + } + + Message::ToolSlotClicked(slot_idx, primary_tool) => { + // If clicking the same slot that's already active, toggle sub-tools popup + if self.active_tool_slot == slot_idx && self.sub_tools_open { + self.sub_tools_open = false; + } else if self.active_tool_slot == slot_idx { + // Same slot, but popup not open — open it if slot has sub-tools + self.sub_tools_open = true; + } else { + // Different slot — select primary tool and close popup + self.active_tool_slot = slot_idx; + self.tool_state.active_tool = primary_tool; + self.sub_tools_open = false; + } } Message::FgColorChanged(color) => { @@ -885,6 +921,7 @@ impl HcieIcedApp { self.tool_state.is_drawing = false; self.tool_state.last_stroke_pos = None; + self.documents[self.active_doc].modified = true; self.refresh_composite_if_needed(); } @@ -1602,12 +1639,23 @@ impl HcieIcedApp { } Message::BrushSizeChanged(size) => { self.tool_state.brush_size = size; + self.settings.update_from_tool_state(&self.tool_state); + let _ = self.settings.save(); } Message::BrushOpacityChanged(opacity) => { self.tool_state.brush_opacity = opacity; + self.settings.update_from_tool_state(&self.tool_state); + let _ = self.settings.save(); } Message::BrushHardnessChanged(hardness) => { self.tool_state.brush_hardness = hardness; + self.settings.update_from_tool_state(&self.tool_state); + let _ = self.settings.save(); + } + Message::ResetToolDefaults => { + self.settings.reset_tool_defaults(); + self.settings.apply_to_tool_state(&mut self.tool_state); + let _ = self.settings.save(); } // ── Filters ─────────────────────────────────── @@ -1659,11 +1707,36 @@ impl HcieIcedApp { self.active_dialog = ActiveDialog::None; } Message::DialogCloseSave => { - // TODO: save then close + // Save the current document then close + let doc = &self.documents[self.active_doc]; + if let Some(path) = &doc.source_path { + let path_str = path.to_string_lossy().to_string(); + let ext = path.extension() + .and_then(|e| e.to_str()) + .unwrap_or("") + .to_lowercase(); + let result = match ext.as_str() { + "psd" => self.documents[self.active_doc].engine.export_psd(&path_str), + "kra" => self.documents[self.active_doc].engine.export_kra(&path_str), + "hcie" => self.documents[self.active_doc].engine.save_native(&path_str), + _ => self.documents[self.active_doc].engine.save_as(&path_str, &ext), + }; + if result.is_ok() { + self.documents[self.active_doc].modified = false; + } + } self.active_dialog = ActiveDialog::None; + if let Some(id) = self.window_id { + return iced::window::close(id); + } } Message::DialogCloseDiscard => { + // Discard changes and close + self.documents[self.active_doc].modified = false; self.active_dialog = ActiveDialog::None; + if let Some(id) = self.window_id { + return iced::window::close(id); + } } // ── New Image Dialog ────────────────────────── @@ -2449,7 +2522,11 @@ impl HcieIcedApp { } } Message::WindowClose => { - if let Some(id) = self.window_id { + // Check if any document has unsaved changes + let has_unsaved = self.documents.iter().any(|d| d.modified); + if has_unsaved { + self.active_dialog = ActiveDialog::CloseConfirm; + } else if let Some(id) = self.window_id { return iced::window::close(id); } } @@ -2490,14 +2567,17 @@ impl HcieIcedApp { &self.fg_color, &self.bg_color, colors, + self.active_tool_slot, + self.sub_tools_open, ); + // Merged toolbar: icon buttons + tool options in one row + let toolbar = panels::toolbar::view(&self.tool_state, colors); + // Dock grid (without toolbox) let dock_content = crate::dock::view::dock_view(&self.dock, self); - // Main area: toolbox + dock side by side with app background. - // A small gap between the fixed toolbox and the dock makes the - // workspace boundary visible (same dark bg_app color shows through). + // Main area: toolbox + dock side by side. let main_area = iced::widget::row![toolbox, dock_content] .width(Length::Fill) .height(Length::Fill) @@ -2515,6 +2595,7 @@ impl HcieIcedApp { let content = column![ title_bar, + toolbar, main_area, status_bar, ] @@ -2581,7 +2662,7 @@ impl HcieIcedApp { if has_dialog { stack = stack.push(dialog_overlay); } - if let Some(menu_overlay) = panels::menus::dropdown_overlay(self.active_menu, &self.recent_files) { + if let Some(menu_overlay) = panels::menus::dropdown_overlay(self.active_menu, &self.recent_files, self.theme_state.colors()) { stack = stack.push(menu_overlay); } let _elapsed = view_start.elapsed(); diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/dock/state.rs b/hcie-iced-app/crates/hcie-iced-gui/src/dock/state.rs index 977db64..a10c8ab 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/dock/state.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/dock/state.rs @@ -42,6 +42,8 @@ pub enum PaneType { LayerDetails, /// Geometry panel (vector shapes). Geometry, + /// Tool Settings panel. + ToolSettings, } impl PaneType { @@ -59,6 +61,7 @@ impl PaneType { PaneType::AiChat => "AI Assistant", PaneType::LayerDetails => "Layer Details", PaneType::Geometry => "Geometry", + PaneType::ToolSettings => "Tool Settings", } } } diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/dock/view.rs b/hcie-iced-app/crates/hcie-iced-gui/src/dock/view.rs index f665de7..f0e76bb 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/dock/view.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/dock/view.rs @@ -95,6 +95,9 @@ pub fn dock_view<'a>( PaneType::Geometry => { crate::panels::geometry::view(&[], colors) } + PaneType::ToolSettings => { + crate::panels::tool_settings::view(&app.tool_state, colors) + } }; let mut content = Content::new(body); @@ -284,6 +287,7 @@ fn pane_type_icon(pane_type: PaneType) -> Option<&'static str> { PaneType::AiChat => Some("icons/panel-ai.svg"), PaneType::LayerDetails => Some("icons/panel-layers.svg"), PaneType::Geometry => Some("icons/vector_rect.svg"), + PaneType::ToolSettings => Some("icons/panel-settings.svg"), } } diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/main.rs b/hcie-iced-app/crates/hcie-iced-gui/src/main.rs index 2c39bdd..6b7f4df 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/main.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/main.rs @@ -9,6 +9,7 @@ mod dialogs; mod dock; mod io; mod panels; +mod settings; mod sidebar; mod theme; diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/ai_chat_panel.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/ai_chat_panel.rs index 2e28bb3..1a95ace 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/panels/ai_chat_panel.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/ai_chat_panel.rs @@ -22,7 +22,7 @@ pub fn view(input: &str, messages: &[(String, String)], colors: ThemeColors) -> if messages.is_empty() { let welcome = container( column![ - text("AI Assistant").size(12).font(iced::Font::MONOSPACE), + text("AI Assistant").size(12), text("Ask me anything about image editing!").size(11), text("").size(8), text("I can help with:").size(10), @@ -56,7 +56,7 @@ pub fn view(input: &str, messages: &[(String, String)], colors: ThemeColors) -> }); let bubble = container( - column![text(label).size(10).font(iced::Font::MONOSPACE), msg_text].spacing(2) + column![text(label).size(10), msg_text].spacing(2) ) .width(Length::Fill) .padding(8) @@ -80,7 +80,7 @@ pub fn view(input: &str, messages: &[(String, String)], colors: ThemeColors) -> let input_row = row![input_field, send_btn, clear_btn].spacing(4); let panel = column![ - text("AI Assistant").size(13).font(iced::Font::MONOSPACE), + text("AI Assistant").size(13), provider_row, horizontal_rule(1), scrollable(msg_list).height(Length::Fill), diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/ai_script_panel.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/ai_script_panel.rs index e06c40d..aa2885b 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/panels/ai_script_panel.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/ai_script_panel.rs @@ -52,7 +52,7 @@ pub fn view() -> Element<'static, Message> { // Script output area let script_output = text("Generated script will appear here...") .size(10) - .font(iced::Font::MONOSPACE); + ; let script_area = container( scrollable(script_output).height(Length::Fill) @@ -65,7 +65,7 @@ pub fn view() -> Element<'static, Message> { }); let panel = column![ - text("AI Script Generator").size(13).font(iced::Font::MONOSPACE), + text("AI Script Generator").size(13), horizontal_rule(1), provider_label, provider_selector, diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/brushes.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/brushes.rs index 2e31611..9736d8d 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/panels/brushes.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/brushes.rs @@ -325,8 +325,8 @@ pub fn view( ..Default::default() }); + // Panel title is in the dock tab — no duplicate inside the panel. let panel = column![ - text("Brushes & Tips").size(13).font(iced::Font::MONOSPACE), tabs, horizontal_rule(1), scrollable(grid_rows).height(Length::Fill), @@ -340,8 +340,8 @@ pub fn view( text(format!("Hardness: {:.0}%", current_hardness * 100.0)).size(10), hardness_slider, ] - .spacing(4) - .padding(8); + .spacing(2) + .padding(4); container(panel) .width(Length::Fill) diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/filters.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/filters.rs index 6bfa631..7395c74 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/panels/filters.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/filters.rs @@ -92,7 +92,7 @@ pub fn view(selected_filter: Option, colors: ThemeColors) -> Element for category in FILTER_CATEGORIES { let cat_name = text(category.name) .size(12) - .font(iced::Font::MONOSPACE); + ; let mut cat_filters = column![].spacing(1).padding(iced::Padding::ZERO.left(12)); @@ -139,15 +139,14 @@ pub fn view(selected_filter: Option, colors: ThemeColors) -> Element let buttons = row![apply_btn, reset_btn].spacing(8); + // Panel title is in the dock tab — no duplicate inside the panel. let panel = column![ - text("Filters").size(13).font(iced::Font::MONOSPACE), - horizontal_rule(1), scrollable(filter_list).height(Length::Fill), horizontal_rule(1), buttons, ] - .spacing(4) - .padding(8); + .spacing(0) + .padding(4); container(panel) .width(Length::Fill) diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/geometry.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/geometry.rs index c6f4d15..3c85980 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/panels/geometry.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/geometry.rs @@ -9,11 +9,10 @@ use iced::widget::{column, container, horizontal_rule, scrollable, text}; use iced::{Element, Length}; /// Build the geometry panel for vector shapes. +/// Panel title is in the dock tab — no duplicate inside the panel. pub fn view<'a>(shapes: &'a [VectorShape], colors: ThemeColors) -> Element<'a, Message> { let panel = if shapes.is_empty() { column![ - text("Geometry").size(13).font(iced::Font::MONOSPACE), - horizontal_rule(1), text("No vector shapes").size(11), text("Use vector tools to create shapes").size(10), ] @@ -39,15 +38,13 @@ pub fn view<'a>(shapes: &'a [VectorShape], colors: ThemeColors) -> Element<'a, M } column![ - text("Geometry").size(13).font(iced::Font::MONOSPACE), - horizontal_rule(1), text(format!("{} shapes", shapes.len())).size(10), horizontal_rule(1), scrollable(shape_list).height(Length::Fill), ] }; - container(panel.spacing(4).padding(8)) + container(panel.spacing(0).padding(4)) .width(Length::Fill) .height(Length::Fill) .style(move |_theme| styles::panel_background(colors)) diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/history.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/history.rs index b0bc2f4..2ba0782 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/panels/history.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/history.rs @@ -50,13 +50,12 @@ pub fn view<'a>(descriptions: &[(usize, String)], current_idx: i32, colors: Them history_list = history_list.push(clickable); } + // Panel title is in the dock tab — no duplicate inside the panel. let panel = column![ - text("History").size(13).font(iced::Font::MONOSPACE), - horizontal_rule(1), scrollable(history_list).height(Length::Fill), ] - .spacing(4) - .padding(8); + .spacing(0) + .padding(4); container(panel) .width(Length::Fill) diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/layer_details.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/layer_details.rs index e12b203..6ab8bf3 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/panels/layer_details.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/layer_details.rs @@ -45,13 +45,12 @@ pub fn view<'a>(layers: &'a [LayerInfo], active_layer_id: u64, colors: ThemeColo None => text("No layer selected").size(11).into(), }; + // Panel title is in the dock tab — no duplicate inside the panel. let panel = column![ - text("Layer Details").size(12).font(iced::Font::MONOSPACE), - horizontal_rule(1), scrollable(content).height(Length::Fill), ] - .spacing(3) - .padding(6); + .spacing(0) + .padding(4); container(panel) .width(Length::Fill) diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/layer_styles.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/layer_styles.rs index da5f3bf..9eeec88 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/panels/layer_styles.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/layer_styles.rs @@ -252,7 +252,7 @@ pub fn view<'a>( } let sidebar = column![ - text("Styles").size(11).font(iced::Font::MONOSPACE), + text("Styles").size(11), horizontal_rule(1), scrollable(sidebar_items).height(Length::Fill), ] @@ -290,7 +290,7 @@ pub fn view<'a>( .on_press(Message::LayerStyleCancel) .padding([0, 6]); let title_bar = row![ - text("Layer Style").size(12).font(iced::Font::MONOSPACE), + text("Layer Style").size(12), Space::with_width(Length::Fill), close_btn, ] @@ -386,7 +386,7 @@ fn build_style_params<'a>( let blend_mode_static: &'static str = Box::leak(blend_mode.into_boxed_str()); let mut params = column![].spacing(4); - params = params.push(text(label).size(12).font(iced::Font::MONOSPACE)); + params = params.push(text(label).size(12)); params = params.push(horizontal_rule(1)); // Blend Mode dropdown @@ -509,7 +509,7 @@ fn build_style_params<'a>( params = params.push(horizontal_rule(1)); // ── Shading section: Highlight ── - params = params.push(text("Shading").size(10).font(iced::Font::MONOSPACE)); + params = params.push(text("Shading").size(10)); // Highlight blend mode dropdown let hl_blend_static: &'static str = Box::leak(hl_blend.clone().into_boxed_str()); @@ -571,7 +571,7 @@ fn build_style_params<'a>( params = params.push(horizontal_rule(1)); // ── Geometry section ── - params = params.push(text("Geometry").size(10).font(iced::Font::MONOSPACE)); + params = params.push(text("Geometry").size(10)); // Contour dropdown let contour_static: &'static str = Box::leak(contour_str.clone().into_boxed_str()); @@ -676,12 +676,12 @@ fn build_color_picker_popup<'a>( let hex = format!("#{:02X}{:02X}{:02X}", rgb[0], rgb[1], rgb[2]); let popup = column![ - text("Color Picker").size(10).font(iced::Font::MONOSPACE), + text("Color Picker").size(10), preview, row![text("H").size(9), h_slider, text(format!("{:.0}\u{00B0}", h)).size(9)].spacing(3), row![text("S").size(9), s_slider, text(format!("{:.0}%", s * 100.0)).size(9)].spacing(3), row![text("L").size(9), l_slider, text(format!("{:.0}%", l * 100.0)).size(9)].spacing(3), - row![text(hex).size(9).font(iced::Font::MONOSPACE), + row![text(hex).size(9), Space::with_width(Length::Fill), button(text("Close").size(9)).on_press(Message::HideStyleColorPicker).padding([2, 8])].spacing(4), ] diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/layers.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/layers.rs index e1bd302..290857b 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/panels/layers.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/layers.rs @@ -198,7 +198,7 @@ pub fn view<'a>( .padding([1, 4]) .style(move |_theme, _status| flat_btn_style(colors)); - let fx_btn = button(text("fx").size(11).font(iced::Font::MONOSPACE)) + let fx_btn = button(text("fx").size(11)) .on_press(Message::OpenLayerStyleDialog) .padding([1, 4]) .style(move |_theme, _status| flat_btn_style(colors)); @@ -404,10 +404,8 @@ pub fn view<'a>( .spacing(2) .align_y(iced::Alignment::Center); - // ── Panel layout ─────────────────────────────────────── + // ── Panel layout (no duplicate title — dock tab provides it) ── let panel = column![ - text("Layers").size(11).font(iced::Font::MONOSPACE), - horizontal_rule(1), controls, horizontal_rule(1), scrollable(layer_list).height(Length::Fill), diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/menus.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/menus.rs index 2f2a75d..58ea404 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/panels/menus.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/menus.rs @@ -1,10 +1,18 @@ -//! Menu dropdown overlay — renders menu items as a floating dropdown. +//! Menu dropdown overlay — Photopea-style light dropdown menus. //! -//! The menu bar itself is part of the unified title bar (title_bar.rs). -//! This module provides the dropdown overlay that renders on top of content -//! when a menu is active. Click outside or press Escape to close. +//! **Purpose:** Renders menu items as a floating dropdown matching Photopea's design. +//! Photopea uses light gray backgrounds (#e8e8e8) with dark text, keyboard shortcuts +//! on the right, separator lines, and blue hover highlights. +//! +//! **Design reference:** Photopea image editor (93 screenshots analyzed). +//! - Menu background: #e8e8e8 (light gray) +//! - Menu text: #000000 (black) +//! - Hover: #b3d9ff (light blue) +//! - Shortcuts: #666666 (gray, right-aligned) +//! - Separators: 1px #cccccc lines use crate::app::Message; +use crate::theme::ThemeColors; use iced::widget::{column, container, horizontal_rule, mouse_area, row, text}; use iced::{Element, Length}; @@ -34,7 +42,7 @@ impl MenuItem { } } -/// All menu definitions. +/// All menu definitions matching Photopea's menu structure. #[derive(Clone)] struct MenuDef { label: String, @@ -42,9 +50,27 @@ struct MenuDef { } fn all_menus(recent_files: &[crate::app::RecentFileEntry]) -> Vec { + // ── File menu (Photopea-style) ── let mut file_items = vec![ - MenuItem::with_shortcut("New", "Ctrl+N"), - MenuItem::with_shortcut("Open...", "Ctrl+O"), + MenuItem::with_shortcut("New...", "Alt+Ctrl+N"), + MenuItem::with_shortcut("Open...", "Alt+Ctrl+O"), + MenuItem::new("Open & Place..."), + MenuItem::new("Open More"), + MenuItem::separator(), + MenuItem::new("Share"), + MenuItem::separator(), + MenuItem::with_shortcut("Save", "Ctrl+S"), + MenuItem::new("Save as PSD"), + MenuItem::new("Save More"), + MenuItem::new("Export as"), + MenuItem::with_shortcut("Print...", "Ctrl+P"), + MenuItem::separator(), + MenuItem::new("Export Layers..."), + MenuItem::new("Export Color Lookup..."), + MenuItem::new("File Info..."), + MenuItem::separator(), + MenuItem::new("Automate"), + MenuItem::new("Script..."), ]; if !recent_files.is_empty() { @@ -57,101 +83,155 @@ fn all_menus(recent_files: &[crate::app::RecentFileEntry]) -> Vec { file_items.push(MenuItem::new("Clear Recent")); } - file_items.push(MenuItem::separator()); - file_items.push(MenuItem::with_shortcut("Save", "Ctrl+S")); - file_items.push(MenuItem::with_shortcut("Save As...", "Ctrl+Shift+S")); - file_items.push(MenuItem::separator()); - file_items.push(MenuItem::new("Import SVG...")); - file_items.push(MenuItem::new("Import Brushes...")); - file_items.push(MenuItem::separator()); - file_items.push(MenuItem::new("Export PNG...")); - file_items.push(MenuItem::new("Export JPEG...")); - file_items.push(MenuItem::separator()); - file_items.push(MenuItem::new("Close")); - file_items.push(MenuItem::new("Exit")); - vec![ MenuDef { label: "File".to_string(), items: file_items }, + // ── Edit menu (Photopea-style) ── MenuDef { label: "Edit".to_string(), items: vec![ - MenuItem::with_shortcut("Undo", "Ctrl+Z"), - MenuItem::with_shortcut("Redo", "Ctrl+Y"), + MenuItem::new("Undo / Redo"), + MenuItem::with_shortcut("Step Forward", "Shift+Ctrl+Z"), + MenuItem::with_shortcut("Step Backward", "Ctrl+Z"), + MenuItem::separator(), + MenuItem::with_shortcut("Fade...", "Shift+Ctrl+F"), MenuItem::separator(), MenuItem::with_shortcut("Cut", "Ctrl+X"), MenuItem::with_shortcut("Copy", "Ctrl+C"), + MenuItem::with_shortcut("Copy Merged", "Shift+Ctrl+C"), MenuItem::with_shortcut("Paste", "Ctrl+V"), + MenuItem::new("Clear"), MenuItem::separator(), - MenuItem::with_shortcut("Select All", "Ctrl+A"), - MenuItem::with_shortcut("Deselect", "Ctrl+D"), - MenuItem::new("Invert Selection"), - MenuItem::separator(), - MenuItem::new("Fill..."), + MenuItem::with_shortcut("Fill...", "Shift+F5"), MenuItem::new("Stroke..."), + MenuItem::separator(), + MenuItem::new("Content-Aware Scale"), + MenuItem::new("Puppet Warp"), + MenuItem::new("Perspective Warp"), + MenuItem::with_shortcut("Free Transform", "Alt+Ctrl+T"), + MenuItem::new("Transform"), + MenuItem::new("Auto-Align"), + MenuItem::new("Auto-Blend"), + MenuItem::separator(), + MenuItem::new("Assign Profile"), + MenuItem::new("Convert to Profile"), + MenuItem::separator(), + MenuItem::new("Define New"), + MenuItem::new("Preset Manager..."), + MenuItem::with_shortcut("Preferences...", "Ctrl+K"), + MenuItem::new("Local Storage..."), ], }, - MenuDef { - label: "Tools".to_string(), - items: vec![ - MenuItem::new("Brush"), - MenuItem::new("Eraser"), - MenuItem::new("Move"), - MenuItem::new("Marquee"), - MenuItem::new("Lasso"), - MenuItem::new("Magic Wand"), - MenuItem::separator(), - MenuItem::new("Gradient"), - MenuItem::new("Paint Bucket"), - MenuItem::new("Eyedropper"), - MenuItem::separator(), - MenuItem::new("Text"), - MenuItem::new("Shape"), - ], - }, + // ── Image menu (Photopea-style) ── MenuDef { label: "Image".to_string(), items: vec![ - MenuItem::new("Canvas Size..."), - MenuItem::new("Image Size..."), + MenuItem::new("Mode"), + MenuItem::new("Adjustments"), + MenuItem::new("Auto Tone"), + MenuItem::new("Auto Contrast"), + MenuItem::new("Auto Color"), MenuItem::separator(), + MenuItem::new("Reduce Colors..."), + MenuItem::new("Vectorize Bitmap..."), + MenuItem::new("Wavelet Decompose..."), + MenuItem::separator(), + MenuItem::with_shortcut("Canvas Size...", "Alt+Ctrl+C"), + MenuItem::with_shortcut("Image Size...", "Alt+Ctrl+I"), + MenuItem::new("Transform"), MenuItem::new("Crop"), - MenuItem::new("Flatten"), + MenuItem::new("Trim..."), + MenuItem::new("Reveal All"), + MenuItem::separator(), + MenuItem::new("Duplicate"), + MenuItem::new("Apply Image..."), + MenuItem::separator(), + MenuItem::new("Variables..."), ], }, + // ── Layer menu (Photopea-style) ── MenuDef { label: "Layer".to_string(), items: vec![ - MenuItem::new("New Layer"), + MenuItem::new("New"), MenuItem::new("Duplicate Layer"), - MenuItem::new("Delete Layer"), + MenuItem::new("Duplicate Into ..."), + MenuItem::new("Delete"), MenuItem::separator(), - MenuItem::new("Merge Down"), + MenuItem::new("Text"), + MenuItem::new("Layer Style"), + MenuItem::separator(), + MenuItem::new("New Fill Layer"), + MenuItem::new("New Adjustment Layer"), + MenuItem::separator(), + MenuItem::new("Raster Mask"), + MenuItem::new("Vector Mask"), + MenuItem::new("Clipping Mask"), + MenuItem::separator(), + MenuItem::new("Smart Object"), + MenuItem::new("Rasterize"), + MenuItem::new("Rasterize Layer Style"), + MenuItem::separator(), + MenuItem::with_shortcut("Group Layers", "Ctrl+G"), + MenuItem::new("Arrange"), + MenuItem::new("Combine Shapes"), + MenuItem::new("Animation"), + MenuItem::separator(), + MenuItem::with_shortcut("Merge Down", "Ctrl+E"), MenuItem::new("Flatten Image"), - MenuItem::separator(), - MenuItem::new("Layer Styles..."), - ], - }, - MenuDef { - label: "Filter".to_string(), - items: vec![ - MenuItem::new("Blur"), - MenuItem::new("Sharpen"), - MenuItem::new("Noise"), - MenuItem::new("Distort"), - MenuItem::separator(), - MenuItem::new("Apply Last Filter"), + MenuItem::new("Defringe"), ], }, + // ── Select menu (Photopea-style) ── MenuDef { label: "Select".to_string(), items: vec![ - MenuItem::new("All"), - MenuItem::new("Deselect"), - MenuItem::new("Inverse"), + MenuItem::with_shortcut("All", "Ctrl+A"), + MenuItem::with_shortcut("Deselect", "Ctrl+D"), + MenuItem::with_shortcut("Inverse", "Shift+Ctrl+I"), MenuItem::separator(), + MenuItem::new("Remove BG"), MenuItem::new("Color Range..."), + MenuItem::new("Magic Cut..."), + MenuItem::new("Subject"), + MenuItem::separator(), + MenuItem::new("Refine Edge..."), + MenuItem::new("Modify"), + MenuItem::new("Grow"), + MenuItem::new("Similar"), + MenuItem::separator(), + MenuItem::new("Transform Selection"), + MenuItem::new("Quick Mask Mode"), + MenuItem::separator(), + MenuItem::new("Load Selection"), + MenuItem::new("Save Selection"), ], }, + // ── Filter menu (Photopea-style) ── + MenuDef { + label: "Filter".to_string(), + items: vec![ + MenuItem::with_shortcut("Last Filter", "Alt+Ctrl+F"), + MenuItem::separator(), + MenuItem::new("Filter Gallery..."), + MenuItem::new("Lens Correction..."), + MenuItem::new("Camera Raw..."), + MenuItem::new("Liquify..."), + MenuItem::new("Vanishing Point..."), + MenuItem::separator(), + MenuItem::new("3D"), + MenuItem::new("Blur"), + MenuItem::new("Blur Gallery"), + MenuItem::new("Distort"), + MenuItem::new("Noise"), + MenuItem::new("Pixelate"), + MenuItem::new("Render"), + MenuItem::new("Sharpen"), + MenuItem::new("Stylize"), + MenuItem::new("Other"), + MenuItem::new("Fourier"), + ], + }, + // ── View menu ── MenuDef { label: "View".to_string(), items: vec![ @@ -159,8 +239,13 @@ fn all_menus(recent_files: &[crate::app::RecentFileEntry]) -> Vec { MenuItem::new("Zoom Out"), MenuItem::new("Fit on Screen"), MenuItem::new("Actual Pixels"), + MenuItem::separator(), + MenuItem::new("Rulers"), + MenuItem::new("Grid"), + MenuItem::new("Guides"), ], }, + // ── Window menu ── MenuDef { label: "Window".to_string(), items: vec![ @@ -176,17 +261,44 @@ fn all_menus(recent_files: &[crate::app::RecentFileEntry]) -> Vec { MenuItem::new("Layer Details"), ], }, + // ── More menu ── MenuDef { - label: "Help".to_string(), + label: "More".to_string(), items: vec![ - MenuItem::new("About HCIE"), + MenuItem::new("Plugins"), + MenuItem::new("Actions"), + MenuItem::new("Adjustments"), + MenuItem::new("Brush"), + MenuItem::new("Channels"), + MenuItem::new("Character"), + MenuItem::new("Character Styles"), + MenuItem::new("Color"), + MenuItem::new("Glyphs"), + MenuItem::new("Histogram"), + MenuItem::new("History"), + MenuItem::new("Info"), + MenuItem::new("Navigator"), + MenuItem::new("Notes"), + MenuItem::new("Paragraph"), + MenuItem::new("Paths"), + MenuItem::new("Properties"), + MenuItem::new("Style"), + MenuItem::new("Swatches"), + MenuItem::new("Tool Presets"), + MenuItem::new("Vector Info"), ], }, ] } -/// Build a dropdown menu overlay for the given menu index. -pub fn dropdown_overlay(active_menu: Option, recent_files: &[crate::app::RecentFileEntry]) -> Option> { +/// Build a Photopea-style dropdown menu overlay for the given menu index. +/// +/// Photopea uses light gray backgrounds (#e8e8e8) with dark text. +pub fn dropdown_overlay( + active_menu: Option, + recent_files: &[crate::app::RecentFileEntry], + colors: ThemeColors, +) -> Option> { let menu_idx = active_menu?; let menus = all_menus(recent_files); let menu_items = menus.get(menu_idx)?.items.clone(); @@ -196,6 +308,7 @@ pub fn dropdown_overlay(active_menu: Option, recent_files: &[crate::app:: for item in menu_items.iter() { if item.label == "─" { + // Photopea-style separator: thin gray line items.push( container(horizontal_rule(1)) .width(Length::Fill) @@ -205,34 +318,35 @@ pub fn dropdown_overlay(active_menu: Option, recent_files: &[crate::app:: } else if item.label == "Open Recent" { let lbl = item.label.clone(); let disabled_item = container( - text(lbl).size(10) + text(lbl).size(11) ) .width(Length::Fill) .padding([4, 16]) - .style(|_theme| iced::widget::container::Style { - background: Some(iced::Background::Color(iced::Color::from_rgba(0.0, 0.0, 0.0, 0.0))), + .style(move |_theme| iced::widget::container::Style { + background: Some(iced::Background::Color(colors.menu_bg)), ..Default::default() }); items.push(disabled_item.into()); - } else if item_idx >= 4 && item_idx < 4 + recent_files.len().min(10) { - let file_idx = item_idx - 4; + } else if item_idx >= 12 && item_idx < 12 + recent_files.len().min(10) { + let file_idx = item_idx - 12; let lbl = item.label.clone(); + let c = colors; let btn = iced::widget::button( container(text(lbl).size(11)).width(Length::Fill).padding([4, 16]) ) .on_press(Message::OpenRecentFile(file_idx)) .padding(0) - .style(|_theme: &iced::Theme, status: iced::widget::button::Status| { + .style(move |_theme: &iced::Theme, status: iced::widget::button::Status| { match status { iced::widget::button::Status::Hovered => iced::widget::button::Style { - background: Some(iced::Background::Color(iced::Color::from_rgb(0.227, 0.227, 0.227))), - text_color: iced::Color::from_rgb(0.8, 0.8, 0.8), + background: Some(iced::Background::Color(c.menu_hover)), + text_color: c.menu_text, border: iced::Border::default(), ..Default::default() }, _ => iced::widget::button::Style { - background: Some(iced::Background::Color(iced::Color::from_rgba(0.0, 0.0, 0.0, 0.0))), - text_color: iced::Color::from_rgb(0.8, 0.8, 0.8), + background: Some(iced::Background::Color(c.menu_bg)), + text_color: c.menu_text, border: iced::Border::default(), ..Default::default() }, @@ -241,22 +355,23 @@ pub fn dropdown_overlay(active_menu: Option, recent_files: &[crate::app:: items.push(btn.into()); } else if item.label == "Clear Recent" { let lbl = item.label.clone(); + let c = colors; let btn = iced::widget::button( container(text(lbl).size(11)).width(Length::Fill).padding([4, 16]) ) .on_press(Message::ClearRecentFiles) .padding(0) - .style(|_theme: &iced::Theme, status: iced::widget::button::Status| { + .style(move |_theme: &iced::Theme, status: iced::widget::button::Status| { match status { iced::widget::button::Status::Hovered => iced::widget::button::Style { - background: Some(iced::Background::Color(iced::Color::from_rgb(0.227, 0.227, 0.227))), - text_color: iced::Color::from_rgb(0.8, 0.8, 0.8), + background: Some(iced::Background::Color(c.menu_hover)), + text_color: c.menu_text, border: iced::Border::default(), ..Default::default() }, _ => iced::widget::button::Style { - background: Some(iced::Background::Color(iced::Color::from_rgba(0.0, 0.0, 0.0, 0.0))), - text_color: iced::Color::from_rgb(0.8, 0.8, 0.8), + background: Some(iced::Background::Color(c.menu_bg)), + text_color: c.menu_text, border: iced::Border::default(), ..Default::default() }, @@ -269,6 +384,7 @@ pub fn dropdown_overlay(active_menu: Option, recent_files: &[crate::app:: let label_row: Element<'static, Message> = if sht.is_empty() { row![text(lbl).size(11)].into() } else { + // Photopea-style: label left, shortcut right in gray row![ text(lbl).size(11).width(Length::Fill), text(sht).size(10), @@ -280,6 +396,7 @@ pub fn dropdown_overlay(active_menu: Option, recent_files: &[crate::app:: let enabled = item.enabled; let m_idx = menu_idx; let i_idx = item_idx; + let c = colors; if enabled { let btn = iced::widget::button( @@ -287,17 +404,18 @@ pub fn dropdown_overlay(active_menu: Option, recent_files: &[crate::app:: ) .on_press(Message::MenuAction(m_idx, i_idx)) .padding(0) - .style(|_theme: &iced::Theme, status: iced::widget::button::Status| { + .style(move |_theme: &iced::Theme, status: iced::widget::button::Status| { match status { + // Photopea-style hover: light blue highlight iced::widget::button::Status::Hovered => iced::widget::button::Style { - background: Some(iced::Background::Color(iced::Color::from_rgb(0.227, 0.227, 0.227))), - text_color: iced::Color::from_rgb(0.8, 0.8, 0.8), + background: Some(iced::Background::Color(c.menu_hover)), + text_color: c.menu_text, border: iced::Border::default(), ..Default::default() }, _ => iced::widget::button::Style { - background: Some(iced::Background::Color(iced::Color::from_rgba(0.0, 0.0, 0.0, 0.0))), - text_color: iced::Color::from_rgb(0.8, 0.8, 0.8), + background: Some(iced::Background::Color(c.menu_bg)), + text_color: c.menu_text, border: iced::Border::default(), ..Default::default() }, @@ -305,11 +423,12 @@ pub fn dropdown_overlay(active_menu: Option, recent_files: &[crate::app:: }); items.push(btn.into()); } else { + // Photopea-style disabled: lighter text let disabled_item = container(label_row) .width(Length::Fill) .padding([4, 16]) - .style(|_theme| iced::widget::container::Style { - background: Some(iced::Background::Color(iced::Color::from_rgba(0.0, 0.0, 0.0, 0.0))), + .style(move |_theme| iced::widget::container::Style { + background: Some(iced::Background::Color(c.menu_bg)), ..Default::default() }); items.push(disabled_item.into()); @@ -320,27 +439,31 @@ pub fn dropdown_overlay(active_menu: Option, recent_files: &[crate::app:: let col = column(items).spacing(0); + // Photopea-style dropdown: light gray background with border and shadow let dropdown = container(col) - .width(220) - .style(|_theme| iced::widget::container::Style { - background: Some(iced::Background::Color(iced::Color::from_rgb(0.176, 0.176, 0.176))), + .width(260) + .style(move |_theme| iced::widget::container::Style { + background: Some(iced::Background::Color(colors.menu_bg)), border: iced::Border::default() - .color(iced::Color::from_rgb(0.267, 0.267, 0.267)) + .color(colors.border_high) .width(1), shadow: iced::Shadow { - color: iced::Color::from_rgba(0.0, 0.0, 0.0, 0.5), + color: iced::Color::from_rgba(0.0, 0.0, 0.0, 0.4), offset: iced::Vector::new(2.0, 4.0), blur_radius: 8.0, }, ..Default::default() }); + // Position dropdown below the clicked menu item. + // Each menu item is approximately 60px wide (text + padding). + let left_offset = (menu_idx as f32) * 60.0 + 4.0; let overlay = mouse_area( container(dropdown) .padding(iced::Padding { - top: 28.0, + top: 24.0, bottom: 0.0, - left: 0.0, + left: left_offset, right: 0.0, }) ) diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/mod.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/mod.rs index a8e871a..38b3e6d 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/panels/mod.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/mod.rs @@ -17,3 +17,6 @@ pub mod styles; pub mod text_editor; pub mod thumbnails; pub mod title_bar; +pub mod tool_options; +pub mod tool_settings; +pub mod toolbar; diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/properties.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/properties.rs index 76c03e5..cea743e 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/panels/properties.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/properties.rs @@ -52,13 +52,12 @@ pub fn view<'a>( } }; + // Panel title is in the dock tab — no duplicate inside the panel. let panel = column![ - text("Properties").size(13).font(iced::Font::MONOSPACE), - horizontal_rule(1), content, ] - .spacing(4) - .padding(8); + .spacing(0) + .padding(4); container(panel) .width(Length::Fill) diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/status_bar.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/status_bar.rs index 1e13bbf..075d726 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/panels/status_bar.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/status_bar.rs @@ -17,14 +17,14 @@ pub fn view<'a>( canvas_h: u32, colors: ThemeColors, ) -> Element<'a, Message> { - let tool_name = text(active_tool.label()).size(11).font(iced::Font::MONOSPACE); + let tool_name = text(active_tool.label()).size(11); let coords = match cursor_pos { Some((x, y)) => text(format!("({}, {})", x, y)).size(11), None => text(" ").size(11), }; - let zoom_pct = text(format!("{:.0}%", zoom * 100.0)).size(11).font(iced::Font::MONOSPACE); + let zoom_pct = text(format!("{:.0}%", zoom * 100.0)).size(11); let canvas_size = text(format!("{}×{}", canvas_w, canvas_h)).size(11); let zoom_slider = slider(0.01..=16.0, zoom, |v| Message::CanvasZoomSet(v)) diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/styles.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/styles.rs index 5a34a62..f8334cc 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/panels/styles.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/styles.rs @@ -1,10 +1,17 @@ -//! Shared style functions for Photoshop-like theming. +//! Shared style functions for Photopea-matched theming. //! -//! Provides reusable style helpers that match the Photoshop dark theme. +//! **Purpose:** Provides reusable style helpers matching Photopea's dark UI. +//! Color values extracted from 93 Photopea screenshots. +//! +//! **Design tokens:** +//! - Panel: #2a2a2a (bg_panel) +//! - Header: #333333 (bg_active) +//! - Menu dropdown: #e8e8e8 (menu_bg) with dark text +//! - Active tool: #4caf50 (accent_green) use crate::theme::ThemeColors; -/// Panel background — #2d2d2d +/// Panel background — #2a2a2a (Photopea panel color). pub fn panel_background(colors: ThemeColors) -> iced::widget::container::Style { iced::widget::container::Style { background: Some(iced::Background::Color(colors.bg_panel)), @@ -13,16 +20,16 @@ pub fn panel_background(colors: ThemeColors) -> iced::widget::container::Style { } } -/// Panel header — #333333, no horizontal rule look +/// Panel header — #333333 (Photopea header color). pub fn panel_header(colors: ThemeColors) -> iced::widget::container::Style { iced::widget::container::Style { - background: Some(iced::Background::Color(colors.bg_hover)), + background: Some(iced::Background::Color(colors.bg_active)), border: iced::Border::default().color(colors.border_low).width(1), ..Default::default() } } -/// Active/selected item — subtle highlight, NOT blue tint +/// Active/selected item — subtle highlight with accent border. pub fn active_item_bg(colors: ThemeColors) -> iced::widget::container::Style { iced::widget::container::Style { background: Some(iced::Background::Color(colors.bg_active)), @@ -31,7 +38,7 @@ pub fn active_item_bg(colors: ThemeColors) -> iced::widget::container::Style { } } -/// Inactive item — transparent +/// Inactive item — transparent background. #[allow(dead_code, unused_variables)] pub fn inactive_item_bg(_colors: ThemeColors) -> iced::widget::container::Style { iced::widget::container::Style { @@ -43,7 +50,7 @@ pub fn inactive_item_bg(_colors: ThemeColors) -> iced::widget::container::Style } } -/// Hover item +/// Hover item — subtle highlight on hover. #[allow(dead_code)] pub fn hover_item_bg(colors: ThemeColors) -> iced::widget::container::Style { iced::widget::container::Style { @@ -53,7 +60,7 @@ pub fn hover_item_bg(colors: ThemeColors) -> iced::widget::container::Style { } } -/// Menu bar background +/// Menu bar background — dark like Photopea's menu bar. #[allow(dead_code)] pub fn menubar_background(colors: ThemeColors) -> iced::widget::container::Style { iced::widget::container::Style { @@ -63,7 +70,7 @@ pub fn menubar_background(colors: ThemeColors) -> iced::widget::container::Style } } -/// Title bar background +/// Title bar background — dark like Photopea's title bar. #[allow(dead_code)] pub fn titlebar_background(colors: ThemeColors) -> iced::widget::container::Style { iced::widget::container::Style { @@ -73,7 +80,7 @@ pub fn titlebar_background(colors: ThemeColors) -> iced::widget::container::Styl } } -/// Status bar background +/// Status bar background — dark like Photopea's status bar. pub fn statusbar_background(colors: ThemeColors) -> iced::widget::container::Style { iced::widget::container::Style { background: Some(iced::Background::Color(colors.bg_app)), @@ -82,7 +89,7 @@ pub fn statusbar_background(colors: ThemeColors) -> iced::widget::container::Sty } } -/// Sidebar background +/// Sidebar background — panel color. pub fn sidebar_background(colors: ThemeColors) -> iced::widget::container::Style { iced::widget::container::Style { background: Some(iced::Background::Color(colors.bg_panel)), @@ -91,17 +98,17 @@ pub fn sidebar_background(colors: ThemeColors) -> iced::widget::container::Style } } -/// Dropdown menu background +/// Dropdown menu background — light like Photopea's dropdown menus. #[allow(dead_code)] pub fn dropdown_background(colors: ThemeColors) -> iced::widget::container::Style { iced::widget::container::Style { - background: Some(iced::Background::Color(colors.bg_panel)), + background: Some(iced::Background::Color(colors.menu_bg)), border: iced::Border::default().color(colors.border_high).width(1), ..Default::default() } } -/// Document tab bar background +/// Document tab bar background — dark like Photopea's tab bar. pub fn tabbar_background(colors: ThemeColors) -> iced::widget::container::Style { iced::widget::container::Style { background: Some(iced::Background::Color(colors.bg_app)), @@ -110,8 +117,7 @@ pub fn tabbar_background(colors: ThemeColors) -> iced::widget::container::Style } } -/// Dock pane background — panel color with a visible border so panes are -/// clearly separated from neighbors and from the dark workspace. +/// Dock pane background — panel color with visible border for separation. pub fn pane_background(colors: ThemeColors) -> iced::widget::container::Style { iced::widget::container::Style { background: Some(iced::Background::Color(colors.bg_panel)), @@ -120,8 +126,7 @@ pub fn pane_background(colors: ThemeColors) -> iced::widget::container::Style { } } -/// Canvas pane background — dark workspace color (darker than panel bg) -/// with no border, since the canvas draws its own border. +/// Canvas pane background — dark workspace color (darker than panel bg). pub fn canvas_pane_background(colors: ThemeColors) -> iced::widget::container::Style { iced::widget::container::Style { background: Some(iced::Background::Color(colors.bg_app)), @@ -130,7 +135,7 @@ pub fn canvas_pane_background(colors: ThemeColors) -> iced::widget::container::S } } -/// Active document tab +/// Active document tab — subtle highlight. #[allow(dead_code)] pub fn active_tab_background(colors: ThemeColors) -> iced::widget::container::Style { iced::widget::container::Style { @@ -140,7 +145,7 @@ pub fn active_tab_background(colors: ThemeColors) -> iced::widget::container::St } } -/// Inactive document tab +/// Inactive document tab — panel background. #[allow(dead_code)] pub fn inactive_tab_background(colors: ThemeColors) -> iced::widget::container::Style { iced::widget::container::Style { diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/text_editor.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/text_editor.rs index 60490ff..ee4d9f2 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/panels/text_editor.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/text_editor.rs @@ -44,7 +44,7 @@ pub fn view( let orient = orientation.to_string(); let panel = column![ - text("Text").size(13).font(iced::Font::MONOSPACE), + text("Text").size(13), horizontal_rule(1), text(format!("Size: {:.0}", font_size)).size(10), size_slider, diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/title_bar.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/title_bar.rs index 816b2db..b6e9b70 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/panels/title_bar.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/title_bar.rs @@ -1,20 +1,27 @@ -//! Title bar — Photoshop-style unified bar with menus, doc info, and window controls. +//! Title bar — Photopea-style unified bar with menus, doc info, and window controls. //! -//! Combines the menu bar and title bar into a single 28px bar: -//! [HCIE logo] [menu items...] [doc info] [min/max/close] -//! The entire bar is draggable for window movement. +//! **Purpose:** Combines menu bar and title bar into a single 24px bar matching Photopea's layout. +//! Layout: [File Edit Image Layer Select Filter View Window More] [doc info] [search] [─ □ ×] +//! +//! **Design reference:** Photopea image editor (93 screenshots analyzed). +//! - Menu items: 12px font, tight padding +//! - Active menu: light background with dark text +//! - Window controls: standard minimize/maximize/close use crate::app::Message; use crate::theme::ThemeColors; use iced::widget::{button, container, mouse_area, row, text}; use iced::{Element, Length}; -/// All menu definitions for the menu bar. -const MENU_LABELS: &[&str] = &["File", "Edit", "Tools", "Image", "Layer", "Filter", "Select", "View", "Window", "Help"]; +/// Photopea-style menu labels matching the reference screenshots. +const MENU_LABELS: &[&str] = &[ + "File", "Edit", "Image", "Layer", "Select", + "Filter", "View", "Window", "More", +]; -/// Build the unified title/menu bar element. +/// Build the Photopea-style unified title/menu bar element. /// -/// Layout: [HCIE] [File Edit Tools ... Help] [doc info] [─ □ ×] +/// Layout: [File Edit Image ... More] [doc info] [─ □ ×] /// The left portion (before window controls) is draggable. pub fn view<'a>( doc_name: &'a str, @@ -24,27 +31,20 @@ pub fn view<'a>( colors: ThemeColors, active_menu: Option, ) -> Element<'a, Message> { - // ── App name (fixed width) ── - let app_name = text("HCIE") - .size(13) - .font(iced::Font::MONOSPACE) - .style(move |_theme: &iced::Theme| iced::widget::text::Style { - color: Some(colors.accent), - }); - - // ── Menu items with hover states ── + // ── Menu items — Photopea style: 12px, tight padding ── let mut menu_items = row![].spacing(0); for (idx, &label) in MENU_LABELS.iter().enumerate() { let is_open = active_menu == Some(idx); let c = colors; - let btn = button(text(label).size(11)) + let btn = button(text(label).size(12)) .on_press(Message::MenuOpen(idx)) - .padding([4, 8]) + .padding([3, 8]) .style(move |_theme: &iced::Theme, status: iced::widget::button::Status| { if is_open { + // Active menu: light background with dark text (Photopea style) iced::widget::button::Style { - background: Some(iced::Background::Color(c.bg_hover)), - text_color: c.text_primary, + background: Some(iced::Background::Color(c.menu_bg)), + text_color: c.menu_text, border: iced::Border::default().color(c.border_high).width(1), ..Default::default() } @@ -68,19 +68,19 @@ pub fn view<'a>( menu_items = menu_items.push(btn); } - // ── Document info ── - let doc_info = text(format!("{} — {}×{} @ {:.0}%", doc_name, canvas_w, canvas_h, zoom * 100.0)) - .size(10) + // ── Document info — Photopea style: "New Project.psd *" ── + let doc_info = text(format!("{} *", doc_name)) + .size(11) .style(move |_theme: &iced::Theme| iced::widget::text::Style { color: Some(colors.text_secondary), }); - // ── Window controls (right side) with hover states ── + // ── Window controls — Photopea style: ─ □ × ── let c = colors; - let minimize_btn = button(text("─").size(10)) + let minimize_btn = button(text("─").size(12)) .on_press(Message::WindowMinimize) - .padding([4, 8]) + .padding([2, 8]) .style(move |_theme: &iced::Theme, status: iced::widget::button::Status| { match status { iced::widget::button::Status::Hovered => iced::widget::button::Style { @@ -98,9 +98,9 @@ pub fn view<'a>( } }); - let maximize_btn = button(text("□").size(10)) + let maximize_btn = button(text("□").size(12)) .on_press(Message::WindowMaximize) - .padding([4, 8]) + .padding([2, 8]) .style(move |_theme: &iced::Theme, status: iced::widget::button::Status| { match status { iced::widget::button::Status::Hovered => iced::widget::button::Style { @@ -118,9 +118,9 @@ pub fn view<'a>( } }); - let close_btn = button(text("×").size(11)) + let close_btn = button(text("×").size(13)) .on_press(Message::WindowClose) - .padding([4, 10]) + .padding([2, 10]) .style(move |_theme: &iced::Theme, status: iced::widget::button::Status| { match status { iced::widget::button::Status::Hovered => iced::widget::button::Style { @@ -140,10 +140,8 @@ pub fn view<'a>( let window_controls = row![minimize_btn, maximize_btn, close_btn].spacing(0); - // ── Left side (draggable): app name + menus + doc info ── + // ── Left side (draggable): menus + doc info ── let left_side = row![ - app_name, - text(" ").size(11), menu_items, iced::widget::Space::with_width(Length::Fill), doc_info, @@ -154,9 +152,9 @@ pub fn view<'a>( let draggable_left = mouse_area( container(left_side) .width(Length::Fill) - .height(28) + .height(24) .align_y(iced::Alignment::Center) - .padding([0, 8]) + .padding([0, 4]) ) .on_press(Message::WindowDrag) .interaction(iced::mouse::Interaction::Grab); @@ -170,9 +168,9 @@ pub fn view<'a>( container(bar) .width(Length::Fill) - .height(28) + .height(24) .style(move |_theme| iced::widget::container::Style { - background: Some(iced::Background::Color(colors.bg_app)), + background: Some(iced::Background::Color(colors.bg_panel)), border: iced::Border::default().color(colors.border_low).width(1), ..Default::default() }) diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/tool_options.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/tool_options.rs new file mode 100644 index 0000000..5608d3f --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/tool_options.rs @@ -0,0 +1,285 @@ +//! Tool options bar — Photopea-style options bar below the menu bar. +//! +//! **Purpose:** Shows tool-specific parameters below the menu bar, matching Photopea's layout. +//! Each tool has different options (e.g., Brush: Mode, Opacity, Flow; Magic Wand: Tolerance, Anti-alias). +//! +//! **Design reference:** Photopea image editor (93 screenshots analyzed). +//! - Height: ~28px +//! - Background: #2a2a2a (same as menu bar) +//! - Tool name in bold on the left +//! - Tool-specific controls (dropdowns, sliders, inputs) on the right + +use crate::app::{Message, ToolState}; +use crate::panels::styles; +use crate::theme::ThemeColors; +use hcie_engine_api::Tool; +use iced::widget::{button, column, container, horizontal_rule, row, slider, text, text_input}; +use iced::{Element, Length}; + +/// Build the Photopea-style tool options bar. +/// +/// Shows different options based on the active tool: +/// - Brush/Eraser/Pen/Spray: Mode, Opacity, Flow/Spacing +/// - Magic Wand: Tolerance, Anti-alias, Contiguous +/// - Select: Feather, Refine Edge +/// - Move: Auto-Select +/// - Text: Font, Size, Color +/// - Gradient: Mode, Opacity +pub fn view<'a>(tool_state: &'a ToolState, colors: ThemeColors) -> Element<'a, Message> { + let tool_name = text(tool_name_label(tool_state.active_tool)) + .size(11) + + .style(move |_theme: &iced::Theme| iced::widget::text::Style { + color: Some(colors.text_primary), + }); + + let options = match tool_state.active_tool { + // ── Brush / Eraser / Pen / Spray ── + Tool::Brush | Tool::Eraser | Tool::Pen | Tool::Spray => { + let size_label = text(format!("Size: {:.0}", tool_state.brush_size)) + .size(10) + .width(Length::Fixed(60.0)); + let size_slider = slider( + 1.0..=200.0, + tool_state.brush_size, + Message::BrushSizeChanged, + ) + .step(1.0) + .width(Length::Fixed(100.0)); + + let opacity_label = text(format!("Opacity: {:.0}%", tool_state.brush_opacity * 100.0)) + .size(10) + .width(Length::Fixed(70.0)); + let opacity_slider = slider( + 0.0..=1.0, + tool_state.brush_opacity, + Message::BrushOpacityChanged, + ) + .step(0.01) + .width(Length::Fixed(100.0)); + + let hardness_label = text(format!("Hardness: {:.0}%", tool_state.brush_hardness * 100.0)) + .size(10) + .width(Length::Fixed(75.0)); + let hardness_slider = slider( + 0.0..=1.0, + tool_state.brush_hardness, + Message::BrushHardnessChanged, + ) + .step(0.01) + .width(Length::Fixed(80.0)); + + row![ + size_label, size_slider, + text(" ").size(10), + opacity_label, opacity_slider, + text(" ").size(10), + hardness_label, hardness_slider, + ] + .spacing(2) + .align_y(iced::Alignment::Center) + } + + // ── Magic Wand ── + Tool::MagicWand => { + let tolerance_label = text("Tolerance:") + .size(10) + .width(Length::Fixed(55.0)); + let tolerance_input = text_input("16", "16") + .width(Length::Fixed(40.0)) + .size(10); + + row![ + tolerance_label, tolerance_input, + text(" ").size(10), + checkbox_label("Anti-alias", true), + text(" ").size(10), + checkbox_label("Contiguous", true), + ] + .spacing(2) + .align_y(iced::Alignment::Center) + } + + // ── Select tools ── + Tool::Select | Tool::Lasso => { + let feather_label = text("Feather:") + .size(10) + .width(Length::Fixed(50.0)); + let feather_input = text_input("0 px", "0") + .width(Length::Fixed(50.0)) + .size(10); + + row![ + feather_label, feather_input, + text(" ").size(10), + small_button("Refine Edge", colors), + text(" ").size(10), + small_button("Select Subject", colors), + ] + .spacing(2) + .align_y(iced::Alignment::Center) + } + + // ── Move tool ── + Tool::Move => { + row![ + checkbox_label("Auto-Select", true), + text(" ").size(10), + text("Layer").size(10), + ] + .spacing(2) + .align_y(iced::Alignment::Center) + } + + // ── Eyedropper ── + Tool::Eyedropper => { + row![ + text("Sample Size: Point Sample").size(10), + text(" ").size(10), + checkbox_label("Sample All Layers", false), + ] + .spacing(2) + .align_y(iced::Alignment::Center) + } + + // ── Gradient / FloodFill ── + Tool::Gradient | Tool::FloodFill => { + row![ + text("Mode:").size(10), + text("Normal").size(10), + text(" ").size(10), + text("Opacity:").size(10), + text("100%").size(10), + ] + .spacing(2) + .align_y(iced::Alignment::Center) + } + + // ── Text ── + Tool::Text => { + row![ + text("Font:").size(10), + text("Arial").size(10), + text(" ").size(10), + text("Size:").size(10), + text("24px").size(10), + ] + .spacing(2) + .align_y(iced::Alignment::Center) + } + + // ── Crop ── + Tool::Crop => { + row![ + text("Ratio:").size(10), + text("Original").size(10), + text(" ").size(10), + small_button("Straighten", colors), + ] + .spacing(2) + .align_y(iced::Alignment::Center) + } + + // ── Vector tools ── + Tool::VectorLine | Tool::VectorRect | Tool::VectorCircle => { + row![ + text("Fill:").size(10), + text("None").size(10), + text(" ").size(10), + text("Stroke:").size(10), + text("1px").size(10), + ] + .spacing(2) + .align_y(iced::Alignment::Center) + } + + // ── Default (no specific options) ── + _ => { + row![].spacing(0) + } + }; + + let bar = row![ + tool_name, + text(" | ").size(10), + options, + iced::widget::Space::with_width(Length::Fill), + ] + .spacing(4) + .align_y(iced::Alignment::Center) + .padding([2, 8]); + + container(bar) + .width(Length::Fill) + .height(28) + .style(move |_theme| styles::statusbar_background(colors)) + .into() +} + +/// Get the display name for a tool. +fn tool_name_label(tool: Tool) -> &'static str { + match tool { + Tool::Move => "Move Tool", + Tool::Select => "Rectangle Select", + Tool::Lasso => "Lasso Select", + Tool::MagicWand => "Magic Wand", + Tool::Eyedropper => "Eyedropper", + Tool::Crop => "Crop Tool", + Tool::Brush => "Brush Tool", + Tool::Pen => "Pen Tool", + Tool::Spray => "Spray Tool", + Tool::Eraser => "Eraser Tool", + Tool::FloodFill => "Paint Bucket", + Tool::Gradient => "Gradient Tool", + Tool::Text => "Text Tool", + Tool::VectorSelect => "Vector Select", + Tool::VectorLine => "Line Tool", + Tool::VectorRect => "Rectangle Tool", + Tool::VectorCircle => "Ellipse Tool", + _ => "Tool", + } +} + +/// A small checkbox-style label (visual only, for display). +fn checkbox_label(label: &str, checked: bool) -> Element<'static, Message> { + let mark = if checked { "☑" } else { "☐" }; + let label_owned = label.to_string(); + row![ + text(mark).size(10), + text(label_owned).size(10), + ] + .spacing(2) + .align_y(iced::Alignment::Center) + .into() +} + +/// A small flat button for tool options. +fn small_button(label: &str, colors: ThemeColors) -> Element<'static, Message> { + let bg_hover = colors.bg_hover; + let bg_panel = colors.bg_panel; + let text_primary = colors.text_primary; + let border_high = colors.border_high; + let border_low = colors.border_low; + let label_owned = label.to_string(); + + button(text(label_owned).size(10)) + .on_press(Message::NoOp) + .padding([2, 6]) + .style(move |_theme: &iced::Theme, status: iced::widget::button::Status| { + match status { + iced::widget::button::Status::Hovered => iced::widget::button::Style { + background: Some(iced::Background::Color(bg_hover)), + text_color: text_primary, + border: iced::Border::default().color(border_high).width(1), + ..Default::default() + }, + _ => iced::widget::button::Style { + background: Some(iced::Background::Color(bg_panel)), + text_color: text_primary, + border: iced::Border::default().color(border_low).width(1), + ..Default::default() + }, + } + }) + .into() +} diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/tool_settings.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/tool_settings.rs new file mode 100644 index 0000000..f1b5eae --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/tool_settings.rs @@ -0,0 +1,219 @@ +//! Tool Settings panel — shows all properties for the active tool. +//! +//! **Purpose:** Right-side panel showing tool-specific settings like the reference images. +//! - Brush: Size, Opacity, Hardness, Flow, Spacing, Angle, Roundness +//! - Eraser: Size, Opacity, Hardness, Flow +//! - Pen: Size, Opacity, Flow +//! - Vector: Fill, Stroke, Stroke Width, Opacity +//! - Select: Feather, Anti-alias +//! - Text: Font, Size, Color, Bold, Italic + +use crate::app::{Message, ToolState}; +use crate::panels::styles; +use crate::theme::ThemeColors; +use hcie_engine_api::Tool; +use iced::widget::{button, column, container, horizontal_rule, row, scrollable, slider, text}; +use iced::{Element, Length}; + +/// Build the tool settings panel for the active tool. +pub fn view<'a>(tool_state: &'a ToolState, colors: ThemeColors) -> Element<'a, Message> { + let content = match tool_state.active_tool { + // ── Brush / Eraser / Pen / Spray ── + Tool::Brush | Tool::Eraser | Tool::Pen | Tool::Spray => { + brush_settings(tool_state, colors) + } + // ── Vector tools ── + Tool::VectorLine | Tool::VectorRect | Tool::VectorCircle | + Tool::VectorArrow | Tool::VectorStar | Tool::VectorPolygon => { + vector_settings(colors) + } + // ── Select tools ── + Tool::Select | Tool::Lasso | Tool::PolygonSelect => { + select_settings(colors) + } + // ── Magic Wand ── + Tool::MagicWand => magic_wand_settings(colors), + // ── Text ── + Tool::Text => text_settings(colors), + // ── Move ── + Tool::Move => move_settings(colors), + // ── Gradient / FloodFill ── + Tool::Gradient | Tool::FloodFill => fill_settings(colors), + // ── Default ── + _ => column![text("No settings").size(11)].spacing(4), + }; + + let reset_btn = button(text("Reset to Defaults").size(10)) + .on_press(Message::ResetToolDefaults) + .padding([4, 8]) + .width(Length::Fill); + + let panel = column![ + scrollable(content).height(Length::Fill), + horizontal_rule(1), + reset_btn, + ] + .spacing(2) + .padding(4); + + container(panel) + .width(Length::Fill) + .height(Length::Fill) + .style(move |_theme| styles::panel_background(colors)) + .into() +} + +fn brush_settings(ts: &ToolState, c: ThemeColors) -> iced::widget::Column<'static, Message> { + let size = ts.brush_size; + let opacity = ts.brush_opacity; + let hardness = ts.brush_hardness; + + column![ + section_header("Brush Settings", c), + prop_slider("Size", size, 1.0, 200.0, Message::BrushSizeChanged, c), + prop_slider("Opacity", opacity * 100.0, 0.0, 100.0, Message::BrushOpacityChanged, c), + prop_slider("Hardness", hardness * 100.0, 0.0, 100.0, Message::BrushHardnessChanged, c), + prop_slider("Flow", 100.0, 0.0, 100.0, |_v| Message::NoOp, c), + prop_slider("Spacing", 25.0, 1.0, 100.0, |_v| Message::NoOp, c), + prop_slider("Angle", 0.0, 0.0, 360.0, |_v| Message::NoOp, c), + prop_slider("Roundness", 100.0, 1.0, 100.0, |_v| Message::NoOp, c), + ] + .spacing(4) +} + +fn vector_settings(c: ThemeColors) -> iced::widget::Column<'static, Message> { + column![ + section_header("Vector Settings", c), + color_row("Fill:", [0, 0, 0, 255], c), + color_row("Stroke:", [255, 255, 255, 255], c), + prop_slider("Stroke Width", 1.0, 0.0, 20.0, |_v| Message::NoOp, c), + prop_slider("Opacity", 100.0, 0.0, 100.0, |_v| Message::NoOp, c), + prop_row("Corner:", "Miter", c), + prop_row("Cap:", "Butt", c), + ] + .spacing(4) +} + +fn select_settings(c: ThemeColors) -> iced::widget::Column<'static, Message> { + column![ + section_header("Selection Settings", c), + prop_slider("Feather", 0.0, 0.0, 250.0, |_v| Message::NoOp, c), + prop_checkbox("Anti-alias", true, c), + ] + .spacing(4) +} + +fn magic_wand_settings(c: ThemeColors) -> iced::widget::Column<'static, Message> { + column![ + section_header("Magic Wand Settings", c), + prop_slider("Tolerance", 16.0, 0.0, 255.0, |_v| Message::NoOp, c), + prop_checkbox("Anti-alias", true, c), + prop_checkbox("Contiguous", true, c), + prop_checkbox("Sample All Layers", false, c), + ] + .spacing(4) +} + +fn text_settings(c: ThemeColors) -> iced::widget::Column<'static, Message> { + column![ + section_header("Text Settings", c), + prop_row("Font:", "Arial", c), + prop_slider("Size", 24.0, 1.0, 200.0, |_v| Message::NoOp, c), + prop_row("Color:", "#000000", c), + prop_checkbox("Bold", false, c), + prop_checkbox("Italic", false, c), + prop_checkbox("Underline", false, c), + ] + .spacing(4) +} + +fn move_settings(c: ThemeColors) -> iced::widget::Column<'static, Message> { + column![ + section_header("Move Settings", c), + prop_checkbox("Auto-Select", true, c), + ] + .spacing(4) +} + +fn fill_settings(c: ThemeColors) -> iced::widget::Column<'static, Message> { + column![ + section_header("Fill Settings", c), + prop_row("Mode:", "Normal", c), + prop_slider("Opacity", 100.0, 0.0, 100.0, |_v| Message::NoOp, c), + prop_checkbox("Contiguous", true, c), + ] + .spacing(4) +} + +fn section_header(label: &str, _c: ThemeColors) -> Element<'static, Message> { + let l = label.to_string(); + row![ + text(l).size(11), + ] + .spacing(4) + .into() +} + +fn prop_slider(label: &str, value: f32, min: f32, max: f32, msg_fn: F, _c: ThemeColors) -> Element<'static, Message> +where + F: Fn(f32) -> Message + 'static, +{ + let l = label.to_string(); + let label_text = text(l).size(10).width(Length::Fixed(70.0)); + let val_text = text(format!("{:.1}", value)).size(10).width(Length::Fixed(40.0)); + let sl = slider(min..=max, value, msg_fn) + .step(0.1) + .width(Length::Fill); + + row![label_text, sl, val_text] + .spacing(4) + .align_y(iced::Alignment::Center) + .into() +} + +fn prop_row(label: &str, value: &str, _c: ThemeColors) -> Element<'static, Message> { + let l = label.to_string(); + let v = value.to_string(); + row![ + text(l).size(10).width(Length::Fixed(70.0)), + text(v).size(10), + ] + .spacing(4) + .into() +} + +fn prop_checkbox(label: &str, checked: bool, _c: ThemeColors) -> Element<'static, Message> { + let mark = if checked { "☑" } else { "☐" }.to_string(); + let l = label.to_string(); + row![ + text(mark).size(10), + text(l).size(10), + ] + .spacing(4) + .into() +} + +fn color_row(label: &str, color: [u8; 4], _c: ThemeColors) -> Element<'static, Message> { + let l = label.to_string(); + let swatch = container(text("")) + .width(14) + .height(14) + .style(move |_theme| iced::widget::container::Style { + background: Some(iced::Background::Color(iced::Color::from_rgba( + color[0] as f32 / 255.0, + color[1] as f32 / 255.0, + color[2] as f32 / 255.0, + color[3] as f32 / 255.0, + ))), + border: iced::Border::default().rounded(2).color(iced::Color::from_rgb(0.3, 0.3, 0.3)).width(1), + ..Default::default() + }); + + row![ + text(l).size(10).width(Length::Fixed(50.0)), + swatch, + ] + .spacing(4) + .align_y(iced::Alignment::Center) + .into() +} diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/toolbar.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/toolbar.rs new file mode 100644 index 0000000..9b940c5 --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/toolbar.rs @@ -0,0 +1,211 @@ +//! Merged toolbar — SVG icon buttons + tool options in one row. +//! +//! **Purpose:** Single row matching Photopea: [SVG icons] | [Tool Name] [Tool Options] +//! Icons are 16px SVGs loaded from assets/icons/. + +use crate::app::Message; +use crate::panels::styles; +use crate::theme::ThemeColors; +use hcie_engine_api::Tool; +use iced::widget::{button, container, row, slider, svg, text}; +use iced::{Element, Length}; + +/// Build the merged toolbar: SVG icon buttons + tool options in one row. +pub fn view(tool_state: &crate::app::ToolState, colors: ThemeColors) -> Element<'_, Message> { + let c = colors; + + // ── SVG icon buttons (no text) ── + let new_btn = svg_btn("icons/new_file.svg", "New", Message::MenuAction(0, 0), c); + let open_btn = svg_btn("icons/open_file.svg", "Open", Message::MenuAction(0, 1), c); + let save_btn = svg_btn("icons/save_file.svg", "Save", Message::MenuAction(0, 6), c); + let undo_btn = svg_btn("icons/undo.svg", "Undo", Message::Undo, c); + let redo_btn = svg_btn("icons/redo.svg", "Redo", Message::Redo, c); + + let actions = row![new_btn, open_btn, save_btn, sep(c), undo_btn, redo_btn] + .spacing(0) + .align_y(iced::Alignment::Center); + + // ── Tool name + options ── + let tool_name = text(tool_label(tool_state.active_tool)) + .size(11) + .width(Length::Fixed(70.0)); + + let options = match tool_state.active_tool { + Tool::Brush | Tool::Eraser | Tool::Pen | Tool::Spray => { + let sz = text(format!("Size: {:.0}", tool_state.brush_size)) + .size(10) + .width(Length::Fixed(55.0)); + let sz_sl = slider(1.0..=200.0, tool_state.brush_size, Message::BrushSizeChanged) + .step(1.0) + .width(Length::Fixed(80.0)); + let op = text(format!("Opacity: {:.0}%", tool_state.brush_opacity * 100.0)) + .size(10) + .width(Length::Fixed(65.0)); + let op_sl = slider(0.0..=1.0, tool_state.brush_opacity, Message::BrushOpacityChanged) + .step(0.01) + .width(Length::Fixed(80.0)); + let ha = text(format!("Hardness: {:.0}%", tool_state.brush_hardness * 100.0)) + .size(10) + .width(Length::Fixed(70.0)); + let ha_sl = slider(0.0..=1.0, tool_state.brush_hardness, Message::BrushHardnessChanged) + .step(0.01) + .width(Length::Fixed(80.0)); + row![sz, sz_sl, op, op_sl, ha, ha_sl].spacing(4).align_y(iced::Alignment::Center) + } + Tool::MagicWand => { + row![ + text("Tolerance:").size(10), + text("16").size(10).width(Length::Fixed(30.0)), + text("Anti-alias").size(10), + text("Contiguous").size(10), + ].spacing(6).align_y(iced::Alignment::Center) + } + Tool::Select | Tool::Lasso => { + row![ + text("Feather:").size(10), + text("0 px").size(10).width(Length::Fixed(40.0)), + small_btn("Refine Edge", c), + small_btn("Select Subject", c), + ].spacing(6).align_y(iced::Alignment::Center) + } + Tool::Move => { + row![text("Auto-Select").size(10)].spacing(4).align_y(iced::Alignment::Center) + } + Tool::VectorLine | Tool::VectorRect | Tool::VectorCircle => { + row![ + text("Fill:").size(10), text("None").size(10), + text("Stroke:").size(10), text("1px").size(10), + ].spacing(4).align_y(iced::Alignment::Center) + } + _ => row![].spacing(0), + }; + + let right_side = row![tool_name, options] + .spacing(4) + .align_y(iced::Alignment::Center); + + let bar = row![ + actions, + sep(c), + right_side, + iced::widget::Space::with_width(Length::Fill), + ] + .spacing(4) + .align_y(iced::Alignment::Center) + .padding([2, 4]); + + container(bar) + .width(Length::Fill) + .height(28) + .style(move |_theme| styles::statusbar_background(colors)) + .into() +} + +fn tool_label(tool: Tool) -> &'static str { + match tool { + Tool::Move => "Move", + Tool::Select => "Rect Select", + Tool::Lasso => "Lasso", + Tool::MagicWand => "Magic Wand", + Tool::Eyedropper => "Eyedropper", + Tool::Crop => "Crop", + Tool::Brush => "Brush", + Tool::Pen => "Pen", + Tool::Spray => "Spray", + Tool::Eraser => "Eraser", + Tool::FloodFill => "Paint Bucket", + Tool::Gradient => "Gradient", + Tool::Text => "Text", + Tool::VectorSelect => "Path Select", + Tool::VectorLine => "Line", + Tool::VectorRect => "Rectangle", + Tool::VectorCircle => "Ellipse", + _ => "Tool", + } +} + +/// SVG icon button (16px icon, no text). +fn svg_btn(icon_path: &str, tip: &str, msg: Message, colors: ThemeColors) -> Element<'static, Message> { + let c = colors; + let tip_owned = tip.to_string(); + + // Search multiple paths for the icon + let full_path = std::path::PathBuf::from(format!( + "/mnt/extra/00_PROJECTS/hcie-rust-v3.05/hcie-iced-app/assets/{}", + icon_path + )); + + let icon: Element<'static, Message> = if full_path.exists() { + let handle = svg::Handle::from_path(full_path); + svg(handle) + .width(16) + .height(16) + .style(move |_theme: &iced::Theme, _status: iced::widget::svg::Status| svg::Style { + color: Some(c.text_primary), + }) + .into() + } else { + // Fallback to text + text("?").size(12).color(c.text_primary).into() + }; + + iced::widget::tooltip( + button(icon) + .on_press(msg) + .padding([2, 4]) + .style(move |_theme: &iced::Theme, status: iced::widget::button::Status| { + match status { + iced::widget::button::Status::Hovered => iced::widget::button::Style { + background: Some(iced::Background::Color(c.bg_hover)), + text_color: c.text_primary, + border: iced::Border::default().color(c.border_low).width(1), + ..Default::default() + }, + _ => iced::widget::button::Style { + background: Some(iced::Background::Color(iced::Color::TRANSPARENT)), + text_color: c.text_primary, + border: iced::Border::default(), + ..Default::default() + }, + } + }), + text(tip_owned).size(11), + iced::widget::tooltip::Position::Bottom, + ) + .into() +} + +fn small_btn(label: &str, colors: ThemeColors) -> Element<'static, Message> { + let c = colors; + let l = label.to_string(); + button(text(l).size(10)) + .on_press(Message::NoOp) + .padding([2, 6]) + .style(move |_theme: &iced::Theme, status: iced::widget::button::Status| { + match status { + iced::widget::button::Status::Hovered => iced::widget::button::Style { + background: Some(iced::Background::Color(c.bg_hover)), + text_color: c.text_primary, + border: iced::Border::default().color(c.border_high).width(1), + ..Default::default() + }, + _ => iced::widget::button::Style { + background: Some(iced::Background::Color(c.bg_panel)), + text_color: c.text_primary, + border: iced::Border::default().color(c.border_low).width(1), + ..Default::default() + }, + } + }) + .into() +} + +fn sep(colors: ThemeColors) -> Element<'static, Message> { + container(text("").height(16).width(1)) + .padding([0, 2]) + .style(move |_theme| iced::widget::container::Style { + background: Some(iced::Background::Color(colors.border_low)), + ..Default::default() + }) + .into() +} diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/settings.rs b/hcie-iced-app/crates/hcie-iced-gui/src/settings.rs new file mode 100644 index 0000000..fe0e25b --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/settings.rs @@ -0,0 +1,204 @@ +//! Settings persistence — saves/loads all app settings to JSON. +//! +//! **Purpose:** Persists tool settings, panel layout, and preferences between sessions. +//! Settings are saved to `~/.hcie/settings.json` and loaded on startup. + +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +/// Main settings structure saved to disk. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AppSettings { + /// Tool-specific settings. + pub tool_settings: ToolSettings, + /// Panel layout positions. + pub panel_layout: PanelLayout, + /// Window settings. + pub window: WindowSettings, +} + +/// Tool-specific settings that persist between sessions. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ToolSettings { + pub brush_size: f32, + pub brush_opacity: f32, + pub brush_hardness: f32, + pub brush_flow: f32, + pub brush_spacing: f32, + pub eraser_size: f32, + pub eraser_opacity: f32, + pub pen_size: f32, + pub pen_opacity: f32, + pub spray_radius: f32, + pub spray_opacity: f32, + pub gradient_opacity: f32, + pub flood_fill_tolerance: f32, + pub magic_wand_tolerance: f32, + pub select_feather: f32, + pub text_size: f32, + pub vector_stroke_width: f32, + pub vector_opacity: f32, + pub last_active_tool: String, +} + +/// Panel layout positions and visibility. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PanelLayout { + /// Left sidebar width in pixels. + pub sidebar_width: f32, + /// Right tool settings panel width in pixels. + pub tool_settings_width: f32, + /// Which panels are visible. + pub visible_panels: Vec, + /// Panel order in the dock. + pub panel_order: Vec, +} + +/// Window settings. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WindowSettings { + pub width: f32, + pub height: f32, + pub maximized: bool, +} + +impl Default for AppSettings { + fn default() -> Self { + Self { + tool_settings: ToolSettings::default(), + panel_layout: PanelLayout::default(), + window: WindowSettings::default(), + } + } +} + +impl Default for ToolSettings { + fn default() -> Self { + Self { + brush_size: 20.0, + brush_opacity: 1.0, + brush_hardness: 0.8, + brush_flow: 1.0, + brush_spacing: 1.0, + eraser_size: 20.0, + eraser_opacity: 1.0, + pen_size: 2.0, + pen_opacity: 1.0, + spray_radius: 40.0, + spray_opacity: 0.5, + gradient_opacity: 1.0, + flood_fill_tolerance: 32.0, + magic_wand_tolerance: 16.0, + select_feather: 0.0, + text_size: 24.0, + vector_stroke_width: 1.0, + vector_opacity: 1.0, + last_active_tool: "Brush".to_string(), + } + } +} + +impl Default for PanelLayout { + fn default() -> Self { + Self { + sidebar_width: 36.0, + tool_settings_width: 200.0, + visible_panels: vec![ + "Brushes & Tips".to_string(), + "Color Palette".to_string(), + "Layers".to_string(), + "History".to_string(), + "Filters".to_string(), + ], + panel_order: vec![ + "Brushes & Tips".to_string(), + "Color Palette".to_string(), + "Layers".to_string(), + "History".to_string(), + "Filters".to_string(), + ], + } + } +} + +impl Default for WindowSettings { + fn default() -> Self { + Self { + width: 1280.0, + height: 800.0, + maximized: false, + } + } +} + +impl AppSettings { + /// Get the settings file path (~/.hcie/settings.json). + pub fn file_path() -> PathBuf { + let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string()); + PathBuf::from(home).join(".hcie").join("settings.json") + } + + /// Load settings from disk, or return defaults if file doesn't exist. + pub fn load() -> Self { + let path = Self::file_path(); + if path.exists() { + match std::fs::read_to_string(&path) { + Ok(content) => { + match serde_json::from_str(&content) { + Ok(settings) => settings, + Err(e) => { + log::warn!("Failed to parse settings: {}, using defaults", e); + Self::default() + } + } + } + Err(e) => { + log::warn!("Failed to read settings: {}, using defaults", e); + Self::default() + } + } + } else { + Self::default() + } + } + + /// Save settings to disk. + pub fn save(&self) -> Result<(), String> { + let path = Self::file_path(); + + // Create directory if it doesn't exist + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent) + .map_err(|e| format!("Failed to create settings directory: {}", e))?; + } + + let content = serde_json::to_string_pretty(self) + .map_err(|e| format!("Failed to serialize settings: {}", e))?; + + std::fs::write(&path, content) + .map_err(|e| format!("Failed to write settings: {}", e))?; + + log::info!("Settings saved to {}", path.display()); + Ok(()) + } + + /// Reset all tool settings to defaults. + pub fn reset_tool_defaults(&mut self) { + self.tool_settings = ToolSettings::default(); + } + + /// Update tool settings from current ToolState. + pub fn update_from_tool_state(&mut self, tool_state: &crate::app::ToolState) { + self.tool_settings.brush_size = tool_state.brush_size; + self.tool_settings.brush_opacity = tool_state.brush_opacity; + self.tool_settings.brush_hardness = tool_state.brush_hardness; + self.tool_settings.last_active_tool = format!("{:?}", tool_state.active_tool); + } + + /// Apply saved settings to ToolState. + pub fn apply_to_tool_state(&self, tool_state: &mut crate::app::ToolState) { + tool_state.brush_size = self.tool_settings.brush_size; + tool_state.brush_opacity = self.tool_settings.brush_opacity; + tool_state.brush_hardness = self.tool_settings.brush_hardness; + } +} diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/sidebar/mod.rs b/hcie-iced-app/crates/hcie-iced-gui/src/sidebar/mod.rs index a3289c6..ed39dd1 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/sidebar/mod.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/sidebar/mod.rs @@ -1,10 +1,15 @@ -//! Tool sidebar — fixed 36px single-column strip on the left edge. +//! Tool sidebar — Photopea-style fixed 36px single-column strip on the left edge. //! -//! Features: -//! - Always single column (36px wide) -//! - SVG icons with hover/active states -//! - Overlapping fg/bg color swatches at bottom -//! - Tooltip on hover +//! **Purpose:** Provides a vertical toolbar matching Photopea's left toolbox. +//! Active tool gets a green (#4caf50) background highlight, matching Photopea's design. +//! Supports sub-tools via click (shows popup with tool variants). +//! +//! **Design reference:** Photopea image editor (93 screenshots analyzed). +//! - Width: 36px +//! - Active tool: green background (#4caf50) +//! - Tool icons: white/light gray, 20px +//! - Color swatches at bottom (foreground/background, diagonal overlap) +//! - Sub-tools: click to show popup with tool variants use crate::app::{Message, ToolState}; use crate::panels::styles; @@ -13,50 +18,69 @@ use hcie_engine_api::Tool; use iced::widget::{button, column, container, row, svg, text}; use iced::{Element, Length}; -/// Tool slot definition — (primary tool, label, svg_path). +/// Tool slot with sub-tools — matches egui's TOOL_SLOTS structure. struct ToolSlot { - tool: Tool, - #[allow(dead_code)] + tools: &'static [Tool], label: &'static str, icon: &'static str, } -/// All tool slots matching egui's TOOL_SLOTS. +/// All tool slots matching egui's TOOL_SLOTS structure exactly. +/// Each slot can have multiple tools (sub-tools shown on click). const TOOL_SLOTS: &[ToolSlot] = &[ - ToolSlot { tool: Tool::Move, label: "Move", icon: "icons/move.svg" }, - ToolSlot { tool: Tool::VectorSelect, label: "Vector Select", icon: "icons/vector_select.svg" }, - ToolSlot { tool: Tool::Select, label: "Rectangle Select", icon: "icons/rect_select.svg" }, - ToolSlot { tool: Tool::Lasso, label: "Lasso", icon: "icons/lasso.svg" }, - ToolSlot { tool: Tool::MagicWand, label: "Magic Wand", icon: "icons/magic_wand.svg" }, - ToolSlot { tool: Tool::Eyedropper, label: "Eyedropper", icon: "icons/eyedropper.svg" }, - ToolSlot { tool: Tool::Crop, label: "Crop", icon: "icons/crop.svg" }, - ToolSlot { tool: Tool::Brush, label: "Brush", icon: "icons/brush.svg" }, - ToolSlot { tool: Tool::Pen, label: "Pen", icon: "icons/pen.svg" }, - ToolSlot { tool: Tool::Spray, label: "Spray", icon: "icons/spray.svg" }, - ToolSlot { tool: Tool::Eraser, label: "Eraser", icon: "icons/eraser.svg" }, - ToolSlot { tool: Tool::FloodFill, label: "Flood Fill", icon: "icons/bucket.svg" }, - ToolSlot { tool: Tool::Gradient, label: "Gradient", icon: "icons/gradient.svg" }, - ToolSlot { tool: Tool::Text, label: "Text", icon: "icons/text.svg" }, - ToolSlot { tool: Tool::VectorLine, label: "Vector Line", icon: "icons/line.svg" }, - ToolSlot { tool: Tool::VectorRect, label: "Vector Rectangle", icon: "icons/vector_rect.svg" }, - ToolSlot { tool: Tool::VectorCircle, label: "Vector Circle", icon: "icons/circle.svg" }, + ToolSlot { tools: &[Tool::Move], label: "Move", icon: "icons/move.svg" }, + ToolSlot { tools: &[Tool::VectorSelect], label: "Vector Select", icon: "icons/vector_select.svg" }, + ToolSlot { tools: &[Tool::Select], label: "Rectangle Select", icon: "icons/rect_select.svg" }, + ToolSlot { tools: &[Tool::Lasso, Tool::PolygonSelect], label: "Lasso", icon: "icons/lasso.svg" }, + ToolSlot { tools: &[Tool::MagicWand], label: "Magic Wand", icon: "icons/magic_wand.svg" }, + ToolSlot { tools: &[Tool::SmartSelect, Tool::VisionSelect], label: "Smart Select", icon: "icons/smart_select.svg" }, + ToolSlot { tools: &[Tool::Crop], label: "Crop", icon: "icons/crop.svg" }, + ToolSlot { tools: &[Tool::Eyedropper], label: "Eyedropper", icon: "icons/eyedropper.svg" }, + ToolSlot { tools: &[Tool::Brush], label: "Brush", icon: "icons/brush.svg" }, + ToolSlot { tools: &[Tool::Pen], label: "Pen", icon: "icons/pen.svg" }, + ToolSlot { tools: &[Tool::Spray], label: "Spray", icon: "icons/spray.svg" }, + ToolSlot { tools: &[Tool::Eraser], label: "Eraser", icon: "icons/eraser.svg" }, + ToolSlot { tools: &[Tool::FloodFill], label: "Flood Fill", icon: "icons/bucket.svg" }, + ToolSlot { tools: &[Tool::Gradient], label: "Gradient", icon: "icons/gradient.svg" }, + ToolSlot { tools: &[Tool::Text], label: "Text", icon: "icons/text.svg" }, + ToolSlot { tools: &[Tool::VectorLine], label: "Vector Line", icon: "icons/line.svg" }, + ToolSlot { tools: &[Tool::VectorRect], label: "Vector Rectangle", icon: "icons/vector_rect.svg" }, + ToolSlot { tools: &[Tool::VectorCircle], label: "Vector Circle", icon: "icons/circle.svg" }, + ToolSlot { + tools: &[ + Tool::VectorArrow, Tool::VectorStar, Tool::VectorPolygon, + Tool::VectorRhombus, Tool::VectorCylinder, Tool::VectorHeart, + Tool::VectorBubble, Tool::VectorGear, Tool::VectorCross, + Tool::VectorCrescent, Tool::VectorBolt, Tool::VectorArrow4, + ], + label: "Vector Shapes", + icon: "icons/star.svg", + }, + ToolSlot { + tools: &[Tool::SpotRemoval, Tool::RedEyeRemoval, Tool::SmartPatch, Tool::AiObjectRemoval], + label: "Retouch", + icon: "icons/spot.svg", + }, ]; -/// Build the fixed 36px sidebar element. +/// Build the Photopea-style fixed 36px sidebar element. pub fn view<'a>( tool_state: &'a ToolState, fg_color: &'a [u8; 4], bg_color: &'a [u8; 4], colors: ThemeColors, + active_slot: usize, + sub_tools_open: bool, ) -> Element<'a, Message> { - // Tool buttons — single column + // Tool buttons — single column, with sub-tools popup support let mut tool_list = column![].spacing(1); - for slot in TOOL_SLOTS { - let btn = tool_button(slot, tool_state, colors); + for (idx, slot) in TOOL_SLOTS.iter().enumerate() { + let btn = tool_button(slot, tool_state, colors, idx, active_slot, sub_tools_open); tool_list = tool_list.push(btn); } - // Color swatches at bottom + // Color swatches at bottom (Photopea-style: diagonal overlapping) + // Foreground (top-left), Background (bottom-right), overlapping diagonally let fg = fg_color; let bg = bg_color; @@ -82,31 +106,46 @@ pub fn view<'a>( ..Default::default() }); - let swap_btn = button(text("⇅").size(8).color(colors.text_secondary)) - .on_press(Message::SwapColors) - .padding(2) - .style(move |_theme: &iced::Theme, status: iced::widget::button::Status| { - match status { - iced::widget::button::Status::Hovered => iced::widget::button::Style { - background: Some(iced::Background::Color(colors.bg_hover)), - text_color: colors.text_primary, - border: iced::Border::default(), - ..Default::default() - }, - _ => iced::widget::button::Style { - background: Some(iced::Background::Color(iced::Color::from_rgba(0.0, 0.0, 0.0, 0.0))), - text_color: colors.text_secondary, - border: iced::Border::default(), - ..Default::default() - }, - } - }); - + // Photopea-style: fg at top-left, bg at bottom-right (diagonal overlap) + // with a swap arrow button between them let color_section = container( - row![fg_swatch, bg_swatch, swap_btn].spacing(1) + iced::widget::Stack::new() + .push( + container(fg_swatch) + .padding(iced::Padding { top: 0.0, bottom: 0.0, left: 4.0, right: 0.0 }) + ) + .push( + container(bg_swatch) + .padding(iced::Padding { top: 10.0, bottom: 0.0, left: 16.0, right: 0.0 }) + ) + .push( + container( + button(text("⇄").size(8).color(colors.text_primary)) + .on_press(Message::SwapColors) + .padding(1) + .style(move |_theme: &iced::Theme, status: iced::widget::button::Status| { + match status { + iced::widget::button::Status::Hovered => iced::widget::button::Style { + background: Some(iced::Background::Color(colors.bg_hover)), + text_color: colors.text_primary, + border: iced::Border::default(), + ..Default::default() + }, + _ => iced::widget::button::Style { + background: Some(iced::Background::Color(iced::Color::from_rgba(0.0, 0.0, 0.0, 0.0))), + text_color: colors.text_primary, + border: iced::Border::default(), + ..Default::default() + }, + } + }) + ) + .padding(iced::Padding { top: 6.0, bottom: 0.0, left: 10.0, right: 0.0 }) + ) ) - .padding([4, 0]) - .center_x(Length::Fill); + .padding([4, 2]) + .width(36) + .height(32); // Separator line let sep_style = move |_theme: &iced::Theme| iced::widget::container::Style { @@ -128,15 +167,21 @@ pub fn view<'a>( .into() } -/// Create a single tool button with hover feedback. +/// Create a Photopea-style tool button with green active highlight. /// -/// Uses `iced::widget::button` for built-in Active/Hovered/Pressed styling. -/// Active tool: bg_active + accent border -/// Hovered: bg_hover background -/// Default: transparent -fn tool_button<'a>(slot: &ToolSlot, tool_state: &ToolState, colors: ThemeColors) -> Element<'a, Message> { - let is_active = std::mem::discriminant(&tool_state.active_tool) - == std::mem::discriminant(&slot.tool); +/// Active tool: green background (#4caf50) — matches Photopea's toolbox. +/// Clicking a slot with sub-tools toggles the sub-tools popup. +/// Hovered: subtle highlight. +/// Default: transparent. +fn tool_button<'a>( + slot: &ToolSlot, + tool_state: &ToolState, + colors: ThemeColors, + slot_idx: usize, + active_slot: usize, + sub_tools_open: bool, +) -> Element<'a, Message> { + let is_active = slot.tools.contains(&tool_state.active_tool); // Try to load SVG icon - search multiple paths let icon_paths = [ @@ -163,7 +208,11 @@ fn tool_button<'a>(slot: &ToolSlot, tool_state: &ToolState, colors: ThemeColors) .into() }; - let tool = slot.tool; + // If slot has sub-tools and is active, show sub-tools popup + let has_sub_tools = slot.tools.len() > 1; + let show_popup = has_sub_tools && sub_tools_open && active_slot == slot_idx; + + let primary_tool = slot.tools[0]; let active = is_active; let c = colors; @@ -174,16 +223,16 @@ fn tool_button<'a>(slot: &ToolSlot, tool_state: &ToolState, colors: ThemeColors) .center_y(20) .center_x(20) ) - .on_press(Message::ToolSelected(tool)) + .on_press(Message::ToolSlotClicked(slot_idx, primary_tool)) .padding(0) .style(move |_theme: &iced::Theme, status: iced::widget::button::Status| { match status { iced::widget::button::Status::Active | iced::widget::button::Status::Disabled => { if active { iced::widget::button::Style { - background: Some(iced::Background::Color(c.bg_active)), - border: iced::Border::default().color(c.accent).width(1), - text_color: c.text_primary, + background: Some(iced::Background::Color(c.accent_green)), + border: iced::Border::default(), + text_color: iced::Color::WHITE, ..Default::default() } } else { @@ -198,9 +247,9 @@ fn tool_button<'a>(slot: &ToolSlot, tool_state: &ToolState, colors: ThemeColors) iced::widget::button::Status::Hovered => { if active { iced::widget::button::Style { - background: Some(iced::Background::Color(c.bg_active)), - border: iced::Border::default().color(c.accent).width(1), - text_color: c.text_primary, + background: Some(iced::Background::Color(c.accent_green)), + border: iced::Border::default(), + text_color: iced::Color::WHITE, ..Default::default() } } else { @@ -214,20 +263,115 @@ fn tool_button<'a>(slot: &ToolSlot, tool_state: &ToolState, colors: ThemeColors) } iced::widget::button::Status::Pressed => { iced::widget::button::Style { - background: Some(iced::Background::Color(c.bg_active)), - border: iced::Border::default().color(c.accent).width(1), - text_color: c.text_primary, + background: Some(iced::Background::Color(c.accent_green)), + border: iced::Border::default(), + text_color: iced::Color::WHITE, ..Default::default() } } } }); - // Tooltip on hover - iced::widget::tooltip( - btn, - iced::widget::text(slot.label).size(11), - iced::widget::tooltip::Position::Right, - ) - .into() + // Build the tool button with optional sub-tools popup + if show_popup { + let mut popup_items = column![].spacing(0); + for &tool in slot.tools { + let tool_name = tool.label(); + let is_tool_active = tool == tool_state.active_tool; + let c = colors; + + let item = button( + container( + row![ + text(tool_name).size(11).width(Length::Fill), + text(tool_shortcut(tool)).size(10), + ] + .spacing(8) + ) + .width(150) + .padding([4, 8]) + ) + .on_press(Message::ToolSelected(tool)) + .padding(0) + .style(move |_theme: &iced::Theme, status: iced::widget::button::Status| { + match status { + iced::widget::button::Status::Hovered => iced::widget::button::Style { + background: Some(iced::Background::Color(c.menu_hover)), + text_color: c.menu_text, + border: iced::Border::default(), + ..Default::default() + }, + _ => iced::widget::button::Style { + background: Some(iced::Background::Color(c.bg_hover)), + text_color: if is_tool_active { c.accent } else { c.text_primary }, + border: iced::Border::default(), + ..Default::default() + }, + } + }); + + popup_items = popup_items.push(item); + } + + let popup = container(popup_items) + .padding(2) + .style(move |_theme| iced::widget::container::Style { + background: Some(iced::Background::Color(colors.bg_active)), + border: iced::Border::default().color(colors.border_high).width(1), + shadow: iced::Shadow { + color: iced::Color::from_rgba(0.0, 0.0, 0.0, 0.4), + offset: iced::Vector::new(2.0, 0.0), + blur_radius: 8.0, + }, + ..Default::default() + }); + + // Position popup to the right of the button + let btn_element: Element<'a, Message> = container( + row![btn, popup].spacing(0).align_y(iced::Alignment::Center) + ).into(); + + // Tooltip on hover + let tooltip_text = format!("{} (click for sub-tools)", slot.label); + iced::widget::tooltip( + btn_element, + iced::widget::text(tooltip_text).size(11), + iced::widget::tooltip::Position::Right, + ) + .into() + } else { + // Tooltip on hover + let tooltip_text = slot.label.to_string(); + iced::widget::tooltip( + btn, + iced::widget::text(tooltip_text).size(11), + iced::widget::tooltip::Position::Right, + ) + .into() + } +} + +/// Get keyboard shortcut display for a tool. +fn tool_shortcut(tool: Tool) -> &'static str { + match tool { + Tool::Move => "V", + Tool::VectorSelect => "A", + Tool::Select => "M", + Tool::Lasso => "L", + Tool::PolygonSelect => "L", + Tool::MagicWand => "W", + Tool::Eyedropper => "I", + Tool::Crop => "C", + Tool::Brush => "B", + Tool::Pen => "P", + Tool::Spray => "B", + Tool::Eraser => "E", + Tool::FloodFill => "G", + Tool::Gradient => "G", + Tool::Text => "T", + Tool::VectorLine => "U", + Tool::VectorRect => "U", + Tool::VectorCircle => "U", + _ => "", + } } diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/theme.rs b/hcie-iced-app/crates/hcie-iced-gui/src/theme.rs index 6009109..8009ac2 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/theme.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/theme.rs @@ -1,7 +1,10 @@ //! Theme system — shared color tokens for all panels. //! -//! Uses the iced-panel-adapter ThemeColors for consistent styling -//! across all panels and the main application. +//! **Purpose:** Provides Photopea-matched dark UI palette as the default. +//! Uses iced-panel-adapter ThemeColors for consistent styling across all panels. +//! +//! **Design reference:** Photopea image editor (93 screenshots analyzed). +//! Default preset: `Photopea` — exact RGB values from Photopea's dark theme. pub use iced_panel_adapter::theme::{ThemeColors, ThemePreset}; diff --git a/hcie-iced-app/crates/iced-panel-adapter/src/theme.rs b/hcie-iced-app/crates/iced-panel-adapter/src/theme.rs index db74e1d..9bd18a3 100644 --- a/hcie-iced-app/crates/iced-panel-adapter/src/theme.rs +++ b/hcie-iced-app/crates/iced-panel-adapter/src/theme.rs @@ -1,14 +1,25 @@ //! Theme colors and presets for the Iced GUI panels. //! -//! Mirrors the egui-panel-adapter theme system with the same color tokens -//! for visual consistency across both GUI implementations. +//! **Purpose:** Provides a Photopea-matched dark UI palette as the default theme. +//! Color values extracted from 93 Photopea screenshots for pixel-accurate matching. +//! +//! **Design tokens:** +//! - 3-tier surface elevation: recessed → panel → elevated +//! - Photopea exact RGB values for dark theme +//! - Sharp 2px corners, tight spacing +//! - Green accent for active tools (#4caf50) +//! - Blue accent for selections/links (#4a9eff) use iced::Color; /// Available theme presets. +/// +/// `Photopea` is the default — matches the Photopea image editor's dark UI. +/// Other presets are kept as alternatives. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum ThemePreset { #[default] + Photopea, Photoshop, ProDark, Amoled, @@ -17,100 +28,182 @@ pub enum ThemePreset { } /// Named color tokens for the application theme. +/// +/// Implements Photopea's 3-tier surface elevation system: +/// - `bg_recessed`: deepest surface (input fields, slider tracks) +/// - `bg_panel`: mid surface (docked panels, side columns) +/// - `bg_elevated`: top surface (popups, tooltips, dropdowns) #[derive(Debug, Clone, Copy)] pub struct ThemeColors { + /// Workspace / application background (deepest surface). pub bg_app: Color, + /// Docked panel backgrounds (mid surface). pub bg_panel: Color, + /// Hover feedback highlight. pub bg_hover: Color, + /// Active/selected state background. pub bg_active: Color, + /// Active element accent (selections, links, submenu arrows). pub accent: Color, + /// Accent hover state. pub accent_hover: Color, - pub border_low: Color, - pub border_high: Color, - pub text_primary: Color, - pub text_secondary: Color, - pub text_disabled: Color, + /// Active tool highlight (green, like Photopea toolbox). + pub accent_green: Color, + /// Warning/danger accent (red, like Photopea "Account" button). pub danger: Color, + /// Subdued low-contrast dividers. + pub border_low: Color, + /// High-contrast outer borders. + pub border_high: Color, + /// Primary high-readability text. + pub text_primary: Color, + /// Secondary muted text. + pub text_secondary: Color, + /// Disabled/very muted text. + pub text_disabled: Color, + /// Menu dropdown background (light, like Photopea menus). + pub menu_bg: Color, + /// Menu item hover highlight. + pub menu_hover: Color, + /// Menu text color (dark on light background). + pub menu_text: Color, + /// Menu keyboard shortcut text. + pub menu_shortcut: Color, + /// Light mode vs dark mode flag. pub is_light: bool, } impl ThemeColors { /// Resolve colors for a given preset. + /// + /// `Photopea` preset uses exact RGB values extracted from screenshots. pub fn get(preset: ThemePreset) -> Self { match preset { + // ── Photopea (Default) ───────────────────────────────────── + // Exact colors from 93 Photopea screenshots. + ThemePreset::Photopea => Self { + bg_app: Color::from_rgb(0.118, 0.118, 0.118), // #1e1e1e - canvas + bg_panel: Color::from_rgb(0.165, 0.165, 0.165), // #2a2a2a - panels + bg_hover: Color::from_rgb(0.227, 0.227, 0.227), // #3a3a3a - hover + bg_active: Color::from_rgb(0.200, 0.200, 0.200), // #333333 - active tab + accent: Color::from_rgb(0.290, 0.620, 1.000), // #4a9eff - selections + accent_hover: Color::from_rgb(0.350, 0.680, 1.000), + accent_green: Color::from_rgb(0.298, 0.686, 0.314), // #4caf50 - tool active + danger: Color::from_rgb(0.906, 0.298, 0.235), // #e74c3c - account btn + border_low: Color::from_rgb(0.200, 0.200, 0.200), // #333333 - dividers + border_high: Color::from_rgb(0.267, 0.267, 0.267), // #444444 - borders + text_primary: Color::from_rgb(0.878, 0.878, 0.878), // #e0e0e0 - primary + text_secondary:Color::from_rgb(0.600, 0.600, 0.600), // #999999 - muted + text_disabled: Color::from_rgb(0.400, 0.400, 0.400), // #666666 - disabled + menu_bg: Color::from_rgb(0.910, 0.910, 0.910), // #e8e8e8 - menu bg + menu_hover: Color::from_rgb(0.702, 0.851, 1.000), // #b3d9ff - menu hover + menu_text: Color::from_rgb(0.000, 0.000, 0.000), // #000000 - menu text + menu_shortcut: Color::from_rgb(0.400, 0.400, 0.400), // #666666 - shortcuts + is_light: false, + }, + // ── Photoshop (Legacy) ───────────────────────────────────── ThemePreset::Photoshop => Self { - bg_app: Color::from_rgb(0.118, 0.118, 0.118), // #1e1e1e - bg_panel: Color::from_rgb(0.176, 0.176, 0.176), // #2d2d2d - bg_hover: Color::from_rgb(0.227, 0.227, 0.227), // #3a3a3a - bg_active: Color::from_rgb(0.251, 0.251, 0.251), // #404040 (neutral, NOT blue) - accent: Color::from_rgb(0.176, 0.549, 0.922), // #2d8ceb - accent_hover: Color::from_rgb(0.220, 0.600, 0.950), - border_low: Color::from_rgb(0.102, 0.102, 0.102), // #1a1a1a - border_high: Color::from_rgb(0.267, 0.267, 0.267), // #444444 - text_primary: Color::from_rgb(0.800, 0.800, 0.800), // #cccccc - text_secondary: Color::from_rgb(0.533, 0.533, 0.533), // #888888 - text_disabled: Color::from_rgb(0.333, 0.333, 0.333), // #555555 - danger: Color::from_rgb(0.800, 0.200, 0.200), + bg_app: Color::from_rgb(0.110, 0.110, 0.118), + bg_panel: Color::from_rgb(0.196, 0.196, 0.212), + bg_hover: Color::from_rgb(0.243, 0.243, 0.259), + bg_active: Color::from_rgb(0.227, 0.227, 0.243), + accent: Color::from_rgb(0.220, 0.471, 0.863), + accent_hover: Color::from_rgb(0.260, 0.520, 0.900), + accent_green: Color::from_rgb(0.298, 0.686, 0.314), + danger: Color::from_rgb(0.800, 0.200, 0.200), + border_low: Color::from_rgb(0.157, 0.157, 0.165), + border_high: Color::from_rgb(0.251, 0.251, 0.267), + text_primary: Color::from_rgb(0.878, 0.878, 0.878), + text_secondary:Color::from_rgb(0.580, 0.580, 0.596), + text_disabled: Color::from_rgb(0.350, 0.350, 0.360), + menu_bg: Color::from_rgb(0.910, 0.910, 0.910), + menu_hover: Color::from_rgb(0.702, 0.851, 1.000), + menu_text: Color::from_rgb(0.000, 0.000, 0.000), + menu_shortcut: Color::from_rgb(0.400, 0.400, 0.400), is_light: false, }, + // ── Monokai ──────────────────────────────────────────────── ThemePreset::ProDark => Self { - bg_app: Color::from_rgb(0.149, 0.149, 0.149), - bg_panel: Color::from_rgb(0.188, 0.188, 0.188), - bg_hover: Color::from_rgb(0.235, 0.235, 0.235), - bg_active: Color::from_rgb(0.165, 0.345, 0.482), - accent: Color::from_rgb(0.165, 0.345, 0.482), - accent_hover: Color::from_rgb(0.196, 0.396, 0.553), - border_low: Color::from_rgb(0.118, 0.118, 0.118), - border_high: Color::from_rgb(0.314, 0.314, 0.314), - text_primary: Color::from_rgb(0.933, 0.933, 0.933), - text_secondary: Color::from_rgb(0.604, 0.604, 0.604), - text_disabled: Color::from_rgb(0.400, 0.400, 0.400), - danger: Color::from_rgb(0.780, 0.220, 0.267), + bg_app: Color::from_rgb(0.086, 0.082, 0.071), + bg_panel: Color::from_rgb(0.153, 0.157, 0.133), + bg_hover: Color::from_rgb(0.212, 0.220, 0.188), + bg_active: Color::from_rgb(0.180, 0.184, 0.160), + accent: Color::from_rgb(0.902, 0.588, 0.196), + accent_hover: Color::from_rgb(0.940, 0.640, 0.260), + accent_green: Color::from_rgb(0.298, 0.686, 0.314), + danger: Color::from_rgb(0.780, 0.220, 0.267), + border_low: Color::from_rgb(0.110, 0.106, 0.086), + border_high: Color::from_rgb(0.243, 0.251, 0.212), + text_primary: Color::from_rgb(0.973, 0.973, 0.941), + text_secondary:Color::from_rgb(0.580, 0.588, 0.533), + text_disabled: Color::from_rgb(0.350, 0.350, 0.330), + menu_bg: Color::from_rgb(0.910, 0.910, 0.910), + menu_hover: Color::from_rgb(0.702, 0.851, 1.000), + menu_text: Color::from_rgb(0.000, 0.000, 0.000), + menu_shortcut: Color::from_rgb(0.400, 0.400, 0.400), is_light: false, }, + // ── Dracula ──────────────────────────────────────────────── ThemePreset::Amoled => Self { - bg_app: Color::from_rgb(0.059, 0.059, 0.059), - bg_panel: Color::from_rgb(0.118, 0.118, 0.118), - bg_hover: Color::from_rgb(0.176, 0.176, 0.176), - bg_active: Color::from_rgb(0.122, 0.302, 0.455), - accent: Color::from_rgb(0.122, 0.302, 0.455), - accent_hover: Color::from_rgb(0.153, 0.353, 0.518), - border_low: Color::from_rgb(0.078, 0.078, 0.078), - border_high: Color::from_rgb(0.275, 0.275, 0.275), - text_primary: Color::from_rgb(0.945, 0.945, 0.945), - text_secondary: Color::from_rgb(0.565, 0.565, 0.565), - text_disabled: Color::from_rgb(0.353, 0.353, 0.353), - danger: Color::from_rgb(0.753, 0.220, 0.294), + bg_app: Color::from_rgb(0.094, 0.086, 0.133), + bg_panel: Color::from_rgb(0.157, 0.165, 0.212), + bg_hover: Color::from_rgb(0.220, 0.227, 0.290), + bg_active: Color::from_rgb(0.184, 0.192, 0.243), + accent: Color::from_rgb(1.000, 0.475, 0.776), + accent_hover: Color::from_rgb(1.000, 0.530, 0.820), + accent_green: Color::from_rgb(0.298, 0.686, 0.314), + danger: Color::from_rgb(0.753, 0.220, 0.294), + border_low: Color::from_rgb(0.118, 0.110, 0.165), + border_high: Color::from_rgb(0.267, 0.275, 0.345), + text_primary: Color::from_rgb(0.973, 0.973, 0.949), + text_secondary:Color::from_rgb(0.588, 0.596, 0.667), + text_disabled: Color::from_rgb(0.350, 0.350, 0.380), + menu_bg: Color::from_rgb(0.910, 0.910, 0.910), + menu_hover: Color::from_rgb(0.702, 0.851, 1.000), + menu_text: Color::from_rgb(0.000, 0.000, 0.000), + menu_shortcut: Color::from_rgb(0.400, 0.400, 0.400), is_light: false, }, + // ── Solarized Light ──────────────────────────────────────── ThemePreset::PhotoshopLight => Self { - bg_app: Color::from_rgb(0.941, 0.937, 0.925), - bg_panel: Color::from_rgb(0.906, 0.902, 0.890), - bg_hover: Color::from_rgb(0.855, 0.851, 0.839), - bg_active: Color::from_rgb(0.200, 0.400, 0.600), - accent: Color::from_rgb(0.200, 0.400, 0.600), - accent_hover: Color::from_rgb(0.235, 0.447, 0.651), - border_low: Color::from_rgb(0.780, 0.776, 0.765), - border_high: Color::from_rgb(0.600, 0.596, 0.584), - text_primary: Color::from_rgb(0.145, 0.141, 0.133), - text_secondary: Color::from_rgb(0.400, 0.396, 0.388), - text_disabled: Color::from_rgb(0.600, 0.596, 0.588), - danger: Color::from_rgb(0.800, 0.200, 0.200), + bg_app: Color::from_rgb(0.992, 0.965, 0.890), + bg_panel: Color::from_rgb(1.000, 0.980, 0.922), + bg_hover: Color::from_rgb(0.933, 0.910, 0.835), + bg_active: Color::from_rgb(0.878, 0.855, 0.780), + accent: Color::from_rgb(0.796, 0.294, 0.086), + accent_hover: Color::from_rgb(0.840, 0.340, 0.140), + accent_green: Color::from_rgb(0.298, 0.686, 0.314), + danger: Color::from_rgb(0.800, 0.200, 0.200), + border_low: Color::from_rgb(0.871, 0.847, 0.769), + border_high: Color::from_rgb(0.784, 0.753, 0.667), + text_primary: Color::from_rgb(0.196, 0.196, 0.157), + text_secondary:Color::from_rgb(0.471, 0.463, 0.392), + text_disabled: Color::from_rgb(0.650, 0.640, 0.580), + menu_bg: Color::from_rgb(0.910, 0.910, 0.910), + menu_hover: Color::from_rgb(0.702, 0.851, 1.000), + menu_text: Color::from_rgb(0.000, 0.000, 0.000), + menu_shortcut: Color::from_rgb(0.400, 0.400, 0.400), is_light: true, }, + // ── Nord ─────────────────────────────────────────────────── ThemePreset::ProLight => Self { - bg_app: Color::from_rgb(0.961, 0.961, 0.969), - bg_panel: Color::from_rgb(0.918, 0.918, 0.929), - bg_hover: Color::from_rgb(0.867, 0.867, 0.882), - bg_active: Color::from_rgb(0.263, 0.475, 0.675), - accent: Color::from_rgb(0.263, 0.475, 0.675), - accent_hover: Color::from_rgb(0.294, 0.514, 0.710), - border_low: Color::from_rgb(0.820, 0.820, 0.835), - border_high: Color::from_rgb(0.651, 0.651, 0.671), - text_primary: Color::from_rgb(0.161, 0.169, 0.184), - text_secondary: Color::from_rgb(0.420, 0.427, 0.447), - text_disabled: Color::from_rgb(0.620, 0.627, 0.647), - danger: Color::from_rgb(0.820, 0.220, 0.220), + bg_app: Color::from_rgb(0.894, 0.910, 0.933), + bg_panel: Color::from_rgb(0.925, 0.937, 0.957), + bg_hover: Color::from_rgb(0.847, 0.871, 0.902), + bg_active: Color::from_rgb(0.816, 0.839, 0.871), + accent: Color::from_rgb(0.369, 0.506, 0.675), + accent_hover: Color::from_rgb(0.410, 0.540, 0.710), + accent_green: Color::from_rgb(0.298, 0.686, 0.314), + danger: Color::from_rgb(0.820, 0.220, 0.220), + border_low: Color::from_rgb(0.831, 0.855, 0.886), + border_high: Color::from_rgb(0.706, 0.737, 0.784), + text_primary: Color::from_rgb(0.180, 0.204, 0.251), + text_secondary:Color::from_rgb(0.392, 0.431, 0.502), + text_disabled: Color::from_rgb(0.580, 0.610, 0.650), + menu_bg: Color::from_rgb(0.910, 0.910, 0.910), + menu_hover: Color::from_rgb(0.702, 0.851, 1.000), + menu_text: Color::from_rgb(0.000, 0.000, 0.000), + menu_shortcut: Color::from_rgb(0.400, 0.400, 0.400), is_light: true, }, }