mirror of
https://github.com/slint-ui/slint.git
synced 2025-08-31 15:47:26 +00:00

because the pending press event might be loss if there is a move between the click and the release This was reported on Mattermost
933 lines
35 KiB
Rust
933 lines
35 KiB
Rust
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
|
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
|
|
|
|
/*! Module handling mouse events
|
|
*/
|
|
#![warn(missing_docs)]
|
|
|
|
use crate::item_tree::{ItemRc, ItemWeak, VisitChildrenResult};
|
|
pub use crate::items::PointerEventButton;
|
|
use crate::items::{ItemRef, TextCursorDirection};
|
|
use crate::lengths::{LogicalPoint, LogicalVector};
|
|
use crate::timers::Timer;
|
|
use crate::window::{WindowAdapter, WindowInner};
|
|
use crate::Property;
|
|
use crate::{component::ComponentRc, SharedString};
|
|
use alloc::rc::Rc;
|
|
use alloc::vec::Vec;
|
|
use const_field_offset::FieldOffsets;
|
|
use core::cell::Cell;
|
|
use core::pin::Pin;
|
|
|
|
/// A mouse or touch event
|
|
///
|
|
/// The only difference with [`crate::platform::WindowEvent`] us that it uses untyped `Point`
|
|
/// TODO: merge with platform::WindowEvent
|
|
#[repr(C)]
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
#[allow(missing_docs)]
|
|
pub enum MouseEvent {
|
|
/// The mouse or finger was pressed
|
|
/// `position` is the position of the mouse when the event happens.
|
|
/// `button` describes the button that is pressed when the event happens.
|
|
/// `click_count` represents the current number of clicks.
|
|
Pressed { position: LogicalPoint, button: PointerEventButton, click_count: u8 },
|
|
/// The mouse or finger was released
|
|
/// `position` is the position of the mouse when the event happens.
|
|
/// `button` describes the button that is pressed when the event happens.
|
|
/// `click_count` represents the current number of clicks.
|
|
Released { position: LogicalPoint, button: PointerEventButton, click_count: u8 },
|
|
/// The position of the pointer has changed
|
|
Moved { position: LogicalPoint },
|
|
/// Wheel was operated.
|
|
/// `pos` is the position of the mouse when the event happens.
|
|
/// `delta_x` is the amount of pixels to scroll in horizontal direction,
|
|
/// `delta_y` is the amount of pixels to scroll in vertical direction.
|
|
Wheel { position: LogicalPoint, delta_x: f32, delta_y: f32 },
|
|
/// The mouse exited the item or component
|
|
Exit,
|
|
}
|
|
|
|
impl MouseEvent {
|
|
/// The position of the cursor for this event, if any
|
|
pub fn position(&self) -> Option<LogicalPoint> {
|
|
match self {
|
|
MouseEvent::Pressed { position, .. } => Some(*position),
|
|
MouseEvent::Released { position, .. } => Some(*position),
|
|
MouseEvent::Moved { position } => Some(*position),
|
|
MouseEvent::Wheel { position, .. } => Some(*position),
|
|
MouseEvent::Exit => None,
|
|
}
|
|
}
|
|
|
|
/// Translate the position by the given value
|
|
pub fn translate(&mut self, vec: LogicalVector) {
|
|
let pos = match self {
|
|
MouseEvent::Pressed { position, .. } => Some(position),
|
|
MouseEvent::Released { position, .. } => Some(position),
|
|
MouseEvent::Moved { position } => Some(position),
|
|
MouseEvent::Wheel { position, .. } => Some(position),
|
|
MouseEvent::Exit => None,
|
|
};
|
|
if let Some(pos) = pos {
|
|
*pos += vec;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// This value is returned by the `input_event` function of an Item
|
|
/// to notify the run-time about how the event was handled and
|
|
/// what the next steps are.
|
|
/// See [`crate::items::ItemVTable::input_event`].
|
|
#[repr(C)]
|
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
|
|
pub enum InputEventResult {
|
|
/// The event was accepted. This may result in additional events, for example
|
|
/// accepting a mouse move will result in a MouseExit event later.
|
|
EventAccepted,
|
|
/// The event was ignored.
|
|
#[default]
|
|
EventIgnored,
|
|
/// All further mouse event need to be sent to this item or component
|
|
GrabMouse,
|
|
}
|
|
|
|
/// This value is returned by the `input_event_filter_before_children` function, which
|
|
/// can specify how to further process the event.
|
|
/// See [`crate::items::ItemVTable::input_event_filter_before_children`].
|
|
#[repr(C)]
|
|
#[derive(Debug, Copy, Clone, PartialEq, Default)]
|
|
pub enum InputEventFilterResult {
|
|
/// The event is going to be forwarded to children, then the [`crate::items::ItemVTable::input_event`]
|
|
/// function is called
|
|
#[default]
|
|
ForwardEvent,
|
|
/// The event will be forwarded to the children, but the [`crate::items::ItemVTable::input_event`] is not
|
|
/// going to be called for this item
|
|
ForwardAndIgnore,
|
|
/// Just like `ForwardEvent`, but even in the case the children grabs the mouse, this function
|
|
/// will still be called for further event
|
|
ForwardAndInterceptGrab,
|
|
/// The event will not be forwarded to children, if a children already had the grab, the
|
|
/// grab will be cancelled with a [`MouseEvent::Exit`] event
|
|
Intercept,
|
|
/// Similar to `Intercept` but the contained [`MouseEvent`] will be forwarded to children
|
|
InterceptAndDispatch(MouseEvent),
|
|
/// The event will be forwarding to the children with a delay (in milliseconds), unless it is
|
|
/// being intercepted.
|
|
/// This is what happens when the flickable wants to delay the event.
|
|
/// This should only be used for Press event, and the event will be sent after the delay, or
|
|
/// if a release event is seen before that delay
|
|
//(Can't use core::time::Duration because it is not repr(c))
|
|
DelayForwarding(u64),
|
|
}
|
|
|
|
/// This module contains the constant character code used to represent the keys.
|
|
#[allow(missing_docs, non_upper_case_globals)]
|
|
pub mod key_codes {
|
|
macro_rules! declare_consts_for_special_keys {
|
|
($($char:literal # $name:ident # $($_qt:ident)|* # $($_winit:ident)|* ;)*) => {
|
|
$(pub const $name : char = $char;)*
|
|
|
|
#[allow(missing_docs)]
|
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
|
#[non_exhaustive]
|
|
#[repr(C)]
|
|
/// The `Key` enum is used to map a specific key by name e.g. `Key::Control` to an
|
|
/// internal used unicode representation. The enum is convertible to [`std::char`] and [`slint::SharedString`](`crate::SharedString`).
|
|
/// Use this with [`slint::platform::WindowEvent`](`crate::platform::WindowEvent`) to supply key events to Slint's platform abstraction.
|
|
///
|
|
/// ```
|
|
/// let slint_key_code: char = slint::platform::Key::Tab.into();
|
|
/// assert_eq!(slint_key_code, '\t')
|
|
/// ```
|
|
pub enum Key {
|
|
$($name,)*
|
|
}
|
|
|
|
impl From<Key> for char {
|
|
fn from(k: Key) -> Self {
|
|
match k {
|
|
$(Key::$name => $name,)*
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<Key> for crate::SharedString {
|
|
fn from(k: Key) -> Self {
|
|
char::from(k).into()
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
i_slint_common::for_each_special_keys!(declare_consts_for_special_keys);
|
|
}
|
|
|
|
/// Internal struct to maintain the pressed/released state of the keys that
|
|
/// map to keyboard modifiers.
|
|
#[derive(Clone, Copy, Default, Debug)]
|
|
pub(crate) struct InternalKeyboardModifierState {
|
|
left_alt: bool,
|
|
right_alt: bool,
|
|
altgr: bool,
|
|
left_control: bool,
|
|
right_control: bool,
|
|
left_meta: bool,
|
|
right_meta: bool,
|
|
left_shift: bool,
|
|
right_shift: bool,
|
|
}
|
|
|
|
impl InternalKeyboardModifierState {
|
|
/// Updates a flag of the modifiers if the key of the given text is pressed.
|
|
/// Returns an updated modifier if detected; None otherwise;
|
|
pub(crate) fn state_update(mut self, pressed: bool, text: &SharedString) -> Option<Self> {
|
|
if let Some(key_code) = text.chars().next() {
|
|
match key_code {
|
|
key_codes::Alt => self.left_alt = pressed,
|
|
key_codes::AltGr => self.altgr = pressed,
|
|
key_codes::Control => self.left_control = pressed,
|
|
key_codes::ControlR => self.right_control = pressed,
|
|
key_codes::Shift => self.left_shift = pressed,
|
|
key_codes::ShiftR => self.right_shift = pressed,
|
|
key_codes::Meta => self.left_meta = pressed,
|
|
key_codes::MetaR => self.right_meta = pressed,
|
|
_ => return None,
|
|
};
|
|
|
|
// Encoded keyboard modifiers must appear as individual key events. This could
|
|
// be relaxed by implementing a string split, but right now WindowEvent::KeyPressed
|
|
// holds only a single char.
|
|
debug_assert_eq!(key_code.len_utf8(), text.len());
|
|
}
|
|
|
|
// Special cases:
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
if self.altgr {
|
|
// Windows sends Ctrl followed by AltGr on AltGr. Disable the Ctrl again!
|
|
self.left_control = false;
|
|
self.right_control = false;
|
|
} else if self.control() && self.alt() {
|
|
// Windows treats Ctrl-Alt as AltGr
|
|
self.left_control = false;
|
|
self.right_control = false;
|
|
self.left_alt = false;
|
|
self.right_alt = false;
|
|
}
|
|
}
|
|
|
|
Some(self)
|
|
}
|
|
|
|
pub fn shift(&self) -> bool {
|
|
self.right_shift || self.left_shift
|
|
}
|
|
pub fn alt(&self) -> bool {
|
|
self.right_alt || self.left_alt
|
|
}
|
|
pub fn meta(&self) -> bool {
|
|
self.right_meta || self.left_meta
|
|
}
|
|
pub fn control(&self) -> bool {
|
|
self.right_control || self.left_control
|
|
}
|
|
}
|
|
|
|
/// KeyboardModifier provides booleans to indicate possible modifier keys
|
|
/// on a keyboard, such as Shift, Control, etc.
|
|
///
|
|
/// On macOS, the command key is mapped to the meta modifier.
|
|
///
|
|
/// On Windows, the windows key is mapped to the meta modifier.
|
|
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
|
#[repr(C)]
|
|
pub struct KeyboardModifiers {
|
|
/// Indicates the alt key on a keyboard.
|
|
pub alt: bool,
|
|
/// Indicates the control key on a keyboard.
|
|
pub control: bool,
|
|
/// Indicates the command key on macos.
|
|
pub meta: bool,
|
|
/// Indicates the shift key on a keyboard.
|
|
pub shift: bool,
|
|
}
|
|
|
|
impl From<InternalKeyboardModifierState> for KeyboardModifiers {
|
|
fn from(internal_state: InternalKeyboardModifierState) -> Self {
|
|
Self {
|
|
alt: internal_state.alt(),
|
|
control: internal_state.control(),
|
|
meta: internal_state.meta(),
|
|
shift: internal_state.shift(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// This enum defines the different kinds of key events that can happen.
|
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
|
|
#[repr(C)]
|
|
pub enum KeyEventType {
|
|
/// A key on a keyboard was pressed.
|
|
#[default]
|
|
KeyPressed = 0,
|
|
/// A key on a keyboard was released.
|
|
KeyReleased = 1,
|
|
/// The input method updates the currently composed text. The KeyEvent's text field is the pre-edit text and
|
|
/// composition_selection specifies the placement of the cursor within the pre-edit text.
|
|
UpdateComposition = 2,
|
|
/// The input method replaces the currently composed text with the final result of the composition.
|
|
CommitComposition = 3,
|
|
}
|
|
|
|
/// Represents a key event sent by the windowing system.
|
|
#[derive(Debug, Clone, PartialEq, Default)]
|
|
#[repr(C)]
|
|
pub struct KeyInputEvent {
|
|
/// The unicode representation of the key pressed.
|
|
pub text: SharedString,
|
|
|
|
// note: this field is not exported in the .slint in the KeyEvent builtin struct
|
|
/// Indicates whether the key was pressed or released
|
|
pub event_type: KeyEventType,
|
|
|
|
/// If the event type is KeyEventType::UpdateComposition, then this field specifies
|
|
/// the start of the selection as byte offsets within the preedit text.
|
|
pub preedit_selection_start: usize,
|
|
/// If the event type is KeyEventType::UpdateComposition, then this field specifies
|
|
/// the end of the selection as byte offsets within the preedit text.
|
|
pub preedit_selection_end: usize,
|
|
}
|
|
|
|
/// Represents a key event.
|
|
#[derive(Debug, Clone, PartialEq, Default)]
|
|
#[repr(C)]
|
|
pub struct KeyEvent {
|
|
/// The keyboard modifiers active at the time of the key press event.
|
|
pub modifiers: KeyboardModifiers,
|
|
|
|
/// The unicode representation of the key pressed.
|
|
pub text: SharedString,
|
|
|
|
// note: this field is not exported in the .slint in the KeyEvent builtin struct
|
|
/// Indicates whether the key was pressed or released
|
|
pub event_type: KeyEventType,
|
|
|
|
/// If the event type is KeyEventType::UpdateComposition, then this field specifies
|
|
/// the start of the selection as byte offsets within the preedit text.
|
|
pub preedit_selection_start: usize,
|
|
/// If the event type is KeyEventType::UpdateComposition, then this field specifies
|
|
/// the end of the selection as byte offsets within the preedit text.
|
|
pub preedit_selection_end: usize,
|
|
}
|
|
|
|
impl KeyEvent {
|
|
/// If a shortcut was pressed, this function returns `Some(StandardShortcut)`.
|
|
/// Otherwise it returns None.
|
|
pub fn shortcut(&self) -> Option<StandardShortcut> {
|
|
if self.modifiers.control && !self.modifiers.shift {
|
|
match self.text.as_str() {
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
"c" => Some(StandardShortcut::Copy),
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
"x" => Some(StandardShortcut::Cut),
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
"v" => Some(StandardShortcut::Paste),
|
|
"a" => Some(StandardShortcut::SelectAll),
|
|
"f" => Some(StandardShortcut::Find),
|
|
"s" => Some(StandardShortcut::Save),
|
|
"p" => Some(StandardShortcut::Print),
|
|
"z" => Some(StandardShortcut::Undo),
|
|
#[cfg(target_os = "windows")]
|
|
"y" => Some(StandardShortcut::Redo),
|
|
"r" => Some(StandardShortcut::Refresh),
|
|
_ => None,
|
|
}
|
|
} else if self.modifiers.control && self.modifiers.shift {
|
|
match self.text.as_str() {
|
|
#[cfg(not(target_os = "windows"))]
|
|
"z" => Some(StandardShortcut::Redo),
|
|
_ => None,
|
|
}
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// If a shortcut concerning text editing was pressed, this function
|
|
/// returns `Some(TextShortcut)`. Otherwise it returns None.
|
|
pub fn text_shortcut(&self) -> Option<TextShortcut> {
|
|
let keycode = self.text.chars().next()?;
|
|
|
|
let move_mod = if cfg!(target_os = "macos") {
|
|
self.modifiers.alt && !self.modifiers.control && !self.modifiers.meta
|
|
} else {
|
|
self.modifiers.control && !self.modifiers.alt && !self.modifiers.meta
|
|
};
|
|
|
|
if move_mod {
|
|
match keycode {
|
|
key_codes::LeftArrow => {
|
|
return Some(TextShortcut::Move(TextCursorDirection::BackwardByWord))
|
|
}
|
|
key_codes::RightArrow => {
|
|
return Some(TextShortcut::Move(TextCursorDirection::ForwardByWord))
|
|
}
|
|
key_codes::UpArrow => {
|
|
return Some(TextShortcut::Move(TextCursorDirection::StartOfParagraph))
|
|
}
|
|
key_codes::DownArrow => {
|
|
return Some(TextShortcut::Move(TextCursorDirection::EndOfParagraph))
|
|
}
|
|
key_codes::Backspace => {
|
|
return Some(TextShortcut::DeleteWordBackward);
|
|
}
|
|
key_codes::Delete => {
|
|
return Some(TextShortcut::DeleteWordForward);
|
|
}
|
|
_ => (),
|
|
};
|
|
}
|
|
|
|
#[cfg(not(target_os = "macos"))]
|
|
{
|
|
if self.modifiers.control && !self.modifiers.alt && !self.modifiers.meta {
|
|
match keycode {
|
|
key_codes::Home => {
|
|
return Some(TextShortcut::Move(TextCursorDirection::StartOfText))
|
|
}
|
|
key_codes::End => {
|
|
return Some(TextShortcut::Move(TextCursorDirection::EndOfText))
|
|
}
|
|
_ => (),
|
|
};
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
if self.modifiers.control {
|
|
match keycode {
|
|
key_codes::LeftArrow => {
|
|
return Some(TextShortcut::Move(TextCursorDirection::StartOfLine))
|
|
}
|
|
key_codes::RightArrow => {
|
|
return Some(TextShortcut::Move(TextCursorDirection::EndOfLine))
|
|
}
|
|
key_codes::UpArrow => {
|
|
return Some(TextShortcut::Move(TextCursorDirection::StartOfText))
|
|
}
|
|
key_codes::DownArrow => {
|
|
return Some(TextShortcut::Move(TextCursorDirection::EndOfText))
|
|
}
|
|
_ => (),
|
|
};
|
|
}
|
|
}
|
|
|
|
if let Ok(direction) = TextCursorDirection::try_from(keycode) {
|
|
Some(TextShortcut::Move(direction))
|
|
} else {
|
|
match keycode {
|
|
key_codes::Backspace => Some(TextShortcut::DeleteBackward),
|
|
key_codes::Delete => Some(TextShortcut::DeleteForward),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Represents a non context specific shortcut.
|
|
pub enum StandardShortcut {
|
|
/// Copy Something
|
|
Copy,
|
|
/// Cut Something
|
|
Cut,
|
|
/// Paste Something
|
|
Paste,
|
|
/// Select All
|
|
SelectAll,
|
|
/// Find/Search Something
|
|
Find,
|
|
/// Save Something
|
|
Save,
|
|
/// Print Something
|
|
Print,
|
|
/// Undo the last action
|
|
Undo,
|
|
/// Redo the last undone action
|
|
Redo,
|
|
/// Refresh
|
|
Refresh,
|
|
}
|
|
|
|
/// Shortcuts that are used when editing text
|
|
pub enum TextShortcut {
|
|
/// Move the cursor
|
|
Move(TextCursorDirection),
|
|
/// Delete the Character to the right of the cursor
|
|
DeleteForward,
|
|
/// Delete the Character to the left of the cursor (aka Backspace).
|
|
DeleteBackward,
|
|
/// Delete the word to the right of the cursor
|
|
DeleteWordForward,
|
|
/// Delete the word to the left of the cursor (aka Ctrl + Backspace).
|
|
DeleteWordBackward,
|
|
}
|
|
|
|
/// Represents how an item's key_event handler dealt with a key event.
|
|
/// An accepted event results in no further event propagation.
|
|
#[repr(C)]
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub enum KeyEventResult {
|
|
/// The event was handled.
|
|
EventAccepted,
|
|
/// The event was not handled and should be sent to other items.
|
|
EventIgnored,
|
|
}
|
|
|
|
/// Represents how an item's focus_event handler dealt with a focus event.
|
|
/// An accepted event results in no further event propagation.
|
|
#[repr(C)]
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub enum FocusEventResult {
|
|
/// The event was handled.
|
|
FocusAccepted,
|
|
/// The event was not handled and should be sent to other items.
|
|
FocusIgnored,
|
|
}
|
|
|
|
/// This event is sent to a component and items when they receive or loose
|
|
/// the keyboard focus.
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
#[repr(C)]
|
|
pub enum FocusEvent {
|
|
/// This event is sent when an item receives the focus.
|
|
FocusIn,
|
|
/// This event is sent when an item looses the focus.
|
|
FocusOut,
|
|
/// This event is sent when the window receives the keyboard focus.
|
|
WindowReceivedFocus,
|
|
/// This event is sent when the window looses the keyboard focus.
|
|
WindowLostFocus,
|
|
}
|
|
|
|
/// This state is used to count the clicks in the `click_interval` from the `PLATFORM_INSTANCE`.
|
|
#[derive(Default)]
|
|
pub struct ClickState {
|
|
click_count_time_stamp: Cell<Option<crate::animations::Instant>>,
|
|
click_count: Cell<u8>,
|
|
click_position: Cell<LogicalPoint>,
|
|
click_button: Cell<PointerEventButton>,
|
|
}
|
|
|
|
impl ClickState {
|
|
/// Resets the timer and count.
|
|
fn restart(&self, position: LogicalPoint, button: PointerEventButton) {
|
|
self.click_count.set(0);
|
|
self.click_count_time_stamp.set(Some(crate::animations::Instant::now()));
|
|
self.click_position.set(position);
|
|
self.click_button.set(button);
|
|
}
|
|
|
|
/// Check if the click is repeated.
|
|
pub fn check_repeat(&self, mouse_event: MouseEvent) -> MouseEvent {
|
|
match mouse_event {
|
|
MouseEvent::Pressed { position, button, .. } => {
|
|
let instant_now = crate::animations::Instant::now();
|
|
|
|
if let Some(click_count_time_stamp) = self.click_count_time_stamp.get() {
|
|
if instant_now - click_count_time_stamp
|
|
< crate::platform::PLATFORM_INSTANCE
|
|
.with(|p| p.get().map(|p| p.click_interval()))
|
|
.unwrap_or_default()
|
|
&& button == self.click_button.get()
|
|
&& (position - self.click_position.get()).square_length() < 100 as _
|
|
{
|
|
self.click_count.set(self.click_count.get() + 1);
|
|
self.click_count_time_stamp.set(Some(instant_now));
|
|
} else {
|
|
self.restart(position, button);
|
|
}
|
|
} else {
|
|
self.restart(position, button);
|
|
}
|
|
|
|
return MouseEvent::Pressed {
|
|
position,
|
|
button,
|
|
click_count: self.click_count.get(),
|
|
};
|
|
}
|
|
MouseEvent::Released { position, button, .. } => {
|
|
return MouseEvent::Released {
|
|
position,
|
|
button,
|
|
click_count: self.click_count.get(),
|
|
}
|
|
}
|
|
_ => {}
|
|
};
|
|
|
|
mouse_event
|
|
}
|
|
}
|
|
|
|
/// The state which a window should hold for the mouse input
|
|
#[derive(Default)]
|
|
pub struct MouseInputState {
|
|
/// The stack of item which contain the mouse cursor (or grab),
|
|
/// along with the last result from the input function
|
|
item_stack: Vec<(ItemWeak, InputEventFilterResult)>,
|
|
/// true if the top item of the stack has the mouse grab
|
|
grabbed: bool,
|
|
delayed: Option<(crate::timers::Timer, MouseEvent)>,
|
|
delayed_exit_items: Vec<ItemWeak>,
|
|
}
|
|
|
|
/// Try to handle the mouse grabber. Return None if the event has been handled, otherwise
|
|
/// return the event that must be handled
|
|
fn handle_mouse_grab(
|
|
mouse_event: MouseEvent,
|
|
window_adapter: &Rc<dyn WindowAdapter>,
|
|
mouse_input_state: &mut MouseInputState,
|
|
) -> Option<MouseEvent> {
|
|
if !mouse_input_state.grabbed || mouse_input_state.item_stack.is_empty() {
|
|
return Some(mouse_event);
|
|
};
|
|
|
|
let mut event = mouse_event;
|
|
let mut intercept = false;
|
|
let mut invalid = false;
|
|
|
|
mouse_input_state.item_stack.retain(|it| {
|
|
if invalid {
|
|
return false;
|
|
}
|
|
let item = if let Some(item) = it.0.upgrade() {
|
|
item
|
|
} else {
|
|
invalid = true;
|
|
return false;
|
|
};
|
|
if intercept {
|
|
item.borrow().as_ref().input_event(MouseEvent::Exit, window_adapter, &item);
|
|
return false;
|
|
}
|
|
let g = item.geometry();
|
|
event.translate(-g.origin.to_vector());
|
|
|
|
let interested = matches!(
|
|
it.1,
|
|
InputEventFilterResult::ForwardAndInterceptGrab
|
|
| InputEventFilterResult::DelayForwarding(_)
|
|
);
|
|
|
|
if interested
|
|
&& item.borrow().as_ref().input_event_filter_before_children(
|
|
event,
|
|
window_adapter,
|
|
&item,
|
|
) == InputEventFilterResult::Intercept
|
|
{
|
|
intercept = true;
|
|
}
|
|
true
|
|
});
|
|
if invalid {
|
|
return Some(mouse_event);
|
|
}
|
|
|
|
let grabber = mouse_input_state.item_stack.last().unwrap().0.upgrade().unwrap();
|
|
let input_result = grabber.borrow().as_ref().input_event(event, window_adapter, &grabber);
|
|
if input_result != InputEventResult::GrabMouse {
|
|
mouse_input_state.grabbed = false;
|
|
// Return a move event so that the new position can be registered properly
|
|
return Some(
|
|
mouse_event
|
|
.position()
|
|
.map_or(MouseEvent::Exit, |position| MouseEvent::Moved { position }),
|
|
);
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn send_exit_events(
|
|
old_input_state: &MouseInputState,
|
|
new_input_state: &mut MouseInputState,
|
|
mut pos: Option<LogicalPoint>,
|
|
window_adapter: &Rc<dyn WindowAdapter>,
|
|
) {
|
|
for it in core::mem::take(&mut new_input_state.delayed_exit_items) {
|
|
let Some(item) = it.upgrade() else { continue };
|
|
item.borrow().as_ref().input_event(MouseEvent::Exit, window_adapter, &item);
|
|
}
|
|
|
|
let mut clipped = false;
|
|
for (idx, it) in old_input_state.item_stack.iter().enumerate() {
|
|
let Some(item) = it.0.upgrade() else { break };
|
|
let g = item.geometry();
|
|
let contains = pos.map_or(false, |p| g.contains(p));
|
|
if let Some(p) = pos.as_mut() {
|
|
*p -= g.origin.to_vector();
|
|
}
|
|
if !contains || clipped {
|
|
if crate::item_rendering::is_clipping_item(item.borrow()) {
|
|
clipped = true;
|
|
}
|
|
item.borrow().as_ref().input_event(MouseEvent::Exit, window_adapter, &item);
|
|
} else if new_input_state.item_stack.get(idx).map_or(true, |(x, _)| *x != it.0) {
|
|
// 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
|
|
if new_input_state.delayed.is_some() {
|
|
new_input_state.delayed_exit_items.push(it.0.clone());
|
|
} else {
|
|
item.borrow().as_ref().input_event(MouseEvent::Exit, window_adapter, &item);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Process the `mouse_event` on the `component`, the `mouse_grabber_stack` is the previous stack
|
|
/// of mouse grabber.
|
|
/// Returns a new mouse grabber stack.
|
|
pub fn process_mouse_input(
|
|
component: ComponentRc,
|
|
mouse_event: MouseEvent,
|
|
window_adapter: &Rc<dyn WindowAdapter>,
|
|
mut mouse_input_state: MouseInputState,
|
|
) -> MouseInputState {
|
|
if matches!(mouse_event, MouseEvent::Released { .. }) {
|
|
mouse_input_state = process_delayed_event(window_adapter, mouse_input_state);
|
|
}
|
|
|
|
let Some(mouse_event) = handle_mouse_grab(mouse_event, window_adapter, &mut mouse_input_state) else { return mouse_input_state };
|
|
|
|
let mut result = MouseInputState::default();
|
|
let root = ItemRc::new(component, 0);
|
|
let r = send_mouse_event_to_item(mouse_event, root, window_adapter, &mut result, false);
|
|
if mouse_input_state.delayed.is_some()
|
|
&& (!r.has_aborted()
|
|
|| Option::zip(result.item_stack.last(), mouse_input_state.item_stack.last())
|
|
.map_or(true, |(a, b)| a.0 != b.0))
|
|
{
|
|
// Keep the delayed event
|
|
return mouse_input_state;
|
|
}
|
|
send_exit_events(&mouse_input_state, &mut result, mouse_event.position(), window_adapter);
|
|
|
|
result
|
|
}
|
|
|
|
pub(crate) fn process_delayed_event(
|
|
window_adapter: &Rc<dyn WindowAdapter>,
|
|
mut mouse_input_state: MouseInputState,
|
|
) -> MouseInputState {
|
|
// the take bellow will also destroy the Timer
|
|
let event = match mouse_input_state.delayed.take() {
|
|
Some(e) => e.1,
|
|
None => return mouse_input_state,
|
|
};
|
|
|
|
let top_item = match mouse_input_state.item_stack.last().unwrap().0.upgrade() {
|
|
Some(i) => i,
|
|
None => return MouseInputState::default(),
|
|
};
|
|
|
|
let mut actual_visitor =
|
|
|component: &ComponentRc, index: usize, _: Pin<ItemRef>| -> VisitChildrenResult {
|
|
send_mouse_event_to_item(
|
|
event,
|
|
ItemRc::new(component.clone(), index),
|
|
window_adapter,
|
|
&mut mouse_input_state,
|
|
true,
|
|
)
|
|
};
|
|
vtable::new_vref!(let mut actual_visitor : VRefMut<crate::item_tree::ItemVisitorVTable> for crate::item_tree::ItemVisitor = &mut actual_visitor);
|
|
vtable::VRc::borrow_pin(top_item.component()).as_ref().visit_children_item(
|
|
top_item.index() as isize,
|
|
crate::item_tree::TraversalOrder::FrontToBack,
|
|
actual_visitor,
|
|
);
|
|
mouse_input_state
|
|
}
|
|
|
|
fn send_mouse_event_to_item(
|
|
mouse_event: MouseEvent,
|
|
item_rc: ItemRc,
|
|
window_adapter: &Rc<dyn WindowAdapter>,
|
|
result: &mut MouseInputState,
|
|
ignore_delays: bool,
|
|
) -> VisitChildrenResult {
|
|
let item = item_rc.borrow();
|
|
let geom = item_rc.geometry();
|
|
// translated in our coordinate
|
|
let mut event_for_children = mouse_event;
|
|
event_for_children.translate(-geom.origin.to_vector());
|
|
|
|
let filter_result = if mouse_event.position().map_or(false, |p| geom.contains(p))
|
|
|| crate::item_rendering::is_clipping_item(item)
|
|
{
|
|
item.as_ref().input_event_filter_before_children(
|
|
event_for_children,
|
|
window_adapter,
|
|
&item_rc,
|
|
)
|
|
} else {
|
|
InputEventFilterResult::ForwardAndIgnore
|
|
};
|
|
|
|
let (forward_to_children, ignore) = match filter_result {
|
|
InputEventFilterResult::ForwardEvent => (true, false),
|
|
InputEventFilterResult::ForwardAndIgnore => (true, true),
|
|
InputEventFilterResult::ForwardAndInterceptGrab => (true, false),
|
|
InputEventFilterResult::Intercept => (false, false),
|
|
InputEventFilterResult::InterceptAndDispatch(new_event) => {
|
|
event_for_children = new_event;
|
|
(true, false)
|
|
}
|
|
InputEventFilterResult::DelayForwarding(_) if ignore_delays => (true, false),
|
|
InputEventFilterResult::DelayForwarding(duration) => {
|
|
let timer = Timer::default();
|
|
let w = Rc::downgrade(window_adapter);
|
|
timer.start(
|
|
crate::timers::TimerMode::SingleShot,
|
|
core::time::Duration::from_millis(duration),
|
|
move || {
|
|
if let Some(w) = w.upgrade() {
|
|
WindowInner::from_pub(w.window()).process_delayed_event();
|
|
}
|
|
},
|
|
);
|
|
result.delayed = Some((timer, event_for_children));
|
|
result
|
|
.item_stack
|
|
.push((item_rc.downgrade(), InputEventFilterResult::DelayForwarding(duration)));
|
|
return VisitChildrenResult::abort(item_rc.index(), 0);
|
|
}
|
|
};
|
|
|
|
result.item_stack.push((item_rc.downgrade(), filter_result));
|
|
if forward_to_children {
|
|
let mut actual_visitor =
|
|
|component: &ComponentRc, index: usize, _: Pin<ItemRef>| -> VisitChildrenResult {
|
|
send_mouse_event_to_item(
|
|
event_for_children,
|
|
ItemRc::new(component.clone(), index),
|
|
window_adapter,
|
|
result,
|
|
ignore_delays,
|
|
)
|
|
};
|
|
vtable::new_vref!(let mut actual_visitor : VRefMut<crate::item_tree::ItemVisitorVTable> for crate::item_tree::ItemVisitor = &mut actual_visitor);
|
|
let r = vtable::VRc::borrow_pin(item_rc.component()).as_ref().visit_children_item(
|
|
item_rc.index() as isize,
|
|
crate::item_tree::TraversalOrder::FrontToBack,
|
|
actual_visitor,
|
|
);
|
|
if r.has_aborted() {
|
|
// the event was intercepted by a children
|
|
if matches!(filter_result, InputEventFilterResult::InterceptAndDispatch(_)) {
|
|
let mut event = mouse_event;
|
|
event.translate(-geom.origin.to_vector());
|
|
item.as_ref().input_event(event, window_adapter, &item_rc);
|
|
}
|
|
return r;
|
|
}
|
|
};
|
|
|
|
let r = if ignore {
|
|
InputEventResult::EventIgnored
|
|
} else {
|
|
let mut event = mouse_event;
|
|
event.translate(-geom.origin.to_vector());
|
|
item.as_ref().input_event(event, window_adapter, &item_rc)
|
|
};
|
|
match r {
|
|
InputEventResult::EventAccepted => VisitChildrenResult::abort(item_rc.index(), 0),
|
|
InputEventResult::EventIgnored => {
|
|
let _pop = result.item_stack.pop();
|
|
debug_assert_eq!(
|
|
_pop.map(|x| (x.0.upgrade().unwrap().index(), x.1)).unwrap(),
|
|
(item_rc.index(), filter_result)
|
|
);
|
|
VisitChildrenResult::CONTINUE
|
|
}
|
|
InputEventResult::GrabMouse => {
|
|
result.item_stack.last_mut().unwrap().1 =
|
|
InputEventFilterResult::ForwardAndInterceptGrab;
|
|
result.grabbed = true;
|
|
VisitChildrenResult::abort(item_rc.index(), 0)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The TextCursorBlinker takes care of providing a toggled boolean property
|
|
/// that can be used to animate a blinking cursor. It's typically stored in the
|
|
/// Window using a Weak and set_binding() can be used to set up a binding on a given
|
|
/// property that'll keep it up-to-date. That binding keeps a strong reference to the
|
|
/// blinker. If the underlying item that uses it goes away, the binding goes away and
|
|
/// so does the blinker.
|
|
#[derive(FieldOffsets)]
|
|
#[repr(C)]
|
|
#[pin]
|
|
pub(crate) struct TextCursorBlinker {
|
|
cursor_visible: Property<bool>,
|
|
cursor_blink_timer: crate::timers::Timer,
|
|
}
|
|
|
|
impl TextCursorBlinker {
|
|
/// Creates a new instance, wrapped in a Pin<Rc<_>> because the boolean property
|
|
/// the blinker properties uses the property system that requires pinning.
|
|
pub fn new() -> Pin<Rc<Self>> {
|
|
Rc::pin(Self {
|
|
cursor_visible: Property::new(true),
|
|
cursor_blink_timer: Default::default(),
|
|
})
|
|
}
|
|
|
|
/// Sets a binding on the provided property that will ensure that the property value
|
|
/// is true when the cursor should be shown and false if not.
|
|
pub fn set_binding(instance: Pin<Rc<TextCursorBlinker>>, prop: &Property<bool>) {
|
|
instance.as_ref().cursor_visible.set(true);
|
|
// Re-start timer, in case.
|
|
Self::start(&instance);
|
|
prop.set_binding(move || {
|
|
TextCursorBlinker::FIELD_OFFSETS.cursor_visible.apply_pin(instance.as_ref()).get()
|
|
});
|
|
}
|
|
|
|
/// Starts the blinking cursor timer that will toggle the cursor and update all bindings that
|
|
/// were installed on properties with set_binding call.
|
|
pub fn start(self: &Pin<Rc<Self>>) {
|
|
if self.cursor_blink_timer.running() {
|
|
self.cursor_blink_timer.restart();
|
|
} else {
|
|
let toggle_cursor = {
|
|
let weak_blinker = pin_weak::rc::PinWeak::downgrade(self.clone());
|
|
move || {
|
|
if let Some(blinker) = weak_blinker.upgrade() {
|
|
let visible = TextCursorBlinker::FIELD_OFFSETS
|
|
.cursor_visible
|
|
.apply_pin(blinker.as_ref())
|
|
.get();
|
|
blinker.cursor_visible.set(!visible);
|
|
}
|
|
}
|
|
};
|
|
self.cursor_blink_timer.start(
|
|
crate::timers::TimerMode::Repeated,
|
|
core::time::Duration::from_millis(500),
|
|
toggle_cursor,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Stops the blinking cursor timer. This is usually used for example when the window that contains
|
|
/// text editable elements looses the focus or is hidden.
|
|
pub fn stop(&self) {
|
|
self.cursor_blink_timer.stop()
|
|
}
|
|
}
|