Merge pull request #600 from DioxusLabs/jk/templates-v3

Template architecture, async components, inline iterators, error boundaries, multiple renderers
This commit is contained in:
Jon Kelley 2022-12-06 17:50:52 -08:00 committed by GitHub
commit 2b4d19247c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
220 changed files with 16174 additions and 18848 deletions

View file

@ -1,4 +1,3 @@
[workspace]
members = [
"packages/dioxus",
@ -13,16 +12,20 @@ members = [
"packages/mobile",
"packages/interpreter",
"packages/fermi",
"packages/tui",
"packages/liveview",
"packages/autofmt",
"packages/rsx",
"docs/guide",
# "packages/tui",
# "packages/native-core",
# "packages/native-core-macro",
]
exclude = [
"packages/tui",
"packages/native-core",
"packages/native-core-macro",
"docs/guide",
]
# This is a "virtual package"
# It is not meant to be published, but is used so "cargo run --example XYZ" works properly
[package]
@ -41,7 +44,7 @@ rust-version = "1.60.0"
[dev-dependencies]
dioxus = { path = "./packages/dioxus" }
dioxus-desktop = { path = "./packages/desktop", features = ["hot-reload"] }
dioxus-desktop = { path = "./packages/desktop" }
dioxus-ssr = { path = "./packages/ssr" }
dioxus-router = { path = "./packages/router" }
fermi = { path = "./packages/fermi" }
@ -63,4 +66,4 @@ env_logger = "0.9.0"
[profile.release]
opt-level = 3
lto = true
debug = true
debug = true

View file

@ -1,6 +1,6 @@
#![allow(non_snake_case)]
use dioxus::events::MouseEvent;
use dioxus::events::MouseData;
use dioxus::prelude::*;
fn main() {
@ -20,7 +20,7 @@ fn App(cx: Scope) -> Element {
// ANCHOR: component_with_handler
#[derive(Props)]
pub struct FancyButtonProps<'a> {
on_click: EventHandler<'a, MouseEvent>,
on_click: EventHandler<'a, MouseData>,
}
pub fn FancyButton<'a>(cx: Scope<'a, FancyButtonProps<'a>>) -> Element<'a> {

View file

@ -1,7 +1,7 @@
// ANCHOR: all
#![allow(non_snake_case)]
use dioxus::events::FormEvent;
use dioxus::events::FormData;
use dioxus::prelude::*;
fn main() {
@ -83,7 +83,7 @@ fn Meme<'a>(cx: Scope<'a>, caption: &'a str) -> Element<'a> {
fn CaptionEditor<'a>(
cx: Scope<'a>,
caption: &'a str,
on_input: EventHandler<'a, FormEvent>,
on_input: EventHandler<'a, FormData>,
) -> Element<'a> {
let input_style = r"
border: none;

View file

@ -1,7 +1,7 @@
// ANCHOR: all
#![allow(non_snake_case)]
use dioxus::events::FormEvent;
use dioxus::events::FormData;
use dioxus::prelude::*;
fn main() {
@ -150,7 +150,7 @@ fn Meme<'a>(cx: Scope<'a>, caption: &'a str) -> Element<'a> {
fn CaptionEditor<'a>(
cx: Scope<'a>,
caption: &'a str,
on_input: EventHandler<'a, FormEvent>,
on_input: EventHandler<'a, FormData>,
) -> Element<'a> {
let is_dark_mode = use_is_dark_mode(&cx);

View file

@ -7,405 +7,407 @@ fn main() {
fn app(cx: Scope) -> Element {
cx.render(rsx! {
div {
align_content: "a",
align_items: "a",
align_self: "a",
alignment_adjust: "a",
alignment_baseline: "a",
all: "a",
alt: "a",
animation: "a",
animation_delay: "a",
animation_direction: "a",
animation_duration: "a",
animation_fill_mode: "a",
animation_iteration_count: "a",
animation_name: "a",
animation_play_state: "a",
animation_timing_function: "a",
azimuth: "a",
backface_visibility: "a",
background: "a",
background_attachment: "a",
background_clip: "a",
background_color: "a",
background_image: "a",
background_origin: "a",
background_position: "a",
background_repeat: "a",
background_size: "a",
background_blend_mode: "a",
baseline_shift: "a",
bleed: "a",
bookmark_label: "a",
bookmark_level: "a",
bookmark_state: "a",
border: "a",
border_color: "a",
border_style: "a",
border_width: "a",
border_bottom: "a",
border_bottom_color: "a",
border_bottom_style: "a",
border_bottom_width: "a",
border_left: "a",
border_left_color: "a",
border_left_style: "a",
border_left_width: "a",
border_right: "a",
border_right_color: "a",
border_right_style: "a",
border_right_width: "a",
border_top: "a",
border_top_color: "a",
border_top_style: "a",
border_top_width: "a",
border_collapse: "a",
border_image: "a",
border_image_outset: "a",
border_image_repeat: "a",
border_image_slice: "a",
border_image_source: "a",
border_image_width: "a",
border_radius: "a",
border_bottom_left_radius: "a",
border_bottom_right_radius: "a",
border_top_left_radius: "a",
border_top_right_radius: "a",
border_spacing: "a",
bottom: "a",
box_decoration_break: "a",
box_shadow: "a",
box_sizing: "a",
box_snap: "a",
break_after: "a",
break_before: "a",
break_inside: "a",
buffered_rendering: "a",
caption_side: "a",
clear: "a",
clear_side: "a",
clip: "a",
clip_path: "a",
clip_rule: "a",
color: "a",
color_adjust: "a",
color_correction: "a",
color_interpolation: "a",
color_interpolation_filters: "a",
color_profile: "a",
color_rendering: "a",
column_fill: "a",
column_gap: "a",
column_rule: "a",
column_rule_color: "a",
column_rule_style: "a",
column_rule_width: "a",
column_span: "a",
columns: "a",
column_count: "a",
column_width: "a",
contain: "a",
content: "a",
counter_increment: "a",
counter_reset: "a",
counter_set: "a",
cue: "a",
cue_after: "a",
cue_before: "a",
cursor: "a",
direction: "a",
display: "a",
display_inside: "a",
display_outside: "a",
display_extras: "a",
display_box: "a",
dominant_baseline: "a",
elevation: "a",
empty_cells: "a",
enable_background: "a",
fill: "a",
fill_opacity: "a",
fill_rule: "a",
filter: "a",
float: "a",
float_defer_column: "a",
float_defer_page: "a",
float_offset: "a",
float_wrap: "a",
flow_into: "a",
flow_from: "a",
flex: "a",
flex_basis: "a",
flex_grow: "a",
flex_shrink: "a",
flex_flow: "a",
flex_direction: "a",
flex_wrap: "a",
flood_color: "a",
flood_opacity: "a",
font: "a",
font_family: "a",
font_size: "a",
font_stretch: "a",
font_style: "a",
font_weight: "a",
font_feature_settings: "a",
font_kerning: "a",
font_language_override: "a",
font_size_adjust: "a",
font_synthesis: "a",
font_variant: "a",
font_variant_alternates: "a",
font_variant_caps: "a",
font_variant_east_asian: "a",
font_variant_ligatures: "a",
font_variant_numeric: "a",
font_variant_position: "a",
footnote_policy: "a",
glyph_orientation_horizontal: "a",
glyph_orientation_vertical: "a",
grid: "a",
grid_auto_flow: "a",
grid_auto_columns: "a",
grid_auto_rows: "a",
grid_template: "a",
grid_template_areas: "a",
grid_template_columns: "a",
grid_template_rows: "a",
grid_area: "a",
grid_column: "a",
grid_column_start: "a",
grid_column_end: "a",
grid_row: "a",
grid_row_start: "a",
grid_row_end: "a",
hanging_punctuation: "a",
height: "a",
hyphenate_character: "a",
hyphenate_limit_chars: "a",
hyphenate_limit_last: "a",
hyphenate_limit_lines: "a",
hyphenate_limit_zone: "a",
hyphens: "a",
icon: "a",
image_orientation: "a",
image_resolution: "a",
image_rendering: "a",
ime: "a",
ime_align: "a",
ime_mode: "a",
ime_offset: "a",
ime_width: "a",
initial_letters: "a",
inline_box_align: "a",
isolation: "a",
justify_content: "a",
justify_items: "a",
justify_self: "a",
kerning: "a",
left: "a",
letter_spacing: "a",
lighting_color: "a",
line_box_contain: "a",
line_break: "a",
line_grid: "a",
line_height: "a",
line_slack: "a",
line_snap: "a",
list_style: "a",
list_style_image: "a",
list_style_position: "a",
list_style_type: "a",
margin: "a",
margin_bottom: "a",
margin_left: "a",
margin_right: "a",
margin_top: "a",
marker: "a",
marker_end: "a",
marker_mid: "a",
marker_pattern: "a",
marker_segment: "a",
marker_start: "a",
marker_knockout_left: "a",
marker_knockout_right: "a",
marker_side: "a",
marks: "a",
marquee_direction: "a",
marquee_play_count: "a",
marquee_speed: "a",
marquee_style: "a",
mask: "a",
mask_image: "a",
mask_repeat: "a",
mask_position: "a",
mask_clip: "a",
mask_origin: "a",
mask_size: "a",
mask_box: "a",
mask_box_outset: "a",
mask_box_repeat: "a",
mask_box_slice: "a",
mask_box_source: "a",
mask_box_width: "a",
mask_type: "a",
max_height: "a",
max_lines: "a",
max_width: "a",
min_height: "a",
min_width: "a",
mix_blend_mode: "a",
nav_down: "a",
nav_index: "a",
nav_left: "a",
nav_right: "a",
nav_up: "a",
object_fit: "a",
object_position: "a",
offset_after: "a",
offset_before: "a",
offset_end: "a",
offset_start: "a",
opacity: "a",
order: "a",
orphans: "a",
outline: "a",
outline_color: "a",
outline_style: "a",
outline_width: "a",
outline_offset: "a",
overflow: "a",
overflow_x: "a",
overflow_y: "a",
overflow_style: "a",
overflow_wrap: "a",
padding: "a",
padding_bottom: "a",
padding_left: "a",
padding_right: "a",
padding_top: "a",
page: "a",
page_break_after: "a",
page_break_before: "a",
page_break_inside: "a",
paint_order: "a",
pause: "a",
pause_after: "a",
pause_before: "a",
perspective: "a",
perspective_origin: "a",
pitch: "a",
pitch_range: "a",
play_during: "a",
pointer_events: "a",
position: "a",
quotes: "a",
region_fragment: "a",
resize: "a",
rest: "a",
rest_after: "a",
rest_before: "a",
richness: "a",
right: "a",
ruby_align: "a",
ruby_merge: "a",
ruby_position: "a",
scroll_behavior: "a",
scroll_snap_coordinate: "a",
scroll_snap_destination: "a",
scroll_snap_points_x: "a",
scroll_snap_points_y: "a",
scroll_snap_type: "a",
shape_image_threshold: "a",
shape_inside: "a",
shape_margin: "a",
shape_outside: "a",
shape_padding: "a",
shape_rendering: "a",
size: "a",
speak: "a",
speak_as: "a",
speak_header: "a",
speak_numeral: "a",
speak_punctuation: "a",
speech_rate: "a",
stop_color: "a",
stop_opacity: "a",
stress: "a",
string_set: "a",
stroke: "a",
stroke_dasharray: "a",
stroke_dashoffset: "a",
stroke_linecap: "a",
stroke_linejoin: "a",
stroke_miterlimit: "a",
stroke_opacity: "a",
stroke_width: "a",
tab_size: "a",
table_layout: "a",
text_align: "a",
text_align_all: "a",
text_align_last: "a",
text_anchor: "a",
text_combine_upright: "a",
text_decoration: "a",
text_decoration_color: "a",
text_decoration_line: "a",
text_decoration_style: "a",
text_decoration_skip: "a",
text_emphasis: "a",
text_emphasis_color: "a",
text_emphasis_style: "a",
text_emphasis_position: "a",
text_emphasis_skip: "a",
text_height: "a",
text_indent: "a",
text_justify: "a",
text_orientation: "a",
text_overflow: "a",
text_rendering: "a",
text_shadow: "a",
text_size_adjust: "a",
text_space_collapse: "a",
text_spacing: "a",
text_transform: "a",
text_underline_position: "a",
text_wrap: "a",
top: "a",
touch_action: "a",
transform: "a",
transform_box: "a",
transform_origin: "a",
transform_style: "a",
transition: "a",
transition_delay: "a",
transition_duration: "a",
transition_property: "a",
unicode_bidi: "a",
vector_effect: "a",
vertical_align: "a",
visibility: "a",
voice_balance: "a",
voice_duration: "a",
voice_family: "a",
voice_pitch: "a",
voice_range: "a",
voice_rate: "a",
voice_stress: "a",
voice_volumn: "a",
volume: "a",
white_space: "a",
widows: "a",
width: "a",
will_change: "a",
word_break: "a",
word_spacing: "a",
word_wrap: "a",
wrap_flow: "a",
wrap_through: "a",
writing_mode: "a",
z_index: "a",
align_content: "a",
align_items: "a",
align_self: "a",
alignment_adjust: "a",
alignment_baseline: "a",
all: "a",
alt: "a",
animation: "a",
animation_delay: "a",
animation_direction: "a",
animation_duration: "a",
animation_fill_mode: "a",
animation_iteration_count: "a",
animation_name: "a",
animation_play_state: "a",
animation_timing_function: "a",
azimuth: "a",
backface_visibility: "a",
background: "a",
background_attachment: "a",
background_clip: "a",
background_color: "a",
background_image: "a",
background_origin: "a",
background_position: "a",
background_repeat: "a",
background_size: "a",
background_blend_mode: "a",
baseline_shift: "a",
bleed: "a",
bookmark_label: "a",
bookmark_level: "a",
bookmark_state: "a",
border: "a",
border_color: "a",
border_style: "a",
border_width: "a",
border_bottom: "a",
border_bottom_color: "a",
border_bottom_style: "a",
border_bottom_width: "a",
border_left: "a",
border_left_color: "a",
border_left_style: "a",
border_left_width: "a",
border_right: "a",
border_right_color: "a",
border_right_style: "a",
border_right_width: "a",
border_top: "a",
border_top_color: "a",
border_top_style: "a",
border_top_width: "a",
border_collapse: "a",
border_image: "a",
border_image_outset: "a",
border_image_repeat: "a",
border_image_slice: "a",
border_image_source: "a",
border_image_width: "a",
border_radius: "a",
border_bottom_left_radius: "a",
border_bottom_right_radius: "a",
border_top_left_radius: "a",
border_top_right_radius: "a",
border_spacing: "a",
bottom: "a",
box_decoration_break: "a",
box_shadow: "a",
box_sizing: "a",
box_snap: "a",
break_after: "a",
break_before: "a",
break_inside: "a",
buffered_rendering: "a",
caption_side: "a",
clear: "a",
clear_side: "a",
clip: "a",
clip_path: "a",
clip_rule: "a",
color: "a",
color_adjust: "a",
color_correction: "a",
color_interpolation: "a",
color_interpolation_filters: "a",
color_profile: "a",
color_rendering: "a",
column_fill: "a",
column_gap: "a",
column_rule: "a",
column_rule_color: "a",
column_rule_style: "a",
column_rule_width: "a",
column_span: "a",
columns: "a",
column_count: "a",
column_width: "a",
contain: "a",
content: "a",
counter_increment: "a",
counter_reset: "a",
counter_set: "a",
cue: "a",
cue_after: "a",
cue_before: "a",
cursor: "a",
direction: "a",
display: "a",
display_inside: "a",
display_outside: "a",
display_extras: "a",
display_box: "a",
dominant_baseline: "a",
elevation: "a",
empty_cells: "a",
enable_background: "a",
fill: "a",
fill_opacity: "a",
fill_rule: "a",
filter: "a",
float: "a",
float_defer_column: "a",
float_defer_page: "a",
float_offset: "a",
float_wrap: "a",
flow_into: "a",
flow_from: "a",
flex: "a",
flex_basis: "a",
flex_grow: "a",
flex_shrink: "a",
flex_flow: "a",
flex_direction: "a",
flex_wrap: "a",
flood_color: "a",
flood_opacity: "a",
font: "a",
font_family: "a",
font_size: "a",
font_stretch: "a",
font_style: "a",
font_weight: "a",
font_feature_settings: "a",
font_kerning: "a",
font_language_override: "a",
font_size_adjust: "a",
font_synthesis: "a",
font_variant: "a",
font_variant_alternates: "a",
font_variant_caps: "a",
font_variant_east_asian: "a",
font_variant_ligatures: "a",
font_variant_numeric: "a",
font_variant_position: "a",
footnote_policy: "a",
glyph_orientation_horizontal: "a",
glyph_orientation_vertical: "a",
grid: "a",
grid_auto_flow: "a",
grid_auto_columns: "a",
grid_auto_rows: "a",
grid_template: "a",
grid_template_areas: "a",
grid_template_columns: "a",
grid_template_rows: "a",
grid_area: "a",
grid_column: "a",
grid_column_start: "a",
grid_column_end: "a",
grid_row: "a",
grid_row_start: "a",
grid_row_end: "a",
hanging_punctuation: "a",
height: "a",
hyphenate_character: "a",
hyphenate_limit_chars: "a",
hyphenate_limit_last: "a",
hyphenate_limit_lines: "a",
hyphenate_limit_zone: "a",
hyphens: "a",
icon: "a",
image_orientation: "a",
image_resolution: "a",
image_rendering: "a",
ime: "a",
ime_align: "a",
ime_mode: "a",
ime_offset: "a",
ime_width: "a",
initial_letters: "a",
inline_box_align: "a",
isolation: "a",
justify_content: "a",
justify_items: "a",
justify_self: "a",
kerning: "a",
left: "a",
letter_spacing: "a",
lighting_color: "a",
line_box_contain: "a",
line_break: "a",
line_grid: "a",
line_height: "a",
line_slack: "a",
line_snap: "a",
list_style: "a",
list_style_image: "a",
list_style_position: "a",
list_style_type: "a",
margin: "a",
margin_bottom: "a",
margin_left: "a",
margin_right: "a",
margin_top: "a",
marker: "a",
marker_end: "a",
marker_mid: "a",
marker_pattern: "a",
marker_segment: "a",
marker_start: "a",
marker_knockout_left: "a",
marker_knockout_right: "a",
marker_side: "a",
marks: "a",
marquee_direction: "a",
marquee_play_count: "a",
marquee_speed: "a",
marquee_style: "a",
mask: "a",
mask_image: "a",
mask_repeat: "a",
mask_position: "a",
mask_clip: "a",
mask_origin: "a",
mask_size: "a",
mask_box: "a",
mask_box_outset: "a",
mask_box_repeat: "a",
mask_box_slice: "a",
mask_box_source: "a",
mask_box_width: "a",
mask_type: "a",
max_height: "a",
max_lines: "a",
max_width: "a",
min_height: "a",
min_width: "a",
mix_blend_mode: "a",
nav_down: "a",
nav_index: "a",
nav_left: "a",
nav_right: "a",
nav_up: "a",
object_fit: "a",
object_position: "a",
offset_after: "a",
offset_before: "a",
offset_end: "a",
offset_start: "a",
opacity: "a",
order: "a",
orphans: "a",
outline: "a",
outline_color: "a",
outline_style: "a",
outline_width: "a",
outline_offset: "a",
overflow: "a",
overflow_x: "a",
overflow_y: "a",
overflow_style: "a",
overflow_wrap: "a",
padding: "a",
padding_bottom: "a",
padding_left: "a",
padding_right: "a",
padding_top: "a",
page: "a",
page_break_after: "a",
page_break_before: "a",
page_break_inside: "a",
paint_order: "a",
pause: "a",
pause_after: "a",
pause_before: "a",
perspective: "a",
perspective_origin: "a",
pitch: "a",
pitch_range: "a",
play_during: "a",
pointer_events: "a",
position: "a",
quotes: "a",
region_fragment: "a",
resize: "a",
rest: "a",
rest_after: "a",
rest_before: "a",
richness: "a",
right: "a",
ruby_align: "a",
ruby_merge: "a",
ruby_position: "a",
scroll_behavior: "a",
scroll_snap_coordinate: "a",
scroll_snap_destination: "a",
scroll_snap_points_x: "a",
scroll_snap_points_y: "a",
scroll_snap_type: "a",
shape_image_threshold: "a",
shape_inside: "a",
shape_margin: "a",
shape_outside: "a",
shape_padding: "a",
shape_rendering: "a",
size: "a",
speak: "a",
speak_as: "a",
speak_header: "a",
speak_numeral: "a",
speak_punctuation: "a",
speech_rate: "a",
stop_color: "a",
stop_opacity: "a",
stress: "a",
string_set: "a",
stroke: "a",
stroke_dasharray: "a",
stroke_dashoffset: "a",
stroke_linecap: "a",
stroke_linejoin: "a",
stroke_miterlimit: "a",
stroke_opacity: "a",
stroke_width: "a",
tab_size: "a",
table_layout: "a",
text_align: "a",
text_align_all: "a",
text_align_last: "a",
text_anchor: "a",
text_combine_upright: "a",
text_decoration: "a",
text_decoration_color: "a",
text_decoration_line: "a",
text_decoration_style: "a",
text_decoration_skip: "a",
text_emphasis: "a",
text_emphasis_color: "a",
text_emphasis_style: "a",
text_emphasis_position: "a",
text_emphasis_skip: "a",
text_height: "a",
text_indent: "a",
text_justify: "a",
text_orientation: "a",
text_overflow: "a",
text_rendering: "a",
text_shadow: "a",
text_size_adjust: "a",
text_space_collapse: "a",
text_spacing: "a",
text_transform: "a",
text_underline_position: "a",
text_wrap: "a",
top: "a",
touch_action: "a",
transform: "a",
transform_box: "a",
transform_origin: "a",
transform_style: "a",
transition: "a",
transition_delay: "a",
transition_duration: "a",
transition_property: "a",
unicode_bidi: "a",
vector_effect: "a",
vertical_align: "a",
visibility: "a",
voice_balance: "a",
voice_duration: "a",
voice_family: "a",
voice_pitch: "a",
voice_range: "a",
voice_rate: "a",
voice_stress: "a",
voice_volumn: "a",
volume: "a",
white_space: "a",
widows: "a",
width: "a",
will_change: "a",
word_break: "a",
word_spacing: "a",
word_wrap: "a",
wrap_flow: "a",
wrap_through: "a",
writing_mode: "a",
z_index: "a",
"This example isn't quite useful yet"
}
})
}

View file

@ -1,4 +1,4 @@
use dioxus::{events::*, prelude::*};
use dioxus::{events::*, html::MouseEvent, prelude::*};
fn main() {
dioxus_desktop::launch(app);

View file

@ -29,9 +29,7 @@ fn app(cx: Scope) -> Element {
cx.render(rsx! {
div {
Child1 {
text: first
}
Child1 { text: first }
}
})
}
@ -59,9 +57,7 @@ struct C2Props<'a> {
fn Child2<'a>(cx: Scope<'a, C2Props<'a>>) -> Element {
cx.render(rsx! {
Child3 {
text: cx.props.text
}
Child3 { text: cx.props.text }
})
}

View file

@ -25,6 +25,7 @@ fn app(cx: Scope) -> Element {
if val.get() == "0" {
val.set(String::new());
}
val.make_mut().push_str(num.to_string().as_str());
};
@ -99,12 +100,8 @@ fn app(cx: Scope) -> Element {
}
}
div { class: "digit-keys",
button { class: "calculator-key key-0", onclick: move |_| input_digit(0),
"0"
}
button { class: "calculator-key key-dot", onclick: move |_| val.make_mut().push('.'),
""
}
button { class: "calculator-key key-0", onclick: move |_| input_digit(0), "0" }
button { class: "calculator-key key-dot", onclick: move |_| val.make_mut().push('.'), "" }
(1..10).map(|k| rsx!{
button {
class: "calculator-key {k}",
@ -116,22 +113,13 @@ fn app(cx: Scope) -> Element {
}
}
div { class: "operator-keys",
button { class: "calculator-key key-divide", onclick: move |_| input_operator("/"),
"÷"
}
button { class: "calculator-key key-multiply", onclick: move |_| input_operator("*"),
"×"
}
button { class: "calculator-key key-subtract", onclick: move |_| input_operator("-"),
""
}
button { class: "calculator-key key-add", onclick: move |_| input_operator("+"),
"+"
}
button { class: "calculator-key key-equals",
onclick: move |_| {
val.set(format!("{}", calc_val(val.as_str())));
},
button { class: "calculator-key key-divide", onclick: move |_| input_operator("/"), "÷" }
button { class: "calculator-key key-multiply", onclick: move |_| input_operator("*"), "×" }
button { class: "calculator-key key-subtract", onclick: move |_| input_operator("-"), "" }
button { class: "calculator-key key-add", onclick: move |_| input_operator("+"), "+" }
button {
class: "calculator-key key-equals",
onclick: move |_| val.set(format!("{}", calc_val(val.as_str()))),
"="
}
}

22
examples/callback.rs Normal file
View file

@ -0,0 +1,22 @@
use dioxus::prelude::*;
fn main() {
dioxus_desktop::launch(app);
}
fn app(cx: Scope) -> Element {
let login = use_callback!(cx, move |_| async move {
let res = reqwest::get("https://dog.ceo/api/breeds/list/all")
.await
.unwrap()
.text()
.await
.unwrap();
println!("{:#?}, ", res);
});
cx.render(rsx! {
button { onclick: login, "Click me!" }
})
}

View file

@ -10,21 +10,23 @@ fn main() {
let mut dom = VirtualDom::new(app);
let _ = dom.rebuild();
let output = dioxus_ssr::render_vdom(&dom);
let output = dioxus_ssr::render(&dom);
println!("{}", output);
}
fn app(cx: Scope) -> Element {
let nf = NodeFactory::new(&cx);
// let nf = NodeFactory::new(&cx);
let mut attrs = dioxus::core::exports::bumpalo::collections::Vec::new_in(nf.bump());
// let mut attrs = dioxus::core::exports::bumpalo::collections::Vec::new_in(nf.bump());
attrs.push(nf.attr("client-id", format_args!("abc123"), None, false));
// attrs.push(nf.attr("client-id", format_args!("abc123"), None, false));
attrs.push(nf.attr("name", format_args!("bob"), None, false));
// attrs.push(nf.attr("name", format_args!("bob"), None, false));
attrs.push(nf.attr("age", format_args!("47"), None, false));
// attrs.push(nf.attr("age", format_args!("47"), None, false));
Some(nf.raw_element("my-element", None, &[], attrs.into_bump_slice(), &[], None))
// Some(nf.raw_element("my-element", None, &[], attrs.into_bump_slice(), &[], None))
todo!()
}

View file

@ -1,12 +1,8 @@
#![allow(non_snake_case)]
//! Render a bunch of doggos!
use dioxus::prelude::*;
use std::collections::HashMap;
fn main() {
dioxus_desktop::launch(app);
dioxus_desktop::launch(|cx| render!(app_root {}));
}
#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
@ -14,10 +10,10 @@ struct ListBreeds {
message: HashMap<String, Vec<String>>,
}
fn app(cx: Scope) -> Element {
let breed = use_state(&cx, || None);
async fn app_root(cx: Scope<'_>) -> Element {
let breed = use_state(cx, || "deerhound".to_string());
let breeds = use_future(&cx, (), |_| async move {
let breeds = use_future!(cx, || async move {
reqwest::get("https://dog.ceo/api/breeds/list/all")
.await
.unwrap()
@ -25,32 +21,26 @@ fn app(cx: Scope) -> Element {
.await
});
match breeds.value() {
Some(Ok(breeds)) => cx.render(rsx! {
div {
match breeds.await {
Ok(breeds) => cx.render(rsx! {
div { height: "500px",
h1 { "Select a dog breed!" }
div { display: "flex",
ul { flex: "50%",
breeds.message.keys().map(|cur_breed| rsx!(
li {
for cur_breed in breeds.message.keys().take(10) {
li { key: "{cur_breed}",
button {
onclick: move |_| breed.set(Some(cur_breed.clone())),
onclick: move |_| breed.set(cur_breed.clone()),
"{cur_breed}"
}
}
))
}
div { flex: "50%",
match breed.get() {
Some(breed) => rsx!( Breed { breed: breed.clone() } ),
None => rsx!("No Breed selected"),
}
}
div { flex: "50%", breed_pic { breed: breed.to_string() } }
}
}
}),
Some(Err(_e)) => cx.render(rsx! { div { "Error fetching breeds" } }),
None => cx.render(rsx! { div { "Loading dogs..." } }),
Err(_e) => cx.render(rsx! { div { "Error fetching breeds" } }),
}
}
@ -60,8 +50,8 @@ struct DogApi {
}
#[inline_props]
fn Breed(cx: Scope, breed: String) -> Element {
let fut = use_future(&cx, (breed,), |(breed,)| async move {
async fn breed_pic(cx: Scope, breed: String) -> Element {
let fut = use_future!(cx, |breed| async move {
reqwest::get(format!("https://dog.ceo/api/breed/{}/images/random", breed))
.await
.unwrap()
@ -69,21 +59,23 @@ fn Breed(cx: Scope, breed: String) -> Element {
.await
});
cx.render(match fut.value() {
Some(Ok(resp)) => rsx! {
button {
onclick: move |_| fut.restart(),
"Click to fetch another doggo"
}
match fut.await {
Ok(resp) => render! {
div {
button {
onclick: move |_| {
println!("clicked");
fut.restart()
},
"Click to fetch another doggo"
}
img {
src: "{resp.message}",
max_width: "500px",
max_height: "500px",
src: "{resp.message}",
}
}
},
Some(Err(_)) => rsx! { div { "loading dogs failed" } },
None => rsx! { div { "loading dogs..." } },
})
Err(_) => render! { div { "loading dogs failed" } },
}
}

View file

@ -18,5 +18,17 @@ fn app(cx: Scope) -> Element {
onclick: move |_| val.set("invalid"),
"Set an invalid number"
}
(0..5).map(|i| rsx! {
demo_c { x: i }
})
})
}
#[inline_props]
fn demo_c(cx: Scope, x: i32) -> Element {
cx.render(rsx! {
h1 {
"asdasdasdasd {x}"
}
})
}

View file

@ -16,7 +16,7 @@ fn app(cx: Scope) -> Element {
oninput: move |e| script.set(e.value.clone()),
}
button {
onclick: move |_| eval(script),
onclick: move |_| eval(script.to_string()),
"Execute"
}
}

View file

@ -15,7 +15,7 @@ fn app(cx: Scope) -> Element {
cx.render(rsx! {
div { "hello {name}!" }
Child {}
ChildWithRef{}
ChildWithRef {}
})
}

View file

@ -21,7 +21,7 @@ fn main() {
fn app(cx: Scope) -> Element {
let files = use_ref(&cx, Files::new);
render! {
cx.render(rsx! {
div {
link { href:"https://fonts.googleapis.com/icon?family=Material+Icons", rel:"stylesheet", }
style { include_str!("./assets/fileexplorer.css") }
@ -62,7 +62,7 @@ fn app(cx: Scope) -> Element {
})
}
}
}
})
}
struct Files {

View file

@ -72,7 +72,7 @@ fn app(cx: Scope) -> Element {
td { class:"col-md-1" }
td { class:"col-md-1", "{item.key}" }
td { class:"col-md-1", onclick: move |_| selected.set(Some(id)),
a { class: "lbl", item.labels }
a { class: "lbl", "{item.labels[0]}{item.labels[1]}{item.labels[2]}" }
}
td { class: "col-md-1",
a { class: "remove", onclick: move |_| { items.write().remove(id); },

View file

@ -0,0 +1,13 @@
use dioxus::prelude::*;
fn main() {
dioxus_desktop::launch(app);
}
fn app(cx: Scope) -> Element {
cx.render(rsx! { generic_child::<i32>{} })
}
fn generic_child<T>(cx: Scope) -> Element {
cx.render(rsx! { div {} })
}

View file

@ -14,7 +14,7 @@ use dioxus_desktop::Config;
fn main() {
let vdom = VirtualDom::new(app);
let content = dioxus_ssr::render_vdom_cfg(&vdom, |f| f.pre_render(true));
let content = dioxus_ssr::pre_render(&vdom);
dioxus_desktop::launch_cfg(app, Config::new().with_prerendered(content));
}

View file

@ -2,7 +2,7 @@
//!
//! There is some conversion happening when input types are checkbox/radio/select/textarea etc.
use dioxus::{events::FormEvent, prelude::*};
use dioxus::prelude::*;
fn main() {
dioxus_desktop::launch(app);
@ -28,7 +28,6 @@ const FIELDS: &[(&str, &str)] = &[
("text", ""),
("time", ""),
("url", ""),
//
// less supported things
("hidden", ""),
("month", ""), // degrades to text most of the time, but works properly as "value'"
@ -114,7 +113,7 @@ fn app(cx: Scope) -> Element {
}
}
FIELDS.iter().map(|(field, value)| rsx!(
FIELDS.iter().map(|(field, value)| rsx! {
div {
input {
id: "{field}",
@ -131,8 +130,7 @@ fn app(cx: Scope) -> Element {
}
br {}
}
))
})
}
})
}

View file

@ -1,7 +1,6 @@
//! This example demonstrates the following:
//! Futures in a callback, Router, and Forms
use dioxus::events::*;
use dioxus::prelude::*;
fn main() {
@ -37,11 +36,10 @@ fn app(cx: Scope) -> Element {
form {
onsubmit: onsubmit,
prevent_default: "onsubmit", // Prevent the default behavior of <form> to post
input { "type": "text", id: "username", name: "username" }
input { r#type: "text", id: "username", name: "username" }
label { "Username" }
br {}
input { "type": "password", id: "password", name: "password" }
input { r#type: "password", id: "password", name: "password" }
label { "Password" }
br {}
button { "Login" }

View file

@ -22,7 +22,7 @@ fn app(cx: Scope) -> Element {
button {
onclick: move |evt| {
println!("clicked! bottom no bubbling");
evt.cancel_bubble();
evt.stop_propogation();
},
"Dont propogate"
}

View file

@ -19,6 +19,7 @@
use dioxus::events::*;
use dioxus::html::input_data::keyboard_types::Key;
use dioxus::html::MouseEvent;
use dioxus::prelude::*;
use dioxus_desktop::wry::application::dpi::LogicalSize;
use dioxus_desktop::{Config, WindowBuilder};

View file

@ -30,7 +30,7 @@ fn app(cx: Scope) -> Element {
}
fn BlogPost(cx: Scope) -> Element {
let post = dioxus_router::use_route(&cx).last_segment()?;
let post = dioxus_router::use_route(&cx).last_segment().unwrap();
cx.render(rsx! {
div {
@ -46,7 +46,7 @@ struct Query {
}
fn User(cx: Scope) -> Element {
let post = dioxus_router::use_route(&cx).last_segment()?;
let post = dioxus_router::use_route(&cx).last_segment().unwrap();
let query = dioxus_router::use_route(&cx)
.query::<Query>()

View file

@ -5,10 +5,11 @@ use dioxus::prelude::*;
fn main() {
let mut vdom = VirtualDom::new(example);
vdom.rebuild();
_ = vdom.rebuild();
let out = dioxus_ssr::render_vdom_cfg(&vdom, |c| c.newline(true).indent(true));
println!("{}", out);
let mut renderer = dioxus_ssr::Renderer::new();
renderer.pretty = true;
renderer.render(&vdom);
}
fn example(cx: Scope) -> Element {

View file

@ -165,13 +165,13 @@ fn app(cx: Scope) -> Element {
// Can pass in props directly as an expression
{
let props = TallerProps {a: "hello", children: Default::default()};
let props = TallerProps {a: "hello", children: cx.render(rsx!(()))};
rsx!(Taller { ..props })
}
// Spreading can also be overridden manually
Taller {
..TallerProps { a: "ballin!", children: Default::default() },
..TallerProps { a: "ballin!", children: cx.render(rsx!(()) )},
a: "not ballin!"
}
@ -183,7 +183,7 @@ fn app(cx: Scope) -> Element {
// Components can be generic too
// This component takes i32 type to give you typed input
TypedInput::<TypedInputProps<i32>> {}
TypedInput::<i32> {}
// Type inference can be used too
TypedInput { initial: 10.0 }

View file

@ -6,23 +6,26 @@ fn main() {
fn app(cx: Scope) -> Element {
cx.render(rsx!(
// Use Map directly to lazily pull elements
(0..10).map(|f| rsx! { "{f}" }),
// Collect into an intermediate collection if necessary
["a", "b", "c"]
.into_iter()
.map(|f| rsx! { "{f}" })
.collect::<Vec<_>>(),
// Use optionals
Some(rsx! { "Some" }),
div {
// Use Map directly to lazily pull elements
(0..10).map(|f| rsx! { "{f}" }),
// Collect into an intermediate collection if necessary, and call into_iter
["a", "b", "c", "d", "e", "f"]
.into_iter()
.map(|f| rsx! { "{f}" })
.collect::<Vec<_>>()
.into_iter(),
// Use optionals
Some(rsx! { "Some" }),
// use a for loop where the body itself is RSX
for name in 0..10 {
rsx! { "{name}" }
div {"{name}"}
}
// Or even use an unterminated conditional
if true {
rsx!{ "hello world!" }
}

View file

@ -5,6 +5,7 @@
use std::fmt::Write;
use dioxus::prelude::*;
use dioxus_ssr::config::Config;
fn main() {
// We can render VirtualDoms
@ -25,14 +26,14 @@ fn main() {
// We can configure the SSR rendering to add ids for rehydration
println!(
"{}",
dioxus_ssr::render_vdom_cfg(&vdom, |c| c.pre_render(true))
dioxus_ssr::render_vdom_cfg(&vdom, Config::default().pre_render(true))
);
// We can even render as a writer
let mut file = String::new();
let _ = file.write_fmt(format_args!(
"{}",
dioxus_ssr::TextRenderer::from_vdom(&vdom, Default::default())
dioxus_ssr::SsrRender::default().render_vdom(&vdom)
));
println!("{}", file);
}

View file

@ -1,6 +1,6 @@
// Thanks to @japsu and their project https://github.com/japsu/jatsi for the example!
use dioxus::{events::MouseEvent, prelude::*};
use dioxus::prelude::*;
fn main() {
dioxus_desktop::launch(app);
@ -9,7 +9,7 @@ fn main() {
fn app(cx: Scope) -> Element {
let val = use_state(&cx, || 5);
render! {
cx.render(rsx! {
div {
user_select: "none",
webkit_user_select: "none",
@ -31,7 +31,7 @@ fn app(cx: Scope) -> Element {
}
}
}
}
})
}
#[derive(Props)]
@ -70,19 +70,21 @@ pub fn Die<'a>(cx: Scope<'a, DieProps<'a>>) -> Element {
.map(|((x, y), _)| {
let dcx = x * OFFSET;
let dcy = y * OFFSET;
rsx!(circle {
cx: "{dcx}",
cy: "{dcy}",
r: "{DOT_RADIUS}",
fill: "#333"
})
rsx! {
circle {
cx: "{dcx}",
cy: "{dcy}",
r: "{DOT_RADIUS}",
fill: "#333"
}
}
});
render! {
cx.render(rsx! {
svg {
onclick: move |e| cx.props.onclick.call(e),
prevent_default: "onclick",
"dioxus-prevent-default": "onclick",
class: "die",
view_box: "-1000 -1000 2000 2000",
@ -97,5 +99,5 @@ pub fn Die<'a>(cx: Scope<'a, DieProps<'a>>) -> Element {
dots
}
}
})
}

View file

@ -22,10 +22,10 @@ pub struct TodoItem {
}
pub fn app(cx: Scope<()>) -> Element {
let todos = use_state(&cx, im_rc::HashMap::<u32, TodoItem>::default);
let filter = use_state(&cx, || FilterState::All);
let draft = use_state(&cx, || "".to_string());
let todo_id = use_state(&cx, || 0);
let todos = use_state(cx, im_rc::HashMap::<u32, TodoItem>::default);
let filter = use_state(cx, || FilterState::All);
let draft = use_state(cx, || "".to_string());
let todo_id = use_state(cx, || 0);
// Filter the todos based on the filter state
let mut filtered_todos = todos
@ -57,7 +57,9 @@ pub fn app(cx: Scope<()>) -> Element {
placeholder: "What needs to be done?",
value: "{draft}",
autofocus: "true",
oninput: move |evt| draft.set(evt.value.clone()),
oninput: move |evt| {
draft.set(evt.value.clone());
},
onkeydown: move |evt| {
if evt.key() == Key::Enter && !draft.is_empty() {
todos.make_mut().insert(
@ -114,7 +116,7 @@ pub struct TodoEntryProps<'a> {
}
pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element {
let is_editing = use_state(&cx, || false);
let is_editing = use_state(cx, || false);
let todos = cx.props.todos.get();
let todo = &todos[&cx.props.id];

View file

@ -35,13 +35,13 @@ fn app(cx: Scope) -> Element {
nav { class: "md:ml-auto flex flex-wrap items-center text-base justify-center" }
button {
class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0",
onmousedown: |evt| evt.cancel_bubble(),
onmousedown: |evt| evt.stop_propogation(),
onclick: move |_| window.set_minimized(true),
"Minimize"
}
button {
class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0",
onmousedown: |evt| evt.cancel_bubble(),
onmousedown: |evt| evt.stop_propogation(),
onclick: move |_| {
window.set_fullscreen(!**fullscreen);
@ -52,7 +52,7 @@ fn app(cx: Scope) -> Element {
}
button {
class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0",
onmousedown: |evt| evt.cancel_bubble(),
onmousedown: |evt| evt.stop_propogation(),
onclick: move |_| window.close(),
"Close"
}
@ -66,7 +66,7 @@ fn app(cx: Scope) -> Element {
div {
button {
class: "inline-flex items-center text-white bg-green-500 border-0 py-1 px-3 hover:bg-green-700 rounded",
onmousedown: |evt| evt.cancel_bubble(),
onmousedown: |evt| evt.stop_propogation(),
onclick: move |_| {
window.set_always_on_top(!always_on_top);
always_on_top.set(!always_on_top);
@ -77,7 +77,7 @@ fn app(cx: Scope) -> Element {
div {
button {
class: "inline-flex items-center text-white bg-blue-500 border-0 py-1 px-3 hover:bg-green-700 rounded",
onmousedown: |evt| evt.cancel_bubble(),
onmousedown: |evt| evt.stop_propogation(),
onclick: move |_| {
window.set_decorations(!decorations);
decorations.set(!decorations);
@ -88,7 +88,7 @@ fn app(cx: Scope) -> Element {
div {
button {
class: "inline-flex items-center text-white bg-blue-500 border-0 py-1 px-3 hover:bg-green-700 rounded",
onmousedown: |evt| evt.cancel_bubble(),
onmousedown: |evt| evt.stop_propogation(),
onclick: move |_| window.set_title("Dioxus Application"),
"Change Title"
}

View file

@ -9,6 +9,7 @@ use syn::{
pub struct InlinePropsBody {
pub attrs: Vec<Attribute>,
pub vis: syn::Visibility,
pub maybe_async: Option<Token![async]>,
pub fn_token: Token![fn],
pub ident: Ident,
pub cx_token: Box<Pat>,
@ -25,6 +26,7 @@ pub struct InlinePropsBody {
impl Parse for InlinePropsBody {
fn parse(input: ParseStream) -> Result<Self> {
let attrs: Vec<Attribute> = input.call(Attribute::parse_outer)?;
let maybe_async: Option<Token![async]> = input.parse().ok();
let vis: Visibility = input.parse()?;
let fn_token = input.parse()?;
@ -57,6 +59,7 @@ impl Parse for InlinePropsBody {
Ok(Self {
vis,
maybe_async,
fn_token,
ident,
generics,
@ -84,6 +87,7 @@ impl ToTokens for InlinePropsBody {
block,
cx_token,
attrs,
maybe_async,
..
} = self;
@ -151,7 +155,7 @@ impl ToTokens for InlinePropsBody {
}
#(#attrs)*
#vis fn #ident #fn_generics (#cx_token: Scope<#scope_lifetime #struct_name #generics>) #output
#maybe_async #vis fn #ident #fn_generics (#cx_token: Scope<#scope_lifetime #struct_name #generics>) #output
#where_clause
{
let #struct_name { #(#field_names),* } = &cx.props;

View file

@ -11,8 +11,7 @@ use dioxus_rsx as rsx;
#[proc_macro]
pub fn format_args_f(input: TokenStream) -> TokenStream {
use rsx::*;
let item = parse_macro_input!(input as IfmtInput);
format_args_f_impl(item)
format_args_f_impl(parse_macro_input!(input as IfmtInput))
.unwrap_or_else(|err| err.to_compile_error())
.into()
}
@ -40,19 +39,6 @@ pub fn rsx(s: TokenStream) -> TokenStream {
}
}
/// A version of the rsx! macro that does not use templates. Used for testing diffing
#[proc_macro]
pub fn rsx_without_templates(s: TokenStream) -> TokenStream {
match syn::parse::<rsx::CallBody>(s) {
Err(err) => err.to_compile_error().into(),
Ok(body) => {
let mut tokens = proc_macro2::TokenStream::new();
body.to_tokens_without_template(&mut tokens);
tokens.into()
}
}
}
/// The render! macro makes it easy for developers to write jsx-style markup in their components.
///
/// The render macro automatically renders rsx - making it unhygenic.
@ -65,18 +51,10 @@ pub fn rsx_without_templates(s: TokenStream) -> TokenStream {
pub fn render(s: TokenStream) -> TokenStream {
match syn::parse::<rsx::CallBody>(s) {
Err(err) => err.to_compile_error().into(),
Ok(body) => {
let mut inner = proc_macro2::TokenStream::new();
body.to_tokens_without_lazynodes(&mut inner);
quote::quote! {
{
let __cx = NodeFactory::new(&cx.scope);
Some(#inner)
}
}
Ok(mut body) => {
body.inline_cx = true;
body.into_token_stream().into()
}
.into_token_stream()
.into(),
}
}

View file

@ -194,7 +194,7 @@ mod field_info {
// children field is automatically defaulted to None
if name == "children" {
builder_attr.default =
Some(syn::parse(quote!(Default::default()).into()).unwrap());
Some(syn::parse(quote!(::dioxus::core::VNode::empty()).into()).unwrap());
}
// auto detect optional

View file

@ -18,40 +18,27 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
bumpalo = { version = "3.6", features = ["collections", "boxed"] }
# faster hashmaps
rustc-hash = "1.1.0"
fxhash = "0.2"
# Used in diffing
longest-increasing-subsequence = "0.1.0"
futures-util = { version = "0.3", default-features = false }
smallvec = "1.6"
slab = "0.4"
futures-channel = "0.3.21"
# internally used
log = "0.4"
# used for noderefs
once_cell = "1.8"
indexmap = "1.7"
# Serialize the Edits for use in Webview/Liveview instances
serde = { version = "1", features = ["derive"], optional = true }
# todo: I want to get rid of this
backtrace = { version = "0.3" }
# allows cloing trait objects
dyn-clone = "1.0.9"
anyhow = "1.0.66"
[dev-dependencies]
tokio = { version = "*", features = ["full"] }
dioxus = { path = "../dioxus" }
[features]
default = []
serialize = ["serde"]
debug_vdom = []
hot-reload = []

View file

@ -1,52 +1,84 @@
# Dioxus-core
# dioxus-core
This is the core crate for the Dioxus Virtual DOM. This README will focus on the technical design and layout of this Virtual DOM implementation. If you want to read more about using Dioxus, then check out the Dioxus crate, documentation, and website.
dioxus-core is a fast and featureful VirtualDom implementation written in and for Rust.
To build new apps with Dioxus or to extend the ecosystem with new hooks or components, use the higher-level `dioxus` crate with the appropriate feature flags.
# Features
- Functions as components
- Hooks for local state
- Task pool for spawning futures
- Template-based architecture
- Asynchronous components
- Suspense boundaries
- Error boundaries through the `anyhow` crate
- Customizable memoization
If just starting out, check out the Guides first.
# General Theory
The dioxus-core `VirtualDom` object is built around the concept of a `Template`. Templates describe a layout tree known at compile time with dynamic parts filled at runtime.
Each component in the VirtualDom works as a dedicated render loop where re-renders are triggered by events external to the VirtualDom, or from the components themselves.
When each component re-renders, it must return an `Element`. In Dioxus, the `Element` type is an alias for `Result<VNode>`. Between two renders, Dioxus compares the inner `VNode` object, and calculates the differences of the dynamic portions of each internal `Template`. If any attributes or elements are different between the old layout and new layout, Dioxus will write modifications to the `Mutations` object.
Dioxus expects the target renderer to save its nodes in a list. Each element is given a numerical ID which can be used to directly index into that list for O(1) lookups.
# Usage
All Dioxus apps start as just a function that takes the [`Scope`] object and returns an [`Element`].
The `dioxus` crate exports the `rsx` macro which transforms a helpful, simpler syntax of Rust into the logic required to build Templates.
First, start with your app:
```rust, ignore
fn app(cx: Scope) -> Element {
render!(div { "hello world" })
cx.render(rsx!( div { "hello world" } ))
}
```
fn main() {
let mut renderer = SomeRenderer::new();
Then, we'll want to create a new VirtualDom using this app as the root component.
// Creating a new virtualdom from a component
let mut dom = VirtualDom::new(app);
```rust, ignore
let mut dom = VirtualDom::new(app);
```
// Patching the renderer with the changes to draw the screen
let edits = dom.rebuild();
renderer.apply(edits);
To build the app into a stream of mutations, we'll use [`VirtualDom::rebuild`]:
// Injecting events
dom.handle_message(SchedulerMsg::Event(UserEvent {
scope_id: None,
priority: EventPriority::High,
element: ElementId(0),
name: "onclick",
data: Arc::new(()),
}));
```rust, ignore
let mutations = dom.rebuild();
// polling asynchronously
dom.wait_for_work().await;
apply_edits_to_real_dom(mutations);
```
// working with a deadline
if let Some(edits) = dom.work_with_deadline(|| false) {
renderer.apply(edits);
We can then wait for any asynchronous components or pending futures using the `wait_for_work()` method. If we have a deadline, then we can use render_with_deadline instead:
```rust, ignore
// Wait for the dom to be marked dirty internally
dom.wait_for_work().await;
// Or wait for a deadline and then collect edits
dom.render_with_deadline(tokio::time::sleep(Duration::from_millis(16)));
```
If an event occurs from outside the virtualdom while waiting for work, then we can cancel the wait using a `select!` block and inject the event.
```rust, ignore
loop {
select! {
evt = real_dom.event() => dom.handle_event("click", evt.data, evt.element, evt.bubbles),
_ = dom.wait_for_work() => {}
}
// getting state of scopes
let scope = dom.get_scope(ScopeId(0)).unwrap();
// Render any work without blocking the main thread for too long
let mutations = dom.render_with_deadline(tokio::time::sleep(Duration::from_millis(10)));
// iterating through the tree
match scope.root_node() {
VNodes::Text(vtext) => dbg!(vtext),
VNodes::Element(vel) => dbg!(vel),
_ => todo!()
}
// And then apply the edits
real_dom.apply(mutations);
}
```
## Internals
@ -82,17 +114,3 @@ The final implementation of Dioxus must:
- Support server-side-rendering (SSR). VNodes should render to a string that can be served via a web server.
- Be "live". Components should be able to be both server rendered and client rendered without needing frontend APIs.
- Be modular. Components and hooks should be work anywhere without worrying about target platform.
## Safety
Dioxus uses unsafe. The design of Dioxus *requires* unsafe (self-referential trees).
All of our test suite passes MIRI without errors.
Dioxus deals with arenas, lifetimes, asynchronous tasks, custom allocators, pinning, and a lot more foundational low-level work that is very difficult to implement with 0 unsafe.
If you don't want to use a crate that uses unsafe, then this crate is not for you.
However, we are always interested in decreasing the scope of the core VirtualDom to make it easier to review. We'd be happy to welcome PRs that can eliminate unsafe code while still upholding the numerous invariants required to execute certain features.

View file

@ -114,6 +114,11 @@ Some essential reading:
- https://web.dev/rail/
- https://indepth.dev/posts/1008/inside-fiber-in-depth-overview-of-the-new-reconciliation-algorithm-in-react
# Templates
If everything is a template, then we'll have the idea that the only children can b Templates
# What's going on?
Dioxus is a framework for "user experience" - not just "user interfaces." Part of the "experience" is keeping the UI

View file

@ -0,0 +1,73 @@
use std::marker::PhantomData;
use crate::{
innerlude::Scoped,
nodes::{ComponentReturn, RenderReturn},
scopes::{Scope, ScopeState},
Element,
};
/// A trait that essentially allows VComponentProps to be used generically
///
/// # Safety
///
/// This should not be implemented outside this module
pub(crate) unsafe trait AnyProps<'a> {
fn props_ptr(&self) -> *const ();
fn render(&'a self, bump: &'a ScopeState) -> RenderReturn<'a>;
unsafe fn memoize(&self, other: &dyn AnyProps) -> bool;
}
pub(crate) struct VProps<'a, P, A, F: ComponentReturn<'a, A> = Element<'a>> {
pub render_fn: fn(Scope<'a, P>) -> F,
pub memo: unsafe fn(&P, &P) -> bool,
pub props: P,
_marker: PhantomData<A>,
}
impl<'a, P, A, F> VProps<'a, P, A, F>
where
F: ComponentReturn<'a, A>,
{
pub(crate) fn new(
render_fn: fn(Scope<'a, P>) -> F,
memo: unsafe fn(&P, &P) -> bool,
props: P,
) -> Self {
Self {
render_fn,
memo,
props,
_marker: PhantomData,
}
}
}
unsafe impl<'a, P, A, F> AnyProps<'a> for VProps<'a, P, A, F>
where
F: ComponentReturn<'a, A>,
{
fn props_ptr(&self) -> *const () {
&self.props as *const _ as *const ()
}
// Safety:
// this will downcast the other ptr as our swallowed type!
// you *must* make this check *before* calling this method
// if your functions are not the same, then you will downcast a pointer into a different type (UB)
unsafe fn memoize(&self, other: &dyn AnyProps) -> bool {
let real_other: &P = &*(other.props_ptr() as *const _ as *const P);
let real_us: &P = &*(self.props_ptr() as *const _ as *const P);
(self.memo)(real_us, real_other)
}
fn render(&'a self, cx: &'a ScopeState) -> RenderReturn<'a> {
let scope: &mut Scoped<P> = cx.bump().alloc(Scoped {
props: &self.props,
scope: cx,
});
// Call the render function directly
(self.render_fn)(scope).into_return(cx)
}
}

View file

@ -1,642 +0,0 @@
use std::fmt::{Arguments, Formatter};
use bumpalo::Bump;
use dyn_clone::{clone_box, DynClone};
/// Possible values for an attribute
// trying to keep values at 3 bytes
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serialize", serde(untagged))]
#[derive(Clone, Debug, PartialEq)]
#[allow(missing_docs)]
pub enum AttributeValue<'a> {
Text(&'a str),
Float32(f32),
Float64(f64),
Int32(i32),
Int64(i64),
Uint32(u32),
Uint64(u64),
Bool(bool),
Vec3Float(f32, f32, f32),
Vec3Int(i32, i32, i32),
Vec3Uint(u32, u32, u32),
Vec4Float(f32, f32, f32, f32),
Vec4Int(i32, i32, i32, i32),
Vec4Uint(u32, u32, u32, u32),
Bytes(&'a [u8]),
Any(ArbitraryAttributeValue<'a>),
}
/// A value that can be converted into an attribute value
pub trait IntoAttributeValue<'a> {
/// Convert into an attribute value
fn into_value(self, bump: &'a Bump) -> AttributeValue<'a>;
}
impl<'a> IntoAttributeValue<'a> for u32 {
fn into_value(self, _: &'a Bump) -> AttributeValue<'a> {
AttributeValue::Uint32(self)
}
}
impl<'a> IntoAttributeValue<'a> for u64 {
fn into_value(self, _: &'a Bump) -> AttributeValue<'a> {
AttributeValue::Uint64(self)
}
}
impl<'a> IntoAttributeValue<'a> for i32 {
fn into_value(self, _: &'a Bump) -> AttributeValue<'a> {
AttributeValue::Int32(self)
}
}
impl<'a> IntoAttributeValue<'a> for i64 {
fn into_value(self, _: &'a Bump) -> AttributeValue<'a> {
AttributeValue::Int64(self)
}
}
impl<'a> IntoAttributeValue<'a> for f32 {
fn into_value(self, _: &'a Bump) -> AttributeValue<'a> {
AttributeValue::Float32(self)
}
}
impl<'a> IntoAttributeValue<'a> for f64 {
fn into_value(self, _: &'a Bump) -> AttributeValue<'a> {
AttributeValue::Float64(self)
}
}
impl<'a> IntoAttributeValue<'a> for bool {
fn into_value(self, _: &'a Bump) -> AttributeValue<'a> {
AttributeValue::Bool(self)
}
}
impl<'a> IntoAttributeValue<'a> for &'a str {
fn into_value(self, _: &'a Bump) -> AttributeValue<'a> {
AttributeValue::Text(self)
}
}
impl<'a> IntoAttributeValue<'a> for Arguments<'_> {
fn into_value(self, bump: &'a Bump) -> AttributeValue<'a> {
use bumpalo::core_alloc::fmt::Write;
let mut str_buf = bumpalo::collections::String::new_in(bump);
str_buf.write_fmt(self).unwrap();
AttributeValue::Text(str_buf.into_bump_str())
}
}
impl<'a> IntoAttributeValue<'a> for &'a [u8] {
fn into_value(self, _: &'a Bump) -> AttributeValue<'a> {
AttributeValue::Bytes(self)
}
}
impl<'a> IntoAttributeValue<'a> for (f32, f32, f32) {
fn into_value(self, _: &'a Bump) -> AttributeValue<'a> {
AttributeValue::Vec3Float(self.0, self.1, self.2)
}
}
impl<'a> IntoAttributeValue<'a> for (i32, i32, i32) {
fn into_value(self, _: &'a Bump) -> AttributeValue<'a> {
AttributeValue::Vec3Int(self.0, self.1, self.2)
}
}
impl<'a> IntoAttributeValue<'a> for (u32, u32, u32) {
fn into_value(self, _: &'a Bump) -> AttributeValue<'a> {
AttributeValue::Vec3Uint(self.0, self.1, self.2)
}
}
impl<'a> IntoAttributeValue<'a> for (f32, f32, f32, f32) {
fn into_value(self, _: &'a Bump) -> AttributeValue<'a> {
AttributeValue::Vec4Float(self.0, self.1, self.2, self.3)
}
}
impl<'a> IntoAttributeValue<'a> for (i32, i32, i32, i32) {
fn into_value(self, _: &'a Bump) -> AttributeValue<'a> {
AttributeValue::Vec4Int(self.0, self.1, self.2, self.3)
}
}
impl<'a> IntoAttributeValue<'a> for (u32, u32, u32, u32) {
fn into_value(self, _: &'a Bump) -> AttributeValue<'a> {
AttributeValue::Vec4Uint(self.0, self.1, self.2, self.3)
}
}
impl<'a, T> IntoAttributeValue<'a> for &'a T
where
T: AnyClone + PartialEq,
{
fn into_value(self, _: &'a Bump) -> AttributeValue<'a> {
AttributeValue::Any(ArbitraryAttributeValue {
value: self,
cmp: |a, b| {
if let Some(a) = a.as_any().downcast_ref::<T>() {
if let Some(b) = b.as_any().downcast_ref::<T>() {
a == b
} else {
false
}
} else {
false
}
},
})
}
}
// todo
#[allow(missing_docs)]
impl<'a> AttributeValue<'a> {
pub fn is_truthy(&self) -> bool {
match self {
AttributeValue::Text(t) => *t == "true",
AttributeValue::Bool(t) => *t,
_ => false,
}
}
pub fn is_falsy(&self) -> bool {
match self {
AttributeValue::Text(t) => *t == "false",
AttributeValue::Bool(t) => !(*t),
_ => false,
}
}
}
impl<'a> std::fmt::Display for AttributeValue<'a> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
AttributeValue::Text(a) => write!(f, "{}", a),
AttributeValue::Float32(a) => write!(f, "{}", a),
AttributeValue::Float64(a) => write!(f, "{}", a),
AttributeValue::Int32(a) => write!(f, "{}", a),
AttributeValue::Int64(a) => write!(f, "{}", a),
AttributeValue::Uint32(a) => write!(f, "{}", a),
AttributeValue::Uint64(a) => write!(f, "{}", a),
AttributeValue::Bool(a) => write!(f, "{}", a),
AttributeValue::Vec3Float(_, _, _) => todo!(),
AttributeValue::Vec3Int(_, _, _) => todo!(),
AttributeValue::Vec3Uint(_, _, _) => todo!(),
AttributeValue::Vec4Float(_, _, _, _) => todo!(),
AttributeValue::Vec4Int(_, _, _, _) => todo!(),
AttributeValue::Vec4Uint(_, _, _, _) => todo!(),
AttributeValue::Bytes(a) => write!(f, "{:?}", a),
AttributeValue::Any(a) => write!(f, "{:?}", a),
}
}
}
#[derive(Clone, Copy)]
#[allow(missing_docs)]
pub struct ArbitraryAttributeValue<'a> {
pub value: &'a dyn AnyClone,
pub cmp: fn(&dyn AnyClone, &dyn AnyClone) -> bool,
}
impl PartialEq for ArbitraryAttributeValue<'_> {
fn eq(&self, other: &Self) -> bool {
(self.cmp)(self.value, other.value)
}
}
impl std::fmt::Debug for ArbitraryAttributeValue<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ArbitraryAttributeValue").finish()
}
}
#[cfg(feature = "serialize")]
impl<'a> serde::Serialize for ArbitraryAttributeValue<'a> {
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
panic!("ArbitraryAttributeValue should not be serialized")
}
}
#[cfg(feature = "serialize")]
impl<'de, 'a> serde::Deserialize<'de> for &'a ArbitraryAttributeValue<'a> {
fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
panic!("ArbitraryAttributeValue is not deserializable!")
}
}
#[cfg(feature = "serialize")]
impl<'de, 'a> serde::Deserialize<'de> for ArbitraryAttributeValue<'a> {
fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
panic!("ArbitraryAttributeValue is not deserializable!")
}
}
/// A clone, sync and send version of `Any`
// we only need the Sync + Send bound when hot reloading is enabled
#[cfg(any(feature = "hot-reload", debug_assertions))]
pub trait AnyClone: std::any::Any + DynClone + Send + Sync {
fn as_any(&self) -> &dyn std::any::Any;
}
#[cfg(not(any(feature = "hot-reload", debug_assertions)))]
pub trait AnyClone: std::any::Any + DynClone {
fn as_any(&self) -> &dyn std::any::Any;
}
#[cfg(any(feature = "hot-reload", debug_assertions))]
impl<T: std::any::Any + DynClone + Send + Sync> AnyClone for T {
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
#[cfg(not(any(feature = "hot-reload", debug_assertions)))]
impl<T: std::any::Any + DynClone> AnyClone for T {
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
dyn_clone::clone_trait_object!(AnyClone);
#[derive(Clone)]
#[allow(missing_docs)]
pub struct OwnedArbitraryAttributeValue {
pub value: Box<dyn AnyClone>,
pub cmp: fn(&dyn AnyClone, &dyn AnyClone) -> bool,
}
impl PartialEq for OwnedArbitraryAttributeValue {
fn eq(&self, other: &Self) -> bool {
(self.cmp)(&*self.value, &*other.value)
}
}
impl std::fmt::Debug for OwnedArbitraryAttributeValue {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OwnedArbitraryAttributeValue").finish()
}
}
#[cfg(feature = "serialize")]
impl serde::Serialize for OwnedArbitraryAttributeValue {
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
panic!("OwnedArbitraryAttributeValue should not be serialized")
}
}
#[cfg(feature = "serialize")]
impl<'de> serde::Deserialize<'de> for &OwnedArbitraryAttributeValue {
fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
panic!("OwnedArbitraryAttributeValue is not deserializable!")
}
}
#[cfg(feature = "serialize")]
impl<'de> serde::Deserialize<'de> for OwnedArbitraryAttributeValue {
fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
panic!("OwnedArbitraryAttributeValue is not deserializable!")
}
}
// todo
#[allow(missing_docs)]
impl<'a> AttributeValue<'a> {
pub fn as_text(&self) -> Option<&'a str> {
match self {
AttributeValue::Text(s) => Some(s),
_ => None,
}
}
pub fn as_float32(&self) -> Option<f32> {
match self {
AttributeValue::Float32(f) => Some(*f),
_ => None,
}
}
pub fn as_float64(&self) -> Option<f64> {
match self {
AttributeValue::Float64(f) => Some(*f),
_ => None,
}
}
pub fn as_int32(&self) -> Option<i32> {
match self {
AttributeValue::Int32(i) => Some(*i),
_ => None,
}
}
pub fn as_int64(&self) -> Option<i64> {
match self {
AttributeValue::Int64(i) => Some(*i),
_ => None,
}
}
pub fn as_uint32(&self) -> Option<u32> {
match self {
AttributeValue::Uint32(i) => Some(*i),
_ => None,
}
}
pub fn as_uint64(&self) -> Option<u64> {
match self {
AttributeValue::Uint64(i) => Some(*i),
_ => None,
}
}
pub fn as_bool(&self) -> Option<bool> {
match self {
AttributeValue::Bool(b) => Some(*b),
_ => None,
}
}
pub fn as_vec3_float(&self) -> Option<(f32, f32, f32)> {
match self {
AttributeValue::Vec3Float(x, y, z) => Some((*x, *y, *z)),
_ => None,
}
}
pub fn as_vec3_int(&self) -> Option<(i32, i32, i32)> {
match self {
AttributeValue::Vec3Int(x, y, z) => Some((*x, *y, *z)),
_ => None,
}
}
pub fn as_vec3_uint(&self) -> Option<(u32, u32, u32)> {
match self {
AttributeValue::Vec3Uint(x, y, z) => Some((*x, *y, *z)),
_ => None,
}
}
pub fn as_vec4_float(&self) -> Option<(f32, f32, f32, f32)> {
match self {
AttributeValue::Vec4Float(x, y, z, w) => Some((*x, *y, *z, *w)),
_ => None,
}
}
pub fn as_vec4_int(&self) -> Option<(i32, i32, i32, i32)> {
match self {
AttributeValue::Vec4Int(x, y, z, w) => Some((*x, *y, *z, *w)),
_ => None,
}
}
pub fn as_vec4_uint(&self) -> Option<(u32, u32, u32, u32)> {
match self {
AttributeValue::Vec4Uint(x, y, z, w) => Some((*x, *y, *z, *w)),
_ => None,
}
}
pub fn as_bytes(&self) -> Option<&[u8]> {
match self {
AttributeValue::Bytes(b) => Some(b),
_ => None,
}
}
pub fn as_any(&self) -> Option<&'a ArbitraryAttributeValue> {
match self {
AttributeValue::Any(a) => Some(a),
_ => None,
}
}
}
/// A owned attribute value.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(
all(feature = "serialize"),
derive(serde::Serialize, serde::Deserialize)
)]
#[allow(missing_docs)]
pub enum OwnedAttributeValue {
Text(String),
Float32(f32),
Float64(f64),
Int32(i32),
Int64(i64),
Uint32(u32),
Uint64(u64),
Bool(bool),
Vec3Float(f32, f32, f32),
Vec3Int(i32, i32, i32),
Vec3Uint(u32, u32, u32),
Vec4Float(f32, f32, f32, f32),
Vec4Int(i32, i32, i32, i32),
Vec4Uint(u32, u32, u32, u32),
Bytes(Vec<u8>),
// TODO: support other types
Any(OwnedArbitraryAttributeValue),
}
impl PartialEq<AttributeValue<'_>> for OwnedAttributeValue {
fn eq(&self, other: &AttributeValue<'_>) -> bool {
match (self, other) {
(Self::Text(l0), AttributeValue::Text(r0)) => l0 == r0,
(Self::Float32(l0), AttributeValue::Float32(r0)) => l0 == r0,
(Self::Float64(l0), AttributeValue::Float64(r0)) => l0 == r0,
(Self::Int32(l0), AttributeValue::Int32(r0)) => l0 == r0,
(Self::Int64(l0), AttributeValue::Int64(r0)) => l0 == r0,
(Self::Uint32(l0), AttributeValue::Uint32(r0)) => l0 == r0,
(Self::Uint64(l0), AttributeValue::Uint64(r0)) => l0 == r0,
(Self::Bool(l0), AttributeValue::Bool(r0)) => l0 == r0,
(Self::Vec3Float(l0, l1, l2), AttributeValue::Vec3Float(r0, r1, r2)) => {
l0 == r0 && l1 == r1 && l2 == r2
}
(Self::Vec3Int(l0, l1, l2), AttributeValue::Vec3Int(r0, r1, r2)) => {
l0 == r0 && l1 == r1 && l2 == r2
}
(Self::Vec3Uint(l0, l1, l2), AttributeValue::Vec3Uint(r0, r1, r2)) => {
l0 == r0 && l1 == r1 && l2 == r2
}
(Self::Vec4Float(l0, l1, l2, l3), AttributeValue::Vec4Float(r0, r1, r2, r3)) => {
l0 == r0 && l1 == r1 && l2 == r2 && l3 == r3
}
(Self::Vec4Int(l0, l1, l2, l3), AttributeValue::Vec4Int(r0, r1, r2, r3)) => {
l0 == r0 && l1 == r1 && l2 == r2 && l3 == r3
}
(Self::Vec4Uint(l0, l1, l2, l3), AttributeValue::Vec4Uint(r0, r1, r2, r3)) => {
l0 == r0 && l1 == r1 && l2 == r2 && l3 == r3
}
(Self::Bytes(l0), AttributeValue::Bytes(r0)) => l0 == r0,
(_, _) => false,
}
}
}
impl<'a> From<AttributeValue<'a>> for OwnedAttributeValue {
fn from(attr: AttributeValue<'a>) -> Self {
match attr {
AttributeValue::Text(t) => OwnedAttributeValue::Text(t.to_owned()),
AttributeValue::Float32(f) => OwnedAttributeValue::Float32(f),
AttributeValue::Float64(f) => OwnedAttributeValue::Float64(f),
AttributeValue::Int32(i) => OwnedAttributeValue::Int32(i),
AttributeValue::Int64(i) => OwnedAttributeValue::Int64(i),
AttributeValue::Uint32(u) => OwnedAttributeValue::Uint32(u),
AttributeValue::Uint64(u) => OwnedAttributeValue::Uint64(u),
AttributeValue::Bool(b) => OwnedAttributeValue::Bool(b),
AttributeValue::Vec3Float(f1, f2, f3) => OwnedAttributeValue::Vec3Float(f1, f2, f3),
AttributeValue::Vec3Int(f1, f2, f3) => OwnedAttributeValue::Vec3Int(f1, f2, f3),
AttributeValue::Vec3Uint(f1, f2, f3) => OwnedAttributeValue::Vec3Uint(f1, f2, f3),
AttributeValue::Vec4Float(f1, f2, f3, f4) => {
OwnedAttributeValue::Vec4Float(f1, f2, f3, f4)
}
AttributeValue::Vec4Int(f1, f2, f3, f4) => OwnedAttributeValue::Vec4Int(f1, f2, f3, f4),
AttributeValue::Vec4Uint(f1, f2, f3, f4) => {
OwnedAttributeValue::Vec4Uint(f1, f2, f3, f4)
}
AttributeValue::Bytes(b) => OwnedAttributeValue::Bytes(b.to_owned()),
AttributeValue::Any(a) => OwnedAttributeValue::Any(OwnedArbitraryAttributeValue {
value: clone_box(a.value),
cmp: a.cmp,
}),
}
}
}
// todo
#[allow(missing_docs)]
impl OwnedAttributeValue {
pub fn as_text(&self) -> Option<&str> {
match self {
OwnedAttributeValue::Text(s) => Some(s),
_ => None,
}
}
pub fn as_float32(&self) -> Option<f32> {
match self {
OwnedAttributeValue::Float32(f) => Some(*f),
_ => None,
}
}
pub fn as_float64(&self) -> Option<f64> {
match self {
OwnedAttributeValue::Float64(f) => Some(*f),
_ => None,
}
}
pub fn as_int32(&self) -> Option<i32> {
match self {
OwnedAttributeValue::Int32(i) => Some(*i),
_ => None,
}
}
pub fn as_int64(&self) -> Option<i64> {
match self {
OwnedAttributeValue::Int64(i) => Some(*i),
_ => None,
}
}
pub fn as_uint32(&self) -> Option<u32> {
match self {
OwnedAttributeValue::Uint32(i) => Some(*i),
_ => None,
}
}
pub fn as_uint64(&self) -> Option<u64> {
match self {
OwnedAttributeValue::Uint64(i) => Some(*i),
_ => None,
}
}
pub fn as_bool(&self) -> Option<bool> {
match self {
OwnedAttributeValue::Bool(b) => Some(*b),
_ => None,
}
}
pub fn as_vec3_float(&self) -> Option<(f32, f32, f32)> {
match self {
OwnedAttributeValue::Vec3Float(x, y, z) => Some((*x, *y, *z)),
_ => None,
}
}
pub fn as_vec3_int(&self) -> Option<(i32, i32, i32)> {
match self {
OwnedAttributeValue::Vec3Int(x, y, z) => Some((*x, *y, *z)),
_ => None,
}
}
pub fn as_vec3_uint(&self) -> Option<(u32, u32, u32)> {
match self {
OwnedAttributeValue::Vec3Uint(x, y, z) => Some((*x, *y, *z)),
_ => None,
}
}
pub fn as_vec4_float(&self) -> Option<(f32, f32, f32, f32)> {
match self {
OwnedAttributeValue::Vec4Float(x, y, z, w) => Some((*x, *y, *z, *w)),
_ => None,
}
}
pub fn as_vec4_int(&self) -> Option<(i32, i32, i32, i32)> {
match self {
OwnedAttributeValue::Vec4Int(x, y, z, w) => Some((*x, *y, *z, *w)),
_ => None,
}
}
pub fn as_vec4_uint(&self) -> Option<(u32, u32, u32, u32)> {
match self {
OwnedAttributeValue::Vec4Uint(x, y, z, w) => Some((*x, *y, *z, *w)),
_ => None,
}
}
pub fn as_bytes(&self) -> Option<&[u8]> {
match self {
OwnedAttributeValue::Bytes(b) => Some(b),
_ => None,
}
}
}

163
packages/core/src/arena.rs Normal file
View file

@ -0,0 +1,163 @@
use crate::{nodes::RenderReturn, nodes::VNode, virtual_dom::VirtualDom, DynamicNode, ScopeId};
use bumpalo::boxed::Box as BumpBox;
/// An Element's unique identifier.
///
/// `ElementId` is a `usize` that is unique across the entire VirtualDOM - but not unique across time. If a component is
/// unmounted, then the `ElementId` will be reused for a new component.
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct ElementId(pub usize);
pub(crate) struct ElementRef {
// the pathway of the real element inside the template
pub path: ElementPath,
// The actual template
pub template: *const VNode<'static>,
}
#[derive(Clone, Copy)]
pub enum ElementPath {
Deep(&'static [u8]),
Root(usize),
}
impl ElementRef {
pub(crate) fn null() -> Self {
Self {
template: std::ptr::null_mut(),
path: ElementPath::Root(0),
}
}
}
impl VirtualDom {
pub(crate) fn next_element(&mut self, template: &VNode, path: &'static [u8]) -> ElementId {
self.next(template, ElementPath::Deep(path))
}
pub(crate) fn next_root(&mut self, template: &VNode, path: usize) -> ElementId {
self.next(template, ElementPath::Root(path))
}
fn next(&mut self, template: &VNode, path: ElementPath) -> ElementId {
let entry = self.elements.vacant_entry();
let id = entry.key();
entry.insert(ElementRef {
template: template as *const _ as *mut _,
path,
});
ElementId(id)
}
pub(crate) fn reclaim(&mut self, el: ElementId) {
self.try_reclaim(el)
.unwrap_or_else(|| panic!("cannot reclaim {:?}", el));
}
pub(crate) fn try_reclaim(&mut self, el: ElementId) -> Option<ElementRef> {
if el.0 == 0 {
panic!(
"Cannot reclaim the root element - {:#?}",
std::backtrace::Backtrace::force_capture()
);
}
self.elements.try_remove(el.0)
}
pub(crate) fn update_template(&mut self, el: ElementId, node: &VNode) {
let node: *const VNode = node as *const _;
self.elements[el.0].template = unsafe { std::mem::transmute(node) };
}
// Drop a scope and all its children
pub(crate) fn drop_scope(&mut self, id: ScopeId) {
if let Some(root) = self.scopes[id.0].as_ref().try_root_node() {
if let RenderReturn::Sync(Ok(node)) = unsafe { root.extend_lifetime_ref() } {
self.drop_scope_inner(node)
}
}
self.scopes[id.0].props.take();
let scope = &mut self.scopes[id.0];
// Drop all the hooks once the children are dropped
// this means we'll drop hooks bottom-up
for hook in scope.hook_list.get_mut().drain(..) {
drop(unsafe { BumpBox::from_raw(hook) });
}
}
fn drop_scope_inner(&mut self, node: &VNode) {
node.clear_listeners();
node.dynamic_nodes.iter().for_each(|node| match node {
DynamicNode::Component(c) => self.drop_scope(c.scope.get().unwrap()),
DynamicNode::Fragment(nodes) => {
nodes.iter().for_each(|node| self.drop_scope_inner(node))
}
DynamicNode::Placeholder(t) => {
self.try_reclaim(t.get());
}
DynamicNode::Text(t) => {
self.try_reclaim(t.id.get());
}
});
for root in node.root_ids {
let id = root.get();
if id.0 != 0 {
self.try_reclaim(id);
}
}
}
/// Descend through the tree, removing any borrowed props and listeners
pub(crate) fn ensure_drop_safety(&self, scope: ScopeId) {
let node = unsafe { self.scopes[scope.0].previous_frame().try_load_node() };
// And now we want to make sure the previous frame has dropped anything that borrows self
if let Some(RenderReturn::Sync(Ok(node))) = node {
self.ensure_drop_safety_inner(node);
}
}
fn ensure_drop_safety_inner(&self, node: &VNode) {
node.clear_listeners();
node.dynamic_nodes.iter().for_each(|child| match child {
// Only descend if the props are borrowed
DynamicNode::Component(c) if !c.static_props => {
self.ensure_drop_safety(c.scope.get().unwrap());
c.props.set(None);
}
DynamicNode::Fragment(f) => f
.iter()
.for_each(|node| self.ensure_drop_safety_inner(node)),
_ => {}
});
}
}
impl ElementPath {
pub(crate) fn is_ascendant(&self, big: &&[u8]) -> bool {
match *self {
ElementPath::Deep(small) => small.len() <= big.len() && small == &big[..small.len()],
ElementPath::Root(r) => big.len() == 1 && big[0] == r as u8,
}
}
}
impl PartialEq<&[u8]> for ElementPath {
fn eq(&self, other: &&[u8]) -> bool {
match *self {
ElementPath::Deep(deep) => deep.eq(*other),
ElementPath::Root(r) => other.len() == 1 && other[0] == r as u8,
}
}
}

View file

@ -0,0 +1,29 @@
use crate::nodes::RenderReturn;
use bumpalo::Bump;
use std::cell::Cell;
pub(crate) struct BumpFrame {
pub bump: Bump,
pub node: Cell<*const RenderReturn<'static>>,
}
impl BumpFrame {
pub(crate) fn new(capacity: usize) -> Self {
let bump = Bump::with_capacity(capacity);
Self {
bump,
node: Cell::new(std::ptr::null()),
}
}
/// Creates a new lifetime out of thin air
pub(crate) unsafe fn try_load_node<'b>(&self) -> Option<&'b RenderReturn<'b>> {
let node = self.node.get();
if node.is_null() {
return None;
}
unsafe { std::mem::transmute(&*node) }
}
}

401
packages/core/src/create.rs Normal file
View file

@ -0,0 +1,401 @@
use std::cell::Cell;
use std::rc::Rc;
use crate::innerlude::{VComponent, VText};
use crate::mutations::Mutation;
use crate::mutations::Mutation::*;
use crate::nodes::VNode;
use crate::nodes::{DynamicNode, TemplateNode};
use crate::virtual_dom::VirtualDom;
use crate::{AttributeValue, ElementId, RenderReturn, ScopeId, SuspenseContext};
impl<'b> VirtualDom {
/// Create a new template [`VNode`] and write it to the [`Mutations`] buffer.
///
/// This method pushes the ScopeID to the internal scopestack and returns the number of nodes created.
pub(crate) fn create_scope(&mut self, scope: ScopeId, template: &'b VNode<'b>) -> usize {
self.scope_stack.push(scope);
let out = self.create(template);
self.scope_stack.pop();
out
}
/// Create this template and write its mutations
pub(crate) fn create(&mut self, template: &'b VNode<'b>) -> usize {
// The best renderers will have templates prehydrated and registered
// Just in case, let's create the template using instructions anyways
if !self.templates.contains_key(&template.template.name) {
self.register_template(template);
}
// Walk the roots, creating nodes and assigning IDs
// todo: adjust dynamic nodes to be in the order of roots and then leaves (ie BFS)
let mut dynamic_attrs = template.template.attr_paths.iter().enumerate().peekable();
let mut dynamic_nodes = template.template.node_paths.iter().enumerate().peekable();
let cur_scope = self.scope_stack.last().copied().unwrap();
let mut on_stack = 0;
for (root_idx, root) in template.template.roots.iter().enumerate() {
// We might need to generate an ID for the root node
on_stack += match root {
TemplateNode::DynamicText { id } | TemplateNode::Dynamic { id } => {
match &template.dynamic_nodes[*id] {
// a dynamic text node doesn't replace a template node, instead we create it on the fly
DynamicNode::Text(VText { id: slot, value }) => {
let id = self.next_element(template, template.template.node_paths[*id]);
slot.set(id);
// Safety: we promise not to re-alias this text later on after committing it to the mutation
let unbounded_text = unsafe { std::mem::transmute(*value) };
self.mutations.push(CreateTextNode {
value: unbounded_text,
id,
});
1
}
DynamicNode::Placeholder(slot) => {
let id = self.next_element(template, template.template.node_paths[*id]);
slot.set(id);
self.mutations.push(CreatePlaceholder { id });
1
}
DynamicNode::Fragment(_) | DynamicNode::Component { .. } => {
self.create_dynamic_node(template, &template.dynamic_nodes[*id], *id)
}
}
}
TemplateNode::Element { .. } | TemplateNode::Text { .. } => {
let this_id = self.next_root(template, root_idx);
template.root_ids[root_idx].set(this_id);
self.mutations.push(LoadTemplate {
name: template.template.name,
index: root_idx,
id: this_id,
});
// we're on top of a node that has a dynamic attribute for a descendant
// Set that attribute now before the stack gets in a weird state
while let Some((mut attr_id, path)) =
dynamic_attrs.next_if(|(_, p)| p[0] == root_idx as u8)
{
// if attribute is on a root node, then we've already created the element
// Else, it's deep in the template and we should create a new id for it
let id = match path.len() {
1 => this_id,
_ => {
let id = self
.next_element(template, template.template.attr_paths[attr_id]);
self.mutations.push(Mutation::AssignId {
path: &path[1..],
id,
});
id
}
};
loop {
let attribute = template.dynamic_attrs.get(attr_id).unwrap();
attribute.mounted_element.set(id);
// Safety: we promise not to re-alias this text later on after committing it to the mutation
let unbounded_name = unsafe { std::mem::transmute(attribute.name) };
match &attribute.value {
AttributeValue::Text(value) => {
// Safety: we promise not to re-alias this text later on after committing it to the mutation
let unbounded_value = unsafe { std::mem::transmute(*value) };
self.mutations.push(SetAttribute {
name: unbounded_name,
value: unbounded_value,
ns: attribute.namespace,
id,
})
}
AttributeValue::Bool(value) => {
self.mutations.push(SetBoolAttribute {
name: unbounded_name,
value: *value,
id,
})
}
AttributeValue::Listener(_) => {
self.mutations.push(NewEventListener {
// all listeners start with "on"
name: &unbounded_name[2..],
scope: cur_scope,
id,
})
}
AttributeValue::Float(_) => todo!(),
AttributeValue::Int(_) => todo!(),
AttributeValue::Any(_) => todo!(),
AttributeValue::None => todo!(),
}
// Only push the dynamic attributes forward if they match the current path (same element)
match dynamic_attrs.next_if(|(_, p)| *p == path) {
Some((next_attr_id, _)) => attr_id = next_attr_id,
None => break,
}
}
}
// We're on top of a node that has a dynamic child for a descendant
// Skip any node that's a root
let mut start = None;
let mut end = None;
// Collect all the dynamic nodes below this root
// We assign the start and end of the range of dynamic nodes since they area ordered in terms of tree path
//
// [0]
// [1, 1] <---|
// [1, 1, 1] <---| these are the range of dynamic nodes below root 1
// [1, 1, 2] <---|
// [2]
//
// We collect each range and then create them and replace the placeholder in the template
while let Some((idx, p)) =
dynamic_nodes.next_if(|(_, p)| p[0] == root_idx as u8)
{
if p.len() == 1 {
continue;
}
if start.is_none() {
start = Some(idx);
}
end = Some(idx);
}
//
if let (Some(start), Some(end)) = (start, end) {
for idx in start..=end {
let node = &template.dynamic_nodes[idx];
let m = self.create_dynamic_node(template, node, idx);
if m > 0 {
self.mutations.push(ReplacePlaceholder {
m,
path: &template.template.node_paths[idx][1..],
});
}
}
}
// elements create only one node :-)
1
}
};
}
on_stack
}
/// Insert a new template into the VirtualDom's template registry
fn register_template(&mut self, template: &'b VNode<'b>) {
// First, make sure we mark the template as seen, regardless if we process it
self.templates
.insert(template.template.name, template.template);
// If it's all dynamic nodes, then we don't need to register it
// Quickly run through and see if it's all just dynamic nodes
let dynamic_roots = template
.template
.roots
.iter()
.filter(|root| {
matches!(
root,
TemplateNode::Dynamic { .. } | TemplateNode::DynamicText { .. }
)
})
.count();
if dynamic_roots == template.template.roots.len() {
return;
}
self.mutations.templates.push(template.template);
}
pub(crate) fn create_dynamic_node(
&mut self,
template: &'b VNode<'b>,
node: &'b DynamicNode<'b>,
idx: usize,
) -> usize {
use DynamicNode::*;
match node {
Text(text) => self.create_dynamic_text(template, text, idx),
Fragment(frag) => self.create_fragment(frag),
Placeholder(frag) => self.create_placeholder(frag, template, idx),
Component(component) => self.create_component_node(template, component, idx),
}
}
fn create_dynamic_text(
&mut self,
template: &'b VNode<'b>,
text: &'b VText<'b>,
idx: usize,
) -> usize {
// Allocate a dynamic element reference for this text node
let new_id = self.next_element(template, template.template.node_paths[idx]);
// Make sure the text node is assigned to the correct element
text.id.set(new_id);
// Safety: we promise not to re-alias this text later on after committing it to the mutation
let value = unsafe { std::mem::transmute(text.value) };
// Add the mutation to the list
self.mutations.push(HydrateText {
id: new_id,
path: &template.template.node_paths[idx][1..],
value,
});
// Since we're hydrating an existing node, we don't create any new nodes
0
}
pub(crate) fn create_placeholder(
&mut self,
slot: &Cell<ElementId>,
template: &'b VNode<'b>,
idx: usize,
) -> usize {
// Allocate a dynamic element reference for this text node
let id = self.next_element(template, template.template.node_paths[idx]);
// Make sure the text node is assigned to the correct element
slot.set(id);
// Assign the ID to the existing node in the template
self.mutations.push(AssignId {
path: &template.template.node_paths[idx][1..],
id,
});
// Since the placeholder is already in the DOM, we don't create any new nodes
0
}
pub(crate) fn create_fragment(&mut self, nodes: &'b [VNode<'b>]) -> usize {
nodes.iter().fold(0, |acc, child| acc + self.create(child))
}
pub(super) fn create_component_node(
&mut self,
template: &'b VNode<'b>,
component: &'b VComponent<'b>,
idx: usize,
) -> usize {
let props = component
.props
.replace(None)
.expect("Props to always exist when a component is being created");
let unbounded_props = unsafe { std::mem::transmute(props) };
let scope = self.new_scope(unbounded_props, component.name);
let scope = scope.id;
component.scope.set(Some(scope));
let return_nodes = unsafe { self.run_scope(scope).extend_lifetime_ref() };
use RenderReturn::*;
match return_nodes {
Sync(Ok(t)) => self.mount_component(scope, template, t, idx),
Sync(Err(_e)) => todo!("Propogate error upwards"),
Async(_) => self.mount_component_placeholder(template, idx, scope),
}
}
fn mount_component(
&mut self,
scope: ScopeId,
parent: &'b VNode<'b>,
new: &'b VNode<'b>,
idx: usize,
) -> usize {
// Keep track of how many mutations are in the buffer in case we need to split them out if a suspense boundary
// is encountered
let mutations_to_this_point = self.mutations.edits.len();
// Create the component's root element
let created = self.create_scope(scope, new);
// If there are no suspense leaves below us, then just don't bother checking anything suspense related
if self.collected_leaves.is_empty() {
return created;
}
// If running the scope has collected some leaves and *this* component is a boundary, then handle the suspense
let boundary = match self.scopes[scope.0].has_context::<Rc<SuspenseContext>>() {
Some(boundary) => boundary,
_ => return created,
};
// Since this is a boundary, use its placeholder within the template as the placeholder for the suspense tree
let new_id = self.next_element(new, parent.template.node_paths[idx]);
// Now connect everything to the boundary
self.scopes[scope.0].placeholder.set(Some(new_id));
// This involves breaking off the mutations to this point, and then creating a new placeholder for the boundary
// Note that we break off dynamic mutations only - since static mutations aren't rendered immediately
let split_off = unsafe {
std::mem::transmute::<Vec<Mutation>, Vec<Mutation>>(
self.mutations.edits.split_off(mutations_to_this_point),
)
};
boundary.mutations.borrow_mut().edits.extend(split_off);
boundary.created_on_stack.set(created);
boundary
.waiting_on
.borrow_mut()
.extend(self.collected_leaves.drain(..));
// Now assign the placeholder in the DOM
self.mutations.push(AssignId {
id: new_id,
path: &parent.template.node_paths[idx][1..],
});
0
}
/// Take the rendered nodes from a component and handle them if they were async
///
/// IE simply assign an ID to the placeholder
fn mount_component_placeholder(
&mut self,
template: &VNode,
idx: usize,
scope: ScopeId,
) -> usize {
let new_id = self.next_element(template, template.template.node_paths[idx]);
// Set the placeholder of the scope
self.scopes[scope.0].placeholder.set(Some(new_id));
// Since the placeholder is already in the DOM, we don't create any new nodes
self.mutations.push(AssignId {
id: new_id,
path: &template.template.node_paths[idx][1..],
});
0
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,19 @@
use crate::ScopeId;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct DirtyScope {
pub height: u32,
pub id: ScopeId,
}
impl PartialOrd for DirtyScope {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.height.cmp(&other.height))
}
}
impl Ord for DirtyScope {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.height.cmp(&other.height)
}
}

View file

@ -1,190 +0,0 @@
use std::{fmt::Write, marker::PhantomData, ops::Deref};
use once_cell::sync::Lazy;
use crate::{
template::{TemplateNodeId, TextTemplateSegment},
AttributeValue, Listener, TextTemplate, VNode,
};
/// A lazily initailized vector
#[derive(Debug, Clone, Copy)]
pub struct LazyStaticVec<T: 'static>(pub &'static Lazy<Vec<T>>);
impl<T: 'static> AsRef<[T]> for LazyStaticVec<T> {
fn as_ref(&self) -> &[T] {
let v: &Vec<_> = self.0.deref();
v.as_ref()
}
}
impl<T> PartialEq for LazyStaticVec<T> {
fn eq(&self, other: &Self) -> bool {
std::ptr::eq(self.0, other.0)
}
}
/// Stores what nodes depend on specific dynamic parts of the template to allow the diffing algorithm to jump to that part of the template instead of travering it
/// This makes adding constant template nodes add no additional cost to diffing.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
pub struct DynamicNodeMapping<
Nodes,
TextOuter,
TextInner,
AttributesOuter,
AttributesInner,
Volatile,
Listeners,
> where
Nodes: AsRef<[Option<TemplateNodeId>]>,
TextOuter: AsRef<[TextInner]>,
TextInner: AsRef<[TemplateNodeId]>,
AttributesOuter: AsRef<[AttributesInner]>,
AttributesInner: AsRef<[(TemplateNodeId, usize)]>,
Volatile: AsRef<[(TemplateNodeId, usize)]>,
Listeners: AsRef<[TemplateNodeId]>,
{
/// The node that depend on each node in the dynamic template
pub nodes: Nodes,
text_inner: PhantomData<TextInner>,
/// The text nodes that depend on each text segment of the dynamic template
pub text: TextOuter,
/// The attributes along with the attribute index in the template that depend on each attribute of the dynamic template
pub attributes: AttributesOuter,
attributes_inner: PhantomData<AttributesInner>,
/// The attributes that are marked as volatile in the template
pub volatile_attributes: Volatile,
/// The listeners that depend on each listener of the dynamic template
pub nodes_with_listeners: Listeners,
}
impl<Nodes, TextOuter, TextInner, AttributesOuter, AttributesInner, Volatile, Listeners>
DynamicNodeMapping<
Nodes,
TextOuter,
TextInner,
AttributesOuter,
AttributesInner,
Volatile,
Listeners,
>
where
Nodes: AsRef<[Option<TemplateNodeId>]>,
TextOuter: AsRef<[TextInner]>,
TextInner: AsRef<[TemplateNodeId]>,
AttributesOuter: AsRef<[AttributesInner]>,
AttributesInner: AsRef<[(TemplateNodeId, usize)]>,
Volatile: AsRef<[(TemplateNodeId, usize)]>,
Listeners: AsRef<[TemplateNodeId]>,
{
/// Creates a new dynamic node mapping
pub const fn new(
nodes: Nodes,
text: TextOuter,
attributes: AttributesOuter,
volatile_attributes: Volatile,
listeners: Listeners,
) -> Self {
DynamicNodeMapping {
nodes,
text_inner: PhantomData,
text,
attributes,
attributes_inner: PhantomData,
volatile_attributes,
nodes_with_listeners: listeners,
}
}
}
/// A dynamic node mapping that is stack allocated
pub type StaticDynamicNodeMapping = DynamicNodeMapping<
&'static [Option<TemplateNodeId>],
&'static [&'static [TemplateNodeId]],
&'static [TemplateNodeId],
&'static [&'static [(TemplateNodeId, usize)]],
&'static [(TemplateNodeId, usize)],
// volatile attribute information is available at compile time, but there is no way for the macro to generate it, so we initialize it lazily instead
LazyStaticVec<(TemplateNodeId, usize)>,
&'static [TemplateNodeId],
>;
#[cfg(any(feature = "hot-reload", debug_assertions))]
/// A dynamic node mapping that is heap allocated
pub type OwnedDynamicNodeMapping = DynamicNodeMapping<
Vec<Option<TemplateNodeId>>,
Vec<Vec<TemplateNodeId>>,
Vec<TemplateNodeId>,
Vec<Vec<(TemplateNodeId, usize)>>,
Vec<(TemplateNodeId, usize)>,
Vec<(TemplateNodeId, usize)>,
Vec<TemplateNodeId>,
>;
/// The dynamic parts used to saturate a template durring runtime
pub struct TemplateContext<'b> {
/// The dynamic nodes
pub nodes: &'b [VNode<'b>],
/// The dynamic text
pub text_segments: &'b [&'b str],
/// The dynamic attributes
pub attributes: &'b [AttributeValue<'b>],
/// The dynamic attributes
// The listeners must not change during the lifetime of the context, use a dynamic node if the listeners change
pub listeners: &'b [Listener<'b>],
/// A optional key for diffing
pub key: Option<&'b str>,
}
impl<'b> TemplateContext<'b> {
/// Resolve text segments to a string
pub fn resolve_text<TextSegments, Text>(
&self,
text: &TextTemplate<TextSegments, Text>,
) -> String
where
TextSegments: AsRef<[TextTemplateSegment<Text>]>,
Text: AsRef<str>,
{
let mut result = String::with_capacity(text.min_size);
self.resolve_text_into(text, &mut result);
result
}
/// Resolve text and writes the result
pub fn resolve_text_into<TextSegments, Text>(
&self,
text: &TextTemplate<TextSegments, Text>,
result: &mut impl Write,
) where
TextSegments: AsRef<[TextTemplateSegment<Text>]>,
Text: AsRef<str>,
{
for seg in text.segments.as_ref() {
match seg {
TextTemplateSegment::Static(s) => {
let _ = result.write_str(s.as_ref());
}
TextTemplateSegment::Dynamic(idx) => {
let _ = result.write_str(self.text_segments[*idx]);
}
}
}
}
/// Resolve an attribute value
pub fn resolve_attribute(&self, idx: usize) -> &'b AttributeValue<'b> {
&self.attributes[idx]
}
/// Resolve a listener
pub fn resolve_listener(&self, idx: usize) -> &'b Listener<'b> {
&self.listeners[idx]
}
/// Resolve a node
pub fn resolve_node(&self, idx: usize) -> &'b VNode<'b> {
&self.nodes[idx]
}
}

View file

@ -0,0 +1,19 @@
use std::cell::RefCell;
use crate::ScopeId;
/// A boundary that will capture any errors from child components
#[allow(dead_code)]
pub struct ErrorBoundary {
error: RefCell<Option<(anyhow::Error, ScopeId)>>,
id: ScopeId,
}
impl ErrorBoundary {
pub fn new(id: ScopeId) -> Self {
Self {
error: RefCell::new(None),
id,
}
}
}

View file

@ -1,182 +1,164 @@
//! Internal and external event system
//!
//!
//! This is all kinda WIP, but the bones are there.
use std::{
cell::{Cell, RefCell},
rc::Rc,
};
use crate::{ElementId, ScopeId};
use std::{any::Any, cell::Cell, fmt::Debug, rc::Rc, sync::Arc};
pub(crate) struct BubbleState {
pub canceled: Cell<bool>,
/// A wrapper around some generic data that handles the event's state
///
///
/// Prevent this event from continuing to bubble up the tree to parent elements.
///
/// # Example
///
/// ```rust, ignore
/// rsx! {
/// button {
/// onclick: move |evt: Event<MouseData>| {
/// evt.cancel_bubble();
///
/// }
/// }
/// }
/// ```
pub struct Event<T: 'static + ?Sized> {
pub(crate) data: Rc<T>,
pub(crate) propogates: Rc<Cell<bool>>,
}
impl BubbleState {
pub fn new() -> Self {
impl<T> Event<T> {
/// Prevent this event from continuing to bubble up the tree to parent elements.
///
/// # Example
///
/// ```rust, ignore
/// rsx! {
/// button {
/// onclick: move |evt: Event<MouseData>| {
/// evt.cancel_bubble();
/// }
/// }
/// }
/// ```
#[deprecated = "use stop_propogation instead"]
pub fn cancel_bubble(&self) {
self.propogates.set(false);
}
/// Prevent this event from continuing to bubble up the tree to parent elements.
///
/// # Example
///
/// ```rust, ignore
/// rsx! {
/// button {
/// onclick: move |evt: Event<MouseData>| {
/// evt.cancel_bubble();
/// }
/// }
/// }
/// ```
pub fn stop_propogation(&self) {
self.propogates.set(false);
}
/// Get a reference to the inner data from this event
///
/// ```rust, ignore
/// rsx! {
/// button {
/// onclick: move |evt: Event<MouseData>| {
/// let data = evt.inner.clone();
/// cx.spawn(async move {
/// println!("{:?}", data);
/// });
/// }
/// }
/// }
/// ```
pub fn inner(&self) -> &Rc<T> {
&self.data
}
}
impl<T: ?Sized> Clone for Event<T> {
fn clone(&self) -> Self {
Self {
canceled: Cell::new(false),
propogates: self.propogates.clone(),
data: self.data.clone(),
}
}
}
/// User Events are events that are shuttled from the renderer into the [`VirtualDom`] through the scheduler channel.
impl<T> std::ops::Deref for Event<T> {
type Target = Rc<T>;
fn deref(&self) -> &Self::Target {
&self.data
}
}
impl<T: std::fmt::Debug> std::fmt::Debug for Event<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UiEvent")
.field("bubble_state", &self.propogates)
.field("data", &self.data)
.finish()
}
}
/// The callback type generated by the `rsx!` macro when an `on` field is specified for components.
///
/// These events will be passed to the appropriate Element given by `mounted_dom_id` and then bubbled up through the tree
/// where each listener is checked and fired if the event name matches.
/// This makes it possible to pass `move |evt| {}` style closures into components as property fields.
///
/// It is the expectation that the event name matches the corresponding event listener, otherwise Dioxus will panic in
/// attempting to downcast the event data.
///
/// Because Event Data is sent across threads, it must be `Send + Sync`. We are hoping to lift the `Sync` restriction but
/// `Send` will not be lifted. The entire `UserEvent` must also be `Send + Sync` due to its use in the scheduler channel.
///
/// # Example
///
/// ```rust, ignore
/// fn App(cx: Scope) -> Element {
/// render!(div {
/// onclick: move |_| println!("Clicked!")
/// rsx!{
/// MyComponent { onclick: move |evt| log::info!("clicked") }
/// }
///
/// #[derive(Props)]
/// struct MyProps<'a> {
/// onclick: EventHandler<'a, MouseEvent>,
/// }
///
/// fn MyComponent(cx: Scope<'a, MyProps<'a>>) -> Element {
/// cx.render(rsx!{
/// button {
/// onclick: move |evt| cx.props.onclick.call(evt),
/// }
/// })
/// }
///
/// let mut dom = VirtualDom::new(App);
/// let mut scheduler = dom.get_scheduler_channel();
/// scheduler.unbounded_send(SchedulerMsg::UiEvent(
/// UserEvent {
/// scope_id: None,
/// priority: EventPriority::Medium,
/// name: "click",
/// element: Some(ElementId(0)),
/// data: Arc::new(ClickEvent { .. })
/// }
/// )).unwrap();
/// ```
#[derive(Debug, Clone)]
pub struct UserEvent {
/// The originator of the event trigger if available
pub scope_id: Option<ScopeId>,
/// The priority of the event to be scheduled around ongoing work
pub priority: EventPriority,
/// The optional real node associated with the trigger
pub element: Option<ElementId>,
/// The event type IE "onclick" or "onmouseover"
pub name: &'static str,
/// If the event is bubbles up through the vdom
pub bubbles: bool,
/// The event data to be passed onto the event handler
pub data: Arc<dyn Any + Send + Sync>,
pub struct EventHandler<'bump, T = ()> {
pub(super) callback: RefCell<Option<ExternalListenerCallback<'bump, T>>>,
}
/// Priority of Event Triggers.
///
/// Internally, Dioxus will abort work that's taking too long if new, more important work arrives. Unlike React, Dioxus
/// won't be afraid to pause work or flush changes to the Real Dom. This is called "cooperative scheduling". Some Renderers
/// implement this form of scheduling internally, however Dioxus will perform its own scheduling as well.
///
/// The ultimate goal of the scheduler is to manage latency of changes, prioritizing "flashier" changes over "subtler" changes.
///
/// React has a 5-tier priority system. However, they break things into "Continuous" and "Discrete" priority. For now,
/// we keep it simple, and just use a 3-tier priority system.
///
/// - `NoPriority` = 0
/// - `LowPriority` = 1
/// - `NormalPriority` = 2
/// - `UserBlocking` = 3
/// - `HighPriority` = 4
/// - `ImmediatePriority` = 5
///
/// We still have a concept of discrete vs continuous though - discrete events won't be batched, but continuous events will.
/// This means that multiple "scroll" events will be processed in a single frame, but multiple "click" events will be
/// flushed before proceeding. Multiple discrete events is highly unlikely, though.
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)]
pub enum EventPriority {
/// Work that must be completed during the EventHandler phase.
///
/// Currently this is reserved for controlled inputs.
Immediate = 3,
/// "High Priority" work will not interrupt other high priority work, but will interrupt medium and low priority work.
///
/// This is typically reserved for things like user interaction.
///
/// React calls these "discrete" events, but with an extra category of "user-blocking" (Immediate).
High = 2,
/// "Medium priority" work is generated by page events not triggered by the user. These types of events are less important
/// than "High Priority" events and will take precedence over low priority events.
///
/// This is typically reserved for VirtualEvents that are not related to keyboard or mouse input.
///
/// React calls these "continuous" events (e.g. mouse move, mouse wheel, touch move, etc).
Medium = 1,
/// "Low Priority" work will always be preempted unless the work is significantly delayed, in which case it will be
/// advanced to the front of the work queue until completed.
///
/// The primary user of Low Priority work is the asynchronous work system (Suspense).
///
/// This is considered "idle" work or "background" work.
Low = 0,
}
/// The internal Dioxus type that carries any event data to the relevant handler.
pub struct AnyEvent {
pub(crate) bubble_state: Rc<BubbleState>,
pub(crate) data: Arc<dyn Any + Send + Sync>,
}
impl AnyEvent {
/// Convert this [`AnyEvent`] into a specific [`UiEvent`] with [`EventData`].
///
/// ```rust, ignore
/// let evt: FormEvent = evvt.downcast().unwrap();
/// ```
#[must_use]
pub fn downcast<T: Send + Sync + 'static>(self) -> Option<UiEvent<T>> {
let AnyEvent { data, bubble_state } = self;
data.downcast::<T>()
.ok()
.map(|data| UiEvent { data, bubble_state })
impl<T> Default for EventHandler<'_, T> {
fn default() -> Self {
Self {
callback: Default::default(),
}
}
}
/// A [`UiEvent`] is a type that wraps various [`EventData`].
///
/// You should prefer to use the name of the event directly, rather than
/// the [`UiEvent`]<T> generic type.
///
/// For the HTML crate, this would include `MouseEvent`, `FormEvent` etc.
pub struct UiEvent<T> {
/// The internal data of the event
/// This is wrapped in an Arc so that it can be sent across threads
pub data: Arc<T>,
type ExternalListenerCallback<'bump, T> = bumpalo::boxed::Box<'bump, dyn FnMut(T) + 'bump>;
#[allow(unused)]
bubble_state: Rc<BubbleState>,
}
impl<T> EventHandler<'_, T> {
/// Call this event handler with the appropriate event type
///
/// This borrows the event using a RefCell. Recursively calling a listener will cause a panic.
pub fn call(&self, event: T) {
if let Some(callback) = self.callback.borrow_mut().as_mut() {
callback(event);
}
}
impl<T: Debug> std::fmt::Debug for UiEvent<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UiEvent").field("data", &self.data).finish()
}
}
impl<T> std::ops::Deref for UiEvent<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.data.as_ref()
}
}
impl<T> UiEvent<T> {
/// Prevent this event from bubbling up the tree.
pub fn cancel_bubble(&self) {
self.bubble_state.canceled.set(true);
/// Forcibly drop the internal handler callback, releasing memory
///
/// This will force any future calls to "call" to not doing anything
pub fn release(&self) {
self.callback.replace(None);
}
}

View file

@ -0,0 +1,103 @@
use crate::innerlude::*;
/// Create inline fragments using Component syntax.
///
/// ## Details
///
/// Fragments capture a series of children without rendering extra nodes.
///
/// Creating fragments explicitly with the Fragment component is particularly useful when rendering lists or tables and
/// a key is needed to identify each item.
///
/// ## Example
///
/// ```rust, ignore
/// rsx!{
/// Fragment { key: "abc" }
/// }
/// ```
///
/// ## Usage
///
/// Fragments are incredibly useful when necessary, but *do* add cost in the diffing phase.
/// Try to avoid highly nested fragments if you can. Unlike React, there is no protection against infinitely nested fragments.
///
/// This function defines a dedicated `Fragment` component that can be used to create inline fragments in the RSX macro.
///
/// You want to use this free-function when your fragment needs a key and simply returning multiple nodes from rsx! won't cut it.
#[allow(non_upper_case_globals, non_snake_case)]
pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element {
let children = cx.props.0.as_ref().map_err(|e| anyhow::anyhow!("{}", e))?;
Ok(VNode {
key: children.key,
parent: children.parent,
template: children.template,
root_ids: children.root_ids,
dynamic_nodes: children.dynamic_nodes,
dynamic_attrs: children.dynamic_attrs,
})
}
pub struct FragmentProps<'a>(Element<'a>);
pub struct FragmentBuilder<'a, const BUILT: bool>(Element<'a>);
impl<'a> FragmentBuilder<'a, false> {
pub fn children(self, children: Element<'a>) -> FragmentBuilder<'a, true> {
FragmentBuilder(children)
}
}
impl<'a, const A: bool> FragmentBuilder<'a, A> {
pub fn build(self) -> FragmentProps<'a> {
FragmentProps(self.0)
}
}
/// Access the children elements passed into the component
///
/// This enables patterns where a component is passed children from its parent.
///
/// ## Details
///
/// Unlike React, Dioxus allows *only* lists of children to be passed from parent to child - not arbitrary functions
/// or classes. If you want to generate nodes instead of accepting them as a list, consider declaring a closure
/// on the props that takes Context.
///
/// If a parent passes children into a component, the child will always re-render when the parent re-renders. In other
/// words, a component cannot be automatically memoized if it borrows nodes from its parent, even if the component's
/// props are valid for the static lifetime.
///
/// ## Example
///
/// ```rust, ignore
/// fn App(cx: Scope) -> Element {
/// cx.render(rsx!{
/// CustomCard {
/// h1 {}
/// p {}
/// }
/// })
/// }
///
/// #[derive(PartialEq, Props)]
/// struct CardProps {
/// children: Element
/// }
///
/// fn CustomCard(cx: Scope<CardProps>) -> Element {
/// cx.render(rsx!{
/// div {
/// h1 {"Title card"}
/// {cx.props.children}
/// }
/// })
/// }
/// ```
impl<'a> Properties for FragmentProps<'a> {
type Builder = FragmentBuilder<'a, false>;
const IS_STATIC: bool = false;
fn builder() -> Self::Builder {
FragmentBuilder(VNode::empty())
}
unsafe fn memoize(&self, _other: &Self) -> bool {
false
}
}

View file

@ -3,7 +3,7 @@
//! This module provides support for a type called `LazyNodes` which is a micro-heap located on the stack to make calls
//! to `rsx!` more efficient.
//!
//! To support returning rsx! from branches in match statements, we need to use dynamic dispatch on [`NodeFactory`] closures.
//! To support returning rsx! from branches in match statements, we need to use dynamic dispatch on [`ScopeState`] closures.
//!
//! This can be done either through boxing directly, or by using dynamic-sized-types and a custom allocator. In our case,
//! we build a tiny alloactor in the stack and allocate the closure into that.
@ -11,13 +11,13 @@
//! The logic for this was borrowed from <https://docs.rs/stack_dst/0.6.1/stack_dst/>. Unfortunately, this crate does not
//! support non-static closures, so we've implemented the core logic of `ValueA` in this module.
use crate::innerlude::{NodeFactory, VNode};
use crate::{innerlude::VNode, ScopeState};
use std::mem;
/// A concrete type provider for closures that build [`VNode`] structures.
///
/// This struct wraps lazy structs that build [`VNode`] trees Normally, we cannot perform a blanket implementation over
/// closures, but if we wrap the closure in a concrete type, we can maintain separate implementations of [`IntoVNode`].
/// closures, but if we wrap the closure in a concrete type, we can use it for different branches in matching.
///
///
/// ```rust, ignore
@ -31,7 +31,7 @@ type StackHeapSize = [usize; 16];
enum StackNodeStorage<'a, 'b> {
Stack(LazyStack),
Heap(Box<dyn FnMut(Option<NodeFactory<'a>>) -> Option<VNode<'a>> + 'b>),
Heap(Box<dyn FnMut(Option<&'a ScopeState>) -> Option<VNode<'a>> + 'b>),
}
impl<'a, 'b> LazyNodes<'a, 'b> {
@ -40,11 +40,11 @@ impl<'a, 'b> LazyNodes<'a, 'b> {
/// If the closure cannot fit into the stack allocation (16 bytes), then it
/// is placed on the heap. Most closures will fit into the stack, and is
/// the most optimal way to use the creation function.
pub fn new(val: impl FnOnce(NodeFactory<'a>) -> VNode<'a> + 'b) -> Self {
pub fn new(val: impl FnOnce(&'a ScopeState) -> VNode<'a> + 'b) -> Self {
// there's no way to call FnOnce without a box, so we need to store it in a slot and use static dispatch
let mut slot = Some(val);
let val = move |fac: Option<NodeFactory<'a>>| {
let val = move |fac: Option<&'a ScopeState>| {
fac.map(
slot.take()
.expect("LazyNodes closure to be called only once"),
@ -65,13 +65,13 @@ impl<'a, 'b> LazyNodes<'a, 'b> {
/// Create a new [`LazyNodes`] closure, but force it onto the heap.
pub fn new_boxed<F>(inner: F) -> Self
where
F: FnOnce(NodeFactory<'a>) -> VNode<'a> + 'b,
F: FnOnce(&'a ScopeState) -> VNode<'a> + 'b,
{
// there's no way to call FnOnce without a box, so we need to store it in a slot and use static dispatch
let mut slot = Some(inner);
Self {
inner: StackNodeStorage::Heap(Box::new(move |fac: Option<NodeFactory<'a>>| {
inner: StackNodeStorage::Heap(Box::new(move |fac: Option<&'a ScopeState>| {
fac.map(
slot.take()
.expect("LazyNodes closure to be called only once"),
@ -82,9 +82,9 @@ impl<'a, 'b> LazyNodes<'a, 'b> {
unsafe fn new_inner<F>(val: F) -> Self
where
F: FnMut(Option<NodeFactory<'a>>) -> Option<VNode<'a>> + 'b,
F: FnMut(Option<&'a ScopeState>) -> Option<VNode<'a>> + 'b,
{
let mut ptr: *const _ = &val as &dyn FnMut(Option<NodeFactory<'a>>) -> Option<VNode<'a>>;
let mut ptr: *const _ = &val as &dyn FnMut(Option<&'a ScopeState>) -> Option<VNode<'a>>;
assert_eq!(
ptr as *const u8, &val as *const _ as *const u8,
@ -160,12 +160,10 @@ impl<'a, 'b> LazyNodes<'a, 'b> {
/// ```rust, ignore
/// let f = LazyNodes::new(move |f| f.element("div", [], [], [] None));
///
/// let fac = NodeFactory::new(&cx);
///
/// let node = f.call(cac);
/// ```
#[must_use]
pub fn call(self, f: NodeFactory<'a>) -> VNode<'a> {
pub fn call(self, f: &'a ScopeState) -> VNode<'a> {
match self.inner {
StackNodeStorage::Heap(mut lazy) => {
lazy(Some(f)).expect("Closure should not be called twice")
@ -182,18 +180,18 @@ struct LazyStack {
}
impl LazyStack {
fn call<'a>(&mut self, f: NodeFactory<'a>) -> VNode<'a> {
fn call<'a>(&mut self, f: &'a ScopeState) -> VNode<'a> {
let LazyStack { buf, .. } = self;
let data = buf.as_ref();
let info_size =
mem::size_of::<*mut dyn FnMut(Option<NodeFactory<'a>>) -> Option<VNode<'a>>>()
mem::size_of::<*mut dyn FnMut(Option<&'a ScopeState>) -> Option<VNode<'a>>>()
/ mem::size_of::<usize>()
- 1;
let info_ofs = data.len() - info_size;
let g: *mut dyn FnMut(Option<NodeFactory<'a>>) -> Option<VNode<'a>> =
let g: *mut dyn FnMut(Option<&'a ScopeState>) -> Option<VNode<'a>> =
unsafe { make_fat_ptr(data[..].as_ptr() as usize, &data[info_ofs..]) };
self.dropped = true;
@ -208,14 +206,14 @@ impl Drop for LazyStack {
let LazyStack { buf, .. } = self;
let data = buf.as_ref();
let info_size = mem::size_of::<
*mut dyn FnMut(Option<NodeFactory<'_>>) -> Option<VNode<'_>>,
>() / mem::size_of::<usize>()
- 1;
let info_size =
mem::size_of::<*mut dyn FnMut(Option<&ScopeState>) -> Option<VNode<'_>>>()
/ mem::size_of::<usize>()
- 1;
let info_ofs = data.len() - info_size;
let g: *mut dyn FnMut(Option<NodeFactory<'_>>) -> Option<VNode<'_>> =
let g: *mut dyn FnMut(Option<&ScopeState>) -> Option<VNode<'_>> =
unsafe { make_fat_ptr(data[..].as_ptr() as usize, &data[info_ofs..]) };
self.dropped = true;
@ -250,73 +248,3 @@ unsafe fn make_fat_ptr<T: ?Sized>(data_ptr: usize, meta_vals: &[usize]) -> *mut
fn round_to_words(len: usize) -> usize {
(len + mem::size_of::<usize>() - 1) / mem::size_of::<usize>()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::innerlude::{Element, Scope, VirtualDom};
#[test]
fn it_works() {
fn app(cx: Scope<()>) -> Element {
cx.render(LazyNodes::new(|f| f.text(format_args!("hello world!"))))
}
let mut dom = VirtualDom::new(app);
dom.rebuild();
let g = dom.base_scope().root_node();
dbg!(g);
}
#[test]
fn it_drops() {
use std::rc::Rc;
struct AppProps {
inner: Rc<i32>,
}
fn app(cx: Scope<AppProps>) -> Element {
struct DropInner {
id: i32,
}
impl Drop for DropInner {
fn drop(&mut self) {
eprintln!("dropping inner");
}
}
let caller = {
let it = (0..10).map(|i| {
let val = cx.props.inner.clone();
LazyNodes::new(move |f| {
eprintln!("hell closure");
let inner = DropInner { id: i };
f.text(format_args!("hello world {:?}, {:?}", inner.id, val))
})
});
LazyNodes::new(|f| {
eprintln!("main closure");
f.fragment_from_iter(it)
})
};
cx.render(caller)
}
let inner = Rc::new(0);
let mut dom = VirtualDom::new_with_props(
app,
AppProps {
inner: inner.clone(),
},
);
dom.rebuild();
drop(dom);
assert_eq!(Rc::strong_count(&inner), 1);
}
}

View file

@ -1,37 +1,43 @@
#![allow(non_snake_case)]
#![doc = include_str!("../README.md")]
#![deny(missing_docs)]
#![warn(missing_docs)]
pub(crate) mod arbitrary_value;
pub(crate) mod diff;
pub(crate) mod dynamic_template_context;
pub(crate) mod events;
pub(crate) mod lazynodes;
pub(crate) mod mutations;
pub(crate) mod nodes;
pub(crate) mod properties;
pub(crate) mod scopes;
pub(crate) mod template;
pub(crate) mod util;
pub(crate) mod virtual_dom;
mod any_props;
mod arena;
mod bump_frame;
mod create;
mod diff;
mod dirty_scope;
mod error_boundary;
mod events;
mod fragment;
mod lazynodes;
mod mutations;
mod nodes;
mod properties;
mod scheduler;
mod scope_arena;
mod scopes;
mod virtual_dom;
pub(crate) mod innerlude {
pub use crate::arbitrary_value::*;
pub use crate::dynamic_template_context::*;
pub use crate::arena::*;
pub use crate::dirty_scope::*;
pub use crate::error_boundary::*;
pub use crate::events::*;
pub use crate::fragment::*;
pub use crate::lazynodes::*;
pub use crate::mutations::*;
pub use crate::nodes::RenderReturn;
pub use crate::nodes::*;
pub use crate::properties::*;
pub use crate::scheduler::*;
pub use crate::scopes::*;
pub use crate::template::*;
pub use crate::util::*;
pub use crate::virtual_dom::*;
/// An [`Element`] is a possibly-none [`VNode`] created by calling `render` on [`Scope`] or [`ScopeState`].
/// An [`Element`] is a possibly-errored [`VNode`] created by calling `render` on [`Scope`] or [`ScopeState`].
///
/// Any [`None`] [`Element`] will automatically be coerced into a placeholder [`VNode`] with the [`VNode::Placeholder`] variant.
pub type Element<'a> = Option<VNode<'a>>;
/// An Errored [`Element`] will propagate the error to the nearest error boundary.
pub type Element<'a> = Result<VNode<'a>, anyhow::Error>;
/// A [`Component`] is a function that takes a [`Scope`] and returns an [`Element`].
///
@ -61,44 +67,23 @@ pub(crate) mod innerlude {
/// )
/// ```
pub type Component<P = ()> = fn(Scope<P>) -> Element;
/// A list of attributes
pub type Attributes<'a> = Option<&'a [Attribute<'a>]>;
}
pub use crate::innerlude::{
AnyEvent, ArbitraryAttributeValue, Attribute, AttributeDiscription, AttributeValue,
CodeLocation, Component, DioxusElement, DomEdit, DynamicNodeMapping, Element, ElementId,
ElementIdIterator, EventHandler, EventPriority, IntoAttributeValue, IntoVNode, LazyNodes,
Listener, Mutations, NodeFactory, OwnedAttributeValue, PathSeg, Properties, RendererTemplateId,
SchedulerMsg, Scope, ScopeId, ScopeState, StaticCodeLocation, StaticDynamicNodeMapping,
StaticPathSeg, StaticTemplateNode, StaticTemplateNodes, StaticTraverse, TaskId, Template,
TemplateAttribute, TemplateAttributeValue, TemplateContext, TemplateElement, TemplateId,
TemplateNode, TemplateNodeId, TemplateNodeType, TemplateValue, TextTemplate,
TextTemplateSegment, UiEvent, UpdateOp, UserEvent, VComponent, VElement, VFragment, VNode,
VPlaceholder, VText, VirtualDom,
};
#[cfg(any(feature = "hot-reload", debug_assertions))]
pub use crate::innerlude::{
OwnedCodeLocation, OwnedDynamicNodeMapping, OwnedPathSeg, OwnedTemplateNode,
OwnedTemplateNodes, OwnedTraverse, SetTemplateMsg,
fc_to_builder, Attribute, AttributeValue, Component, DynamicNode, Element, ElementId, Event,
Fragment, IntoDynNode, LazyNodes, Mutation, Mutations, Properties, RenderReturn, Scope,
ScopeId, ScopeState, Scoped, SuspenseContext, TaskId, Template, TemplateAttribute,
TemplateNode, VComponent, VNode, VText, VirtualDom,
};
/// The purpose of this module is to alleviate imports of many common types
///
/// This includes types like [`Scope`], [`Element`], and [`Component`].
pub mod prelude {
pub use crate::get_line_num;
#[cfg(any(feature = "hot-reload", debug_assertions))]
pub use crate::innerlude::OwnedTemplate;
pub use crate::innerlude::{
fc_to_builder, AttributeDiscription, AttributeValue, Attributes, CodeLocation, Component,
DioxusElement, Element, EventHandler, Fragment, IntoAttributeValue, LazyNodes,
LazyStaticVec, NodeFactory, Properties, Scope, ScopeId, ScopeState, StaticAttributeValue,
StaticCodeLocation, StaticDynamicNodeMapping, StaticPathSeg, StaticTemplate,
StaticTemplateNodes, StaticTraverse, Template, TemplateAttribute, TemplateAttributeValue,
TemplateContext, TemplateElement, TemplateId, TemplateNode, TemplateNodeId,
TemplateNodeType, TextTemplate, TextTemplateSegment, UpdateOp, VNode, VirtualDom,
fc_to_builder, Element, Event, EventHandler, Fragment, LazyNodes, Properties, Scope,
ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, VNode,
VirtualDom,
};
}
@ -106,38 +91,4 @@ pub mod exports {
//! Important dependencies that are used by the rest of the library
//! Feel free to just add the dependencies in your own Crates.toml
pub use bumpalo;
pub use futures_channel;
pub use once_cell;
}
/// Functions that wrap unsafe functionality to prevent us from misusing it at the callsite
pub(crate) mod unsafe_utils {
use crate::VNode;
pub(crate) unsafe fn extend_vnode<'a, 'b>(node: &'a VNode<'a>) -> &'b VNode<'b> {
std::mem::transmute(node)
}
}
#[macro_export]
/// A helper macro for using hooks in async environements.
///
/// # Usage
///
///
/// ```ignore
/// let (data) = use_ref(&cx, || {});
///
/// let handle_thing = move |_| {
/// to_owned![data]
/// cx.spawn(async move {
/// // do stuff
/// });
/// }
/// ```
macro_rules! to_owned {
($($es:ident),+$(,)?) => {$(
#[allow(unused_mut)]
let mut $es = $es.to_owned();
)*}
}

View file

@ -1,407 +1,260 @@
//! Instructions returned by the VirtualDOM on how to modify the Real DOM.
//!
//! This module contains an internal API to generate these instructions.
//!
//! Beware that changing code in this module will break compatibility with
//! interpreters for these types of DomEdits.
use fxhash::FxHashSet;
use crate::innerlude::*;
use std::{any::Any, fmt::Debug};
use crate::{arena::ElementId, ScopeId, Template};
/// ## Mutations
/// A container for all the relevant steps to modify the Real DOM
///
/// This method returns "mutations" - IE the necessary changes to get the RealDOM to match the VirtualDOM. It also
/// includes a list of NodeRefs that need to be applied and effects that need to be triggered after the RealDOM has
/// applied the edits.
/// This object provides a bunch of important information for a renderer to use patch the Real Dom with the state of the
/// VirtualDom. This includes the scopes that were modified, the templates that were discovered, and a list of changes
/// in the form of a [`Mutation`].
///
/// These changes are specific to one subtree, so to patch multiple subtrees, you'd need to handle each set separately.
///
/// Templates, however, apply to all subtrees, not just target subtree.
///
/// Mutations are the only link between the RealDOM and the VirtualDOM.
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
#[derive(Debug, Default)]
#[must_use = "not handling edits can lead to visual inconsistencies in UI"]
pub struct Mutations<'a> {
/// The list of edits that need to be applied for the RealDOM to match the VirtualDOM.
pub edits: Vec<DomEdit<'a>>,
/// The ID of the subtree that these edits are targetting
pub subtree: usize,
/// The list of Scopes that were diffed, created, and removed during the Diff process.
pub dirty_scopes: FxHashSet<ScopeId>,
/// The list of nodes to connect to the RealDOM.
pub refs: Vec<NodeRefMutation<'a>>,
/// Any templates encountered while diffing the DOM.
///
/// These must be loaded into a cache before applying the edits
pub templates: Vec<Template<'a>>,
/// Any mutations required to patch the renderer to match the layout of the VirtualDom
pub edits: Vec<Mutation<'a>>,
}
impl Debug for Mutations<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Mutations")
.field("edits", &self.edits)
.field("noderefs", &self.refs)
.finish()
impl<'a> Mutations<'a> {
/// Rewrites IDs to just be "template", so you can compare the mutations
///
/// Used really only for testing
pub fn santize(mut self) -> Self {
for edit in self.edits.iter_mut() {
if let Mutation::LoadTemplate { name, .. } = edit {
*name = "template"
}
}
self
}
/// Push a new mutation into the dom_edits list
pub(crate) fn push(&mut self, mutation: Mutation<'static>) {
self.edits.push(mutation)
}
}
/// A `DomEdit` represents a serialized form of the VirtualDom's trait-based API. This allows streaming edits across the
/// network or through FFI boundaries.
#[derive(Debug, PartialEq)]
/// A `Mutation` represents a single instruction for the renderer to use to modify the UI tree to match the state
/// of the Dioxus VirtualDom.
///
/// These edits can be serialized and sent over the network or through any interface
#[cfg_attr(
feature = "serialize",
derive(serde::Serialize, serde::Deserialize),
serde(tag = "type")
)]
pub enum DomEdit<'bump> {
/// Pop the topmost node from our stack and append them to the node
/// at the top of the stack.
#[derive(Debug, PartialEq, Eq)]
pub enum Mutation<'a> {
/// Add these m children to the target element
AppendChildren {
/// The parent to append nodes to.
root: Option<u64>,
/// The ID of the element being mounted to
id: ElementId,
/// The ids of the children to append.
children: Vec<u64>,
/// The number of nodes on the stack
m: usize,
},
/// Replace a given (single) node with a handful of nodes currently on the stack.
ReplaceWith {
/// The ID of the node to be replaced.
root: Option<u64>,
/// Assign the element at the given path the target ElementId.
///
/// The path is in the form of a list of indices based on children. Templates cannot have more than 255 children per
/// element, hence the use of a single byte.
///
///
AssignId {
/// The path of the child of the topmost node on the stack
///
/// A path of `[]` represents the topmost node. A path of `[0]` represents the first child.
/// `[0,1,2]` represents 1st child's 2nd child's 3rd child.
path: &'static [u8],
/// The ids of the nodes to replace the root with.
nodes: Vec<u64>,
/// The ID we're assigning to this element/placeholder.
///
/// This will be used later to modify the element or replace it with another element.
id: ElementId,
},
/// Create an placeholder int he DOM that we will use later.
///
/// Dioxus currently requires the use of placeholders to maintain a re-entrance point for things like list diffing
CreatePlaceholder {
/// The ID we're assigning to this element/placeholder.
///
/// This will be used later to modify the element or replace it with another element.
id: ElementId,
},
/// Create a node specifically for text with the given value
CreateTextNode {
/// The text content of this text node
value: &'a str,
/// The ID we're assigning to this specific text nodes
///
/// This will be used later to modify the element or replace it with another element.
id: ElementId,
},
/// Hydrate an existing text node at the given path with the given text.
///
/// Assign this text node the given ID since we will likely need to modify this text at a later point
HydrateText {
/// The path of the child of the topmost node on the stack
///
/// A path of `[]` represents the topmost node. A path of `[0]` represents the first child.
/// `[0,1,2]` represents 1st child's 2nd child's 3rd child.
path: &'static [u8],
/// The value of the textnode that we want to set the placeholder with
value: &'a str,
/// The ID we're assigning to this specific text nodes
///
/// This will be used later to modify the element or replace it with another element.
id: ElementId,
},
/// Load and clone an existing node from a template saved under that specific name
///
/// Dioxus guarantees that the renderer will have already been provided the template.
/// When the template is picked up in the template list, it should be saved under its "name" - here, the name
LoadTemplate {
/// The "name" of the template. When paired with `rsx!`, this is autogenerated
name: &'static str,
/// Which root are we loading from the template?
///
/// The template is stored as a list of nodes. This index represents the position of that root
index: usize,
/// The ID we're assigning to this element being loaded from the template
///
/// This will be used later to move the element around in lists
id: ElementId,
},
/// Replace the target element (given by its ID) with the topmost m nodes on the stack
ReplaceWith {
/// The ID of the node we're going to replace with
id: ElementId,
/// The number of nodes on the stack to use to replace
m: usize,
},
/// Replace an existing element in the template at the given path with the m nodes on the stack
ReplacePlaceholder {
/// The path of the child of the topmost node on the stack
///
/// A path of `[]` represents the topmost node. A path of `[0]` represents the first child.
/// `[0,1,2]` represents 1st child's 2nd child's 3rd child.
path: &'static [u8],
/// The number of nodes on the stack to use to replace
m: usize,
},
/// Insert a number of nodes after a given node.
InsertAfter {
/// The ID of the node to insert after.
root: Option<u64>,
id: ElementId,
/// The ids of the nodes to insert after the target node.
nodes: Vec<u64>,
m: usize,
},
/// Insert a number of nodes before a given node.
InsertBefore {
/// The ID of the node to insert before.
root: Option<u64>,
id: ElementId,
/// The ids of the nodes to insert before the target node.
nodes: Vec<u64>,
m: usize,
},
/// Remove a particular node from the DOM
Remove {
/// The ID of the node to remove.
root: Option<u64>,
/// Set the value of a node's attribute.
SetAttribute {
/// The name of the attribute to set.
name: &'a str,
/// The value of the attribute.
value: &'a str,
/// The ID of the node to set the attribute of.
id: ElementId,
/// The (optional) namespace of the attribute.
/// For instance, "style" is in the "style" namespace.
ns: Option<&'a str>,
},
/// Create a new purely-text node
CreateTextNode {
/// The ID the new node should have.
root: Option<u64>,
/// Set the value of a node's attribute.
SetBoolAttribute {
/// The name of the attribute to set.
name: &'a str,
/// The value of the attribute.
value: bool,
/// The ID of the node to set the attribute of.
id: ElementId,
},
/// Set the textcontent of a node.
SetText {
/// The textcontent of the node
text: &'bump str,
},
value: &'a str,
/// Create a new purely-element node
CreateElement {
/// The ID the new node should have.
root: Option<u64>,
/// The tagname of the node
tag: &'bump str,
/// The number of children nodes that will follow this message.
children: u32,
},
/// Create a new purely-comment node with a given namespace
CreateElementNs {
/// The ID the new node should have.
root: Option<u64>,
/// The namespace of the node
tag: &'bump str,
/// The namespace of the node (like `SVG`)
ns: &'static str,
/// The number of children nodes that will follow this message.
children: u32,
},
/// Create a new placeholder node.
/// In most implementations, this will either be a hidden div or a comment node.
CreatePlaceholder {
/// The ID the new node should have.
root: Option<u64>,
/// The ID of the node to set the textcontent of.
id: ElementId,
},
/// Create a new Event Listener.
NewEventListener {
/// The name of the event to listen for.
event_name: &'static str,
name: &'a str,
/// The ID of the node to attach the listener to.
scope: ScopeId,
/// The ID of the node to attach the listener to.
root: Option<u64>,
id: ElementId,
},
/// Remove an existing Event Listener.
RemoveEventListener {
/// The ID of the node to remove.
root: Option<u64>,
/// The name of the event to remove.
event: &'static str,
},
name: &'a str,
/// Set the textcontent of a node.
SetText {
/// The ID of the node to set the textcontent of.
root: Option<u64>,
/// The textcontent of the node
text: &'bump str,
},
/// Set the value of a node's attribute.
SetAttribute {
/// The ID of the node to set the attribute of.
root: Option<u64>,
/// The name of the attribute to set.
field: &'static str,
/// The value of the attribute.
value: AttributeValue<'bump>,
// value: &'bump str,
/// The (optional) namespace of the attribute.
/// For instance, "style" is in the "style" namespace.
ns: Option<&'bump str>,
},
/// Remove an attribute from a node.
RemoveAttribute {
/// The ID of the node to remove.
root: Option<u64>,
/// The name of the attribute to remove.
name: &'static str,
/// The namespace of the attribute.
ns: Option<&'bump str>,
id: ElementId,
},
/// Clones a node.
CloneNode {
/// The ID of the node to clone.
id: Option<u64>,
/// The ID of the new node.
new_id: u64,
/// Remove a particular node from the DOM
Remove {
/// The ID of the node to remove.
id: ElementId,
},
/// Clones the children of a node. (allows cloning fragments)
CloneNodeChildren {
/// The ID of the node to clone.
id: Option<u64>,
/// The ID of the new node.
new_ids: Vec<u64>,
},
/// Navigates to the last node to the first child of the current node.
FirstChild {},
/// Navigates to the last node to the last child of the current node.
NextSibling {},
/// Navigates to the last node to the parent of the current node.
ParentNode {},
/// Stores the last node with a new id.
StoreWithId {
/// The ID of the node to store.
id: u64,
},
/// Manually set the last node.
SetLastNode {
/// The ID to set the last node to.
id: u64,
/// Push the given root node onto our stack.
PushRoot {
/// The ID of the root node to push.
id: ElementId,
},
}
use rustc_hash::FxHashSet;
use DomEdit::*;
#[allow(unused)]
impl<'a> Mutations<'a> {
pub(crate) fn new() -> Self {
Self {
edits: Vec::new(),
refs: Vec::new(),
dirty_scopes: Default::default(),
}
}
pub(crate) fn replace_with(&mut self, root: Option<u64>, nodes: Vec<u64>) {
self.edits.push(ReplaceWith { nodes, root });
}
pub(crate) fn insert_after(&mut self, root: Option<u64>, nodes: Vec<u64>) {
self.edits.push(InsertAfter { nodes, root });
}
pub(crate) fn insert_before(&mut self, root: Option<u64>, nodes: Vec<u64>) {
self.edits.push(InsertBefore { nodes, root });
}
pub(crate) fn append_children(&mut self, root: Option<u64>, children: Vec<u64>) {
self.edits.push(AppendChildren { root, children });
}
// Remove Nodes from the dom
pub(crate) fn remove(&mut self, id: Option<u64>) {
self.edits.push(Remove { root: id });
}
// Create
pub(crate) fn create_text_node(&mut self, text: &'a str, id: Option<u64>) {
self.edits.push(CreateTextNode { text, root: id });
}
pub(crate) fn create_element(
&mut self,
tag: &'static str,
ns: Option<&'static str>,
id: Option<u64>,
children: u32,
) {
match ns {
Some(ns) => self.edits.push(CreateElementNs {
root: id,
ns,
tag,
children,
}),
None => self.edits.push(CreateElement {
root: id,
tag,
children,
}),
}
}
// placeholders are nodes that don't get rendered but still exist as an "anchor" in the real dom
pub(crate) fn create_placeholder(&mut self, id: Option<u64>) {
self.edits.push(CreatePlaceholder { root: id });
}
// events
pub(crate) fn new_event_listener(&mut self, listener: &Listener, scope: ScopeId) {
let Listener {
event,
mounted_node,
..
} = listener;
let element_id = Some(mounted_node.get().unwrap().into());
self.edits.push(NewEventListener {
scope,
event_name: event,
root: element_id,
});
}
pub(crate) fn remove_event_listener(&mut self, event: &'static str, root: Option<u64>) {
self.edits.push(RemoveEventListener { event, root });
}
// modify
pub(crate) fn set_text(&mut self, text: &'a str, root: Option<u64>) {
self.edits.push(SetText { text, root });
}
pub(crate) fn set_attribute(&mut self, attribute: &'a Attribute<'a>, root: Option<u64>) {
let Attribute {
value, attribute, ..
} = attribute;
self.edits.push(SetAttribute {
field: attribute.name,
value: value.clone(),
ns: attribute.namespace,
root,
});
}
pub(crate) fn remove_attribute(&mut self, attribute: &Attribute, root: Option<u64>) {
let Attribute { attribute, .. } = attribute;
self.edits.push(RemoveAttribute {
name: attribute.name,
ns: attribute.namespace,
root,
});
}
pub(crate) fn mark_dirty_scope(&mut self, scope: ScopeId) {
self.dirty_scopes.insert(scope);
}
pub(crate) fn clone_node(&mut self, id: Option<u64>, new_id: u64) {
self.edits.push(CloneNode { id, new_id });
}
pub(crate) fn clone_node_children(&mut self, id: Option<u64>, new_ids: Vec<u64>) {
self.edits.push(CloneNodeChildren { id, new_ids });
}
pub(crate) fn first_child(&mut self) {
self.edits.push(FirstChild {});
}
pub(crate) fn next_sibling(&mut self) {
self.edits.push(NextSibling {});
}
pub(crate) fn parent_node(&mut self) {
self.edits.push(ParentNode {});
}
pub(crate) fn store_with_id(&mut self, id: u64) {
self.edits.push(StoreWithId { id });
}
pub(crate) fn set_last_node(&mut self, id: u64) {
self.edits.push(SetLastNode { id });
}
}
// refs are only assigned once
pub struct NodeRefMutation<'a> {
pub element: &'a mut Option<once_cell::sync::OnceCell<Box<dyn Any>>>,
pub element_id: ElementId,
}
impl<'a> std::fmt::Debug for NodeRefMutation<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NodeRefMutation")
.field("element_id", &self.element_id)
.finish()
}
}
impl<'a> NodeRefMutation<'a> {
pub fn downcast_ref<T: 'static>(&self) -> Option<&T> {
self.element
.as_ref()
.and_then(|f| f.get())
.and_then(|f| f.downcast_ref::<T>())
}
pub fn downcast_mut<T: 'static>(&mut self) -> Option<&mut T> {
self.element
.as_mut()
.and_then(|f| f.get_mut())
.and_then(|f| f.downcast_mut::<T>())
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,100 +1,5 @@
use crate::innerlude::*;
pub struct FragmentProps<'a>(Element<'a>);
pub struct FragmentBuilder<'a, const BUILT: bool>(Element<'a>);
impl<'a> FragmentBuilder<'a, false> {
pub fn children(self, children: Element<'a>) -> FragmentBuilder<'a, true> {
FragmentBuilder(children)
}
}
impl<'a, const A: bool> FragmentBuilder<'a, A> {
pub fn build(self) -> FragmentProps<'a> {
FragmentProps(self.0)
}
}
/// Access the children elements passed into the component
///
/// This enables patterns where a component is passed children from its parent.
///
/// ## Details
///
/// Unlike React, Dioxus allows *only* lists of children to be passed from parent to child - not arbitrary functions
/// or classes. If you want to generate nodes instead of accepting them as a list, consider declaring a closure
/// on the props that takes Context.
///
/// If a parent passes children into a component, the child will always re-render when the parent re-renders. In other
/// words, a component cannot be automatically memoized if it borrows nodes from its parent, even if the component's
/// props are valid for the static lifetime.
///
/// ## Example
///
/// ```rust, ignore
/// fn App(cx: Scope) -> Element {
/// cx.render(rsx!{
/// CustomCard {
/// h1 {}
/// p {}
/// }
/// })
/// }
///
/// #[derive(PartialEq, Props)]
/// struct CardProps {
/// children: Element
/// }
///
/// fn CustomCard(cx: Scope<CardProps>) -> Element {
/// cx.render(rsx!{
/// div {
/// h1 {"Title card"}
/// {cx.props.children}
/// }
/// })
/// }
/// ```
impl<'a> Properties for FragmentProps<'a> {
type Builder = FragmentBuilder<'a, false>;
const IS_STATIC: bool = false;
fn builder() -> Self::Builder {
FragmentBuilder(None)
}
unsafe fn memoize(&self, _other: &Self) -> bool {
false
}
}
/// Create inline fragments using Component syntax.
///
/// ## Details
///
/// Fragments capture a series of children without rendering extra nodes.
///
/// Creating fragments explicitly with the Fragment component is particularly useful when rendering lists or tables and
/// a key is needed to identify each item.
///
/// ## Example
///
/// ```rust, ignore
/// rsx!{
/// Fragment { key: "abc" }
/// }
/// ```
///
/// ## Usage
///
/// Fragments are incredibly useful when necessary, but *do* add cost in the diffing phase.
/// Try to avoid highly nested fragments if you can. Unlike React, there is no protection against infinitely nested fragments.
///
/// This function defines a dedicated `Fragment` component that can be used to create inline fragments in the RSX macro.
///
/// You want to use this free-function when your fragment needs a key and simply returning multiple nodes from rsx! won't cut it.
#[allow(non_upper_case_globals, non_snake_case)]
pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element {
let i = cx.props.0.as_ref().map(|f| f.decouple());
cx.render(LazyNodes::new(|f| f.fragment_from_iter(i)))
}
/// Every "Props" used for a component must implement the `Properties` trait. This trait gives some hints to Dioxus
/// on how to memoize the props and some additional optimizations that can be made. We strongly encourage using the
/// derive macro to implement the `Properties` trait automatically as guarantee that your memoization strategy is safe.
@ -165,6 +70,6 @@ impl EmptyBuilder {
/// This utility function launches the builder method so rsx! and html! macros can use the typed-builder pattern
/// to initialize a component's props.
pub fn fc_to_builder<'a, T: Properties + 'a>(_: fn(Scope<'a, T>) -> Element) -> T::Builder {
pub fn fc_to_builder<'a, A, T: Properties + 'a>(_: fn(Scope<'a, T>) -> A) -> T::Builder {
T::builder()
}

View file

@ -0,0 +1,48 @@
use crate::ScopeId;
use slab::Slab;
mod suspense;
mod task;
mod wait;
mod waker;
pub use suspense::*;
pub use task::*;
pub use waker::RcWake;
/// The type of message that can be sent to the scheduler.
///
/// These messages control how the scheduler will process updates to the UI.
#[derive(Debug)]
pub(crate) enum SchedulerMsg {
/// Immediate updates from Components that mark them as dirty
Immediate(ScopeId),
/// A task has woken and needs to be progressed
TaskNotified(TaskId),
/// A task has woken and needs to be progressed
SuspenseNotified(SuspenseId),
}
use std::{cell::RefCell, rc::Rc};
pub(crate) struct Scheduler {
pub sender: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
/// Tasks created with cx.spawn
pub tasks: RefCell<Slab<Rc<LocalTask>>>,
/// Async components
pub leaves: RefCell<Slab<Rc<SuspenseLeaf>>>,
}
impl Scheduler {
pub fn new(sender: futures_channel::mpsc::UnboundedSender<SchedulerMsg>) -> Rc<Self> {
Rc::new(Scheduler {
sender,
tasks: RefCell::new(Slab::new()),
leaves: RefCell::new(Slab::new()),
})
}
}

View file

@ -0,0 +1,52 @@
use super::{waker::RcWake, SchedulerMsg};
use crate::ElementId;
use crate::{innerlude::Mutations, Element, ScopeId};
use std::future::Future;
use std::{
cell::{Cell, RefCell},
collections::HashSet,
rc::Rc,
};
/// An ID representing an ongoing suspended component
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
pub(crate) struct SuspenseId(pub usize);
/// A boundary in the VirtualDom that captures all suspended components below it
pub struct SuspenseContext {
pub(crate) id: ScopeId,
pub(crate) waiting_on: RefCell<HashSet<SuspenseId>>,
pub(crate) mutations: RefCell<Mutations<'static>>,
pub(crate) placeholder: Cell<Option<ElementId>>,
pub(crate) created_on_stack: Cell<usize>,
}
impl SuspenseContext {
/// Create a new boundary for suspense
pub fn new(id: ScopeId) -> Self {
Self {
id,
waiting_on: Default::default(),
mutations: RefCell::new(Mutations::default()),
placeholder: Cell::new(None),
created_on_stack: Cell::new(0),
}
}
}
pub(crate) struct SuspenseLeaf {
pub(crate) id: SuspenseId,
pub(crate) scope_id: ScopeId,
pub(crate) tx: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
pub(crate) notified: Cell<bool>,
pub(crate) task: *mut dyn Future<Output = Element<'static>>,
}
impl RcWake for SuspenseLeaf {
fn wake_by_ref(arc_self: &Rc<Self>) {
arc_self.notified.set(true);
_ = arc_self
.tx
.unbounded_send(SchedulerMsg::SuspenseNotified(arc_self.id));
}
}

View file

@ -0,0 +1,66 @@
use super::{waker::RcWake, Scheduler, SchedulerMsg};
use crate::ScopeId;
use std::cell::RefCell;
use std::future::Future;
use std::{pin::Pin, rc::Rc};
/// A task's unique identifier.
///
/// `TaskId` is a `usize` that is unique across the entire VirtualDOM and across time. TaskIDs will never be reused
/// once a Task has been completed.
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
pub struct TaskId(pub usize);
/// the task itself is the waker
pub(crate) struct LocalTask {
pub scope: ScopeId,
pub(super) task: RefCell<Pin<Box<dyn Future<Output = ()> + 'static>>>,
id: TaskId,
tx: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
}
impl Scheduler {
/// Start a new future on the same thread as the rest of the VirtualDom.
///
/// This future will not contribute to suspense resolving, so you should primarily use this for reacting to changes
/// and long running tasks.
///
/// Whenever the component that owns this future is dropped, the future will be dropped as well.
///
/// Spawning a future onto the root scope will cause it to be dropped when the root component is dropped - which
/// will only occur when the VirtuaalDom itself has been dropped.
pub fn spawn(&self, scope: ScopeId, task: impl Future<Output = ()> + 'static) -> TaskId {
let mut tasks = self.tasks.borrow_mut();
let entry = tasks.vacant_entry();
let task_id = TaskId(entry.key());
entry.insert(Rc::new(LocalTask {
id: task_id,
tx: self.sender.clone(),
task: RefCell::new(Box::pin(task)),
scope,
}));
self.sender
.unbounded_send(SchedulerMsg::TaskNotified(task_id))
.expect("Scheduler should exist");
task_id
}
/// Drop the future with the given TaskId
///
/// This does nto abort the task, so you'll want to wrap it in an aborthandle if that's important to you
pub fn remove(&self, id: TaskId) {
self.tasks.borrow_mut().remove(id.0);
}
}
impl RcWake for LocalTask {
fn wake_by_ref(arc_self: &Rc<Self>) {
_ = arc_self
.tx
.unbounded_send(SchedulerMsg::TaskNotified(arc_self.id));
}
}

View file

@ -0,0 +1,106 @@
use futures_util::FutureExt;
use std::{
rc::Rc,
task::{Context, Poll},
};
use crate::{
innerlude::{Mutation, Mutations, SuspenseContext},
nodes::RenderReturn,
ScopeId, TaskId, VNode, VirtualDom,
};
use super::{waker::RcWake, SuspenseId};
impl VirtualDom {
/// Handle notifications by tasks inside the scheduler
///
/// This is precise, meaning we won't poll every task, just tasks that have woken up as notified to use by the
/// queue
pub(crate) fn handle_task_wakeup(&mut self, id: TaskId) {
let mut tasks = self.scheduler.tasks.borrow_mut();
let task = &tasks[id.0];
let waker = task.waker();
let mut cx = Context::from_waker(&waker);
// If the task completes...
if task.task.borrow_mut().as_mut().poll(&mut cx).is_ready() {
// Remove it from the scope so we dont try to double drop it when the scope dropes
self.scopes[task.scope.0].spawned_tasks.remove(&id);
// Remove it from the scheduler
tasks.remove(id.0);
}
}
pub(crate) fn acquire_suspense_boundary(&self, id: ScopeId) -> Rc<SuspenseContext> {
self.scopes[id.0]
.consume_context::<Rc<SuspenseContext>>()
.unwrap()
}
pub(crate) fn handle_suspense_wakeup(&mut self, id: SuspenseId) {
let leaf = self
.scheduler
.leaves
.borrow_mut()
.get(id.0)
.unwrap()
.clone();
let scope_id = leaf.scope_id;
// todo: cache the waker
let waker = leaf.waker();
let mut cx = Context::from_waker(&waker);
// Safety: the future is always pinned to the bump arena
let mut pinned = unsafe { std::pin::Pin::new_unchecked(&mut *leaf.task) };
let as_pinned_mut = &mut pinned;
// the component finished rendering and gave us nodes
// we should attach them to that component and then render its children
// continue rendering the tree until we hit yet another suspended component
if let Poll::Ready(new_nodes) = as_pinned_mut.poll_unpin(&mut cx) {
// safety: we're not going to modify the suspense context but we don't want to make a clone of it
let fiber = self.acquire_suspense_boundary(leaf.scope_id);
let scope = &mut self.scopes[scope_id.0];
let arena = scope.current_frame();
let ret = arena.bump.alloc(RenderReturn::Sync(new_nodes));
arena.node.set(ret);
fiber.waiting_on.borrow_mut().remove(&id);
if let RenderReturn::Sync(Ok(template)) = ret {
let mutations_ref = &mut fiber.mutations.borrow_mut();
let mutations = &mut **mutations_ref;
let template: &VNode = unsafe { std::mem::transmute(template) };
let mutations: &mut Mutations = unsafe { std::mem::transmute(mutations) };
std::mem::swap(&mut self.mutations, mutations);
let place_holder_id = scope.placeholder.get().unwrap();
self.scope_stack.push(scope_id);
let created = self.create(template);
self.scope_stack.pop();
mutations.push(Mutation::ReplaceWith {
id: place_holder_id,
m: created,
});
for leaf in self.collected_leaves.drain(..) {
fiber.waiting_on.borrow_mut().insert(leaf);
}
std::mem::swap(&mut self.mutations, mutations);
if fiber.waiting_on.borrow().is_empty() {
self.finished_fibers.push(fiber.id);
}
}
}
}
}

View file

@ -0,0 +1,36 @@
use std::task::{RawWaker, RawWakerVTable, Waker};
use std::{mem, rc::Rc};
pub trait RcWake: Sized {
/// Create a waker from this self-wakening object
fn waker(self: &Rc<Self>) -> Waker {
unsafe fn rc_vtable<T: RcWake>() -> &'static RawWakerVTable {
&RawWakerVTable::new(
|data| {
let arc = mem::ManuallyDrop::new(Rc::<T>::from_raw(data.cast::<T>()));
let _rc_clone: mem::ManuallyDrop<_> = arc.clone();
RawWaker::new(data, rc_vtable::<T>())
},
|data| Rc::from_raw(data.cast::<T>()).wake(),
|data| {
let arc = mem::ManuallyDrop::new(Rc::<T>::from_raw(data.cast::<T>()));
RcWake::wake_by_ref(&arc);
},
|data| drop(Rc::<T>::from_raw(data.cast::<T>())),
)
}
unsafe {
Waker::from_raw(RawWaker::new(
Rc::into_raw(self.clone()).cast(),
rc_vtable::<Self>(),
))
}
}
fn wake_by_ref(arc_self: &Rc<Self>);
fn wake(self: Rc<Self>) {
Self::wake_by_ref(&self)
}
}

View file

@ -0,0 +1,146 @@
use crate::{
any_props::AnyProps,
bump_frame::BumpFrame,
innerlude::DirtyScope,
innerlude::{SuspenseId, SuspenseLeaf},
nodes::RenderReturn,
scheduler::RcWake,
scopes::{ScopeId, ScopeState},
virtual_dom::VirtualDom,
};
use futures_util::FutureExt;
use std::{
mem,
pin::Pin,
rc::Rc,
task::{Context, Poll},
};
impl VirtualDom {
pub(super) fn new_scope(
&mut self,
props: Box<dyn AnyProps<'static>>,
name: &'static str,
) -> &ScopeState {
let parent = self.acquire_current_scope_raw();
let entry = self.scopes.vacant_entry();
let height = unsafe { parent.map(|f| (*f).height + 1).unwrap_or(0) };
let id = ScopeId(entry.key());
entry.insert(Box::new(ScopeState {
parent,
id,
height,
props: Some(props),
name,
placeholder: Default::default(),
node_arena_1: BumpFrame::new(50),
node_arena_2: BumpFrame::new(50),
spawned_tasks: Default::default(),
render_cnt: Default::default(),
hook_arena: Default::default(),
hook_list: Default::default(),
hook_idx: Default::default(),
shared_contexts: Default::default(),
tasks: self.scheduler.clone(),
}))
}
fn acquire_current_scope_raw(&mut self) -> Option<*mut ScopeState> {
self.scope_stack
.last()
.copied()
.and_then(|id| self.scopes.get_mut(id.0).map(|f| f.as_mut() as *mut _))
}
pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> &RenderReturn {
// Cycle to the next frame and then reset it
// This breaks any latent references, invalidating every pointer referencing into it.
// Remove all the outdated listeners
self.ensure_drop_safety(scope_id);
let mut new_nodes = unsafe {
let scope = self.scopes[scope_id.0].as_mut();
scope.previous_frame_mut().bump.reset();
// Make sure to reset the hook counter so we give out hooks in the right order
scope.hook_idx.set(0);
// safety: due to how we traverse the tree, we know that the scope is not currently aliased
let props = scope.props.as_ref().unwrap().as_ref();
let props: &dyn AnyProps = mem::transmute(props);
props.render(scope).extend_lifetime()
};
// immediately resolve futures that can be resolved
if let RenderReturn::Async(task) = &mut new_nodes {
let mut leaves = self.scheduler.leaves.borrow_mut();
let entry = leaves.vacant_entry();
let suspense_id = SuspenseId(entry.key());
let leaf = Rc::new(SuspenseLeaf {
scope_id,
task: task.as_mut(),
id: suspense_id,
tx: self.scheduler.sender.clone(),
notified: Default::default(),
});
let waker = leaf.waker();
let mut cx = Context::from_waker(&waker);
// safety: the task is already pinned in the bump arena
let mut pinned = unsafe { Pin::new_unchecked(task.as_mut()) };
// Keep polling until either we get a value or the future is not ready
loop {
match pinned.poll_unpin(&mut cx) {
// If nodes are produced, then set it and we can break
Poll::Ready(nodes) => {
new_nodes = RenderReturn::Sync(nodes);
break;
}
// If no nodes are produced but the future woke up immediately, then try polling it again
// This circumvents things like yield_now, but is important is important when rendering
// components that are just a stream of immediately ready futures
_ if leaf.notified.get() => {
leaf.notified.set(false);
continue;
}
// If no nodes are produced, then we need to wait for the future to be woken up
// Insert the future into fiber leaves and break
_ => {
entry.insert(leaf);
self.collected_leaves.push(suspense_id);
break;
}
};
}
};
let scope = &self.scopes[scope_id.0];
// We write on top of the previous frame and then make it the current by pushing the generation forward
let frame = scope.previous_frame();
// set the new head of the bump frame
let alloced = &*frame.bump.alloc(new_nodes);
frame.node.set(alloced);
// And move the render generation forward by one
scope.render_cnt.set(scope.render_cnt.get() + 1);
// remove this scope from dirty scopes
self.dirty_scopes.remove(&DirtyScope {
height: scope.height,
id: scope.id,
});
// rebind the lifetime now that its stored internally
unsafe { mem::transmute(alloced) }
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,30 @@
/*
This is a WIP module
Subtrees allow the virtualdom to split up the mutation stream into smaller chunks which can be directed to different parts of the dom.
It's core to implementing multiwindow desktop support, portals, and alternative inline renderers like react-three-fiber.
The primary idea is to give each renderer a linear element tree managed by Dioxus to maximize performance and minimize memory usage.
This can't be done if two renderers need to share the same native tree.
With subtrees, we have an entirely different slab of elements
*/
use std::borrow::Cow;
use slab::Slab;
use crate::{ElementPath, ScopeId};
/// A collection of elements confined to a single scope under a chunk of the tree
///
/// All elements in this collection are guaranteed to be in the same scope and share the same numbering
///
/// This unit can be multithreaded
/// Whenever multiple subtrees are present, we can perform **parallel diffing**
pub struct Subtree {
id: usize,
namespace: Cow<'static, str>,
root: ScopeId,
elements: Slab<ElementPath>,
}

File diff suppressed because it is too large Load diff

View file

@ -1,127 +0,0 @@
use crate::innerlude::{VNode, VirtualDom};
/// An iterator that only yields "real" [`Element`]s. IE only Elements that are
/// not [`VNode::Component`] or [`VNode::Fragment`], .
pub struct ElementIdIterator<'a> {
vdom: &'a VirtualDom,
// Heuristically we should never bleed into 5 completely nested fragments/components
// Smallvec lets us stack allocate our little stack machine so the vast majority of cases are sane
stack: smallvec::SmallVec<[(u16, &'a VNode<'a>); 5]>,
}
impl<'a> ElementIdIterator<'a> {
/// Create a new iterator from the given [`VirtualDom`] and [`VNode`]
///
/// This will allow you to iterate through all the real childrne of the [`VNode`].
pub fn new(vdom: &'a VirtualDom, node: &'a VNode<'a>) -> Self {
Self {
vdom,
stack: smallvec::smallvec![(0, node)],
}
}
}
impl<'a> Iterator for ElementIdIterator<'a> {
type Item = &'a VNode<'a>;
fn next(&mut self) -> Option<&'a VNode<'a>> {
let mut should_pop = false;
let mut returned_node = None;
let mut should_push = None;
while returned_node.is_none() {
if let Some((count, node)) = self.stack.last_mut() {
match node {
// We can only exit our looping when we get "real" nodes
VNode::Element(_)
| VNode::Text(_)
| VNode::Placeholder(_)
| VNode::TemplateRef(_) => {
// We've recursed INTO an element/text
// We need to recurse *out* of it and move forward to the next
// println!("Found element! Returning it!");
should_pop = true;
returned_node = Some(&**node);
}
// If we get a fragment we push the next child
VNode::Fragment(frag) => {
let count = *count as usize;
if count >= frag.children.len() {
should_pop = true;
} else {
should_push = Some(&frag.children[count]);
}
}
// For components, we load their root and push them onto the stack
VNode::Component(sc) => {
let scope = self.vdom.get_scope(sc.scope.get().unwrap()).unwrap();
// Simply swap the current node on the stack with the root of the component
*node = scope.root_node();
}
}
} else {
// If there's no more items on the stack, we're done!
return None;
}
if should_pop {
self.stack.pop();
if let Some((id, _)) = self.stack.last_mut() {
*id += 1;
}
should_pop = false;
}
if let Some(push) = should_push {
self.stack.push((0, push));
should_push = None;
}
}
returned_node
}
}
/// This intentionally leaks once per element name to allow more flexability when hot reloding templetes
#[cfg(all(any(feature = "hot-reload", debug_assertions), feature = "serde"))]
mod leaky {
use std::sync::Mutex;
use once_cell::sync::Lazy;
use rustc_hash::FxHashSet;
static STATIC_CACHE: Lazy<Mutex<FxHashSet<&'static str>>> =
Lazy::new(|| Mutex::new(FxHashSet::default()));
pub fn deserialize_static_leaky<'de, D>(d: D) -> Result<&'static str, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::Deserialize;
let s = <&str>::deserialize(d)?;
Ok(if let Some(stat) = STATIC_CACHE.lock().unwrap().get(s) {
*stat
} else {
Box::leak(s.into())
})
}
pub fn deserialize_static_leaky_ns<'de, D>(d: D) -> Result<Option<&'static str>, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::Deserialize;
Ok(<Option<&str>>::deserialize(d)?.map(|s| {
if let Some(stat) = STATIC_CACHE.lock().unwrap().get(s) {
*stat
} else {
Box::leak(s.into())
}
}))
}
}
#[cfg(all(any(feature = "hot-reload", debug_assertions), feature = "serde"))]
pub use leaky::*;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,2 @@
struct_variant_width = 100
struct_lit_width = 80

View file

@ -1,9 +1,6 @@
# Testing of Dioxus core
NodeFactory
- [] rsx, html, NodeFactory generate the same structures
Diffing
- [x] create elements
- [x] create text
@ -19,15 +16,13 @@ Diffing
- [x] keyed diffing
- [x] keyed diffing out of order
- [x] keyed diffing with prefix/suffix
- [x] suspended nodes work
- [x] suspended nodes work
Lifecycle
- [] Components mount properly
- [] Components create new child components
- [] Replaced components unmount old components and mount new
- [] Post-render effects are called
- []
Shared Context
- [] Shared context propagates downwards
@ -37,7 +32,7 @@ Suspense
- [] use_suspense generates suspended nodes
Hooks
Hooks
- [] Drop order is maintained
- [] Shared hook state is okay
- [] use_hook works

View file

@ -0,0 +1,75 @@
//! dynamic attributes in dioxus necessitate an allocated node ID.
//!
//! This tests to ensure we clean it up
use dioxus::core::{ElementId, Mutation::*};
use dioxus::prelude::*;
#[test]
fn attrs_cycle() {
let mut dom = VirtualDom::new(|cx| {
let id = cx.generation();
match cx.generation() % 2 {
0 => cx.render(rsx! {
div {}
}),
1 => cx.render(rsx! {
div {
h1 { class: "{id}", id: "{id}" }
}
}),
_ => unreachable!(),
}
});
assert_eq!(
dom.rebuild().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
AppendChildren { m: 1, id: ElementId(0) },
]
);
dom.mark_dirty(ScopeId(0));
assert_eq!(
dom.render_immediate().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
AssignId { path: &[0,], id: ElementId(3,) },
SetAttribute { name: "class", value: "1", id: ElementId(3,), ns: None },
SetAttribute { name: "id", value: "1", id: ElementId(3,), ns: None },
ReplaceWith { id: ElementId(1,), m: 1 },
]
);
dom.mark_dirty(ScopeId(0));
assert_eq!(
dom.render_immediate().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(3) },
ReplaceWith { id: ElementId(2), m: 1 }
]
);
dom.mark_dirty(ScopeId(0));
assert_eq!(
dom.render_immediate().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(2) },
AssignId { path: &[0], id: ElementId(1) },
SetAttribute { name: "class", value: "3", id: ElementId(1), ns: None },
SetAttribute { name: "id", value: "3", id: ElementId(1), ns: None },
ReplaceWith { id: ElementId(3), m: 1 }
]
);
// we take the node taken by attributes since we reused it
dom.mark_dirty(ScopeId(0));
assert_eq!(
dom.render_immediate().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
ReplaceWith { id: ElementId(2), m: 1 }
]
);
}

View file

@ -0,0 +1,15 @@
use dioxus::core::{ElementId, Mutation::*};
use dioxus::prelude::*;
#[test]
fn bool_test() {
let mut app = VirtualDom::new(|cx| cx.render(rsx!(div { hidden: false })));
assert_eq!(
app.rebuild().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
SetBoolAttribute { name: "hidden", value: false, id: ElementId(1,) },
AppendChildren { m: 1, id: ElementId(0) },
]
)
}

View file

@ -0,0 +1,55 @@
#![allow(non_snake_case)]
use dioxus::core::{ElementId, Mutation::*};
use dioxus::prelude::*;
#[test]
fn test_borrowed_state() {
let mut dom = VirtualDom::new(Parent);
assert_eq!(
dom.rebuild().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
LoadTemplate { name: "template", index: 0, id: ElementId(3,) },
HydrateText { path: &[0,], value: "Hello w1!", id: ElementId(4,) },
ReplacePlaceholder { path: &[1,], m: 1 },
ReplacePlaceholder { path: &[0,], m: 1 },
AppendChildren { m: 1, id: ElementId(0) },
]
)
}
fn Parent(cx: Scope) -> Element {
let w1 = cx.use_hook(|| String::from("w1"));
cx.render(rsx! {
div {
Child { name: w1 }
}
})
}
#[derive(Props)]
struct ChildProps<'a> {
name: &'a str,
}
fn Child<'a>(cx: Scope<'a, ChildProps<'a>>) -> Element {
cx.render(rsx! {
div {
h1 { "it's nested" }
Child2 { name: cx.props.name }
}
})
}
#[derive(Props)]
struct Grandchild<'a> {
name: &'a str,
}
fn Child2<'a>(cx: Scope<'a, Grandchild<'a>>) -> Element {
cx.render(rsx!(div { "Hello {cx.props.name}!" }))
}

View file

@ -0,0 +1,28 @@
//! we should properly bubble up errors from components
use dioxus::prelude::*;
fn app(cx: Scope) -> Element {
let raw = match cx.generation() % 2 {
0 => "123.123",
1 => "123.123.123",
_ => unreachable!(),
};
let value = raw.parse::<f32>()?;
cx.render(rsx! {
div { "hello {value}" }
})
}
#[test]
fn bubbles_error() {
let mut dom = VirtualDom::new(app);
let _edits = dom.rebuild().santize();
dom.mark_dirty(ScopeId(0));
_ = dom.render_immediate();
}

View file

@ -0,0 +1,51 @@
use dioxus::core::{ElementId, Mutation::*};
use dioxus::prelude::*;
#[test]
fn state_shares() {
fn app(cx: Scope) -> Element {
cx.provide_context(cx.generation() as i32);
cx.render(rsx!(child_1 {}))
}
fn child_1(cx: Scope) -> Element {
cx.render(rsx!(child_2 {}))
}
fn child_2(cx: Scope) -> Element {
let value = cx.consume_context::<i32>().unwrap();
cx.render(rsx!("Value is {value}"))
}
let mut dom = VirtualDom::new(app);
assert_eq!(
dom.rebuild().santize().edits,
[
CreateTextNode { value: "Value is 0", id: ElementId(1,) },
AppendChildren { m: 1, id: ElementId(0) },
]
);
dom.mark_dirty(ScopeId(0));
_ = dom.render_immediate();
assert_eq!(dom.base_scope().consume_context::<i32>().unwrap(), 1);
dom.mark_dirty(ScopeId(0));
_ = dom.render_immediate();
assert_eq!(dom.base_scope().consume_context::<i32>().unwrap(), 2);
dom.mark_dirty(ScopeId(2));
assert_eq!(
dom.render_immediate().santize().edits,
[SetText { value: "Value is 2", id: ElementId(1,) },]
);
dom.mark_dirty(ScopeId(0));
dom.mark_dirty(ScopeId(2));
let edits = dom.render_immediate();
assert_eq!(
edits.santize().edits,
[SetText { value: "Value is 3", id: ElementId(1,) },]
);
}

View file

@ -0,0 +1,197 @@
#![allow(unused, non_upper_case_globals, non_snake_case)]
//! Prove that the dom works normally through virtualdom methods.
//!
//! This methods all use "rebuild" which completely bypasses the scheduler.
//! Hard rebuilds don't consume any events from the event queue.
use dioxus::core::Mutation::*;
use dioxus::prelude::*;
use dioxus_core::ElementId;
#[test]
fn test_original_diff() {
let mut dom = VirtualDom::new(|cx| {
cx.render(rsx! {
div {
div {
"Hello, world!"
}
}
})
});
let edits = dom.rebuild().santize();
assert_eq!(
edits.edits,
[
// add to root
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
AppendChildren { m: 1, id: ElementId(0) }
]
)
}
#[test]
fn create() {
let mut dom = VirtualDom::new(|cx| {
cx.render(rsx! {
div {
div {
"Hello, world!"
div {
div {
Fragment {
"hello"
"world"
}
}
}
}
}
})
});
let _edits = dom.rebuild().santize();
// todo: we don't test template mutations anymore since the templates are passed along
// assert_eq!(
// edits.templates,
// [
// // create template
// CreateElement { name: "div" },
// CreateElement { name: "div" },
// CreateStaticText { value: "Hello, world!" },
// CreateElement { name: "div" },
// CreateElement { name: "div" },
// CreateStaticPlaceholder {},
// AppendChildren { m: 1 },
// AppendChildren { m: 1 },
// AppendChildren { m: 2 },
// AppendChildren { m: 1 },
// SaveTemplate { name: "template", m: 1 },
// // The fragment child template
// CreateStaticText { value: "hello" },
// CreateStaticText { value: "world" },
// SaveTemplate { name: "template", m: 2 },
// ]
// );
}
#[test]
fn create_list() {
let mut dom = VirtualDom::new(|cx| {
cx.render(rsx! {
(0..3).map(|f| rsx!( div { "hello" } ))
})
});
let _edits = dom.rebuild().santize();
// note: we dont test template edits anymore
// assert_eq!(
// edits.templates,
// [
// // create template
// CreateElement { name: "div" },
// CreateStaticText { value: "hello" },
// AppendChildren { m: 1 },
// SaveTemplate { name: "template", m: 1 }
// ]
// );
}
#[test]
fn create_simple() {
let mut dom = VirtualDom::new(|cx| {
cx.render(rsx! {
div {}
div {}
div {}
div {}
})
});
let edits = dom.rebuild().santize();
// note: we dont test template edits anymore
// assert_eq!(
// edits.templates,
// [
// // create template
// CreateElement { name: "div" },
// CreateElement { name: "div" },
// CreateElement { name: "div" },
// CreateElement { name: "div" },
// // add to root
// SaveTemplate { name: "template", m: 4 }
// ]
// );
}
#[test]
fn create_components() {
let mut dom = VirtualDom::new(|cx| {
cx.render(rsx! {
Child { "abc1" }
Child { "abc2" }
Child { "abc3" }
})
});
#[derive(Props)]
struct ChildProps<'a> {
children: Element<'a>,
}
fn Child<'a>(cx: Scope<'a, ChildProps<'a>>) -> Element {
cx.render(rsx! {
h1 {}
div { &cx.props.children }
p {}
})
}
let _edits = dom.rebuild().santize();
// todo: test this
}
#[test]
fn anchors() {
let mut dom = VirtualDom::new(|cx| {
cx.render(rsx! {
if true {
rsx!( div { "hello" } )
}
if false {
rsx!( div { "goodbye" } )
}
})
});
// note that the template under "false" doesn't show up since it's not loaded
let edits = dom.rebuild().santize();
// note: we dont test template edits anymore
// assert_eq!(
// edits.templates,
// [
// // create each template
// CreateElement { name: "div" },
// CreateStaticText { value: "hello" },
// AppendChildren { m: 1 },
// SaveTemplate { m: 1, name: "template" },
// ]
// );
assert_eq!(
edits.edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
CreatePlaceholder { id: ElementId(2) },
AppendChildren { m: 2, id: ElementId(0) }
]
)
}

View file

@ -0,0 +1,32 @@
// use dioxus::core::Mutation::*;
use dioxus::prelude::*;
#[test]
fn multiroot() {
let mut dom = VirtualDom::new(|cx| {
cx.render(rsx! {
div { "Hello a" }
div { "Hello b" }
div { "Hello c" }
})
});
// note: we dont test template edits anymore
let _templates = dom.rebuild().santize().templates;
// assert_eq!(
// dom.rebuild().santize().templates,
// [
// CreateElement { name: "div" },
// CreateStaticText { value: "Hello a" },
// AppendChildren { m: 1 },
// CreateElement { name: "div" },
// CreateStaticText { value: "Hello b" },
// AppendChildren { m: 1 },
// CreateElement { name: "div" },
// CreateStaticText { value: "Hello c" },
// AppendChildren { m: 1 },
// SaveTemplate { name: "template", m: 3 }
// ]
// )
}

View file

@ -0,0 +1,104 @@
//! Do we create fragments properly across complex boundaries?
use dioxus::core::Mutation::*;
use dioxus::prelude::*;
use dioxus_core::ElementId;
#[test]
fn empty_fragment_creates_nothing() {
fn app(cx: Scope) -> Element {
cx.render(rsx!(()))
}
let mut vdom = VirtualDom::new(app);
let edits = vdom.rebuild();
assert_eq!(
edits.edits,
[
CreatePlaceholder { id: ElementId(1) },
AppendChildren { id: ElementId(0), m: 1 }
]
);
}
#[test]
fn root_fragments_work() {
let mut vdom = VirtualDom::new(|cx| {
cx.render(rsx!(
div { "hello" }
div { "goodbye" }
))
});
assert_eq!(
vdom.rebuild().edits.last().unwrap(),
&AppendChildren { id: ElementId(0), m: 2 }
);
}
#[test]
fn fragments_nested() {
let mut vdom = VirtualDom::new(|cx| {
cx.render(rsx!(
div { "hello" }
div { "goodbye" }
rsx! {
div { "hello" }
div { "goodbye" }
rsx! {
div { "hello" }
div { "goodbye" }
rsx! {
div { "hello" }
div { "goodbye" }
}
}
}
))
});
assert_eq!(
vdom.rebuild().edits.last().unwrap(),
&AppendChildren { id: ElementId(0), m: 8 }
);
}
#[test]
fn fragments_across_components() {
fn app(cx: Scope) -> Element {
cx.render(rsx! {
demo_child {}
demo_child {}
demo_child {}
demo_child {}
})
}
fn demo_child(cx: Scope) -> Element {
let world = "world";
cx.render(rsx! {
"hellO!"
world
})
}
assert_eq!(
VirtualDom::new(app).rebuild().edits.last().unwrap(),
&AppendChildren { id: ElementId(0), m: 8 }
);
}
#[test]
fn list_fragments() {
fn app(cx: Scope) -> Element {
cx.render(rsx!(
h1 {"hello"}
(0..6).map(|f| rsx!( span { "{f}" }))
))
}
assert_eq!(
VirtualDom::new(app).rebuild().edits.last().unwrap(),
&AppendChildren { id: ElementId(0), m: 7 }
);
}

View file

@ -0,0 +1,72 @@
use dioxus::core::Mutation::*;
use dioxus::prelude::*;
use dioxus_core::ElementId;
// A real-world usecase of templates at peak performance
// In react, this would be a lot of node creation.
//
// In Dioxus, we memoize the rsx! body and simplify it down to a few template loads
//
// Also note that the IDs increase linearly. This lets us drive a vec on the renderer for O(1) re-indexing
fn app(cx: Scope) -> Element {
cx.render(rsx! {
div {
(0..3).map(|i| rsx! {
div {
h1 { "hello world! "}
p { "{i}" }
}
})
}
})
}
#[test]
fn list_renders() {
let mut dom = VirtualDom::new(app);
let edits = dom.rebuild().santize();
// note: we dont test template edits anymore
// assert_eq!(
// edits.templates,
// [
// // Create the outer div
// CreateElement { name: "div" },
// // todo: since this is the only child, we should just use
// // append when modify the values (IE no need for a placeholder)
// CreateStaticPlaceholder,
// AppendChildren { m: 1 },
// SaveTemplate { name: "template", m: 1 },
// // Create the inner template div
// CreateElement { name: "div" },
// CreateElement { name: "h1" },
// CreateStaticText { value: "hello world! " },
// AppendChildren { m: 1 },
// CreateElement { name: "p" },
// CreateTextPlaceholder,
// AppendChildren { m: 1 },
// AppendChildren { m: 2 },
// SaveTemplate { name: "template", m: 1 }
// ],
// );
assert_eq!(
edits.edits,
[
// Load the outer div
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
// Load each template one-by-one, rehydrating it
LoadTemplate { name: "template", index: 0, id: ElementId(2) },
HydrateText { path: &[1, 0], value: "0", id: ElementId(3) },
LoadTemplate { name: "template", index: 0, id: ElementId(4) },
HydrateText { path: &[1, 0], value: "1", id: ElementId(5) },
LoadTemplate { name: "template", index: 0, id: ElementId(6) },
HydrateText { path: &[1, 0], value: "2", id: ElementId(7) },
// Replace the 0th childn on the div with the 3 templates on the stack
ReplacePlaceholder { m: 3, path: &[0] },
// Append the container div to the dom
AppendChildren { m: 1, id: ElementId(0) }
],
)
}

View file

@ -0,0 +1,106 @@
use dioxus::core::Mutation::*;
use dioxus::prelude::*;
use dioxus_core::ElementId;
/// Should push the text node onto the stack and modify it
#[test]
fn nested_passthru_creates() {
fn app(cx: Scope) -> Element {
cx.render(rsx! {
pass_thru {
pass_thru {
pass_thru {
div { "hi" }
}
}
}
})
}
#[inline_props]
fn pass_thru<'a>(cx: Scope<'a>, children: Element<'a>) -> Element {
cx.render(rsx!(children))
}
let mut dom = VirtualDom::new(app);
let edits = dom.rebuild().santize();
assert_eq!(
edits.edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
AppendChildren { m: 1, id: ElementId(0) },
]
)
}
/// Should load all the templates and append them
///
/// Take note on how we don't spit out the template for child_comp since it's entirely dynamic
#[test]
fn nested_passthru_creates_add() {
fn app(cx: Scope) -> Element {
cx.render(rsx! {
child_comp {
"1"
child_comp {
"2"
child_comp {
"3"
div {
"hi"
}
}
}
}
})
}
#[inline_props]
fn child_comp<'a>(cx: Scope, children: Element<'a>) -> Element {
cx.render(rsx! { children })
}
let mut dom = VirtualDom::new(app);
assert_eq!(
dom.rebuild().santize().edits,
[
// load 1
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
// load 2
LoadTemplate { name: "template", index: 0, id: ElementId(2) },
// load 3
LoadTemplate { name: "template", index: 0, id: ElementId(3) },
// load div that contains 4
LoadTemplate { name: "template", index: 1, id: ElementId(4) },
AppendChildren { id: ElementId(0), m: 4 },
]
);
}
/// note that the template is all dynamic roots - so it doesn't actually get cached as a template
#[test]
fn dynamic_node_as_root() {
fn app(cx: Scope) -> Element {
let a = 123;
let b = 456;
cx.render(rsx! { "{a}" "{b}" })
}
let mut dom = VirtualDom::new(app);
let edits = dom.rebuild().santize();
// Since the roots were all dynamic, they should not cause any template muations
assert!(edits.templates.is_empty());
// The root node is text, so we just create it on the spot
assert_eq!(
edits.edits,
[
CreateTextNode { value: "123", id: ElementId(1) },
CreateTextNode { value: "456", id: ElementId(2) },
AppendChildren { id: ElementId(0), m: 2 }
]
)
}

View file

@ -0,0 +1,51 @@
use dioxus::core::{ElementId, Mutation::*};
use dioxus::prelude::*;
/// As we clean up old templates, the ID for the node should cycle
#[test]
fn cycling_elements() {
let mut dom = VirtualDom::new(|cx| {
cx.render(match cx.generation() % 2 {
0 => rsx! { div { "wasd" } },
1 => rsx! { div { "abcd" } },
_ => unreachable!(),
})
});
let edits = dom.rebuild().santize();
assert_eq!(
edits.edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
AppendChildren { m: 1, id: ElementId(0) },
]
);
dom.mark_dirty(ScopeId(0));
assert_eq!(
dom.render_immediate().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
ReplaceWith { id: ElementId(1,), m: 1 },
]
);
// notice that the IDs cycle back to ElementId(1), preserving a minimal memory footprint
dom.mark_dirty(ScopeId(0));
assert_eq!(
dom.render_immediate().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
ReplaceWith { id: ElementId(2,), m: 1 },
]
);
dom.mark_dirty(ScopeId(0));
assert_eq!(
dom.render_immediate().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
ReplaceWith { id: ElementId(1,), m: 1 },
]
);
}

View file

@ -0,0 +1,102 @@
use dioxus::core::{ElementId, Mutation::*};
use dioxus::prelude::*;
/// When returning sets of components, we do a light diff of the contents to preserve some react-like functionality
///
/// This means that nav_bar should never get re-created and that we should only be swapping out
/// different pointers
#[test]
fn component_swap() {
fn app(cx: Scope) -> Element {
let render_phase = cx.use_hook(|| 0);
*render_phase += 1;
cx.render(match *render_phase {
0 => rsx! {
nav_bar {}
dash_board {}
},
1 => rsx! {
nav_bar {}
dash_results {}
},
2 => rsx! {
nav_bar {}
dash_board {}
},
3 => rsx! {
nav_bar {}
dash_results {}
},
4 => rsx! {
nav_bar {}
dash_board {}
},
_ => rsx!("blah"),
})
}
fn nav_bar(cx: Scope) -> Element {
cx.render(rsx! {
h1 {
"NavBar"
(0..3).map(|_| rsx!(nav_link {}))
}
})
}
fn nav_link(cx: Scope) -> Element {
cx.render(rsx!( h1 { "nav_link" } ))
}
fn dash_board(cx: Scope) -> Element {
cx.render(rsx!( div { "dashboard" } ))
}
fn dash_results(cx: Scope) -> Element {
cx.render(rsx!( div { "results" } ))
}
let mut dom = VirtualDom::new(app);
let edits = dom.rebuild().santize();
assert_eq!(
edits.edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
LoadTemplate { name: "template", index: 0, id: ElementId(2) },
LoadTemplate { name: "template", index: 0, id: ElementId(3) },
LoadTemplate { name: "template", index: 0, id: ElementId(4) },
ReplacePlaceholder { path: &[1], m: 3 },
LoadTemplate { name: "template", index: 0, id: ElementId(5) },
AppendChildren { m: 2, id: ElementId(0) }
]
);
dom.mark_dirty(ScopeId(0));
assert_eq!(
dom.render_immediate().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(6) },
ReplaceWith { id: ElementId(5), m: 1 }
]
);
dom.mark_dirty(ScopeId(0));
assert_eq!(
dom.render_immediate().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(5) },
ReplaceWith { id: ElementId(6), m: 1 }
]
);
dom.mark_dirty(ScopeId(0));
assert_eq!(
dom.render_immediate().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(6) },
ReplaceWith { id: ElementId(5), m: 1 }
]
);
}

View file

@ -0,0 +1,84 @@
use dioxus::core::Mutation::*;
use dioxus::prelude::*;
use dioxus_core::ElementId;
#[test]
fn text_diff() {
fn app(cx: Scope) -> Element {
let gen = cx.generation();
cx.render(rsx!( h1 { "hello {gen}" } ))
}
let mut vdom = VirtualDom::new(app);
_ = vdom.rebuild();
vdom.mark_dirty(ScopeId(0));
assert_eq!(
vdom.render_immediate().edits,
[SetText { value: "hello 1", id: ElementId(2) }]
);
vdom.mark_dirty(ScopeId(0));
assert_eq!(
vdom.render_immediate().edits,
[SetText { value: "hello 2", id: ElementId(2) }]
);
vdom.mark_dirty(ScopeId(0));
assert_eq!(
vdom.render_immediate().edits,
[SetText { value: "hello 3", id: ElementId(2) }]
);
}
#[test]
fn element_swap() {
fn app(cx: Scope) -> Element {
let gen = cx.generation();
match gen % 2 {
0 => cx.render(rsx!( h1 { "hello 1" } )),
1 => cx.render(rsx!( h2 { "hello 2" } )),
_ => unreachable!(),
}
}
let mut vdom = VirtualDom::new(app);
_ = vdom.rebuild();
vdom.mark_dirty(ScopeId(0));
assert_eq!(
vdom.render_immediate().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
ReplaceWith { id: ElementId(1,), m: 1 },
]
);
vdom.mark_dirty(ScopeId(0));
assert_eq!(
vdom.render_immediate().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
ReplaceWith { id: ElementId(2,), m: 1 },
]
);
vdom.mark_dirty(ScopeId(0));
assert_eq!(
vdom.render_immediate().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
ReplaceWith { id: ElementId(1,), m: 1 },
]
);
vdom.mark_dirty(ScopeId(0));
assert_eq!(
vdom.render_immediate().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
ReplaceWith { id: ElementId(2,), m: 1 },
]
);
}

View file

@ -0,0 +1,356 @@
//! Diffing Tests
//!
//! These tests only verify that the diffing algorithm works properly for single components.
//!
//! It does not validated that component lifecycles work properly. This is done in another test file.
use dioxus::core::{ElementId, Mutation::*};
use dioxus::prelude::*;
/// Should result in moves, but not removals or additions
#[test]
fn keyed_diffing_out_of_order() {
let mut dom = VirtualDom::new(|cx| {
let order = match cx.generation() % 2 {
0 => &[0, 1, 2, 3, /**/ 4, 5, 6, /**/ 7, 8, 9],
1 => &[0, 1, 2, 3, /**/ 6, 4, 5, /**/ 7, 8, 9],
_ => unreachable!(),
};
cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" }))))
});
assert_eq!(
dom.rebuild().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
LoadTemplate { name: "template", index: 0, id: ElementId(3,) },
LoadTemplate { name: "template", index: 0, id: ElementId(4,) },
LoadTemplate { name: "template", index: 0, id: ElementId(5,) },
LoadTemplate { name: "template", index: 0, id: ElementId(6,) },
LoadTemplate { name: "template", index: 0, id: ElementId(7,) },
LoadTemplate { name: "template", index: 0, id: ElementId(8,) },
LoadTemplate { name: "template", index: 0, id: ElementId(9,) },
LoadTemplate { name: "template", index: 0, id: ElementId(10,) },
AppendChildren { m: 10, id: ElementId(0) },
]
);
dom.mark_dirty(ScopeId(0));
assert_eq!(
dom.render_immediate().edits,
[
PushRoot { id: ElementId(7,) },
InsertBefore { id: ElementId(5,), m: 1 },
]
)
}
/// Should result in moves only
#[test]
fn keyed_diffing_out_of_order_adds() {
let mut dom = VirtualDom::new(|cx| {
let order = match cx.generation() % 2 {
0 => &[/**/ 4, 5, 6, 7, 8 /**/],
1 => &[/**/ 8, 7, 4, 5, 6 /**/],
_ => unreachable!(),
};
cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" }))))
});
_ = dom.rebuild();
dom.mark_dirty(ScopeId(0));
assert_eq!(
dom.render_immediate().edits,
[
PushRoot { id: ElementId(5,) },
PushRoot { id: ElementId(4,) },
InsertBefore { id: ElementId(1,), m: 2 },
]
)
}
/// Should result in moves only
#[test]
fn keyed_diffing_out_of_order_adds_3() {
let mut dom = VirtualDom::new(|cx| {
let order = match cx.generation() % 2 {
0 => &[/**/ 4, 5, 6, 7, 8 /**/],
1 => &[/**/ 4, 8, 7, 5, 6 /**/],
_ => unreachable!(),
};
cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" }))))
});
_ = dom.rebuild();
dom.mark_dirty(ScopeId(0));
assert_eq!(
dom.render_immediate().edits,
[
PushRoot { id: ElementId(5,) },
PushRoot { id: ElementId(4,) },
InsertBefore { id: ElementId(2,), m: 2 },
]
);
}
/// Should result in moves onl
#[test]
fn keyed_diffing_out_of_order_adds_4() {
let mut dom = VirtualDom::new(|cx| {
let order = match cx.generation() % 2 {
0 => &[/**/ 4, 5, 6, 7, 8 /**/],
1 => &[/**/ 4, 5, 8, 7, 6 /**/],
_ => unreachable!(),
};
cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" }))))
});
_ = dom.rebuild();
dom.mark_dirty(ScopeId(0));
assert_eq!(
dom.render_immediate().edits,
[
PushRoot { id: ElementId(5,) },
PushRoot { id: ElementId(4,) },
InsertBefore { id: ElementId(3,), m: 2 },
]
);
}
/// Should result in moves onl
#[test]
fn keyed_diffing_out_of_order_adds_5() {
let mut dom = VirtualDom::new(|cx| {
let order = match cx.generation() % 2 {
0 => &[/**/ 4, 5, 6, 7, 8 /**/],
1 => &[/**/ 4, 5, 6, 8, 7 /**/],
_ => unreachable!(),
};
cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" }))))
});
_ = dom.rebuild();
dom.mark_dirty(ScopeId(0));
assert_eq!(
dom.render_immediate().edits,
[
PushRoot { id: ElementId(5,) },
InsertBefore { id: ElementId(4,), m: 1 },
]
);
}
/// Should result in moves onl
#[test]
fn keyed_diffing_additions() {
let mut dom = VirtualDom::new(|cx| {
let order: &[_] = match cx.generation() % 2 {
0 => &[/**/ 4, 5, 6, 7, 8 /**/],
1 => &[/**/ 4, 5, 6, 7, 8, 9, 10 /**/],
_ => unreachable!(),
};
cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" }))))
});
_ = dom.rebuild();
dom.mark_dirty(ScopeId(0));
assert_eq!(
dom.render_immediate().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(6) },
LoadTemplate { name: "template", index: 0, id: ElementId(7) },
InsertAfter { id: ElementId(5), m: 2 }
]
);
}
#[test]
fn keyed_diffing_additions_and_moves_on_ends() {
let mut dom = VirtualDom::new(|cx| {
let order: &[_] = match cx.generation() % 2 {
0 => &[/**/ 4, 5, 6, 7 /**/],
1 => &[/**/ 7, 4, 5, 6, 11, 12 /**/],
_ => unreachable!(),
};
cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" }))))
});
_ = dom.rebuild();
dom.mark_dirty(ScopeId(0));
assert_eq!(
dom.render_immediate().santize().edits,
[
// create 11, 12
LoadTemplate { name: "template", index: 0, id: ElementId(5) },
LoadTemplate { name: "template", index: 0, id: ElementId(6) },
InsertAfter { id: ElementId(3), m: 2 },
// move 7 to the front
PushRoot { id: ElementId(4) },
InsertBefore { id: ElementId(1), m: 1 }
]
);
}
#[test]
fn keyed_diffing_additions_and_moves_in_middle() {
let mut dom = VirtualDom::new(|cx| {
let order: &[_] = match cx.generation() % 2 {
0 => &[/**/ 1, 2, 3, 4 /**/],
1 => &[/**/ 4, 1, 7, 8, 2, 5, 6, 3 /**/],
_ => unreachable!(),
};
cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" }))))
});
_ = dom.rebuild();
// LIS: 4, 5, 6
dom.mark_dirty(ScopeId(0));
assert_eq!(
dom.render_immediate().santize().edits,
[
// create 5, 6
LoadTemplate { name: "template", index: 0, id: ElementId(5) },
LoadTemplate { name: "template", index: 0, id: ElementId(6) },
InsertBefore { id: ElementId(3), m: 2 },
// create 7, 8
LoadTemplate { name: "template", index: 0, id: ElementId(7) },
LoadTemplate { name: "template", index: 0, id: ElementId(8) },
InsertBefore { id: ElementId(2), m: 2 },
// move 7
PushRoot { id: ElementId(4) },
InsertBefore { id: ElementId(1), m: 1 }
]
);
}
#[test]
fn controlled_keyed_diffing_out_of_order() {
let mut dom = VirtualDom::new(|cx| {
let order: &[_] = match cx.generation() % 2 {
0 => &[4, 5, 6, 7],
1 => &[0, 5, 9, 6, 4],
_ => unreachable!(),
};
cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" }))))
});
_ = dom.rebuild();
// LIS: 5, 6
dom.mark_dirty(ScopeId(0));
assert_eq!(
dom.render_immediate().santize().edits,
[
// remove 7
Remove { id: ElementId(4,) },
// move 4 to after 6
PushRoot { id: ElementId(1) },
InsertAfter { id: ElementId(3,), m: 1 },
// create 9 and insert before 6
LoadTemplate { name: "template", index: 0, id: ElementId(4) },
InsertBefore { id: ElementId(3,), m: 1 },
// create 0 and insert before 5
LoadTemplate { name: "template", index: 0, id: ElementId(5) },
InsertBefore { id: ElementId(2,), m: 1 },
]
);
}
#[test]
fn controlled_keyed_diffing_out_of_order_max_test() {
let mut dom = VirtualDom::new(|cx| {
let order: &[_] = match cx.generation() % 2 {
0 => &[0, 1, 2, 3, 4],
1 => &[3, 0, 1, 10, 2],
_ => unreachable!(),
};
cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" }))))
});
_ = dom.rebuild();
dom.mark_dirty(ScopeId(0));
assert_eq!(
dom.render_immediate().santize().edits,
[
Remove { id: ElementId(5,) },
LoadTemplate { name: "template", index: 0, id: ElementId(5) },
InsertBefore { id: ElementId(3,), m: 1 },
PushRoot { id: ElementId(4) },
InsertBefore { id: ElementId(1,), m: 1 },
]
);
}
// noticed some weird behavior in the desktop interpreter
// just making sure it doesnt happen in the core implementation
#[test]
fn remove_list() {
let mut dom = VirtualDom::new(|cx| {
let order: &[_] = match cx.generation() % 2 {
0 => &[9, 8, 7, 6, 5],
1 => &[9, 8],
_ => unreachable!(),
};
cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" }))))
});
_ = dom.rebuild();
dom.mark_dirty(ScopeId(0));
assert_eq!(
dom.render_immediate().santize().edits,
[
Remove { id: ElementId(5) },
Remove { id: ElementId(4) },
Remove { id: ElementId(3) },
]
);
}
#[test]
fn no_common_keys() {
let mut dom = VirtualDom::new(|cx| {
let order: &[_] = match cx.generation() % 2 {
0 => &[1, 2, 3],
1 => &[4, 5, 6],
_ => unreachable!(),
};
cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" }))))
});
_ = dom.rebuild();
dom.mark_dirty(ScopeId(0));
assert_eq!(
dom.render_immediate().santize().edits,
[
Remove { id: ElementId(3) },
Remove { id: ElementId(2) },
LoadTemplate { name: "template", index: 0, id: ElementId(2) },
LoadTemplate { name: "template", index: 0, id: ElementId(3) },
LoadTemplate { name: "template", index: 0, id: ElementId(4) },
ReplaceWith { id: ElementId(1), m: 3 }
]
);
}

View file

@ -0,0 +1,379 @@
use dioxus::core::{ElementId, Mutation::*};
use dioxus::prelude::*;
#[test]
fn list_creates_one_by_one() {
let mut dom = VirtualDom::new(|cx| {
let gen = cx.generation();
cx.render(rsx! {
div {
(0..gen).map(|i| rsx! {
div { "{i}" }
})
}
})
});
// load the div and then assign the empty fragment as a placeholder
assert_eq!(
dom.rebuild().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
AssignId { path: &[0], id: ElementId(2,) },
AppendChildren { id: ElementId(0), m: 1 },
]
);
// Rendering the first item should replace the placeholder with an element
dom.mark_dirty(ScopeId(0));
assert_eq!(
dom.render_immediate().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(3,) },
HydrateText { path: &[0], value: "0", id: ElementId(4,) },
ReplaceWith { id: ElementId(2,), m: 1 },
]
);
// Rendering the next item should insert after the previous
dom.mark_dirty(ScopeId(0));
assert_eq!(
dom.render_immediate().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
HydrateText { path: &[0], value: "1", id: ElementId(5,) },
InsertAfter { id: ElementId(3,), m: 1 },
]
);
// ... and again!
dom.mark_dirty(ScopeId(0));
assert_eq!(
dom.render_immediate().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(6,) },
HydrateText { path: &[0], value: "2", id: ElementId(7,) },
InsertAfter { id: ElementId(2,), m: 1 },
]
);
// once more
dom.mark_dirty(ScopeId(0));
assert_eq!(
dom.render_immediate().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(8,) },
HydrateText { path: &[0], value: "3", id: ElementId(9,) },
InsertAfter { id: ElementId(6,), m: 1 },
]
);
}
#[test]
fn removes_one_by_one() {
let mut dom = VirtualDom::new(|cx| {
let gen = 3 - cx.generation() % 4;
cx.render(rsx! {
div {
(0..gen).map(|i| rsx! {
div { "{i}" }
})
}
})
});
// load the div and then assign the empty fragment as a placeholder
assert_eq!(
dom.rebuild().santize().edits,
[
// The container
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
// each list item
LoadTemplate { name: "template", index: 0, id: ElementId(2) },
HydrateText { path: &[0], value: "0", id: ElementId(3) },
LoadTemplate { name: "template", index: 0, id: ElementId(4) },
HydrateText { path: &[0], value: "1", id: ElementId(5) },
LoadTemplate { name: "template", index: 0, id: ElementId(6) },
HydrateText { path: &[0], value: "2", id: ElementId(7) },
// replace the placeholder in the template with the 3 templates on the stack
ReplacePlaceholder { m: 3, path: &[0] },
// Mount the div
AppendChildren { id: ElementId(0), m: 1 }
]
);
// Remove div(3)
// Rendering the first item should replace the placeholder with an element
dom.mark_dirty(ScopeId(0));
assert_eq!(
dom.render_immediate().santize().edits,
[Remove { id: ElementId(6) }]
);
// Remove div(2)
dom.mark_dirty(ScopeId(0));
assert_eq!(
dom.render_immediate().santize().edits,
[Remove { id: ElementId(4) }]
);
// Remove div(1) and replace with a placeholder
// todo: this should just be a remove with no placeholder
dom.mark_dirty(ScopeId(0));
assert_eq!(
dom.render_immediate().santize().edits,
[
CreatePlaceholder { id: ElementId(3) },
ReplaceWith { id: ElementId(2), m: 1 }
]
);
// load the 3 and replace the placeholder
// todo: this should actually be append to, but replace placeholder is fine for now
dom.mark_dirty(ScopeId(0));
assert_eq!(
dom.render_immediate().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(2) },
HydrateText { path: &[0], value: "0", id: ElementId(4) },
LoadTemplate { name: "template", index: 0, id: ElementId(5) },
HydrateText { path: &[0], value: "1", id: ElementId(6) },
LoadTemplate { name: "template", index: 0, id: ElementId(7) },
HydrateText { path: &[0], value: "2", id: ElementId(8) },
ReplaceWith { id: ElementId(3), m: 3 }
]
);
}
#[test]
fn list_shrink_multiroot() {
let mut dom = VirtualDom::new(|cx| {
cx.render(rsx! {
div {
(0..cx.generation()).map(|i| rsx! {
div { "{i}" }
div { "{i}" }
})
}
})
});
assert_eq!(
dom.rebuild().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
AssignId { path: &[0,], id: ElementId(2,) },
AppendChildren { id: ElementId(0), m: 1 }
]
);
dom.mark_dirty(ScopeId(0));
assert_eq!(
dom.render_immediate().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(3) },
HydrateText { path: &[0], value: "0", id: ElementId(4) },
LoadTemplate { name: "template", index: 1, id: ElementId(5) },
HydrateText { path: &[0], value: "0", id: ElementId(6) },
ReplaceWith { id: ElementId(2), m: 2 }
]
);
dom.mark_dirty(ScopeId(0));
assert_eq!(
dom.render_immediate().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(2) },
HydrateText { path: &[0], value: "1", id: ElementId(7) },
LoadTemplate { name: "template", index: 1, id: ElementId(8) },
HydrateText { path: &[0], value: "1", id: ElementId(9) },
InsertAfter { id: ElementId(5), m: 2 }
]
);
dom.mark_dirty(ScopeId(0));
assert_eq!(
dom.render_immediate().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(10) },
HydrateText { path: &[0], value: "2", id: ElementId(11) },
LoadTemplate { name: "template", index: 1, id: ElementId(12) },
HydrateText { path: &[0], value: "2", id: ElementId(13) },
InsertAfter { id: ElementId(8), m: 2 }
]
);
}
#[test]
fn removes_one_by_one_multiroot() {
let mut dom = VirtualDom::new(|cx| {
let gen = 3 - cx.generation() % 4;
cx.render(rsx! {
div {
(0..gen).map(|i| rsx! {
div { "{i}" }
div { "{i}" }
})
}
})
});
// load the div and then assign the empty fragment as a placeholder
assert_eq!(
dom.rebuild().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
//
LoadTemplate { name: "template", index: 0, id: ElementId(2) },
HydrateText { path: &[0], value: "0", id: ElementId(3) },
LoadTemplate { name: "template", index: 1, id: ElementId(4) },
HydrateText { path: &[0], value: "0", id: ElementId(5) },
//
LoadTemplate { name: "template", index: 0, id: ElementId(6) },
HydrateText { path: &[0], value: "1", id: ElementId(7) },
LoadTemplate { name: "template", index: 1, id: ElementId(8) },
HydrateText { path: &[0], value: "1", id: ElementId(9) },
//
LoadTemplate { name: "template", index: 0, id: ElementId(10) },
HydrateText { path: &[0], value: "2", id: ElementId(11) },
LoadTemplate { name: "template", index: 1, id: ElementId(12) },
HydrateText { path: &[0], value: "2", id: ElementId(13) },
//
ReplacePlaceholder { path: &[0], m: 6 },
//
AppendChildren { id: ElementId(0), m: 1 }
]
);
dom.mark_dirty(ScopeId(0));
assert_eq!(
dom.render_immediate().santize().edits,
[Remove { id: ElementId(10) }, Remove { id: ElementId(12) }]
);
dom.mark_dirty(ScopeId(0));
assert_eq!(
dom.render_immediate().santize().edits,
[Remove { id: ElementId(6) }, Remove { id: ElementId(8) }]
);
dom.mark_dirty(ScopeId(0));
assert_eq!(
dom.render_immediate().santize().edits,
[
Remove { id: ElementId(4) },
CreatePlaceholder { id: ElementId(5) },
ReplaceWith { id: ElementId(2), m: 1 }
]
);
}
#[test]
fn two_equal_fragments_are_equal_static() {
let mut dom = VirtualDom::new(|cx| {
cx.render(rsx! {
(0..5).map(|_| rsx! {
div { "hello" }
})
})
});
_ = dom.rebuild();
assert!(dom.render_immediate().edits.is_empty());
}
#[test]
fn two_equal_fragments_are_equal() {
let mut dom = VirtualDom::new(|cx| {
cx.render(rsx! {
(0..5).map(|i| rsx! {
div { "hello {i}" }
})
})
});
_ = dom.rebuild();
assert!(dom.render_immediate().edits.is_empty());
}
#[test]
fn remove_many() {
let mut dom = VirtualDom::new(|cx| {
let num = match cx.generation() % 3 {
0 => 0,
1 => 1,
2 => 5,
_ => unreachable!(),
};
cx.render(rsx! {
(0..num).map(|i| rsx! { div { "hello {i}" } })
})
});
let edits = dom.rebuild().santize();
assert!(edits.templates.is_empty());
assert_eq!(
edits.edits,
[
CreatePlaceholder { id: ElementId(1,) },
AppendChildren { id: ElementId(0), m: 1 },
]
);
dom.mark_dirty(ScopeId(0));
let edits = dom.render_immediate().santize();
assert_eq!(
edits.edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
HydrateText { path: &[0,], value: "hello 0", id: ElementId(3,) },
ReplaceWith { id: ElementId(1,), m: 1 },
]
);
dom.mark_dirty(ScopeId(0));
let edits = dom.render_immediate().santize();
assert_eq!(
edits.edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
HydrateText { path: &[0,], value: "hello 1", id: ElementId(4,) },
LoadTemplate { name: "template", index: 0, id: ElementId(5,) },
HydrateText { path: &[0,], value: "hello 2", id: ElementId(6,) },
LoadTemplate { name: "template", index: 0, id: ElementId(7,) },
HydrateText { path: &[0,], value: "hello 3", id: ElementId(8,) },
LoadTemplate { name: "template", index: 0, id: ElementId(9,) },
HydrateText { path: &[0,], value: "hello 4", id: ElementId(10,) },
InsertAfter { id: ElementId(2,), m: 4 },
]
);
dom.mark_dirty(ScopeId(0));
let edits = dom.render_immediate().santize();
assert_eq!(
edits.edits,
[
Remove { id: ElementId(9,) },
Remove { id: ElementId(7,) },
Remove { id: ElementId(5,) },
Remove { id: ElementId(1,) },
CreatePlaceholder { id: ElementId(3,) },
ReplaceWith { id: ElementId(2,), m: 1 },
]
);
dom.mark_dirty(ScopeId(0));
let edits = dom.render_immediate().santize();
assert_eq!(
edits.edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
HydrateText { path: &[0,], value: "hello 0", id: ElementId(1,) },
ReplaceWith { id: ElementId(3,), m: 1 },
]
)
}

View file

@ -0,0 +1 @@
//! It should be possible to swap out templates at runtime, enabling hotreloading

View file

@ -0,0 +1,42 @@
use dioxus::core::{ElementId, Mutation};
use dioxus::prelude::*;
fn basic_syntax_is_a_template(cx: Scope) -> Element {
let asd = 123;
let var = 123;
cx.render(rsx! {
div { key: "12345",
class: "asd",
class: "{asd}",
onclick: move |_| {},
div { "{var}" }
div {
h1 { "var" }
p { "you're great!" }
div { background_color: "red",
h1 { "var" }
div { b { "asd" } "not great" }
}
p { "you're great!" }
}
}
})
}
#[test]
fn dual_stream() {
let mut dom = VirtualDom::new(basic_syntax_is_a_template);
let edits = dom.rebuild().santize();
use Mutation::*;
assert_eq!(
edits.edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
SetAttribute { name: "class", value: "123", id: ElementId(1), ns: None },
NewEventListener { name: "click", scope: ScopeId(0), id: ElementId(1) },
HydrateText { path: &[0, 0], value: "123", id: ElementId(2) },
AppendChildren { id: ElementId(0), m: 1 }
],
);
}

View file

@ -0,0 +1,168 @@
#![allow(unused, non_upper_case_globals)]
#![allow(non_snake_case)]
//! Tests for the lifecycle of components.
use dioxus::core::{ElementId, Mutation::*};
use dioxus::prelude::*;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
type Shared<T> = Arc<Mutex<T>>;
#[test]
fn manual_diffing() {
struct AppProps {
value: Shared<&'static str>,
}
fn app(cx: Scope<AppProps>) -> Element {
let val = cx.props.value.lock().unwrap();
cx.render(rsx! { div { "{val}" } })
};
let value = Arc::new(Mutex::new("Hello"));
let mut dom = VirtualDom::new_with_props(app, AppProps { value: value.clone() });
let _ = dom.rebuild();
*value.lock().unwrap() = "goodbye";
assert_eq!(
dom.rebuild().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(3) },
HydrateText { path: &[0], value: "goodbye", id: ElementId(4) },
AppendChildren { m: 1, id: ElementId(0) }
]
);
}
#[test]
fn events_generate() {
fn app(cx: Scope) -> Element {
let count = cx.use_hook(|| 0);
match *count {
0 => cx.render(rsx! {
div { onclick: move |_| *count += 1,
div { "nested" }
"Click me!"
}
}),
_ => cx.render(rsx!(())),
}
};
let mut dom = VirtualDom::new(app);
_ = dom.rebuild();
dom.handle_event("click", Rc::new(MouseData::default()), ElementId(1), true);
dom.mark_dirty(ScopeId(0));
let edits = dom.render_immediate();
assert_eq!(
edits.edits,
[
CreatePlaceholder { id: ElementId(2) },
ReplaceWith { id: ElementId(1), m: 1 }
]
)
}
// #[test]
// fn components_generate() {
// fn app(cx: Scope) -> Element {
// let render_phase = cx.use_hook(|| 0);
// *render_phase += 1;
// cx.render(match *render_phase {
// 1 => rsx_without_templates!("Text0"),
// 2 => rsx_without_templates!(div {}),
// 3 => rsx_without_templates!("Text2"),
// 4 => rsx_without_templates!(Child {}),
// 5 => rsx_without_templates!({ None as Option<()> }),
// 6 => rsx_without_templates!("text 3"),
// 7 => rsx_without_templates!({ (0..2).map(|f| rsx_without_templates!("text {f}")) }),
// 8 => rsx_without_templates!(Child {}),
// _ => todo!(),
// })
// };
// fn Child(cx: Scope) -> Element {
// println!("Running child");
// cx.render(rsx_without_templates! {
// h1 {}
// })
// }
// let mut dom = VirtualDom::new(app);
// let edits = dom.rebuild();
// assert_eq!(
// edits.edits,
// [
// CreateTextNode { root: Some(1), text: "Text0" },
// AppendChildren { root: Some(0), children: vec![1] }
// ]
// );
// assert_eq!(
// dom.hard_diff(ScopeId(0)).edits,
// [
// CreateElement { root: Some(2), tag: "div", children: 0 },
// ReplaceWith { root: Some(1), nodes: vec![2] }
// ]
// );
// assert_eq!(
// dom.hard_diff(ScopeId(0)).edits,
// [
// CreateTextNode { root: Some(1), text: "Text2" },
// ReplaceWith { root: Some(2), nodes: vec![1] }
// ]
// );
// // child {}
// assert_eq!(
// dom.hard_diff(ScopeId(0)).edits,
// [
// CreateElement { root: Some(2), tag: "h1", children: 0 },
// ReplaceWith { root: Some(1), nodes: vec![2] }
// ]
// );
// // placeholder
// assert_eq!(
// dom.hard_diff(ScopeId(0)).edits,
// [
// CreatePlaceholder { root: Some(1) },
// ReplaceWith { root: Some(2), nodes: vec![1] }
// ]
// );
// assert_eq!(
// dom.hard_diff(ScopeId(0)).edits,
// [
// CreateTextNode { root: Some(2), text: "text 3" },
// ReplaceWith { root: Some(1), nodes: vec![2] }
// ]
// );
// assert_eq!(
// dom.hard_diff(ScopeId(0)).edits,
// [
// CreateTextNode { text: "text 0", root: Some(1) },
// CreateTextNode { text: "text 1", root: Some(3) },
// ReplaceWith { root: Some(2), nodes: vec![1, 3] },
// ]
// );
// assert_eq!(
// dom.hard_diff(ScopeId(0)).edits,
// [
// CreateElement { tag: "h1", root: Some(2), children: 0 },
// ReplaceWith { root: Some(1), nodes: vec![2] },
// Remove { root: Some(3) },
// ]
// );
// }

View file

@ -0,0 +1,49 @@
use dioxus::prelude::*;
use dioxus_core::ElementId;
use std::rc::Rc;
#[test]
fn miri_rollover() {
let mut dom = VirtualDom::new(app);
_ = dom.rebuild();
for _ in 0..3 {
dom.handle_event("click", Rc::new(MouseData::default()), ElementId(2), true);
dom.process_events();
_ = dom.render_immediate();
}
}
fn app(cx: Scope) -> Element {
let mut idx = use_state(cx, || 0);
let onhover = |_| println!("go!");
cx.render(rsx! {
div {
button {
onclick: move |_| {
idx += 1;
println!("Clicked");
},
"+"
}
button { onclick: move |_| idx -= 1, "-" }
ul {
(0..**idx).map(|i| rsx! {
child_example { i: i, onhover: onhover }
})
}
}
})
}
#[inline_props]
fn child_example<'a>(cx: Scope<'a>, i: i32, onhover: EventHandler<'a, MouseEvent>) -> Element {
cx.render(rsx! {
li {
onmouseover: move |e| onhover.call(e),
"{i}"
}
})
}

View file

@ -0,0 +1,127 @@
use dioxus::prelude::*;
#[test]
fn app_drops() {
fn app(cx: Scope) -> Element {
cx.render(rsx! {
div {}
})
}
let mut dom = VirtualDom::new(app);
_ = dom.rebuild();
dom.mark_dirty(ScopeId(0));
_ = dom.render_immediate();
}
#[test]
fn hooks_drop() {
fn app(cx: Scope) -> Element {
cx.use_hook(|| String::from("asd"));
cx.use_hook(|| String::from("asd"));
cx.use_hook(|| String::from("asd"));
cx.use_hook(|| String::from("asd"));
cx.render(rsx! {
div {}
})
}
let mut dom = VirtualDom::new(app);
_ = dom.rebuild();
dom.mark_dirty(ScopeId(0));
_ = dom.render_immediate();
}
#[test]
fn contexts_drop() {
fn app(cx: Scope) -> Element {
cx.provide_context(String::from("asd"));
cx.render(rsx! {
div {
child_comp {}
}
})
}
fn child_comp(cx: Scope) -> Element {
let el = cx.consume_context::<String>().unwrap();
cx.render(rsx! {
div { "hello {el}" }
})
}
let mut dom = VirtualDom::new(app);
_ = dom.rebuild();
dom.mark_dirty(ScopeId(0));
_ = dom.render_immediate();
}
#[test]
fn tasks_drop() {
fn app(cx: Scope) -> Element {
cx.spawn(async {
tokio::time::sleep(std::time::Duration::from_millis(100000)).await;
});
cx.render(rsx! {
div { }
})
}
let mut dom = VirtualDom::new(app);
_ = dom.rebuild();
dom.mark_dirty(ScopeId(0));
_ = dom.render_immediate();
}
#[test]
fn root_props_drop() {
struct RootProps(String);
let mut dom = VirtualDom::new_with_props(
|cx| cx.render(rsx!( div { "{cx.props.0}" } )),
RootProps("asdasd".to_string()),
);
_ = dom.rebuild();
dom.mark_dirty(ScopeId(0));
_ = dom.render_immediate();
}
#[test]
fn diffing_drops_old() {
fn app(cx: Scope) -> Element {
cx.render(rsx! {
div {
match cx.generation() % 2 {
0 => rsx!( child_comp1 { name: "asdasd".to_string() }),
1 => rsx!( child_comp2 { name: "asdasd".to_string() }),
_ => todo!()
}
}
})
}
#[inline_props]
fn child_comp1(cx: Scope, name: String) -> Element {
cx.render(rsx! { "Hello {name}" })
}
#[inline_props]
fn child_comp2(cx: Scope, name: String) -> Element {
cx.render(rsx! { "Goodbye {name}" })
}
let mut dom = VirtualDom::new(app);
_ = dom.rebuild();
dom.mark_dirty(ScopeId(0));
_ = dom.render_immediate();
}

View file

@ -0,0 +1,351 @@
#![allow(non_snake_case)]
use std::rc::Rc;
use dioxus::prelude::*;
/// This test checks that we should release all memory used by the virtualdom when it exits.
///
/// When miri runs, it'll let us know if we leaked or aliased.
#[test]
fn test_memory_leak() {
fn app(cx: Scope) -> Element {
let val = cx.generation();
cx.spawn(async {
tokio::time::sleep(std::time::Duration::from_millis(100000)).await;
});
if val == 2 || val == 4 {
return cx.render(rsx!(()));
}
let name = cx.use_hook(|| String::from("numbers: "));
name.push_str("123 ");
cx.render(rsx!(
div { "Hello, world!" }
Child {}
Child {}
Child {}
Child {}
Child {}
Child {}
BorrowedChild { name: name }
BorrowedChild { name: name }
BorrowedChild { name: name }
BorrowedChild { name: name }
BorrowedChild { name: name }
))
}
#[derive(Props)]
struct BorrowedProps<'a> {
name: &'a str,
}
fn BorrowedChild<'a>(cx: Scope<'a, BorrowedProps<'a>>) -> Element {
cx.render(rsx! {
div {
"goodbye {cx.props.name}"
Child {}
Child {}
}
})
}
fn Child(cx: Scope) -> Element {
render!(div { "goodbye world" })
}
let mut dom = VirtualDom::new(app);
_ = dom.rebuild();
for _ in 0..5 {
dom.mark_dirty(ScopeId(0));
_ = dom.render_immediate();
}
}
#[test]
fn memo_works_properly() {
fn app(cx: Scope) -> Element {
let val = cx.generation();
if val == 2 || val == 4 {
return cx.render(rsx!(()));
}
let name = cx.use_hook(|| String::from("asd"));
cx.render(rsx!(
div { "Hello, world! {name}" }
Child { na: "asdfg".to_string() }
))
}
#[derive(PartialEq, Props)]
struct ChildProps {
na: String,
}
fn Child(cx: Scope<ChildProps>) -> Element {
render!(div { "goodbye world" })
}
let mut dom = VirtualDom::new(app);
_ = dom.rebuild();
// todo!()
// dom.hard_diff(ScopeId(0));
// dom.hard_diff(ScopeId(0));
// dom.hard_diff(ScopeId(0));
// dom.hard_diff(ScopeId(0));
// dom.hard_diff(ScopeId(0));
// dom.hard_diff(ScopeId(0));
// dom.hard_diff(ScopeId(0));
}
#[test]
fn free_works_on_root_hooks() {
/*
On Drop, scopearena drops all the hook contents. and props
*/
#[derive(PartialEq, Clone, Props)]
struct AppProps {
inner: Rc<String>,
}
fn app(cx: Scope<AppProps>) -> Element {
let name: &AppProps = cx.use_hook(|| cx.props.clone());
render!(child_component { inner: name.inner.clone() })
}
fn child_component(cx: Scope<AppProps>) -> Element {
render!(div { "{cx.props.inner}" })
}
let ptr = Rc::new("asdasd".to_string());
let mut dom = VirtualDom::new_with_props(app, AppProps { inner: ptr.clone() });
let _ = dom.rebuild();
// ptr gets cloned into props and then into the hook
assert_eq!(Rc::strong_count(&ptr), 4);
drop(dom);
assert_eq!(Rc::strong_count(&ptr), 1);
}
// #[test]
// fn old_props_arent_stale() {
// fn app(cx: Scope) -> Element {
// dbg!("rendering parent");
// let cnt = cx.use_hook(|| 0);
// *cnt += 1;
// if *cnt == 1 {
// render!(div { Child { a: "abcdef".to_string() } })
// } else {
// render!(div { Child { a: "abcdef".to_string() } })
// }
// }
// #[derive(Props, PartialEq)]
// struct ChildProps {
// a: String,
// }
// fn Child(cx: Scope<ChildProps>) -> Element {
// dbg!("rendering child", &cx.props.a);
// render!(div { "child {cx.props.a}" })
// }
// let mut dom = new_dom(app, ());
// let _ = dom.rebuild();
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
// dom.work_with_deadline(|| false);
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
// dom.work_with_deadline(|| false);
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
// dom.work_with_deadline(|| false);
// dbg!("forcing update to child");
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
// dom.work_with_deadline(|| false);
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
// dom.work_with_deadline(|| false);
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
// dom.work_with_deadline(|| false);
// }
// #[test]
// fn basic() {
// fn app(cx: Scope) -> Element {
// render!(div {
// Child { a: "abcdef".to_string() }
// })
// }
// #[derive(Props, PartialEq)]
// struct ChildProps {
// a: String,
// }
// fn Child(cx: Scope<ChildProps>) -> Element {
// dbg!("rendering child", &cx.props.a);
// render!(div { "child {cx.props.a}" })
// }
// let mut dom = new_dom(app, ());
// let _ = dom.rebuild();
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
// dom.work_with_deadline(|| false);
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
// dom.work_with_deadline(|| false);
// }
// #[test]
// fn leak_thru_children() {
// fn app(cx: Scope) -> Element {
// cx.render(rsx! {
// Child {
// name: "asd".to_string(),
// }
// });
// cx.render(rsx! {
// div {}
// })
// }
// #[inline_props]
// fn Child(cx: Scope, name: String) -> Element {
// render!(div { "child {name}" })
// }
// let mut dom = new_dom(app, ());
// let _ = dom.rebuild();
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
// dom.work_with_deadline(|| false);
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
// dom.work_with_deadline(|| false);
// }
// #[test]
// fn test_pass_thru() {
// #[inline_props]
// fn NavContainer<'a>(cx: Scope, children: Element<'a>) -> Element {
// cx.render(rsx! {
// header {
// nav { children }
// }
// })
// }
// fn NavMenu(cx: Scope) -> Element {
// render!( NavBrand {}
// div {
// NavStart {}
// NavEnd {}
// }
// )
// }
// fn NavBrand(cx: Scope) -> Element {
// render!(div {})
// }
// fn NavStart(cx: Scope) -> Element {
// render!(div {})
// }
// fn NavEnd(cx: Scope) -> Element {
// render!(div {})
// }
// #[inline_props]
// fn MainContainer<'a>(
// cx: Scope,
// nav: Element<'a>,
// body: Element<'a>,
// footer: Element<'a>,
// ) -> Element {
// cx.render(rsx! {
// div {
// class: "columns is-mobile",
// div {
// class: "column is-full",
// nav,
// body,
// footer,
// }
// }
// })
// }
// fn app(cx: Scope) -> Element {
// let nav = cx.render(rsx! {
// NavContainer {
// NavMenu {}
// }
// });
// let body = cx.render(rsx! {
// div {}
// });
// let footer = cx.render(rsx! {
// div {}
// });
// cx.render(rsx! {
// MainContainer {
// nav: nav,
// body: body,
// footer: footer,
// }
// })
// }
// let mut dom = new_dom(app, ());
// let _ = dom.rebuild();
// for _ in 0..40 {
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
// dom.work_with_deadline(|| false);
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
// dom.work_with_deadline(|| false);
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
// dom.work_with_deadline(|| false);
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
// dom.work_with_deadline(|| false);
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
// dom.work_with_deadline(|| false);
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
// dom.work_with_deadline(|| false);
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(2)));
// dom.work_with_deadline(|| false);
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(2)));
// dom.work_with_deadline(|| false);
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(2)));
// dom.work_with_deadline(|| false);
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(3)));
// dom.work_with_deadline(|| false);
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(3)));
// dom.work_with_deadline(|| false);
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(3)));
// dom.work_with_deadline(|| false);
// }
// }

View file

@ -0,0 +1,24 @@
//! Tests related to safety of the library.
use std::rc::Rc;
use dioxus::prelude::*;
use dioxus_core::SuspenseContext;
/// Ensure no issues with not calling rebuild
#[test]
fn root_node_isnt_null() {
let dom = VirtualDom::new(|cx| render!("Hello world!"));
let scope = dom.base_scope();
// We haven't built the tree, so trying to get out the root node should fail
assert!(scope.try_root_node().is_none());
// The height should be 0
assert_eq!(scope.height(), 0);
// There should be a default suspense context
// todo: there should also be a default error boundary
assert!(scope.has_context::<Rc<SuspenseContext>>().is_some());
}

View file

@ -0,0 +1,92 @@
use dioxus::core::ElementId;
use dioxus::core::{Mutation::*, SuspenseContext};
use dioxus::prelude::*;
use std::future::IntoFuture;
use std::rc::Rc;
use std::time::Duration;
#[tokio::test]
async fn it_works() {
let mut dom = VirtualDom::new(app);
let mutations = dom.rebuild().santize();
// We should at least get the top-level template in before pausing for the children
// note: we dont test template edits anymore
// assert_eq!(
// mutations.templates,
// [
// CreateElement { name: "div" },
// CreateStaticText { value: "Waiting for child..." },
// CreateStaticPlaceholder,
// AppendChildren { m: 2 },
// SaveTemplate { name: "template", m: 1 }
// ]
// );
// And we should load it in and assign the placeholder properly
assert_eq!(
mutations.edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
// hmmmmmmmmm.... with suspense how do we guarantee that IDs increase linearly?
// can we even?
AssignId { path: &[1], id: ElementId(3) },
AppendChildren { m: 1, id: ElementId(0) },
]
);
// wait just a moment, not enough time for the boundary to resolve
dom.wait_for_work().await;
}
fn app(cx: Scope) -> Element {
cx.render(rsx!(
div {
"Waiting for child..."
suspense_boundary {}
}
))
}
fn suspense_boundary(cx: Scope) -> Element {
cx.use_hook(|| {
cx.provide_context(Rc::new(SuspenseContext::new(cx.scope_id())));
});
// Ensure the right types are found
cx.has_context::<Rc<SuspenseContext>>().unwrap();
cx.render(rsx!(async_child {}))
}
async fn async_child(cx: Scope<'_>) -> Element {
use_future!(cx, || tokio::time::sleep(Duration::from_millis(10))).await;
cx.render(rsx!(async_text {}))
}
async fn async_text(cx: Scope<'_>) -> Element {
let username = use_future!(cx, || async {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
"async child 1"
});
let age = use_future!(cx, || async {
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
1234
});
let (_user, _age) = use_future!(cx, || async {
tokio::join!(
tokio::time::sleep(std::time::Duration::from_secs(1)),
tokio::time::sleep(std::time::Duration::from_secs(2))
);
("async child 1", 1234)
})
.await;
let (username, age) = tokio::join!(username.into_future(), age.into_future());
cx.render(rsx!( div { "Hello! {username}, you are {age}, {_user} {_age}" } ))
}

View file

@ -0,0 +1,42 @@
//! Verify that tasks get polled by the virtualdom properly, and that we escape wait_for_work safely
use dioxus::prelude::*;
use std::time::Duration;
static mut POLL_COUNT: usize = 0;
#[tokio::test]
async fn it_works() {
let mut dom = VirtualDom::new(app);
let _ = dom.rebuild();
tokio::select! {
_ = dom.wait_for_work() => {}
_ = tokio::time::sleep(Duration::from_millis(500)) => {}
};
// By the time the tasks are finished, we should've accumulated ticks from two tasks
// Be warned that by setting the delay to too short, tokio might not schedule in the tasks
assert_eq!(unsafe { POLL_COUNT }, 135);
}
fn app(cx: Scope) -> Element {
cx.use_hook(|| {
cx.spawn(async {
for x in 0..10 {
tokio::time::sleep(Duration::from_micros(50)).await;
unsafe { POLL_COUNT += x }
}
});
cx.spawn(async {
for x in 0..10 {
tokio::time::sleep(Duration::from_micros(25)).await;
unsafe { POLL_COUNT += x * 2 }
}
});
});
cx.render(rsx!(()))
}

View file

@ -27,12 +27,14 @@ tokio = { version = "1.16.1", features = [
"rt-multi-thread",
"rt",
"time",
"macros",
], optional = true, default-features = false }
webbrowser = "0.8.0"
infer = "0.9.0"
infer = "0.11.0"
dunce = "1.0.2"
interprocess = { version = "1.1.1" }
interprocess = { version = "1.1.1", optional = true}
futures-util = "0.3.25"
[target.'cfg(target_os = "ios")'.dependencies]
objc = "0.2.7"
@ -42,12 +44,12 @@ objc_id = "0.1.1"
core-foundation = "0.9.3"
[features]
default = ["tokio_runtime"]
default = ["tokio_runtime", "hot-reload"]
tokio_runtime = ["tokio"]
fullscreen = ["wry/fullscreen"]
transparent = ["wry/transparent"]
tray = ["wry/tray"]
hot-reload = ["dioxus-core/hot-reload"]
hot-reload = ["interprocess"]
[dev-dependencies]
dioxus-core-macro = { path = "../core-macro" }

View file

@ -1,12 +1,15 @@
use crate::desktop_context::{DesktopContext, UserWindowEvent};
use crate::events::{decode_event, EventMessage};
use dioxus_core::*;
use futures_channel::mpsc::{unbounded, UnboundedSender};
use futures_util::StreamExt;
#[cfg(target_os = "ios")]
use objc::runtime::Object;
use std::{
collections::HashMap,
sync::Arc,
sync::{atomic::AtomicBool, Mutex},
time::Duration,
};
use wry::{
self,
@ -16,10 +19,12 @@ use wry::{
pub(super) struct DesktopController {
pub(super) webviews: HashMap<WindowId, WebView>,
pub(super) sender: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
pub(super) pending_edits: Arc<Mutex<Vec<String>>>,
pub(super) quit_app_on_close: bool,
pub(super) is_ready: Arc<AtomicBool>,
pub(super) proxy: EventLoopProxy<UserWindowEvent>,
pub(super) event_tx: UnboundedSender<serde_json::Value>,
#[cfg(target_os = "ios")]
pub(super) views: Vec<*mut Object>,
}
@ -33,64 +38,61 @@ impl DesktopController {
proxy: EventLoopProxy<UserWindowEvent>,
) -> Self {
let edit_queue = Arc::new(Mutex::new(Vec::new()));
let (sender, receiver) = futures_channel::mpsc::unbounded::<SchedulerMsg>();
let (event_tx, mut event_rx) = unbounded();
let proxy2 = proxy.clone();
let pending_edits = edit_queue.clone();
let return_sender = sender.clone();
let desktop_context_proxy = proxy.clone();
std::thread::spawn(move || {
// We create the runtime as multithreaded, so you can still "spawn" onto multiple threads
// We create the runtime as multithreaded, so you can still "tokio::spawn" onto multiple threads
// I'd personally not require tokio to be built-in to Dioxus-Desktop, but the DX is worse without it
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap();
runtime.block_on(async move {
let mut dom =
VirtualDom::new_with_props_and_scheduler(root, props, (sender, receiver));
let window_context = DesktopContext::new(desktop_context_proxy);
dom.base_scope().provide_context(window_context);
// allow other proccesses to send the new rsx text to the @dioxusin ipc channel and recieve erros on the @dioxusout channel
#[cfg(any(feature = "hot-reload", debug_assertions))]
crate::hot_reload::init(&dom);
let edits = dom.rebuild();
edit_queue
.lock()
.unwrap()
.push(serde_json::to_string(&edits.edits).unwrap());
// Make sure the window is ready for any new updates
proxy.send_event(UserWindowEvent::Update).unwrap();
let mut dom = VirtualDom::new_with_props(root, props)
.with_root_context(DesktopContext::new(desktop_context_proxy));
{
let edits = dom.rebuild();
let mut queue = edit_queue.lock().unwrap();
queue.push(serde_json::to_string(&edits).unwrap());
proxy.send_event(UserWindowEvent::EditsReady).unwrap();
}
loop {
dom.wait_for_work().await;
let muts = dom.work_with_deadline(|| false);
for edit in muts {
edit_queue
.lock()
.unwrap()
.push(serde_json::to_string(&edit.edits).unwrap());
tokio::select! {
_ = dom.wait_for_work() => {}
Some(json_value) = event_rx.next() => {
if let Ok(value) = serde_json::from_value::<EventMessage>(json_value) {
let name = value.event.clone();
let el_id = ElementId(value.mounted_dom_id);
if let Some(evt) = decode_event(value) {
dom.handle_event(&name, evt, el_id, dioxus_html::events::event_bubbles(&name));
}
}
}
}
let _ = proxy.send_event(UserWindowEvent::Update);
let muts = dom
.render_with_deadline(tokio::time::sleep(Duration::from_millis(16)))
.await;
edit_queue.lock().unwrap().push(serde_json::to_string(&muts).unwrap());
let _ = proxy.send_event(UserWindowEvent::EditsReady);
}
})
});
Self {
pending_edits,
sender: return_sender,
webviews: HashMap::new(),
is_ready: Arc::new(AtomicBool::new(false)),
quit_app_on_close: true,
proxy: proxy2,
event_tx,
#[cfg(target_os = "ios")]
views: vec![],
}
@ -106,13 +108,23 @@ impl DesktopController {
pub(super) fn try_load_ready_webviews(&mut self) {
if self.is_ready.load(std::sync::atomic::Ordering::Relaxed) {
let mut queue = self.pending_edits.lock().unwrap();
let mut new_queue = Vec::new();
{
let mut queue = self.pending_edits.lock().unwrap();
std::mem::swap(&mut new_queue, &mut *queue);
}
let (_id, view) = self.webviews.iter_mut().next().unwrap();
for edit in queue.drain(..) {
for edit in new_queue.drain(..) {
view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit))
.unwrap();
}
}
}
pub(crate) fn set_template(&self, _serialized_template: String) {
todo!("hot reloading currently WIP")
}
}

View file

@ -1,3 +1,5 @@
use std::rc::Rc;
use crate::controller::DesktopController;
use dioxus_core::ScopeState;
use wry::application::event_loop::ControlFlow;
@ -17,6 +19,13 @@ pub fn use_window(cx: &ScopeState) -> &DesktopContext {
.unwrap()
}
/// Get a closure that executes any JavaScript in the WebView context.
pub fn use_eval(cx: &ScopeState) -> &Rc<dyn Fn(String)> {
let desktop = use_window(cx).clone();
&*cx.use_hook(|| Rc::new(move |script| desktop.eval(script)) as Rc<dyn Fn(String)>)
}
/// An imperative interface to the current window.
///
/// To get a handle to the current window, use the [`use_window`] hook.
@ -152,12 +161,18 @@ impl DesktopContext {
#[derive(Debug)]
pub enum UserWindowEvent {
Update,
EditsReady,
Initialize,
CloseWindow,
DragWindow,
FocusWindow,
/// Set a new Dioxus template for hot-reloading
///
/// Is a no-op in release builds. Must fit the right format for templates
SetTemplate(String),
Visible(bool),
Minimize(bool),
Maximize(bool),
@ -185,107 +200,104 @@ pub enum UserWindowEvent {
PopView,
}
pub(super) fn handler(
user_event: UserWindowEvent,
desktop: &mut DesktopController,
control_flow: &mut ControlFlow,
) {
// currently dioxus-desktop supports a single window only,
// so we can grab the only webview from the map;
// on wayland it is possible that a user event is emitted
// before the webview is initialized. ignore the event.
let webview = if let Some(webview) = desktop.webviews.values().next() {
webview
} else {
return;
};
let window = webview.window();
impl DesktopController {
pub(super) fn handle_event(
&mut self,
user_event: UserWindowEvent,
control_flow: &mut ControlFlow,
) {
// currently dioxus-desktop supports a single window only,
// so we can grab the only webview from the map;
// on wayland it is possible that a user event is emitted
// before the webview is initialized. ignore the event.
let webview = if let Some(webview) = self.webviews.values().next() {
webview
} else {
return;
};
match user_event {
Update => desktop.try_load_ready_webviews(),
CloseWindow => *control_flow = ControlFlow::Exit,
DragWindow => {
// if the drag_window has any errors, we don't do anything
window.fullscreen().is_none().then(|| window.drag_window());
}
Visible(state) => window.set_visible(state),
Minimize(state) => window.set_minimized(state),
Maximize(state) => window.set_maximized(state),
MaximizeToggle => window.set_maximized(!window.is_maximized()),
Fullscreen(state) => {
if let Some(handle) = window.current_monitor() {
window.set_fullscreen(state.then_some(WryFullscreen::Borderless(Some(handle))));
let window = webview.window();
match user_event {
Initialize | EditsReady => self.try_load_ready_webviews(),
SetTemplate(template) => self.set_template(template),
CloseWindow => *control_flow = ControlFlow::Exit,
DragWindow => {
// if the drag_window has any errors, we don't do anything
window.fullscreen().is_none().then(|| window.drag_window());
}
}
FocusWindow => window.set_focus(),
Resizable(state) => window.set_resizable(state),
AlwaysOnTop(state) => window.set_always_on_top(state),
CursorVisible(state) => window.set_cursor_visible(state),
CursorGrab(state) => {
let _ = window.set_cursor_grab(state);
}
SetTitle(content) => window.set_title(&content),
SetDecorations(state) => window.set_decorations(state),
SetZoomLevel(scale_factor) => webview.zoom(scale_factor),
Print => {
if let Err(e) = webview.print() {
// we can't panic this error.
log::warn!("Open print modal failed: {e}");
Visible(state) => window.set_visible(state),
Minimize(state) => window.set_minimized(state),
Maximize(state) => window.set_maximized(state),
MaximizeToggle => window.set_maximized(!window.is_maximized()),
Fullscreen(state) => {
if let Some(handle) = window.current_monitor() {
window.set_fullscreen(state.then_some(WryFullscreen::Borderless(Some(handle))));
}
}
}
DevTool => {
#[cfg(debug_assertions)]
webview.open_devtools();
#[cfg(not(debug_assertions))]
log::warn!("Devtools are disabled in release builds");
}
FocusWindow => window.set_focus(),
Resizable(state) => window.set_resizable(state),
AlwaysOnTop(state) => window.set_always_on_top(state),
Eval(code) => {
if let Err(e) = webview.evaluate_script(code.as_str()) {
// we can't panic this error.
log::warn!("Eval script error: {e}");
CursorVisible(state) => window.set_cursor_visible(state),
CursorGrab(state) => {
let _ = window.set_cursor_grab(state);
}
}
#[cfg(target_os = "ios")]
PushView(view) => unsafe {
use objc::runtime::Object;
use objc::*;
assert!(is_main_thread());
let ui_view = window.ui_view() as *mut Object;
let ui_view_frame: *mut Object = msg_send![ui_view, frame];
let _: () = msg_send![view, setFrame: ui_view_frame];
let _: () = msg_send![view, setAutoresizingMask: 31];
SetTitle(content) => window.set_title(&content),
SetDecorations(state) => window.set_decorations(state),
let ui_view_controller = window.ui_view_controller() as *mut Object;
let _: () = msg_send![ui_view_controller, setView: view];
desktop.views.push(ui_view);
},
SetZoomLevel(scale_factor) => webview.zoom(scale_factor),
Print => {
if let Err(e) = webview.print() {
// we can't panic this error.
log::warn!("Open print modal failed: {e}");
}
}
DevTool => {
#[cfg(debug_assertions)]
webview.open_devtools();
#[cfg(not(debug_assertions))]
log::warn!("Devtools are disabled in release builds");
}
Eval(code) => {
if let Err(e) = webview.evaluate_script(code.as_str()) {
// we can't panic this error.
log::warn!("Eval script error: {e}");
}
}
#[cfg(target_os = "ios")]
PushView(view) => unsafe {
use objc::runtime::Object;
use objc::*;
assert!(is_main_thread());
let ui_view = window.ui_view() as *mut Object;
let ui_view_frame: *mut Object = msg_send![ui_view, frame];
let _: () = msg_send![view, setFrame: ui_view_frame];
let _: () = msg_send![view, setAutoresizingMask: 31];
#[cfg(target_os = "ios")]
PopView => unsafe {
use objc::runtime::Object;
use objc::*;
assert!(is_main_thread());
if let Some(view) = desktop.views.pop() {
let ui_view_controller = window.ui_view_controller() as *mut Object;
let _: () = msg_send![ui_view_controller, setView: view];
}
},
desktop.views.push(ui_view);
},
#[cfg(target_os = "ios")]
PopView => unsafe {
use objc::runtime::Object;
use objc::*;
assert!(is_main_thread());
if let Some(view) = desktop.views.pop() {
let ui_view_controller = window.ui_view_controller() as *mut Object;
let _: () = msg_send![ui_view_controller, setView: view];
}
},
}
}
}
/// Get a closure that executes any JavaScript in the WebView context.
pub fn use_eval<S: std::string::ToString>(cx: &ScopeState) -> &dyn Fn(S) {
let desktop = use_window(cx).clone();
cx.use_hook(|| move |script| desktop.eval(script))
}
#[cfg(target_os = "ios")]
fn is_main_thread() -> bool {
use objc::runtime::{Class, BOOL, NO};

View file

@ -1,13 +1,10 @@
//! Convert a serialized event to an event trigger
use std::any::Any;
use std::sync::Arc;
use dioxus_core::ElementId;
use dioxus_core::{EventPriority, UserEvent};
use dioxus_html::event_bubbles;
use dioxus_html::on::*;
use dioxus_html::events::*;
use serde::{Deserialize, Serialize};
use serde_json::from_value;
use std::any::Any;
use std::rc::Rc;
#[derive(Deserialize, Serialize)]
pub(crate) struct IpcMessage {
@ -35,187 +32,60 @@ pub(crate) fn parse_ipc_message(payload: &str) -> Option<IpcMessage> {
}
}
#[derive(Deserialize, Serialize)]
struct ImEvent {
event: String,
mounted_dom_id: ElementId,
contents: serde_json::Value,
macro_rules! match_data {
(
$m:ident;
$name:ident;
$(
$tip:ty => $($mname:literal)|* ;
)*
) => {
match $name {
$( $($mname)|* => {
let val: $tip = from_value::<$tip>($m).ok()?;
Rc::new(val) as Rc<dyn Any>
})*
_ => return None,
}
};
}
pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent {
let ImEvent {
event,
mounted_dom_id,
contents,
} = serde_json::from_value(val).unwrap();
let mounted_dom_id = Some(mounted_dom_id);
let name = event_name_from_type(&event);
let event = make_synthetic_event(&event, contents);
UserEvent {
name,
priority: EventPriority::Low,
scope_id: None,
element: mounted_dom_id,
bubbles: event_bubbles(name),
data: event,
}
#[derive(Deserialize)]
pub struct EventMessage {
pub contents: serde_json::Value,
pub event: String,
pub mounted_dom_id: usize,
}
fn make_synthetic_event(name: &str, val: serde_json::Value) -> Arc<dyn Any + Send + Sync> {
match name {
"copy" | "cut" | "paste" => {
//
Arc::new(ClipboardData {})
}
"compositionend" | "compositionstart" | "compositionupdate" => {
Arc::new(serde_json::from_value::<CompositionData>(val).unwrap())
}
"keydown" | "keypress" | "keyup" => {
let evt = serde_json::from_value::<KeyboardData>(val).unwrap();
Arc::new(evt)
}
"focus" | "blur" | "focusout" | "focusin" => {
//
Arc::new(FocusData {})
}
pub fn decode_event(value: EventMessage) -> Option<Rc<dyn Any>> {
let val = value.contents;
let name = value.event.as_str();
type DragData = MouseData;
// todo: these handlers might get really slow if the input box gets large and allocation pressure is heavy
// don't have a good solution with the serialized event problem
"change" | "input" | "invalid" | "reset" | "submit" => {
Arc::new(serde_json::from_value::<FormData>(val).unwrap())
}
let evt = match_data! { val; name;
MouseData => "click" | "contextmenu" | "dblclick" | "doubleclick" | "mousedown" | "mouseenter" | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup";
ClipboardData => "copy" | "cut" | "paste";
CompositionData => "compositionend" | "compositionstart" | "compositionupdate";
KeyboardData => "keydown" | "keypress" | "keyup";
FocusData => "blur" | "focus" | "focusin" | "focusout";
FormData => "change" | "input" | "invalid" | "reset" | "submit";
DragData => "drag" | "dragend" | "dragenter" | "dragexit" | "dragleave" | "dragover" | "dragstart" | "drop";
PointerData => "pointerlockchange" | "pointerlockerror" | "pointerdown" | "pointermove" | "pointerup" | "pointerover" | "pointerout" | "pointerenter" | "pointerleave" | "gotpointercapture" | "lostpointercapture";
SelectionData => "selectstart" | "selectionchange" | "select";
TouchData => "touchcancel" | "touchend" | "touchmove" | "touchstart";
ScrollData => "scroll";
WheelData => "wheel";
MediaData => "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied"
| "encrypted" | "ended" | "interruptbegin" | "interruptend" | "loadeddata"
| "loadedmetadata" | "loadstart" | "pause" | "play" | "playing" | "progress"
| "ratechange" | "seeked" | "seeking" | "stalled" | "suspend" | "timeupdate"
| "volumechange" | "waiting" | "error" | "load" | "loadend" | "timeout";
AnimationData => "animationstart" | "animationend" | "animationiteration";
TransitionData => "transitionend";
ToggleData => "toggle";
// ImageData => "load" | "error";
// OtherData => "abort" | "afterprint" | "beforeprint" | "beforeunload" | "hashchange" | "languagechange" | "message" | "offline" | "online" | "pagehide" | "pageshow" | "popstate" | "rejectionhandled" | "storage" | "unhandledrejection" | "unload" | "userproximity" | "vrdisplayactivate" | "vrdisplayblur" | "vrdisplayconnect" | "vrdisplaydeactivate" | "vrdisplaydisconnect" | "vrdisplayfocus" | "vrdisplaypointerrestricted" | "vrdisplaypointerunrestricted" | "vrdisplaypresentchange";
};
"click" | "contextmenu" | "dblclick" | "doubleclick" | "drag" | "dragend" | "dragenter"
| "dragexit" | "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown"
| "mouseenter" | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => {
Arc::new(serde_json::from_value::<MouseData>(val).unwrap())
}
"pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture"
| "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => {
Arc::new(serde_json::from_value::<PointerData>(val).unwrap())
}
"select" => {
//
Arc::new(serde_json::from_value::<SelectionData>(val).unwrap())
}
"touchcancel" | "touchend" | "touchmove" | "touchstart" => {
Arc::new(serde_json::from_value::<TouchData>(val).unwrap())
}
"scroll" => Arc::new(ScrollData {}),
"wheel" => Arc::new(serde_json::from_value::<WheelData>(val).unwrap()),
"animationstart" | "animationend" | "animationiteration" => {
Arc::new(serde_json::from_value::<AnimationData>(val).unwrap())
}
"transitionend" => Arc::new(serde_json::from_value::<TransitionData>(val).unwrap()),
"abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted"
| "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play"
| "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend"
| "timeupdate" | "volumechange" | "waiting" => {
//
Arc::new(MediaData {})
}
"toggle" => Arc::new(ToggleData {}),
_ => Arc::new(()),
}
}
fn event_name_from_type(typ: &str) -> &'static str {
match typ {
"copy" => "copy",
"cut" => "cut",
"paste" => "paste",
"compositionend" => "compositionend",
"compositionstart" => "compositionstart",
"compositionupdate" => "compositionupdate",
"keydown" => "keydown",
"keypress" => "keypress",
"keyup" => "keyup",
"focus" => "focus",
"focusout" => "focusout",
"focusin" => "focusin",
"blur" => "blur",
"change" => "change",
"input" => "input",
"invalid" => "invalid",
"reset" => "reset",
"submit" => "submit",
"click" => "click",
"contextmenu" => "contextmenu",
"doubleclick" => "doubleclick",
"dblclick" => "dblclick",
"drag" => "drag",
"dragend" => "dragend",
"dragenter" => "dragenter",
"dragexit" => "dragexit",
"dragleave" => "dragleave",
"dragover" => "dragover",
"dragstart" => "dragstart",
"drop" => "drop",
"mousedown" => "mousedown",
"mouseenter" => "mouseenter",
"mouseleave" => "mouseleave",
"mousemove" => "mousemove",
"mouseout" => "mouseout",
"mouseover" => "mouseover",
"mouseup" => "mouseup",
"pointerdown" => "pointerdown",
"pointermove" => "pointermove",
"pointerup" => "pointerup",
"pointercancel" => "pointercancel",
"gotpointercapture" => "gotpointercapture",
"lostpointercapture" => "lostpointercapture",
"pointerenter" => "pointerenter",
"pointerleave" => "pointerleave",
"pointerover" => "pointerover",
"pointerout" => "pointerout",
"select" => "select",
"touchcancel" => "touchcancel",
"touchend" => "touchend",
"touchmove" => "touchmove",
"touchstart" => "touchstart",
"scroll" => "scroll",
"wheel" => "wheel",
"animationstart" => "animationstart",
"animationend" => "animationend",
"animationiteration" => "animationiteration",
"transitionend" => "transitionend",
"abort" => "abort",
"canplay" => "canplay",
"canplaythrough" => "canplaythrough",
"durationchange" => "durationchange",
"emptied" => "emptied",
"encrypted" => "encrypted",
"ended" => "ended",
"error" => "error",
"loadeddata" => "loadeddata",
"loadedmetadata" => "loadedmetadata",
"loadstart" => "loadstart",
"pause" => "pause",
"play" => "play",
"playing" => "playing",
"progress" => "progress",
"ratechange" => "ratechange",
"seeked" => "seeked",
"seeking" => "seeking",
"stalled" => "stalled",
"suspend" => "suspend",
"timeupdate" => "timeupdate",
"volumechange" => "volumechange",
"waiting" => "waiting",
"toggle" => "toggle",
a => {
panic!("unsupported event type {:?}", a);
}
}
Some(evt)
}

View file

@ -1,4 +1,7 @@
use dioxus_core::{SchedulerMsg, SetTemplateMsg, VirtualDom};
#![allow(dead_code)]
use dioxus_core::VirtualDom;
use interprocess::local_socket::{LocalSocketListener, LocalSocketStream};
use std::io::{BufRead, BufReader};
use std::time::Duration;
@ -10,32 +13,34 @@ fn handle_error(connection: std::io::Result<LocalSocketStream>) -> Option<LocalS
.ok()
}
pub(crate) fn init(dom: &VirtualDom) {
pub(crate) fn init(_dom: &VirtualDom) {
let latest_in_connection: Arc<Mutex<Option<BufReader<LocalSocketStream>>>> =
Arc::new(Mutex::new(None));
let latest_in_connection_handle = latest_in_connection.clone();
// connect to processes for incoming data
std::thread::spawn(move || {
if let Ok(listener) = LocalSocketListener::bind("@dioxusin") {
let temp_file = std::env::temp_dir().join("@dioxusin");
if let Ok(listener) = LocalSocketListener::bind(temp_file) {
for conn in listener.incoming().filter_map(handle_error) {
*latest_in_connection_handle.lock().unwrap() = Some(BufReader::new(conn));
}
}
});
let mut channel = dom.get_scheduler_channel();
std::thread::spawn(move || {
loop {
if let Some(conn) = &mut *latest_in_connection.lock().unwrap() {
let mut buf = String::new();
match conn.read_line(&mut buf) {
Ok(_) => {
let msg: SetTemplateMsg = serde_json::from_str(&buf).unwrap();
channel
.start_send(SchedulerMsg::SetTemplate(Box::new(msg)))
.unwrap();
todo!()
// let msg: SetTemplateMsg = serde_json::from_str(&buf).unwrap();
// channel
// .start_send(SchedulerMsg::SetTemplate(Box::new(msg)))
// .unwrap();
}
Err(err) => {
if err.kind() != std::io::ErrorKind::WouldBlock {

View file

@ -8,16 +8,20 @@ mod controller;
mod desktop_context;
mod escape;
mod events;
#[cfg(any(feature = "hot-reload", debug_assertions))]
mod hot_reload;
mod protocol;
#[cfg(all(feature = "hot-reload", debug_assertions))]
mod hot_reload;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use desktop_context::UserWindowEvent;
pub use desktop_context::{use_eval, use_window, DesktopContext};
use futures_channel::mpsc::UnboundedSender;
pub use wry;
pub use wry::application as tao;
use crate::events::trigger_from_serialized;
pub use cfg::Config;
use controller::DesktopController;
use dioxus_core::*;
@ -100,99 +104,133 @@ pub fn launch_cfg(root: Component, config_builder: Config) {
/// ```
pub fn launch_with_props<P: 'static + Send>(root: Component<P>, props: P, mut cfg: Config) {
let event_loop = EventLoop::with_user_event();
let mut desktop = DesktopController::new_on_tokio(root, props, event_loop.create_proxy());
let proxy = event_loop.create_proxy();
// We assume that if the icon is None, then the user just didnt set it
event_loop.run(move |window_event, event_loop, control_flow| {
*control_flow = ControlFlow::Wait;
match window_event {
Event::NewEvents(StartCause::Init) => desktop.start(&mut cfg, event_loop),
Event::WindowEvent {
event, window_id, ..
} => match event {
WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
WindowEvent::Destroyed { .. } => desktop.close_window(window_id, control_flow),
_ => {}
},
Event::UserEvent(user_event) => desktop.handle_event(user_event, control_flow),
Event::MainEventsCleared => {}
Event::Resumed => {}
Event::Suspended => {}
Event::LoopDestroyed => {}
Event::RedrawRequested(_id) => {}
_ => {}
}
})
}
impl DesktopController {
fn start(
&mut self,
cfg: &mut Config,
event_loop: &tao::event_loop::EventLoopWindowTarget<UserWindowEvent>,
) {
let webview = build_webview(
cfg,
event_loop,
self.is_ready.clone(),
self.proxy.clone(),
self.event_tx.clone(),
);
self.webviews.insert(webview.window().id(), webview);
}
}
fn build_webview(
cfg: &mut Config,
event_loop: &tao::event_loop::EventLoopWindowTarget<UserWindowEvent>,
is_ready: Arc<AtomicBool>,
proxy: tao::event_loop::EventLoopProxy<UserWindowEvent>,
event_tx: UnboundedSender<serde_json::Value>,
) -> wry::webview::WebView {
let builder = cfg.window.clone();
let window = builder.build(event_loop).unwrap();
let file_handler = cfg.file_drop_handler.take();
let custom_head = cfg.custom_head.clone();
let resource_dir = cfg.resource_dir.clone();
let index_file = cfg.custom_index.clone();
// We assume that if the icon is None in cfg, then the user just didnt set it
if cfg.window.window.window_icon.is_none() {
cfg.window.window.window_icon = Some(
window.set_window_icon(Some(
tao::window::Icon::from_rgba(
include_bytes!("./assets/default_icon.bin").to_vec(),
460,
460,
)
.expect("image parse failed"),
);
));
}
event_loop.run(move |window_event, event_loop, control_flow| {
*control_flow = ControlFlow::Wait;
match window_event {
Event::NewEvents(StartCause::Init) => {
let builder = cfg.window.clone();
let window = builder.build(event_loop).unwrap();
let window_id = window.id();
let (is_ready, sender) = (desktop.is_ready.clone(), desktop.sender.clone());
let proxy = proxy.clone();
let file_handler = cfg.file_drop_handler.take();
let custom_head = cfg.custom_head.clone();
let resource_dir = cfg.resource_dir.clone();
let index_file = cfg.custom_index.clone();
let mut webview = WebViewBuilder::new(window)
.unwrap()
.with_transparent(cfg.window.window.transparent)
.with_url("dioxus://index.html/")
.unwrap()
.with_ipc_handler(move |_window: &Window, payload: String| {
parse_ipc_message(&payload)
.map(|message| match message.method() {
"user_event" => {
let event = trigger_from_serialized(message.params());
log::trace!("User event: {:?}", event);
sender.unbounded_send(SchedulerMsg::Event(event)).unwrap();
let mut webview = WebViewBuilder::new(window)
.unwrap()
.with_transparent(cfg.window.window.transparent)
.with_url("dioxus://index.html/")
.unwrap()
.with_ipc_handler(move |_window: &Window, payload: String| {
parse_ipc_message(&payload)
.map(|message| match message.method() {
"user_event" => {
_ = event_tx.unbounded_send(message.params());
}
"initialize" => {
is_ready.store(true, std::sync::atomic::Ordering::Relaxed);
let _ = proxy.send_event(UserWindowEvent::EditsReady);
}
"browser_open" => {
let data = message.params();
log::trace!("Open browser: {:?}", data);
if let Some(temp) = data.as_object() {
if temp.contains_key("href") {
let url = temp.get("href").unwrap().as_str().unwrap();
if let Err(e) = webbrowser::open(url) {
log::error!("Open Browser error: {:?}", e);
}
"initialize" => {
is_ready.store(true, std::sync::atomic::Ordering::Relaxed);
let _ = proxy.send_event(UserWindowEvent::Update);
}
"browser_open" => {
let data = message.params();
log::trace!("Open browser: {:?}", data);
if let Some(temp) = data.as_object() {
if temp.contains_key("href") {
let url = temp.get("href").unwrap().as_str().unwrap();
if let Err(e) = webbrowser::open(url) {
log::error!("Open Browser error: {:?}", e);
}
}
}
}
_ => (),
})
.unwrap_or_else(|| {
log::warn!("invalid IPC message received");
});
})
.with_custom_protocol(String::from("dioxus"), move |r| {
protocol::desktop_handler(
r,
resource_dir.clone(),
custom_head.clone(),
index_file.clone(),
)
})
.with_file_drop_handler(move |window, evet| {
file_handler
.as_ref()
.map(|handler| handler(window, evet))
.unwrap_or_default()
});
}
}
}
_ => (),
})
.unwrap_or_else(|| {
log::warn!("invalid IPC message received");
});
})
.with_custom_protocol(String::from("dioxus"), move |r| {
protocol::desktop_handler(
r,
resource_dir.clone(),
custom_head.clone(),
index_file.clone(),
)
})
.with_file_drop_handler(move |window, evet| {
file_handler
.as_ref()
.map(|handler| handler(window, evet))
.unwrap_or_default()
});
for (name, handler) in cfg.protocols.drain(..) {
webview = webview.with_custom_protocol(name, handler)
}
for (name, handler) in cfg.protocols.drain(..) {
webview = webview.with_custom_protocol(name, handler)
}
if cfg.disable_context_menu {
// in release mode, we don't want to show the dev tool or reload menus
webview = webview.with_initialization_script(
r#"
if cfg.disable_context_menu {
// in release mode, we don't want to show the dev tool or reload menus
webview = webview.with_initialization_script(
r#"
if (document.addEventListener) {
document.addEventListener('contextmenu', function(e) {
e.preventDefault();
@ -203,32 +241,11 @@ pub fn launch_with_props<P: 'static + Send>(root: Component<P>, props: P, mut cf
});
}
"#,
)
} else {
// in debug, we are okay with the reload menu showing and dev tool
webview = webview.with_devtools(true);
}
)
} else {
// in debug, we are okay with the reload menu showing and dev tool
webview = webview.with_devtools(true);
}
desktop.webviews.insert(window_id, webview.build().unwrap());
}
Event::WindowEvent {
event, window_id, ..
} => match event {
WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
WindowEvent::Destroyed { .. } => desktop.close_window(window_id, control_flow),
_ => {}
},
Event::UserEvent(user_event) => {
desktop_context::handler(user_event, &mut desktop, control_flow)
}
Event::MainEventsCleared => {}
Event::Resumed => {}
Event::Suspended => {}
Event::LoopDestroyed => {}
Event::RedrawRequested(_id) => {}
_ => {}
}
})
webview.build().unwrap()
}

View file

@ -7,7 +7,7 @@ use wry::{
const MODULE_LOADER: &str = r#"
<script>
import("./index.js").then(function (module) {
module.main();
module.main();
});
</script>
"#;

View file

@ -23,9 +23,7 @@ default = ["macro", "hooks", "html"]
macro = ["dioxus-core-macro", "dioxus-rsx"]
html = ["dioxus-html"]
hooks = ["dioxus-hooks"]
hot-reload = [
"dioxus-core/hot-reload",
]
[dev-dependencies]
futures-util = "0.3.21"
@ -34,10 +32,9 @@ rand = { version = "0.8.4", features = ["small_rng"] }
criterion = "0.3.5"
thiserror = "1.0.30"
env_logger = "0.9.0"
tokio = { version = "1.21.2", features = ["full"] }
# dioxus-edit-stream = { path = "../edit-stream" }
[[bench]]
name = "create"
harness = false
[[bench]]
name = "jsframework"

View file

@ -1 +0,0 @@
fn main() {}

View file

@ -5,9 +5,14 @@
//! to be made, but the change application phase will be just as performant as the vanilla wasm_bindgen code. In essence,
//! we are measuring the overhead of Dioxus, not the performance of the "apply" phase.
//!
//! On my MBP 2019:
//! - Dioxus takes 3ms to create 1_000 rows
//! - Dioxus takes 30ms to create 10_000 rows
//!
//! Pre-templates (Mac M1):
//! - 3ms to create 1_000 rows
//! - 30ms to create 10_000 rows
//!
//! Post-templates
//! - 580us to create 1_000 rows
//! - 6.2ms to create 10_000 rows
//!
//! As pure "overhead", these are amazing good numbers, mostly slowed down by hitting the global allocator.
//! These numbers don't represent Dioxus with the heuristic engine installed, so I assume it'll be even faster.
@ -20,25 +25,26 @@ criterion_group!(mbenches, create_rows);
criterion_main!(mbenches);
fn create_rows(c: &mut Criterion) {
static App: Component = |cx| {
fn app(cx: Scope) -> Element {
let mut rng = SmallRng::from_entropy();
render!(table {
tbody {
(0..10_000_usize).map(|f| {
let label = Label::new(&mut rng);
rsx!(Row {
row_id: f,
label: label
render!(
table {
tbody {
(0..10_000_usize).map(|f| {
let label = Label::new(&mut rng);
rsx!( Row { row_id: f, label: label } )
})
})
}
}
})
};
)
}
c.bench_function("create rows", |b| {
let mut dom = VirtualDom::new(app);
dom.rebuild();
b.iter(|| {
let mut dom = VirtualDom::new(App);
let g = dom.rebuild();
assert!(g.edits.len() > 1);
})

View file

@ -0,0 +1,101 @@
use dioxus::prelude::*;
use rand::prelude::*;
fn main() {
let mut dom = VirtualDom::new(app);
dom.rebuild();
for _ in 0..1000 {
dom.rebuild();
}
}
fn app(cx: Scope) -> Element {
let mut rng = SmallRng::from_entropy();
render! (
table {
tbody {
(0..10_000_usize).map(|f| {
let label = Label::new(&mut rng);
rsx!( Row { row_id: f, label: label } )
})
}
}
)
}
#[derive(PartialEq, Props)]
struct RowProps {
row_id: usize,
label: Label,
}
fn Row(cx: Scope<RowProps>) -> Element {
let [adj, col, noun] = cx.props.label.0;
render! {
tr {
td { class:"col-md-1", "{cx.props.row_id}" }
td { class:"col-md-1", onclick: move |_| { /* run onselect */ },
a { class: "lbl", "{adj}" "{col}" "{noun}" }
}
td { class: "col-md-1",
a { class: "remove", onclick: move |_| {/* remove */},
span { class: "glyphicon glyphicon-remove remove", aria_hidden: "true" }
}
}
td { class: "col-md-6" }
}
}
}
#[derive(PartialEq)]
struct Label([&'static str; 3]);
impl Label {
fn new(rng: &mut SmallRng) -> Self {
Label([
ADJECTIVES.choose(rng).unwrap(),
COLOURS.choose(rng).unwrap(),
NOUNS.choose(rng).unwrap(),
])
}
}
static ADJECTIVES: &[&str] = &[
"pretty",
"large",
"big",
"small",
"tall",
"short",
"long",
"handsome",
"plain",
"quaint",
"clean",
"elegant",
"easy",
"angry",
"crazy",
"helpful",
"mushy",
"odd",
"unsightly",
"adorable",
"important",
"inexpensive",
"cheap",
"expensive",
"fancy",
];
static COLOURS: &[&str] = &[
"red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black",
"orange",
];
static NOUNS: &[&str] = &[
"table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger",
"pizza", "mouse", "keyboard",
];

Some files were not shown because too many files have changed in this diff Show more