From 3823c1e8da1c8748677642dcdd5396de35ca3cc0 Mon Sep 17 00:00:00 2001 From: Olivier Goffart Date: Tue, 17 Jun 2025 15:46:11 +0200 Subject: [PATCH] Experimental support for Drag & Drop Add a `DragArea` and `DropArea` elements. It is currently gated as experimental. --- api/cpp/CMakeLists.txt | 1 + api/cpp/cbindgen.rs | 97 ++---- api/cpp/include/slint_window.h | 33 +- internal/backends/qt/qt_widgets/button.rs | 3 + internal/backends/qt/qt_widgets/scrollview.rs | 1 + internal/backends/qt/qt_widgets/slider.rs | 1 + internal/backends/qt/qt_widgets/spinbox.rs | 1 + internal/backends/qt/qt_widgets/tabwidget.rs | 3 + internal/common/builtin_structs.rs | 15 + internal/compiler/builtins.slint | 16 + internal/compiler/typeregister.rs | 9 +- internal/core/input.rs | 68 +++- internal/core/items.rs | 17 +- internal/core/items/drag_n_drop.rs | 312 ++++++++++++++++++ internal/core/items/flickable.rs | 4 + internal/core/items/input_items.rs | 5 + internal/core/rtti.rs | 1 + internal/core/window.rs | 30 +- internal/interpreter/dynamic_item_tree.rs | 2 + tests/cases/elements/dragarea_droparea.slint | 100 ++++++ xtask/src/slintdocs.rs | 4 +- 21 files changed, 610 insertions(+), 113 deletions(-) create mode 100644 internal/core/items/drag_n_drop.rs create mode 100644 tests/cases/elements/dragarea_droparea.slint diff --git a/api/cpp/CMakeLists.txt b/api/cpp/CMakeLists.txt index 450257f0a8..a147316a70 100644 --- a/api/cpp/CMakeLists.txt +++ b/api/cpp/CMakeLists.txt @@ -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) diff --git a/api/cpp/cbindgen.rs b/api/cpp/cbindgen.rs index 570cf88c7c..c9fceb39dc 100644 --- a/api/cpp/cbindgen.rs +++ b/api/cpp/cbindgen.rs @@ -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; + 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; - using LogicalLength = float; struct ItemTreeVTable; struct ItemVTable; using types::IntRect; + using types::Size; + using types::MouseEvent; } template class Model; }", diff --git a/api/cpp/include/slint_window.h b/api/cpp/include/slint_window.h index 88a194a04d..5ab15f8133 100644 --- a/api/cpp/include/slint_window.h +++ b/api/cpp/include/slint_window.h @@ -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 diff --git a/internal/backends/qt/qt_widgets/button.rs b/internal/backends/qt/qt_widgets/button.rs index 7e77f7ad7b..70f0e98463 100644 --- a/internal/backends/qt/qt_widgets/button.rs +++ b/internal/backends/qt/qt_widgets/button.rs @@ -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(); diff --git a/internal/backends/qt/qt_widgets/scrollview.rs b/internal/backends/qt/qt_widgets/scrollview.rs index 38a46d68dd..afe629a0d1 100644 --- a/internal/backends/qt/qt_widgets/scrollview.rs +++ b/internal/backends/qt/qt_widgets/scrollview.rs @@ -225,6 +225,7 @@ impl Item for NativeScrollView { } InputEventResult::EventAccepted } + MouseEvent::DragMove(..) | MouseEvent::Drop(..) => InputEventResult::EventIgnored, }; self.data.set(data); result diff --git a/internal/backends/qt/qt_widgets/slider.rs b/internal/backends/qt/qt_widgets/slider.rs index 620120ca11..2e621ecb48 100644 --- a/internal/backends/qt/qt_widgets/slider.rs +++ b/internal/backends/qt/qt_widgets/slider.rs @@ -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; diff --git a/internal/backends/qt/qt_widgets/spinbox.rs b/internal/backends/qt/qt_widgets/spinbox.rs index 769779b4ab..3fbabbb71b 100644 --- a/internal/backends/qt/qt_widgets/spinbox.rs +++ b/internal/backends/qt/qt_widgets/spinbox.rs @@ -224,6 +224,7 @@ impl Item for NativeSpinBox { true } + MouseEvent::DragMove(..) | MouseEvent::Drop(..) => false, }; data.active_controls = new_control; if changed { diff --git a/internal/backends/qt/qt_widgets/tabwidget.rs b/internal/backends/qt/qt_widgets/tabwidget.rs index 22a322f47f..10a3bfd5f5 100644 --- a/internal/backends/qt/qt_widgets/tabwidget.rs +++ b/internal/backends/qt/qt_widgets/tabwidget.rs @@ -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; diff --git a/internal/common/builtin_structs.rs b/internal/common/builtin_structs.rs index c423ba730a..6c9ea3fb99 100644 --- a/internal/common/builtin_structs.rs +++ b/internal/common/builtin_structs.rs @@ -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 { diff --git a/internal/compiler/builtins.slint b/internal/compiler/builtins.slint index 531f7ce3e1..1fa775385e 100644 --- a/internal/compiler/builtins.slint +++ b/internal/compiler/builtins.slint @@ -186,6 +186,22 @@ export component SwipeGestureHandler { //-default_size_binding:expands_to_parent_geometry } +export component DragArea { + in property enabled: true; + //out property dragging; + in property mime-type; + in property data; + //-default_size_binding:expands_to_parent_geometry + +} +export component DropArea { + in property enabled: true; + callback can-drop(event: DropEvent) -> bool; + callback dropped(event: DropEvent); + out property contains-drag; + //-default_size_binding:expands_to_parent_geometry +} + component MenuItem { in property title; callback activated(); diff --git a/internal/compiler/typeregister.rs b/internal/compiler/typeregister.rs index 5871b2e7ed..72e8e44911 100644 --- a/internal/compiler/typeregister.rs +++ b/internal/compiler/typeregister.rs @@ -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> { 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)) } diff --git a/internal/core/input.rs b/internal/core/input.rs index 3c1760b89a..299ac553c9 100644 --- a/internal/core/input.rs +++ b/internal/core/input.rs @@ -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, delayed: Option<(crate::timers::Timer, MouseEvent)>, delayed_exit_items: Vec, } @@ -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::().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::().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) + } } } diff --git a/internal/core/items.rs b/internal/core/items.rs index 44c9ce7837..a87b9bfdfb 100644 --- a/internal/core/items.rs +++ b/internal/core/items.rs @@ -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; @@ -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")] diff --git a/internal/core/items/drag_n_drop.rs b/internal/core/items/drag_n_drop.rs new file mode 100644 index 0000000000..a7de9e05db --- /dev/null +++ b/internal/core/items/drag_n_drop.rs @@ -0,0 +1,312 @@ +// Copyright © SixtyFPS GmbH +// 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, + pub mime_type: Property, + pub data: Property, + pressed: Cell, + pressed_position: Cell, + 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, + _self_rc: &ItemRc, + ) -> LayoutInfo { + LayoutInfo { stretch: 1., ..LayoutInfo::default() } + } + + fn input_event_filter_before_children( + self: Pin<&Self>, + event: &MouseEvent, + _window_adapter: &Rc, + _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, + _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, + _self_rc: &ItemRc, + ) -> KeyEventResult { + KeyEventResult::EventIgnored + } + + fn focus_event( + self: Pin<&Self>, + _: &FocusEvent, + _window_adapter: &Rc, + _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, + _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, + pub contains_drag: Property, + pub can_drop: Callback, + pub dropped: Callback, + + 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, + _self_rc: &ItemRc, + ) -> LayoutInfo { + LayoutInfo { stretch: 1., ..LayoutInfo::default() } + } + + fn input_event_filter_before_children( + self: Pin<&Self>, + _: &MouseEvent, + _window_adapter: &Rc, + _self_rc: &ItemRc, + ) -> InputEventFilterResult { + InputEventFilterResult::ForwardEvent + } + + fn input_event( + self: Pin<&Self>, + event: &MouseEvent, + window_adapter: &Rc, + _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, + _self_rc: &ItemRc, + ) -> KeyEventResult { + KeyEventResult::EventIgnored + } + + fn focus_event( + self: Pin<&Self>, + _: &FocusEvent, + _window_adapter: &Rc, + _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, + _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(); +} diff --git a/internal/core/items/flickable.rs b/internal/core/items/flickable.rs index d5175b402c..9f99246db7 100644 --- a/internal/core/items/flickable.rs +++ b/internal/core/items/flickable.rs @@ -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, } } diff --git a/internal/core/items/input_items.rs b/internal/core/items/input_items.rs index a077f7b50c..77100f70a9 100644 --- a/internal/core/items/input_items.rs +++ b/internal/core/items/input_items.rs @@ -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, } } diff --git a/internal/core/rtti.rs b/internal/core/rtti.rs index dc7101eac0..8f5a88fe83 100644 --- a/internal/core/rtti.rs +++ b/internal/core/rtti.rs @@ -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::$Name,)* ]; diff --git a/internal/core/window.rs b/internal/core/window.rs index 83526fe8b3..63f5228105 100644 --- a/internal/core/window.rs +++ b/internal/core/window.rs @@ -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 = diff --git a/internal/interpreter/dynamic_item_tree.rs b/internal/interpreter/dynamic_item_tree.rs index 537daa4f74..8722ccf4b2 100644 --- a/internal/interpreter/dynamic_item_tree.rs +++ b/internal/interpreter/dynamic_item_tree.rs @@ -997,6 +997,8 @@ fn generate_rtti() -> HashMap<&'static str, Rc> { rtti_for::(), rtti_for::(), rtti_for::(), + rtti_for::(), + rtti_for::(), rtti_for::(), rtti_for::(), ] diff --git a/tests/cases/elements/dragarea_droparea.slint b/tests/cases/elements/dragarea_droparea.slint new file mode 100644 index 0000000000..afaabca488 --- /dev/null +++ b/tests/cases/elements/dragarea_droparea.slint @@ -0,0 +1,100 @@ +// Copyright © SixtyFPS GmbH +// 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 result; + out property 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(), ""); +``` + +*/ diff --git a/xtask/src/slintdocs.rs b/xtask/src/slintdocs.rs index 1806f39d60..cf4932d5d0 100644 --- a/xtask/src/slintdocs.rs +++ b/xtask/src/slintdocs.rs @@ -213,8 +213,10 @@ pub fn extract_builtin_structs() -> std::collections::BTreeMap