#![warn(missing_docs)]
use crate::item_tree::ItemTreeRc;
use crate::item_tree::{ItemRc, ItemWeak, VisitChildrenResult};
pub use crate::items::PointerEventButton;
use crate::items::{DropEvent, ItemRef, TextCursorDirection};
pub use crate::items::{FocusReason, KeyEvent, KeyboardModifiers};
use crate::lengths::{ItemTransform, LogicalPoint, LogicalVector};
use crate::timers::Timer;
use crate::window::{WindowAdapter, WindowInner};
use crate::{Coord, Property, SharedString};
use alloc::rc::Rc;
use alloc::vec::Vec;
use const_field_offset::FieldOffsets;
use core::cell::Cell;
use core::pin::Pin;
use core::time::Duration;
#[repr(C)]
#[derive(Debug, Clone, PartialEq)]
#[allow(missing_docs)]
pub enum MouseEvent {
Pressed { position: LogicalPoint, button: PointerEventButton, click_count: u8 },
Released { position: LogicalPoint, button: PointerEventButton, click_count: u8 },
Moved { position: LogicalPoint },
Wheel { position: LogicalPoint, delta_x: Coord, delta_y: Coord },
DragMove(DropEvent),
Drop(DropEvent),
Exit,
}
impl MouseEvent {
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::DragMove(e) | MouseEvent::Drop(e) => {
Some(crate::lengths::logical_point_from_api(e.position))
}
MouseEvent::Exit => None,
}
}
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::DragMove(e) | MouseEvent::Drop(e) => {
e.position = crate::api::LogicalPosition::from_euclid(
crate::lengths::logical_point_from_api(e.position) + vec,
);
None
}
MouseEvent::Exit => None,
};
if let Some(pos) = pos {
*pos += vec;
}
}
pub fn transform(&mut self, transform: ItemTransform) {
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::DragMove(e) | MouseEvent::Drop(e) => {
e.position = crate::api::LogicalPosition::from_euclid(
transform
.transform_point(crate::lengths::logical_point_from_api(e.position).cast())
.cast(),
);
None
}
MouseEvent::Exit => None,
};
if let Some(pos) = pos {
*pos = transform.transform_point(pos.cast()).cast();
}
}
fn set_click_count(&mut self, count: u8) {
match self {
MouseEvent::Pressed { click_count, .. } | MouseEvent::Released { click_count, .. } => {
*click_count = count
}
_ => (),
}
}
}
#[repr(u8)]
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
pub enum InputEventResult {
EventAccepted,
#[default]
EventIgnored,
GrabMouse,
StartDrag,
}
#[repr(C)]
#[derive(Debug, Copy, Clone, PartialEq, Default)]
pub enum InputEventFilterResult {
#[default]
ForwardEvent,
ForwardAndIgnore,
ForwardAndInterceptGrab,
Intercept,
DelayForwarding(u64),
}
#[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 $(($_pos:ident))?)|* # $($_xkb:ident)|*;)*) => {
$(pub const $name : char = $char;)*
#[allow(missing_docs)]
#[derive(Debug, Copy, Clone, PartialEq)]
#[non_exhaustive]
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);
}
#[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 {
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,
};
debug_assert_eq!(key_code.len_utf8(), text.len());
}
#[cfg(target_os = "windows")]
{
if self.altgr {
self.left_control = false;
self.right_control = false;
} else if self.control() && self.alt() {
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
}
}
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(),
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
#[repr(u8)]
pub enum KeyEventType {
#[default]
KeyPressed = 0,
KeyReleased = 1,
UpdateComposition = 2,
CommitComposition = 3,
}
impl KeyEvent {
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
}
}
pub fn text_shortcut(&self) -> Option<TextShortcut> {
let keycode = self.text.chars().next()?;
let is_apple = crate::is_apple_platform();
let move_mod = if is_apple {
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))
}
_ => (),
};
}
}
if is_apple && 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))
}
key_codes::Backspace => {
return Some(TextShortcut::DeleteToStartOfLine);
}
_ => (),
};
}
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,
}
}
}
}
pub enum StandardShortcut {
Copy,
Cut,
Paste,
SelectAll,
Find,
Save,
Print,
Undo,
Redo,
Refresh,
}
pub enum TextShortcut {
Move(TextCursorDirection),
DeleteForward,
DeleteBackward,
DeleteWordForward,
DeleteWordBackward,
DeleteToStartOfLine,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum KeyEventResult {
EventAccepted,
#[default]
EventIgnored,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum FocusEventResult {
FocusAccepted,
#[default]
FocusIgnored,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[repr(u8)]
pub enum FocusEvent {
FocusIn(FocusReason),
FocusOut(FocusReason),
}
#[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 {
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);
}
pub fn reset(&self) {
self.click_count.set(0);
self.click_count_time_stamp.replace(None);
}
pub fn check_repeat(&self, mouse_event: MouseEvent, click_interval: Duration) -> 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 < click_interval
&& button == self.click_button.get()
&& (position - self.click_position.get()).square_length() < 100 as _
{
self.click_count.set(self.click_count.get().wrapping_add(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
}
}
#[derive(Default)]
pub struct MouseInputState {
item_stack: Vec<(ItemWeak, InputEventFilterResult)>,
pub(crate) offset: LogicalPoint,
grabbed: bool,
pub(crate) drag_data: Option<DropEvent>,
delayed: Option<(crate::timers::Timer, MouseEvent)>,
delayed_exit_items: Vec<ItemWeak>,
}
impl MouseInputState {
fn top_item(&self) -> Option<ItemRc> {
self.item_stack.last().and_then(|x| x.0.upgrade())
}
pub fn top_item_including_delayed(&self) -> Option<ItemRc> {
self.delayed_exit_items.last().and_then(|x| x.upgrade()).or_else(|| self.top_item())
}
}
pub(crate) 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.clone());
};
let mut event = mouse_event.clone();
let mut intercept = false;
let mut invalid = false;
event.translate(-mouse_input_state.offset.to_vector());
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());
if window_adapter.renderer().supports_transformations() {
if let Some(inverse_transform) = item.inverse_children_transform() {
event.transform(inverse_transform);
}
}
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.clone());
}
let grabber = mouse_input_state.top_item().unwrap();
let input_result = grabber.borrow().as_ref().input_event(&event, window_adapter, &grabber);
match input_result {
InputEventResult::GrabMouse => None,
InputEventResult::StartDrag => {
mouse_input_state.grabbed = false;
let drag_area_item = grabber.downcast::<crate::items::DragArea>().unwrap();
mouse_input_state.drag_data = Some(DropEvent {
mime_type: drag_area_item.as_pin_ref().mime_type(),
data: drag_area_item.as_pin_ref().data(),
position: Default::default(),
});
None
}
_ => {
mouse_input_state.grabbed = false;
Some(
mouse_event
.position()
.map_or(MouseEvent::Exit, |position| MouseEvent::Moved { position }),
)
}
}
}
pub(crate) 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.is_some_and(|p| g.contains(p));
if let Some(p) = pos.as_mut() {
*p -= g.origin.to_vector();
if window_adapter.renderer().supports_transformations() {
if let Some(inverse_transform) = item.inverse_children_transform() {
*p = inverse_transform.transform_point(p.cast()).cast();
}
}
}
if !contains || clipped {
if item.borrow().as_ref().clips_children() {
clipped = true;
}
item.borrow().as_ref().input_event(&MouseEvent::Exit, window_adapter, &item);
} else if new_input_state.item_stack.get(idx).is_none_or(|(x, _)| *x != it.0) {
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);
}
}
}
}
pub fn process_mouse_input(
root: ItemRc,
mouse_event: &MouseEvent,
window_adapter: &Rc<dyn WindowAdapter>,
mouse_input_state: MouseInputState,
) -> MouseInputState {
let mut result =
MouseInputState { drag_data: mouse_input_state.drag_data.clone(), ..Default::default() };
let r = send_mouse_event_to_item(
mouse_event,
root.clone(),
window_adapter,
&mut result,
mouse_input_state.top_item().as_ref(),
false,
);
if mouse_input_state.delayed.is_some()
&& (!r.has_aborted()
|| Option::zip(result.item_stack.last(), mouse_input_state.item_stack.last())
.is_none_or(|(a, b)| a.0 != b.0))
{
return mouse_input_state;
}
send_exit_events(&mouse_input_state, &mut result, mouse_event.position(), window_adapter);
if let MouseEvent::Wheel { position, .. } = mouse_event {
if r.has_aborted() {
return process_mouse_input(
root,
&MouseEvent::Moved { position: *position },
window_adapter,
result,
);
}
}
result
}
pub(crate) fn process_delayed_event(
window_adapter: &Rc<dyn WindowAdapter>,
mut mouse_input_state: MouseInputState,
) -> MouseInputState {
let event = match mouse_input_state.delayed.take() {
Some(e) => e.1,
None => return mouse_input_state,
};
let top_item = match mouse_input_state.top_item() {
Some(i) => i,
None => return MouseInputState::default(),
};
let mut actual_visitor =
|component: &ItemTreeRc, index: u32, _: Pin<ItemRef>| -> VisitChildrenResult {
send_mouse_event_to_item(
&event,
ItemRc::new(component.clone(), index),
window_adapter,
&mut mouse_input_state,
Some(&top_item),
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.item_tree()).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,
last_top_item: Option<&ItemRc>,
ignore_delays: bool,
) -> VisitChildrenResult {
let item = item_rc.borrow();
let geom = item_rc.geometry();
let mut event_for_children = mouse_event.clone();
event_for_children.translate(-geom.origin.to_vector());
if window_adapter.renderer().supports_transformations() {
if let Some(inverse_transform) = item_rc.inverse_children_transform() {
event_for_children.transform(inverse_transform);
}
}
let filter_result = if mouse_event.position().is_some_and(|p| geom.contains(p))
|| item.as_ref().clips_children()
{
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::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,
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: &ItemTreeRc, index: u32, _: Pin<ItemRef>| -> VisitChildrenResult {
send_mouse_event_to_item(
&event_for_children,
ItemRc::new(component.clone(), index),
window_adapter,
result,
last_top_item,
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.item_tree()).as_ref().visit_children_item(
item_rc.index() as isize,
crate::item_tree::TraversalOrder::FrontToBack,
actual_visitor,
);
if r.has_aborted() {
return r;
}
};
let r = if ignore {
InputEventResult::EventIgnored
} else {
let mut event = mouse_event.clone();
event.translate(-geom.origin.to_vector());
if last_top_item.is_none_or(|x| *x != item_rc) {
event.set_click_count(0);
}
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)
}
InputEventResult::StartDrag => {
result.item_stack.last_mut().unwrap().1 =
InputEventFilterResult::ForwardAndInterceptGrab;
result.grabbed = false;
let drag_area_item = item_rc.downcast::<crate::items::DragArea>().unwrap();
result.drag_data = Some(DropEvent {
mime_type: drag_area_item.as_pin_ref().mime_type(),
data: drag_area_item.as_pin_ref().data(),
position: Default::default(),
});
VisitChildrenResult::abort(item_rc.index(), 0)
}
}
}
#[derive(FieldOffsets)]
#[repr(C)]
#[pin]
pub(crate) struct TextCursorBlinker {
cursor_visible: Property<bool>,
cursor_blink_timer: crate::timers::Timer,
}
impl TextCursorBlinker {
pub fn new() -> Pin<Rc<Self>> {
Rc::pin(Self {
cursor_visible: Property::new(true),
cursor_blink_timer: Default::default(),
})
}
pub fn set_binding(
instance: Pin<Rc<TextCursorBlinker>>,
prop: &Property<bool>,
cycle_duration: Duration,
) {
instance.as_ref().cursor_visible.set(true);
Self::start(&instance, cycle_duration);
prop.set_binding(move || {
TextCursorBlinker::FIELD_OFFSETS.cursor_visible.apply_pin(instance.as_ref()).get()
});
}
pub fn start(self: &Pin<Rc<Self>>, cycle_duration: Duration) {
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);
}
}
};
if !cycle_duration.is_zero() {
self.cursor_blink_timer.start(
crate::timers::TimerMode::Repeated,
cycle_duration / 2,
toggle_cursor,
);
}
}
}
pub fn stop(&self) {
self.cursor_blink_timer.stop()
}
}