Files
dioxus/packages/native/src/mutation_writer.rs
T
Jonathan Kelleyotgerrogla 2356820+otgerrogla@users.noreply.github.comNico BurnskokoDavid HusičkaluoxiaozeroEvan Almloff
12c6938346 feat: inline dioxus-native from blitz (#3675)
* Rename dioxus-blitz to dioxus-native

* Add CSS import support (#139)

* Remove data dependencie from NetProvider + global provider.

* Feat CSS import

* Move net handler code out of util

* Fixing test...

* Use the better suited OnceLock instead of LazyLock

* Removing static and move resource callback back out of the document.

Signed-off-by: koko <kokos.kekse@gmail.com>

* Make things work

Signed-off-by: koko <kokos.kekse@gmail.com>

* Fix the servo example

Signed-off-by: koko <kokos.kekse@gmail.com>

* Replace servo image with a more compact version

Signed-off-by: koko <kokos.kekse@gmail.com>

* revert: 8467b271

Signed-off-by: koko <kokos.kekse@gmail.com>

---------

Signed-off-by: koko <kokos.kekse@gmail.com>

* Create readme app

* WPT: reuse FontContext to avoid reloading system fonts

Takes runtime for css-flexbox from 12s -> 8s

* Move Devtools to traits crate

* Suppress unused variable warning

* Implement net for DioxusDocument

Signed-off-by: Nico Burns <nico@nicoburns.com>

* Eliminate BlitzWindowEvent type

Signed-off-by: Nico Burns <nico@nicoburns.com>

* Make net crate use document id instead of window id

Signed-off-by: Nico Burns <nico@nicoburns.com>

* Rename and document net traits

Signed-off-by: Nico Burns <nico@nicoburns.com>

* Split `dioxus-native` crate into `dioxus-native` and `blitz-shell` crates (#163)

* Rename waker module to event

Signed-off-by: Nico Burns <nico@nicoburns.com>

* Create seperate DioxusNativeApplication

Signed-off-by: Nico Burns <nico@nicoburns.com>

* Rename dioxus-native crate to blitz-shell

Signed-off-by: Nico Burns <nico@nicoburns.com>

* Split blitz-shell out of dioxus-native crate

Signed-off-by: Nico Burns <nico@nicoburns.com>

* Remove need for dioxus-native to depend directly on stylo

---------

Signed-off-by: Nico Burns <nico@nicoburns.com>

* Unnest dioxus-native modules

Signed-off-by: Nico Burns <nico@nicoburns.com>

* Split MutationWriter into it's own module

Signed-off-by: Nico Burns <nico@nicoburns.com>

* Split HtmlDocument into it's own crate

Signed-off-by: Nico Burns <nico@nicoburns.com>

* Rename dom field of Window to doc

Signed-off-by: Nico Burns <nico@nicoburns.com>

* Support prefers-color-scheme (dark mode)

* Remove use of the `todo` macro in event handling and add a comment about it being a todo (#168)

Using the `todo` macro causes a panic when this event is encountered. Noop is preferable in this case as it does not crash the application. This is especially annoying on platforms where `Ime::Enabled` event is sent upon application start even when the user is using a keyboard layout that does not need an IME support. There was added a "TODO" comment that informs about this feature being missing. Most IDEs and code editors have support/plugins/extensions to work with this type of comments and this type of comment is already used widely across the codebase. As such, the information about this feature not being implemented is not lost.

* Upgrade to dioxus 0.6-rc.0

Signed-off-by: Nico Burns <nico@nicoburns.com>

* Fix text input in dioxus-native

Signed-off-by: Nico Burns <nico@nicoburns.com>

* Make dioxus-native depend dioxus-core and dioxus-html rather than dioxus

* Remove "full" feature from tokio

* Move Viewport to blitz_traits

Signed-off-by: Nico Burns <nico@nicoburns.com>

* Make tokio optional in dioxus-native and blitz-shell

Signed-off-by: Nico Burns <nico@nicoburns.com>

* Make SVG support optional

* Abstract renderer behind a trait

blitz-shell no longer depends on blitz-renderer-vello

Signed-off-by: Nico Burns <nico@nicoburns.com>

* Split HTML launch functions out in `blitz` crate

* feat: Add mousedown and mouseup event (#172)

* feat: Add mousedown and mouseup event

* feat: Triggering :active pseudo-class

* fix: Triggering :active pseudo-class

* Rename RendererEvent to DomEvent

Signed-off-by: Nico Burns <nico@nicoburns.com>

* Rename Document to BaseDocument. DocumentLike to Document

Signed-off-by: Nico Burns <nico@nicoburns.com>

* Move event and document abstractions to blitz-traits

Signed-off-by: Nico Burns <nico@nicoburns.com>

* Rename BlitzEvent to BlitzShellEvent

Signed-off-by: Nico Burns <nico@nicoburns.com>

* Restyle when style attribute is mutated

* feat: Support text box mouse selection (#182)

* Rename raw_dom_data to data

Signed-off-by: Nico Burns <nico@nicoburns.com>

* Rename dioxus-native to native

* Add to workspace

* simplify launch module, integrate native

* Fix it up again

* Update cargo lock

* FIx: propagate accessibility feature fla

* implement asset resolver

* add native to feature match in cli

* Implement document and history for native

* use no-op eval

* clippy

* typos

* typo

* Convert logs to trace

* Bump blitz to alpha releases

* unset image data

* fix style integration with blitz

* fix a few lints

* wire up ids

* implement a few more events

* add more helpful todo note

* bump ci to allow edition 2024, use newer nightly

* make clippy happy

* also update matrix tests to rust 2024

* Add autofocus

Co-authored-by: otgerrogla 2356820+otgerrogla@users.noreply.github.com

* Wait for the suspense boundary above the router to resolve before sending the first streaming chunk (#3891)

* Change the suspense streaming context after suspense is resolved in the router

* Wait for the suspense context before streaming the first chunk in ssr

* forward more response parts

* warn

* extract the hydration format into a crate

* Fix hydration when no data is sent

* forward web and server features

* Fix suspense that starts on the client

* fix use_server_future on the client

* Add documentation for fullstack hooks and protocol

* downgrade rust edition

* fix clippy

* fix tests

* fix clippy

* Fix pure web compile

* undo changes to the fullstack router example

* fix formatting

* hoist a bunch of deps

---------

Signed-off-by: koko <kokos.kekse@gmail.com>
Signed-off-by: Nico Burns <nico@nicoburns.com>
Co-authored-by: Nico Burns <nico@nicoburns.com>
Co-authored-by: koko <72217393+kokoISnoTarget@users.noreply.github.com>
Co-authored-by: David Husička <25853767+DavidHusicka@users.noreply.github.com>
Co-authored-by: luoxiaozero <48741584+luoxiaozero@users.noreply.github.com>
Co-authored-by: Evan Almloff <evanalmloff@gmail.com>
2025-04-08 17:30:49 -07:00

576 lines
20 KiB
Rust

use crate::{dioxus_document::qual_name, NodeId};
use blitz_dom::{
local_name, namespace_url,
node::{Attribute, NodeSpecificData},
ns, BaseDocument, ElementNodeData, NodeData, QualName, RestyleHint,
};
use dioxus_core::{
AttributeValue, ElementId, Template, TemplateAttribute, TemplateNode, WriteMutations,
};
use rustc_hash::FxHashMap;
use std::collections::HashSet;
/// The state of the Dioxus integration with the RealDom
#[derive(Debug)]
pub struct DioxusState {
/// Store of templates keyed by unique name
templates: FxHashMap<Template, Vec<NodeId>>,
/// Stack machine state for applying dioxus mutations
stack: Vec<NodeId>,
/// Mapping from vdom ElementId -> rdom NodeId
node_id_mapping: Vec<Option<NodeId>>,
}
/// A writer for mutations that can be used with the RealDom.
pub struct MutationWriter<'a> {
/// The realdom associated with this writer
pub doc: &'a mut BaseDocument,
/// The state associated with this writer
pub state: &'a mut DioxusState,
pub style_nodes: HashSet<usize>,
/// The (latest) node which has been mounted in and had autofocus=true, if any
#[cfg(feature = "autofocus")]
pub node_to_autofocus: Option<usize>,
}
impl<'a> MutationWriter<'a> {
pub fn new(doc: &'a mut BaseDocument, state: &'a mut DioxusState) -> Self {
MutationWriter {
doc,
state,
style_nodes: HashSet::new(),
#[cfg(feature = "autofocus")]
node_to_autofocus: None,
}
}
fn is_style_node(&self, node_id: NodeId) -> bool {
self.doc
.get_node(node_id)
.unwrap()
.data
.is_element_with_tag_name(&local_name!("style"))
}
fn maybe_push_style_node(&mut self, node_id: impl Into<Option<NodeId>>) {
if let Some(node_id) = node_id.into() {
if self.is_style_node(node_id) {
self.style_nodes.insert(node_id);
}
}
}
#[track_caller]
fn maybe_push_parent_style_node(&mut self, node_id: NodeId) {
let parent_id = self.doc.get_node(node_id).unwrap().parent;
self.maybe_push_style_node(parent_id);
}
}
impl Drop for MutationWriter<'_> {
fn drop(&mut self) {
// Add/Update inline stylesheets (<style> elements)
for &id in &self.style_nodes {
self.doc.upsert_stylesheet_for_node(id);
}
#[cfg(feature = "autofocus")]
if let Some(node_id) = self.node_to_autofocus {
if self.doc.get_node(node_id).is_some() {
self.doc.set_focus_to(node_id);
}
}
}
}
impl DioxusState {
/// Initialize the DioxusState in the RealDom
pub fn create(_doc: &mut BaseDocument, mount_node: NodeId) -> Self {
Self {
templates: FxHashMap::default(),
stack: vec![mount_node],
node_id_mapping: vec![Some(mount_node)],
}
}
/// Convert an ElementId to a NodeId
pub fn element_to_node_id(&self, element_id: ElementId) -> NodeId {
self.try_element_to_node_id(element_id).unwrap()
}
/// Attempt to convert an ElementId to a NodeId. This will return None if the ElementId is not in the RealDom.
pub fn try_element_to_node_id(&self, element_id: ElementId) -> Option<NodeId> {
self.node_id_mapping.get(element_id.0).copied().flatten()
}
}
impl MutationWriter<'_> {
/// Update an ElementId -> NodeId mapping
fn set_id_mapping(&mut self, node_id: NodeId, element_id: ElementId) {
let element_id: usize = element_id.0;
// Ensure node_id_mapping is large enough to contain element_id
if self.state.node_id_mapping.len() <= element_id {
self.state.node_id_mapping.resize(element_id + 1, None);
}
// Set the new mapping
self.state.node_id_mapping[element_id] = Some(node_id);
}
/// Find a child in the document by child index path
fn load_child(&self, path: &[u8]) -> NodeId {
let mut current = self
.doc
.get_node(*self.state.stack.last().unwrap())
.unwrap();
for i in path {
let new_id = current.children[*i as usize];
current = self.doc.get_node(new_id).unwrap();
}
current.id
}
fn create_template_node(&mut self, node: &TemplateNode) -> NodeId {
match node {
TemplateNode::Element {
tag,
namespace,
attrs,
children,
} => {
let name = qual_name(tag, *namespace);
let attrs = attrs
.iter()
.filter_map(|attr| match attr {
TemplateAttribute::Static {
name,
value,
namespace,
} => Some(Attribute {
name: qual_name(name, *namespace),
value: value.to_string(),
}),
TemplateAttribute::Dynamic { .. } => None,
})
.collect();
let mut data = ElementNodeData::new(name, attrs);
data.flush_style_attribute(self.doc.guard());
let child_ids: Vec<NodeId> = children
.iter()
.map(|child| self.create_template_node(child))
.collect();
let id = self.doc.create_node(NodeData::Element(data));
let node = self.doc.get_node(id).unwrap();
// Initialise style data
*node.stylo_element_data.borrow_mut() = Some(Default::default());
if let Some(src_attr) = node.attr(local_name!("src")) {
crate::assets::fetch_image(self.doc, id, src_attr.to_string());
}
let rel_attr = node.attr(local_name!("rel"));
let href_attr = node.attr(local_name!("href"));
if let (Some("stylesheet"), Some(href)) = (rel_attr, href_attr) {
crate::assets::fetch_linked_stylesheet(self.doc, id, href.to_string());
}
// If the node has an "id" attribute, store it in the ID map.
if let Some(id_attr) = node.attr(local_name!("id")) {
self.doc.nodes_to_id.insert(id_attr.to_string(), id);
}
for &child_id in &child_ids {
self.doc.get_node_mut(child_id).unwrap().parent = Some(id);
}
self.doc.get_node_mut(id).unwrap().children = child_ids;
id
}
TemplateNode::Text { text } => self.doc.create_text_node(text),
TemplateNode::Dynamic { .. } => self.doc.create_node(NodeData::Comment),
}
}
}
impl WriteMutations for MutationWriter<'_> {
fn append_children(&mut self, id: ElementId, m: usize) {
#[cfg(feature = "tracing")]
tracing::info!("append_children id:{} m:{}", id.0, m);
let children = self.state.stack.split_off(self.state.stack.len() - m);
let parent = self.state.element_to_node_id(id);
for child in children {
self.doc.get_node_mut(parent).unwrap().children.push(child);
self.doc.get_node_mut(child).unwrap().parent = Some(parent);
}
self.maybe_push_style_node(parent);
}
fn assign_node_id(&mut self, path: &'static [u8], id: ElementId) {
#[cfg(feature = "tracing")]
tracing::info!("assign_node_id path:{:?} id:{}", path, id.0);
// If there is an existing node already mapped to that ID and
// it has no parent, then drop it
if let Some(node_id) = self.state.try_element_to_node_id(id) {
if let Some(node) = self.doc.get_node(node_id) {
if node.parent.is_none() {
self.doc.remove_and_drop_node(node_id);
}
}
}
let node_id = self.load_child(path);
self.set_id_mapping(node_id, id);
}
fn create_placeholder(&mut self, id: ElementId) {
#[cfg(feature = "tracing")]
tracing::info!("create_placeholder id:{}", id.0);
let node_id = self.doc.create_node(NodeData::Comment);
self.set_id_mapping(node_id, id);
self.state.stack.push(node_id);
}
fn create_text_node(&mut self, value: &str, id: ElementId) {
#[cfg(feature = "tracing")]
tracing::info!("create_text_node id:{} text:{}", id.0, value);
let node_id = self.doc.create_text_node(value);
self.set_id_mapping(node_id, id);
self.state.stack.push(node_id);
}
#[allow(clippy::map_entry)]
fn load_template(&mut self, template: Template, index: usize, id: ElementId) {
if !self.state.templates.contains_key(&template) {
let template_root_ids: Vec<NodeId> = template
.roots
.iter()
.map(|root| self.create_template_node(root))
.collect();
self.state.templates.insert(template, template_root_ids);
}
let template_entry = &self.state.templates[&template];
let template_node_id = template_entry[index];
let clone_id = self.doc.deep_clone_node(template_node_id);
#[cfg(feature = "autofocus")]
autofocus_node_recursive(self.doc, &mut self.node_to_autofocus, clone_id);
self.set_id_mapping(clone_id, id);
self.state.stack.push(clone_id);
}
fn replace_node_with(&mut self, id: ElementId, m: usize) {
#[cfg(feature = "tracing")]
tracing::info!("replace_node_with id:{} m:{}", id.0, m);
let new_nodes = self.state.stack.split_off(self.state.stack.len() - m);
let anchor_node_id = self.state.element_to_node_id(id);
self.maybe_push_parent_style_node(anchor_node_id);
self.doc.insert_before(anchor_node_id, &new_nodes);
self.doc.remove_node(anchor_node_id);
}
fn replace_placeholder_with_nodes(&mut self, path: &'static [u8], m: usize) {
#[cfg(feature = "tracing")]
tracing::info!("replace_placeholder_with_nodes path:{:?} m:{}", path, m);
let new_nodes = self.state.stack.split_off(self.state.stack.len() - m);
let anchor_node_id = self.load_child(path);
self.maybe_push_parent_style_node(anchor_node_id);
self.doc.insert_before(anchor_node_id, &new_nodes);
self.doc.remove_node(anchor_node_id);
}
fn insert_nodes_after(&mut self, id: ElementId, m: usize) {
#[cfg(feature = "tracing")]
tracing::info!("insert_nodes_after id:{} m:{}", id.0, m);
let new_nodes = self.state.stack.split_off(self.state.stack.len() - m);
let anchor_node_id = self.state.element_to_node_id(id);
let next_sibling_id = self
.doc
.get_node(anchor_node_id)
.unwrap()
.forward(1)
.map(|node| node.id);
match next_sibling_id {
Some(anchor_node_id) => {
self.doc.insert_before(anchor_node_id, &new_nodes);
}
None => self.doc.append(anchor_node_id, &new_nodes),
}
self.maybe_push_parent_style_node(anchor_node_id);
}
fn insert_nodes_before(&mut self, id: ElementId, m: usize) {
#[cfg(feature = "tracing")]
tracing::info!("insert_nodes_before id:{} m:{}", id.0, m);
let new_nodes = self.state.stack.split_off(self.state.stack.len() - m);
let anchor_node_id = self.state.element_to_node_id(id);
self.doc.insert_before(anchor_node_id, &new_nodes);
self.maybe_push_parent_style_node(anchor_node_id);
}
fn set_attribute(
&mut self,
name: &'static str,
ns: Option<&'static str>,
value: &AttributeValue,
id: ElementId,
) {
let node_id = self.state.element_to_node_id(id);
#[cfg(feature = "tracing")]
tracing::info!(
"set_attribute node_id:{} ns: {:?} name:{}, value:{:?}",
node_id,
ns,
name,
value
);
self.doc.snapshot_node(node_id);
let mut queued_image = None;
let mut queued_stylesheet = None;
{
let node = &mut self.doc.nodes[node_id];
let stylo_element_data = &mut *node.stylo_element_data.borrow_mut();
if let Some(data) = stylo_element_data {
data.hint |= RestyleHint::restyle_subtree();
}
if let NodeData::Element(ref mut element) = node.data {
if element.name.local == local_name!("input") && name == "checked" {
set_input_checked_state(element, value);
}
// FIXME: support other non-text attributes
else if let AttributeValue::Text(val) = value {
if name == "value" {
// Update text input value
if let Some(input_data) = element.text_input_data_mut() {
input_data.set_text(
&mut self.doc.font_ctx,
&mut self.doc.layout_ctx,
val,
);
}
}
// FIXME check namespace
let existing_attr = element
.attrs
.iter_mut()
.find(|attr| attr.name.local == *name);
if let Some(existing_attr) = existing_attr {
existing_attr.value.clear();
existing_attr.value.push_str(val);
} else {
// we have overloaded the style namespace to accumulate style attributes without a `style` block
if ns == Some("style") {
// todo: need to accumulate style attributes into a single style
//
// element.
} else {
element.attrs.push(Attribute {
name: qual_name(name, ns),
value: val.to_string(),
});
}
}
if name == "style" {
element.flush_style_attribute(&self.doc.guard);
}
if name == "src" && !val.is_empty() {
element.node_specific_data = NodeSpecificData::None;
queued_image = Some(val.to_string());
}
if element.name.local == local_name!("link")
&& name == "href"
&& !val.is_empty()
{
queued_stylesheet = Some(val.to_string());
}
}
if let AttributeValue::None = value {
// Update text input value
if name == "value" {
if let Some(input_data) = element.text_input_data_mut() {
input_data.set_text(
&mut self.doc.font_ctx,
&mut self.doc.layout_ctx,
"",
);
}
}
// FIXME: check namespace
element.attrs.retain(|attr| attr.name.local != *name);
}
}
}
if let Some(queued_image) = queued_image {
crate::assets::fetch_image(self.doc, node_id, queued_image);
}
if let Some(queued_stylesheet) = queued_stylesheet {
crate::assets::fetch_linked_stylesheet(self.doc, node_id, queued_stylesheet);
}
}
fn set_node_text(&mut self, value: &str, id: ElementId) {
#[cfg(feature = "tracing")]
tracing::info!("set_node_text id:{} value:{}", id.0, value);
let node_id = self.state.element_to_node_id(id);
let node = self.doc.get_node_mut(node_id).unwrap();
let text = match node.data {
NodeData::Text(ref mut text) => text,
// todo: otherwise this is basically element.textContent which is a bit different - need to parse as html
_ => return,
};
let changed = text.content != value;
if changed {
text.content.clear();
text.content.push_str(value);
let parent = node.parent;
self.maybe_push_style_node(parent);
}
}
fn create_event_listener(&mut self, _name: &'static str, _id: ElementId) {
// we're going to actually set the listener here as a placeholder - in JS this would also be a placeholder
// we might actually just want to attach the attribute to the root element (delegation)
self.set_attribute(
_name,
None,
&AttributeValue::Text("<rust func>".into()),
_id,
);
// also set the data-dioxus-id attribute so we can find the element later
self.set_attribute(
"data-dioxus-id",
None,
&AttributeValue::Text(_id.0.to_string()),
_id,
);
// let node_id = self.state.element_to_node_id(id);
// let mut node = self.rdom.get_mut(node_id).unwrap();
// node.add_event_listener(name);
}
fn remove_event_listener(&mut self, _name: &'static str, _id: ElementId) {
// let node_id = self.state.element_to_node_id(id);
// let mut node = self.rdom.get_mut(node_id).unwrap();
// node.remove_event_listener(name);
}
fn remove_node(&mut self, id: ElementId) {
#[cfg(feature = "tracing")]
tracing::info!("remove_node id:{}", id.0);
let node_id = self.state.element_to_node_id(id);
self.doc.remove_node(node_id);
}
fn push_root(&mut self, id: ElementId) {
#[cfg(feature = "tracing")]
tracing::info!("push_root id:{}", id.0,);
let node_id = self.state.element_to_node_id(id);
self.state.stack.push(node_id);
}
}
/// Set 'checked' state on an input based on given attributevalue
fn set_input_checked_state(element: &mut ElementNodeData, value: &AttributeValue) {
let checked: bool;
match value {
AttributeValue::Bool(checked_bool) => {
checked = *checked_bool;
}
AttributeValue::Text(val) => {
if let Ok(checked_bool) = val.parse() {
checked = checked_bool;
} else {
return;
};
}
_ => {
return;
}
};
match element.node_specific_data {
NodeSpecificData::CheckboxInput(ref mut checked_mut) => *checked_mut = checked,
// If we have just constructed the element, set the node attribute,
// and NodeSpecificData will be created from that later
// this simulates the checked attribute being set in html,
// and the element's checked property being set from that
NodeSpecificData::None => element.attrs.push(Attribute {
name: QualName {
prefix: None,
ns: ns!(html),
local: local_name!("checked"),
},
value: checked.to_string(),
}),
_ => {}
}
}
#[cfg(feature = "autofocus")]
fn autofocus_node_recursive(
doc: &BaseDocument,
node_to_autofocus: &mut Option<usize>,
node_id: usize,
) {
if let Some(node) = doc.get_node(node_id) {
if node.is_focussable() {
if let NodeData::Element(ref element) = node.data {
if let Some(value) = element.attr(local_name!("autofocus")) {
if value == "true" {
*node_to_autofocus = Some(node_id);
}
}
}
}
for child_node_id in &node.children {
autofocus_node_recursive(doc, node_to_autofocus, *child_node_id);
}
}
}