Three root causes, all fixed and verified on the Pixel 9 Pro: 1. Present phase (backend + renderer): VSYNC_LOCKED_RENDER defers the render to the vsync wake; RENDER_AHEAD presents the previous frame at the start of render() (phase = wake + ~1.1 ms) and holds the new frame one slot. Crash fix: a held wgpu frame must be PRESENTED, never merely dropped, before the next acquire (AlreadyAcquired panic at launch otherwise); discard_pending_frame presents too. 2. Full-res prefetch churn (src/lib.rs): the 8-slot full cache cycled through a ~20-cell viewport forever (137 decodes/20 s) and each eviction free()'d a 50 MB Vec on the UI thread (20-52 ms CPU spikes, ~7/s = the residual SLOWEV NonMain jank). Fix: 60 s tombstone cooldown on evicted paths (idle prefetch skips, urgent tap bypasses) + off-thread reaper for the freed blobs (insert: 21 ms -> 4 us). 3. (earlier) Skia final flush + Mailbox present mode (vendor, M3). Instrumentation (all behind --features perf): OTHR/INVK/INVK_TAG/FLCK lines for cross-thread closure timing and push-site tagging. Warm measured swipe, 8580-file grid: cadence median 8.33 ms (exact 120 Hz), p99 15.5 ms, 5/552 gaps >16.7 ms (0.9%, max 28.3 ms, zero >33 ms); PRES phase median ~994 us, zero late frames. Docs updated (docs/BUGS/scroll-jank-120hz.md, full scorecard + line reference). Also: vendored i-slint-core + wgpu-hal (vkshim interposition patch), vkshim profiling tooling, backend made a non-optional dep, debuggable flag for simpleperf (REMOVE before shipping).
1336 lines
46 KiB
Rust
1336 lines
46 KiB
Rust
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
|
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
|
|
|
use super::{
|
|
EventResult, FocusReasonArg, Item, ItemConsts, ItemRc, ItemRendererRef, KeyEventArg,
|
|
PointerEvent, PointerEventArg, PointerEventButton, PointerEventKind, PointerScrollEvent,
|
|
PointerScrollEventArg, RenderingResult, VoidArg,
|
|
};
|
|
use crate::api::LogicalPosition;
|
|
use crate::input::{
|
|
FocusEvent, FocusEventResult, FocusReason, InputEventFilterResult, InputEventResult,
|
|
InternalKeyEvent, KeyEventResult, KeyEventType, Keys, MouseEvent,
|
|
};
|
|
use crate::item_rendering::CachedRenderingData;
|
|
use crate::items::{ItemTreeVTable, MouseCursorInner};
|
|
use crate::layout::{LayoutInfo, Orientation};
|
|
use crate::lengths::{LogicalLength, LogicalPoint, LogicalRect, LogicalSize, PointLengths};
|
|
use crate::properties::PropertyTracker;
|
|
#[cfg(feature = "rtti")]
|
|
use crate::rtti::*;
|
|
use crate::window::{WindowAdapter, WindowInner};
|
|
use crate::{Callback, Coord, Property};
|
|
use alloc::{boxed::Box, rc::Rc, vec::Vec};
|
|
use const_field_offset::FieldOffsets;
|
|
use core::cell::Cell;
|
|
use core::pin::Pin;
|
|
use i_slint_core_macros::*;
|
|
use vtable::{VRcMapped, VWeakMapped};
|
|
|
|
/// The implementation of the `TouchArea` element
|
|
#[repr(C)]
|
|
#[derive(FieldOffsets, SlintElement, Default)]
|
|
#[pin]
|
|
pub struct TouchArea {
|
|
pub enabled: Property<bool>,
|
|
/// FIXME: We should annotate this as an "output" property.
|
|
pub pressed: Property<bool>,
|
|
pub has_hover: Property<bool>,
|
|
/// FIXME: there should be just one property for the point instead of two.
|
|
/// Could even be merged with pressed in a `Property<Option<Point>>` (of course, in the
|
|
/// implementation item only, for the compiler it would stay separate properties)
|
|
pub pressed_x: Property<LogicalLength>,
|
|
pub pressed_y: Property<LogicalLength>,
|
|
/// FIXME: should maybe be as parameter to the mouse event instead. Or at least just one property
|
|
pub mouse_x: Property<LogicalLength>,
|
|
pub mouse_y: Property<LogicalLength>,
|
|
pub mouse_cursor: Property<MouseCursorInner>,
|
|
pub clicked: Callback<VoidArg>,
|
|
pub double_clicked: Callback<VoidArg>,
|
|
pub moved: Callback<VoidArg>,
|
|
pub pointer_event: Callback<PointerEventArg>,
|
|
pub scroll_event: Callback<PointerScrollEventArg, EventResult>,
|
|
/// FIXME: remove this
|
|
pub cached_rendering_data: CachedRenderingData,
|
|
/// true when we are currently grabbing the mouse
|
|
grabbed: Cell<bool>,
|
|
}
|
|
|
|
impl Item for TouchArea {
|
|
fn init(self: Pin<&Self>, _self_rc: &ItemRc) {}
|
|
|
|
fn deinit(self: Pin<&Self>, _window_adapter: &Rc<dyn WindowAdapter>) {}
|
|
|
|
fn layout_info(
|
|
self: Pin<&Self>,
|
|
_orientation: Orientation,
|
|
_cross_axis_constraint: Coord,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
) -> LayoutInfo {
|
|
LayoutInfo { stretch: 1., ..LayoutInfo::default() }
|
|
}
|
|
|
|
fn input_event_filter_before_children(
|
|
self: Pin<&Self>,
|
|
event: &MouseEvent,
|
|
window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
cursor: &mut MouseCursorInner,
|
|
) -> InputEventFilterResult {
|
|
if !self.enabled() {
|
|
self.has_hover.set(false);
|
|
if self.grabbed.replace(false) {
|
|
self.pressed.set(false);
|
|
Self::FIELD_OFFSETS.pointer_event().apply_pin(self).call(&(PointerEvent {
|
|
button: PointerEventButton::Other,
|
|
kind: PointerEventKind::Cancel,
|
|
modifiers: window_adapter.window().0.context().0.modifiers.get().into(),
|
|
touch_finger_id: 0,
|
|
},));
|
|
}
|
|
return InputEventFilterResult::ForwardAndIgnore;
|
|
}
|
|
if matches!(event, MouseEvent::DragMove { .. } | MouseEvent::Drop { .. }) {
|
|
// Someone else has the grab, don't handle hover
|
|
return InputEventFilterResult::ForwardAndIgnore;
|
|
}
|
|
if let Some(pos) = event.position() {
|
|
Self::FIELD_OFFSETS.mouse_x().apply_pin(self).set(pos.x_length());
|
|
Self::FIELD_OFFSETS.mouse_y().apply_pin(self).set(pos.y_length());
|
|
}
|
|
let hovering = !matches!(event, MouseEvent::Exit);
|
|
Self::FIELD_OFFSETS.has_hover().apply_pin(self).set(hovering);
|
|
if hovering {
|
|
*cursor = self.mouse_cursor();
|
|
}
|
|
InputEventFilterResult::ForwardAndInterceptGrab
|
|
}
|
|
|
|
fn input_event(
|
|
self: Pin<&Self>,
|
|
event: &MouseEvent,
|
|
window_adapter: &Rc<dyn WindowAdapter>,
|
|
self_rc: &ItemRc,
|
|
_: &mut MouseCursorInner,
|
|
) -> InputEventResult {
|
|
if matches!(event, MouseEvent::Exit) {
|
|
Self::FIELD_OFFSETS.has_hover().apply_pin(self).set(false);
|
|
}
|
|
if !self.enabled() {
|
|
return InputEventResult::EventIgnored;
|
|
}
|
|
match event {
|
|
MouseEvent::Pressed { position, button, touch_finger_id, .. } => {
|
|
self.grabbed.set(true);
|
|
if *button == PointerEventButton::Left {
|
|
Self::FIELD_OFFSETS.pressed_x().apply_pin(self).set(position.x_length());
|
|
Self::FIELD_OFFSETS.pressed_y().apply_pin(self).set(position.y_length());
|
|
Self::FIELD_OFFSETS.pressed().apply_pin(self).set(true);
|
|
}
|
|
Self::FIELD_OFFSETS.pointer_event().apply_pin(self).call(&(PointerEvent {
|
|
button: *button,
|
|
kind: PointerEventKind::Down,
|
|
modifiers: window_adapter.window().0.context().0.modifiers.get().into(),
|
|
touch_finger_id: *touch_finger_id,
|
|
},));
|
|
|
|
InputEventResult::GrabMouse
|
|
}
|
|
MouseEvent::Exit => {
|
|
Self::FIELD_OFFSETS.pressed().apply_pin(self).set(false);
|
|
if self.grabbed.replace(false) {
|
|
Self::FIELD_OFFSETS.pointer_event().apply_pin(self).call(&(PointerEvent {
|
|
button: PointerEventButton::Other,
|
|
kind: PointerEventKind::Cancel,
|
|
modifiers: window_adapter.window().0.context().0.modifiers.get().into(),
|
|
touch_finger_id: 0,
|
|
},));
|
|
}
|
|
|
|
InputEventResult::EventAccepted
|
|
}
|
|
|
|
MouseEvent::Released { button, position, click_count, touch_finger_id } => {
|
|
let geometry = self_rc.geometry();
|
|
if *button == PointerEventButton::Left
|
|
&& LogicalRect::new(LogicalPoint::default(), geometry.size).contains(*position)
|
|
&& self.pressed()
|
|
{
|
|
Self::FIELD_OFFSETS.clicked().apply_pin(self).call(&());
|
|
if (click_count % 2) == 1 {
|
|
Self::FIELD_OFFSETS.double_clicked().apply_pin(self).call(&())
|
|
}
|
|
}
|
|
|
|
self.grabbed.set(false);
|
|
if *button == PointerEventButton::Left {
|
|
Self::FIELD_OFFSETS.pressed().apply_pin(self).set(false);
|
|
}
|
|
Self::FIELD_OFFSETS.pointer_event().apply_pin(self).call(&(PointerEvent {
|
|
button: *button,
|
|
kind: PointerEventKind::Up,
|
|
modifiers: window_adapter.window().0.context().0.modifiers.get().into(),
|
|
touch_finger_id: *touch_finger_id,
|
|
},));
|
|
|
|
InputEventResult::EventAccepted
|
|
}
|
|
MouseEvent::Moved { touch_finger_id, .. } => {
|
|
Self::FIELD_OFFSETS.pointer_event().apply_pin(self).call(&(PointerEvent {
|
|
button: PointerEventButton::Other,
|
|
kind: PointerEventKind::Move,
|
|
modifiers: window_adapter.window().0.context().0.modifiers.get().into(),
|
|
touch_finger_id: *touch_finger_id,
|
|
},));
|
|
if self.grabbed.get() {
|
|
Self::FIELD_OFFSETS.moved().apply_pin(self).call(&());
|
|
InputEventResult::GrabMouse
|
|
} else {
|
|
InputEventResult::EventAccepted
|
|
}
|
|
}
|
|
MouseEvent::Wheel { delta_x, delta_y, .. } => {
|
|
let modifiers = window_adapter.window().0.context().0.modifiers.get().into();
|
|
let r = Self::FIELD_OFFSETS.scroll_event().apply_pin(self).call(&(
|
|
PointerScrollEvent { delta_x: *delta_x, delta_y: *delta_y, modifiers },
|
|
));
|
|
if self.grabbed.get() {
|
|
InputEventResult::GrabMouse
|
|
} else {
|
|
match r {
|
|
EventResult::Reject => {
|
|
// We are ignoring the event, so we will be removed from the item_stack,
|
|
// therefore we must remove the has_hover flag as there might be a scroll under us.
|
|
// It will be put back later.
|
|
Self::FIELD_OFFSETS.has_hover().apply_pin(self).set(false);
|
|
InputEventResult::EventIgnored
|
|
}
|
|
EventResult::Accept => InputEventResult::EventAccepted,
|
|
}
|
|
}
|
|
}
|
|
MouseEvent::PinchGesture { .. } | MouseEvent::RotationGesture { .. } => {
|
|
InputEventResult::EventIgnored
|
|
}
|
|
MouseEvent::DragMove { .. } | MouseEvent::Drop { .. } => InputEventResult::EventIgnored,
|
|
}
|
|
}
|
|
|
|
fn capture_key_event(
|
|
self: Pin<&Self>,
|
|
_: &InternalKeyEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
) -> KeyEventResult {
|
|
KeyEventResult::EventIgnored
|
|
}
|
|
|
|
fn key_event(
|
|
self: Pin<&Self>,
|
|
_: &InternalKeyEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
) -> KeyEventResult {
|
|
KeyEventResult::EventIgnored
|
|
}
|
|
|
|
fn focus_event(
|
|
self: Pin<&Self>,
|
|
_: &FocusEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
) -> FocusEventResult {
|
|
FocusEventResult::FocusIgnored
|
|
}
|
|
|
|
fn render(
|
|
self: Pin<&Self>,
|
|
_backend: &mut ItemRendererRef,
|
|
_self_rc: &ItemRc,
|
|
_size: LogicalSize,
|
|
) -> RenderingResult {
|
|
RenderingResult::ContinueRenderingChildren
|
|
}
|
|
|
|
fn bounding_rect(
|
|
self: core::pin::Pin<&Self>,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
mut geometry: LogicalRect,
|
|
) -> LogicalRect {
|
|
geometry.size = LogicalSize::zero();
|
|
geometry
|
|
}
|
|
|
|
fn clips_children(self: core::pin::Pin<&Self>) -> bool {
|
|
false
|
|
}
|
|
}
|
|
|
|
impl ItemConsts for TouchArea {
|
|
const cached_rendering_data_offset: const_field_offset::FieldOffset<
|
|
TouchArea,
|
|
CachedRenderingData,
|
|
> = TouchArea::FIELD_OFFSETS.cached_rendering_data().as_unpinned_projection();
|
|
}
|
|
|
|
impl ItemConsts for KeyBinding {
|
|
const cached_rendering_data_offset: const_field_offset::FieldOffset<
|
|
KeyBinding,
|
|
CachedRenderingData,
|
|
> = KeyBinding::FIELD_OFFSETS.cached_rendering_data().as_unpinned_projection();
|
|
}
|
|
|
|
/// The implementation of the `WindowMoveArea` element
|
|
#[repr(C)]
|
|
#[derive(FieldOffsets, Default, SlintElement)]
|
|
#[pin]
|
|
pub struct WindowMoveArea {
|
|
pub enabled: Property<bool>,
|
|
pressed: Cell<bool>,
|
|
pressed_position: Cell<LogicalPoint>,
|
|
pub cached_rendering_data: CachedRenderingData,
|
|
}
|
|
|
|
impl Item for WindowMoveArea {
|
|
fn init(self: Pin<&Self>, _self_rc: &ItemRc) {}
|
|
|
|
fn deinit(self: Pin<&Self>, _window_adapter: &Rc<dyn WindowAdapter>) {}
|
|
|
|
fn layout_info(
|
|
self: Pin<&Self>,
|
|
_: Orientation,
|
|
_cross_axis_constraint: Coord,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
) -> LayoutInfo {
|
|
LayoutInfo { stretch: 1., ..LayoutInfo::default() }
|
|
}
|
|
|
|
fn input_event_filter_before_children(
|
|
self: Pin<&Self>,
|
|
event: &MouseEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
_: &mut MouseCursorInner,
|
|
) -> InputEventFilterResult {
|
|
if !self.enabled() {
|
|
self.pressed.set(false);
|
|
return InputEventFilterResult::ForwardAndIgnore;
|
|
}
|
|
super::drag_n_drop::press_drag_filter(&self.pressed, &self.pressed_position, event)
|
|
}
|
|
|
|
fn input_event(
|
|
self: Pin<&Self>,
|
|
event: &MouseEvent,
|
|
window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
_: &mut MouseCursorInner,
|
|
) -> InputEventResult {
|
|
match event {
|
|
MouseEvent::Pressed { .. } => InputEventResult::EventAccepted,
|
|
MouseEvent::Exit | MouseEvent::Released { .. } => {
|
|
self.pressed.set(false);
|
|
InputEventResult::EventIgnored
|
|
}
|
|
MouseEvent::Moved { position, .. } => {
|
|
if !self.pressed.get() || !self.enabled() {
|
|
return InputEventResult::EventIgnored;
|
|
}
|
|
if super::drag_n_drop::exceeds_drag_threshold(
|
|
self.pressed_position.get(),
|
|
*position,
|
|
) {
|
|
self.pressed.set(false);
|
|
if let Some(internal) = window_adapter.internal(crate::InternalToken) {
|
|
internal.start_window_move();
|
|
}
|
|
}
|
|
InputEventResult::EventAccepted
|
|
}
|
|
MouseEvent::Wheel { .. } => InputEventResult::EventIgnored,
|
|
MouseEvent::PinchGesture { .. } | MouseEvent::RotationGesture { .. } => {
|
|
InputEventResult::EventIgnored
|
|
}
|
|
MouseEvent::DragMove { .. } | MouseEvent::Drop { .. } => InputEventResult::EventIgnored,
|
|
}
|
|
}
|
|
|
|
fn capture_key_event(
|
|
self: Pin<&Self>,
|
|
_: &InternalKeyEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
) -> KeyEventResult {
|
|
KeyEventResult::EventIgnored
|
|
}
|
|
|
|
fn key_event(
|
|
self: Pin<&Self>,
|
|
_: &InternalKeyEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
) -> KeyEventResult {
|
|
KeyEventResult::EventIgnored
|
|
}
|
|
|
|
fn focus_event(
|
|
self: Pin<&Self>,
|
|
_: &FocusEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
) -> FocusEventResult {
|
|
FocusEventResult::FocusIgnored
|
|
}
|
|
|
|
fn render(
|
|
self: Pin<&Self>,
|
|
_: &mut ItemRendererRef,
|
|
_self_rc: &ItemRc,
|
|
_size: LogicalSize,
|
|
) -> RenderingResult {
|
|
RenderingResult::ContinueRenderingChildren
|
|
}
|
|
|
|
fn bounding_rect(
|
|
self: core::pin::Pin<&Self>,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
mut geometry: LogicalRect,
|
|
) -> LogicalRect {
|
|
geometry.size = LogicalSize::zero();
|
|
geometry
|
|
}
|
|
|
|
fn clips_children(self: core::pin::Pin<&Self>) -> bool {
|
|
false
|
|
}
|
|
}
|
|
|
|
impl ItemConsts for WindowMoveArea {
|
|
const cached_rendering_data_offset: const_field_offset::FieldOffset<
|
|
WindowMoveArea,
|
|
CachedRenderingData,
|
|
> = WindowMoveArea::FIELD_OFFSETS.cached_rendering_data().as_unpinned_projection();
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(FieldOffsets, Default, SlintElement)]
|
|
#[pin]
|
|
pub struct KeyBinding {
|
|
pub keys: Property<Keys>,
|
|
pub enabled: Property<bool>,
|
|
pub activated: Callback<VoidArg>,
|
|
pub cached_rendering_data: CachedRenderingData,
|
|
}
|
|
|
|
impl Item for KeyBinding {
|
|
fn init(self: Pin<&Self>, _self_rc: &ItemRc) {}
|
|
|
|
fn deinit(self: Pin<&Self>, _window_adapter: &Rc<dyn WindowAdapter>) {}
|
|
|
|
fn layout_info(
|
|
self: Pin<&Self>,
|
|
_orientation: crate::items::Orientation,
|
|
_cross_axis_constraint: Coord,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
) -> crate::layout::LayoutInfo {
|
|
Default::default()
|
|
}
|
|
|
|
fn input_event_filter_before_children(
|
|
self: Pin<&Self>,
|
|
_: &crate::input::MouseEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
_: &mut crate::cursor::MouseCursorInner,
|
|
) -> crate::input::InputEventFilterResult {
|
|
Default::default()
|
|
}
|
|
|
|
fn input_event(
|
|
self: Pin<&Self>,
|
|
_: &crate::input::MouseEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
_: &mut crate::cursor::MouseCursorInner,
|
|
) -> crate::input::InputEventResult {
|
|
Default::default()
|
|
}
|
|
|
|
fn capture_key_event(
|
|
self: Pin<&Self>,
|
|
_: &InternalKeyEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
) -> crate::input::KeyEventResult {
|
|
crate::input::KeyEventResult::EventIgnored
|
|
}
|
|
|
|
fn key_event(
|
|
self: Pin<&Self>,
|
|
_: &InternalKeyEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
) -> crate::input::KeyEventResult {
|
|
Default::default()
|
|
}
|
|
|
|
fn focus_event(
|
|
self: Pin<&Self>,
|
|
_: &crate::input::FocusEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
) -> crate::input::FocusEventResult {
|
|
Default::default()
|
|
}
|
|
|
|
fn render(
|
|
self: Pin<&Self>,
|
|
_backend: &mut &mut dyn crate::item_rendering::ItemRenderer,
|
|
_self_rc: &ItemRc,
|
|
_size: crate::lengths::LogicalSize,
|
|
) -> crate::items::RenderingResult {
|
|
Default::default()
|
|
}
|
|
|
|
fn bounding_rect(
|
|
self: Pin<&Self>,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
geometry: crate::lengths::LogicalRect,
|
|
) -> crate::lengths::LogicalRect {
|
|
geometry
|
|
}
|
|
|
|
fn clips_children(self: core::pin::Pin<&Self>) -> bool {
|
|
false
|
|
}
|
|
}
|
|
|
|
/// An optimized ShortcutList that is only initialized when it is
|
|
/// first accessed.
|
|
#[repr(C)]
|
|
#[derive(Default)] // results in a null pointer, which we will initialize on first access
|
|
pub struct MaybeKeyBindingList(Cell<*const KeyBindingList>);
|
|
|
|
impl MaybeKeyBindingList {
|
|
fn ensure_init(&self) {
|
|
// This would be a race condition in Multi-threaded code, but
|
|
// this type isn't Sync, so this function cannot race with another thread.
|
|
if self.0.get().is_null() {
|
|
self.0.set(Box::leak(Box::default()));
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Drop for MaybeKeyBindingList {
|
|
fn drop(&mut self) {
|
|
let ptr = self.0.replace(core::ptr::null());
|
|
if !ptr.is_null() {
|
|
// SAFETY: Must be a pointer returned by `Box::leak`, which is guaranteed by `ensure_init`.
|
|
drop(unsafe { Box::from_raw(ptr as *mut KeyBindingList) });
|
|
}
|
|
}
|
|
}
|
|
|
|
impl MaybeKeyBindingList {
|
|
fn deref_pin(self: Pin<&Self>) -> Pin<&KeyBindingList> {
|
|
self.ensure_init();
|
|
// SAFETY: Must be non-null and properly aligned, which is guaranteed by `ensure_init`.
|
|
unsafe { Pin::new_unchecked(&*self.get_ref().0.get()) }
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
#[pin_project::pin_project]
|
|
pub struct KeyBindingList {
|
|
found: core::cell::RefCell<Vec<VWeakMapped<ItemTreeVTable, KeyBinding>>>,
|
|
#[pin]
|
|
property_tracker: PropertyTracker,
|
|
}
|
|
|
|
/// A runtime item that exposes key events
|
|
#[repr(C)]
|
|
#[derive(FieldOffsets, Default, SlintElement)]
|
|
#[pin]
|
|
pub struct FocusScope {
|
|
pub enabled: Property<bool>,
|
|
pub has_focus: Property<bool>,
|
|
pub focus_on_click: Property<bool>,
|
|
pub focus_on_tab_navigation: Property<bool>,
|
|
pub key_pressed: Callback<KeyEventArg, EventResult>,
|
|
pub key_released: Callback<KeyEventArg, EventResult>,
|
|
pub capture_key_pressed: Callback<KeyEventArg, EventResult>,
|
|
pub capture_key_released: Callback<KeyEventArg, EventResult>,
|
|
pub focus_changed_event: Callback<FocusReasonArg>,
|
|
pub focus_gained: Callback<FocusReasonArg>,
|
|
pub focus_lost: Callback<FocusReasonArg>,
|
|
pub key_bindings: MaybeKeyBindingList,
|
|
/// FIXME: remove this
|
|
pub cached_rendering_data: CachedRenderingData,
|
|
}
|
|
|
|
impl FocusScope {
|
|
fn visit_enabled_key_bindings<R>(
|
|
self: Pin<&Self>,
|
|
self_rc: &ItemRc,
|
|
mut fun: impl FnMut(&VRcMapped<ItemTreeVTable, KeyBinding>) -> Option<R>,
|
|
) -> Option<R> {
|
|
let list = Self::FIELD_OFFSETS.key_bindings().apply_pin(self);
|
|
let list = list.deref_pin();
|
|
|
|
list.project_ref().property_tracker.evaluate_if_dirty(|| {
|
|
let mut found = list.found.borrow_mut();
|
|
found.clear();
|
|
|
|
let mut next = self_rc.first_child();
|
|
while let Some(child) = next {
|
|
if let Some(key_binding) = ItemRc::downcast::<KeyBinding>(&child)
|
|
&& key_binding.as_pin_ref().enabled()
|
|
{
|
|
found.push(VRcMapped::downgrade(&key_binding));
|
|
}
|
|
next = child.next_sibling();
|
|
}
|
|
});
|
|
|
|
let list = list.found.borrow();
|
|
for key_binding in &*list {
|
|
let Some(shortcut) = key_binding.upgrade() else {
|
|
crate::debug_log!("Warning: Found a dropped KeyBinding!");
|
|
continue;
|
|
};
|
|
if let Some(result) = fun(&shortcut) {
|
|
return Some(result);
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
/// Returns the first matching key binding and whether there are multiple matches.
|
|
fn key_binding_for_event(
|
|
self: Pin<&Self>,
|
|
self_rc: &ItemRc,
|
|
inter_key_event: &InternalKeyEvent,
|
|
) -> Option<(VRcMapped<ItemTreeVTable, KeyBinding>, bool)> {
|
|
let mut first_match = None;
|
|
|
|
let ambiguous = self.visit_enabled_key_bindings(self_rc, |key_binding| {
|
|
let keys = key_binding.as_pin_ref().keys();
|
|
if keys.matches(&inter_key_event.key_event) {
|
|
match &first_match {
|
|
Some(key_binding) => {
|
|
return Some(VRcMapped::clone(key_binding));
|
|
}
|
|
None => {
|
|
first_match = Some(VRcMapped::clone(key_binding));
|
|
}
|
|
};
|
|
}
|
|
None
|
|
});
|
|
|
|
first_match.map(|binding| (VRcMapped::clone(&binding), ambiguous.is_some()))
|
|
}
|
|
}
|
|
|
|
impl Item for FocusScope {
|
|
fn init(self: Pin<&Self>, _self_rc: &ItemRc) {}
|
|
|
|
fn deinit(self: Pin<&Self>, _window_adapter: &Rc<dyn WindowAdapter>) {}
|
|
|
|
fn layout_info(
|
|
self: Pin<&Self>,
|
|
_orientation: Orientation,
|
|
_cross_axis_constraint: Coord,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
) -> LayoutInfo {
|
|
LayoutInfo { stretch: 1., ..LayoutInfo::default() }
|
|
}
|
|
|
|
fn input_event_filter_before_children(
|
|
self: Pin<&Self>,
|
|
_: &MouseEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
_: &mut MouseCursorInner,
|
|
) -> InputEventFilterResult {
|
|
InputEventFilterResult::ForwardEvent
|
|
}
|
|
|
|
fn input_event(
|
|
self: Pin<&Self>,
|
|
event: &MouseEvent,
|
|
window_adapter: &Rc<dyn WindowAdapter>,
|
|
self_rc: &ItemRc,
|
|
_: &mut MouseCursorInner,
|
|
) -> InputEventResult {
|
|
if self.enabled()
|
|
&& self.focus_on_click()
|
|
&& matches!(event, MouseEvent::Pressed { .. })
|
|
&& !self.has_focus()
|
|
{
|
|
WindowInner::from_pub(window_adapter.window()).set_focus_item(
|
|
self_rc,
|
|
true,
|
|
FocusReason::PointerClick,
|
|
);
|
|
InputEventResult::EventAccepted
|
|
} else {
|
|
InputEventResult::EventIgnored
|
|
}
|
|
}
|
|
|
|
fn capture_key_event(
|
|
self: Pin<&Self>,
|
|
event: &InternalKeyEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
) -> KeyEventResult {
|
|
let r = match event.event_type {
|
|
KeyEventType::KeyPressed => Self::FIELD_OFFSETS
|
|
.capture_key_pressed()
|
|
.apply_pin(self)
|
|
.call(&(event.key_event.clone(),)),
|
|
KeyEventType::KeyReleased => Self::FIELD_OFFSETS
|
|
.capture_key_released()
|
|
.apply_pin(self)
|
|
.call(&(event.key_event.clone(),)),
|
|
KeyEventType::UpdateComposition | KeyEventType::CommitComposition => {
|
|
EventResult::Reject
|
|
}
|
|
};
|
|
match r {
|
|
EventResult::Accept => KeyEventResult::EventAccepted,
|
|
EventResult::Reject => KeyEventResult::EventIgnored,
|
|
}
|
|
}
|
|
|
|
fn key_event(
|
|
self: Pin<&Self>,
|
|
event: &InternalKeyEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
self_rc: &ItemRc,
|
|
) -> KeyEventResult {
|
|
let r = match event.event_type {
|
|
KeyEventType::KeyPressed => {
|
|
if let Some((key_binding, ambiguous)) = self.key_binding_for_event(self_rc, event) {
|
|
if ambiguous {
|
|
let keys = KeyBinding::FIELD_OFFSETS
|
|
.keys()
|
|
.apply_pin(key_binding.as_pin_ref())
|
|
.get();
|
|
crate::debug_log!(
|
|
"Warning: Multiple matching KeyBinding elements for keys {:?}!",
|
|
keys
|
|
);
|
|
}
|
|
KeyBinding::FIELD_OFFSETS
|
|
.activated()
|
|
.apply_pin(key_binding.as_pin_ref())
|
|
.call(&());
|
|
EventResult::Accept
|
|
} else {
|
|
Self::FIELD_OFFSETS
|
|
.key_pressed()
|
|
.apply_pin(self)
|
|
.call(&(event.key_event.clone(),))
|
|
}
|
|
}
|
|
KeyEventType::KeyReleased => {
|
|
let binding = self.key_binding_for_event(self_rc, event);
|
|
if binding.is_some() {
|
|
// The binding should have already handled the key press,
|
|
// so we just accept the release event if it matches a binding,
|
|
// to ensure the events remain "symmetric" in the key-pressed and key-released callbacks.
|
|
// Either both events appear in the callbacks, or neither do.
|
|
EventResult::Accept
|
|
} else {
|
|
Self::FIELD_OFFSETS
|
|
.key_released()
|
|
.apply_pin(self)
|
|
.call(&(event.key_event.clone(),))
|
|
}
|
|
}
|
|
KeyEventType::UpdateComposition | KeyEventType::CommitComposition => {
|
|
EventResult::Reject
|
|
}
|
|
};
|
|
match r {
|
|
EventResult::Accept => KeyEventResult::EventAccepted,
|
|
EventResult::Reject => KeyEventResult::EventIgnored,
|
|
}
|
|
}
|
|
|
|
fn focus_event(
|
|
self: Pin<&Self>,
|
|
event: &FocusEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
) -> FocusEventResult {
|
|
if !self.enabled() {
|
|
return FocusEventResult::FocusIgnored;
|
|
}
|
|
|
|
match event {
|
|
FocusEvent::FocusIn(reason) => {
|
|
match reason {
|
|
FocusReason::TabNavigation if !self.focus_on_tab_navigation() => {
|
|
return FocusEventResult::FocusIgnored;
|
|
}
|
|
FocusReason::PointerClick if !self.focus_on_click() => {
|
|
return FocusEventResult::FocusIgnored;
|
|
}
|
|
_ => (),
|
|
};
|
|
|
|
self.has_focus.set(true);
|
|
Self::FIELD_OFFSETS.focus_changed_event().apply_pin(self).call(&(*reason,));
|
|
Self::FIELD_OFFSETS.focus_gained().apply_pin(self).call(&(*reason,));
|
|
}
|
|
FocusEvent::FocusOut(reason) => {
|
|
self.has_focus.set(false);
|
|
Self::FIELD_OFFSETS.focus_changed_event().apply_pin(self).call(&(*reason,));
|
|
Self::FIELD_OFFSETS.focus_lost().apply_pin(self).call(&(*reason,));
|
|
}
|
|
}
|
|
FocusEventResult::FocusAccepted
|
|
}
|
|
|
|
fn render(
|
|
self: Pin<&Self>,
|
|
_backend: &mut ItemRendererRef,
|
|
_self_rc: &ItemRc,
|
|
_size: LogicalSize,
|
|
) -> RenderingResult {
|
|
RenderingResult::ContinueRenderingChildren
|
|
}
|
|
|
|
fn bounding_rect(
|
|
self: core::pin::Pin<&Self>,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
mut geometry: LogicalRect,
|
|
) -> LogicalRect {
|
|
geometry.size = LogicalSize::zero();
|
|
geometry
|
|
}
|
|
|
|
fn clips_children(self: core::pin::Pin<&Self>) -> bool {
|
|
false
|
|
}
|
|
}
|
|
|
|
impl ItemConsts for FocusScope {
|
|
const cached_rendering_data_offset: const_field_offset::FieldOffset<
|
|
FocusScope,
|
|
CachedRenderingData,
|
|
> = FocusScope::FIELD_OFFSETS.cached_rendering_data().as_unpinned_projection();
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(FieldOffsets, Default, SlintElement)]
|
|
#[pin]
|
|
pub struct SwipeGestureHandler {
|
|
pub enabled: Property<bool>,
|
|
pub handle_swipe_left: Property<bool>,
|
|
pub handle_swipe_right: Property<bool>,
|
|
pub handle_swipe_up: Property<bool>,
|
|
pub handle_swipe_down: Property<bool>,
|
|
|
|
pub moved: Callback<VoidArg>,
|
|
pub swiped: Callback<VoidArg>,
|
|
pub cancelled: Callback<VoidArg>,
|
|
|
|
pub pressed_position: Property<LogicalPosition>,
|
|
pub current_position: Property<LogicalPosition>,
|
|
pub swiping: Property<bool>,
|
|
|
|
// true when the cursor is pressed down and we haven't cancelled yet for another reason
|
|
pressed: Cell<bool>,
|
|
// capture_events: Cell<bool>,
|
|
/// FIXME: remove this
|
|
pub cached_rendering_data: CachedRenderingData,
|
|
}
|
|
|
|
impl Item for SwipeGestureHandler {
|
|
fn init(self: Pin<&Self>, _self_rc: &ItemRc) {}
|
|
|
|
fn deinit(self: Pin<&Self>, _window_adapter: &Rc<dyn WindowAdapter>) {}
|
|
|
|
fn layout_info(
|
|
self: Pin<&Self>,
|
|
_orientation: Orientation,
|
|
_cross_axis_constraint: Coord,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
) -> LayoutInfo {
|
|
LayoutInfo { stretch: 1., ..LayoutInfo::default() }
|
|
}
|
|
|
|
fn input_event_filter_before_children(
|
|
self: Pin<&Self>,
|
|
event: &MouseEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
_: &mut MouseCursorInner,
|
|
) -> InputEventFilterResult {
|
|
if !self.enabled() {
|
|
if self.pressed.get() {
|
|
self.cancel_impl();
|
|
}
|
|
return InputEventFilterResult::ForwardAndIgnore;
|
|
}
|
|
|
|
match event {
|
|
MouseEvent::Pressed { position, button: PointerEventButton::Left, .. } => {
|
|
Self::FIELD_OFFSETS
|
|
.pressed_position()
|
|
.apply_pin(self)
|
|
.set(crate::lengths::logical_position_to_api(*position));
|
|
self.pressed.set(true);
|
|
InputEventFilterResult::DelayForwarding(
|
|
super::flickable::FORWARD_DELAY.as_millis() as _
|
|
)
|
|
}
|
|
MouseEvent::Exit => {
|
|
self.cancel_impl();
|
|
InputEventFilterResult::ForwardAndIgnore
|
|
}
|
|
MouseEvent::Released { button: PointerEventButton::Left, .. } => {
|
|
if self.swiping() {
|
|
InputEventFilterResult::Intercept
|
|
} else {
|
|
self.pressed.set(false);
|
|
InputEventFilterResult::ForwardEvent
|
|
}
|
|
}
|
|
MouseEvent::Moved { position, .. } => {
|
|
if self.swiping() {
|
|
InputEventFilterResult::Intercept
|
|
} else if !self.pressed.get() {
|
|
InputEventFilterResult::ForwardEvent
|
|
} else if self.is_over_threshold(position) {
|
|
InputEventFilterResult::Intercept
|
|
} else {
|
|
InputEventFilterResult::ForwardAndInterceptGrab
|
|
}
|
|
}
|
|
MouseEvent::Wheel { .. } => InputEventFilterResult::ForwardAndIgnore,
|
|
// Not the left button
|
|
MouseEvent::Pressed { .. } | MouseEvent::Released { .. } => {
|
|
InputEventFilterResult::ForwardAndIgnore
|
|
}
|
|
MouseEvent::PinchGesture { .. } | MouseEvent::RotationGesture { .. } => {
|
|
InputEventFilterResult::ForwardAndIgnore
|
|
}
|
|
MouseEvent::DragMove { .. } | MouseEvent::Drop { .. } => {
|
|
InputEventFilterResult::ForwardAndIgnore
|
|
}
|
|
}
|
|
}
|
|
|
|
fn input_event(
|
|
self: Pin<&Self>,
|
|
event: &MouseEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
_: &mut MouseCursorInner,
|
|
) -> InputEventResult {
|
|
match event {
|
|
MouseEvent::Pressed { .. } => InputEventResult::GrabMouse,
|
|
MouseEvent::Exit => {
|
|
self.cancel_impl();
|
|
InputEventResult::EventIgnored
|
|
}
|
|
MouseEvent::Released { position, .. } => {
|
|
if !self.pressed.get() && !self.swiping() {
|
|
return InputEventResult::EventIgnored;
|
|
}
|
|
self.current_position.set(crate::lengths::logical_position_to_api(*position));
|
|
self.pressed.set(false);
|
|
if self.swiping() {
|
|
Self::FIELD_OFFSETS.swiping().apply_pin(self).set(false);
|
|
Self::FIELD_OFFSETS.swiped().apply_pin(self).call(&());
|
|
InputEventResult::EventAccepted
|
|
} else {
|
|
InputEventResult::EventIgnored
|
|
}
|
|
}
|
|
MouseEvent::Moved { position, .. } => {
|
|
if !self.pressed.get() {
|
|
return InputEventResult::EventAccepted;
|
|
}
|
|
self.current_position.set(crate::lengths::logical_position_to_api(*position));
|
|
let mut swiping = self.swiping();
|
|
if !swiping && self.is_over_threshold(position) {
|
|
Self::FIELD_OFFSETS.swiping().apply_pin(self).set(true);
|
|
swiping = true;
|
|
}
|
|
Self::FIELD_OFFSETS.moved().apply_pin(self).call(&());
|
|
if swiping { InputEventResult::GrabMouse } else { InputEventResult::EventAccepted }
|
|
}
|
|
MouseEvent::Wheel { .. } => InputEventResult::EventIgnored,
|
|
MouseEvent::PinchGesture { .. } | MouseEvent::RotationGesture { .. } => {
|
|
InputEventResult::EventIgnored
|
|
}
|
|
MouseEvent::DragMove { .. } | MouseEvent::Drop { .. } => InputEventResult::EventIgnored,
|
|
}
|
|
}
|
|
|
|
fn capture_key_event(
|
|
self: Pin<&Self>,
|
|
_: &InternalKeyEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
) -> KeyEventResult {
|
|
KeyEventResult::EventIgnored
|
|
}
|
|
|
|
fn key_event(
|
|
self: Pin<&Self>,
|
|
_event: &InternalKeyEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
) -> KeyEventResult {
|
|
KeyEventResult::EventIgnored
|
|
}
|
|
|
|
fn focus_event(
|
|
self: Pin<&Self>,
|
|
_: &FocusEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
) -> FocusEventResult {
|
|
FocusEventResult::FocusIgnored
|
|
}
|
|
|
|
fn render(
|
|
self: Pin<&Self>,
|
|
_backend: &mut ItemRendererRef,
|
|
_self_rc: &ItemRc,
|
|
_size: LogicalSize,
|
|
) -> RenderingResult {
|
|
RenderingResult::ContinueRenderingChildren
|
|
}
|
|
|
|
fn bounding_rect(
|
|
self: core::pin::Pin<&Self>,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
mut geometry: LogicalRect,
|
|
) -> LogicalRect {
|
|
geometry.size = LogicalSize::zero();
|
|
geometry
|
|
}
|
|
|
|
fn clips_children(self: core::pin::Pin<&Self>) -> bool {
|
|
false
|
|
}
|
|
}
|
|
|
|
impl ItemConsts for SwipeGestureHandler {
|
|
const cached_rendering_data_offset: const_field_offset::FieldOffset<Self, CachedRenderingData> =
|
|
Self::FIELD_OFFSETS.cached_rendering_data().as_unpinned_projection();
|
|
}
|
|
|
|
impl SwipeGestureHandler {
|
|
pub fn cancel(self: Pin<&Self>, _: &Rc<dyn WindowAdapter>, _: &ItemRc) {
|
|
self.cancel_impl();
|
|
}
|
|
|
|
fn cancel_impl(self: Pin<&Self>) {
|
|
if !self.pressed.replace(false) {
|
|
debug_assert!(!self.swiping());
|
|
return;
|
|
}
|
|
if self.swiping() {
|
|
Self::FIELD_OFFSETS.swiping().apply_pin(self).set(false);
|
|
Self::FIELD_OFFSETS.cancelled().apply_pin(self).call(&());
|
|
}
|
|
}
|
|
|
|
fn is_over_threshold(self: Pin<&Self>, position: &LogicalPoint) -> bool {
|
|
let pressed_pos = self.pressed_position();
|
|
let dx = position.x - pressed_pos.x as Coord;
|
|
let dy = position.y - pressed_pos.y as Coord;
|
|
let threshold = super::flickable::DISTANCE_THRESHOLD.get();
|
|
(self.handle_swipe_down() && dy > threshold && dy > dx.abs() / 2 as Coord)
|
|
|| (self.handle_swipe_up() && dy < -threshold && dy < -dx.abs() / 2 as Coord)
|
|
|| (self.handle_swipe_left() && dx < -threshold && dx < -dy.abs() / 2 as Coord)
|
|
|| (self.handle_swipe_right() && dx > threshold && dx > dy.abs() / 2 as Coord)
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "ffi")]
|
|
mod ffi {
|
|
use super::*;
|
|
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn slint_swipegesturehandler_cancel(
|
|
s: Pin<&SwipeGestureHandler>,
|
|
window_adapter: *const crate::window::ffi::WindowAdapterRcOpaque,
|
|
self_component: &vtable::VRc<crate::item_tree::ItemTreeVTable>,
|
|
self_index: u32,
|
|
) {
|
|
unsafe {
|
|
let window_adapter = &*(window_adapter as *const Rc<dyn WindowAdapter>);
|
|
let self_rc = ItemRc::new(self_component.clone(), self_index);
|
|
s.cancel(window_adapter, &self_rc);
|
|
}
|
|
}
|
|
|
|
/// # Safety
|
|
/// This must be called using a non-null pointer pointing to a chunk of memory big enough to
|
|
/// hold a MaybeKeybindingList
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn slint_maybe_key_binding_list_init(list: *mut MaybeKeyBindingList) {
|
|
unsafe {
|
|
core::ptr::write(list, MaybeKeyBindingList::default());
|
|
}
|
|
}
|
|
|
|
/// # Safety
|
|
/// This must be called using a non-null pointer pointing to an initialized MaybeKeybindingList
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn slint_maybe_key_binding_list_free(list: *mut MaybeKeyBindingList) {
|
|
unsafe { core::ptr::drop_in_place(list) };
|
|
}
|
|
}
|
|
|
|
/// The implementation of the `ScaleRotateGestureHandler` element.
|
|
///
|
|
/// Provides an API surface for platform-recognized pinch gesture events.
|
|
/// Receives `MouseEvent::PinchGesture` events via the normal mouse event
|
|
/// tree-walk and exposes cumulative scale, center position, and lifecycle callbacks.
|
|
#[repr(C)]
|
|
#[derive(FieldOffsets, Default, SlintElement)]
|
|
#[pin]
|
|
pub struct ScaleRotateGestureHandler {
|
|
pub enabled: Property<bool>,
|
|
|
|
// Output properties
|
|
pub active: Property<bool>,
|
|
/// Cumulative scale factor relative to gesture start. Always 1.0 when the
|
|
/// gesture starts, then updated as the gesture progresses (e.g., 2.0 means
|
|
/// doubled, 0.5 means halved).
|
|
pub scale: Property<f32>,
|
|
/// Cumulative rotation in degrees relative to gesture start. Always 0.0 when
|
|
/// the gesture starts.
|
|
pub rotation: Property<f32>,
|
|
pub center: Property<LogicalPosition>,
|
|
|
|
// Callbacks
|
|
pub started: Callback<VoidArg>,
|
|
pub updated: Callback<VoidArg>,
|
|
pub ended: Callback<VoidArg>,
|
|
pub cancelled: Callback<VoidArg>,
|
|
|
|
/// FIXME: remove this
|
|
pub cached_rendering_data: CachedRenderingData,
|
|
}
|
|
|
|
impl Item for ScaleRotateGestureHandler {
|
|
fn init(self: Pin<&Self>, _self_rc: &ItemRc) {
|
|
self.scale.set(1.0);
|
|
}
|
|
|
|
fn deinit(self: Pin<&Self>, _window_adapter: &Rc<dyn WindowAdapter>) {}
|
|
|
|
fn layout_info(
|
|
self: Pin<&Self>,
|
|
_orientation: Orientation,
|
|
_cross_axis_constraint: Coord,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
) -> LayoutInfo {
|
|
LayoutInfo { stretch: 1., ..LayoutInfo::default() }
|
|
}
|
|
|
|
fn input_event_filter_before_children(
|
|
self: Pin<&Self>,
|
|
event: &MouseEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
_: &mut MouseCursorInner,
|
|
) -> InputEventFilterResult {
|
|
match event {
|
|
// Forward gesture events so inner handlers get first shot
|
|
MouseEvent::PinchGesture { .. } | MouseEvent::RotationGesture { .. }
|
|
if self.enabled() =>
|
|
{
|
|
InputEventFilterResult::ForwardEvent
|
|
}
|
|
// While a gesture is active, intercept non-gesture events to
|
|
// prevent Flickable and other items from processing them concurrently.
|
|
_ if self.active() => InputEventFilterResult::Intercept,
|
|
_ => InputEventFilterResult::ForwardAndIgnore,
|
|
}
|
|
}
|
|
|
|
fn input_event(
|
|
self: Pin<&Self>,
|
|
event: &MouseEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
_: &mut MouseCursorInner,
|
|
) -> InputEventResult {
|
|
use crate::input::TouchPhase;
|
|
match event {
|
|
MouseEvent::PinchGesture { delta, phase, position } => {
|
|
if !self.enabled() {
|
|
if self.active() {
|
|
self.cancel_impl();
|
|
}
|
|
return InputEventResult::EventIgnored;
|
|
}
|
|
let new_scale = self.scale() * (1.0 + delta);
|
|
Self::FIELD_OFFSETS.scale().apply_pin(self).set(new_scale);
|
|
let center = crate::lengths::logical_position_to_api(*position);
|
|
Self::FIELD_OFFSETS.center().apply_pin(self).set(center);
|
|
match phase {
|
|
TouchPhase::Started => {
|
|
if !self.active() {
|
|
Self::FIELD_OFFSETS.active().apply_pin(self).set(true);
|
|
Self::FIELD_OFFSETS.started().apply_pin(self).call(&());
|
|
}
|
|
InputEventResult::GrabMouse
|
|
}
|
|
TouchPhase::Moved => {
|
|
if !self.active() {
|
|
return InputEventResult::EventIgnored;
|
|
}
|
|
Self::FIELD_OFFSETS.updated().apply_pin(self).call(&());
|
|
InputEventResult::GrabMouse
|
|
}
|
|
TouchPhase::Ended => self.end_impl(),
|
|
TouchPhase::Cancelled => {
|
|
self.cancel_impl();
|
|
InputEventResult::EventAccepted
|
|
}
|
|
}
|
|
}
|
|
MouseEvent::RotationGesture { delta, phase, position } => {
|
|
if !self.enabled() {
|
|
return InputEventResult::EventIgnored;
|
|
}
|
|
let center = crate::lengths::logical_position_to_api(*position);
|
|
Self::FIELD_OFFSETS.center().apply_pin(self).set(center);
|
|
let new_rotation = self.rotation() + delta;
|
|
Self::FIELD_OFFSETS.rotation().apply_pin(self).set(new_rotation);
|
|
match phase {
|
|
TouchPhase::Started => {
|
|
if !self.active() {
|
|
Self::FIELD_OFFSETS.active().apply_pin(self).set(true);
|
|
Self::FIELD_OFFSETS.started().apply_pin(self).call(&());
|
|
}
|
|
InputEventResult::GrabMouse
|
|
}
|
|
TouchPhase::Moved => {
|
|
if !self.active() {
|
|
return InputEventResult::EventIgnored;
|
|
}
|
|
Self::FIELD_OFFSETS.updated().apply_pin(self).call(&());
|
|
InputEventResult::GrabMouse
|
|
}
|
|
TouchPhase::Ended => self.end_impl(),
|
|
TouchPhase::Cancelled => {
|
|
self.cancel_impl();
|
|
InputEventResult::EventAccepted
|
|
}
|
|
}
|
|
}
|
|
// Grab mouse during active gesture to maintain exclusivity.
|
|
_ if self.active() => InputEventResult::GrabMouse,
|
|
_ => InputEventResult::EventIgnored,
|
|
}
|
|
}
|
|
|
|
fn capture_key_event(
|
|
self: Pin<&Self>,
|
|
_: &InternalKeyEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
) -> KeyEventResult {
|
|
KeyEventResult::EventIgnored
|
|
}
|
|
|
|
fn key_event(
|
|
self: Pin<&Self>,
|
|
_event: &InternalKeyEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
) -> KeyEventResult {
|
|
KeyEventResult::EventIgnored
|
|
}
|
|
|
|
fn focus_event(
|
|
self: Pin<&Self>,
|
|
_: &FocusEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
) -> FocusEventResult {
|
|
FocusEventResult::FocusIgnored
|
|
}
|
|
|
|
fn render(
|
|
self: Pin<&Self>,
|
|
_backend: &mut ItemRendererRef,
|
|
_self_rc: &ItemRc,
|
|
_size: LogicalSize,
|
|
) -> RenderingResult {
|
|
RenderingResult::ContinueRenderingChildren
|
|
}
|
|
|
|
fn bounding_rect(
|
|
self: core::pin::Pin<&Self>,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
mut geometry: LogicalRect,
|
|
) -> LogicalRect {
|
|
geometry.size = LogicalSize::zero();
|
|
geometry
|
|
}
|
|
|
|
fn clips_children(self: core::pin::Pin<&Self>) -> bool {
|
|
false
|
|
}
|
|
}
|
|
|
|
impl ItemConsts for ScaleRotateGestureHandler {
|
|
const cached_rendering_data_offset: const_field_offset::FieldOffset<Self, CachedRenderingData> =
|
|
Self::FIELD_OFFSETS.cached_rendering_data().as_unpinned_projection();
|
|
}
|
|
|
|
impl ScaleRotateGestureHandler {
|
|
fn cancel_impl(self: Pin<&Self>) {
|
|
if !self.active() {
|
|
return;
|
|
}
|
|
Self::FIELD_OFFSETS.active().apply_pin(self).set(false);
|
|
Self::FIELD_OFFSETS.cancelled().apply_pin(self).call(&());
|
|
// Reset after the callback so handlers can read the last known values
|
|
// to animate back smoothly, matching the pattern where `ended` leaves
|
|
// scale/rotation at their final values.
|
|
Self::FIELD_OFFSETS.scale().apply_pin(self).set(1.0);
|
|
Self::FIELD_OFFSETS.rotation().apply_pin(self).set(0.0);
|
|
}
|
|
|
|
fn end_impl(self: Pin<&Self>) -> InputEventResult {
|
|
if !self.active() {
|
|
return InputEventResult::EventIgnored;
|
|
}
|
|
Self::FIELD_OFFSETS.ended().apply_pin(self).call(&());
|
|
Self::FIELD_OFFSETS.active().apply_pin(self).set(false);
|
|
Self::FIELD_OFFSETS.scale().apply_pin(self).set(1.0);
|
|
Self::FIELD_OFFSETS.rotation().apply_pin(self).set(0.0);
|
|
InputEventResult::EventAccepted
|
|
}
|
|
}
|