- 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.
13 KiB
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-apiandhcie-documentmust 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 returnbool—trueif it committed at least one pending snapshot,falseotherwise. - Add
pub fn has_pending_history(&self) -> boolthat returns whether thepending_historymutex still has uncommitted items.
- Change
- In
hcie-document/src/lib.rs:- Ensure
push_draw_snapshot_subrectandpush_draw_snapshotstill skip no-change snapshots (preserve existing behaviour); no new logic needed.
- Ensure
Fix (GUI side):
- In
hcie-iced-gui/src/app.rsMessage::CanvasPointerReleasedbrush-stroke cleanup block:- After
end_stroke, callcommit_pending_history(). - If
commit_pending_history()returnedfalseandengine.has_pending_history()is true, schedule a delayedCompositeRefreshchain (e.g. 8 ms, 16 ms, 32 ms) usingtokio::time::sleepinsideTask::perform, each time re-checking and re-committing until pending is drained. - Always finish with
refresh_composite_if_needed()socached_historyis updated.
- After
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_toto a different pixel,end_stroke,commit_pending_history(). - Assert
history_len()increased by exactly one andhistory_description(...)is"Brush Stroke"(or the enhanced description after 1.2). - Assert undo restores the blank layer.
- Create a 16×16 engine, set a Round brush,
- Add an iced-level test in
hcie-iced-app/crates/hcie-iced-gui/tests/(or extendfeature_scorecard.rs) that simulates the message sequenceCanvasPointerPressed→CanvasPointerMoved→CanvasPointerReleasedfor a brush stroke and assertscached_historygrows.
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.rsbackground thread:- The interactive
end_stroke()path currently hard-codes"Brush Stroke". Replace with a description that includesself.current_tip.stylelabel (e.g."Brush Stroke (Oil)").
- The interactive
- In
hcie-engine-api/src/lib.rsvector 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 strmapping eachVectorShapevariant to a short label.fn vector_shape_name_or_kind(shape: &VectorShape) -> Stringreturningshape.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.
- Keep the list layout but widen the text column so longer descriptions are not clipped. Reduce index prefix formatting noise: keep
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 shortcutCtrl+Shift+V(replace the existingPaste Specialshortcut mapping; keepPaste Speciallabel as a placeholder or map it to the same command if preferred). - Add the same item to
canvas_context_menubetweenPasteandNew Layer.
- Add
Message:
- In
hcie-iced-gui/src/app.rsMessageenum:- Add
PasteAsNewLayer.
- Add
Handler (in Message dispatch):
- On
PasteAsNewLayer:- Prefer
internal_clipboardif present; otherwise read system clipboard viaio/clipboard::paste_image_from_clipboard(). - If clipboard image exceeds canvas, show the existing
ExpandCanvasdialog first, then proceed. - Compute target size:
- If oversized and user accepted expand: use clipboard width/height as new canvas size.
- Otherwise: keep current canvas size.
- Create a new layer via
engine.add_layer("Pasted Layer"). - Center the clipboard pixels in the new layer using
engine.set_layer_pixels(new_layer_id, centered_pixels). - Push an undo snapshot on the new layer with description
"Paste as New Layer". - Set the new layer as active.
- Call
refresh_composite_if_needed()and returnCompositeRefresh.
- Prefer
Helper to add:
- In
hcie-iced-gui/src/selection/clipboard.rsor a newhcie-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.svgfor hidden layers. - Existing
hcie-iced-app/assets/icons/visible.svgcan be reused as the open eye.
Helper for small SVG buttons:
- In
hcie-iced-gui/src/panels/layers.rs(orstyles.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.
- Add a helper
Row controls:
- Replace the visibility text button (
"\u{1F441}") withvisible.svg/eye_closed.svgbased oninfo.visible. - Replace the lock text (
"L"/"-") withlock.svgfor locked and a subtle open-lock or empty placeholder for unlocked. - Keep the same
Message::LayerToggleVisibility/LayerToggleLockmessages.
Toolbar controls:
- Replace text toolbar buttons with icon buttons:
+Layer→new_file.svgorpanel-layers.svgplus tooltip+Group→ a folder/group SVG (usepanel-layers.svgif no group icon exists)Flatten→ a merge/layers SVG (keep text if no icon)▲/▼move arrows keep unicode arrows but use consistent sizeXdelete keep unicode but ensure visiblefxkeep as text because it's a convention
- Wrap each in the same
svg_icon_btnhelper 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.rsMessage::VectorSelectDragMove:- Remove the
vector_drag_last_render60 FPS throttle block. Updatevector_drag_previewon everyDragMove. - Keep
vector_drag_lastfor pointer tracking.
- Remove the
Draw actual shape geometry in overlay:
- In
hcie-iced-gui/src/canvas/mod.rsOverlayProgram::draw():- Where
vector_drag_previewis 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_drawpreview already does for in-progress shapes (ellipse, line, star, polygon, rounded rect paths). - Apply the preview's
angleandnormalized_bounds()transform so the preview exactly matches the committed shape.
- Draw the same geometry as
- For
VectorShape::FreePath:- Draw the path points transformed by the preview bounds/angle.
- For
- Use the same bright green stroke style (
[0.2, 0.9, 0.4, 0.95]) and keep the edit handles drawn over it.
- Where
Shared preview renderer:
- Extract the existing vector-shape preview drawing code from
vector_drawblock incanvas/mod.rsinto a helperfn 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_drawand forvector_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, usingfill="currentColor"like the other icons.
GUI change:
- In
hcie-iced-app/crates/hcie-iced-gui/src/panels/brushes.rscategory tabs array:- Change Watercolor tab from
"icons/brush.svg"to"icons/watercolor.svg".
- Change Watercolor tab from
No engine changes.
Implementation Order
- Vector transform preview (#4) — pure GUI, safe to start.
- Layers panel icons (#3) — pure GUI.
- Watercolor icon (#5) — pure asset + one-line GUI change.
- Paste as new layer (#2) — pure GUI, can be done in parallel with #1–#3.
- History enhancements + first-stroke fix (#1) — requires unlocking engine crates; do this last to minimize locked-crate exposure.
Validation
cargo check -p hcie-iced-guicargo test -p hcie-iced-guicargo test -p hcie-engine-api --test visual_regressioncargo test -p hcie-engine-api --test performance_stroke_4k -- --nocapturecargo 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-apiandhcie-documentfor history changes. Re-lock immediately after. - Background thread race: The first-stroke fix relies on delayed
CompositeRefreshtasks; 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.rsbehaviour. - No protocol changes: Do not add new
VectorShapevariants or newBrushStylevariants.