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
This commit is contained in:
Alex Gorevski 2026-10-07 13:31:15 -07:00 committed by GitHub
parent 0aabb48687
commit 2050ca083e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 47 additions and 3 deletions

View File

@ -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<egui::Modifiers> {
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);
}
}
}

View File

@ -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);