Skip to content

Drag & drop - #492

Open
Klemen2 wants to merge 2 commits into
DioxusLabs:mainfrom
Klemen2:dnd
Open

Drag & drop#492
Klemen2 wants to merge 2 commits into
DioxusLabs:mainfrom
Klemen2:dnd

Conversation

@Klemen2

@Klemen2 Klemen2 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Stacked on top of: #614

This PR adds the following

Drag events:

  • dragenter event
  • dragover event
  • dragleave event
  • drop event
  • dragstart event
  • drag event
  • dragend event

External drags:

  • dragging files
  • dragging text (plain/html/rtf)
  • ability to downcast data transfer to handle platform specific data types

Internal drags

  • dragging out of the window
  • dragging text (plain)

Shell

  • set_datatransfer

Not included in this pr: dragging text (html), dragging images, dragging links, dragging input/textbox text, dropping to input/textbox, cursor icon, drop effect based on target

WPT results

No changes in test results compared to main.

Generated by the WPT workflow.

@Klemen2
Klemen2 marked this pull request as draft July 6, 2026 11:29
Comment thread packages/blitz-traits/src/events.rs Outdated
@nicoburns

Copy link
Copy Markdown
Member

Will take a proper look but:

This is blocked on most likely next winit release as it's using the new dnd api

Just wanted to say that this seems like a very sensible choice. I'm anticipating that we'll probably get a new Winit beta once the DnD PR lands.

@Klemen2

Klemen2 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

No need to rush, I've only tested the incoming dnd events and not everything is implemented yet, but it's a great start.
I also think that I need to change how DataTransferReceived is handled since i believe it can come after the drop is handeled

@nicoburns nicoburns left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took a look because I was curious and have a left a couple of notes.

And a general comment that:

Drag and Drop actually has three distinct use cases: dragging elements within a page, dragging data out of a page, and dragging data into a page. They have subtly different requirements and implementations.

(https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API)

