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