Skip to main content

zng_ext_input/
pointer_capture.rs

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