And this only currently only implements "dragging data into a page". We don't necessarily need an implementation of all 3 at once, but it's at least worth thinking about when designing the API (and I'd personally be kinda tempted to try to handle "dragging elements within a page" too, as I think that might end up changing the design somewhat).

Comment thread packages/dioxus-native/src/config.rs Outdated
Comment thread packages/blitz-traits/src/events.rs Outdated
@Klemen2

Klemen2 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

I think that the enter/over/leave/drop are now handled correctly (needs #496 to be fixed for bubbling)

I think that the structure can support "dragging elements within a page" but I haven't come to that part yet, also dragable=true needs to handle the event data automagically so yea fun stuff

@Klemen2

Klemen2 commented Jul 11, 2026

Copy link
Copy Markdown
Contributor Author

So I tried implementing dragstart/drag/dragend, but for whatever reason ondragstart is not even getting triggered... am I missing something somewhere? it gets to self.handler.handle_event in the driver, but not to the actual event, I don't think the implementation is correct yet, especially drag/dragend so thats still wip

@nicoburns

nicoburns commented Jul 11, 2026

Copy link
Copy Markdown
Member

@Klemen2 You need to detect and send dragstart (and set blitz-dom into a "dragging" state) yourself! See pointer.rs where we already detect drags for text selection and pan-to-scroll on mobile.

EDIT: well that's for dragging DOM elements with draggable=true. For external drags, you should probably be firing DragStart when first encountering a new DragEnter. Winit DragEvent(enter window) != web dragenter (enter droppable node).

@Klemen2

Klemen2 commented Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

I did some changes and now you can d&d text within the app, haven't implemented the outgoing drags yet, but ondragstart is still not triggering on draggable (implementation is inside pointer.rs)

For external drags, you should probably be firing DragStart

as far as I can tell, dragstart only fires on elements with draggable=true / draggable types (text, images, link) from where the drag started, and doesn't fire at all for external drags

@nicoburns nicoburns added the B-winit Blocked a Winit release label Jul 13, 2026
@Klemen2
Klemen2 requested a review from nicoburns July 22, 2026 16:10
@Klemen2

Klemen2 commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

I think that this is mostly ready, (todo: cursor icon, dragging images, inputs, links)
I don't really know how should i be implementing get_datatransfer for the shell, std::mpsc? or futures oneshot?
Should internal drags be registered with OS (using event loop) when drag starts or when cursor leaves the window (the way its now)? - I think I answered this myself with just asking this question

@nicoburns

Copy link
Copy Markdown
Member

Testing this on macOS I'm getting two files when dropping a single file onto the drop zone

@Klemen2

Klemen2 commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Testing this on macOS I'm getting two files when dropping a single file onto the drop zone

Interesting, I'm not experiencing this on windows

Comment thread packages/blitz-traits/src/events/datatransfer.rs Outdated
Comment thread packages/blitz-dom/src/events/driver.rs
Comment thread packages/blitz-dom/src/events/driver.rs
Comment thread packages/dioxus-native-dom/src/events.rs Outdated
Comment thread packages/blitz-shell/src/window.rs Outdated
@Klemen2
Klemen2 force-pushed the dnd branch 2 times, most recently from dc2795b to 9aadb39 Compare July 24, 2026 15:01
@Klemen2

Klemen2 commented Jul 26, 2026

Copy link
Copy Markdown
Contributor Author

@nicoburns how do I fix this returning 33 for drag start since the drag start event is 34?

let event_kind_idx = event.data.discriminant() as usize;

I don't think that doing this is the solution

let mut event_kind_idx = event.data.discriminant() as usize;
match event.data {
    DomEventData::DragStart(_) => event_kind_idx += 1,
    _ => {}
}

the custom draggable do work after doing this

Fixed it

@Klemen2
Klemen2 marked this pull request as ready for review July 26, 2026 13:06
@Klemen2
Klemen2 force-pushed the dnd branch 4 times, most recently from 30910f7 to b863f8d Compare July 29, 2026 08:24
- dragenter/dragover/drageave/drop
- dragstart/drag/dragend
- outgoing drags
- drop effect (not fully implemented)
- internal drags (not fully implemented)
- shell set_datatransfer (I don't think that get_datatransfer is
  necessary as drop will always be an ui event so custom widgets can use
  that)
@nicoburns

Copy link
Copy Markdown
Member

@Klemen2 AI-review has brought up quite a few issues:


Blocking

  1. DomEventData / DomEventKind discriminant misalignment breaks the handler-count fast path. Registration counts by DomEventKind discriminant (mutation_writer.rs: event_handler_counts[kind.discriminant()]), but dispatch reads event.data.discriminant()DomEventData's index (dioxus_document.rs handle_event). The new variants were added in different orders:

    • DomEventKind: DragStart, Drag, DragEnter, DragOver, DragLeave, Drop, DragEnd
    • DomEventData: DragStart, Drag, DragEnd, DragEnter, DragOver, DragLeave, Drop

    So ondrop reads the ondragend count, ondragend reads ondragenter, ondragenter reads ondragover, etc. A handler registered alone (e.g. only ondrop) is silently never invoked. The example masks this because every zone registers all four events. Easiest robust fix: index by event.data.kind().discriminant() (or reorder DomEventData to match DomEventKind).

  2. DragOver default action has drop_effect backwards. handle_dom_event sets drop_effect = Copy in the default action (blitz-dom/src/events/mod.rs), but run_default_action only runs when the event was not canceled. Per spec a node isn't a drop target unless it cancels dragover, so as written every element that doesn't preventDefault advertises "copy" (droppable cursor everywhere), while elements that do preventDefault keep the previous value (initially None). Should be inverted: default none, set the effect when the event is canceled.

  3. drop is swallowed when no types can be fetched. In WindowEvent::DragDropped, if serial_ids ends up empty (zero advertised types, or every fetch_data_transfer errors), pending_data_transfers is stored but no DataTransferReceived ever arrives, so UiEvent::Drop is never dispatched. Dispatch the drop immediately when serial_ids.is_empty().

  4. INTERNAL_DROP_THRESHOLD heuristic is fragile. internal_drag records the last DragLeft; an OutgoingDragDropped within 50ms is dispatched as an in-window UiEvent::Drop. A genuine drop just outside the window within 50ms of leaving fires drop on the last-hovered node — delivering the drag's data to a handler that shouldn't get it. At minimum this needs a comment naming the platform quirk it works around; ideally decide on actual drop position/geometry rather than timing.

  5. No in-progress guard for internal drags. doc.drag_mode = DragMode::None inside the drag_mode == DragMode::None branch in pointer.rs is a no-op. Until the async UiEvent::DragStart clears mousedown_node_id, further pointermoves re-enter the branch and call set_datatransferstart_drag again. Needs a DragMode::Dnd-style marker.

  6. Click + selection-clear fire after a completed drag. Since drag_mode stays None, do_click in handle_pointerup is true — after a drop a click dispatches on the target and the added doc.clear_text_selection() wipes the selection. Browsers suppress click after a drag, and keep the source selection during a text drag.

  7. start_internal_drag dispatches DragStart before event_loop.start_drag. If start_drag errors, the DOM got dragstart (with id: 0) but dragend never arrives — an unbalanced session. Assign event.id before dispatching and bail to OutgoingCancel on failure.

  8. Sub-document (iframe) drags will target the wrong node or panic. source.get_node_id() is interpreted in the root document's node space — handle_dom_event indexes doc.nodes[target] directly — and map_dom_event_to_ui_event maps DragStart/Drag/DragEnd/DragEnter/DragLeave to None, so nothing forwards them. A drag sourced inside a sub-document hits a wrong node or an out-of-range index. Even if iframe DnD is out of scope for this PR, the panic case is worth a guard.

  9. draggable handling (pointer.rs): only the hit element is checked — browsers use the nearest ancestor with draggable=true, so div[draggable] > span won't drag when grabbing the span. Also v != "false" enables dragging for invalid values like draggable="" — spec says only "true" enables it. is_text_selected_at_position(x, y) uses the current pointer position rather than mousedown_position, so pressing next to a selection and drifting onto it can start a text drag. And draggable | auto_text_drag should be ||.

Non-blocking

  1. TextSelection::is_selected orders endpoints by (Option<NodeId>, sibling_index, offset) — node-id order isn't document order once the DOM mutates (ids are creation-order), and the scheme mixes (parent, idx) anonymous keys with regular node keys. Works for typical same-root selections but fragile; a document-order comparison would be more correct.

  2. WinitDataTransferItems::get_data/types require type_().hint() — externally dropped items with a custom MIME type (no TypeHint mapping) are unreachable via getData and invisible in types, even though the OS delivered the data.

  3. Box::leak per unique MIME subtype in audio_mime_to_static_ext/image_mime_to_static_ext — small unbounded leak driven by external MIME strings. A static match table or interning would avoid it.

  4. set_dnd_action is one event behind: it reads drop_effect inside update_active_drag before UiEvent::DragOver is dispatched, so the OS sees the previous move's effect. Call it after handle_ui_event returns.

  5. Store-mode nits: readable() set on Drop persists into dragend (spec says Protected there); set_drop_effect has no mode check. WinitBlitzFile does blocking std::fs inside async futures (blocks the UI executor for big drops) and returns Some("unknown") as content-type when the hint has no mime mapping — better to return None or infer from extension. DragLeft doesn't clear doc.hover_node_id, leaving stale :hover. The url dep added to blitz-shell appears unused.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

B-winit Blocked a Winit release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants