Skip to main content

dioxus_core/
mutations.rs

1use crate::{AttributeValue, BorrowedAttributeValue, RenderTargetId, Runtime, arena::ElementId};
2use std::{collections::BTreeMap, rc::Rc};
3
4/// Something that can handle the mutations that are generated by the diffing process and apply them to the Real DOM
5///
6/// This object provides a stack-only stream of changes for a renderer to use
7/// to patch the Real DOM with the state of the VirtualDom.
8///
9/// These changes are specific to one subtree, so to patch multiple subtrees, you'd need to handle each set separately.
10///
11/// Mutations are the only link between the RealDOM and the VirtualDOM.
12pub trait WriteMutations {
13    /// Whether this writer applies and retains renderer-local template
14    /// prototype nodes across mutation flushes.
15    ///
16    /// Writers that ignore mutations, or otherwise do not apply emitted
17    /// template prototype mutations to a retained renderer-local node map,
18    /// must return `false` so VirtualDom-side template cache entries are not
19    /// created for renderer nodes that do not actually exist.
20    #[doc(hidden)]
21    fn can_cache_template_roots(&mut self) -> bool {
22        true
23    }
24
25    /// Push the node registered to `id` onto the stack.
26    fn push_id(&mut self, id: ElementId);
27
28    /// Replace the current top node with its `index`th child.
29    fn child(&mut self, index: usize);
30
31    /// Pop and discard the current top node.
32    fn pop(&mut self);
33
34    /// Create an element and push it onto the stack.
35    fn create_element(&mut self, tag: &str, ns: Option<&str>);
36
37    /// Create a text node and push it onto the stack.
38    fn create_text(&mut self, value: &str);
39
40    /// Replace the current top node with a deep clone of itself.
41    fn clone(&mut self);
42
43    /// Append the top `m` nodes to the parent below them. Leaves the parent
44    /// on the stack.
45    fn append_children(&mut self, m: usize);
46
47    /// Replace the target below the top `m` nodes with those nodes. Consumes
48    /// the target and replacement nodes.
49    fn replace_with(&mut self, m: usize);
50
51    /// Insert the top `m` nodes after the anchor below them. Leaves the anchor
52    /// on the stack.
53    fn insert_after(&mut self, m: usize);
54
55    /// Insert the top `m` nodes before the anchor below them. Leaves the anchor
56    /// on the stack.
57    fn insert_before(&mut self, m: usize);
58
59    /// Set the value of a node's attribute.
60    ///
61    /// Name: The name of the attribute to set.
62    /// NS: The (optional) namespace of the attribute. For instance, "style" is in the "style" namespace.
63    /// Value: The value of the attribute.
64    fn set_attribute(&mut self, name: &str, ns: Option<&str>, value: BorrowedAttributeValue<'_>);
65
66    /// Set the text content of a node.
67    ///
68    /// Value: The textcontent of the node
69    fn set_text(&mut self, value: &str);
70
71    /// Create a new Event Listener.
72    ///
73    /// Name: The name of the event to listen for.
74    fn add_event_listener(&mut self, name: &str);
75
76    /// Remove an existing Event Listener.
77    ///
78    /// Name: The name of the event to remove.
79    fn remove_event_listener(&mut self, name: &str);
80
81    /// Remove the current top node from the DOM and consume it.
82    fn remove(&mut self);
83
84    /// Register the current top node under `id`, leaving it on the stack.
85    ///
86    /// Replaces the old `pop_id` (which registered *and* popped): callers that want the node gone
87    /// follow with `pop`, and freshly-created nodes that will be appended just stay on the stack.
88    fn set_id(&mut self, id: ElementId);
89}
90
91macro_rules! write_mutation_methods {
92    ($macro:ident, $($prefix:tt)*) => {
93        $macro! {
94            $($prefix)*
95            push_id(id: ElementId);
96            child(index: usize);
97            pop();
98            create_element(tag: &str, ns: Option<&str>);
99            create_text(value: &str);
100            clone();
101            append_children(m: usize);
102            replace_with(m: usize);
103            insert_after(m: usize);
104            insert_before(m: usize);
105            set_attribute(name: &str, ns: Option<&str>, value: BorrowedAttributeValue<'_>);
106            set_text(value: &str);
107            add_event_listener(name: &str);
108            remove_event_listener(name: &str);
109            remove();
110            set_id(id: ElementId);
111        }
112    };
113}
114
115macro_rules! forward_mut_write_mutations {
116    ($($method:ident($($arg:ident: $arg_ty:ty),*);)*) => {
117        $(
118            fn $method(&mut self, $($arg: $arg_ty),*) {
119                WriteMutations::$method(&mut **self, $($arg),*);
120            }
121        )*
122    };
123}
124
125/// Forward through a mutable reference so writer-generic code can be
126/// instantiated at `&mut dyn WriteMutations`.
127impl<W: WriteMutations + ?Sized> WriteMutations for &mut W {
128    fn can_cache_template_roots(&mut self) -> bool {
129        WriteMutations::can_cache_template_roots(&mut **self)
130    }
131
132    write_mutation_methods!(forward_mut_write_mutations,);
133}
134
135/// Push `id`, run `with_node`, then pop the node.
136///
137/// Invariant: `with_node` must leave the pushed node on top of the renderer
138/// stack. No-op writers still receive the stack operations; their implementation
139/// is responsible for ignoring them while the diff keeps a single control flow.
140pub(crate) fn with_id<R>(
141    to: &mut dyn WriteMutations,
142    id: ElementId,
143    with_node: impl FnOnce(&mut dyn WriteMutations) -> R,
144) -> R {
145    to.push_id(id);
146    let result = with_node(to);
147    to.pop();
148    result
149}
150
151/// Push `id` and let `consume_node` consume it.
152///
153/// Invariant: `consume_node` must consume the pushed node with a terminal stack
154/// operation such as `remove`, `replace_with`, or `pop`.
155pub(crate) fn with_consumed_id<R>(
156    to: &mut dyn WriteMutations,
157    id: ElementId,
158    consume_node: impl FnOnce(&mut dyn WriteMutations) -> R,
159) -> R {
160    to.push_id(id);
161    consume_node(to)
162}
163
164pub(crate) fn remove_id(to: &mut dyn WriteMutations, id: ElementId) {
165    with_consumed_id(to, id, |to| to.remove());
166}
167
168pub(crate) struct TargetedLazyScope<'a, P: FnMut(&mut dyn WriteMutations)> {
169    to: &'a mut dyn WriteMutations,
170    push: P,
171    runtime: Rc<Runtime>,
172    target: RenderTargetId,
173    pushed: bool,
174}
175
176impl<'a, P: FnMut(&mut dyn WriteMutations)> TargetedLazyScope<'a, P> {
177    pub(crate) fn new(to: &'a mut dyn WriteMutations, runtime: Rc<Runtime>, push: P) -> Self {
178        let target = runtime.current_render_target_id();
179        Self {
180            to,
181            push,
182            runtime,
183            target,
184            pushed: false,
185        }
186    }
187
188    #[inline]
189    fn ensure_pushed(&mut self) {
190        if !self.pushed && self.runtime.current_render_target_id() == self.target {
191            (self.push)(self.to);
192            self.pushed = true;
193        }
194    }
195}
196
197impl<P: FnMut(&mut dyn WriteMutations)> Drop for TargetedLazyScope<'_, P> {
198    fn drop(&mut self) {
199        if self.pushed {
200            self.to.pop();
201        }
202    }
203}
204
205macro_rules! forward_lazy_scope_write_mutations {
206    ($($method:ident($($arg:ident: $arg_ty:ty),*);)*) => {
207        $(
208            fn $method(&mut self, $($arg: $arg_ty),*) {
209                self.ensure_pushed();
210                WriteMutations::$method(self.to, $($arg),*);
211            }
212        )*
213    };
214}
215
216impl<P: FnMut(&mut dyn WriteMutations)> WriteMutations for TargetedLazyScope<'_, P> {
217    fn can_cache_template_roots(&mut self) -> bool {
218        self.to.can_cache_template_roots()
219    }
220
221    write_mutation_methods!(forward_lazy_scope_write_mutations,);
222}
223
224pub(crate) fn append_children_to(
225    to: &mut dyn WriteMutations,
226    id: ElementId,
227    runtime: Rc<Runtime>,
228    create_children: impl FnOnce(&mut dyn WriteMutations) -> usize,
229) -> usize {
230    append_children_to_with_result(to, id, runtime, |to| {
231        let count = create_children(to);
232        (count, count)
233    })
234}
235
236pub(crate) fn append_children_to_with_result<R>(
237    to: &mut dyn WriteMutations,
238    id: ElementId,
239    runtime: Rc<Runtime>,
240    create_children: impl FnOnce(&mut dyn WriteMutations) -> (usize, R),
241) -> R {
242    let mut to = TargetedLazyScope::new(to, runtime, move |to| to.push_id(id));
243    let (count, result) = create_children(&mut to);
244    if count > 0 {
245        to.append_children(count);
246    }
247    result
248}
249
250/// A host's collection of render-target writers.
251///
252/// The diff routes each write to the writer for the render target it lands
253/// in. Single-target hosts get this for free: every [`WriteMutations`] writer
254/// is a `MultiWriter` serving [`RenderTargetId::ROOT`] and nothing else.
255/// Hosts that fan out to several sinks (one webview per window, one collector
256/// per portal target) implement [`Self::writer_for`] over their own writer
257/// storage.
258pub trait MultiWriter {
259    /// The writer type serving each individual target.
260    type Writer: WriteMutations;
261
262    /// The writer for `id`, or `None` for a target with no attached host.
263    fn writer_for(&mut self, id: RenderTargetId) -> Option<&mut Self::Writer>;
264}
265
266impl<W: WriteMutations> MultiWriter for W {
267    type Writer = W;
268
269    fn writer_for(&mut self, id: RenderTargetId) -> Option<&mut W> {
270        if id == RenderTargetId::ROOT {
271            Some(self)
272        } else {
273            None
274        }
275    }
276}
277
278impl<W: WriteMutations> MultiWriter for BTreeMap<RenderTargetId, W> {
279    type Writer = W;
280
281    fn writer_for(&mut self, id: RenderTargetId) -> Option<&mut W> {
282        self.get_mut(&id)
283    }
284}
285
286pub(crate) struct TargetRouter<'a, M: MultiWriter> {
287    to: &'a mut M,
288    runtime: Rc<Runtime>,
289}
290
291impl<'a, M: MultiWriter> TargetRouter<'a, M> {
292    pub(crate) fn new(to: &'a mut M, runtime: Rc<Runtime>) -> Self {
293        Self { to, runtime }
294    }
295
296    #[inline]
297    fn current(&mut self) -> Option<&mut M::Writer> {
298        self.to.writer_for(self.runtime.current_render_target_id())
299    }
300
301    #[inline]
302    fn with_current(&mut self, f: impl FnOnce(&mut M::Writer)) {
303        if let Some(w) = self.current() {
304            f(w);
305        }
306    }
307}
308
309macro_rules! route_write_mutations {
310    ($($method:ident($($arg:ident: $arg_ty:ty),*);)*) => {
311        $(
312            fn $method(&mut self, $($arg: $arg_ty),*) {
313                self.with_current(|w| WriteMutations::$method(w, $($arg),*));
314            }
315        )*
316    };
317}
318
319impl<M: MultiWriter> WriteMutations for TargetRouter<'_, M> {
320    fn can_cache_template_roots(&mut self) -> bool {
321        self.current()
322            .is_some_and(WriteMutations::can_cache_template_roots)
323    }
324
325    write_mutation_methods!(route_write_mutations,);
326}
327
328/// A `Mutation` represents a single instruction for the renderer to use to modify the UI tree to match the state
329/// of the Dioxus VirtualDom.
330///
331/// These edits can be serialized and sent over the network or through any interface
332#[derive(Debug, PartialEq)]
333pub enum Mutation {
334    /// Push the node registered to an element id onto the stack.
335    PushId {
336        /// The renderer-local element id to read.
337        id: ElementId,
338    },
339    /// Register the top stack node to an element id, leaving it on the stack.
340    SetId {
341        /// The renderer-local element id to write.
342        id: ElementId,
343    },
344    /// Replace the top stack node with its child at `index`.
345    Child {
346        /// The child index to select.
347        index: usize,
348    },
349    /// Pop and discard the top stack node.
350    Pop,
351    /// Create an element and push it onto the stack.
352    CreateElement {
353        /// The element tag name.
354        tag: String,
355        /// The optional element namespace.
356        ns: Option<String>,
357    },
358    /// Create a text node and push it onto the stack.
359    CreateText {
360        /// The text node value.
361        value: String,
362    },
363    /// Replace the top stack node with a deep clone of itself.
364    Clone,
365    /// Append the top `m` nodes to the parent below them.
366    AppendChildren {
367        /// The number of child nodes to append.
368        m: usize,
369    },
370    /// Replace the target below the top `m` nodes with those nodes.
371    ReplaceWith {
372        /// The number of replacement nodes.
373        m: usize,
374    },
375    /// Insert the top `m` nodes after the anchor below them.
376    InsertAfter {
377        /// The number of inserted nodes.
378        m: usize,
379    },
380    /// Insert the top `m` nodes before the anchor below them.
381    InsertBefore {
382        /// The number of inserted nodes.
383        m: usize,
384    },
385
386    /// Set the value of a node's attribute.
387    SetAttribute {
388        /// The name of the attribute to set.
389        name: String,
390
391        /// The (optional) namespace of the attribute.
392        /// For instance, "style" is in the "style" namespace.
393        ns: Option<String>,
394
395        /// The value of the attribute.
396        value: AttributeValue,
397    },
398
399    /// Set the textcontent of a node.
400    SetText {
401        /// The textcontent of the node
402        value: String,
403    },
404
405    /// Create a new Event Listener.
406    NewEventListener {
407        /// The name of the event to listen for.
408        name: String,
409    },
410
411    /// Remove an existing Event Listener.
412    RemoveEventListener {
413        /// The name of the event to remove.
414        name: String,
415    },
416    /// Remove the top stack node from the renderer tree.
417    Remove,
418}
419
420/// A static list of mutations that can be applied to the DOM. Note: this list does not contain any `Any` attribute values
421#[derive(Debug, PartialEq, Default)]
422pub struct Mutations {
423    /// Any mutations required to patch the renderer to match the layout of the VirtualDom
424    pub edits: Vec<Mutation>,
425}
426
427fn serializable_attribute_value(value: BorrowedAttributeValue<'_>) -> AttributeValue {
428    match value {
429        BorrowedAttributeValue::Text(value) => AttributeValue::Text(value.to_string()),
430        BorrowedAttributeValue::Bool(value) => AttributeValue::Bool(value),
431        BorrowedAttributeValue::Float(value) => AttributeValue::Float(value),
432        BorrowedAttributeValue::Int(value) => AttributeValue::Int(value),
433        BorrowedAttributeValue::None => AttributeValue::None,
434        BorrowedAttributeValue::Any(_) => panic!("Cannot serialize attribute value"),
435    }
436}
437
438macro_rules! collect_field_mutations {
439    ($($method:ident($field:ident: $field_ty:ty) => $variant:ident;)*) => {
440        $(
441            fn $method(&mut self, $field: $field_ty) {
442                self.edits.push(Mutation::$variant { $field })
443            }
444        )*
445    };
446}
447
448macro_rules! collect_string_mutations {
449    ($($method:ident($field:ident) => $variant:ident;)*) => {
450        $(
451            fn $method(&mut self, $field: &str) {
452                self.edits.push(Mutation::$variant {
453                    $field: $field.into(),
454                })
455            }
456        )*
457    };
458}
459
460macro_rules! collect_unit_mutations {
461    ($($method:ident => $variant:ident;)*) => {
462        $(
463            fn $method(&mut self) {
464                self.edits.push(Mutation::$variant)
465            }
466        )*
467    };
468}
469
470impl WriteMutations for Mutations {
471    collect_field_mutations! {
472        push_id(id: ElementId) => PushId;
473        set_id(id: ElementId) => SetId;
474        child(index: usize) => Child;
475        append_children(m: usize) => AppendChildren;
476        replace_with(m: usize) => ReplaceWith;
477        insert_after(m: usize) => InsertAfter;
478        insert_before(m: usize) => InsertBefore;
479    }
480
481    fn create_element(&mut self, tag: &str, ns: Option<&str>) {
482        self.edits.push(Mutation::CreateElement {
483            tag: tag.to_string(),
484            ns: ns.map(str::to_string),
485        })
486    }
487
488    collect_string_mutations! {
489        create_text(value) => CreateText;
490        set_text(value) => SetText;
491        add_event_listener(name) => NewEventListener;
492        remove_event_listener(name) => RemoveEventListener;
493    }
494
495    collect_unit_mutations! {
496        pop => Pop;
497        clone => Clone;
498        remove => Remove;
499    }
500
501    fn set_attribute(&mut self, name: &str, ns: Option<&str>, value: BorrowedAttributeValue<'_>) {
502        self.edits.push(Mutation::SetAttribute {
503            name: name.to_string(),
504            ns: ns.map(str::to_string),
505            value: serializable_attribute_value(value),
506        })
507    }
508}
509
510/// A struct that ignores all mutations
511pub struct NoOpMutations;
512
513macro_rules! noop_write_mutations {
514    ($($method:ident($($arg:ident: $arg_ty:ty),*);)*) => {
515        $(
516            fn $method(&mut self, $($arg: $arg_ty),*) {
517                $(let _ = $arg;)*
518            }
519        )*
520    };
521}
522
523impl WriteMutations for NoOpMutations {
524    fn can_cache_template_roots(&mut self) -> bool {
525        false
526    }
527
528    write_mutation_methods!(noop_write_mutations,);
529}