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