mirror of
https://github.com/slint-ui/slint.git
synced 2025-08-30 23:27:22 +00:00

AltGr on windos sends a left control pressed followed by AltGr pressed on both winit and Qt. On AltGr release it sends left control released followed by AltGr released. So unset left control on windows once AltGr is pressed to avoid treating special characters entered via AltGr key combos to be treated as control sequences -- leading to the input being ignored! Also treat `Alt-Ctlr-X` as `AltGr-X` on windows. Fixes: #2933
932 lines
34 KiB
Rust
932 lines
34 KiB
Rust
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
|
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.0 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))
|
|
}
|
|
_ => (),
|
|
};
|
|
}
|
|
}
|
|
|
|
match TextCursorDirection::try_from(keycode) {
|
|
Ok(direction) => return Some(TextShortcut::Move(direction)),
|
|
_ => (),
|
|
};
|
|
|
|
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() {
|
|
// 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 => {
|
|
return 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)
|
|
);
|
|
return VisitChildrenResult::CONTINUE;
|
|
}
|
|
InputEventResult::GrabMouse => {
|
|
result.item_stack.last_mut().unwrap().1 =
|
|
InputEventFilterResult::ForwardAndInterceptGrab;
|
|
result.grabbed = true;
|
|
return 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()
|
|
}
|
|
}
|