Skip to main content

blockworx_egui/
interaction.rs

1//! The egui shim that fills in one frame's [`Interaction`].
2//!
3//! The type itself is [`blockworx_paint::Interaction`]; this reads it off an
4//! egui [`Response`] and the frame's global input, with every position mapped
5//! into world space on the way through.
6
7use blockworx_geom::Pos2;
8use blockworx_paint::{Event, Interaction, Press, Zoom};
9use egui::Response;
10
11use crate::convert::IntoGeom as _;
12
13/// Build the full `Interaction` for one frame.
14/// Positions in `event` are world-space; flags are read directly from the response and
15/// global input. Both can be set simultaneously — they are independent sources.
16pub(crate) fn compute_interaction(
17    response: &Response,
18    screen_to_world: impl Fn(Pos2) -> Pos2,
19    zoom: Zoom,
20    ui: &egui::Ui,
21) -> Interaction {
22    // Scoped to presses that began on the canvas, so holding a toolbar button
23    // (whose screen point maps to some world position) can't arm a canvas
24    // affordance.
25    let press = (response.is_pointer_button_down_on() && ui.input(|i| i.pointer.primary_down()))
26        .then(|| ui.input(|i| i.pointer.press_origin()))
27        .flatten()
28        .map(|origin| Press {
29            origin: screen_to_world(origin.geom()),
30        });
31    let event = compute_event(response, screen_to_world, zoom);
32    let clicked = matches!(event, Some(Event::Clicked { .. }));
33    let lost_focus = response.lost_focus() || clicked;
34    // Scope enter to when the canvas has keyboard focus so it doesn't fire
35    // while a text field elsewhere is active.
36    let enter_pressed = response.has_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
37    // Any other widget owning the keyboard — a rename TextEdit, the nav
38    // dialog's filter box, the command palette — claims these keys, so a
39    // Backspace editing its text must not delete the selected shape and its
40    // Escape must not cancel an in-progress gesture.
41    let other_focus = ui.memory(|m| m.focused().is_some_and(|id| id != response.id));
42    let delete_pressed = !response.has_focus()
43        && !other_focus
44        && ui.input(|i| i.key_pressed(egui::Key::Delete) || i.key_pressed(egui::Key::Backspace));
45    // Esc from the canvas (no text editor active) — used to cancel an in-progress
46    // route. Same focus scoping as `delete_pressed`. The `View` additionally ORs in
47    // the editor's own Escape (for the block-edit cycle), so editing is unaffected.
48    let escape_pressed =
49        !response.has_focus() && !other_focus && ui.input(|i| i.key_pressed(egui::Key::Escape));
50    let shift = ui.input(|i| i.modifiers.shift);
51    Interaction {
52        event,
53        press,
54        lost_focus,
55        enter_pressed,
56        // Tab originates only from the active text editor; the `View` ORs in last
57        // frame's editor result (like `lost_focus`/`enter_pressed`).
58        tab_pressed: false,
59        escape_pressed,
60        delete_pressed,
61        shift,
62    }
63}
64
65pub(crate) fn compute_event(
66    response: &Response,
67    screen_to_world: impl Fn(Pos2) -> Pos2,
68    zoom: Zoom,
69) -> Option<Event> {
70    if response.double_clicked()
71        && let Some(pos) = response.interact_pointer_pos()
72    {
73        Some(Event::DoubleClicked {
74            pos: screen_to_world(pos.geom()),
75        })
76    } else if response.clicked()
77        && let Some(pos) = response.interact_pointer_pos()
78    {
79        Some(Event::Clicked {
80            pos: screen_to_world(pos.geom()),
81        })
82    } else if response.drag_started()
83        && let Some(pos) = response.interact_pointer_pos()
84    {
85        Some(Event::DragStarted {
86            pos: screen_to_world(pos.geom()),
87        })
88    } else if response.dragged()
89        && let Some(pos) = response.interact_pointer_pos()
90    {
91        Some(Event::Dragging {
92            pos: screen_to_world(pos.geom()),
93            delta: response.drag_delta().geom() / zoom.get(),
94        })
95    } else if response.drag_stopped()
96        && let Some(pos) = response.interact_pointer_pos()
97    {
98        Some(Event::DragStopped {
99            pos: screen_to_world(pos.geom()),
100        })
101    } else if response.hovered()
102        && let Some(pos) = response.hover_pos()
103    {
104        Some(Event::HoverAt(screen_to_world(pos.geom())))
105    } else {
106        None
107    }
108}