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 self.target.window_id() == wgt.0
178                    && let Some(wgt) = WINDOWS.widget_tree(wgt.0).and_then(|t| t.get(wgt.1))
179                {
180                    for wgt in wgt.self_and_ancestors() {
181                        if wgt.id() == self.target.widget_id() {
182                            return true;
183                        }
184                    }
185                }
186                false
187            }
188        }
189    }
190}
191
192app_local! {
193    static POINTER_CAPTURE_SV: PointerCaptureService = {
194        hooks();
195        PointerCaptureService {
196            capture_value: None,
197            capture: var(None),
198
199            mouse_down: Default::default(),
200            touch_down: Default::default(),
201        }
202    };
203}
204
205struct PointerCaptureService {
206    capture_value: Option<CaptureInfo>,
207    capture: Var<Option<CaptureInfo>>,
208
209    mouse_down: HashSet<(WindowId, InputDeviceId, MouseButton)>,
210    touch_down: HashSet<(WindowId, InputDeviceId, TouchId)>,
211}
212
213event! {
214    /// Mouse and touch capture changed event.
215    pub static POINTER_CAPTURE_EVENT: PointerCaptureArgs {
216        let _ = POINTER_CAPTURE_SV.read();
217    };
218}
219
220event_args! {
221    /// [`POINTER_CAPTURE_EVENT`] arguments.
222    pub struct PointerCaptureArgs {
223        /// Previous mouse and touch capture target and mode.
224        pub prev_capture: Option<CaptureInfo>,
225        /// new mouse and capture target and mode.
226        pub new_capture: Option<CaptureInfo>,
227
228        ..
229
230        /// If is in [`prev_capture`] or [`new_capture`] paths start with the current path.
231        ///
232        /// [`prev_capture`]: Self::prev_capture
233        /// [`new_capture`]: Self::new_capture
234        fn is_in_target(&self, id: WidgetId) -> bool {
235            if let Some(p) = &self.prev_capture
236                && p.target.contains(id)
237            {
238                return true;
239            }
240            if let Some(p) = &self.new_capture
241                && p.target.contains(id)
242            {
243                return true;
244            }
245            false
246        }
247    }
248}
249
250impl PointerCaptureArgs {
251    /// If the same widget has pointer capture, but the widget path changed.
252    pub fn is_widget_move(&self) -> bool {
253        match (&self.prev_capture, &self.new_capture) {
254            (Some(prev), Some(new)) => prev.target.widget_id() == new.target.widget_id() && prev.target != new.target,
255            _ => false,
256        }
257    }
258
259    /// If the same widget has pointer capture, but the capture mode changed.
260    pub fn is_mode_change(&self) -> bool {
261        match (&self.prev_capture, &self.new_capture) {
262            (Some(prev), Some(new)) => prev.target.widget_id() == new.target.widget_id() && prev.mode != new.mode,
263            _ => false,
264        }
265    }
266
267    /// If the `widget_id` lost pointer capture with this update.
268    pub fn is_lost(&self, widget_id: WidgetId) -> bool {
269        match (&self.prev_capture, &self.new_capture) {
270            (None, _) => false,
271            (Some(p), None) => p.target.widget_id() == widget_id,
272            (Some(prev), Some(new)) => prev.target.widget_id() == widget_id && new.target.widget_id() != widget_id,
273        }
274    }
275
276    /// If the `widget_id` got pointer capture with this update.
277    pub fn is_got(&self, widget_id: WidgetId) -> bool {
278        match (&self.prev_capture, &self.new_capture) {
279            (_, None) => false,
280            (None, Some(p)) => p.target.widget_id() == widget_id,
281            (Some(prev), Some(new)) => prev.target.widget_id() != widget_id && new.target.widget_id() == widget_id,
282        }
283    }
284}
285
286fn hooks() {
287    WIDGET_TREE_CHANGED_EVENT
288        .hook(|args| {
289            let mut s = POINTER_CAPTURE_SV.write();
290            if let Some(c) = &s.capture_value
291                && c.target.window_id() == args.tree.window_id()
292            {
293                s.continue_capture(&args.tree);
294            }
295            true
296        })
297        .perm();
298
299    RAW_MOUSE_INPUT_EVENT
300        .hook(|args| {
301            let mut s = POINTER_CAPTURE_SV.write();
302            match args.state {
303                ButtonState::Pressed => {
304                    if s.mouse_down.insert((args.window_id, args.device_id, args.button))
305                        && s.mouse_down.len() == 1
306                        && s.touch_down.is_empty()
307                    {
308                        s.on_first_down(args.window_id);
309                    }
310                }
311                ButtonState::Released => {
312                    if s.mouse_down.remove(&(args.window_id, args.device_id, args.button))
313                        && s.mouse_down.is_empty()
314                        && s.touch_down.is_empty()
315                    {
316                        s.on_last_up();
317                    }
318                }
319            }
320            true
321        })
322        .perm();
323
324    RAW_TOUCH_EVENT
325        .hook(|args| {
326            let mut s = POINTER_CAPTURE_SV.write();
327            for touch in &args.touches {
328                match touch.phase {
329                    TouchPhase::Start => {
330                        if s.touch_down.insert((args.window_id, args.device_id, touch.touch))
331                            && s.touch_down.len() == 1
332                            && s.mouse_down.is_empty()
333                        {
334                            s.on_first_down(args.window_id);
335                        }
336                    }
337                    TouchPhase::End | TouchPhase::Cancel => {
338                        if s.touch_down.remove(&(args.window_id, args.device_id, touch.touch))
339                            && s.touch_down.is_empty()
340                            && s.mouse_down.is_empty()
341                        {
342                            s.on_last_up();
343                        }
344                    }
345                    TouchPhase::Move => {}
346                }
347            }
348            true
349        })
350        .perm();
351
352    RAW_WINDOW_CLOSE_EVENT
353        .hook(|args| {
354            POINTER_CAPTURE_SV.write().remove_window(args.window_id);
355            true
356        })
357        .perm();
358
359    fn nest_parent(id: WindowId) -> Option<WindowId> {
360        WINDOWS
361            .vars(id)
362            .and_then(|v| if v.nest_parent().get().is_some() { v.parent().get() } else { None })
363    }
364
365    RAW_WINDOW_FOCUS_EVENT
366        .hook(|args| {
367            let actual_prev = args.prev_focus.map(|id| nest_parent(id).unwrap_or(id));
368            let actual_new = args.new_focus.map(|id| nest_parent(id).unwrap_or(id));
369
370            if actual_prev == actual_new {
371                // can happen when focus moves from parent to nested, or malformed event
372                return true;
373            }
374
375            if let Some(w) = actual_prev {
376                POINTER_CAPTURE_SV.write().remove_window(w);
377            }
378            true
379        })
380        .perm();
381
382    VIEW_PROCESS_INITED_EVENT
383        .hook(|args| {
384            if args.is_respawn {
385                let mut s = POINTER_CAPTURE_SV.write();
386
387                if !s.mouse_down.is_empty() || !s.touch_down.is_empty() {
388                    s.mouse_down.clear();
389                    s.touch_down.clear();
390                    s.on_last_up();
391                }
392            }
393            true
394        })
395        .perm();
396}
397impl PointerCaptureService {
398    fn remove_window(&mut self, window_id: WindowId) {
399        if !self.mouse_down.is_empty() || !self.touch_down.is_empty() {
400            self.mouse_down.retain(|(w, _, _)| *w != window_id);
401            self.touch_down.retain(|(w, _, _)| *w != window_id);
402
403            if self.mouse_down.is_empty() && self.touch_down.is_empty() {
404                self.on_last_up();
405            }
406        }
407    }
408
409    fn on_first_down(&mut self, window_id: WindowId) {
410        if let Some(info) = WINDOWS.widget_tree(window_id) {
411            // default capture
412            self.set_capture(info.root().interaction_path(), CaptureMode::Window);
413        }
414    }
415
416    fn on_last_up(&mut self) {
417        self.unset_capture();
418    }
419
420    fn continue_capture(&mut self, info: &WidgetInfoTree) {
421        let current = self.capture_value.as_ref().unwrap();
422
423        if let Some(widget) = info.get(current.target.widget_id()) {
424            if let Some(new_path) = widget.new_interaction_path(&InteractionPath::from_enabled(current.target.clone())) {
425                // widget moved inside window tree.
426                let mode = current.mode;
427                self.set_capture(new_path, mode);
428            }
429        } else {
430            // widget not found. Returns to default capture.
431            self.set_capture(info.root().interaction_path(), CaptureMode::Window);
432        }
433    }
434
435    fn set_capture(&mut self, target: InteractionPath, mode: CaptureMode) {
436        let new = target.enabled().map(|target| CaptureInfo { target, mode });
437        if new.is_none() {
438            self.unset_capture();
439            return;
440        }
441        if new != self.capture_value {
442            let prev = self.capture_value.take();
443            self.capture_value.clone_from(&new);
444            self.capture.set(new.clone());
445            POINTER_CAPTURE_EVENT.notify(PointerCaptureArgs::now(prev, new));
446        }
447    }
448
449    fn unset_capture(&mut self) {
450        if self.capture_value.is_some() {
451            let prev = self.capture_value.take();
452            self.capture_value = None;
453            self.capture.set(None);
454            POINTER_CAPTURE_EVENT.notify(PointerCaptureArgs::now(prev, None));
455        }
456    }
457}