Phase 3: Pixel tools (brush, eraser, eyedropper, fill) and event routing#9
Merged
Merged
Conversation
…dules
- Add HasPointerData impl to NativeClickData in dioxus-native-dom patch,
synthesising a mouse-type primary pointer from the BlitzMouseButtonEvent.
Fixes the unimplemented!() panic that would fire on any onpointerdown /
onpointermove handler in Phase 3 tool input routing.
- Split the monolithic events.rs (386 lines) into events/{mod,mouse,keyboard,
form,touch}.rs to respect the 300-line / 100-line mod.rs ceilings.
- Remove the fontique patch entry from workspace Cargo.toml; the patch
declared version 0.8.0 but blitz-dom 0.2.4 resolves fontique 0.6.0,
so the patch never applied and generated a spurious cargo warning.
The build is clean without it; re-evaluate if fontconfig_sys linkage
errors appear on CI Linux.
Which case applied: CASE B (HtmlEventConverter panics NOT fixed upstream).
The dioxus-native-dom patch IS applying (to 0.7.9 — dioxus 0.7.4 depends
on native-dom 0.7.9 internally). convert_mouse_data already worked;
convert_pointer_data now delegates to the same NativeClickData path.
No unused-patch warnings remain.
https://claude.ai/code/session_012imcXHNWwg8q1TNRo1un9K
… wiring Implements SPEC.md §7 (tool system) and §11 (canvas interaction): iris-canvas: - Add tool_event.rs: ToolEvent enum (Down/Move/Up/Scroll/Pinch) + PointerButton; all positions in document space. - Add on_tool_event: EventHandler<ToolEvent> prop to IrisCanvasProps. - Wire onmousedown, onmousemove, onmouseup on the canvas element; each converts element_coordinates() → screen_to_doc() → ToolEvent. - Add onwheel handler (correct logic; fires when blitz-shell routes MouseWheel to Dioxus — currently blitz 0.2.x consumes at CSS scroll level; see TODO comment in canvas_widget.rs §11.2). - Ctrl+wheel zooms via viewport.zoom_to(); plain wheel pans viewport.pan. iris-app: - Add tool_dispatch.rs: dispatch_tool_event routes by ToolMode; pixel path logs Down/Move/Up/Scroll/Pinch with doc coordinates. - Wire CanvasArea → IrisCanvas on_tool_event → dispatch_tool_event. dioxus-native-dom patch: - Add events/wheel.rs: NativeWheelData stub implementing HasWheelData; prevents convert_wheel_data from panicking when a future blitz version routes wheel events through Dioxus. Scroll/pinch via wheel requires a blitz-shell patch to call handle_ui_event for MouseWheel; tracked as TODO(iris) SPEC.md §11.2. Mouse down/move/up events fire and dispatch correctly. https://claude.ai/code/session_012imcXHNWwg8q1TNRo1un9K
…n panic SurfaceError::Outdated returned from maybe_blit_and_present() causes an unrecoverable panic on Windows/DX12 when use_wgpu registers a CustomPaintSource during window init. The upstream author left a TODO at surface_renderer.rs:228 indicating this was a known gap. Fix: match on get_current_texture() result and return early (skip frame) for Outdated and Lost; unknown errors still panic. The next redraw event retries successfully. - Copy wgpu_context-0.1.2 to crates/patches/wgpu-context/ - Add tracing dependency for the warn! log on skipped frames - Register wgpu_context patch in workspace [patch.crates-io] - Add docs/patches.md documenting all vendored patches https://claude.ai/code/session_012imcXHNWwg8q1TNRo1un9K
anyrender_vello's blit pass samples the registered texture in a shader (via vello::Renderer::register_texture), which requires both flags in addition to the COPY_SRC already present. COPY_DST is retained for the initial queue.write_texture upload. https://claude.ai/code/session_012imcXHNWwg8q1TNRo1un9K
Blitz's custom paint canvas elements don't participate in the hit-test tree for mouse events — only standard HTML elements receive pointer events. Moving all event handlers to a wrapper div fixes the routing so onmousedown/onmousemove/onmouseup reach the tool dispatch system. COMPAT(blitz) comment documents the workaround. The inner <canvas> element is now purely visual with no event handlers. Verified with software Vulkan renderer: Down/Move/Up events fire with correct doc-space coordinates. Canvas center = doc (0.0, 0.0). https://claude.ai/code/session_012imcXHNWwg8q1TNRo1un9K
New crate iris-tools owns all pixel tool implementations. BrushEngine (src/brush.rs): - Hard round brush with pressure-sensitive size (size_px * pressure) - Softness falloff: hardness 0.0–1.0 controls the feather zone - Sub-pixel spacing interpolation between ToolEvent::Move positions - Porter-Duff "over" compositing into f16 RGBA TileData - erase_mode flag uses destination-out instead of over EraserEngine (src/eraser.rs): wraps BrushEngine with erase_mode=true. Wired into iris-app: - PixelToolState added to AppState (brush + eraser engines) - dispatch_pixel_tool calls on_down/on_move/on_up per ToolEvent - canvas_dirty flag triggers tree_signal sync in canvas_area.rs so IrisCanvasPaintSource composites the painted tiles on next frame - selected_layer initialised to Background layer in AppState::default - Viewport pan initialised to (doc_w/2, doc_h/2) so doc (0,0) is at canvas top-left at zoom=1 (fixes brush landing in negative tile coords) Fix: read-guard from match state.read().tool_mode was kept alive across dispatch_pixel_tool causing AlreadyBorrowed panic on state.write(); resolved by binding tool_mode before the match. Verified: brush strokes appear on canvas; 4 unit tests pass. https://claude.ai/code/session_012imcXHNWwg8q1TNRo1un9K
- canvas_widget.rs: wrapper div fills parent (flex: 1; 100% w/h) instead of being fixed 800×600; reads rendered_size signal for screen_to_doc() so coordinate transforms use actual visible dimensions; adds #1e1e1e background (Change 2 + 4) - canvas_area.rs: rename constants to DOC_WIDTH/DOC_HEIGHT to clarify these are document dimensions, not the rendered canvas element size; TODO(iris): SPEC.md §11.3 for onmounted measurement (Change 1) - compositor/mod.rs: fill_document_background() draws an opaque white rect over the doc boundary before layer compositing so the document extent is visible against the dark canvas background (Change 3) https://claude.ai/code/session_012imcXHNWwg8q1TNRo1un9K
canvas_area.rs: derive canvas_w/canvas_h from window size (1280×800 default) minus shell chrome constants (left=56, right=240, top=176, bottom=24), pass them as IrisCanvas width/height props. These now represent the rendered area, not the document size. canvas_widget.rs: props.width/height IS the rendered size — removed rendered_size signal, use props directly in screen_to_doc() and as <canvas> element dimensions so Blitz calls render() with the correct texture size. Document dimensions come from tree.canvas_width/height in the compositor (already correct). TODO(iris): SPEC.md §11.3 — replace hardcoded window size with a real window-size hook once dioxus-native exposes one. Long-term fix is implementing get_client_rect() in dioxus-native-dom via blitz_dom Node::final_layout, tracked in canvas_widget.rs TODO comment. https://claude.ai/code/session_012imcXHNWwg8q1TNRo1un9K
Adds real rendered-size measurement via Blitz's Taffy layout data,
eliminating hardcoded window dimensions for canvas coordinate transforms.
dioxus-native-dom/events/mod.rs:
- BlitzMountedData { rect: PixelsRect } implements RenderedElementBacking
with a get_client_rect() that returns the pre-captured Taffy layout rect
- convert_mounted_data() downcasts PlatformEventData to BlitzMountedData
- make_pixels_rect() helper builds a PixelsRect from f32 Taffy size fields
dioxus-native-dom/mutation_writer.rs:
- DioxusState gains pending_mounted_ids: Vec<(ElementId, NodeId)>
- create_event_listener() queues elements with onmounted handlers
dioxus-native-dom/dioxus_document.rs:
- DioxusDocument gains deferred_mounted_ids
- initial_build() drains pending → deferred (layout not yet run)
- poll() fires deferred events (node.final_layout.size is now valid)
before render_immediate, then drains new pending → deferred
iris-canvas/canvas_widget.rs:
- rendered_size signal initialised from props (window-minus-chrome estimate)
- onmounted handler spawns async task calling get_client_rect().await,
updates rendered_size with actual element dimensions from Blitz layout
- All event handlers (down/move/up/wheel) read rendered_size for
screen_to_doc() coordinate transforms
https://claude.ai/code/session_012imcXHNWwg8q1TNRo1un9K
initial_build() no longer drains pending_mounted_ids → deferred. Blitz's layout pass (resolve()) runs AFTER the first poll(), not between initial_build() and poll(). Moving events to deferred in initial_build() caused them to fire in poll() #1 before layout, giving final_layout.size = (0, 0). Correct sequence now: initial_build() → pending_mounted_ids accumulates events poll() #1 → fires deferred (empty) → render_immediate → drains pending → deferred resolve() → layout computed poll() #2 → fires deferred (final_layout.size is valid) ✓ Also expand onmounted debug log to show rect.origin alongside size, confirming that width()/height() return size fields, not position. https://claude.ai/code/session_012imcXHNWwg8q1TNRo1un9K
canvas_widget: logs client_x/y (page-relative) vs elem_x/y (element-local after target_origin subtraction) plus rendered_size and doc coords on onmousedown. dioxus_document: logs raw/origin/element coords under the tracing feature flag for each mouse event. https://claude.ai/code/session_012imcXHNWwg8q1TNRo1un9K
…nted The previous approach initialized rendered_size from a hardcoded window-minus-chrome estimate (984×600) and updated it via onmounted. Because onmounted fires async in poll #2, the first click (which can arrive in poll #1) used the wrong size, causing a systematic offset in screen_to_doc (wrong canvas center anchor). IrisCanvasPaintSource::render() now writes the Blitz-reported width/height into a shared Arc<Mutex<(u32,u32)>> before compositing. Event handlers read from this shared size, which is guaranteed to be correct (1144×633 or whatever the actual window is) by the time any click can reach them, since Blitz paints before the window becomes interactive. onmounted is retained for diagnostic logging only. https://claude.ai/code/session_012imcXHNWwg8q1TNRo1un9K
Root cause: blitz-paint passes PHYSICAL pixel dimensions to CustomPaintSource::render() — content_box.width() = layout.size.width * scale (confirmed in blitz-paint-0.2.1/src/render.rs:780). On a 2x Retina display, width=2288, height=1266 while the correct logical CSS size is 1144×633. Storing physical dimensions in shared_size caused screen_to_doc() to use a wrong canvas center (1144,633) instead of (572,316), creating a fixed 572px horizontal offset. On maximize the physical dimensions grew further, making the offset proportionally worse — exactly the "maximizing makes it more pronounced" symptom. Fix: divide width/height by scale at the very top of render() before any early returns, storing the logical CSS size. Also restored the onmounted path as a belt-and-suspenders fallback (fires with logical Taffy layout size in CSS pixels, same value). https://claude.ai/code/session_012imcXHNWwg8q1TNRo1un9K
… in compositor On HiDPI displays, blitz-paint passes physical pixel dimensions to CustomPaintSource::render() (physical = logical × scale). The compositor was calling doc_to_screen() with physical dims while the event handler called screen_to_doc() with logical dims — producing mismatched center anchors that caused the painted stroke to drift from the cursor by an amount proportional to the distance from canvas center. Fix: composite_to_texture() now takes a scale parameter, computes logical_w/h from physical/scale, and passes logical dims to doc_to_screen() / visible_doc_rect() for all viewport transforms. Pixel placement then scales the resulting screen coordinates back to physical pixel coordinates before writing to the accumulation buffer. https://claude.ai/code/session_012imcXHNWwg8q1TNRo1un9K
Eyedropper (iris-tools/src/eyedropper.rs):
sample_color() composites visible pixel layers bottom-to-top with
Porter-Duff over to sample the colour at a document-space point.
Wired to ToolEvent::Down with Eyedropper active; updates
AppState.foreground_color and brush.settings.color.
Flood fill (iris-tools/src/fill.rs):
flood_fill() BFS-fills a contiguous region of similar pixels.
Tolerance is Euclidean RGB distance; fill is bounded by doc_rect
to prevent runaway expansion on blank canvases.
Rectangular marquee (iris-app/src/state.rs + tool_dispatch.rs):
Selection { rect: Option<kurbo::Rect> } added to AppState.
Down starts drag, Move updates rect, Up finalises.
BrushEngine.settings.selection gates stamp() per-pixel so brush
and eraser only paint inside the active selection.
Foreground/background colour (AppState.foreground_color/background_color):
Defaults: black / white. Eyedropper writes foreground_color.
ToolPalette shows two colour swatches; click either to swap fg/bg.
Brush split: brush.rs → brush.rs + brush/paint.rs (was 405 lines, now
214 + 153) to comply with the 300-line ceiling. PixelTool sub-enum
added to state.rs for per-tool dispatch in tool_dispatch.rs.
All 9 iris-tools tests pass. cargo check --workspace clean.
https://claude.ai/code/session_012imcXHNWwg8q1TNRo1un9K
The Ubuntu runner lacks libwayland-dev (and related libs) which are required by wayland-sys at build time. Add an apt-get step covering the full set of system libraries needed by blitz/winit on Linux CI. https://claude.ai/code/session_012imcXHNWwg8q1TNRo1un9K
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements Phase 3 pixel tool pipeline: brush engine with pressure-sensitive sizing and sub-pixel interpolation, eraser (destination-out mode), eyedropper (color sampling), flood fill, and complete event routing from canvas to tool handlers. Refactors event handling in dioxus-native-dom to modularize by event type. Adds wgpu-context crate for surface and buffer rendering utilities.
Key Changes
New crates:
iris-tools: Brush, eraser, eyedropper, and flood-fill implementations with pressure support and tile-based dirty trackingwgpu-context: Extracted wgpu surface/buffer management (SurfaceRenderer, BufferRenderer, DeviceHandle)Tool implementations:
BrushEngine: Hard round brush with pressure-sensitive diameter, sub-pixel spacing interpolation, and f16 tile encodingEraserEngine: Wrapper around BrushEngine with destination-out compositingflood_fill(): BFS fill with Euclidean RGB tolerance and bounded expansionsample_color(): Eyedropper via bottom-to-top Porter-Duff compositing of visible layersEvent routing:
tool_dispatch.rs: Routes ToolEvent to active pixel tool (brush, eraser, eyedropper, fill, marquee)ToolEventenum: Normalised down/move/up/scroll/pinch in document space with pressureEvent handling refactor (dioxus-native-dom):
events.rs(348 lines) into modular submodules:form.rs,keyboard.rs,mouse.rs,touch.rs,wheel.rsBlitzMountedData: Platform-side mounted element backing with post-layout border-box rectmake_pixels_rect(): Helper for PixelsRect construction from blitz layoutCanvas and compositor updates:
IrisCanvas: Addedon_tool_eventhandler and shared size tracking for DPI-aware coordinate transformsCompositor::render(): Accepts physical pixel dimensions and DPI scale; converts to logical for viewport transformspaint_bridge.rs: Tracks rendered_size for event handler coordinate space consistencyUI state and dispatch:
AppState: AddedPixelToolState(brush, eraser, fill_tolerance),Selection,PixelToolenum, foreground/background colorstool_palette.rs: Refactored button styling; added pixel tool selection UIDocumentation:
docs/patches.md: Comprehensive guide to all vendored patches (blitz-dom, blitz-shell, dioxus-native-dom, wgpu-context) with removal conditionsImplementation Details
pressure_sizeis enabled; mouse always reports 1.0All files follow CLAUDE.md rules: ≤300 lines, copyright headers, typed errors, no unsafe, mandatory TODO/COMPAT annotations.
https://claude.ai/code/session_012imcXHNWwg8q1TNRo1un9K