1 | // Copyright © SixtyFPS GmbH <info@slint.dev> |
2 | // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial |
3 | |
4 | /*! Module handling mouse events |
5 | */ |
6 | #![warn (missing_docs)] |
7 | |
8 | use crate::item_tree::ItemTreeRc; |
9 | use crate::item_tree::{ItemRc, ItemWeak, VisitChildrenResult}; |
10 | pub use crate::items::PointerEventButton; |
11 | use crate::items::{ItemRef, TextCursorDirection}; |
12 | pub use crate::items::{KeyEvent, KeyboardModifiers}; |
13 | use crate::lengths::{LogicalPoint, LogicalVector}; |
14 | use crate::timers::Timer; |
15 | use crate::window::{WindowAdapter, WindowInner}; |
16 | use crate::{Coord, Property, SharedString}; |
17 | use alloc::rc::Rc; |
18 | #[cfg (not(feature = "std" ))] |
19 | use alloc::vec::Vec; |
20 | use const_field_offset::FieldOffsets; |
21 | use core::cell::Cell; |
22 | use core::pin::Pin; |
23 | use core::time::Duration; |
24 | |
25 | /// A mouse or touch event |
26 | /// |
27 | /// The only difference with [`crate::platform::WindowEvent`] us that it uses untyped `Point` |
28 | /// TODO: merge with platform::WindowEvent |
29 | #[repr (C)] |
30 | #[derive (Debug, Clone, Copy, PartialEq)] |
31 | #[allow (missing_docs)] |
32 | pub enum MouseEvent { |
33 | /// The mouse or finger was pressed |
34 | /// `position` is the position of the mouse when the event happens. |
35 | /// `button` describes the button that is pressed when the event happens. |
36 | /// `click_count` represents the current number of clicks. |
37 | Pressed { position: LogicalPoint, button: PointerEventButton, click_count: u8 }, |
38 | /// The mouse or finger was released |
39 | /// `position` is the position of the mouse when the event happens. |
40 | /// `button` describes the button that is pressed when the event happens. |
41 | /// `click_count` represents the current number of clicks. |
42 | Released { position: LogicalPoint, button: PointerEventButton, click_count: u8 }, |
43 | /// The position of the pointer has changed |
44 | Moved { position: LogicalPoint }, |
45 | /// Wheel was operated. |
46 | /// `pos` is the position of the mouse when the event happens. |
47 | /// `delta_x` is the amount of pixels to scroll in horizontal direction, |
48 | /// `delta_y` is the amount of pixels to scroll in vertical direction. |
49 | Wheel { position: LogicalPoint, delta_x: Coord, delta_y: Coord }, |
50 | /// The mouse exited the item or component |
51 | Exit, |
52 | } |
53 | |
54 | impl MouseEvent { |
55 | /// The position of the cursor for this event, if any |
56 | pub fn position(&self) -> Option<LogicalPoint> { |
57 | match self { |
58 | MouseEvent::Pressed { position, .. } => Some(*position), |
59 | MouseEvent::Released { position, .. } => Some(*position), |
60 | MouseEvent::Moved { position } => Some(*position), |
61 | MouseEvent::Wheel { position, .. } => Some(*position), |
62 | MouseEvent::Exit => None, |
63 | } |
64 | } |
65 | |
66 | /// Translate the position by the given value |
67 | pub fn translate(&mut self, vec: LogicalVector) { |
68 | let pos = match self { |
69 | MouseEvent::Pressed { position, .. } => Some(position), |
70 | MouseEvent::Released { position, .. } => Some(position), |
71 | MouseEvent::Moved { position } => Some(position), |
72 | MouseEvent::Wheel { position, .. } => Some(position), |
73 | MouseEvent::Exit => None, |
74 | }; |
75 | if let Some(pos) = pos { |
76 | *pos += vec; |
77 | } |
78 | } |
79 | |
80 | /// Set the click count of the pressed or released event |
81 | fn set_click_count(&mut self, count: u8) { |
82 | match self { |
83 | MouseEvent::Pressed { click_count, .. } | MouseEvent::Released { click_count, .. } => { |
84 | *click_count = count |
85 | } |
86 | _ => (), |
87 | } |
88 | } |
89 | } |
90 | |
91 | /// This value is returned by the `input_event` function of an Item |
92 | /// to notify the run-time about how the event was handled and |
93 | /// what the next steps are. |
94 | /// See [`crate::items::ItemVTable::input_event`]. |
95 | #[repr (u8)] |
96 | #[derive (Debug, Copy, Clone, Eq, PartialEq, Default)] |
97 | pub enum InputEventResult { |
98 | /// The event was accepted. This may result in additional events, for example |
99 | /// accepting a mouse move will result in a MouseExit event later. |
100 | EventAccepted, |
101 | /// The event was ignored. |
102 | #[default] |
103 | EventIgnored, |
104 | /// All further mouse event need to be sent to this item or component |
105 | GrabMouse, |
106 | } |
107 | |
108 | /// This value is returned by the `input_event_filter_before_children` function, which |
109 | /// can specify how to further process the event. |
110 | /// See [`crate::items::ItemVTable::input_event_filter_before_children`]. |
111 | #[repr (C)] |
112 | #[derive (Debug, Copy, Clone, PartialEq, Default)] |
113 | pub enum InputEventFilterResult { |
114 | /// The event is going to be forwarded to children, then the [`crate::items::ItemVTable::input_event`] |
115 | /// function is called |
116 | #[default] |
117 | ForwardEvent, |
118 | /// The event will be forwarded to the children, but the [`crate::items::ItemVTable::input_event`] is not |
119 | /// going to be called for this item |
120 | ForwardAndIgnore, |
121 | /// Just like `ForwardEvent`, but even in the case the children grabs the mouse, this function |
122 | /// will still be called for further event |
123 | ForwardAndInterceptGrab, |
124 | /// The event will not be forwarded to children, if a children already had the grab, the |
125 | /// grab will be cancelled with a [`MouseEvent::Exit`] event |
126 | Intercept, |
127 | /// The event will be forwarding to the children with a delay (in milliseconds), unless it is |
128 | /// being intercepted. |
129 | /// This is what happens when the flickable wants to delay the event. |
130 | /// This should only be used for Press event, and the event will be sent after the delay, or |
131 | /// if a release event is seen before that delay |
132 | //(Can't use core::time::Duration because it is not repr(c)) |
133 | DelayForwarding(u64), |
134 | } |
135 | |
136 | /// This module contains the constant character code used to represent the keys. |
137 | #[allow (missing_docs, non_upper_case_globals)] |
138 | pub mod key_codes { |
139 | macro_rules! declare_consts_for_special_keys { |
140 | ($($char:literal # $name:ident # $($_qt:ident)|* # $($_winit:ident $(($_pos:ident))?)|* # $($_xkb:ident)|*;)*) => { |
141 | $(pub const $name : char = $char;)* |
142 | |
143 | #[allow(missing_docs)] |
144 | #[derive(Debug, Copy, Clone, PartialEq)] |
145 | #[non_exhaustive] |
146 | /// The `Key` enum is used to map a specific key by name e.g. `Key::Control` to an |
147 | /// internal used unicode representation. The enum is convertible to [`std::char`] and [`slint::SharedString`](`crate::SharedString`). |
148 | /// Use this with [`slint::platform::WindowEvent`](`crate::platform::WindowEvent`) to supply key events to Slint's platform abstraction. |
149 | /// |
150 | /// # Example |
151 | /// |
152 | /// Send an tab key press event to a window |
153 | /// |
154 | /// ``` |
155 | /// use slint::platform::{WindowEvent, Key}; |
156 | /// fn send_tab_pressed(window: &slint::Window) { |
157 | /// window.dispatch_event(WindowEvent::KeyPressed { text: Key::Tab.into() }); |
158 | /// } |
159 | /// ``` |
160 | pub enum Key { |
161 | $($name,)* |
162 | } |
163 | |
164 | impl From<Key> for char { |
165 | fn from(k: Key) -> Self { |
166 | match k { |
167 | $(Key::$name => $name,)* |
168 | } |
169 | } |
170 | } |
171 | |
172 | impl From<Key> for crate::SharedString { |
173 | fn from(k: Key) -> Self { |
174 | char::from(k).into() |
175 | } |
176 | } |
177 | }; |
178 | } |
179 | |
180 | i_slint_common::for_each_special_keys!(declare_consts_for_special_keys); |
181 | } |
182 | |
183 | /// Internal struct to maintain the pressed/released state of the keys that |
184 | /// map to keyboard modifiers. |
185 | #[derive (Clone, Copy, Default, Debug)] |
186 | pub(crate) struct InternalKeyboardModifierState { |
187 | left_alt: bool, |
188 | right_alt: bool, |
189 | altgr: bool, |
190 | left_control: bool, |
191 | right_control: bool, |
192 | left_meta: bool, |
193 | right_meta: bool, |
194 | left_shift: bool, |
195 | right_shift: bool, |
196 | } |
197 | |
198 | impl InternalKeyboardModifierState { |
199 | /// Updates a flag of the modifiers if the key of the given text is pressed. |
200 | /// Returns an updated modifier if detected; None otherwise; |
201 | pub(crate) fn state_update(mut self, pressed: bool, text: &SharedString) -> Option<Self> { |
202 | if let Some(key_code) = text.chars().next() { |
203 | match key_code { |
204 | key_codes::Alt => self.left_alt = pressed, |
205 | key_codes::AltGr => self.altgr = pressed, |
206 | key_codes::Control => self.left_control = pressed, |
207 | key_codes::ControlR => self.right_control = pressed, |
208 | key_codes::Shift => self.left_shift = pressed, |
209 | key_codes::ShiftR => self.right_shift = pressed, |
210 | key_codes::Meta => self.left_meta = pressed, |
211 | key_codes::MetaR => self.right_meta = pressed, |
212 | _ => return None, |
213 | }; |
214 | |
215 | // Encoded keyboard modifiers must appear as individual key events. This could |
216 | // be relaxed by implementing a string split, but right now WindowEvent::KeyPressed |
217 | // holds only a single char. |
218 | debug_assert_eq!(key_code.len_utf8(), text.len()); |
219 | } |
220 | |
221 | // Special cases: |
222 | #[cfg (target_os = "windows" )] |
223 | { |
224 | if self.altgr { |
225 | // Windows sends Ctrl followed by AltGr on AltGr. Disable the Ctrl again! |
226 | self.left_control = false; |
227 | self.right_control = false; |
228 | } else if self.control() && self.alt() { |
229 | // Windows treats Ctrl-Alt as AltGr |
230 | self.left_control = false; |
231 | self.right_control = false; |
232 | self.left_alt = false; |
233 | self.right_alt = false; |
234 | } |
235 | } |
236 | |
237 | Some(self) |
238 | } |
239 | |
240 | pub fn shift(&self) -> bool { |
241 | self.right_shift || self.left_shift |
242 | } |
243 | pub fn alt(&self) -> bool { |
244 | self.right_alt || self.left_alt |
245 | } |
246 | pub fn meta(&self) -> bool { |
247 | self.right_meta || self.left_meta |
248 | } |
249 | pub fn control(&self) -> bool { |
250 | self.right_control || self.left_control |
251 | } |
252 | } |
253 | |
254 | impl From<InternalKeyboardModifierState> for KeyboardModifiers { |
255 | fn from(internal_state: InternalKeyboardModifierState) -> Self { |
256 | Self { |
257 | alt: internal_state.alt(), |
258 | control: internal_state.control(), |
259 | meta: internal_state.meta(), |
260 | shift: internal_state.shift(), |
261 | } |
262 | } |
263 | } |
264 | |
265 | /// This enum defines the different kinds of key events that can happen. |
266 | #[derive (Copy, Clone, Debug, PartialEq, Eq, Default)] |
267 | #[repr (u8)] |
268 | pub enum KeyEventType { |
269 | /// A key on a keyboard was pressed. |
270 | #[default] |
271 | KeyPressed = 0, |
272 | /// A key on a keyboard was released. |
273 | KeyReleased = 1, |
274 | /// The input method updates the currently composed text. The KeyEvent's text field is the pre-edit text and |
275 | /// composition_selection specifies the placement of the cursor within the pre-edit text. |
276 | UpdateComposition = 2, |
277 | /// The input method replaces the currently composed text with the final result of the composition. |
278 | CommitComposition = 3, |
279 | } |
280 | |
281 | impl KeyEvent { |
282 | /// If a shortcut was pressed, this function returns `Some(StandardShortcut)`. |
283 | /// Otherwise it returns None. |
284 | pub fn shortcut(&self) -> Option<StandardShortcut> { |
285 | if self.modifiers.control && !self.modifiers.shift { |
286 | match self.text.as_str() { |
287 | #[cfg (not(target_arch = "wasm32" ))] |
288 | "c" => Some(StandardShortcut::Copy), |
289 | #[cfg (not(target_arch = "wasm32" ))] |
290 | "x" => Some(StandardShortcut::Cut), |
291 | #[cfg (not(target_arch = "wasm32" ))] |
292 | "v" => Some(StandardShortcut::Paste), |
293 | "a" => Some(StandardShortcut::SelectAll), |
294 | "f" => Some(StandardShortcut::Find), |
295 | "s" => Some(StandardShortcut::Save), |
296 | "p" => Some(StandardShortcut::Print), |
297 | "z" => Some(StandardShortcut::Undo), |
298 | #[cfg (target_os = "windows" )] |
299 | "y" => Some(StandardShortcut::Redo), |
300 | "r" => Some(StandardShortcut::Refresh), |
301 | _ => None, |
302 | } |
303 | } else if self.modifiers.control && self.modifiers.shift { |
304 | match self.text.as_str() { |
305 | #[cfg (not(target_os = "windows" ))] |
306 | "z" => Some(StandardShortcut::Redo), |
307 | _ => None, |
308 | } |
309 | } else { |
310 | None |
311 | } |
312 | } |
313 | |
314 | /// If a shortcut concerning text editing was pressed, this function |
315 | /// returns `Some(TextShortcut)`. Otherwise it returns None. |
316 | pub fn text_shortcut(&self) -> Option<TextShortcut> { |
317 | let keycode = self.text.chars().next()?; |
318 | |
319 | let move_mod = if cfg!(target_os = "macos" ) { |
320 | self.modifiers.alt && !self.modifiers.control && !self.modifiers.meta |
321 | } else { |
322 | self.modifiers.control && !self.modifiers.alt && !self.modifiers.meta |
323 | }; |
324 | |
325 | if move_mod { |
326 | match keycode { |
327 | key_codes::LeftArrow => { |
328 | return Some(TextShortcut::Move(TextCursorDirection::BackwardByWord)) |
329 | } |
330 | key_codes::RightArrow => { |
331 | return Some(TextShortcut::Move(TextCursorDirection::ForwardByWord)) |
332 | } |
333 | key_codes::UpArrow => { |
334 | return Some(TextShortcut::Move(TextCursorDirection::StartOfParagraph)) |
335 | } |
336 | key_codes::DownArrow => { |
337 | return Some(TextShortcut::Move(TextCursorDirection::EndOfParagraph)) |
338 | } |
339 | key_codes::Backspace => { |
340 | return Some(TextShortcut::DeleteWordBackward); |
341 | } |
342 | key_codes::Delete => { |
343 | return Some(TextShortcut::DeleteWordForward); |
344 | } |
345 | _ => (), |
346 | }; |
347 | } |
348 | |
349 | #[cfg (not(target_os = "macos" ))] |
350 | { |
351 | if self.modifiers.control && !self.modifiers.alt && !self.modifiers.meta { |
352 | match keycode { |
353 | key_codes::Home => { |
354 | return Some(TextShortcut::Move(TextCursorDirection::StartOfText)) |
355 | } |
356 | key_codes::End => { |
357 | return Some(TextShortcut::Move(TextCursorDirection::EndOfText)) |
358 | } |
359 | _ => (), |
360 | }; |
361 | } |
362 | } |
363 | |
364 | #[cfg (target_os = "macos" )] |
365 | { |
366 | if self.modifiers.control { |
367 | match keycode { |
368 | key_codes::LeftArrow => { |
369 | return Some(TextShortcut::Move(TextCursorDirection::StartOfLine)) |
370 | } |
371 | key_codes::RightArrow => { |
372 | return Some(TextShortcut::Move(TextCursorDirection::EndOfLine)) |
373 | } |
374 | key_codes::UpArrow => { |
375 | return Some(TextShortcut::Move(TextCursorDirection::StartOfText)) |
376 | } |
377 | key_codes::DownArrow => { |
378 | return Some(TextShortcut::Move(TextCursorDirection::EndOfText)) |
379 | } |
380 | _ => (), |
381 | }; |
382 | } |
383 | } |
384 | |
385 | if let Ok(direction) = TextCursorDirection::try_from(keycode) { |
386 | Some(TextShortcut::Move(direction)) |
387 | } else { |
388 | match keycode { |
389 | key_codes::Backspace => Some(TextShortcut::DeleteBackward), |
390 | key_codes::Delete => Some(TextShortcut::DeleteForward), |
391 | _ => None, |
392 | } |
393 | } |
394 | } |
395 | } |
396 | |
397 | /// Represents a non context specific shortcut. |
398 | pub enum StandardShortcut { |
399 | /// Copy Something |
400 | Copy, |
401 | /// Cut Something |
402 | Cut, |
403 | /// Paste Something |
404 | Paste, |
405 | /// Select All |
406 | SelectAll, |
407 | /// Find/Search Something |
408 | Find, |
409 | /// Save Something |
410 | Save, |
411 | /// Print Something |
412 | Print, |
413 | /// Undo the last action |
414 | Undo, |
415 | /// Redo the last undone action |
416 | Redo, |
417 | /// Refresh |
418 | Refresh, |
419 | } |
420 | |
421 | /// Shortcuts that are used when editing text |
422 | pub enum TextShortcut { |
423 | /// Move the cursor |
424 | Move(TextCursorDirection), |
425 | /// Delete the Character to the right of the cursor |
426 | DeleteForward, |
427 | /// Delete the Character to the left of the cursor (aka Backspace). |
428 | DeleteBackward, |
429 | /// Delete the word to the right of the cursor |
430 | DeleteWordForward, |
431 | /// Delete the word to the left of the cursor (aka Ctrl + Backspace). |
432 | DeleteWordBackward, |
433 | } |
434 | |
435 | /// Represents how an item's key_event handler dealt with a key event. |
436 | /// An accepted event results in no further event propagation. |
437 | #[repr (u8)] |
438 | #[derive (Debug, Clone, Copy, PartialEq)] |
439 | pub enum KeyEventResult { |
440 | /// The event was handled. |
441 | EventAccepted, |
442 | /// The event was not handled and should be sent to other items. |
443 | EventIgnored, |
444 | } |
445 | |
446 | /// Represents how an item's focus_event handler dealt with a focus event. |
447 | /// An accepted event results in no further event propagation. |
448 | #[repr (u8)] |
449 | #[derive (Debug, Clone, Copy, PartialEq)] |
450 | pub enum FocusEventResult { |
451 | /// The event was handled. |
452 | FocusAccepted, |
453 | /// The event was not handled and should be sent to other items. |
454 | FocusIgnored, |
455 | } |
456 | |
457 | /// This event is sent to a component and items when they receive or loose |
458 | /// the keyboard focus. |
459 | #[derive (Debug, Clone, Copy, PartialEq)] |
460 | #[repr (u8)] |
461 | pub enum FocusEvent { |
462 | /// This event is sent when an item receives the focus. |
463 | FocusIn, |
464 | /// This event is sent when an item looses the focus. |
465 | FocusOut, |
466 | /// This event is sent when the window receives the keyboard focus. |
467 | WindowReceivedFocus, |
468 | /// This event is sent when the window looses the keyboard focus. |
469 | WindowLostFocus, |
470 | } |
471 | |
472 | /// This state is used to count the clicks separated by [`crate::platform::Platform::click_interval`] |
473 | #[derive (Default)] |
474 | pub struct ClickState { |
475 | click_count_time_stamp: Cell<Option<crate::animations::Instant>>, |
476 | click_count: Cell<u8>, |
477 | click_position: Cell<LogicalPoint>, |
478 | click_button: Cell<PointerEventButton>, |
479 | } |
480 | |
481 | impl ClickState { |
482 | /// Resets the timer and count. |
483 | fn restart(&self, position: LogicalPoint, button: PointerEventButton) { |
484 | self.click_count.set(0); |
485 | self.click_count_time_stamp.set(Some(crate::animations::Instant::now())); |
486 | self.click_position.set(position); |
487 | self.click_button.set(button); |
488 | } |
489 | |
490 | /// Reset to an invalid state |
491 | pub fn reset(&self) { |
492 | self.click_count.set(0); |
493 | self.click_count_time_stamp.replace(None); |
494 | } |
495 | |
496 | /// Check if the click is repeated. |
497 | pub fn check_repeat(&self, mouse_event: MouseEvent, click_interval: Duration) -> MouseEvent { |
498 | match mouse_event { |
499 | MouseEvent::Pressed { position, button, .. } => { |
500 | let instant_now = crate::animations::Instant::now(); |
501 | |
502 | if let Some(click_count_time_stamp) = self.click_count_time_stamp.get() { |
503 | if instant_now - click_count_time_stamp < click_interval |
504 | && button == self.click_button.get() |
505 | && (position - self.click_position.get()).square_length() < 100 as _ |
506 | { |
507 | self.click_count.set(self.click_count.get() + 1); |
508 | self.click_count_time_stamp.set(Some(instant_now)); |
509 | } else { |
510 | self.restart(position, button); |
511 | } |
512 | } else { |
513 | self.restart(position, button); |
514 | } |
515 | |
516 | return MouseEvent::Pressed { |
517 | position, |
518 | button, |
519 | click_count: self.click_count.get(), |
520 | }; |
521 | } |
522 | MouseEvent::Released { position, button, .. } => { |
523 | return MouseEvent::Released { |
524 | position, |
525 | button, |
526 | click_count: self.click_count.get(), |
527 | } |
528 | } |
529 | _ => {} |
530 | }; |
531 | |
532 | mouse_event |
533 | } |
534 | } |
535 | |
536 | /// The state which a window should hold for the mouse input |
537 | #[derive (Default)] |
538 | pub struct MouseInputState { |
539 | /// The stack of item which contain the mouse cursor (or grab), |
540 | /// along with the last result from the input function |
541 | item_stack: Vec<(ItemWeak, InputEventFilterResult)>, |
542 | /// Offset to apply to the first item of the stack (used if there is a popup) |
543 | pub(crate) offset: LogicalPoint, |
544 | /// true if the top item of the stack has the mouse grab |
545 | grabbed: bool, |
546 | delayed: Option<(crate::timers::Timer, MouseEvent)>, |
547 | delayed_exit_items: Vec<ItemWeak>, |
548 | } |
549 | |
550 | impl MouseInputState { |
551 | /// Return the item in the top of the stack |
552 | pub fn top_item(&self) -> Option<ItemRc> { |
553 | self.item_stack.last().and_then(|x: &(ItemWeak, InputEventFilterResult)| x.0.upgrade()) |
554 | } |
555 | } |
556 | |
557 | /// Try to handle the mouse grabber. Return None if the event has been handled, otherwise |
558 | /// return the event that must be handled |
559 | pub(crate) fn handle_mouse_grab( |
560 | mouse_event: MouseEvent, |
561 | window_adapter: &Rc<dyn WindowAdapter>, |
562 | mouse_input_state: &mut MouseInputState, |
563 | ) -> Option<MouseEvent> { |
564 | if !mouse_input_state.grabbed || mouse_input_state.item_stack.is_empty() { |
565 | return Some(mouse_event); |
566 | }; |
567 | |
568 | let mut event = mouse_event; |
569 | let mut intercept = false; |
570 | let mut invalid = false; |
571 | |
572 | event.translate(-mouse_input_state.offset.to_vector()); |
573 | |
574 | mouse_input_state.item_stack.retain(|it| { |
575 | if invalid { |
576 | return false; |
577 | } |
578 | let item = if let Some(item) = it.0.upgrade() { |
579 | item |
580 | } else { |
581 | invalid = true; |
582 | return false; |
583 | }; |
584 | if intercept { |
585 | item.borrow().as_ref().input_event(MouseEvent::Exit, window_adapter, &item); |
586 | return false; |
587 | } |
588 | let g = item.geometry(); |
589 | event.translate(-g.origin.to_vector()); |
590 | |
591 | let interested = matches!( |
592 | it.1, |
593 | InputEventFilterResult::ForwardAndInterceptGrab |
594 | | InputEventFilterResult::DelayForwarding(_) |
595 | ); |
596 | |
597 | if interested |
598 | && item.borrow().as_ref().input_event_filter_before_children( |
599 | event, |
600 | window_adapter, |
601 | &item, |
602 | ) == InputEventFilterResult::Intercept |
603 | { |
604 | intercept = true; |
605 | } |
606 | true |
607 | }); |
608 | if invalid { |
609 | return Some(mouse_event); |
610 | } |
611 | |
612 | let grabber = mouse_input_state.top_item().unwrap(); |
613 | let input_result = grabber.borrow().as_ref().input_event(event, window_adapter, &grabber); |
614 | if input_result != InputEventResult::GrabMouse { |
615 | mouse_input_state.grabbed = false; |
616 | // Return a move event so that the new position can be registered properly |
617 | Some( |
618 | mouse_event |
619 | .position() |
620 | .map_or(MouseEvent::Exit, |position| MouseEvent::Moved { position }), |
621 | ) |
622 | } else { |
623 | None |
624 | } |
625 | } |
626 | |
627 | pub(crate) fn send_exit_events( |
628 | old_input_state: &MouseInputState, |
629 | new_input_state: &mut MouseInputState, |
630 | mut pos: Option<LogicalPoint>, |
631 | window_adapter: &Rc<dyn WindowAdapter>, |
632 | ) { |
633 | for it in core::mem::take(&mut new_input_state.delayed_exit_items) { |
634 | let Some(item) = it.upgrade() else { continue }; |
635 | item.borrow().as_ref().input_event(MouseEvent::Exit, window_adapter, &item); |
636 | } |
637 | |
638 | let mut clipped = false; |
639 | for (idx, it) in old_input_state.item_stack.iter().enumerate() { |
640 | let Some(item) = it.0.upgrade() else { break }; |
641 | let g = item.geometry(); |
642 | let contains = pos.map_or(false, |p| g.contains(p)); |
643 | if let Some(p) = pos.as_mut() { |
644 | *p -= g.origin.to_vector(); |
645 | } |
646 | if !contains || clipped { |
647 | if crate::item_rendering::is_clipping_item(item.borrow()) { |
648 | clipped = true; |
649 | } |
650 | item.borrow().as_ref().input_event(MouseEvent::Exit, window_adapter, &item); |
651 | } else if new_input_state.item_stack.get(idx).map_or(true, |(x, _)| *x != it.0) { |
652 | // The item is still under the mouse, but no longer in the item stack. We should also sent the exit event, unless we delay it |
653 | if new_input_state.delayed.is_some() { |
654 | new_input_state.delayed_exit_items.push(it.0.clone()); |
655 | } else { |
656 | item.borrow().as_ref().input_event(MouseEvent::Exit, window_adapter, &item); |
657 | } |
658 | } |
659 | } |
660 | } |
661 | |
662 | /// Process the `mouse_event` on the `component`, the `mouse_grabber_stack` is the previous stack |
663 | /// of mouse grabber. |
664 | /// Returns a new mouse grabber stack. |
665 | pub fn process_mouse_input( |
666 | component: ItemTreeRc, |
667 | mouse_event: MouseEvent, |
668 | window_adapter: &Rc<dyn WindowAdapter>, |
669 | mouse_input_state: MouseInputState, |
670 | ) -> MouseInputState { |
671 | let mut result = MouseInputState::default(); |
672 | let root = ItemRc::new(component.clone(), 0); |
673 | let r = send_mouse_event_to_item( |
674 | mouse_event, |
675 | root, |
676 | window_adapter, |
677 | &mut result, |
678 | mouse_input_state.top_item().as_ref(), |
679 | false, |
680 | ); |
681 | if mouse_input_state.delayed.is_some() |
682 | && (!r.has_aborted() |
683 | || Option::zip(result.item_stack.last(), mouse_input_state.item_stack.last()) |
684 | .map_or(true, |(a, b)| a.0 != b.0)) |
685 | { |
686 | // Keep the delayed event |
687 | return mouse_input_state; |
688 | } |
689 | send_exit_events(&mouse_input_state, &mut result, mouse_event.position(), window_adapter); |
690 | |
691 | if let MouseEvent::Wheel { position, .. } = mouse_event { |
692 | if r.has_aborted() { |
693 | // An accepted wheel event might have moved things. Send a move event at the position to reset the has-hover |
694 | return process_mouse_input( |
695 | component, |
696 | MouseEvent::Moved { position }, |
697 | window_adapter, |
698 | result, |
699 | ); |
700 | } |
701 | } |
702 | |
703 | result |
704 | } |
705 | |
706 | pub(crate) fn process_delayed_event( |
707 | window_adapter: &Rc<dyn WindowAdapter>, |
708 | mut mouse_input_state: MouseInputState, |
709 | ) -> MouseInputState { |
710 | // the take bellow will also destroy the Timer |
711 | let event = match mouse_input_state.delayed.take() { |
712 | Some(e) => e.1, |
713 | None => return mouse_input_state, |
714 | }; |
715 | |
716 | let top_item = match mouse_input_state.top_item() { |
717 | Some(i) => i, |
718 | None => return MouseInputState::default(), |
719 | }; |
720 | |
721 | let mut actual_visitor = |
722 | |component: &ItemTreeRc, index: u32, _: Pin<ItemRef>| -> VisitChildrenResult { |
723 | send_mouse_event_to_item( |
724 | event, |
725 | ItemRc::new(component.clone(), index), |
726 | window_adapter, |
727 | &mut mouse_input_state, |
728 | Some(&top_item), |
729 | true, |
730 | ) |
731 | }; |
732 | vtable::new_vref!(let mut actual_visitor : VRefMut<crate::item_tree::ItemVisitorVTable> for crate::item_tree::ItemVisitor = &mut actual_visitor); |
733 | vtable::VRc::borrow_pin(top_item.item_tree()).as_ref().visit_children_item( |
734 | top_item.index() as isize, |
735 | crate::item_tree::TraversalOrder::FrontToBack, |
736 | actual_visitor, |
737 | ); |
738 | mouse_input_state |
739 | } |
740 | |
741 | fn send_mouse_event_to_item( |
742 | mouse_event: MouseEvent, |
743 | item_rc: ItemRc, |
744 | window_adapter: &Rc<dyn WindowAdapter>, |
745 | result: &mut MouseInputState, |
746 | last_top_item: Option<&ItemRc>, |
747 | ignore_delays: bool, |
748 | ) -> VisitChildrenResult { |
749 | let item = item_rc.borrow(); |
750 | let geom = item_rc.geometry(); |
751 | // translated in our coordinate |
752 | let mut event_for_children = mouse_event; |
753 | event_for_children.translate(-geom.origin.to_vector()); |
754 | |
755 | let filter_result = if mouse_event.position().map_or(false, |p| geom.contains(p)) |
756 | || crate::item_rendering::is_clipping_item(item) |
757 | { |
758 | item.as_ref().input_event_filter_before_children( |
759 | event_for_children, |
760 | window_adapter, |
761 | &item_rc, |
762 | ) |
763 | } else { |
764 | InputEventFilterResult::ForwardAndIgnore |
765 | }; |
766 | |
767 | let (forward_to_children, ignore) = match filter_result { |
768 | InputEventFilterResult::ForwardEvent => (true, false), |
769 | InputEventFilterResult::ForwardAndIgnore => (true, true), |
770 | InputEventFilterResult::ForwardAndInterceptGrab => (true, false), |
771 | InputEventFilterResult::Intercept => (false, false), |
772 | InputEventFilterResult::DelayForwarding(_) if ignore_delays => (true, false), |
773 | InputEventFilterResult::DelayForwarding(duration) => { |
774 | let timer = Timer::default(); |
775 | let w = Rc::downgrade(window_adapter); |
776 | timer.start( |
777 | crate::timers::TimerMode::SingleShot, |
778 | Duration::from_millis(duration), |
779 | move || { |
780 | if let Some(w) = w.upgrade() { |
781 | WindowInner::from_pub(w.window()).process_delayed_event(); |
782 | } |
783 | }, |
784 | ); |
785 | result.delayed = Some((timer, event_for_children)); |
786 | result |
787 | .item_stack |
788 | .push((item_rc.downgrade(), InputEventFilterResult::DelayForwarding(duration))); |
789 | return VisitChildrenResult::abort(item_rc.index(), 0); |
790 | } |
791 | }; |
792 | |
793 | result.item_stack.push((item_rc.downgrade(), filter_result)); |
794 | if forward_to_children { |
795 | let mut actual_visitor = |
796 | |component: &ItemTreeRc, index: u32, _: Pin<ItemRef>| -> VisitChildrenResult { |
797 | send_mouse_event_to_item( |
798 | event_for_children, |
799 | ItemRc::new(component.clone(), index), |
800 | window_adapter, |
801 | result, |
802 | last_top_item, |
803 | ignore_delays, |
804 | ) |
805 | }; |
806 | vtable::new_vref!(let mut actual_visitor : VRefMut<crate::item_tree::ItemVisitorVTable> for crate::item_tree::ItemVisitor = &mut actual_visitor); |
807 | let r = vtable::VRc::borrow_pin(item_rc.item_tree()).as_ref().visit_children_item( |
808 | item_rc.index() as isize, |
809 | crate::item_tree::TraversalOrder::FrontToBack, |
810 | actual_visitor, |
811 | ); |
812 | if r.has_aborted() { |
813 | return r; |
814 | } |
815 | }; |
816 | |
817 | let r = if ignore { |
818 | InputEventResult::EventIgnored |
819 | } else { |
820 | let mut event = mouse_event; |
821 | event.translate(-geom.origin.to_vector()); |
822 | if last_top_item.map_or(true, |x| *x != item_rc) { |
823 | event.set_click_count(0); |
824 | } |
825 | item.as_ref().input_event(event, window_adapter, &item_rc) |
826 | }; |
827 | match r { |
828 | InputEventResult::EventAccepted => VisitChildrenResult::abort(item_rc.index(), 0), |
829 | InputEventResult::EventIgnored => { |
830 | let _pop = result.item_stack.pop(); |
831 | debug_assert_eq!( |
832 | _pop.map(|x| (x.0.upgrade().unwrap().index(), x.1)).unwrap(), |
833 | (item_rc.index(), filter_result) |
834 | ); |
835 | VisitChildrenResult::CONTINUE |
836 | } |
837 | InputEventResult::GrabMouse => { |
838 | result.item_stack.last_mut().unwrap().1 = |
839 | InputEventFilterResult::ForwardAndInterceptGrab; |
840 | result.grabbed = true; |
841 | VisitChildrenResult::abort(item_rc.index(), 0) |
842 | } |
843 | } |
844 | } |
845 | |
846 | /// The TextCursorBlinker takes care of providing a toggled boolean property |
847 | /// that can be used to animate a blinking cursor. It's typically stored in the |
848 | /// Window using a Weak and set_binding() can be used to set up a binding on a given |
849 | /// property that'll keep it up-to-date. That binding keeps a strong reference to the |
850 | /// blinker. If the underlying item that uses it goes away, the binding goes away and |
851 | /// so does the blinker. |
852 | #[derive (FieldOffsets)] |
853 | #[repr (C)] |
854 | #[pin] |
855 | pub(crate) struct TextCursorBlinker { |
856 | cursor_visible: Property<bool>, |
857 | cursor_blink_timer: crate::timers::Timer, |
858 | } |
859 | |
860 | impl TextCursorBlinker { |
861 | /// Creates a new instance, wrapped in a Pin<Rc<_>> because the boolean property |
862 | /// the blinker properties uses the property system that requires pinning. |
863 | pub fn new() -> Pin<Rc<Self>> { |
864 | Rc::pin(Self { |
865 | cursor_visible: Property::new(true), |
866 | cursor_blink_timer: Default::default(), |
867 | }) |
868 | } |
869 | |
870 | /// Sets a binding on the provided property that will ensure that the property value |
871 | /// is true when the cursor should be shown and false if not. |
872 | pub fn set_binding(instance: Pin<Rc<TextCursorBlinker>>, prop: &Property<bool>) { |
873 | instance.as_ref().cursor_visible.set(true); |
874 | // Re-start timer, in case. |
875 | Self::start(&instance); |
876 | prop.set_binding(move || { |
877 | TextCursorBlinker::FIELD_OFFSETS.cursor_visible.apply_pin(instance.as_ref()).get() |
878 | }); |
879 | } |
880 | |
881 | /// Starts the blinking cursor timer that will toggle the cursor and update all bindings that |
882 | /// were installed on properties with set_binding call. |
883 | pub fn start(self: &Pin<Rc<Self>>) { |
884 | if self.cursor_blink_timer.running() { |
885 | self.cursor_blink_timer.restart(); |
886 | } else { |
887 | let toggle_cursor = { |
888 | let weak_blinker = pin_weak::rc::PinWeak::downgrade(self.clone()); |
889 | move || { |
890 | if let Some(blinker) = weak_blinker.upgrade() { |
891 | let visible = TextCursorBlinker::FIELD_OFFSETS |
892 | .cursor_visible |
893 | .apply_pin(blinker.as_ref()) |
894 | .get(); |
895 | blinker.cursor_visible.set(!visible); |
896 | } |
897 | } |
898 | }; |
899 | self.cursor_blink_timer.start( |
900 | crate::timers::TimerMode::Repeated, |
901 | Duration::from_millis(500), |
902 | toggle_cursor, |
903 | ); |
904 | } |
905 | } |
906 | |
907 | /// Stops the blinking cursor timer. This is usually used for example when the window that contains |
908 | /// text editable elements looses the focus or is hidden. |
909 | pub fn stop(&self) { |
910 | self.cursor_blink_timer.stop() |
911 | } |
912 | } |
913 | |