Experimental support for Drag & Drop

Add a `DragArea` and `DropArea` elements.
It is currently gated as experimental.
This commit is contained in:
Olivier Goffart 2025-06-17 15:46:11 +02:00
parent 47a556d0e7
commit 3823c1e8da
21 changed files with 610 additions and 113 deletions

View file

@ -373,6 +373,7 @@ if (SLINT_BUILD_RUNTIME)
${CMAKE_CURRENT_BINARY_DIR}/generated_include/slint_sharedvector_internal.h
${CMAKE_CURRENT_BINARY_DIR}/generated_include/slint_string_internal.h
${CMAKE_CURRENT_BINARY_DIR}/generated_include/slint_timer_internal.h
${CMAKE_CURRENT_BINARY_DIR}/generated_include/slint_events_internal.h
)
if(SLINT_FEATURE_INTERPRETER)

View file

@ -119,6 +119,7 @@ fn builtin_structs(path: &Path) -> anyhow::Result<()> {
writeln!(structs_priv, "// This file is auto-generated from {}", file!())?;
writeln!(structs_priv, "#include \"slint_builtin_structs.h\"")?;
writeln!(structs_priv, "#include \"slint_enums_internal.h\"")?;
writeln!(structs_priv, "#include \"slint_point.h\"")?;
writeln!(structs_priv, "namespace slint::cbindgen_private {{")?;
writeln!(structs_priv, "enum class KeyEventType : uint8_t;")?;
macro_rules! struct_file {
@ -218,6 +219,7 @@ fn default_config() -> cbindgen::Config {
("FocusReasonArg".into(), "FocusReason".into()),
("KeyEventArg".into(), "KeyEvent".into()),
("PointerEventArg".into(), "PointerEvent".into()),
("DropEventArg".into(), "DropEvent".into()),
("PointerScrollEventArg".into(), "PointerScrollEvent".into()),
("PointArg".into(), "slint::LogicalPosition".into()),
("FloatArg".into(), "float".into()),
@ -286,6 +288,8 @@ fn gen_corelib(
"Rectangle",
"BasicBorderRectangle",
"BorderRectangle",
"DragArea",
"DropArea",
"ImageItem",
"ClippedImage",
"TouchArea",
@ -368,7 +372,6 @@ fn gen_corelib(
"Property",
"Slice",
"Timer",
"TimerMode",
"PropertyHandleOpaque",
"Callback",
"slint_property_listener_scope_evaluate",
@ -377,6 +380,7 @@ fn gen_corelib(
"CallbackOpaque",
"WindowAdapterRc",
"VoidArg",
"DropEventArg",
"FocusReasonArg",
"KeyEventArg",
"PointerEventArg",
@ -385,29 +389,6 @@ fn gen_corelib(
"Point",
"MenuEntryModel",
"MenuEntryArg",
"slint_color_brighter",
"slint_color_darker",
"slint_color_transparentize",
"slint_color_mix",
"slint_color_with_alpha",
"slint_color_to_hsva",
"slint_color_from_hsva",
"slint_image_size",
"slint_image_path",
"slint_image_load_from_path",
"slint_image_load_from_embedded_data",
"slint_image_from_embedded_textures",
"slint_image_compare_equal",
"slint_image_set_nine_slice_edges",
"slint_image_to_rgb8",
"slint_image_to_rgba8",
"slint_image_to_rgba8_premultiplied",
"slint_timer_start",
"slint_timer_singleshot",
"slint_timer_destroy",
"slint_timer_stop",
"slint_timer_restart",
"slint_timer_running",
"Coord",
"LogicalRect",
"LogicalPoint",
@ -483,15 +464,9 @@ fn gen_corelib(
.iter()
.map(|s| s.to_string())
.collect();
tmp.export.exclude = config
.export
.exclude
.iter()
.filter(|exclusion| !tmp.export.include.iter().any(|inclusion| inclusion == *exclusion))
.cloned()
.collect();
tmp
};
config.export.exclude.extend(timer_config.export.include.iter().cloned());
cbindgen::Builder::new()
.with_config(timer_config)
.with_src(crate_dir.join("timers.rs"))
@ -499,7 +474,7 @@ fn gen_corelib(
.context("Unable to generate bindings for slint_timer_internal.h")?
.write_to_file(include_dir.join("slint_timer_internal.h"));
for (rust_types, extra_excluded_types, internal_header, prelude) in [
for (rust_types, internal_header, prelude) in [
(
vec![
"ImageInner",
@ -521,7 +496,6 @@ fn gen_corelib(
"StaticTextures",
"BorrowedOpenGLTextureOrigin"
],
vec!["Color"],
"slint_image_internal.h",
"namespace slint::cbindgen_private { struct ParsedSVG{}; struct HTMLImage{}; using namespace vtable; namespace types{ struct NineSliceImage{}; } }",
),
@ -532,22 +506,31 @@ fn gen_corelib(
"slint_color_with_alpha",
"slint_color_to_hsva",
"slint_color_from_hsva",],
vec![],
"slint_color_internal.h",
"",
),
(
vec!["PathData", "PathElement", "slint_new_path_elements", "slint_new_path_events"],
vec![],
vec!["PathData", "PathElement", "slint_new_path_elements", "slint_new_path_events", "Point"],
"slint_pathdata_internal.h",
"",
"#include \"slint_sharedvector.h\"\n#include \"slint_point.h\"",
),
(
vec!["Brush", "LinearGradient", "GradientStop", "RadialGradient"],
vec!["Color"],
"slint_brush_internal.h",
"",
),
(
vec!["MouseEvent"],
"slint_events_internal.h",
"#include \"slint_point.h\"
namespace slint::cbindgen_private {
struct KeyEvent; struct PointerEvent;
struct Rect;
using LogicalRect = Rect;
using LogicalPoint = Point2D<float>;
using LogicalLength = float;
}",
)
]
.iter()
{
@ -591,33 +574,15 @@ fn gen_corelib(
"slint_windowrc_is_minimized",
"slint_windowrc_is_maximized",
"slint_windowrc_take_snapshot",
"slint_new_path_elements",
"slint_new_path_events",
"slint_color_brighter",
"slint_color_darker",
"slint_color_transparentize",
"slint_color_mix",
"slint_color_with_alpha",
"slint_color_to_hsva",
"slint_color_from_hsva",
"slint_image_size",
"slint_image_path",
"slint_image_load_from_path",
"slint_image_load_from_embedded_data",
"slint_image_set_nine_slice_edges",
"slint_image_to_rgb8",
"slint_image_to_rgba8",
"slint_image_to_rgba8_premultiplied",
"slint_image_from_embedded_textures",
"slint_image_compare_equal",
]
.iter()
.filter(|exclusion| !rust_types.iter().any(|inclusion| inclusion == *exclusion))
.chain(extra_excluded_types.iter())
.chain(public_exported_types.iter())
.into_iter()
.chain(config.export.exclude.iter().map(|s| s.as_str()))
.filter(|exclusion| !rust_types.iter().any(|inclusion| inclusion == exclusion))
.map(|s| s.to_string())
.collect();
config.export.exclude.extend(rust_types.iter().map(|s| s.to_string()));
special_config.enumeration = cbindgen::EnumConfig {
derive_tagged_enum_copy_assignment: true,
derive_tagged_enum_copy_constructor: true,
@ -646,7 +611,7 @@ fn gen_corelib(
.with_src(crate_dir.join("graphics/image.rs"))
.with_src(crate_dir.join("graphics/image/cache.rs"))
.with_src(crate_dir.join("animations.rs"))
// .with_src(crate_dir.join("input.rs"))
.with_src(crate_dir.join("input.rs"))
.with_src(crate_dir.join("item_rendering.rs"))
.with_src(crate_dir.join("window.rs"))
.with_include("slint_enums_internal.h")
@ -759,6 +724,7 @@ fn gen_corelib(
.with_include("slint_point.h")
.with_include("slint_timer.h")
.with_include("slint_builtin_structs_internal.h")
.with_include("slint_events_internal.h")
.with_after_include(
r"
namespace slint {
@ -766,17 +732,14 @@ namespace slint {
namespace cbindgen_private {
using slint::private_api::WindowAdapterRc;
using namespace vtable;
struct KeyEvent; struct PointerEvent;
using private_api::Property;
using private_api::PathData;
using private_api::Point;
struct Rect;
using LogicalRect = Rect;
using LogicalPoint = Point2D<float>;
using LogicalLength = float;
struct ItemTreeVTable;
struct ItemVTable;
using types::IntRect;
using types::Size;
using types::MouseEvent;
}
template<typename ModelData> class Model;
}",

View file

@ -499,12 +499,8 @@ public:
void dispatch_pointer_press_event(LogicalPosition pos, PointerEventButton button)
{
private_api::assert_main_thread();
using slint::cbindgen_private::MouseEvent;
MouseEvent event { .tag = MouseEvent::Tag::Pressed,
.pressed = MouseEvent::Pressed_Body { .position = { pos.x, pos.y },
.button = button,
.click_count = 0 } };
inner.dispatch_pointer_event(event);
inner.dispatch_pointer_event(
slint::cbindgen_private::MouseEvent::Pressed({ pos.x, pos.y }, button, 0));
}
/// Dispatches a pointer or mouse release event to the scene.
///
@ -516,12 +512,8 @@ public:
void dispatch_pointer_release_event(LogicalPosition pos, PointerEventButton button)
{
private_api::assert_main_thread();
using slint::cbindgen_private::MouseEvent;
MouseEvent event { .tag = MouseEvent::Tag::Released,
.released = MouseEvent::Released_Body { .position = { pos.x, pos.y },
.button = button,
.click_count = 0 } };
inner.dispatch_pointer_event(event);
inner.dispatch_pointer_event(
slint::cbindgen_private::MouseEvent::Released({ pos.x, pos.y }, button, 0));
}
/// Dispatches a pointer exit event to the scene.
///
@ -532,9 +524,7 @@ public:
void dispatch_pointer_exit_event()
{
private_api::assert_main_thread();
using slint::cbindgen_private::MouseEvent;
MouseEvent event { .tag = MouseEvent::Tag::Exit, .moved = {} };
inner.dispatch_pointer_event(event);
inner.dispatch_pointer_event(slint::cbindgen_private::MouseEvent::Exit());
}
/// Dispatches a pointer move event to the scene.
@ -546,10 +536,7 @@ public:
void dispatch_pointer_move_event(LogicalPosition pos)
{
private_api::assert_main_thread();
using slint::cbindgen_private::MouseEvent;
MouseEvent event { .tag = MouseEvent::Tag::Moved,
.moved = MouseEvent::Moved_Body { .position = { pos.x, pos.y } } };
inner.dispatch_pointer_event(event);
inner.dispatch_pointer_event(slint::cbindgen_private::MouseEvent::Moved({ pos.x, pos.y }));
}
/// Dispatches a scroll (or wheel) event to the scene.
@ -563,12 +550,8 @@ public:
void dispatch_pointer_scroll_event(LogicalPosition pos, float delta_x, float delta_y)
{
private_api::assert_main_thread();
using slint::cbindgen_private::MouseEvent;
MouseEvent event { .tag = MouseEvent::Tag::Wheel,
.wheel = MouseEvent::Wheel_Body { .position = { pos.x, pos.y },
.delta_x = delta_x,
.delta_y = delta_y } };
inner.dispatch_pointer_event(event);
inner.dispatch_pointer_event(
slint::cbindgen_private::MouseEvent::Wheel({ pos.x, pos.y }, delta_x, delta_y));
}
/// Set the logical size of this window after a resize event

View file

@ -272,6 +272,9 @@ impl Item for NativeButton {
}
}
MouseEvent::Wheel { .. } => return InputEventResult::EventIgnored,
MouseEvent::DragMove(..) | MouseEvent::Drop(..) => {
return InputEventResult::EventIgnored
}
});
if let MouseEvent::Released { position, .. } = event {
let geo = self_rc.geometry();

View file

@ -225,6 +225,7 @@ impl Item for NativeScrollView {
}
InputEventResult::EventAccepted
}
MouseEvent::DragMove(..) | MouseEvent::Drop(..) => InputEventResult::EventIgnored,
};
self.data.set(data);
result

View file

@ -251,6 +251,7 @@ impl Item for NativeSlider {
debug_assert_ne!(*button, PointerEventButton::Left);
InputEventResult::EventIgnored
}
MouseEvent::DragMove(..) | MouseEvent::Drop(..) => InputEventResult::EventIgnored,
};
data.active_controls = new_control;

View file

@ -224,6 +224,7 @@ impl Item for NativeSpinBox {
true
}
MouseEvent::DragMove(..) | MouseEvent::Drop(..) => false,
};
data.active_controls = new_control;
if changed {

View file

@ -451,6 +451,9 @@ impl Item for NativeTab {
}
}
MouseEvent::Wheel { .. } => return InputEventResult::EventIgnored,
MouseEvent::DragMove(..) | MouseEvent::Drop(..) => {
return InputEventResult::EventIgnored
}
});
let click_on_press = cpp!(unsafe [] -> bool as "bool" {
return qApp->style()->styleHint(QStyle::SH_TabBar_SelectMouseType, nullptr, nullptr) == QEvent::MouseButtonPress;

View file

@ -115,6 +115,21 @@ macro_rules! for_each_builtin_structs {
}
}
/// This structure is passed to the callbacks of the `DropArea` element
struct DropEvent {
@name = "slint::private_api::DropEvent"
export {
/// The mime type of the data being dragged
mime_type: SharedString,
/// The data being dragged
data: SharedString,
/// The current mouse position in coordinates of the `DropArea` element
position: LogicalPosition,
}
private {
}
}
/// Represents an item in a StandardListView and a StandardTableView.
#[non_exhaustive]
struct StandardListViewItem {

View file

@ -186,6 +186,22 @@ export component SwipeGestureHandler {
//-default_size_binding:expands_to_parent_geometry
}
export component DragArea {
in property <bool> enabled: true;
//out property <bool> dragging;
in property <string> mime-type;
in property <string> data;
//-default_size_binding:expands_to_parent_geometry
}
export component DropArea {
in property <bool> enabled: true;
callback can-drop(event: DropEvent) -> bool;
callback dropped(event: DropEvent);
out property <bool> contains-drag;
//-default_size_binding:expands_to_parent_geometry
}
component MenuItem {
in property <string> title;
callback activated();

View file

@ -408,6 +408,7 @@ impl TypeRegister {
($pub_type:ident, SharedString) => { Type::String };
($pub_type:ident, Image) => { Type::Image };
($pub_type:ident, Coord) => { Type::LogicalLength };
($pub_type:ident, LogicalPosition) => { logical_point_type() };
($pub_type:ident, KeyboardModifiers) => { $pub_type.clone() };
($pub_type:ident, $_:ident) => {
BUILTIN.with(|e| Type::Enumeration(e.enums.$pub_type.clone()))
@ -547,8 +548,12 @@ impl TypeRegister {
pub fn builtin() -> Rc<RefCell<Self>> {
let mut register = Self::builtin_internal();
register.elements.remove("ComponentContainer");
register.types.remove("component-factory");
register.elements.remove("ComponentContainer").unwrap();
register.types.remove("component-factory").unwrap();
register.elements.remove("DragArea").unwrap();
register.elements.remove("DropArea").unwrap();
register.types.remove("DropEvent").unwrap(); // Also removed in xtask/src/slintdocs.rs
Rc::new(RefCell::new(register))
}

View file

@ -8,8 +8,8 @@
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::items::{ItemRef, TextCursorDirection};
use crate::lengths::{LogicalPoint, LogicalVector};
use crate::timers::Timer;
use crate::window::{WindowAdapter, WindowInner};
@ -26,7 +26,7 @@ use core::time::Duration;
/// 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)]
#[derive(Debug, Clone, PartialEq)]
#[allow(missing_docs)]
pub enum MouseEvent {
/// The mouse or finger was pressed
@ -46,6 +46,12 @@ pub enum MouseEvent {
/// `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: Coord, delta_y: Coord },
/// The mouse is being dragged over this item.
/// [`InputEventResult::EventIgnored`] means that the item does not handle the drag operation
/// and [`InputEventResult::EventAccepted`] means that the item can accept it.
DragMove(DropEvent),
/// The mouse is released while dregging over this item.
Drop(DropEvent),
/// The mouse exited the item or component
Exit,
}
@ -58,6 +64,9 @@ impl MouseEvent {
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,
}
}
@ -69,6 +78,12 @@ impl MouseEvent {
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 {
@ -102,6 +117,8 @@ pub enum InputEventResult {
EventIgnored,
/// All further mouse event need to be sent to this item or component
GrabMouse,
/// Will start a drag operation. Can only be returned from a [`crate::items::DragArea`] item.
StartDrag,
}
/// This value is returned by the `input_event_filter_before_children` function, which
@ -540,6 +557,9 @@ pub struct MouseInputState {
pub(crate) offset: LogicalPoint,
/// true if the top item of the stack has the mouse grab
grabbed: bool,
/// When this is Some, it means we are in the middle of a drag-drop operation and it contains the dragged data.
/// The `position` field has no signification
pub(crate) drag_data: Option<DropEvent>,
delayed: Option<(crate::timers::Timer, MouseEvent)>,
delayed_exit_items: Vec<ItemWeak>,
}
@ -613,16 +633,27 @@ pub(crate) fn handle_mouse_grab(
let grabber = mouse_input_state.top_item().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
Some(
mouse_event
.position()
.map_or(MouseEvent::Exit, |position| MouseEvent::Moved { position }),
)
} else {
None
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;
// Return a move event so that the new position can be registered properly
Some(
mouse_event
.position()
.map_or(MouseEvent::Exit, |position| MouseEvent::Moved { position }),
)
}
}
}
@ -671,6 +702,7 @@ pub fn process_mouse_input(
mouse_input_state: MouseInputState,
) -> MouseInputState {
let mut result = MouseInputState::default();
result.drag_data = mouse_input_state.drag_data.clone();
let r = send_mouse_event_to_item(
mouse_event,
root.clone(),
@ -841,6 +873,18 @@ fn send_mouse_event_to_item(
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)
}
}
}

View file

@ -20,6 +20,7 @@ When adding an item or a property, it needs to be kept in sync with different pl
#![allow(non_upper_case_globals)]
#![allow(missing_docs)] // because documenting each property of items is redundant
use crate::api::LogicalPosition;
use crate::graphics::{Brush, Color, FontRequest};
use crate::input::{
FocusEvent, FocusEventResult, InputEventFilterResult, InputEventResult, KeyEventResult,
@ -32,6 +33,7 @@ use crate::lengths::{
LogicalBorderRadius, LogicalLength, LogicalRect, LogicalSize, LogicalVector, PointLengths,
RectLengths,
};
pub use crate::menus::MenuItem;
#[cfg(feature = "rtti")]
use crate::rtti::*;
use crate::window::{WindowAdapter, WindowAdapterRc, WindowInner};
@ -54,9 +56,10 @@ mod input_items;
pub use input_items::*;
mod image;
pub use self::image::*;
mod drag_n_drop;
pub use drag_n_drop::*;
#[cfg(feature = "std")]
mod path;
pub use crate::menus::MenuItem;
#[cfg(feature = "std")]
pub use path::*;
@ -70,7 +73,7 @@ pub type KeyEventArg = (KeyEvent,);
type FocusReasonArg = (FocusReason,);
type PointerEventArg = (PointerEvent,);
type PointerScrollEventArg = (PointerScrollEvent,);
type PointArg = (crate::api::LogicalPosition,);
type PointArg = (LogicalPosition,);
type MenuEntryArg = (MenuEntry,);
type MenuEntryModel = crate::model::ModelRc<MenuEntry>;
@ -1047,6 +1050,14 @@ declare_item_vtable! {
fn slint_get_FlickableVTable() -> FlickableVTable for Flickable
}
declare_item_vtable! {
fn slint_get_DragAreaVTable() -> DragAreaVTable for DragArea
}
declare_item_vtable! {
fn slint_get_DropAreaVTable() -> DropAreaVTable for DropArea
}
/// The implementation of the `PropertyAnimation` element
#[repr(C)]
#[derive(FieldOffsets, SlintElement, Clone, Debug)]
@ -1349,7 +1360,7 @@ impl Item for ContextMenu {
}
match event {
MouseEvent::Pressed { position, button: PointerEventButton::Right, .. } => {
self.show.call(&(crate::api::LogicalPosition::from_euclid(position),));
self.show.call(&(LogicalPosition::from_euclid(*position),));
InputEventResult::EventAccepted
}
#[cfg(target_os = "android")]

View file

@ -0,0 +1,312 @@
// 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::{
DropEvent, Item, ItemConsts, ItemRc, MouseCursor, PointerEventButton, RenderingResult,
};
use crate::input::{
FocusEvent, FocusEventResult, InputEventFilterResult, InputEventResult, KeyEvent,
KeyEventResult, MouseEvent,
};
use crate::item_rendering::{CachedRenderingData, ItemRenderer};
use crate::layout::{LayoutInfo, Orientation};
use crate::lengths::{LogicalPoint, LogicalRect, LogicalSize};
#[cfg(feature = "rtti")]
use crate::rtti::*;
use crate::window::WindowAdapter;
use crate::{Callback, Property, SharedString};
use alloc::rc::Rc;
use const_field_offset::FieldOffsets;
use core::cell::Cell;
use core::pin::Pin;
use i_slint_core_macros::*;
pub type DropEventArg = (DropEvent,);
#[repr(C)]
#[derive(FieldOffsets, Default, SlintElement)]
#[pin]
/// The implementation of the `DragArea` element
pub struct DragArea {
pub enabled: Property<bool>,
pub mime_type: Property<SharedString>,
pub data: Property<SharedString>,
pressed: Cell<bool>,
pressed_position: Cell<LogicalPoint>,
pub cached_rendering_data: CachedRenderingData,
}
impl Item for DragArea {
fn init(self: Pin<&Self>, _self_rc: &ItemRc) {}
fn layout_info(
self: Pin<&Self>,
_: Orientation,
_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,
) -> InputEventFilterResult {
if !self.enabled() {
self.cancel();
return InputEventFilterResult::ForwardAndIgnore;
}
match event {
MouseEvent::Pressed { position, button: PointerEventButton::Left, .. } => {
self.pressed_position.set(*position);
self.pressed.set(true);
InputEventFilterResult::ForwardAndInterceptGrab
}
MouseEvent::Exit => {
self.cancel();
InputEventFilterResult::ForwardAndIgnore
}
MouseEvent::Released { button: PointerEventButton::Left, .. } => {
self.pressed.set(false);
InputEventFilterResult::ForwardAndIgnore
}
MouseEvent::Moved { position } => {
if !self.pressed.get() {
InputEventFilterResult::ForwardEvent
} else {
let pressed_pos = self.pressed_position.get();
let dx = (position.x - pressed_pos.x).abs();
let dy = (position.y - pressed_pos.y).abs();
let threshold = super::flickable::DISTANCE_THRESHOLD.get();
if dy > threshold || dx > threshold {
InputEventFilterResult::Intercept
} else {
InputEventFilterResult::ForwardAndInterceptGrab
}
}
}
MouseEvent::Wheel { .. } => InputEventFilterResult::ForwardAndIgnore,
// Not the left button
MouseEvent::Pressed { .. } | MouseEvent::Released { .. } => {
InputEventFilterResult::ForwardAndIgnore
}
MouseEvent::DragMove(..) | MouseEvent::Drop(..) => {
InputEventFilterResult::ForwardAndIgnore
}
}
}
fn input_event(
self: Pin<&Self>,
event: &MouseEvent,
_window_adapter: &Rc<dyn WindowAdapter>,
_self_rc: &ItemRc,
) -> InputEventResult {
match event {
MouseEvent::Pressed { .. } => InputEventResult::EventAccepted,
MouseEvent::Exit => {
self.cancel();
InputEventResult::EventIgnored
}
MouseEvent::Released { .. } => {
self.cancel();
InputEventResult::EventIgnored
}
MouseEvent::Moved { position } => {
if !self.pressed.get() || !self.enabled() {
return InputEventResult::EventIgnored;
}
let pressed_pos = self.pressed_position.get();
let dx = (position.x - pressed_pos.x).abs();
let dy = (position.y - pressed_pos.y).abs();
let threshold = super::flickable::DISTANCE_THRESHOLD.get();
let start_drag = dx > threshold || dy > threshold;
if start_drag {
self.pressed.set(false);
InputEventResult::StartDrag
} else {
InputEventResult::EventAccepted
}
}
MouseEvent::Wheel { .. } => InputEventResult::EventIgnored,
MouseEvent::DragMove(..) | MouseEvent::Drop(..) => InputEventResult::EventIgnored,
}
}
fn key_event(
self: Pin<&Self>,
_: &KeyEvent,
_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 &mut dyn ItemRenderer,
_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 DragArea {
const cached_rendering_data_offset: const_field_offset::FieldOffset<
DragArea,
CachedRenderingData,
> = DragArea::FIELD_OFFSETS.cached_rendering_data.as_unpinned_projection();
}
impl DragArea {
fn cancel(self: Pin<&Self>) {
self.pressed.set(false)
}
}
#[repr(C)]
#[derive(FieldOffsets, Default, SlintElement)]
#[pin]
/// The implementation of the `DropArea` element
pub struct DropArea {
pub enabled: Property<bool>,
pub contains_drag: Property<bool>,
pub can_drop: Callback<DropEventArg, bool>,
pub dropped: Callback<DropEventArg>,
pub cached_rendering_data: CachedRenderingData,
}
impl Item for DropArea {
fn init(self: Pin<&Self>, _self_rc: &ItemRc) {}
fn layout_info(
self: Pin<&Self>,
_: Orientation,
_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,
) -> InputEventFilterResult {
InputEventFilterResult::ForwardEvent
}
fn input_event(
self: Pin<&Self>,
event: &MouseEvent,
window_adapter: &Rc<dyn WindowAdapter>,
_self_rc: &ItemRc,
) -> InputEventResult {
if !self.enabled() {
return InputEventResult::EventIgnored;
}
match event {
MouseEvent::DragMove(event) => {
let r = Self::FIELD_OFFSETS.can_drop.apply_pin(self).call(&(event.clone(),));
if r {
self.contains_drag.set(true);
if let Some(window_adapter) = window_adapter.internal(crate::InternalToken) {
window_adapter.set_mouse_cursor(MouseCursor::Copy);
}
InputEventResult::EventAccepted
} else {
self.contains_drag.set(false);
InputEventResult::EventIgnored
}
}
MouseEvent::Drop(event) => {
self.contains_drag.set(false);
Self::FIELD_OFFSETS.dropped.apply_pin(self).call(&(event.clone(),));
InputEventResult::EventAccepted
}
MouseEvent::Exit => {
self.contains_drag.set(false);
InputEventResult::EventIgnored
}
_ => InputEventResult::EventIgnored,
}
}
fn key_event(
self: Pin<&Self>,
_: &KeyEvent,
_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 &mut dyn ItemRenderer,
_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 DropArea {
const cached_rendering_data_offset: const_field_offset::FieldOffset<
DropArea,
CachedRenderingData,
> = DropArea::FIELD_OFFSETS.cached_rendering_data.as_unpinned_projection();
}

View file

@ -313,6 +313,9 @@ impl FlickableData {
MouseEvent::Pressed { .. } | MouseEvent::Released { .. } => {
InputEventFilterResult::ForwardAndIgnore
}
MouseEvent::DragMove(..) | MouseEvent::Drop(..) => {
InputEventFilterResult::ForwardAndIgnore
}
}
}
@ -406,6 +409,7 @@ impl FlickableData {
}
InputEventResult::EventAccepted
}
MouseEvent::DragMove(..) | MouseEvent::Drop(..) => InputEventResult::EventIgnored,
}
}

View file

@ -202,6 +202,7 @@ impl Item for TouchArea {
}
}
}
MouseEvent::DragMove(..) | MouseEvent::Drop(..) => InputEventResult::EventIgnored,
}
}
@ -487,6 +488,9 @@ impl Item for SwipeGestureHandler {
MouseEvent::Pressed { .. } | MouseEvent::Released { .. } => {
InputEventFilterResult::ForwardAndIgnore
}
MouseEvent::DragMove(..) | MouseEvent::Drop(..) => {
InputEventFilterResult::ForwardAndIgnore
}
}
}
@ -539,6 +543,7 @@ impl Item for SwipeGestureHandler {
InputEventResult::GrabMouse
}
MouseEvent::Wheel { .. } => InputEventResult::EventIgnored,
MouseEvent::DragMove(..) | MouseEvent::Drop(..) => InputEventResult::EventIgnored,
}
}

View file

@ -50,6 +50,7 @@ macro_rules! declare_ValueType_2 {
crate::api::LogicalPosition,
crate::items::FontMetrics,
crate::items::MenuEntry,
crate::items::DropEvent,
crate::model::ModelRc<crate::items::MenuEntry>,
$(crate::items::$Name,)*
];

View file

@ -12,7 +12,7 @@ use crate::api::{
};
use crate::input::{
key_codes, ClickState, FocusEvent, FocusReason, InternalKeyboardModifierState, KeyEvent,
KeyEventType, MouseEvent, MouseInputState, TextCursorBlinker,
KeyEventType, MouseEvent, MouseInputState, PointerEventButton, TextCursorBlinker,
};
use crate::item_tree::{
ItemRc, ItemTreeRc, ItemTreeRef, ItemTreeVTable, ItemTreeWeak, ItemWeak,
@ -585,11 +585,35 @@ impl WindowInner {
// handle multiple press release
event = self.click_state.check_repeat(event, self.ctx.platform().click_interval());
let window_adapter = self.window_adapter();
let mut mouse_input_state = self.mouse_input_state.take();
if let Some(mut drop_event) = mouse_input_state.drag_data.clone() {
match &event {
MouseEvent::Released { position, button: PointerEventButton::Left, .. } => {
if let Some(window_adapter) = window_adapter.internal(crate::InternalToken) {
window_adapter.set_mouse_cursor(MouseCursor::Default);
}
drop_event.position = crate::lengths::logical_position_to_api(*position);
event = MouseEvent::Drop(drop_event);
mouse_input_state.drag_data = None;
}
MouseEvent::Moved { position } => {
if let Some(window_adapter) = window_adapter.internal(crate::InternalToken) {
window_adapter.set_mouse_cursor(MouseCursor::NoDrop);
}
drop_event.position = crate::lengths::logical_position_to_api(*position);
event = MouseEvent::DragMove(drop_event);
}
MouseEvent::Exit => {
mouse_input_state.drag_data = None;
}
_ => {}
}
}
let pressed_event = matches!(event, MouseEvent::Pressed { .. });
let released_event = matches!(event, MouseEvent::Released { .. });
let window_adapter = self.window_adapter();
let mut mouse_input_state = self.mouse_input_state.take();
let last_top_item = mouse_input_state.top_item_including_delayed();
if released_event {
mouse_input_state =

View file

@ -997,6 +997,8 @@ fn generate_rtti() -> HashMap<&'static str, Rc<ItemRTTI>> {
rtti_for::<Rotate>(),
rtti_for::<Opacity>(),
rtti_for::<Layer>(),
rtti_for::<DragArea>(),
rtti_for::<DropArea>(),
rtti_for::<ContextMenu>(),
rtti_for::<MenuItem>(),
]

View file

@ -0,0 +1,100 @@
// 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
export component TestCase inherits Window {
width: 100px;
height: 200px;
in-out property <string> result;
out property <bool> contains-drag <=> da.contains-drag;
VerticalLayout {
Rectangle {
background: inner_touch_area.has-hover ? yellow : red;
DragArea {
mime-type: "text/plain";
data: "Hello World";
inner_touch_area := TouchArea {
x: 50px;
width: 50px;
clicked => { result += "InnerClicked;"; }
}
}
}
Rectangle {
background: da.contains-drag ? green : blue;
da := DropArea {
can-drop(event) => {
debug("can-drop", event);
true
}
dropped(event) => {
result += "D[" + event.data + "];";
debug("dropped", event);
}
}
}
}
}
/*
```rust
use slint::{platform::WindowEvent, LogicalPosition, platform::PointerEventButton};
let instance = TestCase::new().unwrap();
assert_eq!(instance.get_contains_drag(), false);
assert_eq!(instance.get_result(), "");
instance.window().dispatch_event(WindowEvent::PointerPressed { position: LogicalPosition::new(20.0, 25.0), button: PointerEventButton::Left });
slint_testing::mock_elapsed_time(20);
assert_eq!(instance.get_contains_drag(), false);
assert_eq!(instance.get_result(), "");
instance.window().dispatch_event(WindowEvent::PointerMoved { position: LogicalPosition::new(21.0, 40.0) });
slint_testing::mock_elapsed_time(20);
assert_eq!(instance.get_contains_drag(), false);
assert_eq!(instance.get_result(), "");
instance.window().dispatch_event(WindowEvent::PointerMoved { position: LogicalPosition::new(22.0, 120.0) });
slint_testing::mock_elapsed_time(20);
assert_eq!(instance.get_contains_drag(), true);
assert_eq!(instance.get_result(), "");
instance.window().dispatch_event(WindowEvent::PointerReleased { position: LogicalPosition::new(22.0, 120.0), button: PointerEventButton::Left });
slint_testing::mock_elapsed_time(20);
assert_eq!(instance.get_result(), "D[Hello World];");
assert_eq!(instance.get_contains_drag(), false);
instance.set_result("".into());
instance.window().dispatch_event(WindowEvent::PointerPressed { position: LogicalPosition::new(51.0, 50.0), button: PointerEventButton::Left });
slint_testing::mock_elapsed_time(20);
assert_eq!(instance.get_contains_drag(), false);
assert_eq!(instance.get_result(), "");
instance.window().dispatch_event(WindowEvent::PointerMoved { position: LogicalPosition::new(52.0, 50.0) });
slint_testing::mock_elapsed_time(20);
assert_eq!(instance.get_contains_drag(), false);
assert_eq!(instance.get_result(), "");
instance.window().dispatch_event(WindowEvent::PointerReleased { position: LogicalPosition::new(52.0, 50.0), button: PointerEventButton::Left });
slint_testing::mock_elapsed_time(20);
assert_eq!(instance.get_result(), "InnerClicked;");
assert_eq!(instance.get_contains_drag(), false);
instance.set_result("".into());
instance.window().dispatch_event(WindowEvent::PointerPressed { position: LogicalPosition::new(51.0, 15.0), button: PointerEventButton::Left });
slint_testing::mock_elapsed_time(20);
assert_eq!(instance.get_contains_drag(), false);
assert_eq!(instance.get_result(), "");
instance.window().dispatch_event(WindowEvent::PointerMoved { position: LogicalPosition::new(58.0, 40.0) });
slint_testing::mock_elapsed_time(20);
assert_eq!(instance.get_contains_drag(), false);
assert_eq!(instance.get_result(), "");
instance.window().dispatch_event(WindowEvent::PointerMoved { position: LogicalPosition::new(58.0, 120.0) });
assert_eq!(instance.get_contains_drag(), true);
assert_eq!(instance.get_result(), "");
instance.window().dispatch_event(WindowEvent::PointerReleased { position: LogicalPosition::new(58.0, 20.0), button: PointerEventButton::Left });
slint_testing::mock_elapsed_time(20);
assert_eq!(instance.get_contains_drag(), false);
assert_eq!(instance.get_result(), "");
```
*/

View file

@ -213,8 +213,10 @@ pub fn extract_builtin_structs() -> std::collections::BTreeMap<String, StructDoc
// `StateInfo` should not be in the documentation, so remove it again
structs.remove("StateInfo");
// Experimental type
// Internal type
structs.remove("MenuEntry");
// Experimental type
structs.remove("DropEvent");
structs
}