From 2050ca083e0cfe350bc730e88251f564b19a96d9 Mon Sep 17 00:00:00 2001 From: Alex Gorevski Date: Wed, 7 Oct 2026 13:31:15 -0700 Subject: [PATCH] Fix #570: connect brush strokes on Shift-click (#654) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e77cd34a-ce3e-4981-996f-4d37d49df90d --- crates/ui-egui/src/canvas.rs | 18 ++++++++++++++--- crates/ui-egui/src/paint_mouse.rs | 32 +++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 71e9c7b5..e8a2e8da 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -304,6 +304,16 @@ fn pointer_moves(events: &[egui::Event], button: egui::PointerButton, down_at_st out } +/// Modifiers attached to the pointer event itself. Some input sources provide these without a +/// separate `ModifiersChanged` event, so use them for click gestures before falling back to the +/// frame-wide modifier state. +fn pointer_button_modifiers(events: &[egui::Event], button: egui::PointerButton) -> Option { + events.iter().rev().find_map(|e| match e { + egui::Event::PointerButton { button: b, modifiers, .. } if *b == button => Some(*modifiers), + _ => None, + }) +} + /// Abstract tool event, produced by the mouse or by automation (`ui.pointer`). #[derive(Clone, Copy, Debug, PartialEq)] pub enum ToolEvent { @@ -1761,10 +1771,12 @@ pub fn canvas_view(app: &mut PhotocraftApp, ui: &mut egui::Ui, idx: usize, rect: if buttons.clicked && let Some(p) = response.interact_pointer_pos() { + let click_button = if app.secondary_erase { PointerButton::Secondary } else { PointerButton::Primary }; + let click_mods = ui.input(|i| pointer_button_modifiers(&i.events, click_button)).unwrap_or(mods); let d = xf.to_doc(p); match tool { Tool::Zoom => { - let nz = zoom_step(view.zoom, if zoom_out(mods.alt) { -1 } else { 1 }); + let nz = zoom_step(view.zoom, if zoom_out(click_mods.alt) { -1 } else { 1 }); zoom_about(&mut view, &xf, p, nz); } // A click with the (temporary) Hand does nothing, never the tool underneath. @@ -1777,8 +1789,8 @@ pub fn canvas_view(app: &mut PhotocraftApp, ui: &mut egui::Ui, idx: usize, rect: if tool == Tool::Move && app.ui.transform.is_none() { begin_transform_controls_at(app, &ctx, &xf, p); } - tool_event(app, ToolEvent::Down { x: d[0], y: d[1], pressure: 1.0 }, mods); - tool_event(app, ToolEvent::Up { x: d[0], y: d[1] }, mods); + tool_event(app, ToolEvent::Down { x: d[0], y: d[1], pressure: 1.0 }, click_mods); + tool_event(app, ToolEvent::Up { x: d[0], y: d[1] }, click_mods); } } } diff --git a/crates/ui-egui/src/paint_mouse.rs b/crates/ui-egui/src/paint_mouse.rs index 84647c8a..69fdb96b 100644 --- a/crates/ui-egui/src/paint_mouse.rs +++ b/crates/ui-egui/src/paint_mouse.rs @@ -223,6 +223,38 @@ mod tests { assert!(alpha_at(&h, a + (b - a) * 0.5) > 0.9); } + #[test] + fn shift_click_connects_to_the_previous_brush_stroke() { + use egui::{Event, Modifiers}; + + let mut h = harness(None); + let (_, end) = drag(&mut h, PointerButton::Primary); + let previous = h + .state() + .session + .journal + .iter() + .rev() + .find(|(id, _)| id == "paint.stroke") + .map(|(_, p)| p["points"].as_array().unwrap().last().unwrap().clone()) + .unwrap(); + let target = end + vec2(120.0, 80.0); + h.event(Event::PointerMoved(target)); + h.run_steps(1); + for pressed in [true, false] { + h.event(Event::PointerButton { pos: target, button: PointerButton::Primary, pressed, modifiers: Modifiers::SHIFT }); + h.run_steps(2); + } + + let strokes = strokes(&h); + assert_eq!(strokes.len(), 2, "a Shift-click commits one connected stroke"); + let points = strokes[1]["points"].as_array().unwrap(); + assert_eq!(points.len(), 2, "a click has the previous endpoint and clicked endpoint"); + assert_eq!(points[0][0], previous[0]); + assert_eq!(points[0][1], previous[1]); + assert!(alpha_at(&h, end + vec2(60.0, 40.0)) > 0.9, "the segment between the strokes is painted"); + } + #[test] fn right_click_opens_the_brush_picker_and_never_paints() { let mut h = harness(None);