Implement Photopea theme and tool settings panel
- Added Photopea theme implementation report and design specifications. - Created tool options and settings panels to match Photopea's layout. - Implemented settings persistence to save and load user preferences. - Updated toolbar to include SVG icons and tool options in a single row. - Enhanced tool settings with specific parameters for each tool type. - Added functionality for resetting tool settings to defaults.
This commit is contained in:
@@ -1 +1 @@
|
||||
{"pid":373799,"startedAt":1783749172698}
|
||||
{"pid":3241632,"startedAt":1783979147093}
|
||||
@@ -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)
|
||||
```
|
||||
@@ -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 |
|
||||
@@ -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<iced::window::Id>,
|
||||
/// 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();
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ mod dialogs;
|
||||
mod dock;
|
||||
mod io;
|
||||
mod panels;
|
||||
mod settings;
|
||||
mod sidebar;
|
||||
mod theme;
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -92,7 +92,7 @@ pub fn view(selected_filter: Option<FilterType>, 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<FilterType>, 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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
]
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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<MenuDef> {
|
||||
// ── 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<MenuDef> {
|
||||
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<MenuDef> {
|
||||
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<MenuDef> {
|
||||
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<usize>, recent_files: &[crate::app::RecentFileEntry]) -> Option<Element<'static, Message>> {
|
||||
/// 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<usize>,
|
||||
recent_files: &[crate::app::RecentFileEntry],
|
||||
colors: ThemeColors,
|
||||
) -> Option<Element<'static, Message>> {
|
||||
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<usize>, 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<usize>, 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<usize>, 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<usize>, 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<usize>, 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<usize>, 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<usize>, 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<usize>, 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,
|
||||
})
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<usize>,
|
||||
) -> 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()
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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<F>(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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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<String>,
|
||||
/// Panel order in the dock.
|
||||
pub panel_order: Vec<String>,
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user