docs/development/input-event-system.md
Note for AI coding assistants (agents): When to load this document: Working on
internal/core/input.rs,internal/core/item_focus.rs,internal/core/window.rsevent handling, mouse/keyboard/touch processing, or focus management. For general build commands and project structure, see/AGENTS.md.
Slint's input system handles mouse, touch, keyboard events and focus management. Events flow from the platform through the window to items in the item tree, with support for:
| File | Purpose |
|---|---|
internal/core/input.rs | MouseEvent, KeyEvent, event processing |
internal/core/item_focus.rs | Focus chain navigation |
internal/core/window.rs | Window-level event dispatch |
internal/core/items.rs | Item event handlers (input_event, etc.) |
pub enum MouseEvent {
/// Mouse/finger pressed
Pressed {
position: LogicalPoint,
button: PointerEventButton,
click_count: u8,
is_touch: bool,
},
/// Mouse/finger released
Released {
position: LogicalPoint,
button: PointerEventButton,
click_count: u8,
is_touch: bool,
},
/// Pointer moved
Moved { position: LogicalPoint, is_touch: bool },
/// Mouse wheel
Wheel { position: LogicalPoint, delta_x: Coord, delta_y: Coord },
/// Drag operation in progress over item
DragMove(DropEvent),
/// Drop occurred on item
Drop(DropEvent),
/// Mouse exited the item
Exit,
}
The ClickState tracks multi-clicks (double-click, triple-click):
pub struct ClickState {
click_count_time_stamp: Cell<Option<Instant>>,
click_count: Cell<u8>,
click_position: Cell<LogicalPoint>,
click_button: Cell<PointerEventButton>,
}
Logic:
click_interval of previous press, at same position, with same button → increment click_countclick_count is included in Press/Release eventsTracks the current state of mouse interaction:
pub struct MouseInputState {
/// Stack of items under cursor, with their filter results
item_stack: Vec<(ItemWeak, InputEventFilterResult)>,
/// Offset for popup positioning
pub(crate) offset: LogicalPoint,
/// True if an item has grabbed the mouse
grabbed: bool,
/// Active drag-drop data
pub(crate) drag_data: Option<DropEvent>,
/// Delayed event (for Flickable touch handling)
delayed: Option<(Timer, MouseEvent)>,
/// Items pending exit events
delayed_exit_items: Vec<ItemWeak>,
}
┌─────────────────┐
│ Platform │ (winit, Qt, etc.)
│ WindowEvent │
└────────┬────────┘
│
▼
┌─────────────────┐
│ WindowInner:: │ Click counting, modifier tracking
│ process_mouse_ │
│ input() │
└────────┬────────┘
│
▼
┌─────────────────┐
│ handle_mouse_ │ Check if item has grab
│ grab() │ If so, send directly to grabber
└────────┬────────┘
│ (if no grab)
▼
┌─────────────────┐
│ process_mouse_ │ Traverse item tree
│ input() │ front-to-back
└────────┬────────┘
│
▼
┌─────────────────┐
│ send_mouse_ │ For each item:
│ event_to_item()│ 1. filter_before_children
│ │ 2. recurse to children
│ │ 3. input_event
└─────────────────┘
Each item has two event handlers:
// Called before children process the event
fn input_event_filter_before_children(
&self,
event: &MouseEvent,
window_adapter: &Rc<dyn WindowAdapter>,
self_rc: &ItemRc,
) -> InputEventFilterResult;
// Called after children (unless filtered)
fn input_event(
&self,
event: &MouseEvent,
window_adapter: &Rc<dyn WindowAdapter>,
self_rc: &ItemRc,
) -> InputEventResult;
Controls how events are forwarded:
pub enum InputEventFilterResult {
/// Forward to children, then call input_event on self
ForwardEvent,
/// Forward to children, don't call input_event on self
ForwardAndIgnore,
/// Forward, but keep receiving events even if child grabs
ForwardAndInterceptGrab,
/// Don't forward to children, handle here
Intercept,
/// Delay forwarding (for touch scrolling detection)
DelayForwarding(u64), // milliseconds
}
Returned by input_event:
pub enum InputEventResult {
/// Event was handled
EventAccepted,
/// Event was not handled, continue propagation
EventIgnored,
/// Grab all future mouse events until release
GrabMouse,
/// Start drag-drop operation (DragArea only)
StartDrag,
}
When an item returns GrabMouse:
Intercept// In handle_mouse_grab()
if mouse_input_state.grabbed {
// Send event directly to grabber
let grabber = mouse_input_state.top_item().unwrap();
let result = grabber.input_event(&event, ...);
match result {
InputEventResult::GrabMouse => None, // Keep grab
_ => {
mouse_input_state.grabbed = false;
Some(MouseEvent::Moved { ... }) // Resume normal processing
}
}
}
Only DragArea items can start drags:
// DragArea returns StartDrag from input_event
InputEventResult::StartDrag => {
mouse_input_state.grabbed = false;
mouse_input_state.drag_data = Some(DropEvent {
mime_type: drag_area.mime_type(),
data: drag_area.data(),
position: Default::default(),
});
}
Items receive DragMove events:
MouseEvent::DragMove(DropEvent { mime_type, data, position })
Items return EventAccepted to indicate they can receive the drop.
When mouse is released during drag:
MouseEvent::Drop(DropEvent { mime_type, data, position })
pub struct KeyEvent {
pub text: SharedString, // Character or key code
pub modifiers: KeyboardModifiers, // Alt, Ctrl, Shift, Meta
pub event_type: KeyEventType,
// ... IME composition fields
}
pub enum KeyEventType {
KeyPressed,
KeyReleased,
UpdateComposition, // IME pre-edit
CommitComposition, // IME finalized
}
pub struct KeyboardModifiers {
pub alt: bool,
pub control: bool,
pub meta: bool,
pub shift: bool,
}
Special keys are encoded as Unicode private-use characters:
pub mod key_codes {
pub const Backspace: char = '\u{0008}';
pub const Tab: char = '\u{0009}';
pub const Return: char = '\u{000D}';
pub const Escape: char = '\u{001B}';
pub const LeftArrow: char = '\u{F702}';
pub const RightArrow: char = '\u{F703}';
pub const UpArrow: char = '\u{F700}';
pub const DownArrow: char = '\u{F701}';
// ... more in key_codes module
}
Platform KeyEvent
│
▼
WindowInner::process_key_input()
│
├── Update modifier state
│
├── If popup active → send to popup
│
└── Send to focus item
│
├── Item handles → KeyEventResult::EventAccepted
│
└── Item ignores → bubble up to parent
│
└── Continue until handled or root
impl KeyEvent {
/// Check for standard shortcuts (Ctrl+C, etc.)
pub fn shortcut(&self) -> Option<StandardShortcut>;
/// Check for text editing shortcuts
pub fn text_shortcut(&self) -> Option<TextShortcut>;
}
pub enum StandardShortcut {
Copy, Cut, Paste, SelectAll, Find, Save, Print, Undo, Redo, Refresh,
}
pub enum TextShortcut {
Move(TextCursorDirection),
DeleteForward, DeleteBackward,
DeleteWordForward, DeleteWordBackward,
DeleteToStartOfLine,
}
The window tracks the currently focused item:
// In WindowInner
focus_item: RefCell<crate::item_tree::ItemWeak>,
pub fn set_focus_item(
&self,
new_focus_item: &ItemRc,
set_focus: bool, // true = focus, false = clear focus
reason: FocusReason,
)
pub enum FocusReason {
/// Focus changed via click
PointerAction,
/// Focus changed via Tab key
TabNavigation,
/// Focus changed via code (forward-focus, etc.)
Other,
}
Items receive focus events:
pub enum FocusEvent {
FocusIn(FocusReason),
FocusOut(FocusReason),
}
pub enum FocusEventResult {
FocusAccepted,
FocusIgnored,
}
Tab/Shift+Tab navigation traverses the item tree:
// Forward: depth-first, children before siblings
fn default_next_in_local_focus_chain(index: u32, item_tree: &ItemTreeNodeArray) -> Option<u32> {
// First try first child
if let Some(child) = item_tree.first_child(index) {
return Some(child);
}
// Then try next sibling, or parent's next sibling
step_out_of_node(index, item_tree)
}
// Backward: reverse of forward
fn default_previous_in_local_focus_chain(index: u32, item_tree: &ItemTreeNodeArray) -> Option<u32> {
// Try previous sibling's deepest descendant
if let Some(previous) = item_tree.previous_sibling(index) {
Some(step_into_node(item_tree, previous))
} else {
// Or parent
item_tree.parent(index)
}
}
Items can delegate focus via forward-focus property:
component MyInput {
forward-focus: input;
input := TextInput { }
}
For text input cursor animation:
pub struct TextCursorBlinker {
cursor_visible: Property<bool>,
cursor_blink_timer: Timer,
}
impl TextCursorBlinker {
/// Create binding that toggles visibility
pub fn set_binding(
instance: Pin<Rc<TextCursorBlinker>>,
prop: &Property<bool>,
cycle_duration: Duration,
);
/// Start blinking
pub fn start(self: &Pin<Rc<Self>>, cycle_duration: Duration);
/// Stop blinking (e.g., window loses focus)
pub fn stop(&self);
}
For touch interfaces, Flickable delays events to distinguish scroll from tap:
InputEventFilterResult::DelayForwarding(duration_ms)
Flow:
DelayForwarding(150) on touch pressfn input_event(
self: Pin<&Self>,
event: &MouseEvent,
_window_adapter: &Rc<dyn WindowAdapter>,
self_rc: &ItemRc,
) -> InputEventResult {
match event {
MouseEvent::Pressed { button: PointerEventButton::Left, .. } => {
// Handle press
InputEventResult::GrabMouse // Capture further events
}
MouseEvent::Released { .. } => {
// Handle release
InputEventResult::EventAccepted
}
MouseEvent::Moved { position, .. } => {
// Handle move (only received if grabbed)
InputEventResult::GrabMouse
}
_ => InputEventResult::EventIgnored,
}
}
fn input_event_filter_before_children(
self: Pin<&Self>,
event: &MouseEvent,
_window_adapter: &Rc<dyn WindowAdapter>,
_self_rc: &ItemRc,
) -> InputEventFilterResult {
if self.should_intercept(event) {
InputEventFilterResult::Intercept
} else {
InputEventFilterResult::ForwardEvent
}
}
fn focus_event(
self: Pin<&Self>,
event: &FocusEvent,
_window_adapter: &Rc<dyn WindowAdapter>,
_self_rc: &ItemRc,
) -> FocusEventResult {
match event {
FocusEvent::FocusIn(_) => {
// Start cursor blink, etc.
FocusEventResult::FocusAccepted
}
FocusEvent::FocusOut(_) => {
// Stop cursor blink, etc.
FocusEventResult::FocusAccepted
}
}
}
| Issue | Cause | Solution |
|---|---|---|
| Item not receiving events | Not in event path | Check item geometry, clips_children |
| Click not working | Event being grabbed | Check for GrabMouse returns |
| Focus not moving | forward-focus loop | Check focus delegation chain |
| Double-click not detected | Click interval too short | Check platform click_interval |
| Touch scroll not working | DelayForwarding not used | Check Flickable setup |
// Add logging in input_event
fn input_event(...) -> InputEventResult {
eprintln!("input_event: {:?} on {:?}", event, self_rc.index());
// ...
}
// Get current focus item
let focus = window.focus_item();
if let Some(item) = focus.upgrade() {
println!("Focused: {:?}", item.index());
}
# Run input handling tests
cargo test -p i-slint-core input
# Run focus tests
cargo test -p i-slint-core item_focus
# Run with specific test
cargo test -p i-slint-core test_focus_chain