GOOD feat: add Layer Styles panel and enhance filter categories
- Introduced a new Layer Styles panel that displays effects with toggle checkboxes for quick enable/disable functionality. - Updated the filters panel to support collapsible filter categories, allowing users to expand or collapse categories. - Enhanced the application state to manage expanded filter categories using a HashMap. - Modified the layout and sizing of various panels to accommodate the new Layer Styles panel. - Updated theme colors for better visual consistency across the application. - Improved the toolbar to streamline brush size and opacity controls.
This commit is contained in:
@@ -1,234 +1,135 @@
|
||||
# ICED GUI Visual & Behavioral Polish Plan
|
||||
# ICED GUI Visual Parity with egui
|
||||
|
||||
## Context
|
||||
|
||||
This plan addresses the visual and functional issues surfaced during recent ICED GUI testing. The focus is on the Iced build (`hcie-iced-app/crates/hcie-iced-gui/`). Items are grouped by theme and ordered to minimize code conflicts.
|
||||
The current Iced GUI has the same panels as the egui version but several visual/behavioral gaps remain. The goal is to bring the Iced GUI to egui-level polish. This plan is based on a side-by-side pixel-level comparison of `_images/egui.png` (target) vs the current Iced GUI.
|
||||
|
||||
---
|
||||
|
||||
## Gap Analysis (egui vs Iced)
|
||||
|
||||
| # | Area | egui | Iced | Gap |
|
||||
|---|------|------|------|-----|
|
||||
| 1 | **Menu bar** | File, Edit, Tools, Image, Layer, Filter, Select, View, Window, Help (10 items) | File, Edit, Image, Select, View, Filter, Plugin, Window, More, AI, Help (11 items) | Iced has extra Plugin/AI/More menus cluttering the bar; egui has Tools/Layer menus instead |
|
||||
| 2 | **Toolbar** | Compact: Brush dropdown + Size + Opacity | Long: Brush Size + Opacity + Hardness + Tolerance + Feather + Blur Radius + brush tip icons | Iced toolbar is too wide/cluttered; egui is minimal |
|
||||
| 3 | **Brushes grid** | 3-column grid with thumbnail previews | 2-column grid (constrained by narrow dock width) | Iced grid needs more horizontal space |
|
||||
| 4 | **Filter list** | Categorized + collapsible (Blur ▸, Sharpen ▸, Pixelate ▸, Distort ▸, Stylize ▸, Color & Light ▸) | Flat list with no categories | Iced filters need categories + collapsible groups |
|
||||
| 5 | **Layer panel** | Clean: thumbnail + name + eye + lock; opacity/blending inline; +Lay/+Vec/+Grp/Flatten toolbar | Shows "opacity: unavailable in hcie-engine-api fx" debug text; missing layer action buttons | Ugly debug text visible; missing quick-action buttons |
|
||||
| 6 | **Color Palette** | Prominent: large color wheel, FG/BG swatches, Hex input, W/H/G tabs | Same panels exist but ColorPicker is squashed at bottom of right column | ColorPicker needs more vertical space |
|
||||
| 7 | **Properties panel** | Shows: layer name, Opacity, Visible checkbox, Blend Mode dropdown, Type, Tool Settings (Size/Opacity/Hardness/Flow/Spacing/Color Variant) | Exists but layout differs | Should match egui's grouped layout |
|
||||
| 8 | **Right column layout** | ColorPalette (top-right) + LayerStyles/History tabs (mid-right) + Properties (bottom-right) | Filters + Layers + ColorPicker + Properties all stacked | Right column overcrowded; Filters shouldn't be on right |
|
||||
| 9 | **Background** | Consistent dark theme `#2b2b2b` with `#353535` panel bg | Lighter warm theme with inconsistent panel colors | Theme needs refinement for darker, more consistent panels |
|
||||
| 10 | **Status bar** | Tool name + Mouse coords + Canvas size + Zoom% + Zoom slider | Similar but slightly different styling | Minor; acceptable |
|
||||
|
||||
---
|
||||
|
||||
## Tasks
|
||||
|
||||
### 1. Fix theme-inconsistent blue accent buttons
|
||||
### 1. Fix layer panel debug text and add action buttons
|
||||
|
||||
**Problem:** Dialogs like `New Image` show preset buttons as solid blue (`#3355cc`), which clashes with the dark theme and looks unnatural.
|
||||
**Problem:** Layer panel shows "opacity: unavailable in hcie-engine-api fx" which is debug text leaking into the UI. Also missing the `+Lay`, `+Vec`, `+Grp`, `Flatten` quick-action buttons that egui has.
|
||||
|
||||
**Files:**
|
||||
- `hcie-iced-app/crates/hcie-iced-gui/src/dialogs/new_image.rs`
|
||||
- `hcie-iced-app/crates/hcie-iced-gui/src/dialogs/mod.rs` (primary/secondary helpers)
|
||||
- `hcie-iced-app/crates/hcie-iced-gui/src/panels/layers.rs`
|
||||
|
||||
**Changes:**
|
||||
- Add a `preset_btn` helper in `dialogs/mod.rs` that uses theme colors (`bg_hover`/`accent` border/text) rather than the default blue.
|
||||
- Use it for all preset rows.
|
||||
- Ensure primary button (Create) remains theme-accent but with hover/pressed states.
|
||||
- Remove or hide any debug/error text about "unavailable in hcie-engine-api fx"
|
||||
- Add a row of action buttons at the bottom of the layer panel: `+Layer`, `+Group`, `Flatten`, matching egui's `[+Lay] [+Vec] [+Grp] [Flatten]` bar
|
||||
- Wire buttons to existing messages (`LayerAdd`, `LayerFlatten`, etc.)
|
||||
|
||||
---
|
||||
|
||||
### 2. Fix color-picker persistence / session recall
|
||||
### 2. Refactor Filters panel into categorized collapsible groups
|
||||
|
||||
**Problem:** The user reports that foreground/background colors are identical on startup and recent colors are not added/persisted.
|
||||
|
||||
**Observed code:**
|
||||
- `load_colors()` is called in `app.rs:1719` and applied.
|
||||
- `add_recent_color()` exists and is called by `FgColorChanged`/`SwapColors`.
|
||||
- `save_colors()` is called when `colors_dirty` is true.
|
||||
|
||||
**Likely issues:**
|
||||
- `load_colors` returns `PersistedColors` with `#[serde(default)]`, but if any field is absent it becomes `Default`, which is `None`/`empty`. This is fine.
|
||||
- **Root cause candidates:**
|
||||
- `Message::ResetColors` exists, but there may be no UI button mapped to it in the color picker panel.
|
||||
- The saved colors file may not be written because `colors_dirty` is only set on `FgColorChanged`/`BgColorChanged`/`SwapColors`/`ResetColors`. Some popup color changes may not route through those messages.
|
||||
- When the app starts, `fg_color` and `bg_color` are initialized to the same default (`[0,0,0,255]`) before `load_colors()` runs, and then `load_colors` overrides them. This should be correct, but verify.
|
||||
|
||||
**Changes:**
|
||||
- Ensure `PopupColorChanged` routed to `Foreground`/`Background` actually triggers `add_recent_color` and sets `colors_dirty`. It does via `FgColorChanged`/`BgColorChanged`.
|
||||
- Add a **Reset** button in the color picker panel (`color_picker.rs`) that emits `Message::ResetColors`.
|
||||
- Verify `load_colors()` correctly falls back when file is missing; add debug log if parsing fails.
|
||||
- Ensure `save_colors()` stores alpha and recent list (already does).
|
||||
- Remove or explain why `fg_color`/`bg_color` default to the same value in `IcedDocument` / `HcieIcedApp::new` before loading.
|
||||
|
||||
---
|
||||
|
||||
### 3. Right-click to pick secondary color in color palette panel
|
||||
|
||||
**Problem:** Right-click on palette/recent colors should set the background color.
|
||||
|
||||
**Observed code:**
|
||||
- `color_picker.rs` already has `.on_right_press(Message::BgColorChanged(...))` on palette grid cells and recent colors.
|
||||
- However, the wheel canvas only handles right-click inside `ColorWheel::update` and sends `Message::BgColorChanged(color)`.
|
||||
|
||||
**Changes:**
|
||||
- Verify right-click events are not swallowed by the panel/container.
|
||||
- If Iced does not deliver right-clicks on `mouse_area` children reliably, wrap the whole color picker content in a `mouse_area` with `on_right_press` to set background color from current foreground? No — user expects picking the swatch under cursor.
|
||||
- Prefer: keep per-swatch right-click handlers and add a test or visual verification.
|
||||
- If still broken, investigate whether `mouse_area` in Iced 0.13 requires an explicit background hit; add a transparent container around each swatch.
|
||||
|
||||
---
|
||||
|
||||
### 4. Enforce color palette panel minimum height so content stays visible
|
||||
|
||||
**Problem:** Color Palette panel can be collapsed too short; its contents become hidden.
|
||||
|
||||
**File:** `hcie-iced-app/crates/hcie-iced-gui/src/dock/sizing.rs`
|
||||
|
||||
**Change:**
|
||||
- Raise `PaneType::ColorPicker` minimum height from `300.0` to `420.0`.
|
||||
- Consider also raising `min` width from `180.0` to `200.0`.
|
||||
- Ensure the color picker `view` uses `scrollable(content)` so overflow remains usable.
|
||||
|
||||
---
|
||||
|
||||
### 5. Filters panel: parameter sliders should fill ~80% of panel width
|
||||
|
||||
**Problem:** Filter parameter sliders are too narrow and leave large unused space.
|
||||
|
||||
**File:** `hcie-iced-app/crates/hcie-iced-gui/src/panels/filter_params.rs`
|
||||
|
||||
**Changes:**
|
||||
- Audit all `float_slider` / `int_slider` calls and ensure the underlying `plain_slider` width is `Length::Fill` inside a container with `width(Length::Fill)`.
|
||||
- In `filters.rs`, wrap the parameter section in a container with `width(Length::Fill)` and ensure `scrollable(params_panel)` does not clamp width.
|
||||
- Add `width(Length::Fill)` to the `column!` returned by `filter_params::view` if not already present.
|
||||
|
||||
---
|
||||
|
||||
### 6. Filter operations should integrate with history / undo
|
||||
|
||||
**Problem:** Filter Apply is not undoable via the History panel.
|
||||
|
||||
**File:** `hcie-iced-app/crates/hcie-iced-gui/src/app.rs`
|
||||
|
||||
**Change:**
|
||||
- Before `engine.apply_filter(...)` in `Message::FilterApply`, call `engine.push_active_layer_snapshot(...)` (or the appropriate engine history API) with a description like `"Apply <filter_name>"`.
|
||||
- After apply, refresh history cache (`refresh_history`) so the new entry appears in the History panel.
|
||||
- Ensure undo (`Message::HistoryJump`) and redo paths still work after the snapshot is pushed.
|
||||
|
||||
---
|
||||
|
||||
### 7. Brush button should reveal Brushes panel if closed
|
||||
|
||||
**Problem:** User says brush button does not open the Brushes panel.
|
||||
|
||||
**Observed code:** `Message::ToolSelected(Tool::Brush)` already calls `self.dock.reopen_pane(PaneType::Brushes)` at `app.rs:2289`.
|
||||
|
||||
**Possible issue:** `reopen_pane` may silently fail when the pane is auto-hidden or floating; or the brush tool can also be selected via slot click without this path.
|
||||
|
||||
**Changes:**
|
||||
- Verify `ToolSlotClicked`/`SubtoolsToggle` also triggers `reopen_pane(PaneType::Brushes)` when the resulting tool is `Tool::Brush`.
|
||||
- Add a helper `ensure_brushes_panel()` and call it wherever the active tool becomes `Tool::Brush`.
|
||||
- Test with panel closed, auto-hidden, and in a different tab group.
|
||||
|
||||
---
|
||||
|
||||
### 8. Selecting a filter from the Filters menu should reveal the Filters panel
|
||||
|
||||
**Problem:** Filter menu selection should open/raise the Filters panel.
|
||||
|
||||
**Observed code:** `Message::FilterSelect(filter)` already calls `self.dock.reopen_pane(PaneType::Filters)` at `app.rs:4628`.
|
||||
|
||||
**Possible issue:** Same as above — `reopen_pane` may not handle auto-hide/floating states.
|
||||
|
||||
**Changes:**
|
||||
- Audit `DockState::reopen_pane` to ensure it restores auto-hidden panels, focuses existing floating panels, and brings the pane to front if stacked.
|
||||
- If `reopen_pane` is already correct, add visual verification; otherwise fix it.
|
||||
|
||||
---
|
||||
|
||||
### 9. Menu bar vertical padding (+2 px already planned, verify in final screenshot)
|
||||
|
||||
**File:** `hcie-iced-app/crates/hcie-iced-gui/src/panels/title_bar.rs`
|
||||
|
||||
**Already changed:** `MENU_BAR_HEIGHT` 28→30 and menu button widths increased.
|
||||
|
||||
**Remaining:** Confirm no clipping occurs with the new widths; adjust further if "Image" still truncates.
|
||||
|
||||
---
|
||||
|
||||
### 10. Prevent zero-size document creation (wgpu crash)
|
||||
|
||||
**Problem:** The New Image dialog allows width or height to be set to 0. Creating a 0×N document causes the renderer to request a texture with zero height (`resize_texture 800×600 → 4096×0`), which triggers a wgpu validation error and panic.
|
||||
**Problem:** egui shows filters organized into expandable categories (Blur ▸, Sharpen ▸, Pixelate ▸, Distort ▸, Stylize ▸, Color & Light ▸). Iced shows a flat unordered list.
|
||||
|
||||
**Files:**
|
||||
- `hcie-iced-app/crates/hcie-iced-gui/src/dialogs/new_image.rs`
|
||||
- `hcie-iced-app/crates/hcie-iced-gui/src/app.rs` (`DialogNewImageCreate` handler)
|
||||
- `hcie-iced-app/crates/hcie-iced-gui/src/panels/filters.rs`
|
||||
|
||||
**Changes:**
|
||||
- Clamp the width/height sliders to a minimum of `1` in `new_image.rs`.
|
||||
- In `DialogNewImageCreate`, validate `dialog_new_width >= 1 && dialog_new_height >= 1` before creating the engine; if invalid, keep the dialog open and log a warning (or show inline validation text).
|
||||
- Optionally disable the Create button when either dimension is zero.
|
||||
- Remove or ignore the `Custom` preset with `width: 0, height: 0` since it is not a valid document size.
|
||||
- Define filter categories with labels: "Blur", "Sharpen", "Pixelate", "Distort", "Stylize", "Color & Light"
|
||||
- Each category is a collapsible section (▶/▼ toggle) containing its filter items
|
||||
- Add a `FilterCategoryExpanded` state field in `HcieIcedApp` to track which categories are open (default: all open)
|
||||
- Add a `Message::FilterCategoryToggle(String)` message
|
||||
- When no filter is selected, show "Filter Selection" header as in egui
|
||||
- Keep existing filter selection + parameter panel behavior
|
||||
|
||||
---
|
||||
|
||||
### 11. Expose spray particle size / density in the UI
|
||||
### 3. Simplify the merged toolbar to match egui
|
||||
|
||||
**Problem:** Spray tool radius/density settings are present in `tool_settings.rs` but not visible in the merged toolbar or Properties panel.
|
||||
|
||||
**Observed code:**
|
||||
- `tool_settings.rs:132-179` already has `spray_settings()` with Particle Size and Density sliders.
|
||||
- `properties.rs:134` groups `Tool::Spray` with brush properties, which only shows Size/Opacity/Hardness.
|
||||
- `toolbar.rs:95` also groups Spray with Brush and only shows Size/Hardness.
|
||||
|
||||
**Changes:**
|
||||
- In `properties.rs`, when `active_tool == Tool::Spray`, call a new `spray_properties(...)` helper that includes Size, Particle Size, Density, Opacity, and Hardness.
|
||||
- In `toolbar.rs`, add a Spray-specific branch that shows Size, Particle Size, and Density (or move the existing spray controls from the Tool Settings panel into the merged toolbar if that is the intended UX).
|
||||
- Ensure the values are read from `settings.tool_settings.spray_particle_size` / `spray_density` and emit the existing `Message::SprayParticleSizeChanged` / `SprayDensityChanged` messages.
|
||||
|
||||
---
|
||||
|
||||
### 12. Add a custom-shape slot / panel so user SVGs under `~/.local/share/hcie/shapes/` are usable
|
||||
|
||||
**Problem:** The user copied extra SVG files into `/home/dev-user/.local/share/hcie/shapes/`, but they did not appear in the UI and could not be used.
|
||||
|
||||
**Observed code:**
|
||||
- `Engine::new()` initializes `shape_catalog` from `dirs::data_local_dir()/hcie/shapes` (matches `~/.local/share/hcie/shapes`) in `hcie-engine-api/src/lib.rs:558-562`.
|
||||
- `ShapeCatalog::refresh()` in `hcie-engine-api/src/shape_catalog.rs` scans `*.svg` files there and assigns them indices.
|
||||
- `Tool::CustomShape(idx)` exists and is handled in `app.rs:5945-5966`, using a snapshot of `shape_catalog.all()` during `VectorDrawEnd`.
|
||||
- `sidebar/mod.rs` `TOOL_SLOTS` does **not** contain a slot for `Tool::CustomShape`. The vector-shapes slot only lists built-in `VectorArrow`, `VectorStar`, etc.
|
||||
- There is no panel or popup listing the catalog entries for selection.
|
||||
|
||||
**Root cause:** The ICED sidebar has no entry point for `Tool::CustomShape`, so even though the engine loads custom SVGs from `~/.local/share/hcie/shapes/`, the user cannot select or draw them.
|
||||
|
||||
**Decision:** New dock panel with real SVG previews via `iced::widget::svg::Handle::from_memory`.
|
||||
|
||||
**Changes:**
|
||||
- Add `PaneType::CustomShapes` to `dock/state.rs` with label "Custom Shapes".
|
||||
- Create `hcie-iced-app/crates/hcie-iced-gui/src/panels/custom_shapes.rs`.
|
||||
- Accept `shapes: &[ShapeEntry]` and `selected_index: Option<usize>`.
|
||||
- Render each entry as a small card (e.g. 56×56 preview + label below).
|
||||
- Use `iced::widget::Svg::new(Handle::from_memory(entry.svg.into_bytes()))` for the preview, sized to fit the card.
|
||||
- On click, emit `Message::ToolSelected(Tool::CustomShape(index))`.
|
||||
- Highlight the selected card with `colors.accent` border.
|
||||
- Wire the new panel in `dock/view.rs`.
|
||||
- Add the panel to `settings.rs` default `visible_panels` / `panel_order` so it appears in the initial dock layout.
|
||||
- Add a "Refresh" button in the panel that emits `Message::RefreshCustomShapes`; handler calls `engine.shape_catalog.refresh()` and updates `app.cached_custom_shapes`.
|
||||
- Store a cached `Vec<(String, String)>` of custom shapes in `HcieIcedApp` so the panel can be rebuilt cheaply; refresh it whenever the panel is shown or when Refresh is pressed.
|
||||
- Ensure `Tool::CustomShape(idx)` out-of-range cases return `None` in `VectorDrawEnd` (already mostly true; add explicit guard).
|
||||
- Verify that copied SVGs are valid — `ShapeCatalog` silently skips unreadable / non-SVG files.
|
||||
|
||||
### 12b. Custom Shape drawing not triggering on canvas
|
||||
|
||||
**Problem:** Selecting a custom shape in the Custom Shapes panel does not actually draw it on the canvas when dragging.
|
||||
|
||||
**Root cause:** The shared vector-shape drag flow is gated by `is_vector_shape_tool()` and an explicit match arm in the `CanvasPointerPressed` handler. Both locations enumerate the built-in `Tool::Vector*` variants but omit `Tool::CustomShape(_)`, so the custom-shape tool never enters `VectorDrawStart`/`VectorDrawMove`/`VectorDrawEnd` and therefore never reaches the existing `VectorDrawEnd` handler that already knows how to build a `VectorShape::SvgShape` from the catalog.
|
||||
**Problem:** The Iced toolbar shows too many inline sliders (Size, Opacity, Hardness, Tolerance, Feather, Blur Radius, brush tip icons). egui shows only: Brush dropdown + Size + Opacity.
|
||||
|
||||
**Files:**
|
||||
- `hcie-iced-app/crates/hcie-iced-gui/src/app.rs`
|
||||
- `hcie-iced-app/crates/hcie-iced-gui/src/sidebar/mod.rs` (icon support already present; no change)
|
||||
- `hcie-iced-app/crates/hcie-iced-gui/src/panels/toolbar.rs`
|
||||
|
||||
**Changes:**
|
||||
1. In `app.rs` helper `is_vector_shape_tool()` (around line 1287), add `Tool::CustomShape(_)` to the `matches!` pattern.
|
||||
2. In `app.rs` `CanvasPointerPressed` handler (around line 2824), add `Tool::CustomShape(_)` to the arm that starts `Message::VectorDrawStart`.
|
||||
3. Keep `VectorDrawEnd` unchanged; it already snapshots `shape_catalog.all()` (line 5528-5539) and builds a `VectorShape::SvgShape` for `Tool::CustomShape(idx)` with correct bounds.
|
||||
- Reduce toolbar to: [Tool Icon] [Brush Style Dropdown] [Size: ____] [Opacity: ____]
|
||||
- Move Hardness, Tolerance, Feather, Blur Radius to the Properties/Tool Settings panel
|
||||
- Keep brush tip icon row only when a brush tool is active
|
||||
- Make the toolbar narrower and less cluttered
|
||||
|
||||
**Edge cases to consider:**
|
||||
- Out-of-range `Tool::CustomShape(idx)` already returns `None` from `VectorDrawEnd` and produces no shape.
|
||||
- If the active layer is a locked vector layer, the shape-flow first switches to a safe non-vector layer before drawing, just like built-in vector shapes.
|
||||
---
|
||||
|
||||
**Validation:**
|
||||
1. Build: `cargo check -p hcie-iced-gui` and `cargo test -p hcie-iced-gui`.
|
||||
2. Run the Iced GUI.
|
||||
3. Window → Custom Shapes → select a shape.
|
||||
4. Drag on the canvas → a new vector layer containing the custom SVG shape appears.
|
||||
5. Undo reverts the inserted shape (history snapshot is pushed by `engine.add_vector_shape`).
|
||||
### 4. Add Layer Styles panel tab alongside History
|
||||
|
||||
**Problem:** egui has a tabbed right panel with "Layer Styles" / "History" / "AI Assistant" tabs. Iced has no Layer Styles panel.
|
||||
|
||||
**Files:**
|
||||
- `hcie-iced-app/crates/hcie-iced-gui/src/panels/layer_styles.rs` (new)
|
||||
- `hcie-iced-app/crates/hcie-iced-gui/src/dock/state.rs` (add `PaneType::LayerStyles`)
|
||||
- `hcie-iced-app/crates/hcie-iced-gui/src/dock/view.rs` (wire panel)
|
||||
|
||||
**Changes:**
|
||||
- Create a minimal Layer Styles panel that lists common effects: Drop Shadow, Inner Shadow, Outer Glow, Inner Glow, Bevel & Emboss, Satin, Color Overlay, Gradient Overlay, Pattern Overlay, Stroke
|
||||
- Each effect has a checkbox to enable and an expandable settings section
|
||||
- Wire to existing `LayerStyle*` messages
|
||||
- This is a display-only first pass; detailed per-effect parameter editing can follow
|
||||
|
||||
---
|
||||
|
||||
### 5. Widen the right column for Color Palette prominence
|
||||
|
||||
**Problem:** Color Palette is squeezed at the bottom of the right column in Iced. In egui, it's prominent with a large color wheel occupying the top-right area.
|
||||
|
||||
**Files:**
|
||||
- `hcie-iced-app/crates/hcie-iced-gui/src/dock/state.rs` (adjust default layout ratios)
|
||||
- `hcie-iced-app/crates/hcie-iced-gui/src/dock/sizing.rs` (adjust minimum sizes)
|
||||
|
||||
**Changes:**
|
||||
- Swap the default right-column layout: put ColorPicker on top (with more space) and Layers below
|
||||
- Adjust the right_col1 ratio from 0.5 to ~0.6 (more space for ColorPicker)
|
||||
- Ensure ColorPicker minimum height is at least 400px
|
||||
- Remove Properties/History from the right column entirely (move to auto-hide or tabbed)
|
||||
|
||||
---
|
||||
|
||||
### 6. Darken theme panel backgrounds for consistency
|
||||
|
||||
**Problem:** Iced panels use slightly lighter/warmer backgrounds than egui's `#353535`. The overall look feels less polished.
|
||||
|
||||
**Files:**
|
||||
- `hcie-iced-app/crates/iced-panel-adapter/src/theme.rs`
|
||||
|
||||
**Changes:**
|
||||
- Darken `bg_panel` in all themes by ~5% to match egui's darker panel backgrounds
|
||||
- Ensure `border_low` is subtle (~0.2 opacity) for clean panel separation
|
||||
- Verify `bg_active` provides enough contrast for selected items
|
||||
- Do NOT change accent colors; only adjust background/border tones
|
||||
|
||||
---
|
||||
|
||||
### 7. Add document tab bar with proper styling
|
||||
|
||||
**Problem:** egui shows a document tab bar ("Untitled") above the canvas with a clear active tab style. Iced has a tab bar but it needs polish.
|
||||
|
||||
**Files:**
|
||||
- `hcie-iced-app/crates/hcie-iced-gui/src/dock/view.rs` (document tab bar)
|
||||
|
||||
**Changes:**
|
||||
- Style the active tab with a bottom accent border (matching egui)
|
||||
- Add a `+` button for new document at the right end of the tab bar
|
||||
- Ensure the tab bar uses `bg_panel` background, not `bg_active`
|
||||
|
||||
---
|
||||
|
||||
@@ -236,23 +137,10 @@ This plan addresses the visual and functional issues surfaced during recent ICED
|
||||
|
||||
1. `cargo check -p hcie-iced-gui`
|
||||
2. `cargo test -p hcie-iced-gui`
|
||||
3. Manual launch and visual checks:
|
||||
- New Image dialog presets look theme-aware (not bright blue).
|
||||
- Set width or height to 0 → Create button disabled; preset ignores 0-size values.
|
||||
- Create documents with valid sizes → no wgpu panic.
|
||||
- Spray tool selected → Particle Size / Density sliders visible in Tool Settings panel.
|
||||
- Pick colors, close app, reopen — colors and recent list restored.
|
||||
- Right-click a palette swatch sets background color.
|
||||
- Color Palette panel cannot be resized below content height.
|
||||
- Select Brush tool → Brushes panel appears.
|
||||
- Filter menu → Filters panel appears and parameter sliders are wide.
|
||||
- Apply a filter → History panel gains an "Apply …" entry; Undo reverts it.
|
||||
- Custom SVG files in `~/.local/share/hcie/shapes/` appear in the new Custom Shapes panel and can be drawn.
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Is there a reference image or exact hex for the desired preset button style? (Current theme tokens: `bg_hover`, `accent`, `text_primary`.)
|
||||
- Should the New Image dialog enforce a single minimum (e.g. 1×1) or a practical minimum (e.g. 8×8) to avoid 1-pixel-high canvases?
|
||||
- Should custom shapes be rendered as small SVG previews in the panel, or is a text label list acceptable as a first pass? (Recommended: text list first, SVG preview later.)
|
||||
3. Screenshot comparison after changes:
|
||||
- F12 full viewport screenshot
|
||||
- Compare right-column layout: ColorPicker should be prominent on top
|
||||
- Filters panel should show categorized collapsible groups
|
||||
- Layer panel should have no debug text, should show action buttons
|
||||
- Toolbar should be simplified (3-4 items, not 7+)
|
||||
- Theme colors should be consistently darker
|
||||
|
||||
@@ -216,6 +216,8 @@ pub struct HcieIcedApp {
|
||||
pub filter_params: serde_json::Value,
|
||||
/// Filter preview state.
|
||||
pub filter_preview_active: bool,
|
||||
/// Which filter categories are expanded (name → expanded).
|
||||
pub filter_categories_expanded: std::collections::HashMap<String, bool>,
|
||||
/// Immutable active-layer pixels used by filter and adjustment previews.
|
||||
preview_baseline: Option<PreviewBaseline>,
|
||||
/// Original styles restored when the Layer Style dialog is cancelled.
|
||||
@@ -758,6 +760,7 @@ pub enum Message {
|
||||
FilterPreviewToggle(bool),
|
||||
FilterPreviewApply,
|
||||
FilterPreviewRestore,
|
||||
FilterCategoryToggle(String),
|
||||
|
||||
// ── Dialogs ─────────────────────────────────────────
|
||||
DialogOpen(ActiveDialog),
|
||||
@@ -1634,6 +1637,7 @@ impl HcieIcedApp {
|
||||
selected_filter: None,
|
||||
filter_params: serde_json::json!({}),
|
||||
filter_preview_active: false,
|
||||
filter_categories_expanded: std::collections::HashMap::new(),
|
||||
preview_baseline: None,
|
||||
layer_style_baseline: None,
|
||||
pending_file_operation: None,
|
||||
@@ -4851,6 +4855,14 @@ impl HcieIcedApp {
|
||||
}
|
||||
}
|
||||
|
||||
Message::FilterCategoryToggle(category) => {
|
||||
let entry = self
|
||||
.filter_categories_expanded
|
||||
.entry(category)
|
||||
.or_insert(true);
|
||||
*entry = !*entry;
|
||||
}
|
||||
|
||||
// ── Dialogs ───────────────────────────────────
|
||||
Message::DialogOpen(dialog) => {
|
||||
if matches!(
|
||||
|
||||
@@ -87,7 +87,7 @@ pub fn panel_size_policy(panel: PaneType) -> PaneSizePolicy {
|
||||
width: axis(240.0, 400.0, f32::INFINITY, 30),
|
||||
height: axis(140.0, 320.0, f32::INFINITY, 30),
|
||||
},
|
||||
PaneType::Properties | PaneType::Geometry | PaneType::ToolSettings => PaneSizePolicy {
|
||||
PaneType::Properties | PaneType::Geometry | PaneType::ToolSettings | PaneType::LayerStyles => PaneSizePolicy {
|
||||
width: axis(160.0, 280.0, f32::INFINITY, 25),
|
||||
height: axis(120.0, 280.0, f32::INFINITY, 25),
|
||||
},
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
///! The default layout mirrors the egui GUI arrangement with Canvas centered:
|
||||
///!
|
||||
///! ```text
|
||||
///! +--------+------------+-----------+---------+
|
||||
///! | | | | Layers |
|
||||
///! | Brushes| | |---------|
|
||||
///! |--------| Canvas | ColorBox |Properties|
|
||||
///! | Filters| |-----------|---------|
|
||||
///! | | | History | |
|
||||
///! +--------+------------+-----------+---------+
|
||||
///! +--------+------------+-----------+
|
||||
///! | | | ColorBox |
|
||||
///! | Brushes| |-----------|
|
||||
///! |--------| Canvas | Layers |
|
||||
///! | Filters| | |
|
||||
///! | | | |
|
||||
///! +--------+------------+-----------+
|
||||
///! ```
|
||||
use crate::dock::floating::FloatingDragState;
|
||||
use crate::dock::manager::{
|
||||
@@ -59,11 +59,13 @@ pub enum PaneType {
|
||||
AiScript,
|
||||
/// Custom SVG shapes panel.
|
||||
CustomShapes,
|
||||
/// Layer Styles panel.
|
||||
LayerStyles,
|
||||
}
|
||||
|
||||
impl PaneType {
|
||||
/// All panel payloads in stable declaration order.
|
||||
pub const ALL: [Self; 14] = [
|
||||
pub const ALL: [Self; 15] = [
|
||||
Self::Canvas,
|
||||
Self::Layers,
|
||||
Self::History,
|
||||
@@ -78,6 +80,7 @@ impl PaneType {
|
||||
Self::ToolSettings,
|
||||
Self::AiScript,
|
||||
Self::CustomShapes,
|
||||
Self::LayerStyles,
|
||||
];
|
||||
|
||||
/// Display name for the pane type.
|
||||
@@ -97,6 +100,7 @@ impl PaneType {
|
||||
PaneType::ToolSettings => "Tool Settings",
|
||||
PaneType::AiScript => "AI Script Generator",
|
||||
PaneType::CustomShapes => "Custom Shapes",
|
||||
PaneType::LayerStyles => "Layer Styles",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,6 +142,7 @@ impl PaneType {
|
||||
Self::LayerDetails => "D",
|
||||
Self::ToolSettings => "T",
|
||||
Self::CustomShapes => "CS",
|
||||
Self::LayerStyles => "LS",
|
||||
Self::Canvas => "□",
|
||||
}
|
||||
}
|
||||
@@ -218,10 +223,10 @@ mod tests {
|
||||
assert!(dock.is_pane_open(PaneType::Brushes));
|
||||
assert!(dock.invariants_hold());
|
||||
|
||||
let properties = pane(&dock, PaneType::Properties);
|
||||
assert!(dock.float_pane(properties, (1280.0, 800.0)));
|
||||
assert!(dock.auto_hide_floating(PaneType::Properties, DockEdge::Right));
|
||||
assert!(dock.auto_hidden[1].contains(&PaneType::Properties));
|
||||
let filters = pane(&dock, PaneType::Filters);
|
||||
assert!(dock.float_pane(filters, (1280.0, 800.0)));
|
||||
assert!(dock.auto_hide_floating(PaneType::Filters, DockEdge::Right));
|
||||
assert!(dock.auto_hidden[1].contains(&PaneType::Filters));
|
||||
assert!(dock.invariants_hold());
|
||||
}
|
||||
|
||||
@@ -309,13 +314,13 @@ impl DockState {
|
||||
///
|
||||
/// Dock layout (without toolbox), left → right:
|
||||
/// ```text
|
||||
/// +------------+-----------+-----------+-----------+
|
||||
/// | Brushes | | Layers | |
|
||||
/// |------------| Canvas |-----------|Properties |
|
||||
/// | Filters | | ColorBox |-----------|
|
||||
/// | | |-----------| History |
|
||||
/// +------------+-----------+-----------+-----------+
|
||||
/// ~14% ~66% ~10% ~10%
|
||||
/// +------------+-----------+-----------+
|
||||
/// | Brushes | | ColorBox |
|
||||
/// |------------| Canvas |-----------|
|
||||
/// | Filters | | Layers |
|
||||
/// | | | |
|
||||
/// +------------+-----------+-----------+
|
||||
/// ~14% ~66% ~20%
|
||||
/// ```
|
||||
///
|
||||
/// Axis semantics in iced PaneGrid:
|
||||
@@ -330,41 +335,23 @@ impl DockState {
|
||||
b: Box::new(Configuration::Pane(PaneType::Filters)),
|
||||
};
|
||||
|
||||
// Right column 1: Layers (top) / ColorBox (bottom) — horizontal split
|
||||
let right_col1 = Configuration::Split {
|
||||
// Right column: ColorPicker (top, prominent) / Layers (bottom)
|
||||
let right_column = Configuration::Split {
|
||||
axis: iced::widget::pane_grid::Axis::Horizontal,
|
||||
ratio: 0.5,
|
||||
a: Box::new(Configuration::Pane(PaneType::Layers)),
|
||||
b: Box::new(Configuration::Pane(PaneType::ColorPicker)),
|
||||
ratio: 0.6,
|
||||
a: Box::new(Configuration::Pane(PaneType::ColorPicker)),
|
||||
b: Box::new(Configuration::Pane(PaneType::Layers)),
|
||||
};
|
||||
|
||||
// Right column 2: Properties (top) / History (bottom) — horizontal split
|
||||
let right_col2 = Configuration::Split {
|
||||
axis: iced::widget::pane_grid::Axis::Horizontal,
|
||||
ratio: 0.5,
|
||||
a: Box::new(Configuration::Pane(PaneType::Properties)),
|
||||
b: Box::new(Configuration::Pane(PaneType::History)),
|
||||
};
|
||||
|
||||
// Right side: right_col1 (left) / right_col2 (right) — vertical split
|
||||
let right_side = Configuration::Split {
|
||||
axis: iced::widget::pane_grid::Axis::Vertical,
|
||||
ratio: 0.5,
|
||||
a: Box::new(right_col1),
|
||||
b: Box::new(right_col2),
|
||||
};
|
||||
|
||||
// Center + right: Canvas (left) / right_side (right) — vertical split
|
||||
// Canvas gets ~66% — right panels need room for layers + properties
|
||||
// Center + right: Canvas (left) / right_column (right) — vertical split
|
||||
let center_right = Configuration::Split {
|
||||
axis: iced::widget::pane_grid::Axis::Vertical,
|
||||
ratio: 0.66,
|
||||
a: Box::new(Configuration::Pane(PaneType::Canvas)),
|
||||
b: Box::new(right_side),
|
||||
b: Box::new(right_column),
|
||||
};
|
||||
|
||||
// Root: left_column (left) / center_right (right) — vertical split
|
||||
// Left column gets ~14% of total width.
|
||||
let root_config = Configuration::Split {
|
||||
axis: iced::widget::pane_grid::Axis::Vertical,
|
||||
ratio: 0.14,
|
||||
@@ -862,6 +849,7 @@ impl DockState {
|
||||
| PaneType::Properties
|
||||
| PaneType::History
|
||||
| PaneType::LayerDetails
|
||||
| PaneType::LayerStyles
|
||||
| PaneType::AiScript => iced::widget::pane_grid::Axis::Horizontal,
|
||||
_ => iced::widget::pane_grid::Axis::Vertical,
|
||||
};
|
||||
|
||||
@@ -192,6 +192,7 @@ pub fn panel_body<'a>(
|
||||
.engine
|
||||
.active_layer_is_editable(),
|
||||
colors,
|
||||
&app.filter_categories_expanded,
|
||||
),
|
||||
PaneType::ColorPicker => crate::color_picker::view(
|
||||
&app.fg_color,
|
||||
@@ -302,6 +303,12 @@ pub fn panel_body<'a>(
|
||||
colors,
|
||||
)
|
||||
}
|
||||
PaneType::LayerStyles => {
|
||||
let doc = &app.documents[app.active_doc];
|
||||
let active_id = doc.engine.active_layer_id();
|
||||
let layer_styles = doc.engine.get_layer_styles(active_id);
|
||||
crate::panels::layer_styles::panel_view(layer_styles, colors)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -380,14 +387,14 @@ fn document_tab_bar<'a>(
|
||||
if active {
|
||||
iced::widget::button::Style {
|
||||
background: Some(iced::Background::Color(c.bg_active)),
|
||||
border: iced::Border::default().color(c.accent).width(1),
|
||||
border: iced::Border::default().color(c.accent).width(1.0),
|
||||
text_color: c.accent,
|
||||
..Default::default()
|
||||
}
|
||||
} else {
|
||||
iced::widget::button::Style {
|
||||
background: Some(iced::Background::Color(c.bg_panel)),
|
||||
border: iced::Border::default().color(c.border_low).width(1),
|
||||
border: iced::Border::default().color(c.border_low).width(1.0),
|
||||
text_color: c.text_primary,
|
||||
..Default::default()
|
||||
}
|
||||
@@ -397,14 +404,14 @@ fn document_tab_bar<'a>(
|
||||
if active {
|
||||
iced::widget::button::Style {
|
||||
background: Some(iced::Background::Color(c.bg_active)),
|
||||
border: iced::Border::default().color(c.accent).width(1),
|
||||
border: iced::Border::default().color(c.accent).width(1.0),
|
||||
text_color: c.accent,
|
||||
..Default::default()
|
||||
}
|
||||
} else {
|
||||
iced::widget::button::Style {
|
||||
background: Some(iced::Background::Color(c.bg_hover)),
|
||||
border: iced::Border::default().color(c.border_high).width(1),
|
||||
border: iced::Border::default().color(c.border_high).width(1.0),
|
||||
text_color: c.text_primary,
|
||||
..Default::default()
|
||||
}
|
||||
@@ -412,7 +419,7 @@ fn document_tab_bar<'a>(
|
||||
}
|
||||
_ => iced::widget::button::Style {
|
||||
background: Some(iced::Background::Color(c.bg_active)),
|
||||
border: iced::Border::default().color(c.accent).width(1),
|
||||
border: iced::Border::default().color(c.accent).width(1.0),
|
||||
text_color: c.accent,
|
||||
..Default::default()
|
||||
},
|
||||
@@ -470,6 +477,7 @@ fn pane_type_icon(pane_type: PaneType) -> Option<&'static str> {
|
||||
PaneType::ToolSettings => Some("icons/panel-settings.svg"),
|
||||
PaneType::AiScript => Some("icons/panel-ai.svg"),
|
||||
PaneType::CustomShapes => Some("icons/star.svg"),
|
||||
PaneType::LayerStyles => Some("icons/panel-settings.svg"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -156,54 +156,84 @@ const FILTER_CATEGORIES: &[FilterCategory] = &[
|
||||
|
||||
/// Build the filters panel.
|
||||
///
|
||||
/// Shows the filter category list on top, and when a filter is selected,
|
||||
/// displays its parameter editing panel below the list. The preview checkbox,
|
||||
/// Apply, and Reset buttons are always at the bottom.
|
||||
/// Shows the filter category list on top with collapsible groups, and when a
|
||||
/// filter is selected, displays its parameter editing panel below the list.
|
||||
/// The preview checkbox, Apply, and Reset buttons are always at the bottom.
|
||||
pub fn view(
|
||||
selected_filter: Option<FilterType>,
|
||||
filter_params: &serde_json::Value,
|
||||
filter_preview_active: bool,
|
||||
layer_editable: bool,
|
||||
colors: ThemeColors,
|
||||
categories_expanded: &std::collections::HashMap<String, bool>,
|
||||
) -> Element<'static, Message> {
|
||||
let mut filter_list = column![].spacing(2);
|
||||
|
||||
for category in FILTER_CATEGORIES {
|
||||
let cat_name = text(category.name).size(12);
|
||||
let is_expanded = *categories_expanded.get(category.name).unwrap_or(&true);
|
||||
let arrow = if is_expanded { "\u{25BC}" } else { "\u{25B6}" };
|
||||
|
||||
let mut cat_filters = column![].spacing(1).padding(iced::Padding::ZERO.left(12));
|
||||
let cat_header = button(
|
||||
row![
|
||||
text(format!("{} {}", arrow, category.name)).size(12),
|
||||
]
|
||||
.spacing(4)
|
||||
.align_y(iced::Alignment::Center),
|
||||
)
|
||||
.on_press(Message::FilterCategoryToggle(category.name.to_string()))
|
||||
.width(Length::Fill)
|
||||
.padding([4, 6])
|
||||
.style(move |_theme, 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::TRANSPARENT)),
|
||||
text_color: colors.text_primary,
|
||||
border: iced::Border::default(),
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
|
||||
for entry in category.filters {
|
||||
let is_selected = selected_filter == Some(entry.filter_type);
|
||||
let filter_text = text(entry.name).size(11);
|
||||
let filter_text = if is_selected {
|
||||
filter_text.style(move |_theme: &iced::Theme| iced::widget::text::Style {
|
||||
color: Some(colors.accent),
|
||||
})
|
||||
} else {
|
||||
filter_text
|
||||
};
|
||||
filter_list = filter_list.push(cat_header);
|
||||
|
||||
let item_style = if is_selected {
|
||||
styles::active_item_bg(colors)
|
||||
} else {
|
||||
styles::inactive_item_bg(colors)
|
||||
};
|
||||
if is_expanded {
|
||||
let mut cat_filters = column![].spacing(1).padding(iced::Padding::ZERO.left(12));
|
||||
|
||||
let item = container(filter_text)
|
||||
.width(Length::Fill)
|
||||
.padding([3, 6])
|
||||
.style(move |_theme| item_style.clone());
|
||||
for entry in category.filters {
|
||||
let is_selected = selected_filter == Some(entry.filter_type);
|
||||
let filter_text = text(entry.name).size(11);
|
||||
let filter_text = if is_selected {
|
||||
filter_text.style(move |_theme: &iced::Theme| iced::widget::text::Style {
|
||||
color: Some(colors.accent),
|
||||
})
|
||||
} else {
|
||||
filter_text
|
||||
};
|
||||
|
||||
let filter_type = entry.filter_type;
|
||||
let clickable =
|
||||
iced::widget::mouse_area(item).on_press(Message::FilterSelect(filter_type));
|
||||
let item_style = if is_selected {
|
||||
styles::active_item_bg(colors)
|
||||
} else {
|
||||
styles::inactive_item_bg(colors)
|
||||
};
|
||||
|
||||
cat_filters = cat_filters.push(clickable);
|
||||
let item = container(filter_text)
|
||||
.width(Length::Fill)
|
||||
.padding([3, 6])
|
||||
.style(move |_theme| item_style.clone());
|
||||
|
||||
let filter_type = entry.filter_type;
|
||||
let clickable =
|
||||
iced::widget::mouse_area(item).on_press(Message::FilterSelect(filter_type));
|
||||
|
||||
cat_filters = cat_filters.push(clickable);
|
||||
}
|
||||
|
||||
filter_list = filter_list.push(cat_filters);
|
||||
}
|
||||
|
||||
filter_list = filter_list.push(cat_name);
|
||||
filter_list = filter_list.push(cat_filters);
|
||||
}
|
||||
|
||||
let apply_btn = if layer_editable {
|
||||
|
||||
@@ -875,3 +875,66 @@ fn slider_row<'a>(
|
||||
.align_y(iced::Alignment::Center)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Docked Layer Styles panel — lists effects with enable checkboxes.
|
||||
///
|
||||
/// **Purpose:** A simplified, always-visible panel showing all layer effects
|
||||
/// with toggle checkboxes. This is the docked counterpart to the floating
|
||||
/// `view()` dialog; it provides quick enable/disable without full parameter editing.
|
||||
pub fn panel_view<'a>(
|
||||
styles_list: Vec<LayerStyle>,
|
||||
colors: ThemeColors,
|
||||
) -> Element<'a, Message> {
|
||||
let mut effects_list = column![].spacing(1);
|
||||
|
||||
for (i, &(disc, label, color_arr)) in STYLE_DEFS.iter().enumerate() {
|
||||
let enabled = style_enabled(disc, &styles_list);
|
||||
let badge_color = iced::Color::from_rgb(color_arr[0], color_arr[1], color_arr[2]);
|
||||
|
||||
let toggle = iced::widget::checkbox("", enabled)
|
||||
.on_toggle(move |_| Message::LayerStyleToggle(i as u32))
|
||||
.size(11);
|
||||
|
||||
let label_text = text(label)
|
||||
.size(11)
|
||||
.width(Length::Fill)
|
||||
.style(move |_theme: &iced::Theme| iced::widget::text::Style {
|
||||
color: Some(if enabled {
|
||||
badge_color
|
||||
} else {
|
||||
colors.text_secondary
|
||||
}),
|
||||
});
|
||||
|
||||
let item_row = row![toggle, label_text]
|
||||
.spacing(4)
|
||||
.align_y(iced::Alignment::Center);
|
||||
|
||||
let item_style = if enabled {
|
||||
styles::active_item_bg(colors)
|
||||
} else {
|
||||
styles::inactive_item_bg(colors)
|
||||
};
|
||||
|
||||
let item = container(item_row)
|
||||
.width(Length::Fill)
|
||||
.padding([3, 6])
|
||||
.style(move |_theme| item_style.clone());
|
||||
|
||||
effects_list = effects_list.push(item);
|
||||
}
|
||||
|
||||
let panel = column![
|
||||
text("Layer Styles").size(11),
|
||||
horizontal_rule(1),
|
||||
scrollable(effects_list).height(Length::Fill),
|
||||
]
|
||||
.spacing(2)
|
||||
.padding(4);
|
||||
|
||||
container(panel)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.style(move |_theme| styles::panel_background(colors))
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -212,19 +212,6 @@ pub fn view<'a>(
|
||||
.unwrap_or(BlendMode::Normal);
|
||||
let active_opacity = active_info.map(|l| l.opacity).unwrap_or(1.0);
|
||||
let active_locked = active_info.map(|l| l.locked).unwrap_or(false);
|
||||
let active_metadata = active_info
|
||||
.map(|layer| {
|
||||
format!(
|
||||
"{:?} {}x{} parent {}",
|
||||
layer.layer_type,
|
||||
layer.width,
|
||||
layer.height,
|
||||
layer
|
||||
.parent_id
|
||||
.map_or_else(|| "root".to_string(), |id| id.to_string())
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(|| "No active layer".to_string());
|
||||
|
||||
let blend_list = pick_list(
|
||||
BLEND_MODE_ITEMS,
|
||||
@@ -272,7 +259,6 @@ pub fn view<'a>(
|
||||
row![lock_btn, fx_btn]
|
||||
.spacing(8)
|
||||
.align_y(iced::Alignment::Center),
|
||||
text(active_metadata).size(BODY),
|
||||
]
|
||||
.spacing(2);
|
||||
|
||||
@@ -468,9 +454,6 @@ pub fn view<'a>(
|
||||
.on_press(Message::LayerAddGroup)
|
||||
.padding([5, 8])
|
||||
.style(move |_theme, _status| flat_btn_style(colors));
|
||||
let duplicate_btn = button(text("Duplicate").size(BODY)).padding([5, 8]);
|
||||
let mask_btn = button(text("Mask").size(BODY)).padding([5, 8]);
|
||||
let rasterize_btn = button(text("Rasterize").size(BODY)).padding([5, 8]);
|
||||
let flatten_btn = button(text("Flatten").size(BODY))
|
||||
.on_press(Message::LayerFlatten)
|
||||
.padding([5, 8])
|
||||
@@ -492,6 +475,9 @@ pub fn view<'a>(
|
||||
.on_press(Message::OpenLayerStyleDialog)
|
||||
.padding([3, 6])
|
||||
.style(move |_theme, _status| flat_btn_style(colors));
|
||||
let duplicate_btn = button(text("Duplicate").size(BODY)).padding([5, 8]);
|
||||
let mask_btn = button(text("Mask").size(BODY)).padding([5, 8]);
|
||||
let rasterize_btn = button(text("Rasterize").size(BODY)).padding([5, 8]);
|
||||
let toolbar = column![
|
||||
text("Unavailable: duplicate, mask, rasterize").size(BODY),
|
||||
row![duplicate_btn, mask_btn]
|
||||
|
||||
@@ -704,6 +704,10 @@ fn all_menus(recent_files: &[crate::app::RecentFileEntry]) -> Vec<MenuDef> {
|
||||
"Custom Shapes",
|
||||
MenuCommand::TogglePane(PaneType::CustomShapes),
|
||||
),
|
||||
MenuItem::command(
|
||||
"Layer Styles",
|
||||
MenuCommand::TogglePane(PaneType::LayerStyles),
|
||||
),
|
||||
MenuItem::separator(),
|
||||
MenuItem::command(
|
||||
"Theme: Photopea",
|
||||
@@ -798,7 +802,7 @@ pub fn dropdown_overlay(
|
||||
let enabled = item.enabled;
|
||||
|
||||
// Check if this is a Window menu item that should show a checkmark
|
||||
let is_window_panel = menu_idx == 8 && item_idx <= 11;
|
||||
let is_window_panel = menu_idx == 8 && item_idx <= 12;
|
||||
let is_checked = if is_window_panel {
|
||||
let pane_type = match item_idx {
|
||||
0 => Some(PaneType::Layers),
|
||||
@@ -813,6 +817,7 @@ pub fn dropdown_overlay(
|
||||
9 => Some(PaneType::LayerDetails),
|
||||
10 => Some(PaneType::AiScript),
|
||||
11 => Some(PaneType::CustomShapes),
|
||||
12 => Some(PaneType::LayerStyles),
|
||||
_ => None,
|
||||
};
|
||||
pane_type
|
||||
@@ -1254,6 +1259,7 @@ mod tests {
|
||||
"Window/Layer Details",
|
||||
"Window/AI Script Generator",
|
||||
"Window/Custom Shapes",
|
||||
"Window/Layer Styles",
|
||||
"Window/Theme: Photopea",
|
||||
"Window/Theme: Photoshop",
|
||||
"Window/Theme: ProDark (Monokai)",
|
||||
|
||||
@@ -146,30 +146,30 @@ pub fn hover_item_bg(colors: ThemeColors) -> iced::widget::container::Style {
|
||||
}
|
||||
}
|
||||
|
||||
/// Menu bar background — dark like Photopea's menu bar.
|
||||
/// Menu bar background — panel color for visual consistency.
|
||||
#[allow(dead_code)]
|
||||
pub fn menubar_background(colors: ThemeColors) -> iced::widget::container::Style {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
/// Title bar background — dark like Photopea's title bar.
|
||||
/// Title bar background — panel color for visual consistency.
|
||||
#[allow(dead_code)]
|
||||
pub fn titlebar_background(colors: ThemeColors) -> iced::widget::container::Style {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
/// Status bar background — dark like Photopea's status bar.
|
||||
/// Status bar background — panel color for consistency.
|
||||
pub fn statusbar_background(colors: ThemeColors) -> iced::widget::container::Style {
|
||||
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()
|
||||
}
|
||||
@@ -194,10 +194,10 @@ pub fn dropdown_background(colors: ThemeColors) -> iced::widget::container::Styl
|
||||
}
|
||||
}
|
||||
|
||||
/// Document tab bar background — dark like Photopea's tab bar.
|
||||
/// Document tab bar background — panel color for visual consistency.
|
||||
pub fn tabbar_background(colors: ThemeColors) -> iced::widget::container::Style {
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -8,14 +8,13 @@ use crate::app::Message;
|
||||
use crate::panels::menus::MenuCommand;
|
||||
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 Photopea-style toolbar: SVG icon buttons + blend mode/opacity/flow in one row.
|
||||
/// Build the compact toolbar: SVG icon buttons + brush size + opacity in one row.
|
||||
pub fn view<'a>(
|
||||
tool_state: &'a crate::app::ToolState,
|
||||
settings: &'a crate::settings::AppSettings,
|
||||
_settings: &'a crate::settings::AppSettings,
|
||||
colors: ThemeColors,
|
||||
) -> Element<'a, Message> {
|
||||
let c = colors;
|
||||
@@ -46,8 +45,20 @@ pub fn view<'a>(
|
||||
.spacing(0)
|
||||
.align_y(iced::Alignment::Center);
|
||||
|
||||
// ── Blend Mode, Opacity, Flow, Smooth (Photopea-style) ──
|
||||
let blend_value = compact_value("Normal".to_string(), "Blend mode: Normal", c);
|
||||
// ── Brush size + opacity (compact, matching egui) ──
|
||||
let size_value = compact_value(
|
||||
format!("S {:.0}px", tool_state.brush_size),
|
||||
"Brush size",
|
||||
c,
|
||||
);
|
||||
let size_sl = slider(
|
||||
1.0..=200.0,
|
||||
tool_state.brush_size,
|
||||
Message::BrushSizeChanged,
|
||||
)
|
||||
.step(1.0)
|
||||
.width(Length::Fixed(64.0));
|
||||
|
||||
let opacity_value = compact_value(
|
||||
format!("O {:.0}%", tool_state.brush_opacity * 100.0),
|
||||
"Brush opacity",
|
||||
@@ -59,144 +70,17 @@ pub fn view<'a>(
|
||||
Message::BrushOpacityChanged,
|
||||
)
|
||||
.step(0.01)
|
||||
.width(Length::Fixed(52.0));
|
||||
.width(Length::Fixed(64.0));
|
||||
|
||||
let flow_value = compact_value(
|
||||
format!("F {:.0}%", settings.tool_settings.brush_flow * 100.0),
|
||||
"Brush flow",
|
||||
c,
|
||||
);
|
||||
let flow_sl = slider(
|
||||
0.0..=1.0,
|
||||
settings.tool_settings.brush_flow,
|
||||
Message::BrushFlowChanged,
|
||||
)
|
||||
.step(0.01)
|
||||
.width(Length::Fixed(52.0));
|
||||
|
||||
let smooth_value = compact_value("S 0%".to_string(), "Stroke smoothing", c);
|
||||
|
||||
let tool_options = row![
|
||||
blend_value,
|
||||
sep(c),
|
||||
opacity_value,
|
||||
opacity_sl,
|
||||
sep(c),
|
||||
flow_value,
|
||||
flow_sl,
|
||||
sep(c),
|
||||
smooth_value,
|
||||
]
|
||||
.spacing(4)
|
||||
.align_y(iced::Alignment::Center);
|
||||
|
||||
// ── Tool-specific options (right side) ──
|
||||
let tool_options_extra = match tool_state.active_tool {
|
||||
Tool::Brush | Tool::Eraser | Tool::Pen => {
|
||||
let sz = compact_value(format!("{:.0}px", tool_state.brush_size), "Brush size", c);
|
||||
let sz_sl = slider(
|
||||
1.0..=200.0,
|
||||
tool_state.brush_size,
|
||||
Message::BrushSizeChanged,
|
||||
)
|
||||
.step(1.0)
|
||||
.width(Length::Fixed(58.0));
|
||||
let ha = compact_value(
|
||||
format!("H {:.0}%", tool_state.brush_hardness * 100.0),
|
||||
"Brush hardness",
|
||||
c,
|
||||
);
|
||||
let ha_sl = slider(
|
||||
0.0..=1.0,
|
||||
tool_state.brush_hardness,
|
||||
Message::BrushHardnessChanged,
|
||||
)
|
||||
.step(0.01)
|
||||
.width(Length::Fixed(58.0));
|
||||
row![sz, sz_sl, ha, ha_sl]
|
||||
.spacing(4)
|
||||
.align_y(iced::Alignment::Center)
|
||||
}
|
||||
Tool::Spray => {
|
||||
let sz = compact_value(format!("{:.0}px", tool_state.brush_size), "Spray size", c);
|
||||
let sz_sl = slider(1.0..=200.0, tool_state.brush_size, Message::BrushSizeChanged)
|
||||
.step(1.0)
|
||||
.width(Length::Fixed(50.0));
|
||||
let ps = compact_value(
|
||||
format!("P {:.1}px", settings.tool_settings.spray_particle_size),
|
||||
"Particle size",
|
||||
c,
|
||||
);
|
||||
let ps_sl = slider(
|
||||
1.0..=20.0,
|
||||
settings.tool_settings.spray_particle_size,
|
||||
Message::SprayParticleSizeChanged,
|
||||
)
|
||||
.step(0.1)
|
||||
.width(Length::Fixed(50.0));
|
||||
let ds = compact_value(
|
||||
format!("D {:.0}", settings.tool_settings.spray_density),
|
||||
"Particle density",
|
||||
c,
|
||||
);
|
||||
let ds_sl = slider(
|
||||
10.0..=1000.0,
|
||||
settings.tool_settings.spray_density as f32,
|
||||
|v| Message::SprayDensityChanged(v as u32),
|
||||
)
|
||||
.step(1.0)
|
||||
.width(Length::Fixed(50.0));
|
||||
row![sz, sz_sl, ps, ps_sl, ds, ds_sl]
|
||||
.spacing(4)
|
||||
.align_y(iced::Alignment::Center)
|
||||
}
|
||||
Tool::MagicWand => {
|
||||
let tol = settings.tool_settings.magic_wand_tolerance;
|
||||
row![
|
||||
compact_value(format!("T {:.0}", tol), "Selection tolerance", c),
|
||||
slider(0.0..=255.0, tol, |v| Message::SelectionToleranceChanged(
|
||||
v as u8
|
||||
))
|
||||
.step(1.0)
|
||||
.width(Length::Fixed(64.0)),
|
||||
]
|
||||
.spacing(6)
|
||||
.align_y(iced::Alignment::Center)
|
||||
}
|
||||
Tool::Select | Tool::Lasso => {
|
||||
let feather = settings.tool_settings.select_feather;
|
||||
row![
|
||||
compact_value(format!("F {:.0}px", feather), "Selection feather", c),
|
||||
slider(0.0..=250.0, feather, Message::SelectionFeatherChanged)
|
||||
.step(1.0)
|
||||
.width(Length::Fixed(64.0)),
|
||||
]
|
||||
.spacing(6)
|
||||
.align_y(iced::Alignment::Center)
|
||||
}
|
||||
Tool::Move => row![compact_value("Auto".to_string(), "Auto-select layer", c)]
|
||||
.spacing(4)
|
||||
.align_y(iced::Alignment::Center),
|
||||
Tool::VectorLine | Tool::VectorRect | Tool::VectorCircle => {
|
||||
let stroke = settings.tool_settings.vector_stroke_width;
|
||||
row![
|
||||
compact_value(format!("{:.1}px", stroke), "Vector stroke width", c),
|
||||
slider(0.0..=50.0, stroke, Message::VectorStrokeChanged)
|
||||
.step(0.5)
|
||||
.width(Length::Fixed(58.0)),
|
||||
]
|
||||
.spacing(4)
|
||||
.align_y(iced::Alignment::Center)
|
||||
}
|
||||
_ => row![].spacing(0),
|
||||
};
|
||||
let tool_controls = row![size_value, size_sl, sep(c), opacity_value, opacity_sl]
|
||||
.spacing(4)
|
||||
.align_y(iced::Alignment::Center);
|
||||
|
||||
let bar = row![
|
||||
actions,
|
||||
sep(c),
|
||||
tool_options,
|
||||
tool_controls,
|
||||
iced::widget::Space::with_width(Length::Fill),
|
||||
tool_options_extra,
|
||||
]
|
||||
.spacing(4)
|
||||
.align_y(iced::Alignment::Center)
|
||||
@@ -264,32 +148,6 @@ fn svg_btn(
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
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])
|
||||
|
||||
@@ -82,16 +82,16 @@ impl ThemeColors {
|
||||
// ── 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
|
||||
bg_app: Color::from_rgb(0.110, 0.110, 0.110), // #1c1c1c - canvas (darker)
|
||||
bg_panel: Color::from_rgb(0.153, 0.153, 0.153), // #272727 - panels (darker)
|
||||
bg_hover: Color::from_rgb(0.220, 0.220, 0.220), // #383838 - hover
|
||||
bg_active: Color::from_rgb(0.190, 0.190, 0.190), // #303030 - active tab (darker)
|
||||
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
|
||||
border_low: Color::from_rgb(0.180, 0.180, 0.180), // #2e2e2e - dividers (darker)
|
||||
border_high: Color::from_rgb(0.250, 0.250, 0.250), // #404040 - borders (darker)
|
||||
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
|
||||
@@ -103,10 +103,10 @@ impl ThemeColors {
|
||||
},
|
||||
// ── Photoshop (Legacy) ─────────────────────────────────────
|
||||
ThemePreset::Photoshop => Self {
|
||||
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),
|
||||
bg_app: Color::from_rgb(0.100, 0.100, 0.108),
|
||||
bg_panel: Color::from_rgb(0.178, 0.178, 0.194),
|
||||
bg_hover: Color::from_rgb(0.230, 0.230, 0.246),
|
||||
bg_active: Color::from_rgb(0.210, 0.210, 0.226),
|
||||
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),
|
||||
@@ -124,10 +124,10 @@ impl ThemeColors {
|
||||
},
|
||||
// ── Monokai ────────────────────────────────────────────────
|
||||
ThemePreset::ProDark => Self {
|
||||
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),
|
||||
bg_app: Color::from_rgb(0.078, 0.074, 0.063),
|
||||
bg_panel: Color::from_rgb(0.137, 0.141, 0.118),
|
||||
bg_hover: Color::from_rgb(0.200, 0.208, 0.176),
|
||||
bg_active: Color::from_rgb(0.164, 0.168, 0.145),
|
||||
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),
|
||||
@@ -145,10 +145,10 @@ impl ThemeColors {
|
||||
},
|
||||
// ── Dracula ────────────────────────────────────────────────
|
||||
ThemePreset::Amoled => Self {
|
||||
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),
|
||||
bg_app: Color::from_rgb(0.084, 0.076, 0.118),
|
||||
bg_panel: Color::from_rgb(0.141, 0.149, 0.194),
|
||||
bg_hover: Color::from_rgb(0.208, 0.215, 0.276),
|
||||
bg_active: Color::from_rgb(0.170, 0.178, 0.226),
|
||||
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),
|
||||
|
||||
Reference in New Issue
Block a user