Skip to main content

agg_gui/widget/app/
pointer.rs

1//! Pointer-input methods on [`App`] — mouse move/down/up plus the
2//! modal-subtree hit extension they share. Split out of `app.rs`
3//! (800-line guardrail); wheel and keyboard routing stay in `app.rs`.
4
5use super::tree_paths::widget_at_path;
6use crate::event::{Event, Modifiers, MouseButton};
7use crate::geometry::Point;
8use crate::widget::tree::{active_modal_path, dispatch_event, hit_test_subtree};
9use crate::widget::tree_inspector::set_current_mouse_world;
10use crate::widget::{App, Widget};
11
12impl App {
13    /// Extend the active modal widget's path by hit-testing inside its
14    /// subtree at `pos_in_root`, so the modal's own children (buttons,
15    /// fields, scroll views) receive pointer events. Falls back to the
16    /// modal widget itself when nothing inside is hit (the scrim).
17    pub(crate) fn extend_modal_path(&self, modal_path: &[usize], pos_in_root: Point) -> Vec<usize> {
18        let mut widget: &dyn Widget = self.root.as_ref();
19        let mut pos = pos_in_root;
20        for &index in modal_path {
21            let Some(child) = widget.children().get(index) else {
22                return modal_path.to_vec();
23            };
24            let bounds = child.bounds();
25            if let Some(t) = widget.child_transform() {
26                t.inverse_transform(&mut pos.x, &mut pos.y);
27            }
28            pos = Point::new(pos.x - bounds.x, pos.y - bounds.y);
29            widget = child.as_ref();
30        }
31        let mut full = modal_path.to_vec();
32        if let Some(sub_path) = hit_test_subtree(widget, pos) {
33            full.extend(sub_path);
34        }
35        full
36    }
37
38    /// Mouse cursor moved. `screen_y` is Y-down physical pixels.
39    pub fn on_mouse_move(&mut self, screen_x: f64, screen_y: f64) {
40        // Reset cursor so the hovered widget can set it; Default if nothing sets it.
41        crate::cursor::reset_cursor_icon();
42        let screen = self.flip_y(screen_x, screen_y);
43        if crate::widgets::on_screen_keyboard::handle_software_keyboard_mouse_move(screen) {
44            self.drain_keyboard_synthetic_keys();
45            return;
46        }
47        let pos = super::keyboard_scroll::lift_to_world(screen);
48        set_current_mouse_world(pos);
49        if let Some(path) = active_modal_path(self.root.as_ref()) {
50            let path = self.extend_modal_path(&path, pos);
51            let event = Event::MouseMove { pos };
52            dispatch_event(&mut self.root, &path, &event, pos);
53            self.hovered = Some(path);
54            return;
55        }
56        self.dispatch_mouse_move(pos);
57    }
58
59    /// Mouse button pressed. `screen_y` is Y-down physical pixels.
60    pub fn on_mouse_down(
61        &mut self,
62        screen_x: f64,
63        screen_y: f64,
64        button: MouseButton,
65        mods: Modifiers,
66    ) {
67        let screen = self.flip_y(screen_x, screen_y);
68        // On-screen keyboard captures pointer events on its panel area
69        // before anything in the tree gets a look. Returning here also
70        // means the focused widget keeps focus (so the keyboard does
71        // not dismiss itself by stealing focus on every key tap).
72        if crate::widgets::on_screen_keyboard::handle_software_keyboard_mouse_down(
73            screen, button, mods,
74        ) {
75            return;
76        }
77        let pos = super::keyboard_scroll::lift_to_world(screen);
78        set_current_mouse_world(pos);
79        // A press hides any visible central tooltip and keeps it suppressed
80        // until the pointer leaves and re-enters a tipped widget.
81        crate::widgets::tooltip::controller::on_pointer_down();
82        // Count this press so widgets running their own multi-click gesture
83        // (Scene's background double-click) can detect an intervening press
84        // even when a hosted child consumes it before it can bubble.
85        crate::animation::bump_pointer_press_epoch();
86        let modal_path = active_modal_path(self.root.as_ref());
87        let event = Event::MouseDown {
88            pos,
89            button,
90            modifiers: mods,
91        };
92        if let Some(path) = modal_path {
93            // Hit-test inside the modal subtree so its children get the
94            // click, with the same click-to-focus rule as the normal path
95            // (text fields in dialogs need focus to type).
96            let path = self.extend_modal_path(&path, pos);
97            if widget_at_path(&mut self.root, &path).is_focusable() {
98                self.set_focus(Some(path.clone()));
99            } else {
100                self.set_focus(None);
101            }
102            if dispatch_event(&mut self.root, &path, &event, pos).is_consumed() {
103                self.captured = Some(path);
104            }
105            return;
106        }
107        let hit = self.compute_hit(pos);
108
109        // Click-to-focus: if the hit widget is focusable, give it focus.
110        if let Some(ref path) = hit {
111            let w = widget_at_path(&mut self.root, path);
112            if w.is_focusable() {
113                self.set_focus(Some(path.clone()));
114            } else {
115                self.set_focus(None);
116            }
117        } else {
118            self.set_focus(None);
119        }
120
121        if let Some(mut path) = hit {
122            let result = dispatch_event(&mut self.root, &path, &event, pos);
123            if result.is_consumed() {
124                self.maybe_bring_to_front(&mut path);
125                let capture_path = self.compute_hit(pos).unwrap_or(path);
126                self.captured = Some(capture_path);
127            }
128        }
129        // NO blanket request_draw.  Mouse-down on an inert area must not
130        // cause a repaint.  Each widget that changes visual state in
131        // response to a MouseDown (button press, window raise, focus
132        // indicator on the focus-gained widget, etc.) is responsible for
133        // calling `crate::animation::request_draw` itself.
134    }
135
136    /// Mouse button released. `screen_y` is Y-down.
137    pub fn on_mouse_up(
138        &mut self,
139        screen_x: f64,
140        screen_y: f64,
141        button: MouseButton,
142        mods: Modifiers,
143    ) {
144        let screen = self.flip_y(screen_x, screen_y);
145        // On-screen keyboard owns release events on its panel; releases
146        // here commit a key tap and synthesize a `KeyDown`. After
147        // consumption we drain the synthetic-key queue so the focused
148        // text widget receives the character in the same frame.
149        if crate::widgets::on_screen_keyboard::handle_software_keyboard_mouse_up(
150            screen, button, mods,
151        ) {
152            self.captured = None;
153            self.drain_keyboard_synthetic_keys();
154            return;
155        }
156        let pos = super::keyboard_scroll::lift_to_world(screen);
157        set_current_mouse_world(pos);
158        crate::widgets::tooltip::controller::on_pointer_up();
159        let event = Event::MouseUp {
160            pos,
161            button,
162            modifiers: mods,
163        };
164        if let Some(path) = active_modal_path(self.root.as_ref()) {
165            // Deliver the release where the press was captured (button
166            // click completion), falling back to the hit inside the modal.
167            let path = self
168                .captured
169                .take()
170                .unwrap_or_else(|| self.extend_modal_path(&path, pos));
171            dispatch_event(&mut self.root, &path, &event, pos);
172            return;
173        }
174        // Deliver release to captured widget first (if any), then clear capture.
175        if let Some(path) = self.captured.take() {
176            dispatch_event(&mut self.root, &path, &event, pos);
177        } else {
178            let hit = self.compute_hit(pos);
179            if let Some(path) = hit {
180                dispatch_event(&mut self.root, &path, &event, pos);
181            }
182        }
183    }
184}