225 lines
13 KiB
Markdown
225 lines
13 KiB
Markdown
|
|
# Iced GUI UX Improvements Plan
|
|||
|
|
|
|||
|
|
## Goal
|
|||
|
|
Implement five requested UI/UX and functional improvements in the `hcie-iced-gui` workspace, plus fix the reported regression where the first brush stroke does not appear in the History panel.
|
|||
|
|
|
|||
|
|
## Scope & Boundaries
|
|||
|
|
- **Open layer:** `hcie-iced-app/crates/hcie-iced-gui/`, plus the new SVG asset.
|
|||
|
|
- **Locked layers touched only for history fixes:** `hcie-engine-api` and `hcie-document` must be unlocked with the repo scripts, edited, and re-locked.
|
|||
|
|
- All other improvements are GUI-only.
|
|||
|
|
- Preserve protected GPU/dirty-region, partial-upload, and shader-layering behavior.
|
|||
|
|
|
|||
|
|
## Out of Scope
|
|||
|
|
- egui GUI changes (except if a trivial parity icon is needed, noted below).
|
|||
|
|
- New brush engine styles or engine-side brush changes.
|
|||
|
|
- Layer duplicate / mask / rasterize functionality (toolbar placeholders stay as-is; only iconography changes).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 1. History Panel Enhancements + First-Brush-Stroke Fix
|
|||
|
|
|
|||
|
|
### 1.1 Bug: first brush stroke missing from History
|
|||
|
|
**Root cause:** `end_stroke()` creates the undo snapshot on a background thread and stores it in `pending_history`. The GUI calls `commit_pending_history()` once immediately after `end_stroke()`, but the thread may not have finished, so the snapshot is never committed and `cached_history` is never refreshed.
|
|||
|
|
|
|||
|
|
**Fix (engine side):**
|
|||
|
|
- In `hcie-engine-api/src/stroke_cache.rs`:
|
|||
|
|
- Change `commit_pending_history()` to return `bool` — `true` if it committed at least one pending snapshot, `false` otherwise.
|
|||
|
|
- Add `pub fn has_pending_history(&self) -> bool` that returns whether the `pending_history` mutex still has uncommitted items.
|
|||
|
|
- In `hcie-document/src/lib.rs`:
|
|||
|
|
- Ensure `push_draw_snapshot_subrect` and `push_draw_snapshot` still skip no-change snapshots (preserve existing behaviour); no new logic needed.
|
|||
|
|
|
|||
|
|
**Fix (GUI side):**
|
|||
|
|
- In `hcie-iced-gui/src/app.rs` `Message::CanvasPointerReleased` brush-stroke cleanup block:
|
|||
|
|
- After `end_stroke`, call `commit_pending_history()`.
|
|||
|
|
- If `commit_pending_history()` returned `false` **and** `engine.has_pending_history()` is true, schedule a delayed `CompositeRefresh` chain (e.g. 8 ms, 16 ms, 32 ms) using `tokio::time::sleep` inside `Task::perform`, each time re-checking and re-committing until pending is drained.
|
|||
|
|
- Always finish with `refresh_composite_if_needed()` so `cached_history` is updated.
|
|||
|
|
|
|||
|
|
**Regression test:**
|
|||
|
|
- Add a new engine test `hcie-engine-api/tests/brush_stroke_history.rs`:
|
|||
|
|
- Create a 16×16 engine, set a Round brush, `begin_stroke`, `stroke_to` to a different pixel, `end_stroke`, `commit_pending_history()`.
|
|||
|
|
- Assert `history_len()` increased by exactly one and `history_description(...)` is `"Brush Stroke"` (or the enhanced description after 1.2).
|
|||
|
|
- Assert undo restores the blank layer.
|
|||
|
|
- Add an iced-level test in `hcie-iced-app/crates/hcie-iced-gui/tests/` (or extend `feature_scorecard.rs`) that simulates the message sequence `CanvasPointerPressed` → `CanvasPointerMoved` → `CanvasPointerReleased` for a brush stroke and asserts `cached_history` grows.
|
|||
|
|
|
|||
|
|
### 1.2 Rich history descriptions
|
|||
|
|
**Engine side:**
|
|||
|
|
- In `hcie-engine-api/src/stroke_brush.rs`:
|
|||
|
|
- `draw_stroke()` currently pushes `"Brush Stroke"`. Change it to include the brush style label, e.g. `"Brush Stroke (Round)"`, `"Brush Stroke (Watercolor)"`, etc.
|
|||
|
|
- `draw_line()` → `"Line"`, `draw_rect()` → `"Rectangle"`, `draw_filled_rect_rgba()` → `"Fill Rectangle"`, `draw_ellipse()` → `"Ellipse"`.
|
|||
|
|
- In `hcie-engine-api/src/stroke_cache.rs` background thread:
|
|||
|
|
- The interactive `end_stroke()` path currently hard-codes `"Brush Stroke"`. Replace with a description that includes `self.current_tip.style` label (e.g. `"Brush Stroke (Oil)"`).
|
|||
|
|
- In `hcie-engine-api/src/lib.rs` vector mutators:
|
|||
|
|
- `add_vector_shape`: keep `"Add Vector Shape"` but append the shape kind/name when known: `"Add Vector Shape (Rect)"`, `"Add Vector Shape (Path 12 pts)"`, etc.
|
|||
|
|
- `delete_vector_shape`: push a vector snapshot with description `"Delete Vector Shape (<kind>)"`.
|
|||
|
|
- `set_vector_shape_bounds`: `"Resize Vector Shape (<kind>)"`.
|
|||
|
|
- `set_vector_shape_angle`: `"Rotate Vector Shape (<kind>)"`.
|
|||
|
|
- `set_vector_shape_color`: `"Set Stroke Color"`.
|
|||
|
|
- `set_vector_shape_fill_color`: `"Set Fill Color"`.
|
|||
|
|
- `set_vector_shape_fill`: `"Toggle Fill"`.
|
|||
|
|
- `set_vector_shape_stroke`: `"Set Stroke Width"`.
|
|||
|
|
- `set_vector_shape_opacity`: `"Set Shape Opacity"`.
|
|||
|
|
- `set_vector_shape_hardness`: `"Set Shape Hardness"`.
|
|||
|
|
- `reorder_vector_shape`: `"Reorder Vector Shape"`.
|
|||
|
|
- `boolean_vector_shapes`: already has `"Path Boolean ({op})"`; keep.
|
|||
|
|
|
|||
|
|
**Helper to add in `hcie-engine-api/src/lib.rs`:**
|
|||
|
|
- `fn vector_shape_kind(shape: &VectorShape) -> &'static str` mapping each `VectorShape` variant to a short label.
|
|||
|
|
- `fn vector_shape_name_or_kind(shape: &VectorShape) -> String` returning `shape.name()` if non-empty, else the kind label.
|
|||
|
|
|
|||
|
|
**Note:** vector property mutators currently do **not** push snapshots, so undo does not cover them. Adding snapshots here fixes both the history description and makes these operations undoable, which is required for a 9/10 History score.
|
|||
|
|
|
|||
|
|
**GUI side:**
|
|||
|
|
- In `hcie-iced-gui/src/panels/history.rs`:
|
|||
|
|
- Keep the list layout but widen the text column so longer descriptions are not clipped. Reduce index prefix formatting noise: keep `"{}: {}"` but ensure the container does not hard-wrap at 30 chars.
|
|||
|
|
- Add a subtle muted style for future entries and accent style for current (already exists); verify contrast.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 2. Clipboard: Paste as New Layer
|
|||
|
|
|
|||
|
|
### Goal
|
|||
|
|
Add a workflow to paste clipboard pixels directly as a new raster layer, instead of always entering floating transform mode.
|
|||
|
|
|
|||
|
|
### Changes
|
|||
|
|
**Menu / command:**
|
|||
|
|
- In `hcie-iced-gui/src/panels/menus.rs`:
|
|||
|
|
- Add `MenuCommand::PasteAsNewLayer`.
|
|||
|
|
- Add a menu item under Edit: `"Paste as New Layer"` with shortcut `Ctrl+Shift+V` (replace the existing `Paste Special` shortcut mapping; keep `Paste Special` label as a placeholder or map it to the same command if preferred).
|
|||
|
|
- Add the same item to `canvas_context_menu` between `Paste` and `New Layer`.
|
|||
|
|
|
|||
|
|
**Message:**
|
|||
|
|
- In `hcie-iced-gui/src/app.rs` `Message` enum:
|
|||
|
|
- Add `PasteAsNewLayer`.
|
|||
|
|
|
|||
|
|
**Handler (in `Message` dispatch):**
|
|||
|
|
- On `PasteAsNewLayer`:
|
|||
|
|
1. Prefer `internal_clipboard` if present; otherwise read system clipboard via `io/clipboard::paste_image_from_clipboard()`.
|
|||
|
|
2. If clipboard image exceeds canvas, show the existing `ExpandCanvas` dialog first, then proceed.
|
|||
|
|
3. Compute target size:
|
|||
|
|
- If oversized and user accepted expand: use clipboard width/height as new canvas size.
|
|||
|
|
- Otherwise: keep current canvas size.
|
|||
|
|
4. Create a new layer via `engine.add_layer("Pasted Layer")`.
|
|||
|
|
5. Center the clipboard pixels in the new layer using `engine.set_layer_pixels(new_layer_id, centered_pixels)`.
|
|||
|
|
6. Push an undo snapshot on the new layer with description `"Paste as New Layer"`.
|
|||
|
|
7. Set the new layer as active.
|
|||
|
|
8. Call `refresh_composite_if_needed()` and return `CompositeRefresh`.
|
|||
|
|
|
|||
|
|
**Helper to add:**
|
|||
|
|
- In `hcie-iced-gui/src/selection/clipboard.rs` or a new `hcie-iced-gui/src/clipboard.rs`:
|
|||
|
|
- `fn centered_layer_pixels(pixels: &[u8], img_w: u32, img_h: u32, canvas_w: u32, canvas_h: u32) -> Vec<u8>` that returns a full canvas-sized RGBA buffer with the image centered.
|
|||
|
|
|
|||
|
|
**Note:** avoid changing the existing `PasteImage` floating-transform path; add the new path in parallel.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 3. Layers Panel Iconography
|
|||
|
|
|
|||
|
|
### Goal
|
|||
|
|
Replace text-only controls in the Layers panel with intuitive icon buttons, and specifically fix the visibility toggle to use a standard eye/eye-slash icon.
|
|||
|
|
|
|||
|
|
### Changes
|
|||
|
|
**New SVG asset:**
|
|||
|
|
- Create `hcie-iced-app/assets/icons/eye_closed.svg` for hidden layers.
|
|||
|
|
- Existing `hcie-iced-app/assets/icons/visible.svg` can be reused as the open eye.
|
|||
|
|
|
|||
|
|
**Helper for small SVG buttons:**
|
|||
|
|
- In `hcie-iced-gui/src/panels/layers.rs` (or `styles.rs`):
|
|||
|
|
- Add a helper `fn svg_icon_btn<'a>(icon_path: &str, tip: &str, msg: Message, colors: ThemeColors, enabled: bool) -> Element<'a, Message>` that loads the SVG from the absolute assets path (falling back to text) and wraps it in a small flat button with a tooltip.
|
|||
|
|
|
|||
|
|
**Row controls:**
|
|||
|
|
- Replace the visibility text button (`"\u{1F441}"`) with `visible.svg` / `eye_closed.svg` based on `info.visible`.
|
|||
|
|
- Replace the lock text (`"L"` / `"-"`) with `lock.svg` for locked and a subtle open-lock or empty placeholder for unlocked.
|
|||
|
|
- Keep the same `Message::LayerToggleVisibility` / `LayerToggleLock` messages.
|
|||
|
|
|
|||
|
|
**Toolbar controls:**
|
|||
|
|
- Replace text toolbar buttons with icon buttons:
|
|||
|
|
- `+Layer` → `new_file.svg` or `panel-layers.svg` plus tooltip
|
|||
|
|
- `+Group` → a folder/group SVG (use `panel-layers.svg` if no group icon exists)
|
|||
|
|
- `Flatten` → a merge/layers SVG (keep text if no icon)
|
|||
|
|
- `▲` / `▼` move arrows keep unicode arrows but use consistent size
|
|||
|
|
- `X` delete keep unicode but ensure visible
|
|||
|
|
- `fx` keep as text because it's a convention
|
|||
|
|
- Wrap each in the same `svg_icon_btn` helper where an icon exists; for missing icons keep compact text buttons.
|
|||
|
|
|
|||
|
|
**No engine changes.**
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 4. Vector Transform Preview Performance
|
|||
|
|
|
|||
|
|
### Goal
|
|||
|
|
Eliminate latency during vector rotate/scale so the preview updates in real time.
|
|||
|
|
|
|||
|
|
### Root cause
|
|||
|
|
`VectorSelectDragMove` throttles preview updates to ~60 FPS and draws only the rotated bounding box of the preview shape, not the actual shape geometry. The 60 FPS throttle intentionally drops pointer events, which feels laggy, and the wireframe box is a weak visual proxy.
|
|||
|
|
|
|||
|
|
### Changes (all in GUI)
|
|||
|
|
**Remove throttle:**
|
|||
|
|
- In `hcie-iced-gui/src/app.rs` `Message::VectorSelectDragMove`:
|
|||
|
|
- Remove the `vector_drag_last_render` 60 FPS throttle block. Update `vector_drag_preview` on every `DragMove`.
|
|||
|
|
- Keep `vector_drag_last` for pointer tracking.
|
|||
|
|
|
|||
|
|
**Draw actual shape geometry in overlay:**
|
|||
|
|
- In `hcie-iced-gui/src/canvas/mod.rs` `OverlayProgram::draw()`:
|
|||
|
|
- Where `vector_drag_preview` is rendered (around line 1297), replace the simple rotated rectangle stroke with a shape-specific renderer:
|
|||
|
|
- For `VectorShape::Rect`, `Circle`, `Line`, `Arrow`, `Star`, `Polygon`, `Rhombus`, `Cylinder`, `Heart`, `Bubble`, `Gear`, `Cross`, `Crescent`, `Bolt`, `Arrow4`, `SvgShape`:
|
|||
|
|
- Draw the same geometry as `vector_draw` preview already does for in-progress shapes (ellipse, line, star, polygon, rounded rect paths).
|
|||
|
|
- Apply the preview's `angle` and `normalized_bounds()` transform so the preview exactly matches the committed shape.
|
|||
|
|
- For `VectorShape::FreePath`:
|
|||
|
|
- Draw the path points transformed by the preview bounds/angle.
|
|||
|
|
- Use the same bright green stroke style (`[0.2, 0.9, 0.4, 0.95]`) and keep the edit handles drawn over it.
|
|||
|
|
|
|||
|
|
**Shared preview renderer:**
|
|||
|
|
- Extract the existing vector-shape preview drawing code from `vector_draw` block in `canvas/mod.rs` into a helper `fn draw_vector_shape_preview(frame: &mut Frame, shape: &VectorShape, origin_x: f32, origin_y: f32, zoom: f32, stroke_color: iced::Color, line_width: f32)`.
|
|||
|
|
- Reuse it both for in-progress `vector_draw` and for `vector_drag_preview`.
|
|||
|
|
|
|||
|
|
**No engine changes.**
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 5. Brush Panel: Distinct Watercolor Icon
|
|||
|
|
|
|||
|
|
### Goal
|
|||
|
|
Replace the Watercolor category tab icon so it is not identical to the Painting tab.
|
|||
|
|
|
|||
|
|
### Changes
|
|||
|
|
**New SVG asset:**
|
|||
|
|
- Create `hcie-iced-app/assets/icons/watercolor.svg` — a droplet/splat shape, using `fill="currentColor"` like the other icons.
|
|||
|
|
|
|||
|
|
**GUI change:**
|
|||
|
|
- In `hcie-iced-app/crates/hcie-iced-gui/src/panels/brushes.rs` category tabs array:
|
|||
|
|
- Change Watercolor tab from `"icons/brush.svg"` to `"icons/watercolor.svg"`.
|
|||
|
|
|
|||
|
|
**No engine changes.**
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Implementation Order
|
|||
|
|
1. **Vector transform preview** (#4) — pure GUI, safe to start.
|
|||
|
|
2. **Layers panel icons** (#3) — pure GUI.
|
|||
|
|
3. **Watercolor icon** (#5) — pure asset + one-line GUI change.
|
|||
|
|
4. **Paste as new layer** (#2) — pure GUI, can be done in parallel with #1–#3.
|
|||
|
|
5. **History enhancements + first-stroke fix** (#1) — requires unlocking engine crates; do this last to minimize locked-crate exposure.
|
|||
|
|
|
|||
|
|
## Validation
|
|||
|
|
- `cargo check -p hcie-iced-gui`
|
|||
|
|
- `cargo test -p hcie-iced-gui`
|
|||
|
|
- `cargo test -p hcie-engine-api --test visual_regression`
|
|||
|
|
- `cargo test -p hcie-engine-api --test performance_stroke_4k -- --nocapture`
|
|||
|
|
- `cargo test --test feature_scorecard`
|
|||
|
|
- Manual / screenshot verification:
|
|||
|
|
- Draw one brush dab with no drag → History panel shows the entry.
|
|||
|
|
- Draw a second stroke with a different brush style → description includes style name.
|
|||
|
|
- Create a vector shape, change its fill/color/rotate/resize → each appears in History with shape kind and action.
|
|||
|
|
- Copy a selection, use "Paste as New Layer" → new layer appears with pasted pixels.
|
|||
|
|
- Toggle layer visibility → eye icon changes between open/closed.
|
|||
|
|
- Rotate/scale a vector shape → preview follows pointer without frame drops.
|
|||
|
|
- Open Brushes panel, select Watercolor category → new droplet icon is visible.
|
|||
|
|
|
|||
|
|
## Risks & Guardrails
|
|||
|
|
- **Locked crates:** Only unlock `hcie-engine-api` and `hcie-document` for history changes. Re-lock immediately after.
|
|||
|
|
- **Background thread race:** The first-stroke fix relies on delayed `CompositeRefresh` tasks; ensure tasks are cancelled or no-op if the document/engine state changes before they fire.
|
|||
|
|
- **Performance:** Removing the vector drag throttle increases overlay redraws, but overlay drawing is cheap vector geometry on a small canvas; verify no regression on 4K docs.
|
|||
|
|
- **Icon fallback:** SVG icon helper must fall back to text if the icon file is missing, matching `toolbar.rs` behaviour.
|
|||
|
|
- **No protocol changes:** Do not add new `VectorShape` variants or new `BrushStyle` variants.
|