Refactor event handling#513
Draft
nicoburns wants to merge 7 commits into
Draft
Conversation
Signed-off-by: Nico Burns <nico@nicoburns.com>
… queue Previously events derived from state changes (focus events, input events, scroll events, click synthesis) were threaded through dispatch_event callback parameters into a queue owned by the EventDriver. This meant events could only be generated on code paths that had been explicitly plumbed through from a driver: programmatic focus changes (e.g. NodeHandle::set_focus in dioxus-native-dom, or Tab-key focus traversal) silently fired no events. Move the queue onto BaseDocument (queue_event/pop_pending_event) and make the state-changing APIs generate their own events: set_focus_to/clear_focus now queue blur/focusout/focus/focusin themselves (absorbing the old generate_focus_events helper), and scroll/input/click default actions queue directly onto the document. Anything with access to the document can now generate correctly-ordered events without access to a driver. EventDriver drains the document queue in a run-to-completion loop (flush_pending_events, also called at the start of handle_ui_event), and DioxusDocument::poll dispatches events queued by vdom updates (e.g. focus events from a mounted handler calling set_focus). The queue is capped so events cannot accumulate without bound if an embedder never drains them.
Event dispatch in the DOM is reentrant: a listener may synchronously trigger the dispatch of further events (e.g. a focus handler focusing another element), re-entering the handler while it is already on the stack. A `&mut self` receiver makes that impossible to express in Rust, so switch `EventHandler::handle_event_listener` to `&self` and require handlers to use interior mutability. CallbackEventHandler now stores `Rc<dyn Fn>` callbacks instead of `Rc<RefCell<dyn FnMut>>`, so a callback can legally be re-invoked while already executing. DioxusEventHandler now holds an `Rc<Runtime>` instead of `&mut VirtualDom`. Both mirror dioxus-web, whose delegated event handler is a `Closure<dyn Fn>` capturing `Rc<Runtime>` specifically so that it "may be invoked recursively if one event triggers another". This is groundwork for a nested synchronous dispatch_event API.
…steps Listeners previously received a raw `&mut dyn Document`, which let them mutate document state but gave them no way to dispatch events: state-changing APIs like set_focus_to queue their events for dispatch after the current event completes. The DOM spec however requires dispatchEvent and the focusing steps to be synchronous and reentrant: when a focus listener calls focus() on another element, that element's focus listeners run nested, before the outer listener continues. Listeners now receive an `EventContext` which provides document access (doc/doc_mut) plus synchronous dispatch: - `dispatch_event(event) -> bool`: dispatches through capture/target/bubble and runs the default action before returning (DOM dispatchEvent). Returns false if the event was cancelled. - `set_focus(node_id)` / `clear_focus()`: the DOM focusing steps. Runs the focus change and synchronously dispatches exactly the blur/focusout/ focus/focusin events it generated (other queued events stay queued). To support this the dispatch algorithm moves out of EventDriver methods into free functions over (doc, handler) so that it can be called reentrantly from within a listener; the driver and the context both call into the same code. Events generated by default actions continue to go through the document's pending-event queue.
Reentrant dispatch means a listener which (transitively) re-dispatches its own event would recurse until the stack overflows. Track the nesting depth in the EventDriver (shared with listeners through the EventContext) and drop events dispatched beyond MAX_DISPATCH_DEPTH (64) with a warning, mirroring browsers which abort scripts that exceed the engine's recursion limit during dispatch. Dropping (rather than deferring to the event queue) ensures a self-recursive listener terminates instead of turning the run-to-completion loop into an infinite loop.
On the web, dioxus's MountedData::set_focus calls element.focus(), which runs the DOM focusing steps synchronously: blur/focus listeners fire before the set_focus future resolves. dioxus-native's NodeHandle::set_focus used the queued path instead, so onfocus handlers only ran at the next poll or UI event, after code following the .await had already executed. Give NodeHandle an Rc<Runtime> so it can construct a DioxusEventHandler, add driver-level dispatch_event/set_focus/clear_focus to EventDriver (mirroring EventContext, for callers outside a listener invocation), and run the focusing steps synchronously in NodeHandle::set_focus. Since embedder APIs like this create a fresh EventDriver per call, a per-driver recursion counter cannot bound a focus <-> focus loop that recurses through new drivers. Move the dispatch depth onto BaseDocument so the bound holds across drivers.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.