Files
hcie-rust-v3.05/.kilo/plans/1784597466971-iced-gui-ux-improvements-plan.md
phantom 7b4073e55b feat: Enhance brush settings and add new layer paste functionality
- Added brush color variant and variant amount settings to the brush panel.
- Introduced a checkbox for color variant and a slider for variant amount in the brush settings.
- Implemented "Paste as New Layer" functionality, allowing users to paste clipboard images directly onto a new layer.
- Updated menus to include the new paste option with a shortcut.
- Improved layer panel iconography by replacing text buttons with SVG icons for visibility and lock toggles.
- Created new SVG assets for closed eye and watercolor icons.
- Enhanced history panel to provide richer descriptions for brush strokes and vector shape modifications.
- Fixed the issue where the first brush stroke was missing from the history panel.
2026-07-21 17:40:41 +03:00

13 KiB
Raw Permalink Blame History

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 booltrue 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 CanvasPointerPressedCanvasPointerMovedCanvasPointerReleased 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:
    • +Layernew_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.