mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-26 14:10:20 +00:00
Merge pull request #600 from DioxusLabs/jk/templates-v3
Template architecture, async components, inline iterators, error boundaries, multiple renderers
This commit is contained in:
commit
2b4d19247c
220 changed files with 16174 additions and 18848 deletions
15
Cargo.toml
15
Cargo.toml
|
@ -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
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use dioxus::{events::*, prelude::*};
|
||||
use dioxus::{events::*, html::MouseEvent, prelude::*};
|
||||
|
||||
fn main() {
|
||||
dioxus_desktop::launch(app);
|
||||
|
|
|
@ -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 }
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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
22
examples/callback.rs
Normal 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!" }
|
||||
})
|
||||
}
|
|
@ -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!()
|
||||
}
|
||||
|
|
|
@ -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" } },
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ fn app(cx: Scope) -> Element {
|
|||
cx.render(rsx! {
|
||||
div { "hello {name}!" }
|
||||
Child {}
|
||||
ChildWithRef{}
|
||||
ChildWithRef {}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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); },
|
||||
|
|
13
examples/generic_component.rs
Normal file
13
examples/generic_component.rs
Normal 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 {} })
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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 {}
|
||||
}
|
||||
))
|
||||
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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>()
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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!" }
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = []
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
73
packages/core/src/any_props.rs
Normal file
73
packages/core/src/any_props.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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
163
packages/core/src/arena.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
29
packages/core/src/bump_frame.rs
Normal file
29
packages/core/src/bump_frame.rs
Normal 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
401
packages/core/src/create.rs
Normal 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
19
packages/core/src/dirty_scope.rs
Normal file
19
packages/core/src/dirty_scope.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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]
|
||||
}
|
||||
}
|
19
packages/core/src/error_boundary.rs
Normal file
19
packages/core/src/error_boundary.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
103
packages/core/src/fragment.rs
Normal file
103
packages/core/src/fragment.rs
Normal 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
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
)*}
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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()
|
||||
}
|
||||
|
|
48
packages/core/src/scheduler/mod.rs
Normal file
48
packages/core/src/scheduler/mod.rs
Normal 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()),
|
||||
})
|
||||
}
|
||||
}
|
52
packages/core/src/scheduler/suspense.rs
Normal file
52
packages/core/src/scheduler/suspense.rs
Normal 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));
|
||||
}
|
||||
}
|
66
packages/core/src/scheduler/task.rs
Normal file
66
packages/core/src/scheduler/task.rs
Normal 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));
|
||||
}
|
||||
}
|
106
packages/core/src/scheduler/wait.rs
Normal file
106
packages/core/src/scheduler/wait.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
36
packages/core/src/scheduler/waker.rs
Normal file
36
packages/core/src/scheduler/waker.rs
Normal 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)
|
||||
}
|
||||
}
|
146
packages/core/src/scope_arena.rs
Normal file
146
packages/core/src/scope_arena.rs
Normal 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
30
packages/core/src/subtree.rs
Normal file
30
packages/core/src/subtree.rs
Normal 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
|
@ -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
2
packages/core/tests/.rustfmt.toml
Normal file
2
packages/core/tests/.rustfmt.toml
Normal file
|
@ -0,0 +1,2 @@
|
|||
struct_variant_width = 100
|
||||
struct_lit_width = 80
|
|
@ -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
|
75
packages/core/tests/attr_cleanup.rs
Normal file
75
packages/core/tests/attr_cleanup.rs
Normal 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 }
|
||||
]
|
||||
);
|
||||
}
|
15
packages/core/tests/boolattrs.rs
Normal file
15
packages/core/tests/boolattrs.rs
Normal 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) },
|
||||
]
|
||||
)
|
||||
}
|
55
packages/core/tests/borrowedstate.rs
Normal file
55
packages/core/tests/borrowedstate.rs
Normal 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}!" }))
|
||||
}
|
28
packages/core/tests/bubble_error.rs
Normal file
28
packages/core/tests/bubble_error.rs
Normal 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();
|
||||
}
|
51
packages/core/tests/context_api.rs
Normal file
51
packages/core/tests/context_api.rs
Normal 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,) },]
|
||||
);
|
||||
}
|
197
packages/core/tests/create_dom.rs
Normal file
197
packages/core/tests/create_dom.rs
Normal 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) }
|
||||
]
|
||||
)
|
||||
}
|
32
packages/core/tests/create_element.rs
Normal file
32
packages/core/tests/create_element.rs
Normal 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 }
|
||||
// ]
|
||||
// )
|
||||
}
|
104
packages/core/tests/create_fragments.rs
Normal file
104
packages/core/tests/create_fragments.rs
Normal 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 }
|
||||
);
|
||||
}
|
72
packages/core/tests/create_lists.rs
Normal file
72
packages/core/tests/create_lists.rs
Normal 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) }
|
||||
],
|
||||
)
|
||||
}
|
106
packages/core/tests/create_passthru.rs
Normal file
106
packages/core/tests/create_passthru.rs
Normal 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 }
|
||||
]
|
||||
)
|
||||
}
|
51
packages/core/tests/cycle.rs
Normal file
51
packages/core/tests/cycle.rs
Normal 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 },
|
||||
]
|
||||
);
|
||||
}
|
102
packages/core/tests/diff_component.rs
Normal file
102
packages/core/tests/diff_component.rs
Normal 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 }
|
||||
]
|
||||
);
|
||||
}
|
84
packages/core/tests/diff_element.rs
Normal file
84
packages/core/tests/diff_element.rs
Normal 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 },
|
||||
]
|
||||
);
|
||||
}
|
356
packages/core/tests/diff_keyed_list.rs
Normal file
356
packages/core/tests/diff_keyed_list.rs
Normal 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 }
|
||||
]
|
||||
);
|
||||
}
|
379
packages/core/tests/diff_unkeyed_list.rs
Normal file
379
packages/core/tests/diff_unkeyed_list.rs
Normal 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 },
|
||||
]
|
||||
)
|
||||
}
|
1
packages/core/tests/hotreloading.rs
Normal file
1
packages/core/tests/hotreloading.rs
Normal file
|
@ -0,0 +1 @@
|
|||
//! It should be possible to swap out templates at runtime, enabling hotreloading
|
42
packages/core/tests/kitchen_sink.rs
Normal file
42
packages/core/tests/kitchen_sink.rs
Normal 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 }
|
||||
],
|
||||
);
|
||||
}
|
168
packages/core/tests/lifecycle.rs
Normal file
168
packages/core/tests/lifecycle.rs
Normal 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) },
|
||||
// ]
|
||||
// );
|
||||
// }
|
49
packages/core/tests/miri_full_app.rs
Normal file
49
packages/core/tests/miri_full_app.rs
Normal 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}"
|
||||
}
|
||||
})
|
||||
}
|
127
packages/core/tests/miri_simple.rs
Normal file
127
packages/core/tests/miri_simple.rs
Normal 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();
|
||||
}
|
351
packages/core/tests/miri_stress.rs
Normal file
351
packages/core/tests/miri_stress.rs
Normal 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);
|
||||
// }
|
||||
// }
|
24
packages/core/tests/safety.rs
Normal file
24
packages/core/tests/safety.rs
Normal 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());
|
||||
}
|
92
packages/core/tests/suspense.rs
Normal file
92
packages/core/tests/suspense.rs
Normal 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}" } ))
|
||||
}
|
42
packages/core/tests/task.rs
Normal file
42
packages/core/tests/task.rs
Normal 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!(()))
|
||||
}
|
|
@ -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" }
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ use wry::{
|
|||
const MODULE_LOADER: &str = r#"
|
||||
<script>
|
||||
import("./index.js").then(function (module) {
|
||||
module.main();
|
||||
module.main();
|
||||
});
|
||||
</script>
|
||||
"#;
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
fn main() {}
|
|
@ -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);
|
||||
})
|
||||
|
|
101
packages/dioxus/examples/stress.rs
Normal file
101
packages/dioxus/examples/stress.rs
Normal 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
Loading…
Reference in a new issue