Repository: zng-ui/zng (language: rust)

Issue:
Implement drag & drop
* Drag/move inside window. Integrate with `touch_transform`.
* Drag&drop across apps with visual feedback.
   - Visual can be a screen capture of the widget by default. Browsers do this, with some fade-out mask effect and text selection clipping.
   - At  the view-process end we should await for Winit to implement this feature. 


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2bab8bb83..b4c9b3512 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Unreleased
 
+* Add missing `zng::var::OnVarArgs`.
+* **Breaking** Implemented drag&drop on the view-process API.
+* **Breaking** `Event::visit_subscribers` and `Command::visit_scopes` visitor closures now must return `ControlFlow`.
+* **Breaking** Refactored drag&drop in the view API to be general purpose.
 * The view API `ReferenceFrameId` type now reserves some IDs for the view process.
 * Add `border_img` and other related properties and types.
 * **Breaking** Moved `zng::render::RepeatMode` to `zng::widget`.
diff --git a/crates/zng-app/src/event.rs b/crates/zng-app/src/event.rs
index e0bed986b..fd2940ed9 100644
--- a/crates/zng-app/src/event.rs
+++ b/crates/zng-app/src/event.rs
@@ -5,7 +5,7 @@ use std::{
     fmt,
     marker::PhantomData,
     mem,
-    ops::Deref,
+    ops::{ControlFlow, Deref},
     sync::{
         atomic::{AtomicBool, Ordering},
         Arc,
@@ -151,7 +151,7 @@ impl<A: EventArgs> Event<A> {
     ///
     /// Note that trying to subscribe or add hook inside `visit` will deadlock. Inside `visit` you can notify the event and
     /// generate event updates.
-    pub fn visit_subscribers(&self, visit: impl FnMut(WidgetId)) {
+    pub fn visit_subscribers<T>(&self, visit: impl FnMut(WidgetId) -> ControlFlow<T>) -> Option<T> {
         self.as_any().visit_subscribers(visit)
     }
 
@@ -446,10 +446,14 @@ impl AnyEvent {
     ///
     /// Note that trying to subscribe or add hook inside `visit` will deadlock. Inside `visit` you can notify the event and
     /// generate event updates.
-    pub fn visit_subscribers(&self, mut visit: impl FnMut(WidgetId)) {
+    pub fn visit_subscribers<T>(&self, mut visit: impl FnMut(WidgetId) -> ControlFlow<T>) -> Option<T> {
         for sub in self.local.read().widget_subs.keys() {
-            visit(*sub);
+            match visit(*sub) {
+                ControlFlow::Continue(_) => continue,
+                ControlFlow::Break(r) => return Some(r),
+            }
         }
+        None
     }
 
     /// Returns `true` if any app level callback is registered for this event.
diff --git a/crates/zng-app/src/event/command.rs b/crates/zng-app/src/event/command.rs
index 80058a7b9..0d4516678 100644
--- a/crates/zng-app/src/event/command.rs
+++ b/crates/zng-app/src/event/command.rs
@@ -511,11 +511,15 @@ impl Command {
     /// Calls `visitor` for each scope of this command.
     ///
     /// Note that scoped commands are removed if unused, see [`with_meta`](Self::with_meta) for more details.
-    pub fn visit_scopes(&self, mut visitor: impl FnMut(Command)) {
+    pub fn visit_scopes<T>(&self, mut visitor: impl FnMut(Command) -> ControlFlow<T>) -> Option<T> {
         let read = self.local.read();
         for &scope in read.scopes.keys() {
-            visitor(self.scoped(scope));
+            match visitor(self.scoped(scope)) {
+                ControlFlow::Continue(_) => continue,
+                ControlFlow::Break(r) => return Some(r),
+            }
         }
+        None
     }
 
     /// Schedule a command update without param.
@@ -525,7 +529,7 @@ impl Command {
 
     /// Schedule a command update without param for all scopes inside `parent`.
     pub fn notify_descendants(&self, parent: &WidgetInfo) {
-        self.visit_scopes(|parse_cmd| {
+        self.visit_scopes::<()>(|parse_cmd| {
             if let CommandScope::Widget(id) = parse_cmd.scope() {
                 if let Some(scope) = parent.tree().get(id) {
                     if scope.is_descendant(parent) {
@@ -533,6 +537,7 @@ impl Command {
                     }
                 }
             }
+            ControlFlow::Continue(())
         });
     }
 
diff --git a/crates/zng-app/src/running.rs b/crates/zng-app/src/running.rs
index 5ae730b70..ec0becdd0 100644
--- a/crates/zng-app/src/running.rs
+++ b/crates/zng-app/src/running.rs
@@ -211,17 +211,34 @@ impl<E: AppExtension> RunningApp<E> {
                 );
                 self.notify_event(RAW_WINDOW_CHANGED_EVENT.new_update(args), observer);
             }
-            Event::DroppedFile { window: w_id, file } => {
-                let args = RawDroppedFileArgs::now(window_id(w_id), file);
-                self.notify_event(RAW_DROPPED_FILE_EVENT.new_update(args), observer);
+            Event::DragHovered { window, data, allowed } => {
+                let args = RawDragHoveredArgs::now(window_id(window), data, allowed);
+                self.notify_event(RAW_DRAG_HOVERED_EVENT.new_update(args), observer);
             }
-            Event::HoveredFile { window: w_id, file } => {
-                let args = RawHoveredFileArgs::now(window_id(w_id), file);
-                self.notify_event(RAW_HOVERED_FILE_EVENT.new_update(args), observer);
+            Event::DragMoved {
+                window,
+                coalesced_pos,
+                position,
+            } => {
+                let args = RawDragMovedArgs::now(window_id(window), coalesced_pos, position);
+                self.notify_event(RAW_DRAG_MOVED_EVENT.new_update(args), observer);
+            }
+            Event::DragDropped {
+                window,
+                data,
+                allowed,
+                drop_id,
+            } => {
+                let args = RawDragDroppedArgs::now(window_id(window), data, allowed, drop_id);
+                self.notify_event(RAW_DRAG_DROPPED_EVENT.new_update(args), observer);
+            }
+            Event::DragCancelled { window } => {
+                let args = RawDragCancelledArgs::now(window_id(window));
+                self.notify_event(RAW_DRAG_CANCELLED_EVENT.new_update(args), observer);
             }
-            Event::HoveredFileCancelled(w_id) => {
-                let args = RawHoveredFileCancelledArgs::now(window_id(w_id));
-                self.notify_event(RAW_HOVERED_FILE_CANCELLED_EVENT.new_update(args), observer);
+            Event::AppDragEnded { window, drag, applied } => {
+                let args = RawAppDragEndedArgs::now(window_id(window), drag, applied);
+                self.notify_event(RAW_APP_DRAG_ENDED_EVENT.new_update(args), observer);
             }
             Event::FocusChanged { prev, new } => {
                 let args = RawWindowFocusArgs::now(prev.map(window_id), new.map(window_id));
@@ -1394,7 +1411,6 @@ impl AppProcessService {
 
 /// App events.
 #[derive(Debug)]
-#[expect(clippy::large_enum_variant)]
 pub(crate) enum AppEvent {
     /// Event from the View Process.
     ViewEvent(zng_view_api::Event),
diff --git a/crates/zng-app/src/update.rs b/crates/zng-app/src/update.rs
index 3ac29b368..7b492282e 100644
--- a/crates/zng-app/src/update.rs
+++ b/crates/zng-app/src/update.rs
@@ -1771,7 +1771,7 @@ impl OnUpdateHandle {
 
     /// If another handle has called [`perm`](Self::perm).
     ///
-    /// If `true` the var binding will stay active until the app exits, unless [`unsubscribe`](Self::unsubscribe) is called.
+    /// If `true` the handler will stay active until the app exits, unless [`unsubscribe`](Self::unsubscribe) is called.
     pub fn is_permanent(&self) -> bool {
         self.0.is_permanent()
     }
diff --git a/crates/zng-app/src/view_process.rs b/crates/zng-app/src/view_process.rs
index 17f9d35c5..02a5f17e7 100644
--- a/crates/zng-app/src/view_process.rs
+++ b/crates/zng-app/src/view_process.rs
@@ -26,6 +26,7 @@ use zng_view_api::{
     api_extension::{ApiExtensionId, ApiExtensionName, ApiExtensionPayload, ApiExtensionRecvError, ApiExtensions},
     config::{AnimationsConfig, ChromeConfig, ColorsConfig, FontAntiAliasing, LocaleConfig, MultiClickConfig, TouchConfig},
     dialog::{FileDialog, FileDialogResponse, MsgDialog, MsgDialogResponse},
+    drag_drop::{DragDropData, DragDropEffect, DragDropError},
     font::FontOptions,
     image::{ImageMaskMode, ImagePpi, ImageRequest, ImageTextureId},
     ipc::{IpcBytes, IpcBytesReceiver},
@@ -33,7 +34,7 @@ use zng_view_api::{
         CursorIcon, FocusIndicator, FrameRequest, FrameUpdateRequest, HeadlessOpenData, HeadlessRequest, MonitorInfo, RenderMode,
         ResizeDirection, VideoMode, WindowButton, WindowRequest, WindowStateAll,
     },
-    Event, FocusResult, ViewProcessGen, ViewProcessOffline,
+    DragDropId, Event, FocusResult, ViewProcessGen, ViewProcessOffline,
 };
 
 use zng_view_api::{
@@ -826,6 +827,24 @@ impl ViewWindow {
         self.0.call(|id, p| p.drag_resize(id, direction))
     }
 
+    /// Start a drag and drop operation, if the window is pressed.
+    ///
+    /// A [`RAW_APP_DRAG_ENDED_EVENT`] will be received when the operation finishes.
+    ///
+    /// [`RAW_APP_DRAG_ENDED_EVENT`]: raw_events::RAW_APP_DRAG_ENDED_EVENT
+    pub fn start_drag_drop(
+        &self,
+        data: Vec<DragDropData>,
+        allowed_effects: DragDropEffect,
+    ) -> Result<std::result::Result<DragDropId, DragDropError>> {
+        self.0.call(|id, p| p.start_drag_drop(id, data, allowed_effects))
+    }
+
+    /// Notify the drag source of what effect was applied for a received drag&drop.
+    pub fn drag_dropped(&self, drop_id: DragDropId, applied: DragDropEffect) -> Result<()> {
+        self.0.call(|id, p| p.drag_dropped(id, drop_id, applied))
+    }
+
     /// Open system title bar context menu.
     pub fn open_title_bar_context_menu(&self, position: DipPoint) -> Result<()> {
         self.0.call(|id, p| p.open_title_bar_context_menu(id, position))
diff --git a/crates/zng-app/src/view_process/raw_events.rs b/crates/zng-app/src/view_process/raw_events.rs
index ab145fa19..5e98168f6 100644
--- a/crates/zng-app/src/view_process/raw_events.rs
+++ b/crates/zng-app/src/view_process/raw_events.rs
@@ -14,8 +14,6 @@
 //! [`notify`]: crate::event::Event::notify
 //! [`DeviceId`]: crate::view_process::raw_device_events::DeviceId
 
-use std::path::PathBuf;
-
 use zng_layout::unit::{DipPoint, DipSideOffsets, DipSize, Factor, PxPoint, PxRect};
 use zng_txt::Txt;
 use zng_view_api::{
@@ -23,11 +21,12 @@ use zng_view_api::{
     config::{
         AnimationsConfig, ChromeConfig, ColorsConfig, FontAntiAliasing, KeyRepeatConfig, LocaleConfig, MultiClickConfig, TouchConfig,
     },
+    drag_drop::{DragDropData, DragDropEffect},
     keyboard::{Key, KeyCode, KeyLocation, KeyState},
     mouse::{ButtonState, MouseButton, MouseScrollDelta},
     touch::{TouchPhase, TouchUpdate},
     window::{EventCause, FrameId, FrameWaitId, HeadlessOpenData, MonitorInfo, WindowStateAll},
-    AxisId, Ime,
+    AxisId, DragDropId, Ime,
 };
 
 use crate::{
@@ -259,13 +258,15 @@ event_args! {
         }
     }
 
-    /// Arguments for the [`RAW_DROPPED_FILE_EVENT`].
-    pub struct RawDroppedFileArgs {
-        /// Window where it was dropped.
+    /// Arguments for the [`RAW_DRAG_HOVERED_EVENT`].
+    pub struct RawDragHoveredArgs {
+        /// Window where it was dragged over.
         pub window_id: WindowId,
 
-        /// Path to file that was dropped.
-        pub file: PathBuf,
+        /// Data payload.
+        pub data: Vec<DragDropData>,
+        /// Allowed effects.
+        pub allowed: DragDropEffect,
 
         ..
 
@@ -275,13 +276,18 @@ event_args! {
         }
     }
 
-    /// Arguments for the [`RAW_HOVERED_FILE_EVENT`].
-    pub struct RawHoveredFileArgs {
-        /// Window where it was dragged over.
+    /// Arguments for the [`RAW_DRAG_MOVED_EVENT`].
+    pub struct RawDragMovedArgs {
+        /// Window that is hovered by drag&drop.
         pub window_id: WindowId,
 
-        /// Path to file that was dragged over the window.
-        pub file: PathBuf,
+        /// Cursor positions in between the previous event and this one.
+        ///
+        /// Drag move events can be coalesced, i.e. multiple moves packed into a single event.
+        pub coalesced_pos: Vec<DipPoint>,
+
+        /// Position of the cursor over the window, (0, 0) is the top-left.
+        pub position: DipPoint,
 
         ..
 
@@ -291,12 +297,30 @@ event_args! {
         }
     }
 
-    /// Arguments for the [`RAW_HOVERED_FILE_CANCELLED_EVENT`].
-    ///
-    /// The file is the one that was last [hovered] into the window.
-    ///
-    /// [hovered]: RAW_HOVERED_FILE_EVENT
-    pub struct RawHoveredFileCancelledArgs {
+    /// Arguments for the [`RAW_DRAG_DROPPED_EVENT`].
+    pub struct RawDragDroppedArgs {
+        /// Window where it was dropped.
+        pub window_id: WindowId,
+
+        /// Data payload.
+        pub data: Vec<DragDropData>,
+        /// Allowed effects.
+        pub allowed: DragDropEffect,
+        /// ID of this drop operation.
+        ///
+        /// Handlers must call `drag_dropped` with this ID and what effect was applied to the data.
+        pub drop_id: DragDropId,
+
+        ..
+
+        /// Broadcast to all widgets.
+        fn delivery_list(&self, list: &mut UpdateDeliveryList) {
+            list.search_all()
+        }
+    }
+
+    /// Arguments for the [`RAW_DRAG_CANCELLED_EVENT`].
+    pub struct RawDragCancelledArgs {
         /// Window where the file was previously dragged over.
         pub window_id: WindowId,
 
@@ -308,6 +332,27 @@ event_args! {
         }
     }
 
+    /// Arguments for the [`RAW_APP_DRAG_ENDED_EVENT`].
+    pub struct RawAppDragEndedArgs {
+        /// Window that started the drag operation.
+        pub window_id: WindowId,
+
+        /// ID of the drag & drop operation.
+        pub id: DragDropId,
+
+        /// Effect applied to the data by the drop target.
+        ///
+        /// Is a single flag if the data was dropped in a valid drop target, or is empty if was canceled.
+        pub applied: DragDropEffect,
+
+        ..
+
+        /// Broadcast to all widgets.
+        fn delivery_list(&self, list: &mut UpdateDeliveryList) {
+            list.search_all()
+        }
+    }
+
     /// Arguments for the [`RAW_MOUSE_MOVED_EVENT`].
     pub struct RawMouseMovedArgs {
         /// Window the mouse was moved over.
@@ -704,18 +749,20 @@ event! {
     /// A window was destroyed.
     pub static RAW_WINDOW_CLOSE_EVENT: RawWindowCloseArgs;
 
-    /// A file was drag-dropped on a window.
-    pub static RAW_DROPPED_FILE_EVENT: RawDroppedFileArgs;
+    /// Data was dragged over a window.
+    pub static RAW_DRAG_HOVERED_EVENT: RawDragHoveredArgs;
 
-    /// A file was dragged over a window.
-    ///
-    /// If the file is dropped [`RAW_DROPPED_FILE_EVENT`] will raise.
-    pub static RAW_HOVERED_FILE_EVENT: RawHoveredFileArgs;
+    /// Data dragging over the window has moved.
+    pub static RAW_DRAG_MOVED_EVENT: RawDragMovedArgs;
 
-    /// A dragging file was moved away from the window or the operation was cancelled.
-    ///
-    /// The file is the last one that emitted a [`RAW_HOVERED_FILE_EVENT`].
-    pub static RAW_HOVERED_FILE_CANCELLED_EVENT: RawHoveredFileCancelledArgs;
+    /// Data was drag-dropped on a window.
+    pub static RAW_DRAG_DROPPED_EVENT: RawDragDroppedArgs;
+
+    /// Data was dragged away from the window or the operation was cancelled.
+    pub static RAW_DRAG_CANCELLED_EVENT: RawDragCancelledArgs;
+
+    /// Drag & drop operation started by the app has dropped or was cancelled.
+    pub static RAW_APP_DRAG_ENDED_EVENT: RawAppDragEndedArgs;
 
     /// Mouse pointer moved over a window.
     pub static RAW_MOUSE_MOVED_EVENT: RawMouseMovedArgs;
diff --git a/crates/zng-app/src/widget/info.rs b/crates/zng-app/src/widget/info.rs
index 9b1e6e9f7..d2af1ccd7 100644
--- a/crates/zng-app/src/widget/info.rs
+++ b/crates/zng-app/src/widget/info.rs
@@ -304,7 +304,7 @@ impl WidgetInfoTree {
         }
 
         let mut changes = IdMap::new();
-        TRANSFORM_CHANGED_EVENT.visit_subscribers(|wid| {
+        TRANSFORM_CHANGED_EVENT.visit_subscribers::<()>(|wid| {
             if let Some(wgt) = self.get(wid) {
                 let transform = wgt.inner_transform();
                 match frame.transform_changed_subs.entry(wid) {
@@ -319,6 +319,7 @@ impl WidgetInfoTree {
                     }
                 }
             }
+            ops::ControlFlow::Continue(())
         });
         if !changes.is_empty() {
             if (frame.transform_changed_subs.len() - changes.len()) > 500 {
@@ -332,7 +333,7 @@ impl WidgetInfoTree {
         drop(frame); // wgt.visibility can read frame
 
         let mut changes = IdMap::new();
-        VISIBILITY_CHANGED_EVENT.visit_subscribers(|wid| {
+        VISIBILITY_CHANGED_EVENT.visit_subscribers::<()>(|wid| {
             if let Some(wgt) = self.get(wid) {
                 let visibility = wgt.visibility();
                 let mut frame = self.0.frame.write();
@@ -348,6 +349,7 @@ impl WidgetInfoTree {
                     }
                 }
             }
+            ops::ControlFlow::Continue(())
         });
         if !changes.is_empty() {
             if (self.0.frame.read().visibility_changed_subs.len() - changes.len()) > 500 {
diff --git a/crates/zng-app/src/widget/info/builder.rs b/crates/zng-app/src/widget/info/builder.rs
index 2f9439196..0f8f6f9ca 100644
--- a/crates/zng-app/src/widget/info/builder.rs
+++ b/crates/zng-app/src/widget/info/builder.rs
@@ -362,7 +362,7 @@ impl WidgetInfoBuilder {
             WIDGET_INFO_CHANGED_EVENT.notify(args);
 
             let mut targets = IdSet::default();
-            INTERACTIVITY_CHANGED_EVENT.visit_subscribers(|wid| {
+            INTERACTIVITY_CHANGED_EVENT.visit_subscribers::<()>(|wid| {
                 if let Some(wgt) = tree.get(wid) {
                     let prev = prev_tree.get(wid).map(|w| w.interactivity());
                     let new_int = wgt.interactivity();
@@ -370,6 +370,7 @@ impl WidgetInfoBuilder {
                         targets.insert(wid);
                     }
                 }
+                ops::ControlFlow::Continue(())
             });
             if !targets.is_empty() {
                 let args = InteractivityChangedArgs::now(prev_tree, tree.clone(), targets);
diff --git a/crates/zng-ext-input/Cargo.toml b/crates/zng-ext-input/Cargo.toml
index 697bc073c..4b8911583 100644
--- a/crates/zng-ext-input/Cargo.toml
+++ b/crates/zng-ext-input/Cargo.toml
@@ -29,3 +29,4 @@ parking_lot = "0.12"
 serde = { version = "1.0", features = ["derive"] }
 bitflags = { version = "2.5", features = ["serde", "bytemuck"] }
 atomic = "0.6"
+uuid = "1.1"
\ No newline at end of file
diff --git a/crates/zng-ext-input/src/drag_drop.rs b/crates/zng-ext-input/src/drag_drop.rs
new file mode 100644
index 000000000..c4982209d
--- /dev/null
+++ b/crates/zng-ext-input/src/drag_drop.rs
@@ -0,0 +1,810 @@
+//! Drag & drop gesture events and service.
+
+use std::{mem, sync::Arc};
+
+use parking_lot::Mutex;
+use zng_app::{
+    event::{event, event_args, AnyEventArgs},
+    static_id,
+    update::{EventUpdate, UPDATES},
+    view_process::raw_events::{
+        RAW_APP_DRAG_ENDED_EVENT, RAW_DRAG_CANCELLED_EVENT, RAW_DRAG_DROPPED_EVENT, RAW_DRAG_HOVERED_EVENT, RAW_DRAG_MOVED_EVENT,
+    },
+    widget::{
+        info::{HitTestInfo, InteractionPath, WidgetInfo, WidgetInfoBuilder},
+        WidgetId, WIDGET,
+    },
+    window::WindowId,
+    AppExtension,
+};
+use zng_app_context::app_local;
+use zng_ext_window::{NestedWindowWidgetInfoExt as _, WINDOWS, WINDOWS_DRAG_DROP};
+use zng_handle::{Handle, HandleOwner, WeakHandle};
+use zng_layout::unit::{DipPoint, DipToPx as _, PxToDip as _};
+use zng_state_map::StateId;
+use zng_txt::{formatx, Txt};
+use zng_var::{var, ArcVar, ReadOnlyArcVar, Var};
+use zng_view_api::{mouse::ButtonState, touch::TouchPhase, DragDropId};
+
+use crate::{mouse::MOUSE_INPUT_EVENT, touch::TOUCH_INPUT_EVENT};
+
+pub use zng_view_api::drag_drop::{DragDropData, DragDropEffect};
+
+/// Application extension that provides drag&drop events and service.
+///
+/// # Events
+///
+/// Events this extension provides.
+///
+/// * [`DROP_EVENT`]
+/// * [`DRAG_HOVERED_EVENT`]
+/// * [`DRAG_MOVE_EVENT`]
+/// * [`DRAG_START_EVENT`]
+/// * [`DRAG_END_EVENT`]
+/// * [`DROP_EVENT`]
+///
+/// # Services
+///
+/// Services this extension provides.
+///
+/// * [`DRAG_DROP`]
+#[derive(Default)]
+pub struct DragDropManager {
+    // last cursor move position (scaled).
+    pos: DipPoint,
+    // last cursor move over `pos_window` and source device.
+    pos_window: Option<WindowId>,
+    // last cursor move hit-test (on the pos_window or a nested window).
+    hits: Option<HitTestInfo>,
+    hovered: Option<InteractionPath>,
+}
+
+impl AppExtension for DragDropManager {
+    fn event_preview(&mut self, update: &mut EventUpdate) {
+        let mut update_sv = false;
+        if let Some(args) = RAW_DRAG_DROPPED_EVENT.on(update) {
+            // system drop
+            let mut sv = DRAG_DROP_SV.write();
+            let len = sv.system_dragging.len();
+            for data in &args.data {
+                sv.system_dragging.retain(|d| d != data);
+            }
+            update_sv = len != sv.system_dragging.len();
+
+            // view-process can notify multiple drops in sequence with the same ID, so we only notify DROP_EVENT
+            // on he next update
+            if self.pos_window == Some(args.window_id) {
+                if let Some(hovered) = &self.hovered {
+                    match &mut sv.pending_drop {
+                        Some((id, target, data, allowed)) => {
+                            if target != hovered {
+                                tracing::error!("drop sequence across different hovered")
+                            } else if *id != args.drop_id {
+                                tracing::error!("drop_id changed mid sequence")
+                            } else if *allowed != args.allowed {
+                                tracing::error!("allowed effects changed mid sequence")
+                            } else {
+                                data.extend(args.data.iter().cloned());
+                            }
+                        }
+                        None => sv.pending_drop = Some((args.drop_id, hovered.clone(), args.data.clone(), args.allowed)),
+                    }
+                }
+            }
+            UPDATES.update(None);
+        } else if let Some(args) = RAW_DRAG_HOVERED_EVENT.on(update) {
+            // system drag hover window
+            update_sv = true;
+            DRAG_DROP_SV.write().system_dragging.extend(args.data.iter().cloned());
+        } else if let Some(args) = RAW_DRAG_MOVED_EVENT.on(update) {
+            // code adapted from the MouseManager implementation for mouse hovered
+            let moved = self.pos != args.position || self.pos_window != Some(args.window_id);
+            if moved {
+                self.pos = args.position;
+                self.pos_window = Some(args.window_id);
+
+                let mut position = args.position;
+
+                // mouse_move data
+                let mut frame_info = match WINDOWS.widget_tree(args.window_id) {
+                    Ok(f) => f,
+                    Err(_) => {
+                        // window not found
+                        if let Some(hovered) = self.hovered.take() {
+                            DRAG_HOVERED_EVENT.notify(DragHoveredArgs::now(
+                                Some(hovered),
+                                None,
+                                position,
+                                HitTestInfo::no_hits(args.window_id),
+                            ));
+                            self.pos_window = None;
+                        }
+                        return;
+                    }
+                };
+
+                let mut pos_hits = frame_info.root().hit_test(position.to_px(frame_info.scale_factor()));
+
+                let target = if let Some(t) = pos_hits.target() {
+                    if let Some(w) = frame_info.get(t.widget_id) {
+                        if let Some(f) = w.nested_window_tree() {
+                            // nested window hit
+                            frame_info = f;
+                            let factor = frame_info.scale_factor();
+                            let pos = position.to_px(factor);
+                            let pos = w.inner_transform().inverse().and_then(|t| t.transform_point(pos)).unwrap_or(pos);
+                            pos_hits = frame_info.root().hit_test(pos);
+                            position = pos.to_dip(factor);
+                            pos_hits
+                                .target()
+                                .and_then(|h| frame_info.get(h.widget_id))
+                                .map(|w| w.interaction_path())
+                                .unwrap_or_else(|| frame_info.root().interaction_path())
+                        } else {
+                            w.interaction_path()
+                        }
+                    } else {
+                        tracing::error!("hits target `{}` not found", t.widget_id);
+                        frame_info.root().interaction_path()
+                    }
+                } else {
+                    frame_info.root().interaction_path()
+                }
+                .unblocked();
+
+                self.hits = Some(pos_hits.clone());
+
+                // drag_enter/leave.
+                let hovered_args = if self.hovered != target {
+                    let prev_target = mem::replace(&mut self.hovered, target.clone());
+                    let args = DragHoveredArgs::now(prev_target, target.clone(), position, pos_hits.clone());
+                    Some(args)
+                } else {
+                    None
+                };
+
+                // mouse_move
+                if let Some(target) = target {
+                    let args = DragMoveArgs::now(frame_info.window_id(), args.coalesced_pos.clone(), position, pos_hits, target);
+                    DRAG_MOVE_EVENT.notify(args);
+                }
+
+                if let Some(args) = hovered_args {
+                    DRAG_HOVERED_EVENT.notify(args);
+                }
+            }
+        } else if let Some(args) = RAW_DRAG_CANCELLED_EVENT.on(update) {
+            // system drag cancelled of dragged out of all app windows
+            let mut sv = DRAG_DROP_SV.write();
+            update_sv = !sv.system_dragging.is_empty();
+            sv.system_dragging.clear();
+
+            if let Some(prev) = self.hovered.take() {
+                self.pos_window = None;
+                DRAG_HOVERED_EVENT.notify(DragHoveredArgs::now(
+                    Some(prev),
+                    None,
+                    self.pos,
+                    self.hits.take().unwrap_or_else(|| HitTestInfo::no_hits(args.window_id)),
+                ));
+            }
+        } else if let Some(args) = RAW_APP_DRAG_ENDED_EVENT.on(update) {
+            let mut sv = DRAG_DROP_SV.write();
+            sv.app_dragging.retain(|d| {
+                if d.view_id != args.id {
+                    return true;
+                }
+
+                if !args.applied.is_empty() && !d.allowed.contains(args.applied) {
+                    tracing::error!(
+                        "drop target applied disallowed effect, allowed={:?}, applied={:?}",
+                        d.allowed,
+                        args.applied
+                    );
+                }
+
+                DRAG_END_EVENT.notify(DragEndArgs::now(d.target.clone(), args.applied));
+
+                false
+            });
+        }
+
+        if update_sv {
+            DRAG_DROP.update_var();
+        }
+    }
+
+    fn event(&mut self, update: &mut EventUpdate) {
+        if let Some(args) = MOUSE_INPUT_EVENT.on_unhandled(update) {
+            if matches!(args.state, ButtonState::Pressed) {
+                if let Some(wgt) = WINDOWS.widget_info(args.target.widget_id()) {
+                    if let Some(wgt) = wgt.self_and_ancestors().find(|w| w.is_draggable()) {
+                        // unhandled mouse press on draggable
+                        args.propagation().stop();
+                        let target = wgt.interaction_path();
+                        let args = DragStartArgs::now(target.clone());
+                        DRAG_START_EVENT.notify(args);
+                        DRAG_DROP_SV.write().app_drag = Some(AppDragging {
+                            target,
+                            data: vec![],
+                            handles: vec![],
+                            allowed: DragDropEffect::empty(),
+                            view_id: DragDropId(0),
+                        }); // calls to DRAG_DROP.drag are now valid
+                    }
+                }
+            }
+        } else if let Some(args) = TOUCH_INPUT_EVENT.on_unhandled(update) {
+            if matches!(args.phase, TouchPhase::Start) {
+                if let Some(wgt) = WINDOWS.widget_info(args.target.widget_id()) {
+                    if let Some(wgt) = wgt.self_and_ancestors().find(|w| w.is_draggable()) {
+                        // unhandled touch start on draggable
+                        args.propagation().stop();
+                        let target = wgt.interaction_path();
+                        let args = DragStartArgs::now(target.clone());
+                        DRAG_START_EVENT.notify(args);
+                        DRAG_DROP_SV.write().app_drag = Some(AppDragging {
+                            target,
+                            data: vec![],
+                            handles: vec![],
+                            allowed: DragDropEffect::empty(),
+                            view_id: DragDropId(0),
+                        }); // calls to DRAG_DROP.drag are now valid
+                    }
+                }
+            }
+        } else if let Some(args) = DRAG_START_EVENT.on(update) {
+            // finished notifying draggable drag start
+            let mut sv = DRAG_DROP_SV.write();
+            let mut data = sv.app_drag.take();
+            let mut cancel = args.propagation_handle.is_stopped();
+            if !cancel {
+                if let Some(d) = &mut data {
+                    if d.data.is_empty() {
+                        d.data.push(encode_widget_id(args.target.widget_id()));
+                        d.allowed = DragDropEffect::all();
+                    }
+                    match WINDOWS_DRAG_DROP.start_drag_drop(d.target.window_id(), mem::take(&mut d.data), d.allowed) {
+                        Ok(id) => {
+                            d.view_id = id;
+                            sv.app_dragging.push(data.take().unwrap());
+                        }
+                        Err(e) => {
+                            tracing::error!("cannot start drag&drop, {e}");
+                            cancel = true;
+                        }
+                    }
+                } else {
+                    tracing::warn!("external notification of DRAG_START_EVENT ignored")
+                }
+            }
+            if cancel {
+                if let Some(d) = data {
+                    DRAG_END_EVENT.notify(DragEndArgs::now(d.target, DragDropEffect::empty()));
+                }
+            }
+        } else if let Some(args) = DROP_EVENT.on(update) {
+            let _ = WINDOWS_DRAG_DROP.drag_dropped(args.target.window_id(), args.drop_id, *args.applied.lock());
+        }
+    }
+
+    fn update_preview(&mut self) {
+        let mut sv = DRAG_DROP_SV.write();
+
+        // fulfill drop requests
+        if let Some((id, target, data, allowed)) = sv.pending_drop.take() {
+            let window_id = self.pos_window.take().unwrap();
+            let hits = self.hits.take().unwrap_or_else(|| HitTestInfo::no_hits(window_id));
+            DRAG_HOVERED_EVENT.notify(DragHoveredArgs::now(Some(target.clone()), None, self.pos, hits.clone()));
+            DROP_EVENT.notify(DropArgs::now(
+                target,
+                data,
+                allowed,
+                self.pos,
+                hits,
+                id,
+                Arc::new(Mutex::new(DragDropEffect::empty())),
+            ));
+        }
+    }
+}
+
+/// Drag & drop service.
+#[allow(non_camel_case_types)]
+pub struct DRAG_DROP;
+impl DRAG_DROP {
+    /// All data current dragging.
+    pub fn dragging_data(&self) -> ReadOnlyArcVar<Vec<DragDropData>> {
+        DRAG_DROP_SV.read().data.read_only()
+    }
+
+    /// Start dragging `data`.
+    ///
+    /// This method will only work if a [`DRAG_START_EVENT`] is notifying. Handlers of draggable widgets
+    /// can provide custom drag data using this method.
+    ///
+    /// Returns a handle that can be dropped to cancel the drag operation. A [`DRAG_END_EVENT`] notifies
+    /// the draggable widget on cancel or drop. Logs an error message and returns a dummy handle on error.
+    ///
+    /// Note that the `allowed_effects` apply to all data, if a previous handler already set data with an incompatible
+    /// effect the call is an error and the data ignored.
+    pub fn drag(&self, data: DragDropData, allowed_effects: DragDropEffect) -> DragHandle {
+        let mut sv = DRAG_DROP_SV.write();
+        if let Some(d) = &mut sv.app_drag {
+            if allowed_effects.is_empty() {
+                tracing::error!("cannot drag, no `allowed_effects`");
+                return DragHandle::dummy();
+            }
+
+            if d.allowed.is_empty() {
+                d.allowed = allowed_effects;
+            } else {
+                if !d.allowed.contains(allowed_effects) {
+                    tracing::error!("cannot drag, other data already set with incompatible `allowed_effects`");
+                    return DragHandle::dummy();
+                }
+                d.allowed |= allowed_effects
+            }
+
+            d.data.push(data);
+            let (owner, handle) = DragHandle::new();
+            d.handles.push(owner);
+            return handle;
+        }
+        tracing::error!("cannot drag, not in `DRAG_START_EVENT` interval");
+        DragHandle::dummy()
+    }
+
+    fn update_var(&self) {
+        let sv = DRAG_DROP_SV.read();
+        sv.data.set(sv.system_dragging.clone());
+    }
+}
+
+app_local! {
+    static DRAG_DROP_SV: DragDropService = DragDropService {
+        data: var(vec![]),
+        system_dragging: vec![],
+        app_drag: None,
+        app_dragging: vec![],
+        pending_drop: None,
+    };
+}
+struct DragDropService {
+    data: ArcVar<Vec<DragDropData>>,
+
+    system_dragging: Vec<DragDropData>,
+
+    app_drag: Option<AppDragging>,
+    app_dragging: Vec<AppDragging>,
+
+    pending_drop: Option<(DragDropId, InteractionPath, Vec<DragDropData>, DragDropEffect)>,
+}
+struct AppDragging {
+    target: InteractionPath,
+    data: Vec<DragDropData>,
+    handles: Vec<HandleOwner<()>>,
+    allowed: DragDropEffect,
+    view_id: DragDropId,
+}
+
+/// Represents dragging data.
+///
+/// Drop all clones of this handle to cancel the drag operation.
+#[derive(Clone, PartialEq, Eq, Hash, Debug)]
+#[repr(transparent)]
+#[must_use = "dropping the handle cancels the drag operation"]
+pub struct DragHandle(Handle<()>);
+impl DragHandle {
+    fn new() -> (HandleOwner<()>, Self) {
+        let (owner, handle) = Handle::new(());
+        (owner, Self(handle))
+    }
+
+    /// New handle to nothing.
+    pub fn dummy() -> Self {
+        Self(Handle::dummy(()))
+    }
+
+    /// Drops the handle but does **not** cancel the drag operation.
+    ///
+    /// The drag data stays alive until the user completes or cancels the operation.
+    pub fn perm(self) {
+        self.0.perm();
+    }
+
+    /// If another handle has called [`perm`](Self::perm).
+    ///
+    /// If `true` operation will run to completion.
+    pub fn is_permanent(&self) -> bool {
+        self.0.is_permanent()
+    }
+
+    /// Drops the handle and forces operation the cancel.
+    pub fn cancel(self) {
+        self.0.force_drop()
+    }
+
+    /// If another handle has called [`cancel`](Self::cancel).
+    pub fn is_canceled(&self) -> bool {
+        self.0.is_dropped()
+    }
+
+    /// Create a weak handle.
+    pub fn downgrade(&self) -> WeakDragHandle {
+        WeakDragHandle(self.0.downgrade())
+    }
+}
+/// Weak [`DragHandle`].
+#[derive(Clone, PartialEq, Eq, Hash, Default, Debug)]
+pub struct WeakDragHandle(WeakHandle<()>);
+impl WeakDragHandle {
+    /// New weak handle that does not upgrade.
+    pub fn new() -> Self {
+        Self(WeakHandle::new())
+    }
+
+    /// Gets the strong handle if it is still subscribed.
+    pub fn upgrade(&self) -> Option<DragHandle> {
+        self.0.upgrade().map(DragHandle)
+    }
+}
+
+/// [`WidgetInfo`] extensions for drag & drop service.
+pub trait WidgetInfoDragDropExt {
+    /// If this widget can be dragged and dropped.
+    fn is_draggable(&self) -> bool;
+}
+impl WidgetInfoDragDropExt for WidgetInfo {
+    fn is_draggable(&self) -> bool {
+        self.meta().flagged(*IS_DRAGGABLE_ID)
+    }
+}
+
+/// [`WidgetInfoBuilder`] extensions for drag & drop service.
+pub trait WidgetInfoBuilderDragDropExt {
+    /// Flag the widget as draggable.
+    fn draggable(&mut self);
+}
+impl WidgetInfoBuilderDragDropExt for WidgetInfoBuilder {
+    fn draggable(&mut self) {
+        self.flag_meta(*IS_DRAGGABLE_ID);
+    }
+}
+
+static_id! {
+    static ref IS_DRAGGABLE_ID: StateId<()>;
+}
+
+event_args! {
+    /// Arguments for [`DROP_EVENT`].
+    pub struct DropArgs {
+        /// Hovered target of the drag&drop gesture.
+        pub target: InteractionPath,
+        /// Drag&drop data payload.
+        pub data: Vec<DragDropData>,
+        /// Drop effects that the drag source allows.
+        pub allowed: DragDropEffect,
+        /// Position of the cursor in the window's content area.
+        pub position: DipPoint,
+        /// Hit-test result for the cursor point in the window.
+        pub hits: HitTestInfo,
+
+        drop_id: DragDropId,
+        applied: Arc<Mutex<DragDropEffect>>,
+
+        ..
+
+        fn delivery_list(&self, list: &mut UpdateDeliveryList) {
+            list.insert_wgt(&self.target);
+        }
+    }
+
+    /// Arguments for [`DRAG_HOVERED_EVENT`].
+    pub struct DragHoveredArgs {
+        /// Previous hovered target.
+        pub prev_target: Option<InteractionPath>,
+        /// New hovered target.
+        pub target: Option<InteractionPath>,
+        /// Position of the cursor in the window's content area.
+        pub position: DipPoint,
+        /// Hit-test result for the cursor point in the window.
+        pub hits: HitTestInfo,
+
+        ..
+
+        fn delivery_list(&self, list: &mut UpdateDeliveryList) {
+            if let Some(p) = &self.prev_target {
+                list.insert_wgt(p);
+            }
+            if let Some(p) = &self.target {
+                list.insert_wgt(p);
+            }
+        }
+    }
+
+    /// [`DRAG_MOVE_EVENT`] arguments.
+    pub struct DragMoveArgs {
+        /// Id of window that received the event.
+        pub window_id: WindowId,
+
+        /// Positions of the cursor in between the previous event and this one.
+        ///
+        /// Drag move events can be coalesced, i.e. multiple moves packed into a single event.
+        pub coalesced_pos: Vec<DipPoint>,
+
+        /// Position of the cursor in the window's content area.
+        pub position: DipPoint,
+
+        /// Hit-test result for the cursor point in the window.
+        pub hits: HitTestInfo,
+
+        /// Full path to the top-most hit in [`hits`](DragMoveArgs::hits).
+        pub target: InteractionPath,
+
+        ..
+
+        /// The [`target`].
+        ///
+        /// [`target`]: Self::target
+        fn delivery_list(&self, list: &mut UpdateDeliveryList) {
+            list.insert_wgt(&self.target);
+        }
+    }
+
+    /// Arguments for [`DRAG_START_EVENT`].
+    pub struct DragStartArgs {
+        /// Draggable widget that has started dragging.
+        pub target: InteractionPath,
+
+        ..
+
+        fn delivery_list(&self, list: &mut UpdateDeliveryList) {
+            list.insert_wgt(&self.target);
+        }
+    }
+
+    /// Arguments for [`DRAG_END_EVENT`].
+    pub struct DragEndArgs {
+        /// Draggable widget that was dragging.
+        pub target: InteractionPath,
+
+        /// Effect applied by the drop target on the data.
+        ///
+        /// Is empty or a single flag.
+        pub applied: DragDropEffect,
+
+        ..
+
+        fn delivery_list(&self, list: &mut UpdateDeliveryList) {
+            list.insert_wgt(&self.target);
+        }
+
+        /// The `applied` field can only be empty or only have a single flag set.
+        fn validate(&self) -> Result<(), Txt> {
+            if self.applied.is_empty() && self.applied.len() > 1 {
+                return Err("only one or none `DragDropEffect` can be applied".into());
+            }
+            Ok(())
+        }
+    }
+}
+event! {
+    /// Drag&drop action finished over some drop target widget.
+    pub static DROP_EVENT: DropArgs;
+    /// Drag&drop enter or exit a drop target widget.
+    pub static DRAG_HOVERED_EVENT: DragHoveredArgs;
+    /// Drag&drop is dragging over the target widget.
+    pub static DRAG_MOVE_EVENT: DragMoveArgs;
+    /// Drag&drop started dragging a draggable widget.
+    ///
+    /// If propagation is stopped the drag operation is cancelled. Handlers can use
+    /// [`DRAG_DROP.drag`] to set the data, otherwise the widget ID will be dragged.
+    ///
+    /// [`DRAG_DROP.drag`]: DRAG_DROP::drag
+    pub static DRAG_START_EVENT: DragStartArgs;
+
+    /// Drag&drop gesture started from the draggable widget has ended.
+    pub static DRAG_END_EVENT: DragEndArgs;
+}
+
+impl DropArgs {
+    /// If the `widget_id` is in the [`target`] is enabled.
+    ///
+    /// [`target`]: Self::target
+    pub fn is_enabled(&self, widget_id: WidgetId) -> bool {
+        self.target.interactivity_of(widget_id).map(|i| i.is_enabled()).unwrap_or(false)
+    }
+
+    /// If the `widget_id` is in the [`target`] is disabled.
+    ///
+    /// [`target`]: Self::target
+    pub fn is_disabled(&self, widget_id: WidgetId) -> bool {
+        self.target.interactivity_of(widget_id).map(|i| i.is_disabled()).unwrap_or(false)
+    }
+
+    /// Stop propagation and set the `effect` that was applied to the data.
+    ///
+    /// Logs an error if propagation is already stopped.
+    ///
+    /// # Panics
+    ///
+    /// Panics if `effect` sets more then one flag or is not [`allowed`].
+    ///
+    /// [`allowed`]: Self::allowed
+    pub fn applied(&self, effect: DragDropEffect) {
+        assert!(effect.len() > 1, "can only apply one effect");
+        assert!(self.allowed.contains(effect), "source does not allow this effect");
+
+        let mut e = self.applied.lock();
+        if !self.propagation().is_stopped() {
+            self.propagation().stop();
+            *e = effect;
+        } else {
+            tracing::error!("drop already handled");
+        }
+    }
+}
+
+impl DragHoveredArgs {
+    /// Gets the [`DRAG_DROP.dragging_data`].
+    ///
+    /// [`DRAG_DROP.dragging_data`]: DRAG_DROP::dragging_data
+    pub fn data(&self) -> ReadOnlyArcVar<Vec<DragDropData>> {
+        DRAG_DROP.dragging_data()
+    }
+
+    /// Returns `true` if the [`WIDGET`] was not hovered, but now is.
+    ///
+    /// [`WIDGET`]: zng_app::widget::WIDGET
+    pub fn is_drag_enter(&self) -> bool {
+        !self.was_over() && self.is_over()
+    }
+
+    /// Returns `true` if the [`WIDGET`] was hovered, but now isn't.
+    ///
+    /// [`WIDGET`]: zng_app::widget::WIDGET
+    pub fn is_drag_leave(&self) -> bool {
+        self.was_over() && !self.is_over()
+    }
+
+    /// Returns `true` if the [`WIDGET`] is in [`prev_target`].
+    ///
+    /// [`prev_target`]: Self::prev_target
+    /// [`prev_capture`]: Self::prev_capture
+    /// [`WIDGET`]: zng_app::widget::WIDGET
+    pub fn was_over(&self) -> bool {
+        if let Some(t) = &self.prev_target {
+            return t.contains(WIDGET.id());
+        }
+
+        false
+    }
+
+    /// Returns `true` if the [`WIDGET`] is in [`target`].
+    ///
+    /// [`target`]: Self::target
+    /// [`capture`]: Self::capture
+    /// [`WIDGET`]: zng_app::widget::WIDGET
+    pub fn is_over(&self) -> bool {
+        if let Some(t) = &self.target {
+            return t.contains(WIDGET.id());
+        }
+
+        false
+    }
+
+    /// Returns `true` if the widget was enabled in [`prev_target`].
+    ///
+    /// [`prev_target`]: Self::prev_target
+    pub fn was_enabled(&self, widget_id: WidgetId) -> bool {
+        self.prev_target
+            .as_ref()
+            .and_then(|t| t.interactivity_of(widget_id))
+            .map(|itr| itr.is_enabled())
+            .unwrap_or(false)
+    }
+
+    /// Returns `true` if the widget was disabled in [`prev_target`].
+    ///
+    /// [`prev_target`]: Self::prev_target
+    pub fn was_disabled(&self, widget_id: WidgetId) -> bool {
+        self.prev_target
+            .as_ref()
+            .and_then(|t| t.interactivity_of(widget_id))
+            .map(|itr| itr.is_disabled())
+            .unwrap_or(false)
+    }
+
+    /// Returns `true` if the widget is enabled in [`target`].
+    ///
+    /// [`target`]: Self::target
+    pub fn is_enabled(&self, widget_id: WidgetId) -> bool {
+        self.target
+            .as_ref()
+            .and_then(|t| t.interactivity_of(widget_id))
+            .map(|itr| itr.is_enabled())
+            .unwrap_or(false)
+    }
+
+    /// Returns `true` if the widget is disabled in [`target`].
+    ///
+    /// [`target`]: Self::target
+    pub fn is_disabled(&self, widget_id: WidgetId) -> bool {
+        self.target
+            .as_ref()
+            .and_then(|t| t.interactivity_of(widget_id))
+            .map(|itr| itr.is_disabled())
+            .unwrap_or(false)
+    }
+
+    /// Returns `true` if the [`WIDGET`] was not hovered or was disabled, but now is hovered and enabled.
+    ///
+    /// [`WIDGET`]: zng_app::widget::WIDGET
+    pub fn is_drag_enter_enabled(&self) -> bool {
+        (!self.was_over() || self.was_disabled(WIDGET.id())) && self.is_over() && self.is_enabled(WIDGET.id())
+    }
+
+    /// Returns `true` if the [`WIDGET`] was hovered and enabled, but now is not hovered or is disabled.
+    ///
+    /// [`WIDGET`]: zng_app::widget::WIDGET
+    pub fn is_drag_leave_enabled(&self) -> bool {
+        self.was_over() && self.was_enabled(WIDGET.id()) && (!self.is_over() || self.is_disabled(WIDGET.id()))
+    }
+
+    /// Returns `true` if the [`WIDGET`] was not hovered or was enabled, but now is hovered and disabled.
+    ///
+    /// [`WIDGET`]: zng_app::widget::WIDGET
+    pub fn is_drag_enter_disabled(&self) -> bool {
+        (!self.was_over() || self.was_enabled(WIDGET.id())) && self.is_over() && self.is_disabled(WIDGET.id())
+    }
+
+    /// Returns `true` if the [`WIDGET`] was hovered and disabled, but now is not hovered or is enabled.
+    ///
+    /// [`WIDGET`]: zng_app::widget::WIDGET
+    pub fn is_drag_leave_disabled(&self) -> bool {
+        self.was_over() && self.was_disabled(WIDGET.id()) && (!self.is_over() || self.is_enabled(WIDGET.id()))
+    }
+}
+
+impl DragEndArgs {
+    /// Data was dropped on a valid target.
+    pub fn was_dropped(&self) -> bool {
+        !self.applied.is_empty()
+    }
+
+    /// Stopped dragging without dropping on a valid drop target.
+    pub fn was_canceled(&self) -> bool {
+        self.applied.is_empty()
+    }
+}
+
+/// Encode an widget ID for drag&drop data.
+pub fn encode_widget_id(id: WidgetId) -> DragDropData {
+    DragDropData::Text {
+        format: formatx!("zng/{}", APP_GUID.read().simple()),
+        data: formatx!("wgt-{}", id.get()),
+    }
+}
+
+/// Decode an widget ID from drag&drop data.
+///
+/// The ID will only decode if it was encoded by the same app instance.
+pub fn decode_widget_id(data: &DragDropData) -> Option<WidgetId> {
+    if let DragDropData::Text { format, data } = data {
+        if let Some(guid) = format.strip_prefix("zng/") {
+            if let Some(id) = data.strip_prefix("wgt-") {
+                if guid == APP_GUID.read().simple().to_string() {
+                    if let Ok(id) = id.parse::<u64>() {
+                        return Some(WidgetId::from_raw(id));
+                    }
+                }
+            }
+        }
+    }
+    None
+}
+
+app_local! {
+    static APP_GUID: uuid::Uuid = uuid::Uuid::new_v4();
+}
diff --git a/crates/zng-ext-input/src/lib.rs b/crates/zng-ext-input/src/lib.rs
index d96cece8e..11d1271e7 100644
--- a/crates/zng-ext-input/src/lib.rs
+++ b/crates/zng-ext-input/src/lib.rs
@@ -12,6 +12,7 @@
 #[macro_use]
 extern crate bitflags;
 
+pub mod drag_drop;
 pub mod focus;
 pub mod gesture;
 pub mod keyboard;
diff --git a/crates/zng-ext-window/src/control.rs b/crates/zng-ext-window/src/control.rs
index 4e5ce994c..8a3592a43 100644
--- a/crates/zng-ext-window/src/control.rs
+++ b/crates/zng-ext-window/src/control.rs
@@ -41,11 +41,12 @@ use zng_state_map::StateId;
 use zng_var::{AnyVar, ReadOnlyArcVar, Var, VarHandle, VarHandles};
 use zng_view_api::{
     config::{ColorScheme, FontAntiAliasing},
+    drag_drop::{DragDropData, DragDropEffect, DragDropError},
     window::{
         EventCause, FrameCapture, FrameId, FrameRequest, FrameUpdateRequest, FrameWaitId, HeadlessRequest, RenderMode, WindowRequest,
         WindowState, WindowStateAll,
     },
-    FocusResult, Ime, ViewProcessOffline,
+    DragDropId, FocusResult, Ime, ViewProcessOffline,
 };
 use zng_wgt::prelude::WidgetInfo;
 
@@ -53,7 +54,7 @@ use crate::{
     cmd::{WindowCommands, MINIMIZE_CMD, RESTORE_CMD},
     AutoSize, FrameCaptureMode, FrameImageReadyArgs, HeadlessMonitor, MonitorInfo, StartPosition, WINDOW_Ext, WidgetInfoImeArea,
     WindowChangedArgs, WindowIcon, WindowRoot, WindowVars, FRAME_IMAGE_READY_EVENT, MONITORS, MONITORS_CHANGED_EVENT, WINDOWS,
-    WINDOW_CHANGED_EVENT, WINDOW_FOCUS,
+    WINDOWS_DRAG_DROP, WINDOW_CHANGED_EVENT, WINDOW_FOCUS,
 };
 
 struct ImageResources {
@@ -1370,6 +1371,21 @@ impl HeadedCtrl {
         });
     }
 
+    pub fn start_drag_drop(&mut self, data: Vec<DragDropData>, allowed_effects: DragDropEffect) -> Result<DragDropId, DragDropError> {
+        if let Some(view) = &self.window {
+            if let Ok(r) = view.start_drag_drop(data, allowed_effects) {
+                return r;
+            }
+        }
+        Err(DragDropError::CannotStart("view not available".into()))
+    }
+
+    pub fn drag_dropped(&mut self, drop_id: DragDropId, applied: DragDropEffect) {
+        if let Some(view) = &self.window {
+            let _ = view.drag_dropped(drop_id, applied);
+        }
+    }
+
     pub fn bring_to_top(&mut self) {
         self.update_gen(|view| {
             let _ = view.bring_to_top();
@@ -1679,6 +1695,15 @@ impl HeadlessWithRendererCtrl {
         self.surface = None;
     }
 
+    pub fn start_drag_drop(&mut self, data: Vec<DragDropData>, allowed_effects: DragDropEffect) -> Result<DragDropId, DragDropError> {
+        let _ = (data, allowed_effects);
+        Err(DragDropError::CannotStart("cannot start drag&drop from headless window".into()))
+    }
+
+    pub fn drag_dropped(&mut self, drop_id: DragDropId, applied: DragDropEffect) {
+        let _ = (drop_id, applied);
+    }
+
     fn view_task(&mut self, task: Box<dyn FnOnce(Option<&ViewWindow>) + Send>) {
         task(None)
     }
@@ -1855,6 +1880,15 @@ impl HeadlessCtrl {
         self.content.close();
     }
 
+    pub fn start_drag_drop(&mut self, data: Vec<DragDropData>, allowed_effects: DragDropEffect) -> Result<DragDropId, DragDropError> {
+        let _ = (data, allowed_effects);
+        Err(DragDropError::CannotStart("cannot start drag&drop from headless window".into()))
+    }
+
+    pub fn drag_dropped(&mut self, drop_id: DragDropId, applied: DragDropEffect) {
+        let _ = (drop_id, applied);
+    }
+
     fn view_task(&mut self, task: Box<dyn FnOnce(Option<&ViewWindow>) + Send>) {
         task(None);
     }
@@ -2351,6 +2385,24 @@ impl WindowCtrl {
         }
     }
 
+    pub fn start_drag_drop(&mut self, data: Vec<DragDropData>, allowed_effects: DragDropEffect) -> Result<DragDropId, DragDropError> {
+        match &mut self.0 {
+            WindowCtrlMode::Headed(c) => c.start_drag_drop(data, allowed_effects),
+            WindowCtrlMode::Headless(c) => c.start_drag_drop(data, allowed_effects),
+            WindowCtrlMode::HeadlessWithRenderer(c) => c.start_drag_drop(data, allowed_effects),
+            WindowCtrlMode::Nested(c) => c.start_drag_drop(data, allowed_effects),
+        }
+    }
+
+    pub fn drag_dropped(&mut self, drop_id: DragDropId, applied: DragDropEffect) {
+        match &mut self.0 {
+            WindowCtrlMode::Headed(c) => c.drag_dropped(drop_id, applied),
+            WindowCtrlMode::Headless(c) => c.drag_dropped(drop_id, applied),
+            WindowCtrlMode::HeadlessWithRenderer(c) => c.drag_dropped(drop_id, applied),
+            WindowCtrlMode::Nested(c) => c.drag_dropped(drop_id, applied),
+        }
+    }
+
     pub fn bring_to_top(&mut self) {
         match &mut self.0 {
             WindowCtrlMode::Headed(c) => c.bring_to_top(),
@@ -2492,6 +2544,17 @@ impl NestedCtrl {
         RAW_WINDOW_FOCUS_EVENT.notify(args);
     }
 
+    pub fn start_drag_drop(&mut self, data: Vec<DragDropData>, allowed_effects: DragDropEffect) -> Result<DragDropId, DragDropError> {
+        if let Some((win_id, _)) = &self.c.lock().host {
+            return WINDOWS_DRAG_DROP.start_drag_drop(*win_id, data, allowed_effects);
+        }
+        Err(DragDropError::CannotStart("nested window host unavailable".into()))
+    }
+
+    pub fn drag_dropped(&mut self, drop_id: DragDropId, applied: DragDropEffect) {
+        let _ = (drop_id, applied);
+    }
+
     fn bring_to_top(&self) {
         if let Some((win_id, _)) = &self.c.lock().host {
             let _ = WINDOWS.bring_to_top(*win_id);
diff --git a/crates/zng-ext-window/src/service.rs b/crates/zng-ext-window/src/service.rs
index 5683f37a5..c25e7be56 100644
--- a/crates/zng-ext-window/src/service.rs
+++ b/crates/zng-ext-window/src/service.rs
@@ -37,7 +37,7 @@ use zng_task::{
     rayon::iter::{IntoParallelRefMutIterator, ParallelIterator},
     ParallelIteratorExt, UiTask,
 };
-use zng_txt::{formatx, Txt};
+use zng_txt::{formatx, ToTxt as _, Txt};
 use zng_unique_id::{IdMap, IdSet};
 use zng_var::{
     impl_from_and_into_var, response_done_var, response_var, types::WeakArcVar, var, AnyWeakVar, ArcVar, BoxedVar, LocalVar,
@@ -46,9 +46,10 @@ use zng_var::{
 use zng_view_api::{
     api_extension::{ApiExtensionId, ApiExtensionPayload},
     config::{ChromeConfig, ColorsConfig},
+    drag_drop::{DragDropData, DragDropEffect, DragDropError},
     image::ImageMaskMode,
     window::{RenderMode, WindowState},
-    ViewProcessOffline,
+    DragDropId, ViewProcessOffline,
 };
 use zng_wgt::node::with_context_var;
 
@@ -1474,6 +1475,35 @@ impl WINDOWS {
     }
 }
 
+/// Raw drag&drop API.
+#[allow(non_camel_case_types)]
+pub struct WINDOWS_DRAG_DROP;
+impl WINDOWS_DRAG_DROP {
+    /// Request a start of drag&drop from the window.
+    pub fn start_drag_drop(
+        &self,
+        window_id: WindowId,
+        data: Vec<DragDropData>,
+        allowed_effects: DragDropEffect,
+    ) -> Result<DragDropId, DragDropError> {
+        match WINDOWS_SV.write().windows.get_mut(&window_id) {
+            Some(w) => w.start_drag_drop(data, allowed_effects),
+            None => Err(DragDropError::CannotStart(WindowNotFound(window_id).to_txt())),
+        }
+    }
+
+    /// Notify the drag source of what effect was applied for a received drag&drop.
+    pub fn drag_dropped(&self, window_id: WindowId, drop_id: DragDropId, applied: DragDropEffect) -> Result<(), WindowNotFound> {
+        match WINDOWS_SV.write().windows.get_mut(&window_id) {
+            Some(w) => {
+                w.drag_dropped(drop_id, applied);
+                Ok(())
+            }
+            None => Err(WindowNotFound(window_id)),
+        }
+    }
+}
+
 /// Window data visible in [`Windows`], detached so we can make the window visible inside the window content.
 struct AppWindowInfo {
     id: WindowId,
@@ -1664,6 +1694,14 @@ impl AppWindow {
         self.ctrl_in_ctx(|ctrl| ctrl.focus());
     }
 
+    pub fn start_drag_drop(&mut self, data: Vec<DragDropData>, allowed_effects: DragDropEffect) -> Result<DragDropId, DragDropError> {
+        self.ctrl_in_ctx(|ctx| ctx.start_drag_drop(data, allowed_effects))
+    }
+
+    pub fn drag_dropped(&mut self, drop_id: DragDropId, applied: DragDropEffect) {
+        self.ctrl_in_ctx(|ctx| ctx.drag_dropped(drop_id, applied))
+    }
+
     pub fn bring_to_top(&mut self) {
         self.ctrl_in_ctx(|ctrl| ctrl.bring_to_top());
     }
diff --git a/crates/zng-view-api/src/drag_drop.rs b/crates/zng-view-api/src/drag_drop.rs
new file mode 100644
index 000000000..f06a81649
--- /dev/null
+++ b/crates/zng-view-api/src/drag_drop.rs
@@ -0,0 +1,111 @@
+//! Drag&drop types.
+
+use std::{fmt, path::PathBuf};
+
+use zng_txt::Txt;
+
+use crate::ipc::IpcBytes;
+
+use bitflags::bitflags;
+
+/// Drag&drop data payload.
+#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)]
+pub enum DragDropData {
+    /// Text encoded data.
+    ///
+    /// This can be HTML or JSON for example.
+    Text {
+        /// MIME type of the data.
+        ///
+        /// Plain text is `"text/plain"`.
+        format: Txt,
+        /// Data.
+        data: Txt,
+    },
+    /// File or directory path.
+    Path(PathBuf),
+    /// Binary encoded data.
+    ///
+    /// This can be an image for example.
+    Binary {
+        /// MIME type of the data.
+        format: Txt,
+        /// Data.
+        data: IpcBytes,
+    },
+}
+impl fmt::Debug for DragDropData {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Text { format, data } => write!(f, "Text {{ format: {:?}, data: {} bytes }}", format, data.len()),
+            Self::Path(data) => write!(f, "Path({})", data.display()),
+            Self::Binary { format, data } => write!(f, "Binary {{ format: {:?}, data: {} bytes }}", format, data.len()),
+        }
+    }
+}
+
+#[cfg(feature = "var")]
+zng_var::impl_from_and_into_var! {
+    fn from(plain: Txt) -> DragDropData {
+        DragDropData::Text {
+            format: "text/plain".into(),
+            data: plain,
+        }
+    }
+
+    fn from(plain: String) -> DragDropData {
+        Txt::from(plain).into()
+    }
+
+    fn from(plain: &'static str) -> DragDropData {
+        Txt::from(plain).into()
+    }
+
+    fn from(path: PathBuf) -> DragDropData {
+        DragDropData::Path(path)
+    }
+}
+
+bitflags! {
+    /// Drag&drop drop effect on the data source.
+    #[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
+    pub struct DragDropEffect: u8 {
+        /// Indicates that the dragged data will be copied from its present location to the drop location.
+        const COPY = 0b001;
+        /// Indicates that the dragged data will be moved from its present location to the drop location.
+        const MOVE = 0b010;
+        /// Indicates that some form of relationship or connection will be created between the source and drop locations.
+        const LINK = 0b100;
+    }
+}
+impl DragDropEffect {
+    /// Count effects flagged.
+    pub fn len(&self) -> u8 {
+        [DragDropEffect::COPY, DragDropEffect::MOVE, DragDropEffect::LINK]
+            .into_iter()
+            .filter(|&f| self.contains(f))
+            .count() as u8
+    }
+}
+
+/// Error for drag start or cancel error.
+#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
+pub enum DragDropError {
+    /// View-process implementer does not support any of the provided data types.
+    NotSupported,
+    /// Cannot start dragging.
+    CannotStart(Txt),
+}
+impl fmt::Display for DragDropError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            DragDropError::NotSupported => write!(f, "not supported"),
+            DragDropError::CannotStart(txt) => write!(f, "cannot start, {txt}"),
+        }
+    }
+}
+impl std::error::Error for DragDropError {
+    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+        None
+    }
+}
diff --git a/crates/zng-view-api/src/lib.rs b/crates/zng-view-api/src/lib.rs
index 843ac0b8e..1b22a1997 100644
--- a/crates/zng-view-api/src/lib.rs
+++ b/crates/zng-view-api/src/lib.rs
@@ -22,6 +22,7 @@
 #![warn(missing_docs)]
 #![warn(unused_extern_crates)]
 
+use drag_drop::{DragDropData, DragDropEffect, DragDropError};
 #[cfg(ipc)]
 use serde::{Deserialize, Serialize};
 
@@ -35,6 +36,7 @@ pub mod clipboard;
 pub mod config;
 pub mod dialog;
 pub mod display_list;
+pub mod drag_drop;
 pub mod font;
 pub mod image;
 pub mod ipc;
@@ -500,6 +502,20 @@ declare_api! {
     /// Set the clipboard content.
     pub fn write_clipboard(&mut self, data: ClipboardData) -> Result<(), ClipboardError>;
 
+    /// Start a drag and drop operation, if the window is pressed.
+    pub fn start_drag_drop(
+        &mut self,
+        id: WindowId,
+        data: Vec<DragDropData>,
+        allowed_effects: DragDropEffect,
+    ) -> Result<DragDropId, DragDropError>;
+
+    /// Cancel a drag and drop operation.
+    pub fn cancel_drag_drop(&mut self, id: WindowId, drag_id: DragDropId);
+
+    /// Notify the drag source of what effect was applied for a received drag&drop.
+    pub fn drag_dropped(&mut self, id: WindowId, drop_id: DragDropId, applied: DragDropEffect);
+
     /// Enable or disable IME by setting a cursor area.
     ///
     /// In mobile platforms also shows the software keyboard for `Some(_)` and hides it for `None`.
diff --git a/crates/zng-view-api/src/types.rs b/crates/zng-view-api/src/types.rs
index e6f690a6a..7eae27d93 100644
--- a/crates/zng-view-api/src/types.rs
+++ b/crates/zng-view-api/src/types.rs
@@ -7,6 +7,7 @@ use crate::{
         AnimationsConfig, ChromeConfig, ColorsConfig, FontAntiAliasing, KeyRepeatConfig, LocaleConfig, MultiClickConfig, TouchConfig,
     },
     dialog::{DialogId, FileDialogResponse, MsgDialogResponse},
+    drag_drop::{DragDropData, DragDropEffect},
     image::{ImageId, ImageLoadedData, ImagePpi},
     ipc::IpcBytes,
     keyboard::{Key, KeyCode, KeyLocation, KeyState},
@@ -15,7 +16,7 @@ use crate::{
     window::{EventFrameRendered, FrameId, HeadlessOpenData, MonitorId, MonitorInfo, WindowChanged, WindowId, WindowOpenData},
 };
 use serde::{Deserialize, Serialize};
-use std::{fmt, path::PathBuf};
+use std::fmt;
 use zng_txt::Txt;
 use zng_unit::{DipPoint, PxRect, PxSize, Rgba};
 
@@ -106,6 +107,11 @@ declare_id! {
 #[serde(transparent)]
 pub struct AxisId(pub u32);
 
+/// Identifier for a drag drop operation.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[serde(transparent)]
+pub struct DragDropId(pub u32);
+
 #[derive(Debug, Clone, Serialize, Deserialize)]
 /// View process is online.
 ///
@@ -205,28 +211,53 @@ pub enum Event {
     /// [`EventCause`]: crate::window::EventCause
     WindowChanged(WindowChanged),
 
-    /// A file has been dropped into the window.
-    ///
-    /// When the user drops multiple files at once, this event will be emitted for each file separately.
-    DroppedFile {
+    /// A drag&drop gesture started dragging over the window.
+    DragHovered {
+        /// Window that is hovered.
+        window: WindowId,
+        /// Data payload.
+        data: Vec<DragDropData>,
+        /// Allowed effects.
+        allowed: DragDropEffect,
+    },
+    /// A drag&drop gesture moved over the window.
+    DragMoved {
+        /// Window that is hovered.
+        window: WindowId,
+        /// Cursor positions in between the previous event and this one.
+        coalesced_pos: Vec<DipPoint>,
+        /// Cursor position, relative to the window top-left in device independent pixels.
+        position: DipPoint,
+    },
+    /// A drag&drop gesture finished over the window.
+    DragDropped {
         /// Window that received the file drop.
         window: WindowId,
-        /// Path to the file that was dropped.
-        file: PathBuf,
+        /// Data payload.
+        data: Vec<DragDropData>,
+        /// Allowed effects.
+        allowed: DragDropEffect,
+        /// ID of this drop operation.
+        ///
+        /// Handlers must call `drag_dropped` with this ID and what effect was applied to the data.
+        drop_id: DragDropId,
     },
-    /// A file is being hovered over the window.
-    ///
-    /// When the user hovers multiple files at once, this event will be emitted for each file separately.
-    HoveredFile {
-        /// Window that was hovered by drag-drop.
+    /// A drag&drop gesture stopped hovering the window without dropping.
+    DragCancelled {
+        /// Window that was previous hovered.
         window: WindowId,
-        /// Path to the file being dragged.
-        file: PathBuf,
     },
-    /// A file was hovered, but has exited the window.
-    ///
-    /// There will be a single event triggered even if multiple files were hovered.
-    HoveredFileCancelled(WindowId),
+    /// A drag started by the app was dropped or canceled.
+    AppDragEnded {
+        /// Window that started the drag.
+        window: WindowId,
+        /// Drag ID.
+        drag: DragDropId,
+        /// Effect applied to the data by the drop target.
+        ///
+        /// Is a single flag if the data was dropped in a valid drop target, or is empty if was canceled.
+        applied: DragDropEffect,
+    },
 
     /// App window(s) focus changed.
     FocusChanged {
@@ -594,6 +625,22 @@ impl Event {
                 coalesced_pos.extend(n_coal_pos);
                 *position = n_pos;
             }
+            (
+                DragMoved {
+                    window,
+                    coalesced_pos,
+                    position,
+                },
+                DragMoved {
+                    window: n_window,
+                    coalesced_pos: n_coal_pos,
+                    position: n_pos,
+                },
+            ) if *window == n_window => {
+                coalesced_pos.push(*position);
+                coalesced_pos.extend(n_coal_pos);
+                *position = n_pos;
+            }
             // raw mouse motion.
             (
                 DeviceMouseMotion { device, delta },
@@ -776,6 +823,40 @@ impl Event {
             (LocaleChanged(config), LocaleChanged(n_config)) => {
                 *config = n_config;
             }
+            // drag hovered
+            (
+                DragHovered {
+                    window,
+                    data,
+                    allowed: effects,
+                },
+                DragHovered {
+                    window: n_window,
+                    data: mut n_data,
+                    allowed: n_effects,
+                },
+            ) if *window == n_window && effects.contains(n_effects) => {
+                data.append(&mut n_data);
+            }
+            // drag dropped
+            (
+                DragDropped {
+                    window,
+                    data,
+                    allowed,
+                    drop_id,
+                },
+                DragDropped {
+                    window: n_window,
+                    data: mut n_data,
+                    allowed: n_allowed,
+                    drop_id: n_drop_id,
+                },
+            ) if *window == n_window && allowed.contains(n_allowed) && *drop_id == n_drop_id => {
+                data.append(&mut n_data);
+            }
+            // drag cancelled
+            (DragCancelled { window }, DragCancelled { window: n_window }) if *window == n_window => {}
             (_, e) => return Err(e),
         }
         Ok(())
@@ -875,9 +956,10 @@ pub enum RepeatMode {
     /// border. Extra space will be distributed in between tiles to achieve the proper fit.
     Space,
 }
-/// Converts `true` to `Repeat` and `false` to the default `Stretch`.
-impl From<bool> for RepeatMode {
-    fn from(value: bool) -> Self {
+#[cfg(feature = "var")]
+zng_var::impl_from_and_into_var! {
+    /// Converts `true` to `Repeat` and `false` to the default `Stretch`.
+    fn from(value: bool) -> RepeatMode {
         match value {
             true => RepeatMode::Repeat,
             false => RepeatMode::Stretch,
diff --git a/crates/zng-view/src/lib.rs b/crates/zng-view/src/lib.rs
index 2a9adb954..1d84598b1 100644
--- a/crates/zng-view/src/lib.rs
+++ b/crates/zng-view/src/lib.rs
@@ -92,7 +92,9 @@
 #![warn(unused_extern_crates)]
 
 use std::{
-    fmt, mem, thread,
+    fmt, mem,
+    path::PathBuf,
+    thread,
     time::{Duration, Instant},
 };
 
@@ -145,6 +147,7 @@ use zng_unit::{Dip, DipPoint, DipRect, DipSideOffsets, DipSize, Factor, Px, PxPo
 use zng_view_api::{
     api_extension::{ApiExtensionId, ApiExtensionPayload},
     dialog::{DialogId, FileDialog, MsgDialog, MsgDialogResponse},
+    drag_drop::*,
     font::{FontFaceId, FontId, FontOptions, FontVariationName},
     image::{ImageId, ImageLoadedData, ImageMaskMode, ImageRequest, ImageTextureId},
     ipc::{IpcBytes, IpcBytesReceiver},
@@ -370,6 +373,8 @@ pub(crate) struct App {
     config_listener_exit: Option<Box<dyn FnOnce()>>,
 
     app_state: AppState,
+    drag_drop_hovered: Option<(WindowId, DipPoint)>,
+    drag_drop_next_move: Option<(Instant, PathBuf)>,
     exited: bool,
 }
 impl fmt::Debug for App {
@@ -614,17 +619,66 @@ impl winit::application::ApplicationHandler<AppEvent> for App {
                 self.windows.remove(i);
                 self.notify(Event::WindowClosed(id));
             }
-            WindowEvent::DroppedFile(file) => {
+            WindowEvent::HoveredFile(file) => {
                 linux_modal_dialog_bail!();
-                self.notify(Event::DroppedFile { window: id, file })
+
+                // winit does not provide mouse move events during drag/drop,
+                // so we enable device events to get mouse move, and use native APIs to get
+                // the cursor position on the window.
+                if !self.device_events {
+                    winit_loop.listen_device_events(winit::event_loop::DeviceEvents::Always);
+                }
+                self.drag_drop_hovered = Some((id, DipPoint::splat(Dip::new(-1000))));
+                self.notify(Event::DragHovered {
+                    window: id,
+                    data: vec![DragDropData::Path(file)],
+                    allowed: DragDropEffect::all(),
+                });
             }
-            WindowEvent::HoveredFile(file) => {
+            WindowEvent::DroppedFile(file) => {
                 linux_modal_dialog_bail!();
-                self.notify(Event::HoveredFile { window: id, file })
+
+                if !self.device_events {
+                    winit_loop.listen_device_events(winit::event_loop::DeviceEvents::Never);
+                }
+
+                let mut delay_to_next_move = true;
+
+                // some systems (x11) don't receive even device mouse move
+                if let Some(position) = self.windows[i].drag_drop_cursor_pos() {
+                    self.notify(Event::DragMoved {
+                        window: id,
+                        coalesced_pos: vec![],
+                        position,
+                    });
+                    delay_to_next_move = false;
+                } else if let Some((_, pos)) = self.drag_drop_hovered {
+                    delay_to_next_move = pos.x < Dip::new(0);
+                }
+
+                if delay_to_next_move {
+                    self.drag_drop_next_move = Some((Instant::now(), file));
+                } else {
+                    self.notify(Event::DragDropped {
+                        window: id,
+                        data: vec![DragDropData::Path(file)],
+                        allowed: DragDropEffect::all(),
+                        drop_id: DragDropId(0),
+                    });
+                }
             }
             WindowEvent::HoveredFileCancelled => {
                 linux_modal_dialog_bail!();
-                self.notify(Event::HoveredFileCancelled(id))
+
+                self.drag_drop_hovered = None;
+                if !self.device_events {
+                    winit_loop.listen_device_events(winit::event_loop::DeviceEvents::Never);
+                }
+
+                if self.drag_drop_next_move.is_none() {
+                    // x11 sends a cancelled after drop
+                    self.notify(Event::DragCancelled { window: id });
+                }
             }
             WindowEvent::Focused(mut focused) => {
                 if self.windows[i].focused_changed(&mut focused) {
@@ -733,6 +787,23 @@ impl winit::application::ApplicationHandler<AppEvent> for App {
                         position: p,
                     });
                 }
+
+                if let Some((drop_moment, file)) = self.drag_drop_next_move.take() {
+                    if drop_moment.elapsed() < Duration::from_millis(300) {
+                        let window_id = self.windows[i].id();
+                        self.notify(Event::DragMoved {
+                            window: window_id,
+                            coalesced_pos: vec![],
+                            position: p,
+                        });
+                        self.notify(Event::DragDropped {
+                            window: window_id,
+                            data: vec![DragDropData::Path(file)],
+                            allowed: DragDropEffect::all(),
+                            drop_id: DragDropId(0),
+                        });
+                    }
+                }
             }
             WindowEvent::CursorEntered { device_id } => {
                 linux_modal_dialog_bail!();
@@ -948,7 +1019,7 @@ impl winit::application::ApplicationHandler<AppEvent> for App {
             let mut winit_loop_guard = self.winit_loop.set(winit_loop);
 
             let d_id = self.device_id(device_id);
-            match event {
+            match &event {
                 DeviceEvent::Added => self.notify(Event::DeviceAdded(d_id)),
                 DeviceEvent::Removed => self.notify(Event::DeviceRemoved(d_id)),
                 DeviceEvent::MouseMotion { delta } => self.notify(Event::DeviceMouseMotion {
@@ -957,17 +1028,17 @@ impl winit::application::ApplicationHandler<AppEvent> for App {
                 }),
                 DeviceEvent::MouseWheel { delta } => self.notify(Event::DeviceMouseWheel {
                     device: d_id,
-                    delta: util::winit_mouse_wheel_delta_to_zng(delta),
+                    delta: util::winit_mouse_wheel_delta_to_zng(*delta),
                 }),
                 DeviceEvent::Motion { axis, value } => self.notify(Event::DeviceMotion {
                     device: d_id,
-                    axis: AxisId(axis),
-                    value,
+                    axis: AxisId(*axis),
+                    value: *value,
                 }),
                 DeviceEvent::Button { button, state } => self.notify(Event::DeviceButton {
                     device: d_id,
-                    button: ButtonId(button),
-                    state: util::element_state_to_button_state(state),
+                    button: ButtonId(*button),
+                    state: util::element_state_to_button_state(*state),
                 }),
                 DeviceEvent::Key(k) => self.notify(Event::DeviceKey {
                     device: d_id,
@@ -978,6 +1049,24 @@ impl winit::application::ApplicationHandler<AppEvent> for App {
 
             winit_loop_guard.unset(&mut self.winit_loop);
         }
+
+        if let Some((id, pos)) = &mut self.drag_drop_hovered {
+            if let DeviceEvent::MouseMotion { .. } = &event {
+                if let Some(win) = self.windows.iter().find(|w| w.id() == *id) {
+                    if let Some(new_pos) = win.drag_drop_cursor_pos() {
+                        if *pos != new_pos {
+                            *pos = new_pos;
+                            let event = Event::DragMoved {
+                                window: *id,
+                                coalesced_pos: vec![],
+                                position: *pos,
+                            };
+                            self.notify(event);
+                        }
+                    }
+                }
+            }
+        }
     }
 
     fn about_to_wait(&mut self, winit_loop: &ActiveEventLoop) {
@@ -1279,6 +1368,8 @@ impl App {
             pending_modifiers_update: None,
             pending_modifiers_focus_clear: false,
             config_listener_exit: None,
+            drag_drop_hovered: None,
+            drag_drop_next_move: None,
 
             #[cfg(not(any(windows, target_os = "android")))]
             arboard: None,
@@ -2142,6 +2233,24 @@ impl Api for App {
         )))
     }
 
+    fn start_drag_drop(
+        &mut self,
+        id: WindowId,
+        data: Vec<DragDropData>,
+        allowed_effects: DragDropEffect,
+    ) -> Result<DragDropId, DragDropError> {
+        let _ = (id, data, allowed_effects); // TODO, wait winit
+        Err(DragDropError::NotSupported)
+    }
+
+    fn cancel_drag_drop(&mut self, id: WindowId, drag_id: DragDropId) {
+        let _ = (id, drag_id);
+    }
+
+    fn drag_dropped(&mut self, id: WindowId, drop_id: DragDropId, applied: DragDropEffect) {
+        let _ = (id, drop_id, applied); // TODO, wait winit
+    }
+
     fn set_system_shutdown_warn(&mut self, id: WindowId, reason: Txt) {
         self.with_window(id, move |w| w.set_system_shutdown_warn(reason), || ())
     }
diff --git a/crates/zng-view/src/window.rs b/crates/zng-view/src/window.rs
index afbbef2dd..35ac9989b 100644
--- a/crates/zng-view/src/window.rs
+++ b/crates/zng-view/src/window.rs
@@ -2090,6 +2090,26 @@ impl Window {
             tracing::warn!("system shutdown warn not implemented on {}", std::env::consts::OS);
         }
     }
+
+    pub(crate) fn drag_drop_cursor_pos(&self) -> Option<DipPoint> {
+        #[cfg(windows)]
+        {
+            let mut pt = windows::Win32::Foundation::POINT::default();
+            // SAFETY: normal call
+            if unsafe { windows::Win32::UI::WindowsAndMessaging::GetCursorPos(&mut pt) }.is_ok() {
+                let cursor_pos = PxPoint::new(Px(pt.x), Px(pt.y));
+                let win_pos = self.window.inner_position().unwrap_or_default().to_px();
+                let pos = cursor_pos - win_pos.to_vector();
+                if pos.x >= Px(0) && pos.y >= Px(0) {
+                    let size = self.window.inner_size().to_px();
+                    if pos.x <= size.width && pos.y <= size.height {
+                        return Some(pos.to_dip(self.scale_factor()));
+                    }
+                }
+            }
+        }
+        None
+    }
 }
 impl Drop for Window {
     fn drop(&mut self) {
diff --git a/crates/zng-wgt-input/src/drag_drop.rs b/crates/zng-wgt-input/src/drag_drop.rs
new file mode 100644
index 000000000..3dea30f67
--- /dev/null
+++ b/crates/zng-wgt-input/src/drag_drop.rs
@@ -0,0 +1,138 @@
+//! Drag& drop properties, event properties.
+
+use zng_ext_input::drag_drop::{
+    DragEndArgs, DragHoveredArgs, DragStartArgs, DropArgs, WidgetInfoBuilderDragDropExt as _, DRAG_END_EVENT, DRAG_HOVERED_EVENT,
+    DRAG_START_EVENT, DROP_EVENT,
+};
+use zng_wgt::prelude::*;
+
+/// If this widget can be dragged in a drag&drop operation.
+///
+/// When this is `true` the widget can be dragged and dropped within the same app or it can handle [`on_drag_start`] and
+/// use the [`DRAG_DROP.drag`] service to set a system wide drag data.
+///
+/// [`on_drag_start`]: fn@on_drag_start
+/// [`DRAG_DROP.drag`]: zng_ext_input::drag_drop::DRAG_DROP::drag
+#[property(CONTEXT, default(false))]
+pub fn draggable(child: impl UiNode, input: impl IntoVar<bool>) -> impl UiNode {
+    let input = input.into_var();
+    match_node(child, move |_c, op| match op {
+        UiNodeOp::Init => {
+            WIDGET.sub_var_info(&input);
+        }
+        UiNodeOp::Info { info } => {
+            if input.get() {
+                info.draggable();
+            }
+        }
+        _ => {}
+    })
+}
+
+event_property! {
+    /// Draggable widget started dragging.
+    ///
+    /// To receive this event in a widget set [`draggable`] to `true`.
+    ///
+    /// [`draggable`]: fn@draggable
+    pub fn drag_start {
+        event: DRAG_START_EVENT,
+        args: DragStartArgs,
+    }
+
+    /// Draggable widget stopped dragging.
+    ///
+    /// This event is always paired with [`on_drag_start`] first.
+    ///
+    /// [`on_drag_start`]: fn@on_drag_start
+    pub fn drag_end {
+        event: DRAG_END_EVENT,
+        args: DragEndArgs,
+    }
+
+    /// Dragging cursor entered or exited the widget area and the widget is enabled.
+    pub fn drag_hovered {
+        event: DRAG_HOVERED_EVENT,
+        args: DragHoveredArgs,
+        filter: |args| args.is_drag_enter_enabled()
+    }
+    /// Dragging cursor entered the widget area and the widget is enabled.
+    pub fn drag_enter {
+        event: DRAG_HOVERED_EVENT,
+        args: DragHoveredArgs,
+        filter: |args| args.is_drag_enter_enabled(),
+    }
+    /// Dragging cursor exited the widget area and the widget is enabled.
+    pub fn drag_leave {
+        event: DRAG_HOVERED_EVENT,
+        args: DragHoveredArgs,
+        filter: |args| args.is_drag_leave_enabled(),
+    }
+
+    /// Dragging cursor dropped data in the widget area and the widget is enabled.
+    pub fn drop {
+        event: DROP_EVENT,
+        args: DropArgs,
+        filter: |args| args.is_enabled(WIDGET.id()),
+    }
+}
+
+/// If the dragging cursor is over the widget or a descendant and the widget is enabled.
+///
+/// The value is always `false` when the widget is not [`ENABLED`], use [`is_drag_hovered_disabled`] to implement *disabled hovered* visuals.
+///
+/// [`is_cap_hovered`]: fn@is_cap_hovered
+/// [`ENABLED`]: Interactivity::ENABLED
+/// [`is_drag_hovered_disabled`]: fn@is_drag_hovered_disabled
+#[property(EVENT)]
+pub fn is_drag_hovered(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
+    event_state(child, state, false, DRAG_HOVERED_EVENT, |args| {
+        if args.is_drag_enter_enabled() {
+            Some(true)
+        } else if args.is_drag_leave_enabled() {
+            Some(false)
+        } else {
+            None
+        }
+    })
+}
+
+/// If the dragging cursor is over the widget or a descendant and the widget is disabled.
+#[property(EVENT)]
+pub fn is_drag_hovered_disabled(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
+    event_state(child, state, false, DRAG_HOVERED_EVENT, |args| {
+        if args.is_drag_enter_disabled() {
+            Some(true)
+        } else if args.is_drag_leave_disabled() {
+            Some(false)
+        } else {
+            None
+        }
+    })
+}
+
+/// If the draggable widget is dragging.
+#[property(EVENT)]
+pub fn is_dragging(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
+    let state = state.into_var();
+    match_node(child, move |_, op| match op {
+        UiNodeOp::Init => {
+            WIDGET.sub_event(&DRAG_START_EVENT).sub_event(&DRAG_END_EVENT);
+        }
+        UiNodeOp::Deinit => {
+            let _ = state.set(false);
+        }
+        UiNodeOp::Event { update } => {
+            if let Some(args) = DRAG_START_EVENT.on(update) {
+                if args.target.contains(WIDGET.id()) {
+                    let _ = state.set(true);
+                }
+            } else if let Some(args) = DRAG_END_EVENT.on(update) {
+                if args.target.contains(WIDGET.id()) {
+                    let _ = state.set(false);
+                }
+            }
+        }
+        _ => {}
+    })
+}
diff --git a/crates/zng-wgt-input/src/lib.rs b/crates/zng-wgt-input/src/lib.rs
index 0c00e3da8..cd0d70b61 100644
--- a/crates/zng-wgt-input/src/lib.rs
+++ b/crates/zng-wgt-input/src/lib.rs
@@ -10,6 +10,7 @@
 #![warn(missing_docs)]
 
 pub mod cmd;
+pub mod drag_drop;
 pub mod focus;
 pub mod gesture;
 pub mod keyboard;
diff --git a/crates/zng/Cargo.toml b/crates/zng/Cargo.toml
index 0d69a4a23..598787953 100644
--- a/crates/zng/Cargo.toml
+++ b/crates/zng/Cargo.toml
@@ -266,7 +266,7 @@ zng-ext-single-instance = { path = "../zng-ext-single-instance", version = "0.3.
 tracing = "0.1"
 
 # view
-zng-view-api = { path = "../zng-view-api", version = "0.10.5", default-features = false }
+zng-view-api = { path = "../zng-view-api", version = "0.10.5", default-features = false, features = ["var"] }
 
 [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
 zng-view = { path = "../zng-view", version = "0.6.5", default-features = false, optional = true }
diff --git a/crates/zng/src/drag_drop.rs b/crates/zng/src/drag_drop.rs
new file mode 100644
index 000000000..a96773356
--- /dev/null
+++ b/crates/zng/src/drag_drop.rs
@@ -0,0 +1,61 @@
+//! Drag&drop service, types and events.
+//!
+//! The example below defines a window that shows the current dragging data that has entered any of the app windows.
+//!
+//! ```
+//! use zng::prelude::*;
+//! # let _scope = APP.defaults();
+//! use zng::drag_drop::*;
+//!
+//! let data = var::<Vec<DragDropData>>(vec![]);
+//! # let _ =
+//! Window! {
+//!     padding = 20;
+//!     child = Container! {
+//!         widget::border = 5, widget::BorderSides::dashed(colors::GRAY);
+//!         widget::corner_radius = 15;
+//!         child_align = Align::CENTER;
+//!         on_drag_enter = hn!(data, |_| {
+//!             data.set(DRAG_DROP.dragging_data().get());
+//!         });
+//!         on_drag_leave = hn!(data, |_| {
+//!             data.set(vec![]);
+//!         });
+//!         on_drop = hn!(data, |args: &DropArgs| {
+//!             data.set(args.data.clone());
+//!         });
+//!         child = Text!(data.map(|d| if d.is_empty() {
+//!             Txt::from("drag over to inspect")
+//!         } else {
+//!             formatx!("{d:#?}")
+//!         }));
+//!     }
+//! }
+//! # ;
+//! ```
+//!
+//!
+//! # Limitations
+//!
+//! Drag&drop depends on the view-process backend, the default view-process (`zng-view`) is currently very limited:
+//!
+//! * No drag start from the app.
+//! * Only file path drops.
+//! * No support in Wayland, you can work around by calling `std::env::remove_var("WAYLAND_DISPLAY");` before `zng::env::init!()` in
+//!   your main function, this enables XWayland that has support for the basic file path drop.
+//! * In X11 and macOS there is no cursor position notification on hover, just on drop, `DRAG_HOVERED_EVENT` and `DRAG_MOVE_EVENT`
+//!   based event properties will only fire once for the widget that is about to receive a drop.
+//!
+//! # Full API
+//!
+//! See [`zng_ext_input::drag_drop`] and [`zng_wgt_input::drag_drop`] for the full drag&drop API.
+
+pub use zng_ext_input::drag_drop::{
+    DragDropData, DragDropEffect, DragEndArgs, DragHandle, DragHoveredArgs, DragMoveArgs, DragStartArgs, DropArgs, WeakDragHandle,
+    DRAG_DROP, DRAG_END_EVENT, DRAG_HOVERED_EVENT, DRAG_MOVE_EVENT, DRAG_START_EVENT, DROP_EVENT,
+};
+
+pub use zng_wgt_input::drag_drop::{
+    draggable, on_drag_end, on_drag_enter, on_drag_hovered, on_drag_leave, on_drag_start, on_drop, on_pre_drag_end, on_pre_drag_enter,
+    on_pre_drag_hovered, on_pre_drag_leave, on_pre_drag_start, on_pre_drop,
+};
diff --git a/crates/zng/src/lib.rs b/crates/zng/src/lib.rs
index e333ae353..a17238cd9 100644
--- a/crates/zng/src/lib.rs
+++ b/crates/zng/src/lib.rs
@@ -492,6 +492,7 @@ pub mod container;
 pub mod data_context;
 pub mod data_view;
 pub mod dialog;
+pub mod drag_drop;
 pub mod env;
 pub mod event;
 pub mod focus;
@@ -811,7 +812,7 @@ mod defaults {
     use zng_ext_fs_watcher::FsWatcherManager;
     use zng_ext_image::ImageManager;
     use zng_ext_input::{
-        focus::FocusManager, gesture::GestureManager, keyboard::KeyboardManager, mouse::MouseManager,
+        drag_drop::DragDropManager, focus::FocusManager, gesture::GestureManager, keyboard::KeyboardManager, mouse::MouseManager,
         pointer_capture::PointerCaptureManager, touch::TouchManager,
     };
     use zng_ext_l10n::L10nManager;
@@ -851,6 +852,7 @@ mod defaults {
         /// * [`WindowManager`]
         /// * [`FontManager`]
         /// * [`FocusManager`]
+        /// * [`DragDropManager`]
         /// * [`ImageManager`]
         /// * [`ClipboardManager`]
         /// * [`UndoManager`]
@@ -880,6 +882,7 @@ mod defaults {
                 .extend(WindowManager::default())
                 .extend(FontManager::default())
                 .extend(FocusManager::default())
+                .extend(DragDropManager::default())
                 .extend(ImageManager::default())
                 .extend(ClipboardManager::default())
                 .extend(UndoManager::default());
diff --git a/crates/zng/src/var.rs b/crates/zng/src/var.rs
index 9635aa972..98cb1aa1e 100644
--- a/crates/zng/src/var.rs
+++ b/crates/zng/src/var.rs
@@ -323,7 +323,7 @@ pub use zng_var::{
     VarValue, WeakVar, VARS,
 };
 
-pub use zng_app::widget::{AnyVarSubscribe, VarLayout, VarSubscribe};
+pub use zng_app::widget::{AnyVarSubscribe, OnVarArgs, VarLayout, VarSubscribe};
 
 /// Var animation types and functions.
 pub mod animation {
diff --git a/examples/README.md b/examples/README.md
index 5846a640e..f406dafa2 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -360,4 +360,4 @@ Done.
 
 ## Local Example
 
-You can create a local "example" for manual testing in `/examples/test/`. This dir is gitignored.
\ No newline at end of file
+You can create a local "example" for manual testing in `/examples/test*`. This dir is gitignored.
\ No newline at end of file


Test patch: