zng_ext_input/
pointer_capture.rs

1//! Mouse and touch capture.
2
3use std::{
4    collections::{HashMap, HashSet},
5    fmt, mem,
6};
7
8use zng_app::{
9    AppExtension,
10    event::{event, event_args},
11    update::{EventUpdate, UPDATES},
12    view_process::{
13        VIEW_PROCESS_INITED_EVENT,
14        raw_device_events::DeviceId,
15        raw_events::{
16            RAW_FRAME_RENDERED_EVENT, RAW_MOUSE_INPUT_EVENT, RAW_MOUSE_MOVED_EVENT, RAW_TOUCH_EVENT, RAW_WINDOW_CLOSE_EVENT,
17            RAW_WINDOW_FOCUS_EVENT,
18        },
19    },
20    widget::{
21        WIDGET, WidgetId,
22        info::{InteractionPath, WIDGET_INFO_CHANGED_EVENT, WidgetInfoTree, WidgetPath},
23    },
24    window::{WINDOW, WindowId},
25};
26use zng_app_context::app_local;
27use zng_ext_window::{NestedWindowWidgetInfoExt, WINDOWS};
28use zng_layout::unit::{DipPoint, DipToPx};
29use zng_var::{ArcVar, ReadOnlyArcVar, Var, impl_from_and_into_var, var};
30use zng_view_api::{
31    mouse::{ButtonState, MouseButton},
32    touch::{TouchId, TouchPhase},
33};
34
35/// Application extension that provides mouse and touch capture service.
36///
37/// # Events
38///
39/// Events this extension provides.
40///
41/// * [`POINTER_CAPTURE_EVENT`]
42///
43/// # Services
44///
45/// Services this extension provides.
46///
47/// * [`POINTER_CAPTURE`]
48#[derive(Default)]
49pub struct PointerCaptureManager {
50    mouse_position: HashMap<(WindowId, DeviceId), DipPoint>,
51    mouse_down: HashSet<(WindowId, DeviceId, MouseButton)>,
52    touch_down: HashSet<(WindowId, DeviceId, TouchId)>,
53    capture: Option<CaptureInfo>,
54}
55impl AppExtension for PointerCaptureManager {
56    fn event(&mut self, update: &mut EventUpdate) {
57        if let Some(args) = RAW_FRAME_RENDERED_EVENT.on(update) {
58            if let Some(c) = &self.capture {
59                if c.target.window_id() == args.window_id {
60                    if let Ok(info) = WINDOWS.widget_tree(args.window_id) {
61                        self.continue_capture(&info);
62                    }
63                    // else will receive close event.
64                }
65            }
66        } else if let Some(args) = RAW_MOUSE_MOVED_EVENT.on(update) {
67            self.mouse_position.insert((args.window_id, args.device_id), args.position);
68        } else if let Some(args) = RAW_MOUSE_INPUT_EVENT.on(update) {
69            match args.state {
70                ButtonState::Pressed => {
71                    if self.mouse_down.insert((args.window_id, args.device_id, args.button))
72                        && self.mouse_down.len() == 1
73                        && self.touch_down.is_empty()
74                    {
75                        self.on_first_down(
76                            args.window_id,
77                            self.mouse_position
78                                .get(&(args.window_id, args.device_id))
79                                .copied()
80                                .unwrap_or_default(),
81                        );
82                    }
83                }
84                ButtonState::Released => {
85                    if self.mouse_down.remove(&(args.window_id, args.device_id, args.button))
86                        && self.mouse_down.is_empty()
87                        && self.touch_down.is_empty()
88                    {
89                        self.on_last_up();
90                    }
91                }
92            }
93        } else if let Some(args) = RAW_TOUCH_EVENT.on(update) {
94            for touch in &args.touches {
95                match touch.phase {
96                    TouchPhase::Start => {
97                        if self.touch_down.insert((args.window_id, args.device_id, touch.touch))
98                            && self.touch_down.len() == 1
99                            && self.mouse_down.is_empty()
100                        {
101                            self.on_first_down(args.window_id, touch.position);
102                        }
103                    }
104                    TouchPhase::End | TouchPhase::Cancel => {
105                        if self.touch_down.remove(&(args.window_id, args.device_id, touch.touch))
106                            && self.touch_down.is_empty()
107                            && self.mouse_down.is_empty()
108                        {
109                            self.on_last_up();
110                        }
111                    }
112                    TouchPhase::Move => {}
113                }
114            }
115        } else if let Some(args) = WIDGET_INFO_CHANGED_EVENT.on(update) {
116            if let Some(c) = &self.capture {
117                if c.target.window_id() == args.window_id {
118                    self.continue_capture(&args.tree);
119                }
120            }
121        } else if let Some(args) = RAW_WINDOW_CLOSE_EVENT.on(update) {
122            self.remove_window(args.window_id);
123        } else if let Some(args) = RAW_WINDOW_FOCUS_EVENT.on(update) {
124            let actual_prev = args.prev_focus.map(|id| WINDOWS.nest_parent(id).map(|(p, _)| p).unwrap_or(id));
125            let actual_new = args.new_focus.map(|id| WINDOWS.nest_parent(id).map(|(p, _)| p).unwrap_or(id));
126
127            if actual_prev == actual_new {
128                // can happen when focus moves from parent to nested, or malformed event
129                return;
130            }
131
132            if let Some(w) = actual_prev {
133                self.remove_window(w);
134            }
135        } else if let Some(args) = VIEW_PROCESS_INITED_EVENT.on(update) {
136            if args.is_respawn && (!self.mouse_down.is_empty() || !self.touch_down.is_empty()) {
137                self.mouse_down.clear();
138                self.touch_down.clear();
139                self.on_last_up();
140            }
141        }
142    }
143
144    fn update(&mut self) {
145        if let Some(current) = &self.capture {
146            let mut cap = POINTER_CAPTURE_SV.write();
147            if let Some((widget_id, mode)) = cap.capture_request.take() {
148                let is_win_focused = match WINDOWS.is_focused(current.target.window_id()) {
149                    Ok(mut f) => {
150                        if !f {
151                            // nested windows can take two updates to receive focus
152                            if let Some(parent) = WINDOWS.nest_parent(current.target.window_id()).map(|(p, _)| p) {
153                                f = WINDOWS.is_focused(parent) == Ok(true);
154                            }
155                        }
156                        f
157                    }
158                    Err(_) => false,
159                };
160                if is_win_focused {
161                    // current window pressed
162                    if let Some(widget) = WINDOWS.widget_tree(current.target.window_id()).unwrap().get(widget_id) {
163                        // request valid
164                        self.set_capture(&mut cap, widget.interaction_path(), mode);
165                    }
166                }
167            } else if mem::take(&mut cap.release_requested) && current.mode != CaptureMode::Window {
168                // release capture (back to default capture).
169                let target = current.target.root_path();
170                self.set_capture(&mut cap, InteractionPath::from_enabled(target.into_owned()), CaptureMode::Window);
171            }
172        }
173    }
174}
175impl PointerCaptureManager {
176    fn remove_window(&mut self, window_id: WindowId) {
177        self.mouse_position.retain(|(w, _), _| *w != window_id);
178
179        if !self.mouse_down.is_empty() || !self.touch_down.is_empty() {
180            self.mouse_down.retain(|(w, _, _)| *w != window_id);
181            self.touch_down.retain(|(w, _, _)| *w != window_id);
182
183            if self.mouse_down.is_empty() && self.touch_down.is_empty() {
184                self.on_last_up();
185            }
186        }
187    }
188
189    fn on_first_down(&mut self, window_id: WindowId, point: DipPoint) {
190        if let Ok(mut info) = WINDOWS.widget_tree(window_id) {
191            let mut cap = POINTER_CAPTURE_SV.write();
192            cap.release_requested = false;
193
194            let mut point = point.to_px(info.scale_factor());
195
196            // hit-test for nested window
197            if let Some(t) = info.root().hit_test(point).target() {
198                if let Some(w) = info.get(t.widget_id) {
199                    if let Some(t) = w.nested_window_tree() {
200                        info = t;
201                        point = w
202                            .inner_transform()
203                            .inverse()
204                            .and_then(|t| t.transform_point(point))
205                            .unwrap_or(point);
206                    }
207                }
208            }
209
210            if let Some((widget_id, mode)) = cap.capture_request.take() {
211                if let Some(w_info) = info.get(widget_id) {
212                    if w_info.hit_test(point).contains(widget_id) {
213                        // capture for widget
214                        self.set_capture(&mut cap, w_info.interaction_path(), mode);
215                        return;
216                    }
217                }
218            }
219
220            // default capture
221            self.set_capture(&mut cap, info.root().interaction_path(), CaptureMode::Window);
222        }
223    }
224
225    fn on_last_up(&mut self) {
226        let mut cap = POINTER_CAPTURE_SV.write();
227        cap.release_requested = false;
228        cap.capture_request = None;
229        self.unset_capture(&mut cap);
230    }
231
232    fn continue_capture(&mut self, info: &WidgetInfoTree) {
233        let current = self.capture.as_ref().unwrap();
234
235        if let Some(widget) = info.get(current.target.widget_id()) {
236            if let Some(new_path) = widget.new_interaction_path(&InteractionPath::from_enabled(current.target.clone())) {
237                // widget moved inside window tree.
238                let mode = current.mode;
239                self.set_capture(&mut POINTER_CAPTURE_SV.write(), new_path, mode);
240            }
241        } else {
242            // widget not found. Returns to default capture.
243            self.set_capture(&mut POINTER_CAPTURE_SV.write(), info.root().interaction_path(), CaptureMode::Window);
244        }
245    }
246
247    fn set_capture(&mut self, cap: &mut PointerCaptureService, target: InteractionPath, mode: CaptureMode) {
248        let new = target.enabled().map(|target| CaptureInfo { target, mode });
249        if new.is_none() {
250            self.unset_capture(cap);
251            return;
252        }
253        if new != self.capture {
254            let prev = self.capture.take();
255            self.capture.clone_from(&new);
256            cap.capture_value.clone_from(&new);
257            cap.capture.set(new.clone());
258            POINTER_CAPTURE_EVENT.notify(PointerCaptureArgs::now(prev, new));
259        }
260    }
261
262    fn unset_capture(&mut self, cap: &mut PointerCaptureService) {
263        if self.capture.is_some() {
264            let prev = self.capture.take();
265            cap.capture_value = None;
266            cap.capture.set(None);
267            POINTER_CAPTURE_EVENT.notify(PointerCaptureArgs::now(prev, None));
268        }
269    }
270}
271
272/// Mouse and touch capture service.
273///
274/// Mouse and touch is **captured** when mouse and touch events are redirected to a specific target. The user
275/// can still move the cursor or touch contact outside of the target but the widgets outside do not react to this.
276///
277/// You can request capture by calling [`capture_widget`](POINTER_CAPTURE::capture_widget) or
278/// [`capture_subtree`](POINTER_CAPTURE::capture_subtree) with a widget that was pressed by a mouse button or by touch.
279/// The capture will last for as long as any of the mouse buttons or touch contacts are pressed, the widget is visible
280/// and the window is focused.
281///
282/// Windows capture by default, this cannot be disabled. For other widgets this is optional.
283///
284/// # Provider
285///
286/// This service is provided by the [`PointerCaptureManager`] extension.
287#[expect(non_camel_case_types)]
288pub struct POINTER_CAPTURE;
289impl POINTER_CAPTURE {
290    /// Variable that gets the current capture target and mode.
291    pub fn current_capture(&self) -> ReadOnlyArcVar<Option<CaptureInfo>> {
292        POINTER_CAPTURE_SV.read().capture.read_only()
293    }
294
295    /// Set a widget to redirect all mouse and touch events to.
296    ///
297    /// The capture will be set only if the widget is pressed.
298    pub fn capture_widget(&self, widget_id: WidgetId) {
299        let mut m = POINTER_CAPTURE_SV.write();
300        m.capture_request = Some((widget_id, CaptureMode::Widget));
301        UPDATES.update(None);
302    }
303
304    /// Set a widget to be the root of a capture subtree.
305    ///
306    /// Mouse and touch events targeting inside the subtree go to target normally. Mouse and touch events outside
307    /// the capture root are redirected to the capture root.
308    ///
309    /// The capture will be set only if the widget is pressed.
310    pub fn capture_subtree(&self, widget_id: WidgetId) {
311        let mut m = POINTER_CAPTURE_SV.write();
312        m.capture_request = Some((widget_id, CaptureMode::Subtree));
313        UPDATES.update(None);
314    }
315
316    /// Release the current mouse and touch capture back to window.
317    ///
318    /// **Note:** The capture is released automatically when the mouse buttons or touch are released
319    /// or when the window loses focus.
320    pub fn release_capture(&self) {
321        let mut m = POINTER_CAPTURE_SV.write();
322        m.release_requested = true;
323        UPDATES.update(None);
324    }
325
326    /// Latest capture, already valid for the current raw mouse or touch event cycle.
327    pub(crate) fn current_capture_value(&self) -> Option<CaptureInfo> {
328        POINTER_CAPTURE_SV.read().capture_value.clone()
329    }
330}
331
332/// Mouse and touch capture mode.
333#[derive(Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
334pub enum CaptureMode {
335    /// Mouse and touch captured by the window only.
336    ///
337    /// Default behavior.
338    Window,
339    /// Mouse and touch events inside the widget sub-tree permitted. Mouse events
340    /// outside of the widget redirected to the widget.
341    Subtree,
342
343    /// Mouse and touch events redirected to the widget.
344    Widget,
345}
346impl fmt::Debug for CaptureMode {
347    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
348        if f.alternate() {
349            write!(f, "CaptureMode::")?;
350        }
351        match self {
352            CaptureMode::Window => write!(f, "Window"),
353            CaptureMode::Subtree => write!(f, "Subtree"),
354            CaptureMode::Widget => write!(f, "Widget"),
355        }
356    }
357}
358impl Default for CaptureMode {
359    /// [`CaptureMode::Window`]
360    fn default() -> Self {
361        CaptureMode::Window
362    }
363}
364impl_from_and_into_var! {
365    /// Convert `true` to [`CaptureMode::Widget`] and `false` to [`CaptureMode::Window`].
366    fn from(widget: bool) -> CaptureMode {
367        if widget {
368            CaptureMode::Widget
369        } else {
370            CaptureMode::Window
371        }
372    }
373}
374
375/// Information about mouse and touch capture in a mouse or touch event argument.
376#[derive(Debug, Clone, PartialEq, Eq)]
377pub struct CaptureInfo {
378    /// Widget that is capturing all mouse and touch events. The widget and all ancestors are [`ENABLED`].
379    ///
380    /// This is the window root widget for capture mode `Window`.
381    ///
382    /// [`ENABLED`]: zng_app::widget::info::Interactivity::ENABLED
383    pub target: WidgetPath,
384    /// Capture mode, see [`allows`](Self::allows) for more details.
385    pub mode: CaptureMode,
386}
387impl CaptureInfo {
388    /// If the widget is allowed by the current capture.
389    ///
390    /// This method uses [`WINDOW`] and [`WIDGET`] to identify the widget context.
391    ///
392    /// | Mode           | Allows                                             |
393    /// |----------------|----------------------------------------------------|
394    /// | `Window`       | All widgets in the same window.                    |
395    /// | `Subtree`      | All widgets that have the `target` in their path.  |
396    /// | `Widget`       | Only the `target` widget.                          |
397    ///
398    /// [`WIDGET`]: zng_app::widget::WIDGET
399    /// [`WINDOW`]: zng_app::window::WINDOW
400    pub fn allows(&self) -> bool {
401        match self.mode {
402            CaptureMode::Window => self.target.window_id() == WINDOW.id(),
403            CaptureMode::Widget => self.target.widget_id() == WIDGET.id(),
404            CaptureMode::Subtree => {
405                let tree = WINDOW.info();
406                if let Some(wgt) = tree.get(WIDGET.id()) {
407                    for wgt in wgt.self_and_ancestors() {
408                        if wgt.id() == self.target.widget_id() {
409                            return true;
410                        }
411                    }
412                }
413                false
414            }
415        }
416    }
417}
418
419app_local! {
420    static POINTER_CAPTURE_SV: PointerCaptureService = PointerCaptureService {
421        capture_value: None,
422        capture: var(None),
423        capture_request: None,
424        release_requested: false,
425    };
426}
427
428struct PointerCaptureService {
429    capture_value: Option<CaptureInfo>,
430    capture: ArcVar<Option<CaptureInfo>>,
431    capture_request: Option<(WidgetId, CaptureMode)>,
432    release_requested: bool,
433}
434
435event! {
436    /// Mouse and touch capture changed event.
437    pub static POINTER_CAPTURE_EVENT: PointerCaptureArgs;
438}
439
440event_args! {
441    /// [`POINTER_CAPTURE_EVENT`] arguments.
442    pub struct PointerCaptureArgs {
443        /// Previous mouse and touch capture target and mode.
444        pub prev_capture: Option<CaptureInfo>,
445        /// new mouse and capture target and mode.
446        pub new_capture: Option<CaptureInfo>,
447
448        ..
449
450        /// The [`prev_capture`] and [`new_capture`] paths start with the current path.
451        ///
452        /// [`prev_capture`]: Self::prev_capture
453        /// [`new_capture`]: Self::new_capture
454        fn delivery_list(&self, list: &mut UpdateDeliveryList) {
455            if let Some(p) = &self.prev_capture {
456                list.insert_wgt(&p.target);
457            }
458            if let Some(p) = &self.new_capture {
459                list.insert_wgt(&p.target);
460            }
461        }
462    }
463}
464
465impl PointerCaptureArgs {
466    /// If the same widget has pointer capture, but the widget path changed.
467    pub fn is_widget_move(&self) -> bool {
468        match (&self.prev_capture, &self.new_capture) {
469            (Some(prev), Some(new)) => prev.target.widget_id() == new.target.widget_id() && prev.target != new.target,
470            _ => false,
471        }
472    }
473
474    /// If the same widget has pointer capture, but the capture mode changed.
475    pub fn is_mode_change(&self) -> bool {
476        match (&self.prev_capture, &self.new_capture) {
477            (Some(prev), Some(new)) => prev.target.widget_id() == new.target.widget_id() && prev.mode != new.mode,
478            _ => false,
479        }
480    }
481
482    /// If the `widget_id` lost pointer capture with this update.
483    pub fn is_lost(&self, widget_id: WidgetId) -> bool {
484        match (&self.prev_capture, &self.new_capture) {
485            (None, _) => false,
486            (Some(p), None) => p.target.widget_id() == widget_id,
487            (Some(prev), Some(new)) => prev.target.widget_id() == widget_id && new.target.widget_id() != widget_id,
488        }
489    }
490
491    /// If the `widget_id` got pointer capture with this update.
492    pub fn is_got(&self, widget_id: WidgetId) -> bool {
493        match (&self.prev_capture, &self.new_capture) {
494            (_, None) => false,
495            (None, Some(p)) => p.target.widget_id() == widget_id,
496            (Some(prev), Some(new)) => prev.target.widget_id() != widget_id && new.target.widget_id() == widget_id,
497        }
498    }
499}