# Iced Brushes Panel — Compact Layout + Real-Time Preview ## Goal Make the `hcie-iced-app` Brushes & Tips panel more compact and add a real-time brush-tip preview that reacts to current brush parameters and foreground color. **Scope is limited to the Iced GUI layer.** Engine crates stay locked and untouched; all rendering goes through the public `hcie_engine_api` surface. --- ## Context - Active target workspace (per user override): `hcie-iced-app/`. - Primary file: `hcie-iced-app/crates/hcie-iced-gui/src/panels/brushes.rs`. - Panel wiring: `hcie-iced-app/crates/hcie-iced-gui/src/dock/view.rs` calls `crate::panels::brushes::view(...)`. - Current layout: - Category tabs with 16 px SVG icons. - Thumbnail image 48 px inside a 56 px cell. - Two-column grid with 4 px spacing. - Label text size 9 px. - A bottom "Preview" area using a static style-keyed 64 px image. - Available engine APIs (already public in `hcie_engine_api`): - `Engine::render_brush_stamp_preview(width, height, &BrushTip, color)` — fast single-dab preview. - `Engine::render_brush_preview(width, height, &BrushTip, color, &points)` — full stroke preview. --- ## Decisions | Decision | Choice | |----------|--------| | Preview API | `Engine::render_brush_stamp_preview` (fast, parameter-aware). | | Thumbnail image size | 36 px. | | Thumbnail cell size | 40 px. | | Grid columns | 3. | | Label text | 8 px, kept. | | Grid/item spacing | 3 px. | | Tab icon size | Keep 16 px (already compact). | | Live preview color | Current foreground color `fg_color`. | | Cache key | `(style, size_byte, hardness_byte, opacity_byte, fg_color)` so the preview updates when parameters change. | | Watercolor preset thumbnails | Keep current splat renderer but shrink to 36 px image / 40 px cell and align with the new grid. | | Imported preset thumbnails | Keep list row style but shrink preview to 36 px image in 40 px cell. | --- ## Implementation Steps ### 1. Add foreground color to panel signature In `hcie-iced-app/crates/hcie-iced-gui/src/dock/view.rs`, pass `app.fg_color` into `brushes::view(...)`. In `hcie-iced-app/crates/hcie-iced-gui/src/panels/brushes.rs`, extend the function signature: ```rust pub fn view( current_style: BrushStyle, current_size: f32, current_opacity: f32, current_hardness: f32, brush_color_variant: bool, brush_variant_amount: f32, active_category: BrushCategory, imported_presets: &[hcie_engine_api::BrushPreset], active_brush_preset: Option<&hcie_engine_api::BrushPreset>, fg_color: [u8; 4], // NEW colors: ThemeColors, ) -> Element<'static, Message> ``` ### 2. Introduce compact layout constants At the top of `brushes.rs`, replace/adjust constants: ```rust const THUMB_SIZE: u32 = 40; const PREVIEW_IMG_SIZE: u32 = 36; const GRID_COLUMNS: usize = 3; const GRID_SPACING: u16 = 3; const ITEM_SPACING: u16 = 3; const LABEL_SIZE: u16 = 8; ``` Also add a real-time preview cache keyed by parameters: ```rust static LIVE_PREVIEW_CACHE: OnceLock>> = OnceLock::new(); #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] struct PreviewKey { style_idx: usize, size: u8, // quantized to whole px hardness: u8, // 0..100 opacity: u8, // 0..100 r: u8, g: u8, b: u8, a: u8, } ``` ### 3. Build a parameter-aware preview generator Add a helper that constructs a `BrushTip` from the current tool state and calls `Engine::render_brush_stamp_preview`: ```rust fn build_tip_from_state(style: BrushStyle, size: f32, opacity: f32, hardness: f32) -> hcie_engine_api::BrushTip { hcie_engine_api::BrushTip { style, size, opacity, hardness, ..Default::default() } } ``` Add: ```rust fn get_live_stamp_preview( style: BrushStyle, size: f32, opacity: f32, hardness: f32, fg_color: [u8; 4], ) -> iced::widget::image::Handle { let key = PreviewKey { style_idx: style as usize, size: size.clamp(1.0, 255.0) as u8, hardness: (hardness * 100.0).clamp(0.0, 100.0) as u8, opacity: (opacity * 100.0).clamp(0.0, 100.0) as u8, r: fg_color[0], g: fg_color[1], b: fg_color[2], a: fg_color[3], }; let cache = LIVE_PREVIEW_CACHE.get_or_init(|| Mutex::new(HashMap::new())); let mut cache = cache.lock().unwrap_or_else(|p| p.into_inner()); if let Some(handle) = cache.get(&key) { return handle.clone(); } let tip = build_tip_from_state(style, size, opacity, hardness); let pixels = hcie_engine_api::Engine::render_brush_stamp_preview( PREVIEW_IMG_SIZE, PREVIEW_IMG_SIZE, &tip, fg_color, ); let handle = iced::widget::image::Handle::from_rgba( PREVIEW_IMG_SIZE, PREVIEW_IMG_SIZE, pixels, ); cache.insert(key, handle.clone()); handle } ``` ### 4. Update existing thumbnail cache to new size - Keep `BRUSH_PREVIEW_CACHE` for style-only previews. - Regenerate it with the new `THUMB_SIZE` (40 px). The existing sine-wave `generate_brush_preview` can stay, just adjust the drawing bounds to match. - Ensure the function returns `width=THUMB_SIZE, height=THUMB_SIZE` pixels. ### 5. Re-layout the grid Change the grid construction from 2 columns to 3 columns using `GRID_COLUMNS`: ```rust let mut grid_rows = column![].spacing(GRID_SPACING); let mut current_row = row![].spacing(GRID_SPACING); for (idx, preset) in filtered_styles.iter().enumerate() { // ... build 40 px cell with 36 px image + 8 px label ... current_row = current_row.push(clickable); if (idx + 1) % GRID_COLUMNS == 0 || idx == filtered_styles.len() - 1 { grid_rows = grid_rows.push(current_row); current_row = row![].spacing(GRID_SPACING); } } ``` Apply the same 3-column compact layout to: - Built-in styles grid. - Watercolor presets grid. - Media presets grid. For imported presets, keep the list-row layout but set the preview image to 36 px and container to 40 px. ### 6. Replace the bottom static preview with the live preview Locate the existing code: ```rust let preview_handle = get_brush_preview(current_style); let live_preview = container(iced::widget::image(preview_handle).width(64).height(64)) ... ``` Replace with: ```rust let preview_handle = get_live_stamp_preview( current_style, current_size, current_opacity, current_hardness, fg_color, ); let live_preview = container(iced::widget::image(preview_handle).width(PREVIEW_IMG_SIZE as f32).height(PREVIEW_IMG_SIZE as f32)) .width(Length::Fill) .height(56) .center_x(Length::Fill) .center_y(Length::Fill) .style(move |_theme| styles::recessed_control(colors)); ``` Make the preview area slightly shorter (56 px instead of 80 px) to reclaim vertical space. ### 7. Tighten slider/controls spacing - Reduce the column padding from `4` to `3`. - Keep sliders, but make the panel column spacing `2` or `3`. --- ## Affected Files 1. `hcie-iced-app/crates/hcie-iced-gui/src/panels/brushes.rs` — main panel layout and preview logic. 2. `hcie-iced-app/crates/hcie-iced-gui/src/dock/view.rs` — pass `app.fg_color` to `brushes::view`. No engine crates are modified. No `Cargo.toml` changes are needed because `hcie_engine_api` is already a dependency. --- ## Validation 1. `cargo check -p hcie-iced-gui` must pass. 2. `cargo test -p hcie-iced-gui brushes` (or `cargo test -p hcie-iced-gui`) must pass; the existing tests in `brushes.rs` (`catalog_contains_every_engine_brush_style`, `category_filter_keeps_expected_groups`, `media_crates_expose_complete_unique_catalog`) must remain green. 3. Launch the Iced GUI and open the Brushes & Tips panel. Visually verify: - 3-column grid of 40 px cells. - 8 px labels still readable. - Bottom preview area updates when size, opacity, hardness, or foreground color changes. - Selection border still highlights the active brush. 4. If the Iced CLI screenshot mechanism works, capture the Brushes panel for before/after comparison. --- ## Risks 1. **Cache growth**: the parameter-keyed cache can grow quickly if size/opacity/hardness are continuous. Mitigation: quantize size to `u8`, hardness/opacity to 0..100, and fg color to full bytes; this bounds the space to ~256 * 101 * 101 * 256^4 possible keys but only populated entries consume memory. 2. **Performance of `render_brush_stamp_preview`**: small single-dab previews (36x36 or 40x40) should be fast; if not, consider throttling updates to panel redraws only. 3. **Iced layout overflow**: switching to 3 columns with 3 px spacing in a narrow pane may still wrap. If so, the pane minimum width in `dock/sizing.rs` may need a small bump (currently 160 px preferred 300 px). Test first; only bump if required. 4. **Tests assume style-only cache**: the existing `BRUSH_PREVIEW_CACHE` tests do not inspect cache contents, so resizing should not break them. --- ## Out of Scope - EGUI (`hcie-egui-app`) changes — explicitly excluded by the user; it remains a reference only. - Engine-side brush rendering changes — engine crates are locked. - New brush engine presets or categories. - Bitmap/imported brush stamp preview fidelity beyond what `render_brush_stamp_preview` already provides.