diff --git a/Cargo.toml b/Cargo.toml index 2b67e1b1a..50a0bfbf6 100644 --- a/Cargo.toml +++ b/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 \ No newline at end of file +debug = true diff --git a/docs/guide/examples/event_handler_prop.rs b/docs/guide/examples/event_handler_prop.rs index a333f86fe..2257ce25b 100644 --- a/docs/guide/examples/event_handler_prop.rs +++ b/docs/guide/examples/event_handler_prop.rs @@ -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> { diff --git a/docs/guide/examples/meme_editor.rs b/docs/guide/examples/meme_editor.rs index 67bef123b..ecf75ccbc 100644 --- a/docs/guide/examples/meme_editor.rs +++ b/docs/guide/examples/meme_editor.rs @@ -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; diff --git a/docs/guide/examples/meme_editor_dark_mode.rs b/docs/guide/examples/meme_editor_dark_mode.rs index 85fed5664..41283df79 100644 --- a/docs/guide/examples/meme_editor_dark_mode.rs +++ b/docs/guide/examples/meme_editor_dark_mode.rs @@ -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); diff --git a/examples/all_css.rs b/examples/all_css.rs index 768383b78..5e646a284 100644 --- a/examples/all_css.rs +++ b/examples/all_css.rs @@ -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" } }) } diff --git a/examples/all_events.rs b/examples/all_events.rs index d2c2ef0d7..493df0c86 100644 --- a/examples/all_events.rs +++ b/examples/all_events.rs @@ -1,4 +1,4 @@ -use dioxus::{events::*, prelude::*}; +use dioxus::{events::*, html::MouseEvent, prelude::*}; fn main() { dioxus_desktop::launch(app); diff --git a/examples/borrowed.rs b/examples/borrowed.rs index 7de2ff80d..95641bbbc 100644 --- a/examples/borrowed.rs +++ b/examples/borrowed.rs @@ -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 } }) } diff --git a/examples/calculator.rs b/examples/calculator.rs index 4f5d7dedd..93e298010 100644 --- a/examples/calculator.rs +++ b/examples/calculator.rs @@ -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()))), "=" } } diff --git a/examples/callback.rs b/examples/callback.rs new file mode 100644 index 000000000..bf2e3e3a0 --- /dev/null +++ b/examples/callback.rs @@ -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!" } + }) +} diff --git a/examples/custom_element.rs b/examples/custom_element.rs index 1c870e157..19bfa0fd9 100644 --- a/examples/custom_element.rs +++ b/examples/custom_element.rs @@ -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!() } diff --git a/examples/dog_app.rs b/examples/dog_app.rs index 5c78ed0bf..363d85f4b 100644 --- a/examples/dog_app.rs +++ b/examples/dog_app.rs @@ -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>, } -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" } }, + } } diff --git a/examples/error_handle.rs b/examples/error_handle.rs index 1d5d82b6d..5c64915cd 100644 --- a/examples/error_handle.rs +++ b/examples/error_handle.rs @@ -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}" + } }) } diff --git a/examples/eval.rs b/examples/eval.rs index 495288fcb..ad7a493f0 100644 --- a/examples/eval.rs +++ b/examples/eval.rs @@ -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" } } diff --git a/examples/fermi.rs b/examples/fermi.rs index 06bc38fc5..de5fb6d68 100644 --- a/examples/fermi.rs +++ b/examples/fermi.rs @@ -15,7 +15,7 @@ fn app(cx: Scope) -> Element { cx.render(rsx! { div { "hello {name}!" } Child {} - ChildWithRef{} + ChildWithRef {} }) } diff --git a/examples/file_explorer.rs b/examples/file_explorer.rs index e0acb4ae6..16b3b5faf 100644 --- a/examples/file_explorer.rs +++ b/examples/file_explorer.rs @@ -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 { diff --git a/examples/framework_benchmark.rs b/examples/framework_benchmark.rs index a88946af4..df5884b29 100644 --- a/examples/framework_benchmark.rs +++ b/examples/framework_benchmark.rs @@ -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); }, diff --git a/examples/generic_component.rs b/examples/generic_component.rs new file mode 100644 index 000000000..9757b1032 --- /dev/null +++ b/examples/generic_component.rs @@ -0,0 +1,13 @@ +use dioxus::prelude::*; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { generic_child::{} }) +} + +fn generic_child(cx: Scope) -> Element { + cx.render(rsx! { div {} }) +} diff --git a/examples/hydration.rs b/examples/hydration.rs index 692944f6b..b525a3fbc 100644 --- a/examples/hydration.rs +++ b/examples/hydration.rs @@ -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)); } diff --git a/examples/inputs.rs b/examples/inputs.rs index 9b6ec104f..223e3a7e7 100644 --- a/examples/inputs.rs +++ b/examples/inputs.rs @@ -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 {} } - )) - + }) } }) } diff --git a/examples/login_form.rs b/examples/login_form.rs index a0bc85a17..74e634d5d 100644 --- a/examples/login_form.rs +++ b/examples/login_form.rs @@ -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
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" } diff --git a/examples/nested_listeners.rs b/examples/nested_listeners.rs index 163a827a2..3938959a7 100644 --- a/examples/nested_listeners.rs +++ b/examples/nested_listeners.rs @@ -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" } diff --git a/examples/pattern_model.rs b/examples/pattern_model.rs index 7feff27b6..9d835d255 100644 --- a/examples/pattern_model.rs +++ b/examples/pattern_model.rs @@ -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}; diff --git a/examples/router.rs b/examples/router.rs index 20b168670..a4118111a 100644 --- a/examples/router.rs +++ b/examples/router.rs @@ -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::() diff --git a/examples/rsx_compile_fail.rs b/examples/rsx_compile_fail.rs index 2859dc53e..375219609 100644 --- a/examples/rsx_compile_fail.rs +++ b/examples/rsx_compile_fail.rs @@ -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 { diff --git a/examples/rsx_usage.rs b/examples/rsx_usage.rs index a610fe334..99598479f 100644 --- a/examples/rsx_usage.rs +++ b/examples/rsx_usage.rs @@ -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::> {} + TypedInput:: {} // Type inference can be used too TypedInput { initial: 10.0 } diff --git a/examples/simple_list.rs b/examples/simple_list.rs index d4616c19b..84cd9db71 100644 --- a/examples/simple_list.rs +++ b/examples/simple_list.rs @@ -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::>(), - - // 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::>() + .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!" } } diff --git a/examples/ssr.rs b/examples/ssr.rs index 7d3e5b615..0bdebc419 100644 --- a/examples/ssr.rs +++ b/examples/ssr.rs @@ -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); } diff --git a/examples/svg.rs b/examples/svg.rs index b331e7a6a..631a38030 100644 --- a/examples/svg.rs +++ b/examples/svg.rs @@ -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 } - } + }) } diff --git a/examples/todomvc.rs b/examples/todomvc.rs index 71ceee8e1..b5277133e 100644 --- a/examples/todomvc.rs +++ b/examples/todomvc.rs @@ -22,10 +22,10 @@ pub struct TodoItem { } pub fn app(cx: Scope<()>) -> Element { - let todos = use_state(&cx, im_rc::HashMap::::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::::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]; diff --git a/examples/window_event.rs b/examples/window_event.rs index 8805bcfdd..cd752fa17 100644 --- a/examples/window_event.rs +++ b/examples/window_event.rs @@ -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" } diff --git a/packages/core-macro/src/inlineprops.rs b/packages/core-macro/src/inlineprops.rs index 05d9d5240..dc9c4711b 100644 --- a/packages/core-macro/src/inlineprops.rs +++ b/packages/core-macro/src/inlineprops.rs @@ -9,6 +9,7 @@ use syn::{ pub struct InlinePropsBody { pub attrs: Vec, pub vis: syn::Visibility, + pub maybe_async: Option, pub fn_token: Token![fn], pub ident: Ident, pub cx_token: Box, @@ -25,6 +26,7 @@ pub struct InlinePropsBody { impl Parse for InlinePropsBody { fn parse(input: ParseStream) -> Result { let attrs: Vec = input.call(Attribute::parse_outer)?; + let maybe_async: Option = 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; diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index de0155e14..475000016 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -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::(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::(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(), } } diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs index 56a3407f7..b614acb43 100644 --- a/packages/core-macro/src/props/mod.rs +++ b/packages/core-macro/src/props/mod.rs @@ -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 diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 0191ffa61..f779e6ba2 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -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 = [] diff --git a/packages/core/README.md b/packages/core/README.md index 9b0b14fff..d3ad1946f 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -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`. 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. - diff --git a/packages/core/architecture.md b/packages/core/architecture.md index 87c3cf4a6..84fea3917 100644 --- a/packages/core/architecture.md +++ b/packages/core/architecture.md @@ -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 diff --git a/packages/core/src/any_props.rs b/packages/core/src/any_props.rs new file mode 100644 index 000000000..c3ea4dc2c --- /dev/null +++ b/packages/core/src/any_props.rs @@ -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, +} + +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

= cx.bump().alloc(Scoped { + props: &self.props, + scope: cx, + }); + + // Call the render function directly + (self.render_fn)(scope).into_return(cx) + } +} diff --git a/packages/core/src/arbitrary_value.rs b/packages/core/src/arbitrary_value.rs deleted file mode 100644 index dbf92c16f..000000000 --- a/packages/core/src/arbitrary_value.rs +++ /dev/null @@ -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::() { - if let Some(b) = b.as_any().downcast_ref::() { - 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(&self, _serializer: S) -> Result - 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(_deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - panic!("ArbitraryAttributeValue is not deserializable!") - } -} -#[cfg(feature = "serialize")] -impl<'de, 'a> serde::Deserialize<'de> for ArbitraryAttributeValue<'a> { - fn deserialize(_deserializer: D) -> Result - 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 AnyClone for T { - fn as_any(&self) -> &dyn std::any::Any { - self - } -} -#[cfg(not(any(feature = "hot-reload", debug_assertions)))] -impl 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, - 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(&self, _serializer: S) -> Result - where - S: serde::Serializer, - { - panic!("OwnedArbitraryAttributeValue should not be serialized") - } -} -#[cfg(feature = "serialize")] -impl<'de> serde::Deserialize<'de> for &OwnedArbitraryAttributeValue { - fn deserialize(_deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - panic!("OwnedArbitraryAttributeValue is not deserializable!") - } -} -#[cfg(feature = "serialize")] -impl<'de> serde::Deserialize<'de> for OwnedArbitraryAttributeValue { - fn deserialize(_deserializer: D) -> Result - 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 { - match self { - AttributeValue::Float32(f) => Some(*f), - _ => None, - } - } - - pub fn as_float64(&self) -> Option { - match self { - AttributeValue::Float64(f) => Some(*f), - _ => None, - } - } - - pub fn as_int32(&self) -> Option { - match self { - AttributeValue::Int32(i) => Some(*i), - _ => None, - } - } - - pub fn as_int64(&self) -> Option { - match self { - AttributeValue::Int64(i) => Some(*i), - _ => None, - } - } - - pub fn as_uint32(&self) -> Option { - match self { - AttributeValue::Uint32(i) => Some(*i), - _ => None, - } - } - - pub fn as_uint64(&self) -> Option { - match self { - AttributeValue::Uint64(i) => Some(*i), - _ => None, - } - } - - pub fn as_bool(&self) -> Option { - 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), - // TODO: support other types - Any(OwnedArbitraryAttributeValue), -} - -impl PartialEq> 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> 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 { - match self { - OwnedAttributeValue::Float32(f) => Some(*f), - _ => None, - } - } - - pub fn as_float64(&self) -> Option { - match self { - OwnedAttributeValue::Float64(f) => Some(*f), - _ => None, - } - } - - pub fn as_int32(&self) -> Option { - match self { - OwnedAttributeValue::Int32(i) => Some(*i), - _ => None, - } - } - - pub fn as_int64(&self) -> Option { - match self { - OwnedAttributeValue::Int64(i) => Some(*i), - _ => None, - } - } - - pub fn as_uint32(&self) -> Option { - match self { - OwnedAttributeValue::Uint32(i) => Some(*i), - _ => None, - } - } - - pub fn as_uint64(&self) -> Option { - match self { - OwnedAttributeValue::Uint64(i) => Some(*i), - _ => None, - } - } - - pub fn as_bool(&self) -> Option { - 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, - } - } -} diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs new file mode 100644 index 000000000..98960fca7 --- /dev/null +++ b/packages/core/src/arena.rs @@ -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 { + 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, + } + } +} diff --git a/packages/core/src/bump_frame.rs b/packages/core/src/bump_frame.rs new file mode 100644 index 000000000..44ea4f2d6 --- /dev/null +++ b/packages/core/src/bump_frame.rs @@ -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) } + } +} diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs new file mode 100644 index 000000000..cc63a0e8b --- /dev/null +++ b/packages/core/src/create.rs @@ -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, + 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::>() { + 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>( + 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 + } +} diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index ed92781c9..0384192e2 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -1,859 +1,390 @@ -#![warn(clippy::pedantic)] -#![allow(clippy::cast_possible_truncation)] - -//! This module contains the stateful [`DiffState`] and all methods to diff [`VNode`]s, their properties, and their children. -//! -//! The [`DiffState`] calculates the diffs between the old and new frames, updates the new nodes, and generates a set -//! of mutations for the renderer to apply. -//! -//! ## Notice: -//! -//! The inspiration and code for this module was originally taken from Dodrio (@fitzgen) and then modified to support -//! Components, Fragments, Suspense, `SubTree` memoization, incremental diffing, cancellation, pausing, priority -//! scheduling, and additional batching operations. -//! -//! ## Implementation Details: -//! -//! ### IDs for elements -//! -------------------- -//! All nodes are addressed by their IDs. -//! We don't necessarily require that DOM changes happen instantly during the diffing process, so the implementor may choose -//! to batch nodes if it is more performant for their application. The element IDs are indices into the internal element -//! array. The expectation is that implementors will use the ID as an index into a Vec of real nodes, allowing for passive -//! garbage collection as the [`crate::VirtualDom`] replaces old nodes. -//! -//! When new vnodes are created through `cx.render`, they won't know which real node they correspond to. During diffing, -//! we always make sure to copy over the ID. If we don't do this properly, the [`ElementId`] will be populated incorrectly -//! and brick the user's page. -//! -//! ### Fragment Support -//! -------------------- -//! Fragments (nodes without a parent) are supported through a combination of "replace with" and anchor vnodes. Fragments -//! can be particularly challenging when they are empty, so the anchor node lets us "reserve" a spot for the empty -//! fragment to be replaced with when it is no longer empty. This is guaranteed by logic in the [`crate::innerlude::NodeFactory`] - it is -//! impossible to craft a fragment with 0 elements - they must always have at least a single placeholder element. Adding -//! "dummy" nodes _is_ inefficient, but it makes our diffing algorithm faster and the implementation is completely up to -//! the platform. -//! -//! Other implementations either don't support fragments or use a "child + sibling" pattern to represent them. Our code is -//! vastly simpler and more performant when we can just create a placeholder element while the fragment has no children. -//! -//! ### Suspense -//! ------------ -//! Dioxus implements Suspense slightly differently than React. In React, each fiber is manually progressed until it runs -//! into a promise-like value. React will then work on the next "ready" fiber, checking back on the previous fiber once -//! it has finished its new work. In Dioxus, we use a similar approach, but try to completely render the tree before -//! switching sub-fibers. Instead, each future is submitted into a futures-queue and the node is manually loaded later on. -//! Due to the frequent calls to [`crate::virtual_dom::VirtualDom::work_with_deadline`] we can get the pure "fetch-as-you-render" behavior of React Fiber. -//! -//! We're able to use this approach because we use placeholder nodes - futures that aren't ready still get submitted to -//! DOM, but as a placeholder. -//! -//! Right now, the "suspense" queue is intertwined with hooks. In the future, we should allow any future to drive attributes -//! and contents, without the need for a `use_suspense` hook. In the interim, this is the quickest way to get Suspense working. -//! -//! ## Subtree Memoization -//! ----------------------- -//! We also employ "subtree memoization" which saves us from having to check trees which hold no dynamic content. We can -//! detect if a subtree is "static" by checking if its children are "static". Since we dive into the tree depth-first, the -//! calls to "create" propagate this information upwards. Structures like the one below are entirely static: -//! ```rust, ignore -//! rsx!( div { class: "hello world", "this node is entirely static" } ) -//! ``` -//! Because the subtrees won't be diffed, their "real node" data will be stale (invalid), so it's up to the reconciler to -//! track nodes created in a scope and clean up all relevant data. Support for this is currently WIP and depends on comp-time -//! hashing of the subtree from the rsx! macro. We do a very limited form of static analysis via static string pointers as -//! a way of short-circuiting the most expensive checks. -//! -//! ## Bloom Filter and Heuristics -//! ------------------------------ -//! For all components, we employ some basic heuristics to speed up allocations and pre-size bump arenas. The heuristics are -//! currently very rough, but will get better as time goes on. The information currently tracked includes the size of a -//! bump arena after first render, the number of hooks, and the number of nodes in the tree. -//! -//! ## Garbage Collection -//! --------------------- -//! Dioxus uses a passive garbage collection system to clean up old nodes once the work has been completed. This garbage -//! collection is done internally once the main diffing work is complete. After the "garbage" is collected, Dioxus will then -//! start to re-use old keys for new nodes. This results in a passive memory management system that is very efficient. -//! -//! The IDs used by the key/map are just an index into a Vec. This means that Dioxus will drive the key allocation strategy -//! so the client only needs to maintain a simple list of nodes. By default, Dioxus will not manually clean up old nodes -//! for the client. As new nodes are created, old nodes will be over-written. -//! -//! ## Further Reading and Thoughts -//! ---------------------------- -//! There are more ways of increasing diff performance here that are currently not implemented. -//! - Strong memoization of subtrees. -//! - Guided diffing. -//! - Certain web-dom-specific optimizations. -//! -//! More info on how to improve this diffing algorithm: -//! - +use std::cell::Cell; use crate::{ - dynamic_template_context::TemplateContext, - innerlude::{ - AnyProps, ElementId, Mutations, ScopeArena, ScopeId, VComponent, VElement, VFragment, - VNode, VPlaceholder, VText, - }, - template::{ - Template, TemplateAttribute, TemplateElement, TemplateNode, TemplateNodeId, - TemplateNodeType, TemplateValue, TextTemplateSegment, VTemplateRef, - }, - Attribute, TemplateAttributeValue, + arena::ElementId, + innerlude::{DirtyScope, VComponent, VText}, + mutations::Mutation, + nodes::RenderReturn, + nodes::{DynamicNode, VNode}, + scopes::ScopeId, + virtual_dom::VirtualDom, + AttributeValue, TemplateNode, }; -use bumpalo::Bump; -use rustc_hash::{FxHashMap, FxHashSet}; -use smallvec::{smallvec, SmallVec}; -pub(crate) struct DiffState<'bump> { - pub(crate) scopes: &'bump ScopeArena, - pub(crate) mutations: Mutations<'bump>, - pub(crate) force_diff: bool, - pub(crate) scope_stack: SmallVec<[ScopeId; 5]>, -} +use fxhash::{FxHashMap, FxHashSet}; +use DynamicNode::*; -impl<'b> DiffState<'b> { - pub fn new(scopes: &'b ScopeArena) -> Self { - Self { - scopes, - mutations: Mutations::new(), - force_diff: false, - scope_stack: smallvec![], - } - } +impl<'b> VirtualDom { + pub(super) fn diff_scope(&mut self, scope: ScopeId) { + let scope_state = &mut self.scopes[scope.0]; - pub fn diff_scope(&mut self, parent: ElementId, scopeid: ScopeId) { - let (old, new) = (self.scopes.wip_head(scopeid), self.scopes.fin_head(scopeid)); + self.scope_stack.push(scope); + unsafe { + // Load the old and new bump arenas + let old = scope_state + .previous_frame() + .try_load_node() + .expect("Call rebuild before diffing"); - self.scope_stack.push(scopeid); - { - self.diff_node(parent, old, new); + let new = scope_state + .current_frame() + .try_load_node() + .expect("Call rebuild before diffing"); + + use RenderReturn::{Async, Sync}; + + match (old, new) { + (Sync(Ok(l)), Sync(Ok(r))) => self.diff_node(l, r), + + // Err cases + (Sync(Ok(l)), Sync(Err(e))) => self.diff_ok_to_err(l, e), + (Sync(Err(e)), Sync(Ok(r))) => self.diff_err_to_ok(e, r), + (Sync(Err(_eo)), Sync(Err(_en))) => { /* nothing */ } + + // Async + (Sync(Ok(_l)), Async(_)) => todo!(), + (Sync(Err(_e)), Async(_)) => todo!(), + (Async(_), Sync(Ok(_r))) => todo!(), + (Async(_), Sync(Err(_e))) => { /* nothing */ } + (Async(_), Async(_)) => { /* nothing */ } + }; } self.scope_stack.pop(); - - self.mutations.mark_dirty_scope(scopeid); } - pub fn diff_node( - &mut self, - parent: ElementId, - old_node: &'b VNode<'b>, - new_node: &'b VNode<'b>, - ) { - use VNode::{Component, Element, Fragment, Placeholder, TemplateRef, Text}; - match (old_node, new_node) { - (Text(old), Text(new)) => { - self.diff_text_nodes(old, new, old_node, new_node); + fn diff_ok_to_err(&mut self, _l: &'b VNode<'b>, _e: &anyhow::Error) {} + fn diff_err_to_ok(&mut self, _e: &anyhow::Error, _l: &'b VNode<'b>) {} + + fn diff_node(&mut self, left_template: &'b VNode<'b>, right_template: &'b VNode<'b>) { + if left_template.template.name != right_template.template.name { + return self.light_diff_templates(left_template, right_template); + } + + for (left_attr, right_attr) in left_template + .dynamic_attrs + .iter() + .zip(right_template.dynamic_attrs.iter()) + { + // Move over the ID from the old to the new + right_attr + .mounted_element + .set(left_attr.mounted_element.get()); + + // We want to make sure anything listener that gets pulled is valid + if let AttributeValue::Listener(_) = right_attr.value { + self.update_template(left_attr.mounted_element.get(), right_template); } - (Placeholder(old), Placeholder(new)) => { - self.diff_placeholder_nodes(old, new, old_node, new_node); + if left_attr.value != right_attr.value || left_attr.volatile { + // todo: add more types of attribute values + match right_attr.value { + AttributeValue::Text(text) => { + let name = unsafe { std::mem::transmute(left_attr.name) }; + let value = unsafe { std::mem::transmute(text) }; + self.mutations.push(Mutation::SetAttribute { + id: left_attr.mounted_element.get(), + ns: right_attr.namespace, + name, + value, + }); + } + // todo: more types of attribute values + _ => todo!("other attribute types"), + } } + } - (Element(old), Element(new)) => { - self.diff_element_nodes(old, new, old_node, new_node); - } + for (idx, (left_node, right_node)) in left_template + .dynamic_nodes + .iter() + .zip(right_template.dynamic_nodes.iter()) + .enumerate() + { + match (left_node, right_node) { + (Text(left), Text(right)) => self.diff_vtext(left, right), + (Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right), + (Placeholder(left), Placeholder(right)) => { + right.set(left.get()); + } + (Component(left), Component(right)) => { + self.diff_vcomponent(left, right, right_template, idx) + } + (Placeholder(left), Fragment(right)) => { + self.replace_placeholder_with_nodes(left, right) + } + (Fragment(left), Placeholder(right)) => { + self.replace_nodes_with_placeholder(left, right) + } + _ => todo!(), + }; + } - (Component(old), Component(new)) => { - self.diff_component_nodes(parent, old, new, old_node, new_node); - } - - (Fragment(old), Fragment(new)) => { - self.diff_fragment_nodes(parent, old, new); - } - - (TemplateRef(old), TemplateRef(new)) => { - self.diff_template_ref_nodes(parent, old, new, old_node, new_node); - } - - ( - Component(_) | Fragment(_) | Text(_) | Element(_) | Placeholder(_) | TemplateRef(_), - Component(_) | Fragment(_) | Text(_) | Element(_) | Placeholder(_) | TemplateRef(_), - ) => self.replace_node(parent, old_node, new_node), + // Make sure the roots get transferred over + for (left, right) in left_template + .root_ids + .iter() + .zip(right_template.root_ids.iter()) + { + right.set(left.get()); } } - pub fn create_node(&mut self, parent: ElementId, node: &'b VNode<'b>, nodes: &mut Vec) { - match node { - VNode::Text(vtext) => self.create_text_node(vtext, node, nodes), - VNode::Placeholder(anchor) => self.create_anchor_node(anchor, node, nodes), - VNode::Element(element) => self.create_element_node(parent, element, node, nodes), - VNode::Fragment(frag) => self.create_fragment_node(parent, frag, nodes), - VNode::Component(component) => self.create_component_node(parent, component, nodes), - VNode::TemplateRef(temp) => self.create_template_ref_node(parent, temp, node, nodes), - } + fn replace_placeholder_with_nodes(&mut self, l: &'b Cell, r: &'b [VNode<'b>]) { + let m = self.create_children(r); + let id = l.get(); + self.mutations.push(Mutation::ReplaceWith { id, m }); + self.reclaim(id); } - fn create_text_node(&mut self, text: &'b VText<'b>, node: &'b VNode<'b>, nodes: &mut Vec) { - let real_id = self.scopes.reserve_node(node); - text.id.set(Some(real_id)); + fn replace_nodes_with_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b Cell) { + // Remove the old nodes, except for one + self.remove_nodes(&l[1..]); + + // Now create the new one + let first = self.replace_inner(&l[0]); + + // Create the placeholder first, ensuring we get a dedicated ID for the placeholder + let placeholder = self.next_element(&l[0], &[]); + r.set(placeholder); self.mutations - .create_text_node(text.text, Some(real_id.as_u64())); - nodes.push(real_id.0 as u64); + .push(Mutation::CreatePlaceholder { id: placeholder }); + + self.mutations + .push(Mutation::ReplaceWith { id: first, m: 1 }); + + self.try_reclaim(first); } - fn create_anchor_node( + fn diff_vcomponent( &mut self, - anchor: &'b VPlaceholder, - node: &'b VNode<'b>, - nodes: &mut Vec, + left: &'b VComponent<'b>, + right: &'b VComponent<'b>, + right_template: &'b VNode<'b>, + idx: usize, ) { - let real_id = self.scopes.reserve_node(node); - anchor.id.set(Some(real_id)); - self.mutations.create_placeholder(Some(real_id.as_u64())); - nodes.push(real_id.0 as u64); - } - - fn create_element_node( - &mut self, - parent: ElementId, - element: &'b VElement<'b>, - node: &'b VNode<'b>, - nodes: &mut Vec, - ) { - let VElement { - tag: tag_name, - listeners, - attributes, - children, - namespace, - id: dom_id, - parent: parent_id, - .. - } = &element; - - parent_id.set(Some(parent)); - - let real_id = self.scopes.reserve_node(node); - - dom_id.set(Some(real_id)); - - { + // Replace components that have different render fns + if left.render_fn != right.render_fn { + let created = self.create_component_node(right_template, right, idx); + let head = unsafe { + self.scopes[left.scope.get().unwrap().0] + .root_node() + .extend_lifetime_ref() + }; + let id = match head { + RenderReturn::Sync(Ok(node)) => self.replace_inner(node), + _ => todo!(), + }; self.mutations - .create_element(tag_name, *namespace, Some(real_id.as_u64()), 0); + .push(Mutation::ReplaceWith { id, m: created }); + self.drop_scope(left.scope.get().unwrap()); + return; + } - let cur_scope_id = self.current_scope(); + // Make sure the new vcomponent has the right scopeid associated to it + let scope_id = left.scope.get().unwrap(); + right.scope.set(Some(scope_id)); - for listener in listeners.iter() { - listener.mounted_node.set(Some(real_id)); - self.mutations.new_event_listener(listener, cur_scope_id); + // copy out the box for both + let old = self.scopes[scope_id.0].props.as_ref(); + let new = right.props.replace(None).unwrap(); + + // If the props are static, then we try to memoize by setting the new with the old + // The target scopestate still has the reference to the old props, so there's no need to update anything + // This also implicitly drops the new props since they're not used + if left.static_props && unsafe { old.as_ref().unwrap().memoize(new.as_ref()) } { + return; + } + + // First, move over the props from the old to the new, dropping old props in the process + self.scopes[scope_id.0].props = unsafe { std::mem::transmute(new) }; + + // Now run the component and diff it + self.run_scope(scope_id); + self.diff_scope(scope_id); + } + + /// Lightly diff the two templates, checking only their roots. + /// + /// The goal here is to preserve any existing component state that might exist. This is to preserve some React-like + /// behavior where the component state is preserved when the component is re-rendered. + /// + /// This is implemented by iterating each root, checking if the component is the same, if it is, then diff it. + /// + /// We then pass the new template through "create" which should be smart enough to skip roots. + /// + /// Currently, we only handle the case where the roots are the same component list. If there's any sort of deviation, + /// IE more nodes, less nodes, different nodes, or expressions, then we just replace the whole thing. + /// + /// This is mostly implemented to help solve the issue where the same component is rendered under two different + /// conditions: + /// + /// ```rust, ignore + /// if enabled { + /// rsx!{ Component { enabled_sign: "abc" } } + /// } else { + /// rsx!{ Component { enabled_sign: "xyz" } } + /// } + /// ``` + /// + /// However, we should not that it's explicit in the docs that this is not a guarantee. If you need to preserve state, + /// then you should be passing in separate props instead. + /// + /// ```rust, ignore + /// let props = if enabled { + /// ComponentProps { enabled_sign: "abc" } + /// } else { + /// ComponentProps { enabled_sign: "xyz" } + /// }; + /// + /// rsx! { + /// Component { ..props } + /// } + /// ``` + fn light_diff_templates(&mut self, left: &'b VNode<'b>, right: &'b VNode<'b>) { + match matching_components(left, right) { + None => self.replace(left, right), + Some(components) => components + .into_iter() + .enumerate() + .for_each(|(idx, (l, r))| self.diff_vcomponent(l, r, right, idx)), + } + } + + /// Diff the two text nodes + /// + /// This just moves the ID of the old node over to the new node, and then sets the text of the new node if it's + /// different. + fn diff_vtext(&mut self, left: &'b VText<'b>, right: &'b VText<'b>) { + let id = left.id.get(); + + right.id.set(id); + if left.value != right.value { + let value = unsafe { std::mem::transmute(right.value) }; + self.mutations.push(Mutation::SetText { id, value }); + } + } + + /// Remove all the top-level nodes, returning the firstmost root ElementId + /// + /// All IDs will be garbage collected + fn replace_inner(&mut self, node: &'b VNode<'b>) -> ElementId { + let id = match node.dynamic_root(0) { + None => node.root_ids[0].get(), + Some(Text(t)) => t.id.get(), + Some(Placeholder(e)) => e.get(), + Some(Fragment(nodes)) => { + let id = self.replace_inner(&nodes[0]); + self.remove_nodes(&nodes[1..]); + id } - - for attr in attributes.iter() { - self.mutations.set_attribute(attr, Some(real_id.as_u64())); - } - - if !children.is_empty() { - self.create_and_append_children(real_id, children); - } - } - - nodes.push(real_id.0 as u64); - } - - fn create_fragment_node( - &mut self, - parent: ElementId, - frag: &'b VFragment<'b>, - nodes: &mut Vec, - ) { - self.create_children(parent, frag.children, nodes); - } - - fn create_component_node( - &mut self, - parent: ElementId, - vcomponent: &'b VComponent<'b>, - nodes: &mut Vec, - ) { - let parent_idx = self.current_scope(); - - // the component might already exist - if it does, we need to reuse it - // this makes figure out when to drop the component more complicated - let new_idx = if let Some(idx) = vcomponent.scope.get() { - assert!(self.scopes.get_scope(idx).is_some()); - idx - } else { - // Insert a new scope into our component list - let props: Box = vcomponent.props.borrow_mut().take().unwrap(); - let props: Box = unsafe { std::mem::transmute(props) }; - self.scopes - .new_with_key(vcomponent.user_fc, props, Some(parent_idx), parent) - }; - - // Actually initialize the caller's slot with the right address - vcomponent.scope.set(Some(new_idx)); - - log::trace!( - "created component \"{}\", id: {:?} parent {:?}", - vcomponent.fn_name, - new_idx, - parent_idx, - ); - - // if vcomponent.can_memoize { - // // todo: implement promotion logic. save us from boxing props that we don't need - // } else { - // // track this component internally so we know the right drop order - // } - - self.enter_scope(new_idx); - - // Run the scope for one iteration to initialize it - self.scopes.run_scope(new_idx); - self.mutations.mark_dirty_scope(new_idx); - - // Take the node that was just generated from running the component - let nextnode = self.scopes.fin_head(new_idx); - self.create_node(parent, nextnode, nodes); - - self.leave_scope(); - } - - pub(crate) fn create_template_ref_node( - &mut self, - parent: ElementId, - new: &'b VTemplateRef<'b>, - _node: &'b VNode<'b>, - nodes: &mut Vec, - ) { - let (id, just_created) = { - let mut resolver = self.scopes.template_resolver.borrow_mut(); - resolver.get_or_create_client_id(&new.template_id, self.scopes) - }; - - let template = { - let templates = self.scopes.templates.borrow(); - templates.get(&new.template_id).unwrap().clone() - }; - let template = template.borrow(); - - if just_created { - self.register_template(&template, id); - } - - new.template_ref_id - .set(Some(self.scopes.reserve_template_ref(new))); - - let template_ref_id = new.template_ref_id.get().unwrap(); - - let root_nodes = template.root_nodes(); - nodes.extend(root_nodes.iter().map(|node_id| { - let real_id = self.scopes.reserve_template_node(template_ref_id, *node_id); - new.set_node_id(*node_id, real_id); - real_id.as_u64() - })); - - self.mutations.clone_node_children( - Some(id.as_u64()), - nodes[(nodes.len() - root_nodes.len())..].to_vec(), - ); - - new.hydrate(parent, &template, self); - } - - pub(crate) fn diff_text_nodes( - &mut self, - old: &'b VText<'b>, - new: &'b VText<'b>, - _old_node: &'b VNode<'b>, - new_node: &'b VNode<'b>, - ) { - if std::ptr::eq(old, new) { - return; - } - - // if the node is comming back not assigned, that means it was borrowed but removed - let root = match old.id.get() { - Some(id) => id, - None => self.scopes.reserve_node(new_node), - }; - - if old.text != new.text { - self.mutations.set_text(new.text, Some(root.as_u64())); - } - - self.scopes.update_node(new_node, root); - - new.id.set(Some(root)); - } - - pub(crate) fn diff_placeholder_nodes( - &mut self, - old: &'b VPlaceholder, - new: &'b VPlaceholder, - _old_node: &'b VNode<'b>, - new_node: &'b VNode<'b>, - ) { - if std::ptr::eq(old, new) { - return; - } - - // if the node is comming back not assigned, that means it was borrowed but removed - let root = match old.id.get() { - Some(id) => id, - None => self.scopes.reserve_node(new_node), - }; - - self.scopes.update_node(new_node, root); - new.id.set(Some(root)); - } - - fn diff_element_nodes( - &mut self, - old: &'b VElement<'b>, - new: &'b VElement<'b>, - old_node: &'b VNode<'b>, - new_node: &'b VNode<'b>, - ) { - if std::ptr::eq(old, new) { - return; - } - - // if the node is comming back not assigned, that means it was borrowed but removed - let root = match old.id.get() { - Some(id) => id, - None => self.scopes.reserve_node(new_node), - }; - - // If the element type is completely different, the element needs to be re-rendered completely - // This is an optimization React makes due to how users structure their code - // - // This case is rather rare (typically only in non-keyed lists) - if new.tag != old.tag || new.namespace != old.namespace { - self.replace_node(root, old_node, new_node); - return; - } - - self.scopes.update_node(new_node, root); - - new.id.set(Some(root)); - new.parent.set(old.parent.get()); - - // todo: attributes currently rely on the element on top of the stack, but in theory, we only need the id of the - // element to modify its attributes. - // it would result in fewer instructions if we just set the id directly. - // it would also clean up this code some, but that's not very important anyways - - // Diff Attributes - // - // It's extraordinarily rare to have the number/order of attributes change - // In these cases, we just completely erase the old set and make a new set - // - // TODO: take a more efficient path than this - if old.attributes.len() == new.attributes.len() { - for (old_attr, new_attr) in old.attributes.iter().zip(new.attributes.iter()) { - if !old_attr.is_static && old_attr.value != new_attr.value - || new_attr.attribute.volatile - { - self.mutations.set_attribute(new_attr, Some(root.as_u64())); + Some(Component(comp)) => { + let scope = comp.scope.get().unwrap(); + match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { + RenderReturn::Sync(Ok(t)) => self.replace_inner(t), + _ => todo!("cannot handle nonstandard nodes"), } } - } else { - for attribute in old.attributes { - self.mutations - .remove_attribute(attribute, Some(root.as_u64())); - } - for attribute in new.attributes { - self.mutations.set_attribute(attribute, Some(root.as_u64())); - } - } - - // Diff listeners - // - // It's extraordinarily rare to have the number/order of listeners change - // In the cases where the listeners change, we completely wipe the data attributes and add new ones - // - // We also need to make sure that all listeners are properly attached to the parent scope (fix_listener) - // - // TODO: take a more efficient path than this - let cur_scope_id = self.current_scope(); - - if old.listeners.len() == new.listeners.len() { - for (old_l, new_l) in old.listeners.iter().zip(new.listeners.iter()) { - new_l.mounted_node.set(old_l.mounted_node.get()); - if old_l.event != new_l.event { - self.mutations - .remove_event_listener(old_l.event, Some(root.as_u64())); - self.mutations.new_event_listener(new_l, cur_scope_id); - } - } - } else { - for listener in old.listeners { - self.mutations - .remove_event_listener(listener.event, Some(root.as_u64())); - } - for listener in new.listeners { - listener.mounted_node.set(Some(root)); - self.mutations.new_event_listener(listener, cur_scope_id); - } - } - - match (old.children.len(), new.children.len()) { - (0, 0) => {} - (0, _) => { - let mut created = Vec::new(); - self.create_children(root, new.children, &mut created); - self.mutations.append_children(Some(root.as_u64()), created); - } - (_, _) => self.diff_children(root, old.children, new.children), }; - } - fn diff_component_nodes( - &mut self, - parent: ElementId, - old: &'b VComponent<'b>, - new: &'b VComponent<'b>, - old_node: &'b VNode<'b>, - new_node: &'b VNode<'b>, - ) { - let scope_addr = old - .scope - .get() - .expect("existing component nodes should have a scope"); - - if std::ptr::eq(old, new) { - return; + // Just remove the rest from the dom + for (idx, _) in node.template.roots.iter().enumerate().skip(1) { + self.remove_root_node(node, idx); } - // Make sure we're dealing with the same component (by function pointer) - if old.user_fc == new.user_fc { - self.enter_scope(scope_addr); - { - // Make sure the new component vnode is referencing the right scope id - new.scope.set(Some(scope_addr)); + // Garabge collect all of the nodes since this gets used in replace + self.clean_up_node(node); - // make sure the component's caller function is up to date - let scope = self - .scopes - .get_scope(scope_addr) - .unwrap_or_else(|| panic!("could not find {:?}", scope_addr)); + id + } - // take the new props out regardless - // when memoizing, push to the existing scope if memoization happens - let new_props = new - .props - .borrow_mut() - .take() - .expect("new component props should exist"); - - let should_diff = { - if old.can_memoize { - // safety: we trust the implementation of "memoize" - let props_are_the_same = unsafe { - let new_ref = new_props.as_ref(); - scope.props.borrow().as_ref().unwrap().memoize(new_ref) - }; - !props_are_the_same || self.force_diff - } else { - true - } - }; - - if should_diff { - let _old_props = scope - .props - .replace(unsafe { std::mem::transmute(Some(new_props)) }); - - // this should auto drop the previous props - self.scopes.run_scope(scope_addr); - self.mutations.mark_dirty_scope(scope_addr); - - self.diff_node( - parent, - self.scopes.wip_head(scope_addr), - self.scopes.fin_head(scope_addr), - ); - } else { - // memoization has taken place - drop(new_props); - }; + /// Clean up the node, not generating mutations + /// + /// Simply walks through the dynamic nodes + fn clean_up_node(&mut self, node: &'b VNode<'b>) { + for (idx, dyn_node) in node.dynamic_nodes.iter().enumerate() { + // Roots are cleaned up automatically? + if node.template.node_paths[idx].len() == 1 { + continue; } - self.leave_scope(); - } else { - self.replace_node(parent, old_node, new_node); - } - } - fn diff_fragment_nodes( - &mut self, - parent: ElementId, - old: &'b VFragment<'b>, - new: &'b VFragment<'b>, - ) { - if std::ptr::eq(old, new) { - return; - } - - // This is the case where options or direct vnodes might be used. - // In this case, it's faster to just skip ahead to their diff - if old.children.len() == 1 && new.children.len() == 1 { - if !std::ptr::eq(old, new) { - self.diff_node(parent, &old.children[0], &new.children[0]); - } - return; - } - - debug_assert!(!old.children.is_empty()); - debug_assert!(!new.children.is_empty()); - - self.diff_children(parent, old.children, new.children); - } - - #[allow(clippy::too_many_lines)] - fn diff_template_ref_nodes( - &mut self, - parent: ElementId, - old: &'b VTemplateRef<'b>, - new: &'b VTemplateRef<'b>, - old_node: &'b VNode<'b>, - new_node: &'b VNode<'b>, - ) { - fn diff_attributes<'b, Nodes, Attributes, V, Children, Listeners, TextSegments, Text>( - nodes: &Nodes, - ctx: ( - &mut DiffState<'b>, - &'b Bump, - &'b VTemplateRef<'b>, - &Template, - usize, - ), - ) where - Nodes: AsRef<[TemplateNode]>, - Attributes: AsRef<[TemplateAttribute]>, - V: TemplateValue, - Children: AsRef<[TemplateNodeId]>, - Listeners: AsRef<[usize]>, - TextSegments: AsRef<[TextTemplateSegment]>, - Text: AsRef, - { - let (diff_state, scope_bump, new, template, idx) = ctx; - for (node_id, attr_idx) in template.get_dynamic_nodes_for_attribute_index(idx) { - if let TemplateNodeType::Element(el) = &nodes.as_ref()[node_id.0].node_type { - let TemplateElement { attributes, .. } = el; - let attr = &attributes.as_ref()[*attr_idx]; - let attribute = Attribute { - attribute: attr.attribute, - value: new.dynamic_context.resolve_attribute(idx).clone(), - is_static: false, + match dyn_node { + Component(comp) => { + let scope = comp.scope.get().unwrap(); + match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { + RenderReturn::Sync(Ok(t)) => self.clean_up_node(t), + _ => todo!("cannot handle nonstandard nodes"), }; - let real_id = new.get_node_id(*node_id); - diff_state - .mutations - .set_attribute(scope_bump.alloc(attribute), Some(real_id.as_u64())); - } else { - panic!("expected element node"); } + Text(t) => self.reclaim(t.id.get()), + Placeholder(t) => self.reclaim(t.get()), + Fragment(nodes) => nodes.iter().for_each(|node| self.clean_up_node(node)), + }; + } + + // we clean up nodes with dynamic attributes, provided the node is unique and not a root node + let mut id = None; + for (idx, attr) in node.dynamic_attrs.iter().enumerate() { + // We'll clean up the root nodes either way, so don't worry + if node.template.attr_paths[idx].len() == 1 { + continue; } - } - fn set_attribute<'b, Attributes, V, Children, Listeners, TextSegments, Text>( - node: &TemplateNode, - ctx: (&mut DiffState<'b>, &'b Bump, &'b VTemplateRef<'b>, usize), - ) where - Attributes: AsRef<[TemplateAttribute]>, - V: TemplateValue, - Children: AsRef<[TemplateNodeId]>, - Listeners: AsRef<[usize]>, - TextSegments: AsRef<[TextTemplateSegment]>, - Text: AsRef, - { - let (diff_state, scope_bump, new, template_attr_idx) = ctx; - if let TemplateNodeType::Element(el) = &node.node_type { - let TemplateElement { attributes, .. } = el; - let attr = &attributes.as_ref()[template_attr_idx]; - let value = match &attr.value { - TemplateAttributeValue::Dynamic(idx) => { - new.dynamic_context.resolve_attribute(*idx).clone() - } - TemplateAttributeValue::Static(value) => value.allocate(scope_bump), - }; - let attribute = Attribute { - attribute: attr.attribute, - value, - is_static: false, - }; - let real_id = new.get_node_id(node.id); - diff_state - .mutations - .set_attribute(scope_bump.alloc(attribute), Some(real_id.as_u64())); - } else { - panic!("expected element node"); + let next_id = attr.mounted_element.get(); + + if id == Some(next_id) { + continue; } - } - fn diff_text<'b, Attributes, V, Children, Listeners, TextSegments, Text>( - node: &TemplateNode, - ctx: ( - &mut DiffState<'b>, - &'b VTemplateRef<'b>, - &TemplateContext<'b>, - ), - ) where - Attributes: AsRef<[TemplateAttribute]>, - V: TemplateValue, - Children: AsRef<[TemplateNodeId]>, - Listeners: AsRef<[usize]>, - TextSegments: AsRef<[TextTemplateSegment]>, - Text: AsRef, - { - let (diff, new, dynamic_context) = ctx; - if let TemplateNodeType::Text(text) = &node.node_type { - let text = dynamic_context.resolve_text(text); - let real_id = new.get_node_id(node.id); - diff.mutations.set_text( - diff.current_scope_bump().alloc(text), - Some(real_id.as_u64()), - ); - } else { - panic!("expected text node"); - } - } + id = Some(next_id); - if std::ptr::eq(old, new) { - return; - } - - // if the templates are different, just rebuild it - if old.template_id != new.template_id - || self - .scopes - .template_resolver - .borrow() - .is_dirty(&new.template_id) - { - self.replace_node(parent, old_node, new_node); - return; - } - - if let Some(template_ref_id) = old.template_ref_id.get() { - self.scopes.update_template_ref(template_ref_id, new); - new.template_ref_id.set(Some(template_ref_id)); - } else { - new.template_ref_id - .set(Some(self.scopes.reserve_template_ref(new))); - } - new.parent.set(Some(parent)); - new.node_ids.replace(old.node_ids.take()); - - let scope_bump = &self.current_scope_bump(); - - let template = { - let templates = self.scopes.templates.borrow(); - templates.get(&new.template_id).unwrap().clone() - }; - let template = template.borrow(); - - // diff dynamic attributes - for (idx, (old_attr, new_attr)) in old - .dynamic_context - .attributes - .iter() - .zip(new.dynamic_context.attributes.iter()) - .enumerate() - { - if old_attr != new_attr { - template.with_nodes( - diff_attributes, - diff_attributes, - (self, scope_bump, new, &template, idx), - ); - } - } - - // set all volatile attributes - for (id, idx) in template.volatile_attributes() { - template.with_node( - id, - set_attribute, - set_attribute, - (self, scope_bump, new, idx), - ); - } - - // diff dynmaic nodes - for (old_node, new_node) in old - .dynamic_context - .nodes - .iter() - .zip(new.dynamic_context.nodes.iter()) - { - self.diff_node(parent, old_node, new_node); - } - - // diff dynamic text - // text nodes could rely on multiple dynamic text parts, so we keep a record of which ones to rerender and send the diffs at the end - let mut dirty_text_nodes = FxHashSet::default(); - for (idx, (old_text, new_text)) in old - .dynamic_context - .text_segments - .iter() - .zip(new.dynamic_context.text_segments.iter()) - .enumerate() - { - if old_text != new_text { - for node_id in template.get_dynamic_nodes_for_text_index(idx) { - dirty_text_nodes.insert(*node_id); - } - } - } - for node_id in dirty_text_nodes { - template.with_node( - node_id, - diff_text, - diff_text, - (self, new, &new.dynamic_context), - ); + self.reclaim(next_id); } } - // Diff the given set of old and new children. - // - // The parent must be on top of the change list stack when this function is - // entered: - // - // [... parent] - // - // the change list stack is in the same state when this function returns. - // - // If old no anchors are provided, then it's assumed that we can freely append to the parent. - // - // Remember, non-empty lists does not mean that there are real elements, just that there are virtual elements. - // - // Fragment nodes cannot generate empty children lists, so we can assume that when a list is empty, it belongs only - // to an element, and appending makes sense. - fn diff_children(&mut self, parent: ElementId, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { - if std::ptr::eq(old, new) { - return; - } - - // Remember, fragments can never be empty (they always have a single child) - match (old, new) { - ([], []) => {} - ([], _) => self.create_and_append_children(parent, new), - (_, []) => self.remove_nodes(old, true), - _ => { - let new_is_keyed = new[0].key().is_some(); - let old_is_keyed = old[0].key().is_some(); - - debug_assert!( - new.iter().all(|n| n.key().is_some() == new_is_keyed), - "all siblings must be keyed or all siblings must be non-keyed" - ); - debug_assert!( - old.iter().all(|o| o.key().is_some() == old_is_keyed), - "all siblings must be keyed or all siblings must be non-keyed" - ); - - if new_is_keyed && old_is_keyed { - self.diff_keyed_children(parent, old, new); - } else { - self.diff_non_keyed_children(parent, old, new); - } + fn remove_root_node(&mut self, node: &'b VNode<'b>, idx: usize) { + match node.dynamic_root(idx) { + Some(Text(i)) => { + let id = i.id.get(); + self.mutations.push(Mutation::Remove { id }); + self.reclaim(id); } + Some(Placeholder(e)) => { + let id = e.get(); + self.mutations.push(Mutation::Remove { id }); + self.reclaim(id); + } + Some(Fragment(nodes)) => self.remove_nodes(nodes), + Some(Component(comp)) => { + let scope = comp.scope.get().unwrap(); + match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { + RenderReturn::Sync(Ok(t)) => self.remove_node(t), + _ => todo!("cannot handle nonstandard nodes"), + }; + } + None => { + let id = node.root_ids[idx].get(); + self.mutations.push(Mutation::Remove { id }); + self.reclaim(id); + } + }; + } + + fn diff_non_empty_fragment(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { + let new_is_keyed = new[0].key.is_some(); + let old_is_keyed = old[0].key.is_some(); + debug_assert!( + new.iter().all(|n| n.key.is_some() == new_is_keyed), + "all siblings must be keyed or all siblings must be non-keyed" + ); + debug_assert!( + old.iter().all(|o| o.key.is_some() == old_is_keyed), + "all siblings must be keyed or all siblings must be non-keyed" + ); + + if new_is_keyed && old_is_keyed { + self.diff_keyed_children(old, new); + } else { + self.diff_non_keyed_children(old, new); } } @@ -865,12 +396,7 @@ impl<'b> DiffState<'b> { // [... parent] // // the change list stack is in the same state when this function returns. - fn diff_non_keyed_children( - &mut self, - parent: ElementId, - old: &'b [VNode<'b>], - new: &'b [VNode<'b>], - ) { + fn diff_non_keyed_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { use std::cmp::Ordering; // Handled these cases in `diff_children` before calling this function. @@ -878,15 +404,13 @@ impl<'b> DiffState<'b> { debug_assert!(!old.is_empty()); match old.len().cmp(&new.len()) { - Ordering::Greater => self.remove_nodes(&old[new.len()..], true), - Ordering::Less => { - self.create_and_insert_after(parent, &new[old.len()..], old.last().unwrap()); - } + Ordering::Greater => self.remove_nodes(&old[new.len()..]), + Ordering::Less => self.create_and_insert_after(&new[old.len()..], old.last().unwrap()), Ordering::Equal => {} } for (new, old) in new.iter().zip(old.iter()) { - self.diff_node(parent, old, new); + self.diff_node(old, new); } } @@ -906,18 +430,13 @@ impl<'b> DiffState<'b> { // https://github.com/infernojs/inferno/blob/36fd96/packages/inferno/src/DOM/patching.ts#L530-L739 // // The stack is empty upon entry. - fn diff_keyed_children( - &mut self, - parent: ElementId, - old: &'b [VNode<'b>], - new: &'b [VNode<'b>], - ) { + fn diff_keyed_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { if cfg!(debug_assertions) { - let mut keys = rustc_hash::FxHashSet::default(); + let mut keys = fxhash::FxHashSet::default(); let mut assert_unique_keys = |children: &'b [VNode<'b>]| { keys.clear(); for child in children { - let key = child.key(); + let key = child.key; debug_assert!( key.is_some(), "if any sibling is keyed, all siblings must be keyed" @@ -939,7 +458,7 @@ impl<'b> DiffState<'b> { // // `shared_prefix_count` is the count of how many nodes at the start of // `new` and `old` share the same keys. - let (left_offset, right_offset) = match self.diff_keyed_ends(parent, old, new) { + let (left_offset, right_offset) = match self.diff_keyed_ends(old, new) { Some(count) => count, None => return, }; @@ -958,25 +477,25 @@ impl<'b> DiffState<'b> { if new_middle.is_empty() { // remove the old elements - self.remove_nodes(old_middle, true); + self.remove_nodes(old_middle); } else if old_middle.is_empty() { // there were no old elements, so just create the new elements // we need to find the right "foothold" though - we shouldn't use the "append" at all if left_offset == 0 { // insert at the beginning of the old list let foothold = &old[old.len() - right_offset]; - self.create_and_insert_before(parent, new_middle, foothold); + self.create_and_insert_before(new_middle, foothold); } else if right_offset == 0 { // insert at the end the old list let foothold = old.last().unwrap(); - self.create_and_insert_after(parent, new_middle, foothold); + self.create_and_insert_after(new_middle, foothold); } else { // inserting in the middle let foothold = &old[left_offset - 1]; - self.create_and_insert_after(parent, new_middle, foothold); + self.create_and_insert_after(new_middle, foothold); } } else { - self.diff_keyed_middle(parent, old_middle, new_middle); + self.diff_keyed_middle(old_middle, new_middle); } } @@ -987,7 +506,6 @@ impl<'b> DiffState<'b> { /// If there is no offset, then this function returns None and the diffing is complete. fn diff_keyed_ends( &mut self, - parent: ElementId, old: &'b [VNode<'b>], new: &'b [VNode<'b>], ) -> Option<(usize, usize)> { @@ -995,24 +513,24 @@ impl<'b> DiffState<'b> { for (old, new) in old.iter().zip(new.iter()) { // abort early if we finally run into nodes with different keys - if old.key() != new.key() { + if old.key != new.key { break; } - self.diff_node(parent, old, new); + self.diff_node(old, new); left_offset += 1; } // If that was all of the old children, then create and append the remaining // new children and we're finished. if left_offset == old.len() { - self.create_and_insert_after(parent, &new[left_offset..], old.last().unwrap()); + self.create_and_insert_after(&new[left_offset..], old.last().unwrap()); return None; } // And if that was all of the new children, then remove all of the remaining // old children and we're finished. if left_offset == new.len() { - self.remove_nodes(&old[left_offset..], true); + self.remove_nodes(&old[left_offset..]); return None; } @@ -1020,10 +538,10 @@ impl<'b> DiffState<'b> { let mut right_offset = 0; for (old, new) in old.iter().rev().zip(new.iter().rev()) { // abort early if we finally run into nodes with different keys - if old.key() != new.key() { + if old.key != new.key { break; } - self.diff_node(parent, old, new); + self.diff_node(old, new); right_offset += 1; } @@ -1044,7 +562,7 @@ impl<'b> DiffState<'b> { // // Upon exit from this function, it will be restored to that same self. #[allow(clippy::too_many_lines)] - fn diff_keyed_middle(&mut self, parent: ElementId, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { + fn diff_keyed_middle(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { /* 1. Map the old keys into a numerical ordering based on indices. 2. Create a map of old key to its index @@ -1066,11 +584,10 @@ impl<'b> DiffState<'b> { 8. Finally, generate instructions to remove any old children. 9. Generate instructions to finally diff children that are the same between both */ - // 0. Debug sanity checks // Should have already diffed the shared-key prefixes and suffixes. - debug_assert_ne!(new.first().map(VNode::key), old.first().map(VNode::key)); - debug_assert_ne!(new.last().map(VNode::key), old.last().map(VNode::key)); + debug_assert_ne!(new.first().map(|i| i.key), old.first().map(|i| i.key)); + debug_assert_ne!(new.last().map(|i| i.key), old.last().map(|i| i.key)); // 1. Map the old keys into a numerical ordering based on indices. // 2. Create a map of old key to its index @@ -1078,7 +595,7 @@ impl<'b> DiffState<'b> { let old_key_to_old_index = old .iter() .enumerate() - .map(|(i, o)| (o.key().unwrap(), i)) + .map(|(i, o)| (o.key.unwrap(), i)) .collect::>(); let mut shared_keys = FxHashSet::default(); @@ -1087,7 +604,7 @@ impl<'b> DiffState<'b> { let new_index_to_old_index = new .iter() .map(|node| { - let key = node.key().unwrap(); + let key = node.key.unwrap(); if let Some(&index) = old_key_to_old_index.get(&key) { shared_keys.insert(key); index @@ -1100,15 +617,15 @@ impl<'b> DiffState<'b> { // If none of the old keys are reused by the new children, then we remove all the remaining old children and // create the new children afresh. if shared_keys.is_empty() { - if let Some(first_old) = old.get(0) { - self.remove_nodes(&old[1..], true); - let mut nodes_created = Vec::new(); - self.create_children(parent, new, &mut nodes_created); - self.replace_inner(first_old, nodes_created); + if old.get(0).is_some() { + self.remove_nodes(&old[1..]); + self.replace_many(&old[0], new); } else { // I think this is wrong - why are we appending? // only valid of the if there are no trailing elements - self.create_and_append_children(parent, new); + // self.create_and_append_children(new); + + todo!("we should never be appending - just creating N"); } return; } @@ -1116,9 +633,9 @@ impl<'b> DiffState<'b> { // remove any old children that are not shared // todo: make this an iterator for child in old { - let key = child.key().unwrap(); + let key = child.key.unwrap(); if !shared_keys.contains(&key) { - self.remove_nodes([child], true); + self.remove_node(child); } } @@ -1146,10 +663,10 @@ impl<'b> DiffState<'b> { } for idx in &lis_sequence { - self.diff_node(parent, &old[new_index_to_old_index[*idx]], &new[*idx]); + self.diff_node(&old[new_index_to_old_index[*idx]], &new[*idx]); } - let mut nodes_created = Vec::new(); + let mut nodes_created = 0; // add mount instruction for the first items not covered by the lis let last = *lis_sequence.last().unwrap(); @@ -1158,16 +675,19 @@ impl<'b> DiffState<'b> { let new_idx = idx + last + 1; let old_index = new_index_to_old_index[new_idx]; if old_index == u32::MAX as usize { - self.create_node(parent, new_node, &mut nodes_created); + nodes_created += self.create(new_node); } else { - self.diff_node(parent, &old[old_index], new_node); - self.get_all_real_nodes(new_node, &mut nodes_created); + self.diff_node(&old[old_index], new_node); + nodes_created += self.push_all_real_nodes(new_node); } } - let last = Some(self.find_last_element(&new[last]).unwrap().as_u64()); - self.mutations.insert_after(last, nodes_created); - nodes_created = Vec::new(); + let id = self.find_last_element(&new[last]); + self.mutations.push(Mutation::InsertAfter { + id, + m: nodes_created, + }); + nodes_created = 0; } // for each spacing, generate a mount instruction @@ -1179,17 +699,20 @@ impl<'b> DiffState<'b> { let new_idx = idx + next + 1; let old_index = new_index_to_old_index[new_idx]; if old_index == u32::MAX as usize { - self.create_node(parent, new_node, &mut nodes_created); + nodes_created += self.create(new_node); } else { - self.diff_node(parent, &old[old_index], new_node); - self.get_all_real_nodes(new_node, &mut nodes_created); + self.diff_node(&old[old_index], new_node); + nodes_created += self.push_all_real_nodes(new_node); } } - let first = Some(self.find_first_element(&new[last]).unwrap().as_u64()); - self.mutations.insert_before(first, nodes_created); + let id = self.find_first_element(&new[last]); + self.mutations.push(Mutation::InsertBefore { + id, + m: nodes_created, + }); - nodes_created = Vec::new(); + nodes_created = 0; } last = *next; } @@ -1200,327 +723,227 @@ impl<'b> DiffState<'b> { for (idx, new_node) in new[..first_lis].iter().enumerate() { let old_index = new_index_to_old_index[idx]; if old_index == u32::MAX as usize { - self.create_node(parent, new_node, &mut nodes_created); + nodes_created += self.create(new_node); } else { - self.diff_node(parent, &old[old_index], new_node); - self.get_all_real_nodes(new_node, &mut nodes_created); + self.diff_node(&old[old_index], new_node); + nodes_created += self.push_all_real_nodes(new_node); } } - let first = Some(self.find_first_element(&new[first_lis]).unwrap().as_u64()); - self.mutations.insert_before(first, nodes_created); + let id = self.find_first_element(&new[first_lis]); + self.mutations.push(Mutation::InsertBefore { + id, + m: nodes_created, + }); } } - pub fn replace_node(&mut self, parent: ElementId, old: &'b VNode<'b>, new: &'b VNode<'b>) { - let mut nodes_vec = Vec::new(); - self.create_node(parent, new, &mut nodes_vec); - self.replace_inner(old, nodes_vec); + /// Remove these nodes from the dom + /// Wont generate mutations for the inner nodes + fn remove_nodes(&mut self, nodes: &'b [VNode<'b>]) { + // note that we iterate in reverse to unlink lists of nodes in their rough index order + nodes.iter().rev().for_each(|node| self.remove_node(node)); } - fn replace_inner(&mut self, old: &'b VNode<'b>, nodes_created: Vec) { - match old { - VNode::Element(el) => { - let id = old - .try_mounted_id() - .unwrap_or_else(|| panic!("broke on {:?}", old)); + fn remove_node(&mut self, node: &'b VNode<'b>) { + for (idx, _) in node.template.roots.iter().enumerate() { + let id = match node.dynamic_root(idx) { + Some(Text(t)) => t.id.get(), + Some(Placeholder(t)) => t.get(), + Some(Fragment(t)) => return self.remove_nodes(t), + Some(Component(comp)) => return self.remove_component(comp.scope.get().unwrap()), + None => node.root_ids[idx].get(), + }; - self.mutations - .replace_with(Some(id.as_u64()), nodes_created); - self.remove_nodes(el.children, false); - self.scopes.collect_garbage(id); + self.mutations.push(Mutation::Remove { id }) + } + + self.clean_up_node(node); + + for root in node.root_ids { + let id = root.get(); + if id.0 != 0 { + self.reclaim(id); } + } + } - VNode::Text(_) | VNode::Placeholder(_) => { - let id = old - .try_mounted_id() - .unwrap_or_else(|| panic!("broke on {:?}", old)); + fn remove_component(&mut self, scope_id: ScopeId) { + let height = self.scopes[scope_id.0].height; + self.dirty_scopes.remove(&DirtyScope { + height, + id: scope_id, + }); - self.mutations - .replace_with(Some(id.as_u64()), nodes_created); - self.scopes.collect_garbage(id); - } + // I promise, since we're descending down the tree, this is safe + match unsafe { self.scopes[scope_id.0].root_node().extend_lifetime_ref() } { + RenderReturn::Sync(Ok(t)) => self.remove_node(t), + _ => todo!("cannot handle nonstandard nodes"), + } + } - VNode::Fragment(f) => { - self.replace_inner(&f.children[0], nodes_created); - self.remove_nodes(f.children.iter().skip(1), true); - } + /// Push all the real nodes on the stack + fn push_all_real_nodes(&mut self, node: &'b VNode<'b>) -> usize { + let mut onstack = 0; - VNode::Component(c) => { - log::trace!("Replacing component {:?}", old); - let scope_id = c.scope.get().unwrap(); - let node = self.scopes.fin_head(scope_id); - - self.enter_scope(scope_id); - { - self.replace_inner(node, nodes_created); - - log::trace!("Replacing component x2 {:?}", old); - - let scope = self.scopes.get_scope(scope_id).unwrap(); - c.scope.set(None); - let props = scope.props.take().unwrap(); - c.props.borrow_mut().replace(props); - self.scopes.try_remove(scope_id); + for (idx, _) in node.template.roots.iter().enumerate() { + match node.dynamic_root(idx) { + Some(Text(t)) => { + self.mutations.push(Mutation::PushRoot { id: t.id.get() }); + onstack += 1; } - self.leave_scope(); - } - - VNode::TemplateRef(template_ref) => { - let templates = self.scopes.templates.borrow(); - let template = templates.get(&template_ref.template_id).unwrap().borrow(); - let mut root_iter = template.root_nodes().iter(); - let first_real_id = template_ref.get_node_id(*root_iter.next().unwrap()); - self.mutations - .replace_with(Some(first_real_id.as_u64()), nodes_created); - for id in root_iter { - let real_id = template_ref.get_node_id(*id); - self.mutations.remove(Some(real_id.as_u64())); + Some(Placeholder(t)) => { + self.mutations.push(Mutation::PushRoot { id: t.get() }); + onstack += 1; } - - self.remove_nodes(template_ref.dynamic_context.nodes, true); - - if let Some(id) = template_ref.template_ref_id.get() { - self.scopes.template_refs.borrow_mut().remove(id.0); - } - - for id in template_ref.node_ids.borrow().iter() { - if let Some(id) = id.get() { - self.scopes.collect_garbage(*id); + Some(Fragment(nodes)) => { + for node in *nodes { + onstack += self.push_all_real_nodes(node); } } - } - } - } - - pub fn remove_nodes(&mut self, nodes: impl IntoIterator>, gen_muts: bool) { - for node in nodes { - match node { - VNode::Text(t) => { - // this check exists because our null node will be removed but does not have an ID - if let Some(id) = t.id.get() { - self.scopes.collect_garbage(id); - t.id.set(None); - - if gen_muts { - self.mutations.remove(Some(id.as_u64())); + Some(Component(comp)) => { + let scope = comp.scope.get().unwrap(); + onstack += + match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { + RenderReturn::Sync(Ok(node)) => self.push_all_real_nodes(node), + _ => todo!(), } - } } - VNode::Placeholder(a) => { - let id = a.id.get().unwrap(); - self.scopes.collect_garbage(id); - a.id.set(None); - - if gen_muts { - self.mutations.remove(Some(id.as_u64())); - } + None => { + self.mutations.push(Mutation::PushRoot { + id: node.root_ids[idx].get(), + }); + onstack += 1; } - VNode::Element(e) => { - let id = e.id.get().unwrap(); + }; + } - if gen_muts { - self.mutations.remove(Some(id.as_u64())); - } + onstack + } - self.scopes.collect_garbage(id); - e.id.set(None); + fn create_children(&mut self, nodes: &'b [VNode<'b>]) -> usize { + nodes.iter().fold(0, |acc, child| acc + self.create(child)) + } - self.remove_nodes(e.children, false); - } + fn create_and_insert_before(&mut self, new: &'b [VNode<'b>], before: &'b VNode<'b>) { + let m = self.create_children(new); + let id = self.find_first_element(before); + self.mutations.push(Mutation::InsertBefore { id, m }) + } - VNode::Fragment(f) => { - self.remove_nodes(f.children, gen_muts); - } + fn create_and_insert_after(&mut self, new: &'b [VNode<'b>], after: &'b VNode<'b>) { + let m = self.create_children(new); + let id = self.find_last_element(after); + self.mutations.push(Mutation::InsertAfter { id, m }) + } - VNode::Component(c) => { - self.enter_scope(c.scope.get().unwrap()); - { - let scope_id = c.scope.get().unwrap(); - let root = self.scopes.root_node(scope_id); - self.remove_nodes([root], gen_muts); - - let scope = self.scopes.get_scope(scope_id).unwrap(); - c.scope.set(None); - - let props = scope.props.take().unwrap(); - c.props.borrow_mut().replace(props); - self.scopes.try_remove(scope_id); - } - self.leave_scope(); - } - - VNode::TemplateRef(template_ref) => { - let templates = self.scopes.templates.borrow(); - let template = templates.get(&template_ref.template_id).unwrap().borrow(); - if gen_muts { - for id in template.root_nodes() { - let real_id = template_ref.get_node_id(*id); - self.mutations.remove(Some(real_id.as_u64())); - } - } - - self.remove_nodes(template_ref.dynamic_context.nodes, gen_muts); - - if let Some(id) = template_ref.template_ref_id.get() { - self.scopes.template_refs.borrow_mut().remove(id.0); - } - - for id in template_ref.node_ids.borrow().iter() { - if let Some(id) = id.get() { - self.scopes.collect_garbage(*id); - } - } + fn find_first_element(&self, node: &'b VNode<'b>) -> ElementId { + match node.dynamic_root(0) { + None => node.root_ids[0].get(), + Some(Text(t)) => t.id.get(), + Some(Fragment(t)) => self.find_first_element(&t[0]), + Some(Placeholder(t)) => t.get(), + Some(Component(comp)) => { + let scope = comp.scope.get().unwrap(); + match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { + RenderReturn::Sync(Ok(t)) => self.find_first_element(t), + _ => todo!("cannot handle nonstandard nodes"), } } } } - fn create_children( - &mut self, - parent: ElementId, - nodes: &'b [VNode<'b>], - nodes_vec: &mut Vec, - ) { - nodes_vec.reserve(nodes.len()); - for node in nodes { - self.create_node(parent, node, nodes_vec); - } - } - - fn create_and_append_children(&mut self, parent: ElementId, nodes: &'b [VNode<'b>]) { - let mut nodes_vec = Vec::with_capacity(nodes.len()); - self.create_children(parent, nodes, &mut nodes_vec); - self.mutations - .append_children(Some(parent.as_u64()), nodes_vec); - } - - fn create_and_insert_after( - &mut self, - parent: ElementId, - nodes: &'b [VNode<'b>], - after: &'b VNode<'b>, - ) { - let mut nodes_vec = Vec::with_capacity(nodes.len()); - self.create_children(parent, nodes, &mut nodes_vec); - let last = self.find_last_element(after).unwrap(); - self.mutations.insert_after(Some(last.as_u64()), nodes_vec); - } - - fn create_and_insert_before( - &mut self, - parent: ElementId, - nodes: &'b [VNode<'b>], - before: &'b VNode<'b>, - ) { - let mut nodes_vec = Vec::with_capacity(nodes.len()); - self.create_children(parent, nodes, &mut nodes_vec); - let first = self.find_first_element(before).unwrap(); - self.mutations - .insert_before(Some(first.as_u64()), nodes_vec); - } - - pub fn current_scope(&self) -> ScopeId { - self.scope_stack.last().copied().expect("no current scope") - } - - fn enter_scope(&mut self, scope: ScopeId) { - self.scope_stack.push(scope); - } - - fn leave_scope(&mut self) { - self.scope_stack.pop(); - } - - fn find_last_element(&mut self, vnode: &'b VNode<'b>) -> Option { - let mut search_node = Some(vnode); - loop { - match &search_node.take().unwrap() { - VNode::Text(t) => break t.id.get(), - VNode::Element(t) => break t.id.get(), - VNode::Placeholder(t) => break t.id.get(), - VNode::Fragment(frag) => search_node = frag.children.last(), - VNode::Component(el) => { - let scope_id = el.scope.get().unwrap(); - search_node = Some(self.scopes.root_node(scope_id)); - } - VNode::TemplateRef(t) => { - let templates = self.scopes.templates.borrow(); - let template = templates.get(&t.template_id).unwrap(); - let template = template.borrow(); - break template.root_nodes().last().map(|id| t.get_node_id(*id)); + fn find_last_element(&self, node: &'b VNode<'b>) -> ElementId { + match node.dynamic_root(node.template.roots.len() - 1) { + None => node.root_ids.last().unwrap().get(), + Some(Text(t)) => t.id.get(), + Some(Fragment(t)) => self.find_last_element(t.last().unwrap()), + Some(Placeholder(t)) => t.get(), + Some(Component(comp)) => { + let scope = comp.scope.get().unwrap(); + match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { + RenderReturn::Sync(Ok(t)) => self.find_last_element(t), + _ => todo!("cannot handle nonstandard nodes"), } } } } - fn find_first_element(&mut self, vnode: &'b VNode<'b>) -> Option { - let mut search_node = Some(vnode); - loop { - match &search_node.take().expect("search node to have an ID") { - VNode::Text(t) => break t.id.get(), - VNode::Element(t) => break t.id.get(), - VNode::Placeholder(t) => break t.id.get(), - VNode::Fragment(frag) => search_node = Some(&frag.children[0]), - VNode::Component(el) => { - let scope = el.scope.get().expect("element to have a scope assigned"); - search_node = Some(self.scopes.root_node(scope)); - } - VNode::TemplateRef(t) => { - let templates = self.scopes.templates.borrow(); - let template = templates.get(&t.template_id).unwrap(); - let template = template.borrow(); - break template.root_nodes().first().map(|id| t.get_node_id(*id)); - } - } - } + fn replace(&mut self, left: &'b VNode<'b>, right: &'b VNode<'b>) { + let first = self.find_first_element(left); + let id = self.replace_inner(left); + let created = self.create(right); + self.mutations.push(Mutation::ReplaceWith { + id: first, + m: created, + }); + self.try_reclaim(id); } - // recursively push all the nodes of a tree onto the stack and return how many are there - fn get_all_real_nodes(&mut self, node: &'b VNode<'b>, nodes: &mut Vec) { - match node { - VNode::Text(_) | VNode::Placeholder(_) | VNode::Element(_) => { - nodes.push(node.mounted_id().0 as u64); - } - - VNode::TemplateRef(template_ref) => { - let templates = self.scopes.templates.borrow(); - let template = templates.get(&template_ref.template_id).unwrap(); - let template = template.borrow(); - nodes.extend( - template - .root_nodes() - .iter() - .map(|id| template_ref.get_node_id(*id).as_u64()), - ); - } - - VNode::Fragment(frag) => { - nodes.reserve(frag.children.len()); - for child in frag.children { - self.get_all_real_nodes(child, nodes); - } - } - - VNode::Component(c) => { - let scope_id = c.scope.get().unwrap(); - let root = self.scopes.root_node(scope_id); - self.get_all_real_nodes(root, nodes); - } - } + fn replace_many(&mut self, left: &'b VNode<'b>, right: &'b [VNode<'b>]) { + let first = self.find_first_element(left); + let id = self.replace_inner(left); + let created = self.create_children(right); + self.mutations.push(Mutation::ReplaceWith { + id: first, + m: created, + }); + self.try_reclaim(id); + } +} + +fn matching_components<'a>( + left: &'a VNode<'a>, + right: &'a VNode<'a>, +) -> Option, &'a VComponent<'a>)>> { + if left.template.roots.len() != right.template.roots.len() { + return None; } + + // run through the components, ensuring they're the same + left.template + .roots + .iter() + .zip(right.template.roots.iter()) + .map(|(l, r)| { + let (l, r) = match (l, r) { + (TemplateNode::Dynamic { id: l }, TemplateNode::Dynamic { id: r }) => (l, r), + _ => return None, + }; + + let (l, r) = match (&left.dynamic_nodes[*l], &right.dynamic_nodes[*r]) { + (Component(l), Component(r)) => (l, r), + _ => return None, + }; - pub(crate) fn current_scope_bump(&self) -> &'b Bump { - &self - .scopes - .get_scope(self.current_scope()) - .unwrap() - .fin_frame() - .bump + Some((l, r)) + }) + .collect() +} + +/// We can apply various optimizations to dynamic nodes that are the single child of their parent. +/// +/// IE +/// - for text - we can use SetTextContent +/// - for clearning children we can use RemoveChildren +/// - for appending children we can use AppendChildren +#[allow(dead_code)] +fn is_dyn_node_only_child(node: &VNode, idx: usize) -> bool { + let path = node.template.node_paths[idx]; + + // use a loop to index every static node's children until the path has run out + // only break if the last path index is a dynamic node + let mut static_node = &node.template.roots[path[0] as usize]; + + for i in 1..path.len() - 1 { + match static_node { + TemplateNode::Element { children, .. } => static_node = &children[path[i] as usize], + _ => return false, + } } - pub fn register_template(&mut self, template: &Template, id: ElementId) { - let bump = &self.scopes.template_bump; - template.create(&mut self.mutations, bump, id); + match static_node { + TemplateNode::Element { children, .. } => children.len() == 1, + _ => false, } } diff --git a/packages/core/src/dirty_scope.rs b/packages/core/src/dirty_scope.rs new file mode 100644 index 000000000..4dfbd9bd3 --- /dev/null +++ b/packages/core/src/dirty_scope.rs @@ -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 { + Some(self.height.cmp(&other.height)) + } +} + +impl Ord for DirtyScope { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.height.cmp(&other.height) + } +} diff --git a/packages/core/src/dynamic_template_context.rs b/packages/core/src/dynamic_template_context.rs deleted file mode 100644 index 577a738de..000000000 --- a/packages/core/src/dynamic_template_context.rs +++ /dev/null @@ -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(pub &'static Lazy>); - -impl AsRef<[T]> for LazyStaticVec { - fn as_ref(&self) -> &[T] { - let v: &Vec<_> = self.0.deref(); - v.as_ref() - } -} - -impl PartialEq for LazyStaticVec { - 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]>, - 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, - /// 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, - /// 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 - DynamicNodeMapping< - Nodes, - TextOuter, - TextInner, - AttributesOuter, - AttributesInner, - Volatile, - Listeners, - > -where - Nodes: AsRef<[Option]>, - 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], - &'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>, - Vec>, - Vec, - Vec>, - Vec<(TemplateNodeId, usize)>, - Vec<(TemplateNodeId, usize)>, - Vec, ->; - -/// 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( - &self, - text: &TextTemplate, - ) -> String - where - TextSegments: AsRef<[TextTemplateSegment]>, - Text: AsRef, - { - 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( - &self, - text: &TextTemplate, - result: &mut impl Write, - ) where - TextSegments: AsRef<[TextTemplateSegment]>, - Text: AsRef, - { - 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] - } -} diff --git a/packages/core/src/error_boundary.rs b/packages/core/src/error_boundary.rs new file mode 100644 index 000000000..6f40c2484 --- /dev/null +++ b/packages/core/src/error_boundary.rs @@ -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>, + id: ScopeId, +} + +impl ErrorBoundary { + pub fn new(id: ScopeId) -> Self { + Self { + error: RefCell::new(None), + id, + } + } +} diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 0668f7582..583f44c33 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -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, +/// 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| { +/// evt.cancel_bubble(); +/// +/// } +/// } +/// } +/// ``` +pub struct Event { + pub(crate) data: Rc, + pub(crate) propogates: Rc>, } -impl BubbleState { - pub fn new() -> Self { +impl Event { + /// Prevent this event from continuing to bubble up the tree to parent elements. + /// + /// # Example + /// + /// ```rust, ignore + /// rsx! { + /// button { + /// onclick: move |evt: Event| { + /// 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| { + /// 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| { + /// let data = evt.inner.clone(); + /// cx.spawn(async move { + /// println!("{:?}", data); + /// }); + /// } + /// } + /// } + /// ``` + pub fn inner(&self) -> &Rc { + &self.data + } +} + +impl Clone for Event { + 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 std::ops::Deref for Event { + type Target = Rc; + fn deref(&self) -> &Self::Target { + &self.data + } +} + +impl std::fmt::Debug for Event { + 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, - - /// 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, - - /// 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, +pub struct EventHandler<'bump, T = ()> { + pub(super) callback: RefCell>>, } -/// 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, - pub(crate) data: Arc, -} - -impl AnyEvent { - /// Convert this [`AnyEvent`] into a specific [`UiEvent`] with [`EventData`]. - /// - /// ```rust, ignore - /// let evt: FormEvent = evvt.downcast().unwrap(); - /// ``` - #[must_use] - pub fn downcast(self) -> Option> { - let AnyEvent { data, bubble_state } = self; - - data.downcast::() - .ok() - .map(|data| UiEvent { data, bubble_state }) +impl 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`] generic type. -/// -/// For the HTML crate, this would include `MouseEvent`, `FormEvent` etc. -pub struct UiEvent { - /// The internal data of the event - /// This is wrapped in an Arc so that it can be sent across threads - pub data: Arc, +type ExternalListenerCallback<'bump, T> = bumpalo::boxed::Box<'bump, dyn FnMut(T) + 'bump>; - #[allow(unused)] - bubble_state: Rc, -} +impl 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 std::fmt::Debug for UiEvent { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("UiEvent").field("data", &self.data).finish() - } -} - -impl std::ops::Deref for UiEvent { - type Target = T; - - fn deref(&self) -> &Self::Target { - self.data.as_ref() - } -} - -impl UiEvent { - /// 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); } } diff --git a/packages/core/src/fragment.rs b/packages/core/src/fragment.rs new file mode 100644 index 000000000..776d3e37a --- /dev/null +++ b/packages/core/src/fragment.rs @@ -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) -> 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 + } +} diff --git a/packages/core/src/lazynodes.rs b/packages/core/src/lazynodes.rs index 23c74eca0..020b307d9 100644 --- a/packages/core/src/lazynodes.rs +++ b/packages/core/src/lazynodes.rs @@ -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 . 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>) -> Option> + 'b>), + Heap(Box) -> Option> + '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>| { + 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(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>| { + 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(val: F) -> Self where - F: FnMut(Option>) -> Option> + 'b, + F: FnMut(Option<&'a ScopeState>) -> Option> + 'b, { - let mut ptr: *const _ = &val as &dyn FnMut(Option>) -> Option>; + let mut ptr: *const _ = &val as &dyn FnMut(Option<&'a ScopeState>) -> Option>; 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>) -> Option>>() + mem::size_of::<*mut dyn FnMut(Option<&'a ScopeState>) -> Option>>() / mem::size_of::() - 1; let info_ofs = data.len() - info_size; - let g: *mut dyn FnMut(Option>) -> Option> = + let g: *mut dyn FnMut(Option<&'a ScopeState>) -> Option> = 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>) -> Option>, - >() / mem::size_of::() - - 1; + let info_size = + mem::size_of::<*mut dyn FnMut(Option<&ScopeState>) -> Option>>() + / mem::size_of::() + - 1; let info_ofs = data.len() - info_size; - let g: *mut dyn FnMut(Option>) -> Option> = + let g: *mut dyn FnMut(Option<&ScopeState>) -> Option> = unsafe { make_fat_ptr(data[..].as_ptr() as usize, &data[info_ofs..]) }; self.dropped = true; @@ -250,73 +248,3 @@ unsafe fn make_fat_ptr(data_ptr: usize, meta_vals: &[usize]) -> *mut fn round_to_words(len: usize) -> usize { (len + mem::size_of::() - 1) / mem::size_of::() } - -#[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, - } - - fn app(cx: Scope) -> 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); - } -} diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 305d2c97c..613a4a115 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -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>; + /// An Errored [`Element`] will propagate the error to the nearest error boundary. + pub type Element<'a> = Result, 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

= fn(Scope

) -> 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(); - )*} } diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index 32f450f22..2512ae9fa 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -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>, + /// 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, - /// The list of nodes to connect to the RealDOM. - pub refs: Vec>, + /// Any templates encountered while diffing the DOM. + /// + /// These must be loaded into a cache before applying the edits + pub templates: Vec>, + + /// Any mutations required to patch the renderer to match the layout of the VirtualDom + pub edits: Vec>, } -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, + /// The ID of the element being mounted to + id: ElementId, - /// The ids of the children to append. - children: Vec, + /// 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, + /// 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, + /// 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, + id: ElementId, /// The ids of the nodes to insert after the target node. - nodes: Vec, + m: usize, }, /// Insert a number of nodes before a given node. InsertBefore { /// The ID of the node to insert before. - root: Option, + id: ElementId, /// The ids of the nodes to insert before the target node. - nodes: Vec, + m: usize, }, - /// Remove a particular node from the DOM - Remove { - /// The ID of the node to remove. - root: Option, + /// 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, + /// 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, - - /// 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, - - /// 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, + /// 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, + id: ElementId, }, /// Remove an existing Event Listener. RemoveEventListener { - /// The ID of the node to remove. - root: Option, - /// 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, - - /// 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, - - /// 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, - - /// 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, - - /// 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, - - /// The ID of the new node. - new_ids: Vec, - }, - - /// 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, nodes: Vec) { - self.edits.push(ReplaceWith { nodes, root }); - } - - pub(crate) fn insert_after(&mut self, root: Option, nodes: Vec) { - self.edits.push(InsertAfter { nodes, root }); - } - - pub(crate) fn insert_before(&mut self, root: Option, nodes: Vec) { - self.edits.push(InsertBefore { nodes, root }); - } - - pub(crate) fn append_children(&mut self, root: Option, children: Vec) { - self.edits.push(AppendChildren { root, children }); - } - - // Remove Nodes from the dom - pub(crate) fn remove(&mut self, id: Option) { - self.edits.push(Remove { root: id }); - } - - // Create - pub(crate) fn create_text_node(&mut self, text: &'a str, id: Option) { - self.edits.push(CreateTextNode { text, root: id }); - } - - pub(crate) fn create_element( - &mut self, - tag: &'static str, - ns: Option<&'static str>, - id: Option, - 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) { - 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) { - self.edits.push(RemoveEventListener { event, root }); - } - - // modify - pub(crate) fn set_text(&mut self, text: &'a str, root: Option) { - self.edits.push(SetText { text, root }); - } - - pub(crate) fn set_attribute(&mut self, attribute: &'a Attribute<'a>, root: Option) { - 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) { - 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, new_id: u64) { - self.edits.push(CloneNode { id, new_id }); - } - - pub(crate) fn clone_node_children(&mut self, id: Option, new_ids: Vec) { - 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>>, - 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(&self) -> Option<&T> { - self.element - .as_ref() - .and_then(|f| f.get()) - .and_then(|f| f.downcast_ref::()) - } - pub fn downcast_mut(&mut self) -> Option<&mut T> { - self.element - .as_mut() - .and_then(|f| f.get_mut()) - .and_then(|f| f.downcast_mut::()) - } -} diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index fed8c548d..02c5bed99 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -1,949 +1,576 @@ -//! Virtual Node Support -//! -//! VNodes represent lazily-constructed VDom trees that support diffing and event handlers. These VNodes should be *very* -//! cheap and *very* fast to construct - building a full tree should be quick. use crate::{ - dynamic_template_context::TemplateContext, - innerlude::{ - AttributeValue, ComponentPtr, Element, IntoAttributeValue, Properties, Scope, ScopeId, - ScopeState, Template, TemplateId, - }, - lazynodes::LazyNodes, - template::VTemplateRef, - AnyEvent, Component, + any_props::AnyProps, arena::ElementId, Element, Event, LazyNodes, ScopeId, ScopeState, }; -use bumpalo::{boxed::Box as BumpBox, Bump}; +use bumpalo::boxed::Box as BumpBox; +use bumpalo::Bump; use std::{ + any::{Any, TypeId}, cell::{Cell, RefCell}, - fmt::{Arguments, Debug, Formatter}, - rc::Rc, + fmt::Arguments, + future::Future, }; -/// A composable "VirtualNode" to declare a User Interface in the Dioxus VirtualDOM. +pub type TemplateId = &'static str; + +/// The actual state of the component's most recent computation /// -/// VNodes are designed to be lightweight and used with with a bump allocator. To create a VNode, you can use either of: +/// Because Dioxus accepts components in the form of `async fn(Scope) -> Result`, we need to support both +/// sync and async versions. /// -/// - the `rsx!` macro -/// - the [`NodeFactory`] API -pub enum VNode<'src> { - /// Text VNodes are simply bump-allocated (or static) string slices - /// - /// # Example - /// - /// ```rust, ignore - /// let mut vdom = VirtualDom::new(); - /// let node = vdom.render_vnode(rsx!( "hello" )); - /// - /// if let VNode::Text(vtext) = node { - /// assert_eq!(vtext.text, "hello"); - /// assert_eq!(vtext.dom_id.get(), None); - /// assert_eq!(vtext.is_static, true); - /// } - /// ``` - Text(&'src VText<'src>), +/// Dioxus will do its best to immediately resolve any async components into a regular Element, but as an implementor +/// you might need to handle the case where there's no node immediately ready. +pub enum RenderReturn<'a> { + /// A currently-available element + Sync(Element<'a>), - /// Element VNodes are VNodes that may contain attributes, listeners, a key, a tag, and children. - /// - /// # Example - /// - /// ```rust, ignore - /// let mut vdom = VirtualDom::new(); - /// - /// let node = vdom.render_vnode(rsx!{ - /// div { - /// key: "a", - /// onclick: |e| log::info!("clicked"), - /// hidden: "true", - /// style: { background_color: "red" }, - /// "hello" - /// } - /// }); - /// - /// if let VNode::Element(velement) = node { - /// assert_eq!(velement.tag_name, "div"); - /// assert_eq!(velement.namespace, None); - /// assert_eq!(velement.key, Some("a")); - /// } - /// ``` - Element(&'src VElement<'src>), - - /// Fragment nodes may contain many VNodes without a single root. - /// - /// # Example - /// - /// ```rust, ignore - /// rsx!{ - /// a {} - /// link {} - /// style {} - /// "asd" - /// Example {} - /// } - /// ``` - Fragment(&'src VFragment<'src>), - - /// Component nodes represent a mounted component with props, children, and a key. - /// - /// # Example - /// - /// ```rust, ignore - /// fn Example(cx: Scope) -> Element { - /// ... - /// } - /// - /// let mut vdom = VirtualDom::new(); - /// - /// let node = vdom.render_vnode(rsx!( Example {} )); - /// - /// if let VNode::Component(vcomp) = node { - /// assert_eq!(vcomp.user_fc, Example as *const ()); - /// } - /// ``` - Component(&'src VComponent<'src>), - - /// Placeholders are a type of placeholder VNode used when fragments don't contain any children. - /// - /// Placeholders cannot be directly constructed via public APIs. - /// - /// # Example - /// - /// ```rust, ignore - /// let mut vdom = VirtualDom::new(); - /// - /// let node = vdom.render_vnode(rsx!( Fragment {} )); - /// - /// if let VNode::Fragment(frag) = node { - /// let root = &frag.children[0]; - /// assert_eq!(root, VNode::Anchor); - /// } - /// ``` - Placeholder(&'src VPlaceholder), - - /// Templetes ase generated by the rsx macro to eleminate diffing static nodes. - TemplateRef(&'src VTemplateRef<'src>), + /// An ongoing future that will resolve to a [`Element`] + Async(BumpBox<'a, dyn Future> + 'a>), } -impl<'src> VNode<'src> { - /// Get the VNode's "key" used in the keyed diffing algorithm. - pub fn key(&self) -> Option<&'src str> { - match &self { - VNode::Element(el) => el.key, - VNode::Component(c) => c.key, - VNode::Fragment(f) => f.key, - VNode::Text(_t) => None, - VNode::Placeholder(_f) => None, - VNode::TemplateRef(t) => t.dynamic_context.key, - } - } - - /// Get the ElementID of the mounted VNode. - /// - /// Panics if the mounted ID is None or if the VNode is not represented by a single Element. - pub fn mounted_id(&self) -> ElementId { - self.try_mounted_id().unwrap() - } - - /// Try to get the ElementID of the mounted VNode. - /// - /// Returns None if the VNode is not mounted, or if the VNode cannot be presented by a mounted ID (Fragment/Component) - pub fn try_mounted_id(&self) -> Option { - match &self { - VNode::Text(el) => el.id.get(), - VNode::Element(el) => el.id.get(), - VNode::Placeholder(el) => el.id.get(), - VNode::Fragment(_) => None, - VNode::Component(_) => None, - VNode::TemplateRef(_) => None, - } - } - - // Create an "owned" version of the vnode. - pub(crate) fn decouple(&self) -> VNode<'src> { - match *self { - VNode::Text(t) => VNode::Text(t), - VNode::Element(e) => VNode::Element(e), - VNode::Component(c) => VNode::Component(c), - VNode::Placeholder(a) => VNode::Placeholder(a), - VNode::Fragment(f) => VNode::Fragment(f), - VNode::TemplateRef(t) => VNode::TemplateRef(t), - } - } -} - -impl Debug for VNode<'_> { - fn fmt(&self, s: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - match &self { - VNode::Element(el) => s - .debug_struct("VNode::Element") - .field("name", &el.tag) - .field("key", &el.key) - .field("attrs", &el.attributes) - .field("children", &el.children) - .field("id", &el.id) - .finish(), - VNode::Text(t) => s - .debug_struct("VNode::Text") - .field("text", &t.text) - .field("id", &t.id) - .finish(), - VNode::Placeholder(t) => s - .debug_struct("VNode::Placholder") - .field("id", &t.id) - .finish(), - VNode::Fragment(frag) => s - .debug_struct("VNode::Fragment") - .field("children", &frag.children) - .finish(), - VNode::Component(comp) => s - .debug_struct("VNode::Component") - .field("name", &comp.fn_name) - .field("fnptr", &comp.user_fc) - .field("key", &comp.key) - .field("scope", &comp.scope) - .finish(), - VNode::TemplateRef(temp) => s - .debug_struct("VNode::TemplateRef") - .field("template_id", &temp.template_id) - .finish(), - } - } -} - -/// An Element's unique identifier. +/// A reference to a template along with any context needed to hydrate it /// -/// `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. -#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serialize", serde(transparent))] -pub struct ElementId(pub usize); -impl std::fmt::Display for ElementId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl ElementId { - /// Convert the ElementId to a `u64`. - pub fn as_u64(&self) -> u64 { - (*self).into() - } -} - -impl From for u64 { - fn from(el: ElementId) -> u64 { - el.0 as u64 - } -} - -fn empty_cell() -> Cell> { - Cell::new(None) -} - -/// A placeholder node only generated when Fragments don't have any children. -pub struct VPlaceholder { - /// The [`ElementId`] of the placeholder. - pub id: Cell>, -} - -/// A bump-allocated string slice and metadata. -pub struct VText<'src> { - /// The [`ElementId`] of the VText. - pub id: Cell>, - - /// The text of the VText. - pub text: &'src str, - - /// An indiciation if this VText can be ignored during diffing - /// Is usually only when there are no strings to be formatted (so the text is &'static str) - pub is_static: bool, -} - -/// A list of VNodes with no single root. -pub struct VFragment<'src> { - /// The key of the fragment to be used during keyed diffing. - pub key: Option<&'src str>, - - /// Fragments can never have zero children. Enforced by NodeFactory. +/// The dynamic parts of the template are stored separately from the static parts. This allows faster diffing by skipping +/// static parts of the template. +#[derive(Debug, Clone)] +pub struct VNode<'a> { + /// The key given to the root of this template. /// - /// You *can* make a fragment with no children, but it's not a valid fragment and your VDom will panic. - pub children: &'src [VNode<'src>], -} - -/// An element like a "div" with children, listeners, and attributes. -pub struct VElement<'a> { - /// The [`ElementId`] of the VText. - pub id: Cell>, - - /// The key of the element to be used during keyed diffing. + /// In fragments, this is the key of the first child. In other cases, it is the key of the root. pub key: Option<&'a str>, - /// The tag name of the element. - /// - /// IE "div" - pub tag: &'static str, + /// When rendered, this template will be linked to its parent manually + pub parent: Option, - /// The namespace of the VElement - /// - /// IE "svg" - pub namespace: Option<&'static str>, + /// The static nodes and static descriptor of the template + pub template: Template<'static>, - /// The parent of the Element (if any). - /// - /// Used when bubbling events - pub parent: Cell>, + /// The IDs for the roots of this template - to be used when moving the template around and removing it from + /// the actual Dom + pub root_ids: &'a [Cell], - /// The Listeners of the VElement. - pub listeners: &'a [Listener<'a>], + /// The dynamic parts of the template + pub dynamic_nodes: &'a [DynamicNode<'a>], - /// The attributes of the VElement. - pub attributes: &'a [Attribute<'a>], - - /// The children of the VElement. - pub children: &'a [VNode<'a>], + /// The dynamic parts of the template + pub dynamic_attrs: &'a [Attribute<'a>], } -impl Debug for VElement<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("VElement") - .field("tag_name", &self.tag) - .field("namespace", &self.namespace) - .field("key", &self.key) - .field("id", &self.id) - .field("parent", &self.parent) - .field("listeners", &self.listeners.len()) - .field("attributes", &self.attributes) - .field("children", &self.children) +impl<'a> VNode<'a> { + /// Create a template with no nodes that will be skipped over during diffing + pub fn empty() -> Element<'a> { + Ok(VNode { + key: None, + parent: None, + root_ids: &[], + dynamic_nodes: &[], + dynamic_attrs: &[], + template: Template { + name: "dioxus-empty", + roots: &[], + node_paths: &[], + attr_paths: &[], + }, + }) + } + + /// Load a dynamic root at the given index + /// + /// Returns [`None`] if the root is actually a static node (Element/Text) + pub fn dynamic_root(&self, idx: usize) -> Option<&'a DynamicNode<'a>> { + match &self.template.roots[idx] { + TemplateNode::Element { .. } | TemplateNode::Text { text: _ } => None, + TemplateNode::Dynamic { id } | TemplateNode::DynamicText { id } => { + Some(&self.dynamic_nodes[*id]) + } + } + } + + pub(crate) fn clear_listeners(&self) { + for attr in self.dynamic_attrs { + if let AttributeValue::Listener(l) = &attr.value { + l.borrow_mut().take(); + } + } + } +} + +/// A static layout of a UI tree that describes a set of dynamic and static nodes. +/// +/// This is the core innovation in Dioxus. Most UIs are made of static nodes, yet participate in diffing like any +/// dynamic node. This struct can be created at compile time. It promises that its name is unique, allow Dioxus to use +/// its static description of the UI to skip immediately to the dynamic nodes during diffing. +/// +/// For this to work properly, the [`Template::name`] *must* be unique across your entire project. This can be done via variety of +/// ways, with the suggested approach being the unique code location (file, line, col, etc). +#[cfg_attr(feature = "serialize", derive(serde::Serialize))] +#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord)] +pub struct Template<'a> { + /// The name of the template. This must be unique across your entire program for template diffing to work properly + /// + /// If two templates have the same name, it's likely that Dioxus will panic when diffing. + pub name: &'a str, + + /// The list of template nodes that make up the template + /// + /// Unlike react, calls to `rsx!` can have multiple roots. This list supports that paradigm. + pub roots: &'a [TemplateNode<'a>], + + /// The paths of each node relative to the root of the template. + /// + /// These will be one segment shorter than the path sent to the renderer since those paths are relative to the + /// topmost element, not the `roots` field. + pub node_paths: &'a [&'a [u8]], + + /// The paths of each dynamic attribute relative to the root of the template + /// + /// These will be one segment shorter than the path sent to the renderer since those paths are relative to the + /// topmost element, not the `roots` field. + pub attr_paths: &'a [&'a [u8]], +} + +/// A statically known node in a layout. +/// +/// This can be created at compile time, saving the VirtualDom time when diffing the tree +#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize), serde(tag = "type"))] +pub enum TemplateNode<'a> { + /// An statically known element in the dom. + /// + /// In HTML this would be something like `

` + Element { + /// The name of the element + /// + /// IE for a div, it would be the string "div" + tag: &'a str, + + /// The namespace of the element + /// + /// In HTML, this would be a valid URI that defines a namespace for all elements below it + /// SVG is an example of this namespace + namespace: Option<&'a str>, + + /// A list of possibly dynamic attribues for this element + /// + /// An attribute on a DOM node, such as `id="my-thing"` or `href="https://example.com"`. + attrs: &'a [TemplateAttribute<'a>], + + /// A list of template nodes that define another set of template nodes + children: &'a [TemplateNode<'a>], + }, + + /// This template node is just a piece of static text + Text { + /// The actual text + text: &'a str, + }, + + /// This template node is unknown, and needs to be created at runtime. + Dynamic { + /// The index of the dynamic node in the VNode's dynamic_nodes list + id: usize, + }, + + /// This template node is known to be some text, but needs to be created at runtime + /// + /// This is separate from the pure Dynamic variant for various optimizations + DynamicText { + /// The index of the dynamic node in the VNode's dynamic_nodes list + id: usize, + }, +} + +/// A node created at runtime +/// +/// This node's index in the DynamicNode list on VNode should match its repsective `Dynamic` index +#[derive(Debug)] +pub enum DynamicNode<'a> { + /// A component node + /// + /// Most of the time, Dioxus will actually know which component this is as compile time, but the props and + /// assigned scope are dynamic. + /// + /// The actual VComponent can be dynamic between two VNodes, though, allowing implementations to swap + /// the render function at runtime + Component(VComponent<'a>), + + /// A text node + Text(VText<'a>), + + /// A placeholder + /// + /// Used by suspense when a node isn't ready and by fragments that don't render anything + /// + /// In code, this is just an ElementId whose initial value is set to 0 upon creation + Placeholder(Cell), + + /// A list of VNodes. + /// + /// Note that this is not a list of dynamic nodes. These must be VNodes and created through conditional rendering + /// or iterators. + Fragment(&'a [VNode<'a>]), +} + +impl Default for DynamicNode<'_> { + fn default() -> Self { + Self::Placeholder(Default::default()) + } +} + +/// An instance of a child component +pub struct VComponent<'a> { + /// The name of this component + pub name: &'static str, + + /// Are the props valid for the 'static lifetime? + /// + /// Internally, this is used as a guarantee. Externally, this might be incorrect, so don't count on it. + /// + /// This flag is assumed by the [`crate::Properties`] trait which is unsafe to implement + pub static_props: bool, + + /// The assigned Scope for this component + pub scope: Cell>, + + /// The function pointer of the component, known at compile time + /// + /// It is possible that components get folded at comppile time, so these shouldn't be really used as a key + pub render_fn: *const (), + + pub(crate) props: Cell + 'a>>>, +} + +impl<'a> std::fmt::Debug for VComponent<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("VComponent") + .field("name", &self.name) + .field("static_props", &self.static_props) + .field("scope", &self.scope) .finish() } } -/// A trait for any generic Dioxus Element. -/// -/// This trait provides the ability to use custom elements in the `rsx!` macro. -/// -/// ```rust, ignore -/// struct my_element; -/// -/// impl DioxusElement for my_element { -/// const TAG_NAME: "my_element"; -/// const NAME_SPACE: None; -/// } -/// -/// let _ = rsx!{ -/// my_element {} -/// }; -/// ``` -pub trait DioxusElement { - /// The tag name of the element. - const TAG_NAME: &'static str; +/// An instance of some text, mounted to the DOM +#[derive(Debug)] +pub struct VText<'a> { + /// The actual text itself + pub value: &'a str, - /// The namespace of the element. - const NAME_SPACE: Option<&'static str>; - - /// The tag name of the element. - #[inline] - fn tag_name(&self) -> &'static str { - Self::TAG_NAME - } - - /// The namespace of the element. - #[inline] - fn namespace(&self) -> Option<&'static str> { - Self::NAME_SPACE - } + /// The ID of this node in the real DOM + pub id: Cell, } -type StaticStr = &'static str; - -/// A discription of the attribute -#[derive(Clone, Copy, Debug, PartialEq, Hash, Eq)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize))] +/// An attribute of the TemplateNode, created at compile time +#[derive(Debug, PartialEq, Hash, Eq, PartialOrd, Ord)] #[cfg_attr( - all(feature = "serialize", any(feature = "hot-reload", debug_assertions)), - derive(serde::Deserialize) + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + serde(tag = "type") )] -pub struct AttributeDiscription { +pub enum TemplateAttribute<'a> { + /// This attribute is entirely known at compile time, enabling + Static { + /// The name of this attribute. + /// + /// For example, the `href` attribute in `href="https://example.com"`, would have the name "href" + name: &'a str, + + /// The value of this attribute, known at compile time + /// + /// Currently this only accepts &str, so values, even if they're known at compile time, are not known + value: &'a str, + + /// The namespace of this attribute. Does not exist in the HTML spec + namespace: Option<&'a str>, + }, + + /// The attribute in this position is actually determined dynamically at runtime + /// + /// This is the index into the dynamic_attributes field on the container VNode + Dynamic { + /// The index + id: usize, + }, +} + +/// An attribute on a DOM node, such as `id="my-thing"` or `href="https://example.com"` +#[derive(Debug)] +pub struct Attribute<'a> { /// The name of the attribute. - #[cfg_attr( - all(feature = "serialize", any(feature = "hot-reload", debug_assertions)), - serde(deserialize_with = "crate::util::deserialize_static_leaky") - )] - pub name: StaticStr, + pub name: &'a str, + + /// The value of the attribute + pub value: AttributeValue<'a>, /// The namespace of the attribute. /// - /// Doesn't exist in the html spec. - /// Used in Dioxus to denote "style" tags and other attribute groups. - #[cfg_attr( - all(feature = "serialize", any(feature = "hot-reload", debug_assertions)), - serde(deserialize_with = "crate::util::deserialize_static_leaky_ns") - )] - pub namespace: Option, + /// Doesn’t exist in the html spec. Used in Dioxus to denote “style” tags and other attribute groups. + pub namespace: Option<&'static str>, - /// An indication of we should always try and set the attribute. - /// Used in controlled components to ensure changes are propagated. + /// The element in the DOM that this attribute belongs to + pub mounted_element: Cell, + + /// An indication of we should always try and set the attribute. Used in controlled components to ensure changes are propagated pub volatile: bool, } -/// An attribute on a DOM node, such as `id="my-thing"` or -/// `href="https://example.com"`. -#[derive(Clone, Debug)] -pub struct Attribute<'a> { - /// The discription of the attribute. - pub attribute: AttributeDiscription, +/// Any of the built-in values that the Dioxus VirtualDom supports as dynamic attributes on elements +/// +/// These are built-in to be faster during the diffing process. To use a custom value, use the [`AttributeValue::Any`] +/// variant. +pub enum AttributeValue<'a> { + /// Text attribute + Text(&'a str), - /// An indication if this attribute can be ignored during diffing + /// A float + Float(f64), + + /// Signed integer + Int(i64), + + /// Boolean + Bool(bool), + + /// A listener, like "onclick" + Listener(RefCell>>), + + /// An arbitrary value that implements PartialEq and is static + Any(BumpBox<'a, dyn AnyValue>), + + /// A "none" value, resulting in the removal of an attribute from the dom + None, +} + +type ListenerCb<'a> = BumpBox<'a, dyn FnMut(Event) + 'a>; + +impl<'a> std::fmt::Debug for AttributeValue<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Text(arg0) => f.debug_tuple("Text").field(arg0).finish(), + Self::Float(arg0) => f.debug_tuple("Float").field(arg0).finish(), + Self::Int(arg0) => f.debug_tuple("Int").field(arg0).finish(), + Self::Bool(arg0) => f.debug_tuple("Bool").field(arg0).finish(), + Self::Listener(_) => f.debug_tuple("Listener").finish(), + Self::Any(_) => f.debug_tuple("Any").finish(), + Self::None => write!(f, "None"), + } + } +} + +impl<'a> PartialEq for AttributeValue<'a> { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Text(l0), Self::Text(r0)) => l0 == r0, + (Self::Float(l0), Self::Float(r0)) => l0 == r0, + (Self::Int(l0), Self::Int(r0)) => l0 == r0, + (Self::Bool(l0), Self::Bool(r0)) => l0 == r0, + (Self::Listener(_), Self::Listener(_)) => true, + (Self::Any(l0), Self::Any(r0)) => l0.any_cmp(r0.as_ref()), + _ => core::mem::discriminant(self) == core::mem::discriminant(other), + } + } +} + +#[doc(hidden)] +pub trait AnyValue { + fn any_cmp(&self, other: &dyn AnyValue) -> bool; + fn our_typeid(&self) -> TypeId; +} + +impl AnyValue for T { + fn any_cmp(&self, other: &dyn AnyValue) -> bool { + if self.type_id() != other.our_typeid() { + return false; + } + + self == unsafe { &*(other as *const _ as *const T) } + } + + fn our_typeid(&self) -> TypeId { + self.type_id() + } +} + +#[doc(hidden)] +pub trait ComponentReturn<'a, A = ()> { + fn into_return(self, cx: &'a ScopeState) -> RenderReturn<'a>; +} + +impl<'a> ComponentReturn<'a> for Element<'a> { + fn into_return(self, _cx: &ScopeState) -> RenderReturn<'a> { + RenderReturn::Sync(self) + } +} + +#[doc(hidden)] +pub struct AsyncMarker; +impl<'a, F> ComponentReturn<'a, AsyncMarker> for F +where + F: Future> + 'a, +{ + fn into_return(self, cx: &'a ScopeState) -> RenderReturn<'a> { + let f: &mut dyn Future> = cx.bump().alloc(self); + RenderReturn::Async(unsafe { BumpBox::from_raw(f) }) + } +} + +impl<'a> RenderReturn<'a> { + pub(crate) unsafe fn extend_lifetime_ref<'c>(&self) -> &'c RenderReturn<'c> { + unsafe { std::mem::transmute(self) } + } + pub(crate) unsafe fn extend_lifetime<'c>(self) -> RenderReturn<'c> { + unsafe { std::mem::transmute(self) } + } +} + +/// A trait that allows various items to be converted into a dynamic node for the rsx macro +pub trait IntoDynNode<'a, A = ()> { + /// Consume this item along with a scopestate and produce a DynamicNode /// - /// Usually only when there are no strings to be formatted (so the value is &'static str) - pub is_static: bool, - - /// The value of the attribute. - pub value: AttributeValue<'a>, + /// You can use the bump alloactor of the scopestate to creat the dynamic node + fn into_vnode(self, cx: &'a ScopeState) -> DynamicNode<'a>; } -/// An event listener. -/// IE onclick, onkeydown, etc -pub struct Listener<'bump> { - /// The ID of the node that this listener is mounted to - /// Used to generate the event listener's ID on the DOM - pub mounted_node: Cell>, - - /// The type of event to listen for. - /// - /// IE "click" - whatever the renderer needs to attach the listener by name. - pub event: &'static str, - - /// The actual callback that the user specified - pub(crate) callback: InternalHandler<'bump>, +impl<'a> IntoDynNode<'a> for () { + fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { + DynamicNode::default() + } +} +impl<'a> IntoDynNode<'a> for VNode<'a> { + fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { + DynamicNode::Fragment(_cx.bump().alloc([self])) + } } -pub type InternalHandler<'bump> = &'bump RefCell>>; -type InternalListenerCallback<'bump> = BumpBox<'bump, dyn FnMut(AnyEvent) + 'bump>; -type ExternalListenerCallback<'bump, T> = BumpBox<'bump, dyn FnMut(T) + 'bump>; - -/// The callback type generated by the `rsx!` macro when an `on` field is specified for components. -/// -/// This makes it possible to pass `move |evt| {}` style closures into components as property fields. -/// -/// -/// # Example -/// -/// ```rust, ignore -/// -/// 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), -/// } -/// }) -/// } -/// -/// ``` -pub struct EventHandler<'bump, T = ()> { - /// The (optional) callback that the user specified - /// Uses a `RefCell` to allow for interior mutability, and FnMut closures. - pub callback: RefCell>>, -} - -impl<'a, T> Default for EventHandler<'a, T> { - fn default() -> Self { - Self { - callback: RefCell::new(None), +// An element that's an error is currently lost into the ether +impl<'a> IntoDynNode<'a> for Element<'a> { + fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { + match self { + Ok(val) => val.into_vnode(_cx), + _ => DynamicNode::default(), } } } -impl EventHandler<'_, T> { - /// Call this event handler with the appropriate event type - pub fn call(&self, event: T) { - log::trace!("calling event handler"); - if let Some(callback) = self.callback.borrow_mut().as_mut() { - callback(event); +impl<'a, T: IntoDynNode<'a>> IntoDynNode<'a> for Option { + fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { + match self { + Some(val) => val.into_vnode(_cx), + None => DynamicNode::default(), } - log::trace!("done"); - } - - /// Forcibly drop the internal handler callback, releasing memory - pub fn release(&self) { - self.callback.replace(None); } } -/// Virtual Components for custom user-defined components -/// Only supports the functional syntax -pub struct VComponent<'src> { - /// The key of the component to be used during keyed diffing. - pub key: Option<&'src str>, - - /// The ID of the component. - /// Will not be assigned until after the component has been initialized. - pub scope: Cell>, - - /// An indication if the component is static (can be memozied) - pub can_memoize: bool, - - /// The function pointer to the component's render function. - pub user_fc: ComponentPtr, - - /// The actual name of the component. - pub fn_name: &'static str, - - /// The props of the component. - pub props: RefCell>>, -} - -pub(crate) struct VComponentProps

{ - pub render_fn: Component

, - pub memo: unsafe fn(&P, &P) -> bool, - pub props: P, -} - -pub trait AnyProps { - fn as_ptr(&self) -> *const (); - fn render<'a>(&'a self, bump: &'a ScopeState) -> Element<'a>; - unsafe fn memoize(&self, other: &dyn AnyProps) -> bool; -} - -impl

AnyProps for VComponentProps

{ - fn as_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.as_ptr() as *const _ as *const P); - let real_us: &P = &*(self.as_ptr() as *const _ as *const P); - (self.memo)(real_us, real_other) - } - - fn render<'a>(&'a self, scope: &'a ScopeState) -> Element<'a> { - let props = unsafe { std::mem::transmute::<&P, &P>(&self.props) }; - (self.render_fn)(Scope { scope, props }) +impl<'a> IntoDynNode<'a> for &Element<'a> { + fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { + match self.as_ref() { + Ok(val) => val.clone().into_vnode(_cx), + _ => DynamicNode::default(), + } } } -/// This struct provides an ergonomic API to quickly build VNodes. -/// -/// NodeFactory is used to build VNodes in the component's memory space. -/// This struct adds metadata to the final VNode about listeners, attributes, and children -#[derive(Copy, Clone)] -pub struct NodeFactory<'a> { - pub(crate) scope: &'a ScopeState, - pub(crate) bump: &'a Bump, -} - -impl<'a> NodeFactory<'a> { - /// Create a new [`NodeFactory`] from a [`Scope`] or [`ScopeState`] - pub fn new(scope: &'a ScopeState) -> NodeFactory<'a> { - NodeFactory { - scope, - bump: &scope.wip_frame().bump, - } - } - - /// Get the custom allocator for this component - #[inline] - pub fn bump(&self) -> &'a bumpalo::Bump { - self.bump - } - - /// Directly pass in text blocks without the need to use the format_args macro. - pub fn static_text(&self, text: &'static str) -> VNode<'a> { - VNode::Text(self.bump.alloc(VText { - id: empty_cell(), - text, - is_static: true, - })) - } - - /// Parses a lazy text Arguments and returns a string and a flag indicating if the text is 'static - /// - /// Text that's static may be pointer compared, making it cheaper to diff - pub fn raw_text(&self, args: Arguments) -> (&'a str, bool) { - match args.as_str() { - Some(static_str) => (static_str, true), - None => { - use bumpalo::core_alloc::fmt::Write; - let mut str_buf = bumpalo::collections::String::new_in(self.bump); - str_buf.write_fmt(args).unwrap(); - (str_buf.into_bump_str(), false) - } - } - } - - /// Create some text that's allocated along with the other vnodes - /// - pub fn text(&self, args: Arguments) -> VNode<'a> { - let (text, is_static) = self.raw_text(args); - - VNode::Text(self.bump.alloc(VText { - text, - is_static, - id: empty_cell(), - })) - } - - /// Create a new [`VNode::Element`] - pub fn element( - &self, - el: impl DioxusElement, - listeners: &'a [Listener<'a>], - attributes: &'a [Attribute<'a>], - children: &'a [VNode<'a>], - key: Option, - ) -> VNode<'a> { - self.raw_element( - el.tag_name(), - el.namespace(), - listeners, - attributes, - children, - key, - ) - } - - /// Create a new [`VNode::Element`] without the trait bound - /// - /// IE pass in "div" instead of `div` - pub fn raw_element( - &self, - tag_name: &'static str, - namespace: Option<&'static str>, - listeners: &'a [Listener<'a>], - attributes: &'a [Attribute<'a>], - children: &'a [VNode<'a>], - key: Option, - ) -> VNode<'a> { - let key = key.map(|f| self.raw_text(f).0); - - let mut items = self.scope.items.borrow_mut(); - for listener in listeners { - let long_listener = unsafe { std::mem::transmute(listener) }; - items.listeners.push(long_listener); - } - - VNode::Element(self.bump.alloc(VElement { - tag: tag_name, - key, - namespace, - listeners, - attributes, - children, - id: empty_cell(), - parent: empty_cell(), - })) - } - - /// Create a new [`Attribute`] from a attribute discrimination and a value - pub fn attr_disciption( - &self, - discription: AttributeDiscription, - val: impl IntoAttributeValue<'a>, - ) -> Attribute<'a> { - Attribute { - attribute: discription, - is_static: false, - value: val.into_value(self.bump), - } - } - - /// Create a new [`Attribute`] - pub fn attr( - &self, - name: &'static str, - val: impl IntoAttributeValue<'a>, - namespace: Option<&'static str>, - is_volatile: bool, - ) -> Attribute<'a> { - Attribute { - attribute: AttributeDiscription { - name, - namespace, - volatile: is_volatile, - }, - is_static: false, - value: val.into_value(self.bump), - } - } - - /// Create a new [`Attribute`] using non-arguments - pub fn custom_attr( - &self, - name: &'static str, - value: AttributeValue<'a>, - namespace: Option<&'static str>, - is_volatile: bool, - is_static: bool, - ) -> Attribute<'a> { - Attribute { - attribute: AttributeDiscription { - name, - namespace, - volatile: is_volatile, - }, - is_static, - value, - } - } - - /// Create a new [`VNode::Component`] - pub fn component

( - &self, - component: fn(Scope<'a, P>) -> Element, - props: P, - key: Option, - fn_name: &'static str, - ) -> VNode<'a> - where - P: Properties + 'a, - { - let vcomp = self.bump.alloc(VComponent { - key: key.map(|f| self.raw_text(f).0), - scope: Default::default(), - can_memoize: P::IS_STATIC, - user_fc: component as ComponentPtr, - fn_name, - props: RefCell::new(Some(Box::new(VComponentProps { - props, - memo: P::memoize, // smuggle the memoization function across borders - - // i'm sorry but I just need to bludgeon the lifetimes into place here - // this is safe because we're managing all lifetimes to originate from previous calls - // the intricacies of Rust's lifetime system make it difficult to properly express - // the transformation from this specific lifetime to the for<'a> lifetime - render_fn: unsafe { std::mem::transmute(component) }, - }))), - }); - - if !P::IS_STATIC { - let vcomp = &*vcomp; - let vcomp = unsafe { std::mem::transmute(vcomp) }; - self.scope.items.borrow_mut().borrowed_props.push(vcomp); - } - - VNode::Component(vcomp) - } - - /// Create a new [`Listener`] - pub fn listener(self, event: &'static str, callback: InternalHandler<'a>) -> Listener<'a> { - Listener { - event, - mounted_node: Cell::new(None), - callback, - } - } - - /// Create a new [`VNode::Fragment`] from a root of the rsx! call - pub fn fragment_root<'b, 'c>( - self, - node_iter: impl IntoIterator + 'c> + 'b, - ) -> VNode<'a> { - let mut nodes = bumpalo::collections::Vec::new_in(self.bump); - - for node in node_iter { - nodes.push(node.into_vnode(self)); - } - - if nodes.is_empty() { - VNode::Placeholder(self.bump.alloc(VPlaceholder { id: empty_cell() })) - } else { - VNode::Fragment(self.bump.alloc(VFragment { - children: nodes.into_bump_slice(), - key: None, - })) - } - } - - /// Create a new [`VNode::Fragment`] from any iterator - pub fn fragment_from_iter<'c, I, J>( - self, - node_iter: impl IntoVNode<'a, I, J> + 'c, - ) -> VNode<'a> { - node_iter.into_vnode(self) - } - - /// Create a new [`VNode`] from any iterator of children - pub fn create_children( - self, - node_iter: impl IntoIterator>, - ) -> Element<'a> { - let mut nodes = bumpalo::collections::Vec::new_in(self.bump); - - for node in node_iter { - nodes.push(node.into_vnode(self)); - } - - if nodes.is_empty() { - Some(VNode::Placeholder( - self.bump.alloc(VPlaceholder { id: empty_cell() }), - )) - } else { - let children = nodes.into_bump_slice(); - - Some(VNode::Fragment(self.bump.alloc(VFragment { - children, - key: None, - }))) - } - } - - /// Create a new [`EventHandler`] from an [`FnMut`] - pub fn event_handler(self, f: impl FnMut(T) + 'a) -> EventHandler<'a, T> { - let handler: &mut dyn FnMut(T) = self.bump.alloc(f); - let caller = unsafe { BumpBox::from_raw(handler as *mut dyn FnMut(T)) }; - let callback = RefCell::new(Some(caller)); - EventHandler { callback } - } - - /// Create a refrence to a template - pub fn template_ref( - &self, - id: TemplateId, - template: Template, - dynamic_context: TemplateContext<'a>, - ) -> VNode<'a> { - let borrow_ref = self.scope.templates.borrow(); - // We only create the template if it doesn't already exist to allow for hot reloading - if !borrow_ref.contains_key(&id) { - drop(borrow_ref); - let mut borrow_mut = self.scope.templates.borrow_mut(); - borrow_mut.insert(id.clone(), Rc::new(RefCell::new(template))); - } - VNode::TemplateRef(self.bump.alloc(VTemplateRef { - dynamic_context, - template_id: id, - node_ids: RefCell::new(Vec::new()), - parent: Cell::new(None), - template_ref_id: Cell::new(None), - })) +impl<'a, 'b> IntoDynNode<'a> for LazyNodes<'a, 'b> { + fn into_vnode(self, cx: &'a ScopeState) -> DynamicNode<'a> { + DynamicNode::Fragment(cx.bump().alloc([self.call(cx)])) } } -impl Debug for NodeFactory<'_> { - fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - Ok(()) +impl<'a> IntoDynNode<'_> for &'a str { + fn into_vnode(self, cx: &ScopeState) -> DynamicNode { + cx.text_node(format_args!("{}", self)) } } -/// Trait implementations for use in the rsx! and html! macros. -/// -/// ## Details -/// -/// This section provides convenience methods and trait implementations for converting common structs into a format accepted -/// by the macros. -/// -/// All dynamic content in the macros must flow in through `fragment_from_iter`. Everything else must be statically layed out. -/// We pipe basically everything through `fragment_from_iter`, so we expect a very specific type: -/// ```rust, ignore -/// impl IntoIterator> -/// ``` -/// -/// As such, all node creation must go through the factory, which is only available in the component context. -/// These strict requirements make it possible to manage lifetimes and state. -pub trait IntoVNode<'a, I = (), J = ()> { - /// Convert this into a [`VNode`], using the [`NodeFactory`] as a source of allocation - fn into_vnode(self, cx: NodeFactory<'a>) -> VNode<'a>; +impl IntoDynNode<'_> for String { + fn into_vnode(self, cx: &ScopeState) -> DynamicNode { + cx.text_node(format_args!("{}", self)) + } } -// TODO: do we even need this? It almost seems better not to -// // For the case where a rendered VNode is passed into the rsx! macro through curly braces -impl<'a> IntoVNode<'a> for VNode<'a> { - fn into_vnode(self, _: NodeFactory<'a>) -> VNode<'a> { +impl<'b> IntoDynNode<'b> for Arguments<'_> { + fn into_vnode(self, cx: &'b ScopeState) -> DynamicNode<'b> { + cx.text_node(self) + } +} + +impl<'a> IntoDynNode<'a> for &'a VNode<'a> { + fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { + DynamicNode::Fragment(_cx.bump().alloc([VNode { + parent: self.parent, + template: self.template, + root_ids: self.root_ids, + key: self.key, + dynamic_nodes: self.dynamic_nodes, + dynamic_attrs: self.dynamic_attrs, + }])) + } +} + +pub trait IntoTemplate<'a> { + fn into_template(self, _cx: &'a ScopeState) -> VNode<'a>; +} +impl<'a> IntoTemplate<'a> for VNode<'a> { + fn into_template(self, _cx: &'a ScopeState) -> VNode<'a> { self } } - -// Conveniently, we also support "null" (nothing) passed in -impl IntoVNode<'_> for () { - fn into_vnode(self, cx: NodeFactory) -> VNode { - VNode::Placeholder(cx.bump.alloc(VPlaceholder { id: empty_cell() })) - } -} - -impl<'a, 'b> IntoVNode<'a> for LazyNodes<'a, 'b> { - fn into_vnode(self, cx: NodeFactory<'a>) -> VNode<'a> { +impl<'a, 'b> IntoTemplate<'a> for LazyNodes<'a, 'b> { + fn into_template(self, cx: &'a ScopeState) -> VNode<'a> { self.call(cx) } } -impl<'b> IntoVNode<'_> for &'b str { - fn into_vnode(self, cx: NodeFactory) -> VNode { - cx.text(format_args!("{}", self)) - } -} - -impl IntoVNode<'_> for String { - fn into_vnode(self, cx: NodeFactory) -> VNode { - cx.text(format_args!("{}", self)) - } -} - -impl IntoVNode<'_> for Arguments<'_> { - fn into_vnode(self, cx: NodeFactory) -> VNode { - cx.text(self) - } -} - -impl<'a> IntoVNode<'a> for &VNode<'a> { - fn into_vnode(self, _cx: NodeFactory<'a>) -> VNode<'a> { - // borrowed nodes are strange - self.decouple() - } -} - // Note that we're using the E as a generic but this is never crafted anyways. +#[doc(hidden)] pub struct FromNodeIterator; -impl<'a, T, I, E> IntoVNode<'a, FromNodeIterator, E> for T +impl<'a, T, I> IntoDynNode<'a, FromNodeIterator> for T where - T: IntoIterator, - I: IntoVNode<'a, E>, + T: Iterator, + I: IntoTemplate<'a>, { - fn into_vnode(self, cx: NodeFactory<'a>) -> VNode<'a> { - let mut nodes = bumpalo::collections::Vec::new_in(cx.bump); + fn into_vnode(self, cx: &'a ScopeState) -> DynamicNode<'a> { + let mut nodes = bumpalo::collections::Vec::new_in(cx.bump()); - for node in self { - nodes.push(node.into_vnode(cx)); - } + nodes.extend(self.into_iter().map(|node| node.into_template(cx))); - if nodes.is_empty() { - VNode::Placeholder(cx.bump.alloc(VPlaceholder { id: empty_cell() })) - } else { - let children = nodes.into_bump_slice(); - - if cfg!(debug_assertions) - && children.len() > 1 - && children.last().unwrap().key().is_none() - { - // todo: make the backtrace prettier or remove it altogether - log::error!( - r#" - Warning: Each child in an array or iterator should have a unique "key" prop. - Not providing a key will lead to poor performance with lists. - See docs.rs/dioxus for more information. - ------------- - {:?} - "#, - backtrace::Backtrace::new() - ); - } - - VNode::Fragment(cx.bump.alloc(VFragment { - children, - key: None, - })) + match nodes.into_bump_slice() { + children if children.is_empty() => DynamicNode::default(), + children => DynamicNode::Fragment(children), } } } + +/// 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 &'a str { + fn into_value(self, _: &'a Bump) -> AttributeValue<'a> { + AttributeValue::Text(self) + } +} +impl<'a> IntoAttributeValue<'a> for f64 { + fn into_value(self, _: &'a Bump) -> AttributeValue<'a> { + AttributeValue::Float(self) + } +} +impl<'a> IntoAttributeValue<'a> for i64 { + fn into_value(self, _: &'a Bump) -> AttributeValue<'a> { + AttributeValue::Int(self) + } +} +impl<'a> IntoAttributeValue<'a> for bool { + fn into_value(self, _: &'a Bump) -> AttributeValue<'a> { + AttributeValue::Bool(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()) + } +} diff --git a/packages/core/src/properties.rs b/packages/core/src/properties.rs index e138acaa6..f8be8a0dd 100644 --- a/packages/core/src/properties.rs +++ b/packages/core/src/properties.rs @@ -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) -> 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() } diff --git a/packages/core/src/scheduler/mod.rs b/packages/core/src/scheduler/mod.rs new file mode 100644 index 000000000..a6442184d --- /dev/null +++ b/packages/core/src/scheduler/mod.rs @@ -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, + + /// Tasks created with cx.spawn + pub tasks: RefCell>>, + + /// Async components + pub leaves: RefCell>>, +} + +impl Scheduler { + pub fn new(sender: futures_channel::mpsc::UnboundedSender) -> Rc { + Rc::new(Scheduler { + sender, + tasks: RefCell::new(Slab::new()), + leaves: RefCell::new(Slab::new()), + }) + } +} diff --git a/packages/core/src/scheduler/suspense.rs b/packages/core/src/scheduler/suspense.rs new file mode 100644 index 000000000..8d7d7337e --- /dev/null +++ b/packages/core/src/scheduler/suspense.rs @@ -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>, + pub(crate) mutations: RefCell>, + pub(crate) placeholder: Cell>, + pub(crate) created_on_stack: Cell, +} + +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, + pub(crate) notified: Cell, + pub(crate) task: *mut dyn Future>, +} + +impl RcWake for SuspenseLeaf { + fn wake_by_ref(arc_self: &Rc) { + arc_self.notified.set(true); + _ = arc_self + .tx + .unbounded_send(SchedulerMsg::SuspenseNotified(arc_self.id)); + } +} diff --git a/packages/core/src/scheduler/task.rs b/packages/core/src/scheduler/task.rs new file mode 100644 index 000000000..fb4378104 --- /dev/null +++ b/packages/core/src/scheduler/task.rs @@ -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 + 'static>>>, + id: TaskId, + tx: futures_channel::mpsc::UnboundedSender, +} + +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 + '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) { + _ = arc_self + .tx + .unbounded_send(SchedulerMsg::TaskNotified(arc_self.id)); + } +} diff --git a/packages/core/src/scheduler/wait.rs b/packages/core/src/scheduler/wait.rs new file mode 100644 index 000000000..2bf6cdfa1 --- /dev/null +++ b/packages/core/src/scheduler/wait.rs @@ -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 { + self.scopes[id.0] + .consume_context::>() + .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); + } + } + } + } +} diff --git a/packages/core/src/scheduler/waker.rs b/packages/core/src/scheduler/waker.rs new file mode 100644 index 000000000..762020167 --- /dev/null +++ b/packages/core/src/scheduler/waker.rs @@ -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) -> Waker { + unsafe fn rc_vtable() -> &'static RawWakerVTable { + &RawWakerVTable::new( + |data| { + let arc = mem::ManuallyDrop::new(Rc::::from_raw(data.cast::())); + let _rc_clone: mem::ManuallyDrop<_> = arc.clone(); + RawWaker::new(data, rc_vtable::()) + }, + |data| Rc::from_raw(data.cast::()).wake(), + |data| { + let arc = mem::ManuallyDrop::new(Rc::::from_raw(data.cast::())); + RcWake::wake_by_ref(&arc); + }, + |data| drop(Rc::::from_raw(data.cast::())), + ) + } + + unsafe { + Waker::from_raw(RawWaker::new( + Rc::into_raw(self.clone()).cast(), + rc_vtable::(), + )) + } + } + + fn wake_by_ref(arc_self: &Rc); + + fn wake(self: Rc) { + Self::wake_by_ref(&self) + } +} diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs new file mode 100644 index 000000000..af36f1aa6 --- /dev/null +++ b/packages/core/src/scope_arena.rs @@ -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>, + 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) } + } +} diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index f5ec56644..b1bbb42fa 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -1,575 +1,53 @@ -use crate::{innerlude::*, template::TemplateNodeId, unsafe_utils::extend_vnode}; -use bumpalo::Bump; -use futures_channel::mpsc::UnboundedSender; -use rustc_hash::FxHashMap; -use slab::Slab; +use crate::{ + any_props::AnyProps, + any_props::VProps, + arena::ElementId, + bump_frame::BumpFrame, + innerlude::{DynamicNode, EventHandler, VComponent, VText}, + innerlude::{Scheduler, SchedulerMsg}, + lazynodes::LazyNodes, + nodes::{ComponentReturn, IntoAttributeValue, IntoDynNode, RenderReturn}, + Attribute, AttributeValue, Element, Event, Properties, TaskId, +}; +use bumpalo::{boxed::Box as BumpBox, Bump}; use std::{ any::{Any, TypeId}, cell::{Cell, RefCell}, collections::{HashMap, HashSet}, + fmt::Arguments, future::Future, - pin::Pin, rc::Rc, sync::Arc, }; -pub(crate) enum NodePtr { - VNode(*const VNode<'static>), - TemplateNode { - template_ref: TemplateRefId, - node_id: TemplateNodeId, - }, - Phantom, -} +/// A wrapper around the [`Scoped`] object that contains a reference to the [`ScopeState`] and properties for a given +/// component. +/// +/// The [`Scope`] is your handle to the [`crate::VirtualDom`] and the component state. Every component is given its own +/// [`ScopeState`] and merged with its properties to create a [`Scoped`]. +/// +/// The [`Scope`] handle specifically exists to provide a stable reference to these items for the lifetime of the +/// component render. +pub type Scope<'a, T = ()> = &'a Scoped<'a, T>; -pub(crate) type NodeSlab = Slab; - -/// for traceability, we use the raw fn pointer to identify the function -/// we also get the component name, but that's not necessarily unique in the app -pub(crate) type ComponentPtr = *mut std::os::raw::c_void; - -pub(crate) struct Heuristic { - hook_arena_size: usize, - node_arena_size: usize, -} - -// a slab-like arena with stable references even when new scopes are allocated -// uses a bump arena as a backing +// This ScopedType exists because we want to limit the amount of monomorphization that occurs when making inner +// state type generic over props. When the state is generic, it causes every method to be monomorphized for every +// instance of Scope in the codebase. // -// has an internal heuristics engine to pre-allocate arenas to the right size -pub(crate) struct ScopeArena { - pub scope_gen: Cell, - pub bump: Bump, - pub scopes: RefCell>, - pub heuristics: RefCell>, - pub free_scopes: RefCell>, - // All nodes are stored here. This mimics the allocations needed on the renderer side. - // Some allocations are needed on the renderer to render nodes in templates that are - // not needed in dioxus, for these allocations None is used so that the id is preseved and passive memory managment is preserved. - pub nodes: RefCell, - pub tasks: Rc, - pub template_resolver: RefCell, - pub templates: Rc>>>>, - // this is used to store intermidiate artifacts of creating templates, so that the lifetime aligns with Mutations<'bump>. - pub template_bump: Bump, - pub template_refs: RefCell>>, -} - -impl ScopeArena { - pub(crate) fn new(sender: UnboundedSender) -> Self { - let bump = Bump::new(); - - // allocate a container for the root element - // this will *never* show up in the diffing process - // todo: figure out why this is necessary. i forgot. whoops. - let el = bump.alloc(VElement { - tag: "root", - namespace: None, - key: None, - id: Cell::new(Some(ElementId(0))), - parent: Default::default(), - listeners: &[], - attributes: &[], - children: &[], - }); - - let node = bump.alloc(VNode::Element(el)); - let mut nodes = Slab::new(); - let root_id = nodes.insert(NodePtr::VNode(unsafe { - std::mem::transmute(node as *const _) - })); - - debug_assert_eq!(root_id, 0); - - Self { - scope_gen: Cell::new(0), - bump, - scopes: RefCell::new(FxHashMap::default()), - heuristics: RefCell::new(FxHashMap::default()), - free_scopes: RefCell::new(Vec::new()), - nodes: RefCell::new(nodes), - tasks: Rc::new(TaskQueue { - tasks: RefCell::new(FxHashMap::default()), - task_map: RefCell::new(FxHashMap::default()), - gen: Cell::new(0), - sender, - }), - template_resolver: RefCell::new(TemplateResolver::default()), - templates: Rc::new(RefCell::new(FxHashMap::default())), - template_bump: Bump::new(), - template_refs: RefCell::new(Slab::new()), - } - } - - /// Safety: - /// - Obtaining a mutable reference to any Scope is unsafe - /// - Scopes use interior mutability when sharing data into components - pub(crate) fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> { - unsafe { self.scopes.borrow().get(&id).map(|f| &**f) } - } - - pub(crate) fn get_scope_raw(&self, id: ScopeId) -> Option<*mut ScopeState> { - self.scopes.borrow().get(&id).copied() - } - - pub(crate) fn new_with_key( - &self, - fc_ptr: ComponentPtr, - vcomp: Box, - parent_scope: Option, - container: ElementId, - ) -> ScopeId { - // Increment the ScopeId system. ScopeIDs are never reused - let new_scope_id = ScopeId(self.scope_gen.get()); - self.scope_gen.set(self.scope_gen.get() + 1); - - // Get the height of the scope - let height = parent_scope - .and_then(|id| self.get_scope(id).map(|scope| scope.height + 1)) - .unwrap_or_default(); - - let parent_scope = parent_scope.and_then(|f| self.get_scope_raw(f)); - - /* - This scopearena aggressively reuses old scopes when possible. - We try to minimize the new allocations for props/arenas. - - However, this will probably lead to some sort of fragmentation. - I'm not exactly sure how to improve this today. - */ - if let Some(old_scope) = self.free_scopes.borrow_mut().pop() { - // reuse the old scope - let scope = unsafe { &mut *old_scope }; - - scope.container = container; - scope.our_arena_idx = new_scope_id; - scope.parent_scope = parent_scope; - scope.height = height; - scope.fnptr = fc_ptr; - scope.props.get_mut().replace(vcomp); - scope.frames[0].reset(); - scope.frames[1].reset(); - scope.shared_contexts.get_mut().clear(); - scope.items.get_mut().listeners.clear(); - scope.items.get_mut().borrowed_props.clear(); - scope.hook_idx.set(0); - scope.hook_vals.get_mut().clear(); - - let any_item = self.scopes.borrow_mut().insert(new_scope_id, scope); - debug_assert!(any_item.is_none()); - } else { - // else create a new scope - let (node_capacity, hook_capacity) = self - .heuristics - .borrow() - .get(&fc_ptr) - .map(|h| (h.node_arena_size, h.hook_arena_size)) - .unwrap_or_default(); - - self.scopes.borrow_mut().insert( - new_scope_id, - self.bump.alloc(ScopeState { - container, - our_arena_idx: new_scope_id, - parent_scope, - height, - fnptr: fc_ptr, - props: RefCell::new(Some(vcomp)), - frames: [BumpFrame::new(node_capacity), BumpFrame::new(node_capacity)], - - generation: 0.into(), - - tasks: self.tasks.clone(), - shared_contexts: RefCell::default(), - - items: RefCell::new(SelfReferentialItems { - listeners: Vec::default(), - borrowed_props: Vec::default(), - }), - - hook_arena: Bump::new(), - hook_vals: RefCell::new(Vec::with_capacity(hook_capacity)), - hook_idx: Cell::default(), - - templates: self.templates.clone(), - }), - ); - } - - new_scope_id - } - - // Removes a scope and its descendents from the arena - pub fn try_remove(&self, id: ScopeId) { - self.ensure_drop_safety(id); - - // Dispose of any ongoing tasks - let mut tasks = self.tasks.tasks.borrow_mut(); - let mut task_map = self.tasks.task_map.borrow_mut(); - if let Some(cur_tasks) = task_map.remove(&id) { - for task in cur_tasks { - tasks.remove(&task); - } - } - - // Safety: - // - ensure_drop_safety ensures that no references to this scope are in use - // - this raw pointer is removed from the map - let scope = unsafe { &mut *self.scopes.borrow_mut().remove(&id).unwrap() }; - scope.reset(); - - self.free_scopes.borrow_mut().push(scope); - } - - pub fn reserve_node<'a>(&self, node: &'a VNode<'a>) -> ElementId { - let mut els = self.nodes.borrow_mut(); - let entry = els.vacant_entry(); - let key = entry.key(); - let id = ElementId(key); - let node = unsafe { extend_vnode(node) }; - entry.insert(NodePtr::VNode(node as *const _)); - id - } - - pub fn reserve_template_ref<'a>(&self, template_ref: &'a VTemplateRef<'a>) -> TemplateRefId { - let mut refs = self.template_refs.borrow_mut(); - let entry = refs.vacant_entry(); - let key = entry.key(); - let id = TemplateRefId(key); - let static_ref: &VTemplateRef<'static> = unsafe { std::mem::transmute(template_ref) }; - entry.insert(static_ref as *const _); - id - } - - pub fn reserve_template_node( - &self, - template_ref_id: TemplateRefId, - node_id: TemplateNodeId, - ) -> ElementId { - let mut els = self.nodes.borrow_mut(); - let entry = els.vacant_entry(); - let key = entry.key(); - let id = ElementId(key); - entry.insert(NodePtr::TemplateNode { - template_ref: template_ref_id, - node_id, - }); - id - } - - pub fn reserve_phantom_node(&self) -> ElementId { - let mut els = self.nodes.borrow_mut(); - let entry = els.vacant_entry(); - let key = entry.key(); - let id = ElementId(key); - entry.insert(NodePtr::Phantom); - id - } - - pub fn update_node<'a>(&self, node: &'a VNode<'a>, id: ElementId) { - let node = unsafe { extend_vnode(node) }; - *self.nodes.borrow_mut().get_mut(id.0).unwrap() = NodePtr::VNode(node); - } - - pub fn update_template_ref<'a>( - &self, - template_ref_id: TemplateRefId, - template_ref: &'a VTemplateRef<'a>, - ) { - let template_ref = unsafe { std::mem::transmute(template_ref) }; - *self - .template_refs - .borrow_mut() - .get_mut(template_ref_id.0) - .unwrap() = template_ref; - } - - pub fn collect_garbage(&self, id: ElementId) { - self.nodes.borrow_mut().remove(id.0); - } - - /// This method cleans up any references to data held within our hook list. This prevents mutable aliasing from - /// causing UB in our tree. +// +/// A wrapper around a component's [`ScopeState`] and properties. The [`ScopeState`] provides the majority of methods +/// for the VirtualDom and component state. +pub struct Scoped<'a, T = ()> { + /// The component's state and handle to the scheduler. /// - /// This works by cleaning up our references from the bottom of the tree to the top. The directed graph of components - /// essentially forms a dependency tree that we can traverse from the bottom to the top. As we traverse, we remove - /// any possible references to the data in the hook list. - /// - /// References to hook data can only be stored in listeners and component props. During diffing, we make sure to log - /// all listeners and borrowed props so we can clear them here. - /// - /// This also makes sure that drop order is consistent and predictable. All resources that rely on being dropped will - /// be dropped. - pub(crate) fn ensure_drop_safety(&self, scope_id: ScopeId) { - if let Some(scope) = self.get_scope(scope_id) { - let mut items = scope.items.borrow_mut(); - - // make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we - // run the hooks (which hold an &mut Reference) - // recursively call ensure_drop_safety on all children - items.borrowed_props.drain(..).for_each(|comp| { - if let Some(scope_id) = comp.scope.get() { - self.ensure_drop_safety(scope_id); - } - drop(comp.props.take()); - }); - - // Now that all the references are gone, we can safely drop our own references in our listeners. - items - .listeners - .drain(..) - .for_each(|listener| drop(listener.callback.borrow_mut().take())); - } - } - - pub(crate) fn run_scope(&self, id: ScopeId) { - // 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(id); - - // todo: we *know* that this is aliased by the contents of the scope itself - let scope = unsafe { &mut *self.get_scope_raw(id).expect("could not find scope") }; - - log::trace!("running scope {:?} symbol: {:?}", id, scope.fnptr); - - // Safety: - // - We dropped the listeners, so no more &mut T can be used while these are held - // - All children nodes that rely on &mut T are replaced with a new reference - scope.hook_idx.set(0); - - { - // Safety: - // - We've dropped all references to the wip bump frame with "ensure_drop_safety" - unsafe { scope.reset_wip_frame() }; - - let items = scope.items.borrow(); - - // guarantee that we haven't screwed up - there should be no latent references anywhere - debug_assert!(items.listeners.is_empty()); - debug_assert!(items.borrowed_props.is_empty()); - } - - /* - If the component returns None, then we fill in a placeholder node. This will wipe what was there. - An alternate approach is to leave the Real Dom the same, but that can lead to safety issues and a lot more checks. - - Instead, we just treat the `None` as a shortcut to placeholder. - If the developer wants to prevent a scope from updating, they should control its memoization instead. - - Also, the way we implement hooks allows us to cut rendering short before the next hook is recalled. - I'm not sure if React lets you abort the component early, but we let you do that. - */ - - let props = scope.props.borrow(); - let render = props.as_ref().unwrap(); - if let Some(node) = render.render(scope) { - let frame = scope.wip_frame(); - let node = frame.bump.alloc(node); - frame.node.set(unsafe { extend_vnode(node) }); - } else { - let frame = scope.wip_frame(); - let node = frame - .bump - .alloc(VNode::Placeholder(frame.bump.alloc(VPlaceholder { - id: Cell::default(), - }))); - frame.node.set(unsafe { extend_vnode(node) }); - } - - // make the "wip frame" contents the "finished frame" - // any future dipping into completed nodes after "render" will go through "fin head" - scope.cycle_frame(); - } - - pub fn call_listener_with_bubbling(&self, event: &UserEvent, element: ElementId) { - let nodes = self.nodes.borrow(); - let mut cur_el = Some(element); - - let state = Rc::new(BubbleState::new()); - - while let Some(id) = cur_el.take() { - if state.canceled.get() { - // stop bubbling if canceled - return; - } - if let Some(ptr) = nodes.get(id.0) { - match ptr { - NodePtr::VNode(ptr) => { - let real_el = unsafe { &**ptr }; - log::trace!("looking for listener on {:?}", real_el); - - if let VNode::Element(real_el) = real_el { - for listener in real_el.listeners.iter() { - if listener.event == event.name { - log::trace!("calling listener {:?}", listener.event); - - let mut cb = listener.callback.borrow_mut(); - if let Some(cb) = cb.as_mut() { - // todo: arcs are pretty heavy to clone - // we really want to convert arc to rc - // unfortunately, the SchedulerMsg must be send/sync to be sent across threads - // we could convert arc to rc internally or something - (cb)(AnyEvent { - bubble_state: state.clone(), - data: event.data.clone(), - }); - } - break; - } - } - - cur_el = real_el.parent.get(); - } - } - NodePtr::TemplateNode { - node_id, - template_ref, - } => { - let template_refs = self.template_refs.borrow(); - let template_ptr = template_refs.get(template_ref.0).unwrap(); - let template_ref = unsafe { &**template_ptr }; - log::trace!("looking for listener in node {:?}", node_id); - let templates = self.templates.borrow(); - let template = templates.get(&template_ref.template_id).unwrap(); - cur_el = template.borrow().with_nodes( - bubble_template, - bubble_template, - (*node_id, template_ref, event, &state), - ); - } - _ => panic!("Expected Real Node"), - } - } - - if !event.bubbles { - return; - } - } - - fn bubble_template<'b, Attributes, V, Children, Listeners, TextSegments, Text, Nodes>( - nodes: &Nodes, - ctx: ( - TemplateNodeId, - &VTemplateRef<'b>, - &UserEvent, - &Rc, - ), - ) -> Option - where - Nodes: AsRef<[TemplateNode]>, - Attributes: AsRef<[TemplateAttribute]>, - V: TemplateValue, - Children: AsRef<[TemplateNodeId]>, - Listeners: AsRef<[usize]>, - TextSegments: AsRef<[TextTemplateSegment]>, - Text: AsRef, - { - let (start, template_ref, event, state) = ctx; - let dynamic_context = &template_ref.dynamic_context; - let mut current = &nodes.as_ref()[start.0]; - loop { - if let TemplateNodeType::Element(el) = ¤t.node_type { - let TemplateElement { listeners, .. } = el; - for listener_idx in listeners.as_ref() { - let listener = dynamic_context.resolve_listener(*listener_idx); - if listener.event == event.name { - log::trace!("calling listener {:?}", listener.event); - - let mut cb = listener.callback.borrow_mut(); - if let Some(cb) = cb.as_mut() { - // todo: arcs are pretty heavy to clone - // we really want to convert arc to rc - // unfortunately, the SchedulerMsg must be send/sync to be sent across threads - // we could convert arc to rc internally or something - (cb)(AnyEvent { - bubble_state: state.clone(), - data: event.data.clone(), - }); - } - break; - } - } - - if let Some(id) = current.parent { - current = &nodes.as_ref()[id.0]; - } else { - return template_ref.parent.get(); - } - } else { - return None; - } - } - } - } - - // The head of the bumpframe is the first linked NodeLink - pub fn wip_head(&self, id: ScopeId) -> &VNode { - let scope = self.get_scope(id).unwrap(); - let frame = scope.wip_frame(); - let node = unsafe { &*frame.node.get() }; - unsafe { extend_vnode(node) } - } - - // The head of the bumpframe is the first linked NodeLink - pub fn fin_head(&self, id: ScopeId) -> &VNode { - let scope = self.get_scope(id).unwrap(); - let frame = scope.fin_frame(); - let node = unsafe { &*frame.node.get() }; - unsafe { extend_vnode(node) } - } - - pub fn root_node(&self, id: ScopeId) -> &VNode { - self.fin_head(id) - } - - // this is totally okay since all our nodes are always in a valid state - pub fn get_element(&self, id: ElementId) -> Option<&VNode> { - self.nodes.borrow().get(id.0).and_then(|ptr| match ptr { - NodePtr::VNode(ptr) => Some(unsafe { extend_vnode(&**ptr) }), - _ => None, - }) - } -} - -/// Components in Dioxus use the "Context" object to interact with their lifecycle. -/// -/// This lets components access props, schedule updates, integrate hooks, and expose shared state. -/// -/// For the most part, the only method you should be using regularly is `render`. -/// -/// ## Example -/// -/// ```ignore -/// #[derive(Props)] -/// struct ExampleProps { -/// name: String -/// } -/// -/// fn Example(cx: Scope) -> Element { -/// cx.render(rsx!{ div {"Hello, {cx.props.name}"} }) -/// } -/// ``` -pub struct Scope<'a, P = ()> { - /// The internal ScopeState for this component + /// Stores things like the custom bump arena, spawn functions, hooks, and the scheduler. pub scope: &'a ScopeState, - /// The props for this component - pub props: &'a P, + /// The component's properties. + pub props: &'a T, } -impl

Copy for Scope<'_, P> {} -impl

Clone for Scope<'_, P> { - fn clone(&self) -> Self { - Self { - scope: self.scope, - props: self.props, - } - } -} - -impl<'a, P> std::ops::Deref for Scope<'a, P> { - // rust will auto deref again to the original 'a lifetime at the call site +impl<'a, T> std::ops::Deref for Scoped<'a, T> { type Target = &'a ScopeState; fn deref(&self) -> &Self::Target { &self.scope @@ -578,68 +56,116 @@ impl<'a, P> std::ops::Deref for Scope<'a, P> { /// A component's unique identifier. /// -/// `ScopeId` is a `usize` that is unique across the entire [`VirtualDom`] and across time. [`ScopeID`]s will never be reused -/// once a component has been unmounted. +/// `ScopeId` is a `usize` that acts a key for the internal slab of Scopes. This means that the key is not unqiue across +/// time. We do try and guarantee that between calls to `wait_for_work`, no ScopeIds will be recycled in order to give +/// time for any logic that relies on these IDs to properly update. #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)] pub struct ScopeId(pub usize); -/// A task's unique identifier. +/// A component's state separate from its props. /// -/// `TaskId` is a `usize` that is unique across the entire [`VirtualDom`] and across time. [`TaskID`]s 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 { - /// The global ID of the task - pub id: usize, - - /// The original scope that this task was scheduled in - pub scope: ScopeId, -} - -/// Every component in Dioxus is represented by a `ScopeState`. -/// -/// Scopes contain the state for hooks, the component's props, and other lifecycle information. -/// -/// Scopes are allocated in a generational arena. As components are mounted/unmounted, they will replace slots of dead components. -/// The actual contents of the hooks, though, will be allocated with the standard allocator. These should not allocate as frequently. -/// -/// We expose the `Scope` type so downstream users can traverse the Dioxus [`VirtualDom`] for whatever -/// use case they might have. +/// This struct exists to provide a common interface for all scopes without relying on generics. pub struct ScopeState { - pub(crate) parent_scope: Option<*mut ScopeState>, - pub(crate) container: ElementId, - pub(crate) our_arena_idx: ScopeId, + pub(crate) render_cnt: Cell, + pub(crate) name: &'static str, + + pub(crate) node_arena_1: BumpFrame, + pub(crate) node_arena_2: BumpFrame, + + pub(crate) parent: Option<*mut ScopeState>, + pub(crate) id: ScopeId, + pub(crate) height: u32, - pub(crate) fnptr: ComponentPtr, - pub(crate) props: RefCell>>, - // nodes, items - pub(crate) frames: [BumpFrame; 2], - pub(crate) generation: Cell, - pub(crate) items: RefCell>, - - // hooks pub(crate) hook_arena: Bump, - pub(crate) hook_vals: RefCell>, + pub(crate) hook_list: RefCell>, pub(crate) hook_idx: Cell, - // shared state -> todo: move this out of scopestate pub(crate) shared_contexts: RefCell>>, - pub(crate) tasks: Rc, - // templates - pub(crate) templates: Rc>>>>, + pub(crate) tasks: Rc, + pub(crate) spawned_tasks: HashSet, + + pub(crate) props: Option>>, + pub(crate) placeholder: Cell>, } -pub struct SelfReferentialItems<'a> { - pub(crate) listeners: Vec<&'a Listener<'a>>, - pub(crate) borrowed_props: Vec<&'a VComponent<'a>>, -} +impl<'src> ScopeState { + pub(crate) fn current_frame(&self) -> &BumpFrame { + match self.render_cnt.get() % 2 { + 0 => &self.node_arena_1, + 1 => &self.node_arena_2, + _ => unreachable!(), + } + } + + pub(crate) fn previous_frame(&self) -> &BumpFrame { + match self.render_cnt.get() % 2 { + 1 => &self.node_arena_1, + 0 => &self.node_arena_2, + _ => unreachable!(), + } + } + + pub(crate) fn previous_frame_mut(&mut self) -> &mut BumpFrame { + match self.render_cnt.get() % 2 { + 1 => &mut self.node_arena_1, + 0 => &mut self.node_arena_2, + _ => unreachable!(), + } + } + + /// Get the name of this component + pub fn name(&self) -> &str { + self.name + } + + /// Get the current render since the inception of this component + /// + /// This can be used as a helpful diagnostic when debugging hooks/renders, etc + pub fn generation(&self) -> usize { + self.render_cnt.get() + } + + /// Get a handle to the currently active bump arena for this Scope + /// + /// This is a bump memory allocator. Be careful using this directly since the contents will be wiped on the next render. + /// It's easy to leak memory here since the drop implementation will not be called for any objects allocated in this arena. + /// + /// If you need to allocate items that need to be dropped, use bumpalo's box. + pub fn bump(&self) -> &Bump { + // note that this is actually the previous frame since we use that as scratch space while the component is rendering + &self.previous_frame().bump + } + + /// Get a handle to the currently active head node arena for this Scope + /// + /// This is useful for traversing the tree outside of the VirtualDom, such as in a custom renderer or in SSR. + /// + /// Panics if the tree has not been built yet. + pub fn root_node(&self) -> &RenderReturn { + self.try_root_node() + .expect("The tree has not been built yet. Make sure to call rebuild on the tree before accessing its nodes.") + } + + /// Try to get a handle to the currently active head node arena for this Scope + /// + /// This is useful for traversing the tree outside of the VirtualDom, such as in a custom renderer or in SSR. + /// + /// Returns [`None`] if the tree has not been built yet. + pub fn try_root_node(&self) -> Option<&RenderReturn> { + let ptr = self.current_frame().node.get(); + + if ptr.is_null() { + return None; + } + + let r: &RenderReturn = unsafe { &*ptr }; + + unsafe { std::mem::transmute(r) } + } -// Public methods exposed to libraries and components -impl ScopeState { /// Get the height of this Scope - IE the number of scopes above it. /// /// A Scope with a height of `0` is the root scope - there are no other scopes above it. @@ -658,9 +184,9 @@ impl ScopeState { self.height } - /// Get the Parent of this [`Scope`] within this Dioxus [`VirtualDom`]. + /// Get the Parent of this [`Scope`] within this Dioxus [`crate::VirtualDom`]. /// - /// This ID is not unique across Dioxus [`VirtualDom`]s or across time. IDs will be reused when components are unmounted. + /// This ID is not unique across Dioxus [`crate::VirtualDom`]s or across time. IDs will be reused when components are unmounted. /// /// The base component will not have a parent, and will return `None`. /// @@ -676,12 +202,12 @@ impl ScopeState { /// ``` pub fn parent(&self) -> Option { // safety: the pointer to our parent is *always* valid thanks to the bump arena - self.parent_scope.map(|p| unsafe { &*p }.our_arena_idx) + self.parent.map(|p| unsafe { &*p }.id) } - /// Get the ID of this Scope within this Dioxus [`VirtualDom`]. + /// Get the ID of this Scope within this Dioxus [`crate::VirtualDom`]. /// - /// This ID is not unique across Dioxus [`VirtualDom`]s or across time. IDs will be reused when components are unmounted. + /// This ID is not unique across Dioxus [`crate::VirtualDom`]s or across time. IDs will be reused when components are unmounted. /// /// # Example /// @@ -693,17 +219,12 @@ impl ScopeState { /// assert_eq!(base.scope_id(), 0); /// ``` pub fn scope_id(&self) -> ScopeId { - self.our_arena_idx - } - - /// Get a handle to the raw update scheduler channel - pub fn scheduler_channel(&self) -> UnboundedSender { - self.tasks.sender.clone() + self.id } /// Create a subscription that schedules a future render for the reference component /// - /// ## Notice: you should prefer using [`schedule_update_any`] and [`scope_id`] + /// ## Notice: you should prefer using [`Self::schedule_update_any`] and [`Self::scope_id`] pub fn schedule_update(&self) -> Arc { let (chan, id) = (self.tasks.sender.clone(), self.scope_id()); Arc::new(move || drop(chan.unbounded_send(SchedulerMsg::Immediate(id)))) @@ -719,16 +240,14 @@ impl ScopeState { Arc::new(move |id| drop(chan.unbounded_send(SchedulerMsg::Immediate(id)))) } - /// Get the [`ScopeId`] of a mounted component. - /// - /// `ScopeId` is not unique for the lifetime of the [`VirtualDom`] - a [`ScopeId`] will be reused if a component is unmounted. + /// Mark this scope as dirty, and schedule a render for it. pub fn needs_update(&self) { self.needs_update_any(self.scope_id()); } /// Get the [`ScopeId`] of a mounted component. /// - /// `ScopeId` is not unique for the lifetime of the [`VirtualDom`] - a [`ScopeId`] will be reused if a component is unmounted. + /// `ScopeId` is not unique for the lifetime of the [`crate::VirtualDom`] - a [`ScopeId`] will be reused if a component is unmounted. pub fn needs_update_any(&self, id: ScopeId) { self.tasks .sender @@ -736,20 +255,45 @@ impl ScopeState { .expect("Scheduler to exist if scope exists"); } - /// Get the Root Node of this scope - pub fn root_node(&self) -> &VNode { - let node = unsafe { &*self.fin_frame().node.get() }; - unsafe { std::mem::transmute(node) } + /// Return any context of type T if it exists on this scope + pub fn has_context(&self) -> Option { + self.shared_contexts + .borrow() + .get(&TypeId::of::())? + .downcast_ref::() + .cloned() } - /// This method enables the ability to expose state to children further down the [`VirtualDom`] Tree. + /// Try to retrieve a shared state with type `T` from any parent scope. + /// + /// Clones the state if it exists. + pub fn consume_context(&self) -> Option { + if let Some(this_ctx) = self.has_context() { + return Some(this_ctx); + } + + let mut search_parent = self.parent; + while let Some(parent_ptr) = search_parent { + // safety: all parent pointers are valid thanks to the bump arena + let parent = unsafe { &*parent_ptr }; + if let Some(shared) = parent.shared_contexts.borrow().get(&TypeId::of::()) { + return shared.downcast_ref::().cloned(); + } + search_parent = parent.parent; + } + None + } + + /// Expose state to children further down the [`crate::VirtualDom`] Tree. Does not require `clone` on the context, + /// though we do recommend it. /// /// This is a "fundamental" operation and should only be called during initialization of a hook. /// - /// For a hook that provides the same functionality, use `use_provide_context` and `use_consume_context` instead. + /// For a hook that provides the same functionality, use `use_provide_context` and `use_context` instead. /// - /// When the component is dropped, so is the context. Be aware of this behavior when consuming - /// the context via Rc/Weak. + /// If a state is provided that already exists, the new value will not be inserted. Instead, this method will + /// return the existing value. This behavior is chosen so shared values do not need to be `Clone`. This particular + /// behavior might change in the future. /// /// # Example /// @@ -767,102 +311,18 @@ impl ScopeState { /// } /// ``` pub fn provide_context(&self, value: T) -> T { + let value2 = value.clone(); + self.shared_contexts .borrow_mut() - .insert(TypeId::of::(), Box::new(value.clone())) - .and_then(|f| f.downcast::().ok()); - value - } + .insert(TypeId::of::(), Box::new(value)); - /// Provide a context for the root component from anywhere in your app. - /// - /// - /// # Example - /// - /// ```rust, ignore - /// struct SharedState(&'static str); - /// - /// static App: Component = |cx| { - /// cx.use_hook(|| cx.provide_root_context(SharedState("world"))); - /// render!(Child {}) - /// } - /// - /// static Child: Component = |cx| { - /// let state = cx.consume_state::(); - /// render!(div { "hello {state.0}" }) - /// } - /// ``` - pub fn provide_root_context(&self, value: T) -> T { - // if we *are* the root component, then we can just provide the context directly - if self.scope_id() == ScopeId(0) { - self.shared_contexts - .borrow_mut() - .insert(TypeId::of::(), Box::new(value.clone())) - .and_then(|f| f.downcast::().ok()); - return value; - } - - let mut search_parent = self.parent_scope; - - while let Some(parent) = search_parent.take() { - let parent = unsafe { &*parent }; - - if parent.scope_id() == ScopeId(0) { - let exists = parent - .shared_contexts - .borrow_mut() - .insert(TypeId::of::(), Box::new(value.clone())); - - if exists.is_some() { - log::warn!("Context already provided to parent scope - replacing it"); - } - return value; - } - - search_parent = parent.parent_scope; - } - - unreachable!("all apps have a root scope") - } - - /// Try to retrieve a shared state with type T from the any parent Scope. - pub fn consume_context(&self) -> Option { - if let Some(shared) = self.shared_contexts.borrow().get(&TypeId::of::()) { - Some( - (*shared - .downcast_ref::() - .expect("Context of type T should exist")) - .clone(), - ) - } else { - let mut search_parent = self.parent_scope; - - while let Some(parent_ptr) = search_parent { - // safety: all parent pointers are valid thanks to the bump arena - let parent = unsafe { &*parent_ptr }; - if let Some(shared) = parent.shared_contexts.borrow().get(&TypeId::of::()) { - return Some( - shared - .downcast_ref::() - .expect("Context of type T should exist") - .clone(), - ); - } - search_parent = parent.parent_scope; - } - None - } + value2 } /// Pushes the future onto the poll queue to be polled after the component renders. pub fn push_future(&self, fut: impl Future + 'static) -> TaskId { - // wake up the scheduler if it is sleeping - self.tasks - .sender - .unbounded_send(SchedulerMsg::NewTask(self.our_arena_idx)) - .expect("Scheduler should exist"); - - self.tasks.spawn(self.our_arena_idx, fut) + self.tasks.spawn(self.id, fut) } /// Spawns the future but does not return the [`TaskId`] @@ -870,27 +330,30 @@ impl ScopeState { self.push_future(fut); } - /// Spawn a future that Dioxus will never clean up + /// Spawn a future that Dioxus won't clean up when this component is unmounted /// /// This is good for tasks that need to be run after the component has been dropped. pub fn spawn_forever(&self, fut: impl Future + 'static) -> TaskId { + // The root scope will never be unmounted so we can just add the task at the top of the app + let id = self.tasks.spawn(ScopeId(0), fut); + // wake up the scheduler if it is sleeping self.tasks .sender - .unbounded_send(SchedulerMsg::NewTask(self.our_arena_idx)) + .unbounded_send(SchedulerMsg::TaskNotified(id)) .expect("Scheduler should exist"); - // The root scope will never be unmounted so we can just add the task at the top of the app - self.tasks.spawn(ScopeId(0), fut) + id } - /// Informs the scheduler that this task is no longer needed and should be removed - /// on next poll. + /// Informs the scheduler that this task is no longer needed and should be removed. + /// + /// This drops the task immediately. pub fn remove_future(&self, id: TaskId) { self.tasks.remove(id); } - /// Take a lazy [`VNode`] structure and actually build it with the context of the Vdoms efficient [`VNode`] allocator. + /// Take a lazy [`crate::VNode`] structure and actually build it with the context of the efficient [`bumpalo::Bump`] allocator. /// /// ## Example /// @@ -903,11 +366,124 @@ impl ScopeState { /// cx.render(lazy_tree) /// } ///``` - pub fn render<'src>(&'src self, rsx: LazyNodes<'src, '_>) -> Option> { - Some(rsx.call(NodeFactory { - scope: self, - bump: &self.wip_frame().bump, - })) + pub fn render(&'src self, rsx: LazyNodes<'src, '_>) -> Element<'src> { + Ok(rsx.call(self)) + } + + /// Create a dynamic text node using [`Arguments`] and the [`ScopeState`]'s internal [`Bump`] allocator + pub fn text_node(&'src self, args: Arguments) -> DynamicNode<'src> { + DynamicNode::Text(VText { + value: self.raw_text(args), + id: Default::default(), + }) + } + + /// Allocate some text inside the [`ScopeState`] from [`Arguments`] + /// + /// Uses the currently active [`Bump`] allocator + pub fn raw_text(&'src self, args: Arguments) -> &'src str { + args.as_str().unwrap_or_else(|| { + use bumpalo::core_alloc::fmt::Write; + let mut str_buf = bumpalo::collections::String::new_in(self.bump()); + str_buf.write_fmt(args).unwrap(); + str_buf.into_bump_str() + }) + } + + /// Convert any item that implements [`IntoDynNode`] into a [`DynamicNode`] using the internal [`Bump`] allocator + pub fn make_node<'c, I>(&'src self, into: impl IntoDynNode<'src, I> + 'c) -> DynamicNode { + into.into_vnode(self) + } + + /// Create a new [`Attribute`] from a name, value, namespace, and volatile bool + /// + /// "Volatile" referes to whether or not Dioxus should always override the value. This helps prevent the UI in + /// some renderers stay in sync with the VirtualDom's understanding of the world + pub fn attr( + &'src self, + name: &'static str, + value: impl IntoAttributeValue<'src>, + namespace: Option<&'static str>, + volatile: bool, + ) -> Attribute<'src> { + Attribute { + name, + namespace, + volatile, + mounted_element: Default::default(), + value: value.into_value(self.bump()), + } + } + + /// Create a new [`DynamicNode::Component`] variant + /// + /// + /// The given component can be any of four signatures. Remember that an [`Element`] is really a [`Result`]. + /// + /// ```rust, ignore + /// // Without explicit props + /// fn(Scope) -> Element; + /// async fn(Scope<'_>) -> Element; + /// + /// // With explicit props + /// fn(Scope) -> Element; + /// async fn(Scope>) -> Element; + /// ``` + pub fn component>( + &'src self, + component: fn(Scope<'src, P>) -> F, + props: P, + fn_name: &'static str, + ) -> DynamicNode<'src> + where + P: Properties + 'src, + { + let vcomp = VProps::new(component, P::memoize, props); + + // cast off the lifetime of the render return + let as_dyn: Box + '_> = Box::new(vcomp); + let extended: Box + 'src> = unsafe { std::mem::transmute(as_dyn) }; + + DynamicNode::Component(VComponent { + name: fn_name, + render_fn: component as *const (), + static_props: P::IS_STATIC, + props: Cell::new(Some(extended)), + scope: Cell::new(None), + }) + } + + /// Create a new [`EventHandler`] from an [`FnMut`] + pub fn event_handler(&'src self, f: impl FnMut(T) + 'src) -> EventHandler<'src, T> { + let handler: &mut dyn FnMut(T) = self.bump().alloc(f); + let caller = unsafe { BumpBox::from_raw(handler as *mut dyn FnMut(T)) }; + let callback = RefCell::new(Some(caller)); + EventHandler { callback } + } + + /// Create a new [`AttributeValue`] with the listener variant from a callback + /// + /// The callback must be confined to the lifetime of the ScopeState + pub fn listener( + &'src self, + mut callback: impl FnMut(Event) + 'src, + ) -> AttributeValue<'src> { + // safety: there's no other way to create a dynamicly-dispatched bump box other than alloc + from-raw + // This is the suggested way to build a bumpbox + // + // In theory, we could just use regular boxes + let boxed: BumpBox<'src, dyn FnMut(_) + 'src> = unsafe { + BumpBox::from_raw(self.bump().alloc(move |event: Event| { + if let Ok(data) = event.data.downcast::() { + callback(Event { + propogates: event.propogates, + data, + }) + } + })) + }; + + AttributeValue::Listener(RefCell::new(Some(boxed))) } /// Store a value between renders. The foundational hook for all other hooks. @@ -928,19 +504,17 @@ impl ScopeState { /// ``` #[allow(clippy::mut_from_ref)] pub fn use_hook(&self, initializer: impl FnOnce() -> State) -> &mut State { - let mut vals = self.hook_vals.borrow_mut(); + let cur_hook = self.hook_idx.get(); + let mut hook_list = self.hook_list.borrow_mut(); - let hook_len = vals.len(); - let cur_idx = self.hook_idx.get(); - - if cur_idx >= hook_len { - vals.push(self.hook_arena.alloc(initializer())); + if cur_hook >= hook_list.len() { + hook_list.push(self.hook_arena.alloc(initializer())); } - vals - .get(cur_idx) + hook_list + .get(cur_hook) .and_then(|inn| { - self.hook_idx.set(cur_idx + 1); + self.hook_idx.set(cur_hook + 1); let raw_box = unsafe { &mut **inn }; raw_box.downcast_mut::() }) @@ -954,157 +528,4 @@ impl ScopeState { "###, ) } - - /// The "work in progress frame" represents the frame that is currently being worked on. - pub(crate) fn wip_frame(&self) -> &BumpFrame { - match self.generation.get() & 1 { - 0 => &self.frames[0], - _ => &self.frames[1], - } - } - - /// Mutable access to the "work in progress frame" - used to clear it - pub(crate) fn wip_frame_mut(&mut self) -> &mut BumpFrame { - match self.generation.get() & 1 { - 0 => &mut self.frames[0], - _ => &mut self.frames[1], - } - } - - /// Access to the frame where finalized nodes existed - pub(crate) fn fin_frame(&self) -> &BumpFrame { - match self.generation.get() & 1 { - 1 => &self.frames[0], - _ => &self.frames[1], - } - } - - /// Reset this component's frame - /// - /// # Safety: - /// - /// This method breaks every reference of every [`VNode`] in the current frame. - /// - /// Calling reset itself is not usually a big deal, but we consider it important - /// due to the complex safety guarantees we need to uphold. - pub(crate) unsafe fn reset_wip_frame(&mut self) { - self.wip_frame_mut().bump.reset(); - } - - /// Cycle to the next generation - pub(crate) fn cycle_frame(&self) { - self.generation.set(self.generation.get() + 1); - } - - // todo: disable bookkeeping on drop (unncessary) - pub(crate) fn reset(&mut self) { - // first: book keaping - self.hook_idx.set(0); - self.parent_scope = None; - self.generation.set(0); - - // next: shared context data - self.shared_contexts.get_mut().clear(); - - // next: reset the node data - let SelfReferentialItems { - borrowed_props, - listeners, - } = self.items.get_mut(); - borrowed_props.clear(); - listeners.clear(); - self.frames[0].reset(); - self.frames[1].reset(); - - // Free up the hook values - self.hook_vals.get_mut().drain(..).for_each(|state| { - let as_mut = unsafe { &mut *state }; - let boxed = unsafe { bumpalo::boxed::Box::from_raw(as_mut) }; - drop(boxed); - }); - - // Finally, clear the hook arena - self.hook_arena.reset(); - } -} - -pub(crate) struct BumpFrame { - pub bump: Bump, - pub node: Cell<*const VNode<'static>>, -} -impl BumpFrame { - pub(crate) fn new(capacity: usize) -> Self { - let bump = Bump::with_capacity(capacity); - let node = bump.alloc(VText { - text: "placeholdertext", - id: Cell::default(), - is_static: false, - }); - let node = bump.alloc(VNode::Text(unsafe { - &*(node as *mut VText as *const VText) - })); - let nodes = Cell::new(node as *const _); - Self { bump, node: nodes } - } - - pub(crate) fn reset(&mut self) { - self.bump.reset(); - let node = self.bump.alloc(VText { - text: "placeholdertext", - id: Cell::default(), - is_static: false, - }); - let node = self.bump.alloc(VNode::Text(unsafe { - &*(node as *mut VText as *const VText) - })); - self.node.set(node as *const _); - } -} - -pub(crate) struct TaskQueue { - pub(crate) tasks: RefCell>, - pub(crate) task_map: RefCell>>, - gen: Cell, - sender: UnboundedSender, -} - -pub(crate) type InnerTask = Pin>>; -impl TaskQueue { - fn spawn(&self, scope: ScopeId, task: impl Future + 'static) -> TaskId { - let pinned = Box::pin(task); - let id = self.gen.get(); - self.gen.set(id + 1); - let tid = TaskId { id, scope }; - - self.tasks.borrow_mut().insert(tid, pinned); - - // also add to the task map - // when the component is unmounted we know to remove it from the map - self.task_map - .borrow_mut() - .entry(scope) - .or_default() - .insert(tid); - - tid - } - - fn remove(&self, id: TaskId) { - if let Ok(mut tasks) = self.tasks.try_borrow_mut() { - tasks.remove(&id); - if let Some(task_map) = self.task_map.borrow_mut().get_mut(&id.scope) { - task_map.remove(&id); - } - } - // the task map is still around, but it'll be removed when the scope is unmounted - } - - pub(crate) fn has_tasks(&self) -> bool { - !self.tasks.borrow().is_empty() - } -} - -#[test] -fn sizeof() { - dbg!(std::mem::size_of::()); } diff --git a/packages/core/src/subtree.rs b/packages/core/src/subtree.rs new file mode 100644 index 000000000..57d09efe2 --- /dev/null +++ b/packages/core/src/subtree.rs @@ -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, +} diff --git a/packages/core/src/template.rs b/packages/core/src/template.rs deleted file mode 100644 index caf3d7ffb..000000000 --- a/packages/core/src/template.rs +++ /dev/null @@ -1,1302 +0,0 @@ -//! Templates are used to skip diffing on any static parts of the rsx. -//! TemplateNodes are different from VNodes in that they can contain partial dynamic and static content in the same node. -//! For example: -//! ``` -//! # use dioxus::prelude::*; -//! # let color = "red"; -//! # let dynamic_text_1 = "a"; -//! # let dynamic_text_2 = "b"; -//! # let dynamic_iterator = std::iter::once(""); -//! rsx! { -//! div { -//! color: "{color}", -//! "Hello, world", -//! "{dynamic_text_1}", -//! "{dynamic_text_2}", -//! dynamic_iterator -//! } -//! }; -//! ``` -//! The above will turn into a template that contains information on how to build div { "Hello, world" } and then every refrence to the template will hydrate with the value of dynamic_text_1, dynamic_text_2, dynamic_iterator, and the color property. -//! The rsx macro will both generate the template and the `DynamicNodeMapping` struct that contains the information on what parts of the template depend on each value of the dynamic context. -//! In templates with many dynamic parts, this allows the diffing algorithm to skip traversing the template to find what part to hydrate. -//! Each dynamic part will contain a index into the dynamic context to determine what value to use. The indexes are origionally ordered by traversing the tree depth first from the root. -//! The indexes for the above would be as follows: -//! ``` -//! # use dioxus::prelude::*; -//! # let color = "red"; -//! # let dynamic_text_1 = "a"; -//! # let dynamic_text_2 = "b"; -//! # let dynamic_iterator = std::iter::once(""); -//! rsx! { -//! div { -//! color: "{color}", // attribute index 0 -//! "Hello, world", -//! "{dynamic_text_1}", // text index 0 -//! "{dynamic_text_2}", // text index 1 -//! dynamic_iterator // node index 0 -//! } -//! }; -//! ``` -//! Including these indexes allows hot reloading to move the dynamic parts of the template around. -//! The templates generated by rsx are stored as 'static refrences, but you can change the template at runtime to allow hot reloading. -//! The template could be replaced with a new one at runtime: -//! ``` -//! # use dioxus::prelude::*; -//! # let color = "red"; -//! # let dynamic_text_1 = "a"; -//! # let dynamic_text_2 = "b"; -//! # let dynamic_iterator = std::iter::once(""); -//! rsx! { -//! div { -//! "Hello, world", -//! dynamic_iterator // node index 0 -//! h1 { -//! background_color: "{color}", // attribute index 0 -//! "{dynamic_text_2}", // text index 1 -//! } -//! h1 { -//! color: "{color}", // attribute index 0 -//! "{dynamic_text_1}", // text index 0 -//! } -//! } -//! }; -//! ``` -//! Notice how the indecies are no longer in depth first traversal order, and indecies are no longer unique. Attributes and dynamic parts of the text can be duplicated, but dynamic vnodes and componets cannot. -//! To minimize the cost of allowing hot reloading on applications that do not use it there are &'static and owned versions of template nodes, and dynamic node mapping. -//! -//! Notes: -//! 1) The template allow diffing to scale with reactivity. -//! With a virtual dom the diffing cost scales with the number of nodes in the dom. With templates the cost scales with the number of dynamic parts of the dom. The dynamic template context links any parts of the template that can change which allows the diffing algorithm to skip traversing the template and find what part to hydrate in constant time. - -use once_cell::unsync::OnceCell; -use std::{ - cell::{Cell, RefCell}, - hash::Hash, - marker::PhantomData, - ptr, -}; - -use rustc_hash::FxHashMap; - -use bumpalo::Bump; - -use crate::{ - diff::DiffState, dynamic_template_context::TemplateContext, nodes::AttributeDiscription, - scopes::ScopeArena, Attribute, AttributeValue, ElementId, Mutations, OwnedAttributeValue, - OwnedDynamicNodeMapping, StaticDynamicNodeMapping, -}; - -#[derive(Debug, Clone, Copy)] -pub(crate) struct TemplateRefId(pub usize); - -/// The location of a charicter. Used to track the location of rsx calls for hot reloading. -#[derive(Debug, Clone, PartialEq, Eq)] -#[cfg_attr( - all(feature = "serialize", any(feature = "hot-reload", debug_assertions)), - derive(serde::Serialize) -)] -pub struct StaticCodeLocation { - /// the path to the crate that contains the location - pub crate_path: &'static str, - /// the path within the crate to the file that contains the location - pub file_path: &'static str, - /// the line number of the location - pub line: u32, - /// the column number of the location - pub column: u32, -} - -/// The location of a charicter. Used to track the location of rsx calls for hot reloading. -#[derive(Debug, Clone, PartialEq, Eq)] -#[cfg(any(feature = "hot-reload", debug_assertions))] -#[cfg_attr( - all(feature = "serialize", any(feature = "hot-reload", debug_assertions)), - derive(serde::Serialize, serde::Deserialize) -)] -pub struct OwnedCodeLocation { - /// the path to the crate that contains the location - pub crate_path: String, - /// the path within the crate to the file that contains the location - pub file_path: String, - /// the line number of the location - pub line: u32, - /// the column number of the location - pub column: u32, -} - -/// The location of a charicter. Used to track the location of rsx calls for hot reloading. -#[derive(Clone, Eq, Debug)] -#[cfg_attr( - all(feature = "serialize", any(feature = "hot-reload", debug_assertions)), - derive(serde::Serialize) -)] -#[cfg_attr( - all(feature = "serialize", any(feature = "hot-reload", debug_assertions)), - serde(untagged) -)] -pub enum CodeLocation { - /// A loctation that is created at compile time. - Static(&'static StaticCodeLocation), - #[cfg(any(feature = "hot-reload", debug_assertions))] - /// A loctation that is created at runtime. - Dynamic(Box), -} - -#[cfg(all(feature = "serialize", any(feature = "hot-reload", debug_assertions)))] -impl<'de> serde::Deserialize<'de> for CodeLocation { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - Ok(Self::Dynamic(Box::new(OwnedCodeLocation::deserialize( - deserializer, - )?))) - } -} - -impl Hash for CodeLocation { - fn hash(&self, state: &mut H) { - match self { - CodeLocation::Static(loc) => { - let loc: &'static _ = *loc; - state.write_usize((loc as *const _) as usize); - } - #[cfg(any(feature = "hot-reload", debug_assertions))] - CodeLocation::Dynamic(loc) => { - let (crate_path, file_path): (&str, &str) = (&loc.crate_path, &loc.file_path); - crate_path.hash(state); - file_path.hash(state); - state.write_u32(loc.line); - state.write_u32(loc.column); - } - } - } -} - -impl PartialEq for CodeLocation { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Self::Static(l), Self::Static(r)) => ptr::eq(*l, *r), - #[cfg(any(feature = "hot-reload", debug_assertions))] - (Self::Dynamic(l), Self::Dynamic(r)) => l == r, - #[cfg(any(feature = "hot-reload", debug_assertions))] - (Self::Static(l), Self::Dynamic(r)) => **r == **l, - #[cfg(any(feature = "hot-reload", debug_assertions))] - (Self::Dynamic(l), Self::Static(r)) => **l == **r, - } - } -} - -#[cfg(any(feature = "hot-reload", debug_assertions))] -impl PartialEq for OwnedCodeLocation { - fn eq(&self, other: &StaticCodeLocation) -> bool { - self.crate_path == other.crate_path - && self.file_path == other.file_path - && self.line == other.line - && self.column == other.column - } -} - -impl CodeLocation { - /// Get the line number of the location. - pub fn line(&self) -> u32 { - match self { - CodeLocation::Static(loc) => loc.line, - #[cfg(any(feature = "hot-reload", debug_assertions))] - CodeLocation::Dynamic(loc) => loc.line, - } - } - - /// Get the column number of the location. - pub fn column(&self) -> u32 { - match self { - CodeLocation::Static(loc) => loc.column, - #[cfg(any(feature = "hot-reload", debug_assertions))] - CodeLocation::Dynamic(loc) => loc.column, - } - } - - /// Get the path within the crate to the location. - pub fn file_path(&self) -> &str { - match self { - CodeLocation::Static(loc) => loc.file_path, - #[cfg(any(feature = "hot-reload", debug_assertions))] - CodeLocation::Dynamic(loc) => loc.file_path.as_str(), - } - } - - /// Get the path of the crate to the location. - pub fn crate_path(&self) -> &str { - match self { - CodeLocation::Static(loc) => loc.crate_path, - #[cfg(any(feature = "hot-reload", debug_assertions))] - CodeLocation::Dynamic(loc) => loc.crate_path.as_str(), - } - } - - #[cfg(any(feature = "hot-reload", debug_assertions))] - /// Create an owned code location from a code location. - pub fn to_owned(&self) -> OwnedCodeLocation { - match self { - CodeLocation::Static(loc) => OwnedCodeLocation { - crate_path: loc.crate_path.to_owned(), - file_path: loc.file_path.to_owned(), - line: loc.line, - column: loc.column, - }, - #[cfg(any(feature = "hot-reload", debug_assertions))] - CodeLocation::Dynamic(loc) => *loc.clone(), - } - } -} - -/// get the code location of the code that called this function -#[macro_export] -macro_rules! get_line_num { - () => {{ - const LOC: CodeLocation = CodeLocation::Static(&StaticCodeLocation { - crate_path: env!("CARGO_MANIFEST_DIR"), - file_path: file!(), - line: line!(), - column: column!(), - }); - LOC - }}; -} - -/// An Template's unique identifier within the vdom. -/// -/// `TemplateId` is a refrence to the location in the code the template was created. -#[derive(Clone, PartialEq, Eq, Hash, Debug)] -#[cfg_attr( - all(feature = "serialize", any(feature = "hot-reload", debug_assertions)), - derive(serde::Serialize, serde::Deserialize) -)] -pub struct TemplateId(pub CodeLocation); - -/// An Template's unique identifier within the renderer. -/// -/// `RendererTemplateId` is a unique id of the template sent to the renderer. It is unique across time. -#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] -pub struct RendererTemplateId(pub usize); - -impl From for u64 { - fn from(id: RendererTemplateId) -> u64 { - id.0 as u64 - } -} - -/// A TemplateNode's unique identifier. -/// -/// `TemplateNodeId` is a `usize` that is only unique across the template that contains it, it is not unique across multaple instances of that template. -#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serialize", serde(transparent))] -pub struct TemplateNodeId(pub usize); - -/// A refrence to a template along with any context needed to hydrate it -pub struct VTemplateRef<'a> { - pub(crate) template_ref_id: Cell>, - pub template_id: TemplateId, - pub dynamic_context: TemplateContext<'a>, - /// The parent of the template - pub(crate) parent: Cell>, - // any nodes that already have ids assigned to them in the renderer - pub node_ids: RefCell>>, -} - -impl<'a> VTemplateRef<'a> { - // update the template with content from the dynamic context - pub(crate) fn hydrate<'b: 'a>( - &'b self, - parent: ElementId, - template: &Template, - diff_state: &mut DiffState<'a>, - ) { - fn traverse_seg<'b, T, O, Nodes, Attributes, V, Children, Listeners, TextSegments, Text>( - seg: &PathSeg, - nodes: &Nodes, - diff_state: &mut DiffState<'b>, - template_ref: &'b VTemplateRef<'b>, - parent: ElementId, - ) where - Nodes: AsRef<[TemplateNode]>, - Attributes: AsRef<[TemplateAttribute]>, - V: TemplateValue, - Children: AsRef<[TemplateNodeId]>, - Listeners: AsRef<[usize]>, - TextSegments: AsRef<[TextTemplateSegment]>, - Text: AsRef, - T: Traversable, - O: AsRef<[UpdateOp]>, - { - let mut current_node_id = None; - let mut temp_id = false; - for op in seg.ops.as_ref() { - match op { - UpdateOp::StoreNode(id) => { - if let Some(real_id) = template_ref.try_get_node_id(*id) { - current_node_id = Some(real_id); - nodes.as_ref()[id.0].hydrate(real_id, diff_state, template_ref); - } else { - let real_id = diff_state.scopes.reserve_template_node( - template_ref.template_ref_id.get().unwrap(), - *id, - ); - current_node_id = Some(real_id); - template_ref.set_node_id(*id, real_id); - diff_state.mutations.store_with_id(real_id.as_u64()); - nodes.as_ref()[id.0].hydrate(real_id, diff_state, template_ref); - } - } - UpdateOp::AppendChild(id) => { - let node = &nodes.as_ref()[id.0]; - match &node.node_type { - TemplateNodeType::DynamicNode(idx) => { - if current_node_id.is_none() { - // create a temporary node to come back to later - let id = diff_state.scopes.reserve_phantom_node(); - diff_state.mutations.store_with_id(id.as_u64()); - temp_id = true; - current_node_id = Some(id); - } - let id = current_node_id.unwrap(); - let mut created = Vec::new(); - let node = template_ref.dynamic_context.resolve_node(*idx); - diff_state.create_node(id, node, &mut created); - diff_state.mutations.set_last_node(id.as_u64()); - diff_state.mutations.append_children(None, created); - } - _ => panic!("can only insert dynamic nodes"), - } - } - UpdateOp::InsertBefore(id) | UpdateOp::InsertAfter(id) => { - let node = &nodes.as_ref()[id.0]; - match &node.node_type { - TemplateNodeType::DynamicNode(idx) => { - if current_node_id.is_none() { - // create a temporary node to come back to later - let id = diff_state.scopes.reserve_phantom_node(); - diff_state.mutations.store_with_id(id.as_u64()); - temp_id = true; - current_node_id = Some(id); - } - let id = current_node_id.unwrap(); - let mut created = Vec::new(); - let node = template_ref.dynamic_context.resolve_node(*idx); - diff_state.create_node(parent, node, &mut created); - diff_state.mutations.set_last_node(id.as_u64()); - match op { - UpdateOp::InsertBefore(_) => { - diff_state.mutations.insert_before(None, created); - } - UpdateOp::InsertAfter(_) => { - diff_state.mutations.insert_after(None, created); - } - _ => unreachable!(), - } - } - _ => panic!("can only insert dynamic nodes"), - } - } - } - } - match (seg.traverse.first_child(), seg.traverse.next_sibling()) { - (Some(child), Some(sibling)) => { - if current_node_id.is_none() { - // create a temporary node to come back to later - let id = diff_state.scopes.reserve_phantom_node(); - diff_state.mutations.store_with_id(id.as_u64()); - temp_id = true; - current_node_id = Some(id); - } - let id = current_node_id.unwrap(); - diff_state.mutations.first_child(); - traverse_seg(child, nodes, diff_state, template_ref, id); - diff_state.mutations.set_last_node(id.as_u64()); - diff_state.mutations.next_sibling(); - traverse_seg(sibling, nodes, diff_state, template_ref, parent); - } - (Some(seg), None) => { - if current_node_id.is_none() { - let id = diff_state.scopes.reserve_phantom_node(); - diff_state.mutations.store_with_id(id.as_u64()); - temp_id = true; - current_node_id = Some(id); - } - let id = current_node_id.unwrap(); - diff_state.mutations.first_child(); - traverse_seg(seg, nodes, diff_state, template_ref, id); - } - (None, Some(seg)) => { - diff_state.mutations.next_sibling(); - traverse_seg(seg, nodes, diff_state, template_ref, parent); - } - (None, None) => {} - } - if temp_id { - if let Some(id) = current_node_id { - // remove the temporary node - diff_state.scopes.collect_garbage(id); - } - } - } - fn hydrate_inner<'b, Nodes, Attributes, V, Children, Listeners, TextSegments, Text>( - nodes: &Nodes, - ctx: ( - &mut DiffState<'b>, - &'b VTemplateRef<'b>, - &Template, - ElementId, - ), - ) where - Nodes: AsRef<[TemplateNode]>, - Attributes: AsRef<[TemplateAttribute]>, - V: TemplateValue, - Children: AsRef<[TemplateNodeId]>, - Listeners: AsRef<[usize]>, - TextSegments: AsRef<[TextTemplateSegment]>, - Text: AsRef, - { - let (diff_state, template_ref, template, parent) = ctx; - - match template { - Template::Static(s) => { - if let Some(seg) = &s.dynamic_path { - diff_state.mutations.set_last_node( - template_ref.get_node_id(template.root_nodes()[0]).as_u64(), - ); - traverse_seg(seg, nodes, diff_state, template_ref, parent); - } - } - Template::Owned(o) => { - if let Some(seg) = &o.dynamic_path { - diff_state.mutations.set_last_node( - template_ref.get_node_id(template.root_nodes()[0]).as_u64(), - ); - traverse_seg(seg, nodes, diff_state, template_ref, parent); - } - } - } - } - - template.with_nodes( - hydrate_inner, - hydrate_inner, - (diff_state, self, template, parent), - ); - } - - pub(crate) fn get_node_id(&self, id: TemplateNodeId) -> ElementId { - self.try_get_node_id(id).unwrap() - } - - pub(crate) fn try_get_node_id(&self, id: TemplateNodeId) -> Option { - let node_ids = self.node_ids.borrow(); - node_ids.get(id.0).and_then(|cell| cell.get().copied()) - } - - pub(crate) fn set_node_id(&self, id: TemplateNodeId, real_id: ElementId) { - let mut ids = self.node_ids.borrow_mut(); - if ids.len() <= id.0 { - ids.resize(id.0 + 1, OnceCell::new()); - } - ids[id.0].set(real_id).unwrap(); - } -} - -/// A template that is created at compile time -#[derive(Debug, PartialEq)] -pub struct StaticTemplate { - /// The nodes in the template - pub nodes: StaticTemplateNodes, - /// The ids of the root nodes in the template - pub root_nodes: StaticRootNodes, - /// Any nodes that contain dynamic components. This is stored in the tmeplate to avoid traversing the tree every time a template is refrenced. - pub dynamic_mapping: StaticDynamicNodeMapping, - /// The path to take to update the template with dynamic content (starts from the first root node) - pub dynamic_path: Option, -} - -/// A template that is created at runtime -#[derive(Debug, Clone, PartialEq)] -#[cfg_attr( - all(feature = "serialize", any(feature = "hot-reload", debug_assertions)), - derive(serde::Serialize, serde::Deserialize) -)] -#[cfg(any(feature = "hot-reload", debug_assertions))] -pub struct OwnedTemplate { - /// The nodes in the template - pub nodes: OwnedTemplateNodes, - /// The ids of the root nodes in the template - pub root_nodes: OwnedRootNodes, - /// Any nodes that contain dynamic components. This is stored in the tmeplate to avoid traversing the tree every time a template is refrenced. - pub dynamic_mapping: OwnedDynamicNodeMapping, - /// The path to take to update the template with dynamic content - pub dynamic_path: Option, -} - -/// A template used to skip diffing on some static parts of the rsx -#[derive(Debug, Clone, PartialEq)] -pub enum Template { - /// A template that is createded at compile time - Static(&'static StaticTemplate), - #[cfg(any(feature = "hot-reload", debug_assertions))] - /// A template that is created at runtime - Owned(OwnedTemplate), -} - -impl Template { - pub(crate) fn create<'b>(&self, mutations: &mut Mutations<'b>, bump: &'b Bump, id: ElementId) { - let children = match self { - Template::Static(s) => self.count_real_nodes(s.root_nodes), - #[cfg(any(feature = "hot-reload", debug_assertions))] - Template::Owned(o) => self.count_real_nodes(&o.root_nodes), - }; - mutations.create_element("template", None, Some(id.into()), children as u32); - let empty = match self { - Template::Static(s) => s.nodes.is_empty(), - #[cfg(any(feature = "hot-reload", debug_assertions))] - Template::Owned(o) => o.nodes.is_empty(), - }; - if !empty { - let roots = match self { - Template::Static(s) => s.root_nodes, - #[cfg(any(feature = "hot-reload", debug_assertions))] - Template::Owned(o) => &o.root_nodes, - }; - for root in roots { - self.create_node(mutations, bump, *root); - } - } - } - - fn create_node<'b>(&self, mutations: &mut Mutations<'b>, bump: &'b Bump, id: TemplateNodeId) { - fn crate_node_inner<'b, Attributes, V, Children, Listeners, TextSegments, Text>( - node: &TemplateNode, - ctx: (&mut Mutations<'b>, &'b Bump, &Template), - ) where - Attributes: AsRef<[TemplateAttribute]>, - V: TemplateValue, - Children: AsRef<[TemplateNodeId]>, - Listeners: AsRef<[usize]>, - TextSegments: AsRef<[TextTemplateSegment]>, - Text: AsRef, - { - let (mutations, bump, template) = ctx; - match &node.node_type { - TemplateNodeType::Element(el) => { - let TemplateElement { - tag, - namespace, - attributes, - children, - .. - } = el; - mutations.create_element( - tag, - *namespace, - None, - template.count_real_nodes(children.as_ref()) as u32, - ); - for attr in attributes.as_ref() { - if let TemplateAttributeValue::Static(val) = &attr.value { - let val: AttributeValue<'b> = val.allocate(bump); - let attribute = Attribute { - attribute: attr.attribute, - is_static: true, - value: val, - }; - mutations.set_attribute(bump.alloc(attribute), None); - } - } - for child in children.as_ref() { - template.create_node(mutations, bump, *child); - } - } - TemplateNodeType::Text(text) => { - let mut text_iter = text.segments.as_ref().iter(); - if let (Some(TextTemplateSegment::Static(txt)), None) = - (text_iter.next(), text_iter.next()) - { - mutations.create_text_node(bump.alloc_str(txt.as_ref()), None); - } else { - mutations.create_text_node("", None); - } - } - TemplateNodeType::DynamicNode(_) => {} - } - } - self.with_node( - id, - crate_node_inner, - crate_node_inner, - (mutations, bump, self), - ); - } - - #[cfg(any(feature = "hot-reload", debug_assertions))] - pub(crate) fn with_node( - &self, - id: TemplateNodeId, - mut f1: F1, - mut f2: F2, - ctx: Ctx, - ) -> R - where - F1: FnMut(&StaticTemplateNode, Ctx) -> R, - F2: FnMut(&OwnedTemplateNode, Ctx) -> R, - { - match self { - Template::Static(s) => f1(&s.nodes[id.0], ctx), - Template::Owned(o) => f2(&o.nodes[id.0], ctx), - } - } - - #[cfg(not(any(feature = "hot-reload", debug_assertions)))] - pub(crate) fn with_node( - &self, - id: TemplateNodeId, - mut f1: F1, - _f2: F2, - ctx: Ctx, - ) -> R - where - F1: FnMut(&StaticTemplateNode, Ctx) -> R, - F2: FnMut(&StaticTemplateNode, Ctx) -> R, - { - match self { - Template::Static(s) => f1(&s.nodes[id.0], ctx), - } - } - - #[cfg(any(feature = "hot-reload", debug_assertions))] - pub(crate) fn with_nodes<'a, F1, F2, Ctx, R>(&'a self, mut f1: F1, mut f2: F2, ctx: Ctx) -> R - where - F1: FnMut(&'a &'static [StaticTemplateNode], Ctx) -> R, - F2: FnMut(&'a Vec, Ctx) -> R, - { - match self { - Template::Static(s) => f1(&s.nodes, ctx), - Template::Owned(o) => f2(&o.nodes, ctx), - } - } - - #[cfg(not(any(feature = "hot-reload", debug_assertions)))] - pub(crate) fn with_nodes<'a, F1, F2, Ctx, R>(&'a self, mut f1: F1, _f2: F2, ctx: Ctx) -> R - where - F1: FnMut(&'a &'static [StaticTemplateNode], Ctx) -> R, - F2: FnMut(&'a &'static [StaticTemplateNode], Ctx) -> R, - { - match self { - Template::Static(s) => f1(&s.nodes, ctx), - } - } - - fn count_real_nodes(&self, ids: &[TemplateNodeId]) -> usize { - fn count_real_nodes_inner( - nodes: &Nodes, - id: TemplateNodeId, - ) -> usize - where - Nodes: AsRef<[TemplateNode]>, - Attributes: AsRef<[TemplateAttribute]>, - V: TemplateValue, - Children: AsRef<[TemplateNodeId]>, - Listeners: AsRef<[usize]>, - TextSegments: AsRef<[TextTemplateSegment]>, - Text: AsRef, - { - match &nodes.as_ref()[id.0].node_type { - TemplateNodeType::DynamicNode(_) => 0, - TemplateNodeType::Element(_) => 1, - TemplateNodeType::Text(_) => 1, - } - } - ids.iter() - .map(|id| self.with_nodes(count_real_nodes_inner, count_real_nodes_inner, *id)) - .sum() - } - - pub(crate) fn volatile_attributes<'a>( - &'a self, - ) -> Box + 'a> { - match self { - Template::Static(s) => Box::new( - s.dynamic_mapping - .volatile_attributes - .as_ref() - .iter() - .copied(), - ), - #[cfg(any(feature = "hot-reload", debug_assertions))] - Template::Owned(o) => Box::new(o.dynamic_mapping.volatile_attributes.iter().copied()), - } - } - - pub(crate) fn get_dynamic_nodes_for_text_index(&self, idx: usize) -> &[TemplateNodeId] { - match self { - Template::Static(s) => s.dynamic_mapping.text[idx], - #[cfg(any(feature = "hot-reload", debug_assertions))] - Template::Owned(o) => o.dynamic_mapping.text[idx].as_ref(), - } - } - - pub(crate) fn get_dynamic_nodes_for_attribute_index( - &self, - idx: usize, - ) -> &[(TemplateNodeId, usize)] { - match self { - Template::Static(s) => s.dynamic_mapping.attributes[idx], - #[cfg(any(feature = "hot-reload", debug_assertions))] - Template::Owned(o) => o.dynamic_mapping.attributes[idx].as_ref(), - } - } - - pub(crate) fn root_nodes(&self) -> &[TemplateNodeId] { - match self { - Template::Static(s) => s.root_nodes, - #[cfg(any(feature = "hot-reload", debug_assertions))] - Template::Owned(o) => &o.root_nodes, - } - } -} - -/// A array of stack allocated Template nodes -pub type StaticTemplateNodes = &'static [StaticTemplateNode]; -#[cfg(any(feature = "hot-reload", debug_assertions))] -/// A vec of heep allocated Template nodes -pub type OwnedTemplateNodes = Vec; - -/// A stack allocated Template node -pub type StaticTemplateNode = TemplateNode< - &'static [TemplateAttribute], - StaticAttributeValue, - &'static [TemplateNodeId], - &'static [usize], - &'static [TextTemplateSegment<&'static str>], - &'static str, ->; - -#[cfg(any(feature = "hot-reload", debug_assertions))] -/// A heap allocated Template node -pub type OwnedTemplateNode = TemplateNode< - Vec>, - OwnedAttributeValue, - Vec, - Vec, - Vec>, - String, ->; - -/// A stack allocated list of root Template nodes -pub type StaticRootNodes = &'static [TemplateNodeId]; - -#[cfg(any(feature = "hot-reload", debug_assertions))] -/// A heap allocated list of root Template nodes -pub type OwnedRootNodes = Vec; - -/// Templates can only contain a limited subset of VNodes and keys are not needed, as diffing will be skipped. -/// Dynamic parts of the Template are inserted into the VNode using the `TemplateContext` by traversing the tree in order and filling in dynamic parts -/// This template node is generic over the storage of the nodes to allow for owned and &'static versions. -#[derive(Debug, Clone, PartialEq, Eq)] -#[cfg_attr( - all(feature = "serialize", any(feature = "hot-reload", debug_assertions)), - derive(serde::Serialize, serde::Deserialize) -)] -pub struct TemplateNode -where - Attributes: AsRef<[TemplateAttribute]>, - V: TemplateValue, - Children: AsRef<[TemplateNodeId]>, - Listeners: AsRef<[usize]>, - TextSegments: AsRef<[TextTemplateSegment]>, - Text: AsRef, -{ - /// The ID of the [`TemplateNode`]. Note that this is not an elenemt id, and should be allocated seperately from VNodes on the frontend. - pub id: TemplateNodeId, - /// The depth of the node in the template node tree - /// Root nodes have a depth of 0 - pub depth: usize, - /// The type of the [`TemplateNode`]. - pub node_type: TemplateNodeType, - /// The parent of this node. - pub parent: Option, -} - -impl - TemplateNode -where - Attributes: AsRef<[TemplateAttribute]>, - V: TemplateValue, - Children: AsRef<[TemplateNodeId]>, - Listeners: AsRef<[usize]>, - TextSegments: AsRef<[TextTemplateSegment]>, - Text: AsRef, -{ - fn hydrate<'b>( - &self, - real_node_id: ElementId, - diff_state: &mut DiffState<'b>, - template_ref: &'b VTemplateRef<'b>, - ) { - match &self.node_type { - TemplateNodeType::Element(el) => { - let TemplateElement { - attributes, - listeners, - .. - } = el; - for attr in attributes.as_ref() { - if let TemplateAttributeValue::Dynamic(idx) = attr.value { - let attribute = Attribute { - attribute: attr.attribute, - value: template_ref - .dynamic_context - .resolve_attribute(idx) - .to_owned(), - is_static: false, - }; - let scope_bump = diff_state.current_scope_bump(); - diff_state.mutations.set_attribute( - scope_bump.alloc(attribute), - Some(real_node_id.as_u64()), - ); - } - } - for listener_idx in listeners.as_ref() { - let listener = template_ref.dynamic_context.resolve_listener(*listener_idx); - listener.mounted_node.set(Some(real_node_id)); - diff_state - .mutations - .new_event_listener(listener, diff_state.current_scope()); - } - } - TemplateNodeType::Text(text) => { - let scope_bump = diff_state.current_scope_bump(); - let mut bump_str = - bumpalo::collections::String::with_capacity_in(text.min_size, scope_bump); - template_ref - .dynamic_context - .resolve_text_into(text, &mut bump_str); - - diff_state - .mutations - .set_text(bump_str.into_bump_str(), Some(real_node_id.as_u64())); - } - TemplateNodeType::DynamicNode(_) => {} - } - } -} - -/// A template for an attribute -#[derive(Debug, Clone, PartialEq, Eq)] -#[cfg_attr( - all(feature = "serialize", any(feature = "hot-reload", debug_assertions)), - derive(serde::Serialize, serde::Deserialize) -)] -pub struct TemplateAttribute { - /// The discription of the attribute - pub attribute: AttributeDiscription, - /// The value of the attribute - pub value: TemplateAttributeValue, -} - -/// A template attribute value that is either dynamic or static -#[derive(Debug, Clone, PartialEq, Eq)] -#[cfg_attr( - all(feature = "serialize", any(feature = "hot-reload", debug_assertions)), - derive(serde::Serialize, serde::Deserialize) -)] -pub enum TemplateAttributeValue { - /// A static attribute - Static(V), - /// A dynamic attribute - Dynamic(usize), -} - -/// The value for an attribute in a template -pub trait TemplateValue { - /// Allocates the attribute in a bump allocator - fn allocate<'b>(&self, bump: &'b Bump) -> AttributeValue<'b>; -} - -impl TemplateValue for StaticAttributeValue { - fn allocate<'b>(&self, bump: &'b Bump) -> AttributeValue<'b> { - match self.clone() { - StaticAttributeValue::Text(txt) => AttributeValue::Text(bump.alloc_str(txt)), - StaticAttributeValue::Bytes(bytes) => { - AttributeValue::Bytes(bump.alloc_slice_copy(bytes)) - } - StaticAttributeValue::Float32(f) => AttributeValue::Float32(f), - StaticAttributeValue::Float64(f) => AttributeValue::Float64(f), - StaticAttributeValue::Int32(i) => AttributeValue::Int32(i), - StaticAttributeValue::Int64(i) => AttributeValue::Int64(i), - StaticAttributeValue::Uint32(u) => AttributeValue::Uint32(u), - StaticAttributeValue::Uint64(u) => AttributeValue::Uint64(u), - StaticAttributeValue::Bool(b) => AttributeValue::Bool(b), - StaticAttributeValue::Vec3Float(f1, f2, f3) => AttributeValue::Vec3Float(f1, f2, f3), - StaticAttributeValue::Vec3Int(i1, i2, i3) => AttributeValue::Vec3Int(i1, i2, i3), - StaticAttributeValue::Vec3Uint(u1, u2, u3) => AttributeValue::Vec3Uint(u1, u2, u3), - StaticAttributeValue::Vec4Float(f1, f2, f3, f4) => { - AttributeValue::Vec4Float(f1, f2, f3, f4) - } - StaticAttributeValue::Vec4Int(i1, i2, i3, i4) => { - AttributeValue::Vec4Int(i1, i2, i3, i4) - } - StaticAttributeValue::Vec4Uint(u1, u2, u3, u4) => { - AttributeValue::Vec4Uint(u1, u2, u3, u4) - } - } - } -} - -#[cfg(any(feature = "hot-reload", debug_assertions))] -impl TemplateValue for OwnedAttributeValue { - fn allocate<'b>(&self, bump: &'b Bump) -> AttributeValue<'b> { - match self.clone() { - OwnedAttributeValue::Text(txt) => AttributeValue::Text(bump.alloc(txt)), - OwnedAttributeValue::Bytes(bytes) => AttributeValue::Bytes(bump.alloc(bytes)), - OwnedAttributeValue::Float32(f) => AttributeValue::Float32(f), - OwnedAttributeValue::Float64(f) => AttributeValue::Float64(f), - OwnedAttributeValue::Int32(i) => AttributeValue::Int32(i), - OwnedAttributeValue::Int64(i) => AttributeValue::Int64(i), - OwnedAttributeValue::Uint32(u) => AttributeValue::Uint32(u), - OwnedAttributeValue::Uint64(u) => AttributeValue::Uint64(u), - OwnedAttributeValue::Bool(b) => AttributeValue::Bool(b), - OwnedAttributeValue::Vec3Float(f1, f2, f3) => AttributeValue::Vec3Float(f1, f2, f3), - OwnedAttributeValue::Vec3Int(i1, i2, i3) => AttributeValue::Vec3Int(i1, i2, i3), - OwnedAttributeValue::Vec3Uint(u1, u2, u3) => AttributeValue::Vec3Uint(u1, u2, u3), - OwnedAttributeValue::Vec4Float(f1, f2, f3, f4) => { - AttributeValue::Vec4Float(f1, f2, f3, f4) - } - OwnedAttributeValue::Vec4Int(i1, i2, i3, i4) => AttributeValue::Vec4Int(i1, i2, i3, i4), - OwnedAttributeValue::Vec4Uint(u1, u2, u3, u4) => { - AttributeValue::Vec4Uint(u1, u2, u3, u4) - } - OwnedAttributeValue::Any(owned) => { - AttributeValue::Any(crate::ArbitraryAttributeValue { - value: bump.alloc(owned.value), - cmp: owned.cmp, - }) - } - } - } -} - -/// The kind of node the template is. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr( - all(feature = "serialize", any(feature = "hot-reload", debug_assertions)), - derive(serde::Serialize, serde::Deserialize) -)] -pub enum TemplateNodeType -where - Attributes: AsRef<[TemplateAttribute]>, - Children: AsRef<[TemplateNodeId]>, - Listeners: AsRef<[usize]>, - V: TemplateValue, - TextSegments: AsRef<[TextTemplateSegment]>, - Text: AsRef, -{ - /// A element node (e.g. div{}). - Element(TemplateElement), - /// A text node (e.g. "Hello World"). - Text(TextTemplate), - /// A dynamic node (e.g. (0..10).map(|i| cx.render(rsx!{div{}}))) - /// The index in the dynamic node array this node should be replaced with - DynamicNode(usize), -} - -type StaticStr = &'static str; - -/// A element template -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr( - all(feature = "serialize", any(feature = "hot-reload", debug_assertions)), - derive(serde::Serialize, serde::Deserialize) -)] -pub struct TemplateElement -where - Attributes: AsRef<[TemplateAttribute]>, - Children: AsRef<[TemplateNodeId]>, - Listeners: AsRef<[usize]>, - V: TemplateValue, -{ - /// The tag name of the element - #[cfg_attr( - all(feature = "serialize", any(feature = "hot-reload", debug_assertions)), - serde(deserialize_with = "crate::util::deserialize_static_leaky") - )] - pub tag: StaticStr, - /// The namespace of the element - #[cfg_attr( - all(feature = "serialize", any(feature = "hot-reload", debug_assertions)), - serde(deserialize_with = "crate::util::deserialize_static_leaky_ns") - )] - pub namespace: Option, - /// The attributes that modify the element - pub attributes: Attributes, - /// The ids of the children of the element - pub children: Children, - /// The ids of the listeners of the element - pub listeners: Listeners, - value: PhantomData, -} - -impl TemplateElement -where - Attributes: AsRef<[TemplateAttribute]>, - Children: AsRef<[TemplateNodeId]>, - Listeners: AsRef<[usize]>, - V: TemplateValue, -{ - /// create a new element template - pub const fn new( - tag: &'static str, - namespace: Option<&'static str>, - attributes: Attributes, - children: Children, - listeners: Listeners, - ) -> Self { - TemplateElement { - tag, - namespace, - attributes, - children, - listeners, - value: PhantomData, - } - } -} - -/// A template for some text that may contain dynamic segments for example "Hello {name}" contains the static segment "Hello " and the dynamic segment "{name}". -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr( - all(feature = "serialize", any(feature = "hot-reload", debug_assertions)), - derive(serde::Serialize, serde::Deserialize) -)] -pub struct TextTemplate -where - Segments: AsRef<[TextTemplateSegment]>, - Text: AsRef, -{ - /// The segments of the template. - pub segments: Segments, - /// The minimum size of the output text. - pub min_size: usize, - text: PhantomData, -} - -impl TextTemplate -where - Segments: AsRef<[TextTemplateSegment]>, - Text: AsRef, -{ - /// create a new template from the segments it is composed of. - pub const fn new(segments: Segments, min_size: usize) -> Self { - TextTemplate { - segments, - min_size, - text: PhantomData, - } - } -} - -/// A segment of a text template that may be dynamic or static. -#[derive(Debug, Clone, PartialEq, Eq)] -#[cfg_attr( - all(feature = "serialize", any(feature = "hot-reload", debug_assertions)), - derive(serde::Serialize, serde::Deserialize) -)] -pub enum TextTemplateSegment -where - Text: AsRef, -{ - /// A constant text segment - Static(Text), - /// A dynamic text segment - Dynamic(usize), -} - -/// A template value that is created at compile time that is sync. -#[derive(Debug, Clone, PartialEq)] -#[allow(missing_docs)] -pub enum StaticAttributeValue { - Text(&'static 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(&'static [u8]), -} - -#[derive(Default)] -pub(crate) struct TemplateResolver { - // maps a id to the rendererid and if that template needs to be re-created - pub template_id_mapping: FxHashMap, -} - -impl TemplateResolver { - #[cfg(any(feature = "hot-reload", debug_assertions))] - pub fn mark_dirty(&mut self, id: &TemplateId) { - if let Some((_, dirty)) = self.template_id_mapping.get_mut(id) { - println!("marking dirty {:?}", id); - *dirty = true; - } else { - println!("failed {:?}", id); - } - } - - pub fn is_dirty(&self, id: &TemplateId) -> bool { - matches!(self.template_id_mapping.get(id), Some((_, true))) - } - - // returns (id, if the id was created) - pub fn get_or_create_client_id( - &mut self, - template_id: &TemplateId, - scopes: &ScopeArena, - ) -> (ElementId, bool) { - if let Some(id) = self.template_id_mapping.get(template_id) { - *id - } else { - let renderer_id = scopes.reserve_phantom_node(); - self.template_id_mapping - .insert(template_id.clone(), (renderer_id, false)); - (renderer_id, true) - } - } -} - -#[cfg(any(feature = "hot-reload", debug_assertions))] -/// A message telling the virtual dom to set a template -#[derive(Debug, Clone)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -pub struct SetTemplateMsg(pub TemplateId, pub OwnedTemplate); - -#[cfg(any(feature = "hot-reload", debug_assertions))] -/// A path segment that lives on the heap. -pub type OwnedPathSeg = PathSeg>; - -#[cfg(any(feature = "hot-reload", debug_assertions))] -#[cfg_attr( - all(feature = "serialize", any(feature = "hot-reload", debug_assertions)), - derive(serde::Serialize, serde::Deserialize) -)] -#[derive(Debug, Clone, PartialEq)] -/// A traverse message that lives on the heap. -pub enum OwnedTraverse { - /// Halt traversal - Halt, - /// Traverse to the first child of the current node. - FirstChild(Box), - /// Traverse to the next sibling of the current node. - NextSibling(Box), - /// Traverse to the both the first child and next sibling of the current node. - Both(Box<(OwnedPathSeg, OwnedPathSeg)>), -} - -#[cfg(any(feature = "hot-reload", debug_assertions))] -impl Traversable> for OwnedTraverse { - fn first_child(&self) -> Option<&OwnedPathSeg> { - match self { - OwnedTraverse::FirstChild(p) => Some(p), - OwnedTraverse::Both(ps) => Some(&ps.0), - _ => None, - } - } - - fn next_sibling(&self) -> Option<&OwnedPathSeg> { - match self { - OwnedTraverse::NextSibling(p) => Some(p), - OwnedTraverse::Both(ps) => Some(&ps.1), - _ => None, - } - } -} - -/// A path segment that lives on the stack. -pub type StaticPathSeg = PathSeg; - -#[derive(Debug, Clone, PartialEq)] -/// A traverse message that lives on the stack. -pub enum StaticTraverse { - /// Halt traversal - Halt, - /// Traverse to the first child of the current node. - FirstChild(&'static StaticPathSeg), - /// Traverse to the next sibling of the current node. - NextSibling(&'static StaticPathSeg), - /// Traverse to the both the first child and next sibling of the current node. - Both(&'static (StaticPathSeg, StaticPathSeg)), -} - -impl Traversable<&'static [UpdateOp]> for StaticTraverse { - fn first_child(&self) -> Option<&StaticPathSeg> { - match self { - StaticTraverse::FirstChild(p) => Some(p), - StaticTraverse::Both((p, _)) => Some(p), - _ => None, - } - } - - fn next_sibling(&self) -> Option<&StaticPathSeg> { - match self { - StaticTraverse::NextSibling(p) => Some(p), - StaticTraverse::Both((_, p)) => Some(p), - _ => None, - } - } -} - -pub trait Traversable> -where - Self: Sized, -{ - fn first_child(&self) -> Option<&PathSeg>; - fn next_sibling(&self) -> Option<&PathSeg>; -} - -#[derive(Debug, Clone, PartialEq, Eq)] -#[cfg_attr( - all(feature = "serialize", any(feature = "hot-reload", debug_assertions)), - derive(serde::Serialize, serde::Deserialize) -)] -/// A path segment that defines a way to traverse a template node and resolve dynamic sections. -pub struct PathSeg, O: AsRef<[UpdateOp]>> { - /// The operation to perform on the current node. - pub ops: O, - /// The next traversal step. - pub traverse: T, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -#[cfg_attr( - all(feature = "serialize", any(feature = "hot-reload", debug_assertions)), - derive(serde::Serialize, serde::Deserialize) -)] -/// A operation that can be applied to a template node when intially updating it. -pub enum UpdateOp { - /// Store a dynamic node on the renderer - StoreNode(TemplateNodeId), - /// Insert a dynamic node before the current node - InsertBefore(TemplateNodeId), - /// Insert a dynamic node after the current node - InsertAfter(TemplateNodeId), - /// Append a dynamic node to the current node - AppendChild(TemplateNodeId), -} diff --git a/packages/core/src/util.rs b/packages/core/src/util.rs deleted file mode 100644 index 30f1f73d3..000000000 --- a/packages/core/src/util.rs +++ /dev/null @@ -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>> = - 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, D::Error> - where - D: serde::Deserializer<'de>, - { - use serde::Deserialize; - Ok(>::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::*; diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 09637d8a1..ced92acb0 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -2,17 +2,30 @@ //! //! This module provides the primary mechanics to create a hook-based, concurrent VDOM for Rust. -use std::{collections::VecDeque, iter::FromIterator, task::Poll}; - -use crate::diff::DiffState; -use crate::innerlude::*; -use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender}; -use futures_util::{future::poll_fn, StreamExt}; -use rustc_hash::FxHashSet; +use crate::{ + any_props::VProps, + arena::{ElementId, ElementRef}, + innerlude::{DirtyScope, ErrorBoundary, Mutations, Scheduler, SchedulerMsg}, + mutations::Mutation, + nodes::RenderReturn, + nodes::{Template, TemplateId}, + scheduler::SuspenseId, + scopes::{ScopeId, ScopeState}, + AttributeValue, Element, Event, Scope, SuspenseContext, +}; +use futures_util::{pin_mut, StreamExt}; +use slab::Slab; +use std::{ + any::Any, + borrow::BorrowMut, + cell::Cell, + collections::{BTreeSet, HashMap}, + future::Future, + rc::Rc, +}; /// A virtual node system that progresses user events and diffs UI trees. /// -/// /// ## Guide /// /// Components are defined as simple functions that take [`Scope`] and return an [`Element`]. @@ -50,95 +63,112 @@ use rustc_hash::FxHashSet; /// let edits = vdom.rebuild(); /// ``` /// -/// To inject UserEvents into the VirtualDom, call [`VirtualDom::get_scheduler_channel`] to get access to the scheduler. +/// To call listeners inside the VirtualDom, call [`VirtualDom::handle_event`] with the appropriate event data. /// /// ```rust, ignore -/// let channel = vdom.get_scheduler_channel(); -/// channel.send_unbounded(SchedulerMsg::UserEvent(UserEvent { -/// // ... -/// })) +/// vdom.handle_event(event); /// ``` /// -/// While waiting for UserEvents to occur, call [`VirtualDom::wait_for_work`] to poll any futures inside the VirtualDom. +/// While no events are ready, call [`VirtualDom::wait_for_work`] to poll any futures inside the VirtualDom. /// /// ```rust, ignore /// vdom.wait_for_work().await; /// ``` /// -/// Once work is ready, call [`VirtualDom::work_with_deadline`] to compute the differences between the previous and +/// Once work is ready, call [`VirtualDom::render_with_deadline`] to compute the differences between the previous and /// current UI trees. This will return a [`Mutations`] object that contains Edits, Effects, and NodeRefs that need to be /// handled by the renderer. /// /// ```rust, ignore -/// let mutations = vdom.work_with_deadline(|| false); -/// for edit in mutations { -/// apply(edit); +/// let mutations = vdom.work_with_deadline(tokio::time::sleep(Duration::from_millis(100))); +/// +/// for edit in mutations.edits { +/// real_dom.apply(edit); /// } /// ``` /// +/// To not wait for suspense while diffing the VirtualDom, call [`VirtualDom::render_immediate`] or pass an immediately +/// ready future to [`VirtualDom::render_with_deadline`]. +/// +/// /// ## Building an event loop around Dioxus: /// /// Putting everything together, you can build an event loop around Dioxus by using the methods outlined above. -/// /// ```rust, ignore -/// fn App(cx: Scope) -> Element { -/// cx.render(rsx!{ +/// fn app(cx: Scope) -> Element { +/// cx.render(rsx! { /// div { "Hello World" } /// }) /// } /// -/// async fn main() { -/// let mut dom = VirtualDom::new(App); +/// let dom = VirtualDom::new(app); /// -/// let mut inital_edits = dom.rebuild(); -/// apply_edits(inital_edits); +/// real_dom.apply(dom.rebuild()); /// -/// loop { -/// dom.wait_for_work().await; -/// let frame_timeout = TimeoutFuture::new(Duration::from_millis(16)); -/// let deadline = || (&mut frame_timeout).now_or_never(); -/// let edits = dom.run_with_deadline(deadline).await; -/// apply_edits(edits); +/// loop { +/// select! { +/// _ = dom.wait_for_work() => {} +/// evt = real_dom.wait_for_event() => dom.handle_event(evt), /// } +/// +/// real_dom.apply(dom.render_immediate()); +/// } +/// ``` +/// +/// ## Waiting for suspense +/// +/// Because Dioxus supports suspense, you can use it for server-side rendering, static site generation, and other usecases +/// where waiting on portions of the UI to finish rendering is important. To wait for suspense, use the +/// [`VirtualDom::render_with_deadline`] method: +/// +/// ```rust, ignore +/// let dom = VirtualDom::new(app); +/// +/// let deadline = tokio::time::sleep(Duration::from_millis(100)); +/// let edits = dom.render_with_deadline(deadline).await; +/// ``` +/// +/// ## Use with streaming +/// +/// If not all rendering is done by the deadline, it might be worthwhile to stream the rest later. To do this, we +/// suggest rendering with a deadline, and then looping between [`VirtualDom::wait_for_work`] and render_immediate until +/// no suspended work is left. +/// +/// ```rust, ignore +/// let dom = VirtualDom::new(app); +/// +/// let deadline = tokio::time::sleep(Duration::from_millis(20)); +/// let edits = dom.render_with_deadline(deadline).await; +/// +/// real_dom.apply(edits); +/// +/// while dom.has_suspended_work() { +/// dom.wait_for_work().await; +/// real_dom.apply(dom.render_immediate()); /// } /// ``` pub struct VirtualDom { - root: ElementId, - scopes: ScopeArena, + pub(crate) templates: HashMap>, + pub(crate) scopes: Slab>, + pub(crate) dirty_scopes: BTreeSet, + pub(crate) scheduler: Rc, - pending_messages: VecDeque, - dirty_scopes: Vec, - removed_scopes: FxHashSet, + // Every element is actually a dual reference - one to the template and the other to the dynamic node in that template + pub(crate) elements: Slab, - channel: ( - UnboundedSender, - UnboundedReceiver, - ), + // While diffing we need some sort of way of breaking off a stream of suspended mutations. + pub(crate) scope_stack: Vec, + pub(crate) collected_leaves: Vec, + + // Whenever a suspense tree is finished, we push its boundary onto this stack. + // When "render_with_deadline" is called, we pop the stack and return the mutations + pub(crate) finished_fibers: Vec, + + pub(crate) rx: futures_channel::mpsc::UnboundedReceiver, + + pub(crate) mutations: Mutations<'static>, } -/// 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 enum SchedulerMsg { - /// Events from the Renderer - Event(UserEvent), - - /// Immediate updates from Components that mark them as dirty - Immediate(ScopeId), - - /// Mark all components as dirty and update them - DirtyAll, - - #[cfg(any(feature = "hot-reload", debug_assertions))] - /// Mark a template as dirty, used for hot reloading - SetTemplate(Box), - - /// New tasks from components that should be polled when the next poll is ready - NewTask(ScopeId), -} - -// Methods to create the VirtualDom impl VirtualDom { /// Create a new VirtualDom with a component that does not have special props. /// @@ -160,8 +190,8 @@ impl VirtualDom { /// ``` /// /// Note: the VirtualDom is not progressed, you must either "run_with_deadline" or use "rebuild" to progress it. - pub fn new(root: Component) -> Self { - Self::new_with_props(root, ()) + pub fn new(app: fn(Scope) -> Element) -> Self { + Self::new_with_props(app, ()) } /// Create a new VirtualDom with the given properties for the root component. @@ -194,140 +224,191 @@ impl VirtualDom { /// let mut dom = VirtualDom::new_with_props(Example, SomeProps { name: "jane" }); /// let mutations = dom.rebuild(); /// ``` - pub fn new_with_props

(root: Component

, root_props: P) -> Self - where - P: 'static, - { - Self::new_with_props_and_scheduler( - root, - root_props, - futures_channel::mpsc::unbounded::(), - ) - } + pub fn new_with_props(root: fn(Scope

) -> Element, root_props: P) -> Self { + let (tx, rx) = futures_channel::mpsc::unbounded(); + let mut dom = Self { + rx, + scheduler: Scheduler::new(tx), + templates: Default::default(), + scopes: Slab::default(), + elements: Default::default(), + scope_stack: Vec::new(), + dirty_scopes: BTreeSet::new(), + collected_leaves: Vec::new(), + finished_fibers: Vec::new(), + mutations: Mutations::default(), + }; - /// Launch the VirtualDom, but provide your own channel for receiving and sending messages into the scheduler - /// - /// This is useful when the VirtualDom must be driven from outside a thread and it doesn't make sense to wait for the - /// VirtualDom to be created just to retrieve its channel receiver. - /// - /// ```rust, ignore - /// let channel = futures_channel::mpsc::unbounded(); - /// let dom = VirtualDom::new_with_scheduler(Example, (), channel); - /// ``` - pub fn new_with_props_and_scheduler( - root: Component

, - root_props: P, - channel: ( - UnboundedSender, - UnboundedReceiver, - ), - ) -> Self { - let scopes = ScopeArena::new(channel.0.clone()); - - scopes.new_with_key( - root as ComponentPtr, - Box::new(VComponentProps { - props: root_props, - memo: |_a, _b| unreachable!("memo on root will neve be run"), - render_fn: root, - }), - None, - ElementId(0), + let root = dom.new_scope( + Box::new(VProps::new(root, |_, _| unreachable!(), root_props)), + "app", ); - Self { - root: ElementId(0), - scopes, - channel, - dirty_scopes: Vec::from_iter([ScopeId(0)]), - pending_messages: VecDeque::new(), - removed_scopes: FxHashSet::default(), - } + // The root component is always a suspense boundary for any async children + // This could be unexpected, so we might rethink this behavior later + // + // We *could* just panic if the suspense boundary is not found + root.provide_context(Rc::new(SuspenseContext::new(ScopeId(0)))); + + // Unlike react, we provide a default error boundary that just renders the error as a string + root.provide_context(Rc::new(ErrorBoundary::new(ScopeId(0)))); + + // the root element is always given element ID 0 since it's the container for the entire tree + dom.elements.insert(ElementRef::null()); + + dom } - /// Get the [`Scope`] for the root component. - /// - /// This is useful for traversing the tree from the root for heuristics or alternative renderers that use Dioxus - /// directly. - /// - /// This method is equivalent to calling `get_scope(ScopeId(0))` - /// - /// # Example - /// - /// ```rust, ignore - /// let mut dom = VirtualDom::new(example); - /// dom.rebuild(); - /// - /// - /// ``` - pub fn base_scope(&self) -> &ScopeState { - self.get_scope(ScopeId(0)).unwrap() - } - - /// Get the [`ScopeState`] for a component given its [`ScopeId`] - /// - /// # Example - /// - /// + /// Get the state for any scope given its ID /// + /// This is useful for inserting or removing contexts from a scope, or rendering out its root node pub fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> { - self.scopes.get_scope(id) + self.scopes.get(id.0).map(|f| f.as_ref()) } - /// Get an [`UnboundedSender`] handle to the channel used by the scheduler. + /// Get the single scope at the top of the VirtualDom tree that will always be around /// - /// # Example - /// - /// ```rust, ignore - /// let dom = VirtualDom::new(App); - /// let sender = dom.get_scheduler_channel(); - /// ``` - pub fn get_scheduler_channel(&self) -> UnboundedSender { - self.channel.0.clone() + /// This scope has a ScopeId of 0 and is the root of the tree + pub fn base_scope(&self) -> &ScopeState { + self.scopes.get(0).unwrap() } - /// Try to get an element from its ElementId - pub fn get_element(&self, id: ElementId) -> Option<&VNode> { - self.scopes.get_element(id) + /// Build the virtualdom with a global context inserted into the base scope + /// + /// This is useful for what is essentially dependency injection when building the app + pub fn with_root_context(self, context: T) -> Self { + self.base_scope().provide_context(context); + self } - /// Add a new message to the scheduler queue directly. + /// Manually mark a scope as requiring a re-render /// + /// Whenever the VirtualDom "works", it will re-render this scope + pub fn mark_dirty(&mut self, id: ScopeId) { + let height = self.scopes[id.0].height; + self.dirty_scopes.insert(DirtyScope { height, id }); + } + + /// Determine whether or not a scope is currently in a suspended state /// - /// This method makes it possible to send messages to the scheduler from outside the VirtualDom without having to - /// call `get_schedule_channel` and then `send`. + /// This does not mean the scope is waiting on its own futures, just that the tree that the scope exists in is + /// currently suspended. + pub fn is_scope_suspended(&self, id: ScopeId) -> bool { + !self.scopes[id.0] + .consume_context::>() + .unwrap() + .waiting_on + .borrow() + .is_empty() + } + + /// Determine if the tree is at all suspended. Used by SSR and other outside mechanisms to determine if the tree is + /// ready to be rendered. + pub fn has_suspended_work(&self) -> bool { + !self.scheduler.leaves.borrow().is_empty() + } + + /// Call a listener inside the VirtualDom with data from outside the VirtualDom. /// - /// # Example - /// ```rust, ignore - /// let dom = VirtualDom::new(App); - /// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); - /// ``` - pub fn handle_message(&mut self, msg: SchedulerMsg) { - if self.channel.0.unbounded_send(msg).is_ok() { - self.process_all_messages(); + /// This method will identify the appropriate element. The data must match up with the listener delcared. Note that + /// this method does not give any indication as to the success of the listener call. If the listener is not found, + /// nothing will happen. + /// + /// It is up to the listeners themselves to mark nodes as dirty. + /// + /// If you have multiple events, you can call this method multiple times before calling "render_with_deadline" + pub fn handle_event( + &mut self, + name: &str, + data: Rc, + element: ElementId, + bubbles: bool, + ) { + /* + ------------------------ + The algorithm works by walking through the list of dynamic attributes, checking their paths, and breaking when + we find the target path. + + With the target path, we try and move up to the parent until there is no parent. + Due to how bubbling works, we call the listeners before walking to the parent. + + If we wanted to do capturing, then we would accumulate all the listeners and call them in reverse order. + ---------------------- + + For a visual demonstration, here we present a tree on the left and whether or not a listener is collected on the + right. + + | <-- yes (is ascendant) + | | | <-- no (is not direct ascendant) + | | <-- yes (is ascendant) + | | | | | <--- target element, break early, don't check other listeners + | | | <-- no, broke early + | <-- no, broke early + */ + let mut parent_path = self.elements.get(element.0); + let mut listeners = vec![]; + + // We will clone this later. The data itself is wrapped in RC to be used in callbacks if required + let uievent = Event { + propogates: Rc::new(Cell::new(bubbles)), + data, + }; + + // Loop through each dynamic attribute in this template before moving up to the template's parent. + while let Some(el_ref) = parent_path { + // safety: we maintain references of all vnodes in the element slab + let template = unsafe { &*el_ref.template }; + let target_path = el_ref.path; + + for (idx, attr) in template.dynamic_attrs.iter().enumerate() { + let this_path = template.template.attr_paths[idx]; + + // listeners are required to be prefixed with "on", but they come back to the virtualdom with that missing + // we should fix this so that we look for "onclick" instead of "click" + if &attr.name[2..] == name && target_path.is_ascendant(&this_path) { + listeners.push(&attr.value); + + // Break if the event doesn't bubble anyways + if !bubbles { + break; + } + + // Break if this is the exact target element. + // This means we won't call two listeners with the same name on the same element. This should be + // documented, or be rejected from the rsx! macro outright + if target_path == this_path { + break; + } + } + } + + // Now that we've accumulated all the parent attributes for the target element, call them in reverse order + // We check the bubble state between each call to see if the event has been stopped from bubbling + for listener in listeners.drain(..).rev() { + if let AttributeValue::Listener(listener) = listener { + if let Some(cb) = listener.borrow_mut().as_deref_mut() { + cb(uievent.clone()); + } + + if !uievent.propogates.get() { + return; + } + } + } + + parent_path = template.parent.and_then(|id| self.elements.get(id.0)); } } - /// Check if the [`VirtualDom`] has any pending updates or work to be done. - /// - /// # Example - /// - /// ```rust, ignore - /// let dom = VirtualDom::new(App); - /// - /// // the dom is "dirty" when it is started and must be rebuilt to get the first render - /// assert!(dom.has_any_work()); - /// ``` - pub fn has_work(&self) -> bool { - !(self.dirty_scopes.is_empty() && self.pending_messages.is_empty()) - } - /// Wait for the scheduler to have any work. /// - /// This method polls the internal future queue *and* the scheduler channel. - /// To add work to the VirtualDom, insert a message via the scheduler channel. + /// This method polls the internal future queue, waiting for suspense nodes, tasks, or other work. This completes when + /// any work is ready. If multiple scopes are marked dirty from a task or a suspense tree is finished, this method + /// will exit. /// - /// This lets us poll async tasks during idle periods without blocking the main thread. + /// This method is cancel-safe, so you're fine to discard the future in a select block. + /// + /// This lets us poll async tasks and suspended trees during idle periods without blocking the main thread. /// /// # Example /// @@ -336,484 +417,211 @@ impl VirtualDom { /// let sender = dom.get_scheduler_channel(); /// ``` pub async fn wait_for_work(&mut self) { + let mut some_msg = None; + loop { - if !self.dirty_scopes.is_empty() && self.pending_messages.is_empty() { - break; - } + match some_msg.take() { + // If a bunch of messages are ready in a sequence, try to pop them off synchronously + Some(msg) => match msg { + SchedulerMsg::Immediate(id) => self.mark_dirty(id), + SchedulerMsg::TaskNotified(task) => self.handle_task_wakeup(task), + SchedulerMsg::SuspenseNotified(id) => self.handle_suspense_wakeup(id), + }, - if self.pending_messages.is_empty() { - if self.scopes.tasks.has_tasks() { - use futures_util::future::{select, Either}; + // If they're not ready, then we should wait for them to be ready + None => { + match self.rx.try_next() { + Ok(Some(val)) => some_msg = Some(val), + Ok(None) => return, + Err(_) => { + // If we have any dirty scopes, or finished fiber trees then we should exit + if !self.dirty_scopes.is_empty() || !self.finished_fibers.is_empty() { + return; + } - let scopes = &mut self.scopes; - let task_poll = poll_fn(|cx| { - let mut tasks = scopes.tasks.tasks.borrow_mut(); - tasks.retain(|_, task| task.as_mut().poll(cx).is_pending()); - - match tasks.is_empty() { - true => Poll::Ready(()), - false => Poll::Pending, + some_msg = self.rx.next().await } - }); - - match select(task_poll, self.channel.1.next()).await { - Either::Left((_, _)) => {} - Either::Right((msg, _)) => self.pending_messages.push_front(msg.unwrap()), } - } else { - self.pending_messages - .push_front(self.channel.1.next().await.unwrap()); } } - - // Move all the messages into the queue - self.process_all_messages(); - } - } - - /// Manually kick the VirtualDom to process any - pub fn process_all_messages(&mut self) { - // clear out the scheduler queue - while let Ok(Some(msg)) = self.channel.1.try_next() { - self.pending_messages.push_front(msg); - } - - // process all the messages pulled from the queue - while let Some(msg) = self.pending_messages.pop_back() { - self.process_message(msg); - } - } - - /// Handle an individual message for the scheduler. - /// - /// This will either call an event listener or mark a component as dirty. - pub fn process_message(&mut self, msg: SchedulerMsg) { - match msg { - SchedulerMsg::NewTask(_id) => { - // uh, not sure? I think end up re-polling it anyways - } - SchedulerMsg::Event(event) => { - if let Some(element) = event.element { - self.scopes.call_listener_with_bubbling(&event, element); - } - } - SchedulerMsg::Immediate(s) => { - self.mark_dirty_scope(s); - } - SchedulerMsg::DirtyAll => { - let dirty = self - .scopes - .scopes - .borrow() - .keys() - .copied() - .collect::>(); - for id in dirty { - self.mark_dirty_scope(id); - } - } - #[cfg(any(feature = "hot-reload", debug_assertions))] - SchedulerMsg::SetTemplate(msg) => { - let SetTemplateMsg(id, tmpl) = *msg; - if self - .scopes - .templates - .borrow_mut() - .insert( - id.clone(), - std::rc::Rc::new(std::cell::RefCell::new(Template::Owned(tmpl))), - ) - .is_some() - { - self.scopes.template_resolver.borrow_mut().mark_dirty(&id) - } - - // mark any scopes that used the template as dirty - self.process_message(SchedulerMsg::DirtyAll); - } } } - /// Run the virtualdom with a deadline. - /// - /// This method will perform any outstanding diffing work and try to return as many mutations as possible before the - /// deadline is reached. This method accepts a closure that returns `true` if the deadline has been reached. To wrap - /// your future into a deadline, consider the `now_or_never` method from `future_utils`. - /// - /// ```rust, ignore - /// let mut vdom = VirtualDom::new(App); - /// - /// let timeout = TimeoutFuture::from_ms(16); - /// let deadline = || (&mut timeout).now_or_never(); - /// - /// let mutations = vdom.work_with_deadline(deadline); - /// ``` - /// - /// This method is useful when needing to schedule the virtualdom around other tasks on the main thread to prevent - /// "jank". It will try to finish whatever work it has by the deadline to free up time for other work. - /// - /// If the work is not finished by the deadline, Dioxus will store it for later and return when work_with_deadline - /// is called again. This means you can ensure some level of free time on the VirtualDom's thread during the work phase. - /// - /// For use in the web, it is expected that this method will be called to be executed during "idle times" and the - /// mutations to be applied during the "paint times" IE "animation frames". With this strategy, it is possible to craft - /// entirely jank-free applications that perform a ton of work. - /// - /// In general use, Dioxus is plenty fast enough to not need to worry about this. - /// - /// # Example - /// - /// ```rust, ignore - /// fn App(cx: Scope) -> Element { - /// cx.render(rsx!( div {"hello"} )) - /// } - /// - /// let mut dom = VirtualDom::new(App); - /// - /// loop { - /// let mut timeout = TimeoutFuture::from_ms(16); - /// let deadline = move || (&mut timeout).now_or_never(); - /// - /// let mutations = dom.run_with_deadline(deadline).await; - /// - /// apply_mutations(mutations); - /// } - /// ``` - #[allow(unused)] - pub fn work_with_deadline(&mut self, mut deadline: impl FnMut() -> bool) -> Vec { - let mut committed_mutations = vec![]; - self.scopes.template_bump.reset(); - self.removed_scopes.clear(); - - while !self.dirty_scopes.is_empty() { - let scopes = &self.scopes; - let mut diff_state = DiffState::new(scopes); - - let mut ran_scopes = FxHashSet::default(); - - // Sort the scopes by height. Theoretically, we'll de-duplicate scopes by height - self.dirty_scopes - .retain(|id| scopes.get_scope(*id).is_some()); - - self.dirty_scopes.sort_by(|a, b| { - let h1 = scopes.get_scope(*a).unwrap().height; - let h2 = scopes.get_scope(*b).unwrap().height; - h1.cmp(&h2).reverse() - }); - - if let Some(scopeid) = self.dirty_scopes.pop() { - if scopes.get_scope(scopeid).is_some() - && !self.removed_scopes.contains(&scopeid) - && !ran_scopes.contains(&scopeid) - { - ran_scopes.insert(scopeid); - - self.scopes.run_scope(scopeid); - - diff_state.diff_scope(self.root, scopeid); - - let DiffState { mutations, .. } = diff_state; - - self.removed_scopes - .extend(mutations.dirty_scopes.iter().copied()); - - if !mutations.edits.is_empty() { - committed_mutations.push(mutations); - } - - // todo: pause the diff machine - // if diff_state.work(&mut deadline) { - // let DiffState { mutations, .. } = diff_state; - // for scope in &mutations.dirty_scopes { - // self.dirty_scopes.remove(scope); - // } - // committed_mutations.push(mutations); - // } else { - // // leave the work in an incomplete state - // // - // // todo: we should store the edits and re-apply them later - // // for now, we just dump the work completely (threadsafe) - // return committed_mutations; - // } - } + /// Process all events in the queue until there are no more left + pub fn process_events(&mut self) { + while let Ok(Some(msg)) = self.rx.try_next() { + match msg { + SchedulerMsg::Immediate(id) => self.mark_dirty(id), + SchedulerMsg::TaskNotified(task) => self.handle_task_wakeup(task), + SchedulerMsg::SuspenseNotified(id) => self.handle_suspense_wakeup(id), } } - - committed_mutations } /// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom from scratch. /// - /// The diff machine expects the RealDom's stack to be the root of the application. + /// The mutations item expects the RealDom's stack to be the root of the application. /// /// Tasks will not be polled with this method, nor will any events be processed from the event queue. Instead, the /// root component will be ran once and then diffed. All updates will flow out as mutations. /// /// All state stored in components will be completely wiped away. /// + /// Any templates previously registered will remain. + /// /// # Example /// ```rust, ignore /// static App: Component = |cx| cx.render(rsx!{ "hello world" }); + /// /// let mut dom = VirtualDom::new(); /// let edits = dom.rebuild(); /// /// apply_edits(edits); /// ``` pub fn rebuild(&mut self) -> Mutations { - let scope_id = ScopeId(0); + match unsafe { self.run_scope(ScopeId(0)).extend_lifetime_ref() } { + // Rebuilding implies we append the created elements to the root + RenderReturn::Sync(Ok(node)) => { + let m = self.create_scope(ScopeId(0), node); + self.mutations.edits.push(Mutation::AppendChildren { + id: ElementId(0), + m, + }); + } + // If an error occurs, we should try to render the default error component and context where the error occured + RenderReturn::Sync(Err(e)) => panic!("Cannot catch errors during rebuild {:?}", e), + RenderReturn::Async(_) => unreachable!("Root scope cannot be an async component"), + } - let mut diff_state = DiffState::new(&self.scopes); - self.scopes.run_scope(scope_id); - - diff_state.scope_stack.push(scope_id); - - let node = self.scopes.fin_head(scope_id); - let mut created = Vec::new(); - diff_state.create_node(self.root, node, &mut created); - - diff_state - .mutations - .append_children(Some(self.root.as_u64()), created); - - self.dirty_scopes.clear(); - assert!(self.dirty_scopes.is_empty()); - - diff_state.mutations + self.finalize() } - /// Compute a manual diff of the VirtualDom between states. - /// - /// This can be useful when state inside the DOM is remotely changed from the outside, but not propagated as an event. - /// - /// In this case, every component will be diffed, even if their props are memoized. This method is intended to be used - /// to force an update of the DOM when the state of the app is changed outside of the app. - /// - /// To force a reflow of the entire VirtualDom, use `ScopeId(0)` as the scope_id. - /// - /// # Example - /// ```rust, ignore - /// #[derive(PartialEq, Props)] - /// struct AppProps { - /// value: Shared<&'static str>, - /// } - /// - /// static App: Component = |cx| { - /// let val = cx.value.borrow(); - /// cx.render(rsx! { div { "{val}" } }) - /// }; - /// - /// let value = Rc::new(RefCell::new("Hello")); - /// let mut dom = VirtualDom::new_with_props(App, AppProps { value: value.clone(), }); - /// - /// let _ = dom.rebuild(); - /// - /// *value.borrow_mut() = "goodbye"; - /// - /// let edits = dom.hard_diff(ScopeId(0)); - /// ``` - pub fn hard_diff(&mut self, scope_id: ScopeId) -> Mutations { - let mut diff_machine = DiffState::new(&self.scopes); - self.scopes.run_scope(scope_id); + /// Render whatever the VirtualDom has ready as fast as possible without requiring an executor to progress + /// suspended subtrees. + pub fn render_immediate(&mut self) -> Mutations { + // Build a waker that won't wake up since our deadline is already expired when it's polled + let waker = futures_util::task::noop_waker(); + let mut cx = std::task::Context::from_waker(&waker); - let (old, new) = ( - diff_machine.scopes.wip_head(scope_id), - diff_machine.scopes.fin_head(scope_id), - ); + // Now run render with deadline but dont even try to poll any async tasks + let fut = self.render_with_deadline(std::future::ready(())); + pin_mut!(fut); - diff_machine.force_diff = true; - diff_machine.scope_stack.push(scope_id); - let scope = diff_machine.scopes.get_scope(scope_id).unwrap(); - - diff_machine.diff_node(scope.container, old, new); - - diff_machine.mutations + // The root component is not allowed to be async + match fut.poll(&mut cx) { + std::task::Poll::Ready(mutations) => mutations, + std::task::Poll::Pending => panic!("render_immediate should never return pending"), + } } - /// Renders an `rsx` call into the Base Scope's allocator. + /// Render what you can given the timeline and then move on /// - /// Useful when needing to render nodes from outside the VirtualDom, such as in a test. + /// It's generally a good idea to put some sort of limit on the suspense process in case a future is having issues. /// - /// ```rust, ignore - /// fn Base(cx: Scope) -> Element { - /// render!(div {}) - /// } - /// - /// let dom = VirtualDom::new(Base); - /// let nodes = dom.render_nodes(rsx!("div")); - /// ``` - pub fn render_vnodes<'a>(&'a self, lazy_nodes: LazyNodes<'a, '_>) -> &'a VNode<'a> { - let scope = self.scopes.get_scope(ScopeId(0)).unwrap(); - let frame = scope.wip_frame(); - let factory = NodeFactory::new(scope); - let node = lazy_nodes.call(factory); - frame.bump.alloc(node) - } + /// If no suspense trees are present + pub async fn render_with_deadline(&mut self, deadline: impl Future) -> Mutations { + pin_mut!(deadline); - /// Renders an `rsx` call into the Base Scope's allocator. - /// - /// Useful when needing to render nodes from outside the VirtualDom, such as in a test. - /// - /// ```rust, ignore - /// fn Base(cx: Scope) -> Element { - /// render!(div {}) - /// } - /// - /// let dom = VirtualDom::new(Base); - /// let nodes = dom.render_nodes(rsx!("div")); - /// ``` - pub fn diff_vnodes<'a>(&'a self, old: &'a VNode<'a>, new: &'a VNode<'a>) -> Mutations<'a> { - let mut machine = DiffState::new(&self.scopes); - machine.scope_stack.push(ScopeId(0)); - machine.diff_node(self.root, old, new); + loop { + // first, unload any complete suspense trees + for finished_fiber in self.finished_fibers.drain(..) { + let scope = &mut self.scopes[finished_fiber.0]; + let context = scope.has_context::>().unwrap(); - machine.mutations - } + self.mutations + .templates + .append(&mut context.mutations.borrow_mut().templates); - /// Renders an `rsx` call into the Base Scope's allocator. - /// - /// Useful when needing to render nodes from outside the VirtualDom, such as in a test. - /// - /// - /// ```rust, ignore - /// fn Base(cx: Scope) -> Element { - /// render!(div {}) - /// } - /// - /// let dom = VirtualDom::new(Base); - /// let nodes = dom.render_nodes(rsx!("div")); - /// ``` - pub fn create_vnodes<'a>(&'a self, nodes: LazyNodes<'a, '_>) -> Mutations<'a> { - let mut machine = DiffState::new(&self.scopes); - machine.scope_stack.push(ScopeId(0)); - let node = self.render_vnodes(nodes); - let mut created = Vec::new(); - machine.create_node(self.root, node, &mut created); - machine - .mutations - .append_children(Some(self.root.as_u64()), created); - machine.mutations - } + self.mutations + .edits + .append(&mut context.mutations.borrow_mut().edits); - /// Renders an `rsx` call into the Base Scopes's arena. - /// - /// Useful when needing to diff two rsx! calls from outside the VirtualDom, such as in a test. - /// - /// - /// ```rust, ignore - /// fn Base(cx: Scope) -> Element { - /// render!(div {}) - /// } - /// - /// let dom = VirtualDom::new(Base); - /// let nodes = dom.render_nodes(rsx!("div")); - /// ``` - pub fn diff_lazynodes<'a>( - &'a self, - left: LazyNodes<'a, '_>, - right: LazyNodes<'a, '_>, - ) -> (Mutations<'a>, Mutations<'a>) { - let (old, new) = (self.render_vnodes(left), self.render_vnodes(right)); + // TODO: count how many nodes are on the stack? + self.mutations.push(Mutation::ReplaceWith { + id: context.placeholder.get().unwrap(), + m: 1, + }) + } - let mut create = DiffState::new(&self.scopes); - create.scope_stack.push(ScopeId(0)); - let mut created = Vec::new(); - create.create_node(self.root, old, &mut created); - create - .mutations - .append_children(Some(self.root.as_u64()), created); + // Next, diff any dirty scopes + // We choose not to poll the deadline since we complete pretty quickly anyways + if let Some(dirty) = self.dirty_scopes.iter().next().cloned() { + self.dirty_scopes.remove(&dirty); - let mut edit = DiffState::new(&self.scopes); - edit.scope_stack.push(ScopeId(0)); - edit.diff_node(self.root, old, new); + // if the scope is currently suspended, then we should skip it, ignoring any tasks calling for an update + if self.is_scope_suspended(dirty.id) { + continue; + } - (create.mutations, edit.mutations) - } + // Save the current mutations length so we can split them into boundary + let mutations_to_this_point = self.mutations.edits.len(); - /// Runs a function with the template associated with a given id. - pub fn with_template(&self, id: &TemplateId, mut f: impl FnMut(&Template) -> R) -> R { - self.scopes - .templates - .borrow() - .get(id) - .map(|inner| { - let borrow = inner; - f(&borrow.borrow()) - }) - .unwrap() - } + // Run the scope and get the mutations + self.run_scope(dirty.id); + self.diff_scope(dirty.id); - fn mark_dirty_scope(&mut self, scope_id: ScopeId) { - let scopes = &self.scopes; - if let Some(scope) = scopes.get_scope(scope_id) { - let height = scope.height; - let id = scope_id.0; - if let Err(index) = self.dirty_scopes.binary_search_by(|new| { - let scope = scopes.get_scope(*new).unwrap(); - let new_height = scope.height; - let new_id = &scope.scope_id(); - height.cmp(&new_height).then(new_id.0.cmp(&id)) - }) { - self.dirty_scopes.insert(index, scope_id); - log::info!("mark_dirty_scope: {:?}", self.dirty_scopes); + // If suspended leaves are present, then we should find the boundary for this scope and attach things + // No placeholder necessary since this is a diff + if !self.collected_leaves.is_empty() { + let mut boundary = self.scopes[dirty.id.0] + .consume_context::>() + .unwrap(); + + let boundary_mut = boundary.borrow_mut(); + + // Attach mutations + boundary_mut + .mutations + .borrow_mut() + .edits + .extend(self.mutations.edits.split_off(mutations_to_this_point)); + + // Attach suspended leaves + boundary + .waiting_on + .borrow_mut() + .extend(self.collected_leaves.drain(..)); + } + } + + // If there's more work, then just continue, plenty of work to do + if !self.dirty_scopes.is_empty() { + continue; + } + + // If there's no pending suspense, then we have no reason to wait for anything + if self.scheduler.leaves.borrow().is_empty() { + return self.finalize(); + } + + // Poll the suspense leaves in the meantime + let mut work = self.wait_for_work(); + + // safety: this is okay since we don't touch the original future + let pinned = unsafe { std::pin::Pin::new_unchecked(&mut work) }; + + // If the deadline is exceded (left) then we should return the mutations we have + use futures_util::future::{select, Either}; + if let Either::Left((_, _)) = select(&mut deadline, pinned).await { + // release the borrowed + drop(work); + return self.finalize(); } } } -} -/* -Scopes and ScopeArenas are never dropped internally. -An app will always occupy as much memory as its biggest form. + /// Swap the current mutations with a new + fn finalize(&mut self) -> Mutations { + // todo: make this a routine + let mut out = Mutations::default(); + std::mem::swap(&mut self.mutations, &mut out); + out + } +} -This means we need to handle all specifics of drop *here*. It's easier -to reason about centralizing all the drop -logic in one spot rather than scattered in each module. - -Broadly speaking, we want to use the remove_nodes method to clean up *everything* -This will drop listeners, borrowed props, and hooks for all components. -We need to do this in the correct order - nodes at the very bottom must be dropped first to release -the borrow chain. - -Once the contents of the tree have been cleaned up, we can finally clean up the -memory used by ScopeState itself. - -questions: -should we build a vcomponent for the root? -- probably - yes? -- store the vcomponent in the root dom - -- 1: Use remove_nodes to use the ensure_drop_safety pathway to safely drop the tree -- 2: Drop the ScopeState itself -*/ impl Drop for VirtualDom { fn drop(&mut self) { - // the best way to drop the dom is to replace the root scope with a dud - // the diff infrastructure will then finish the rest - let scope = self.scopes.get_scope(ScopeId(0)).unwrap(); - - // todo: move the remove nodes method onto scopearena - // this will clear *all* scopes *except* the root scope - let mut machine = DiffState::new(&self.scopes); - machine.remove_nodes([scope.root_node()], false); - - // Now, clean up the root scope - // safety: there are no more references to the root scope - let scope = unsafe { &mut *self.scopes.get_scope_raw(ScopeId(0)).unwrap() }; - scope.reset(); - - // make sure there are no "live" components - for (_, scopeptr) in self.scopes.scopes.get_mut().drain() { - // safety: all scopes were made in the bump's allocator - // They are never dropped until now. The only way to drop is through Box. - let scope = unsafe { bumpalo::boxed::Box::from_raw(scopeptr) }; - drop(scope); - } - - for scopeptr in self.scopes.free_scopes.get_mut().drain(..) { - // safety: all scopes were made in the bump's allocator - // They are never dropped until now. The only way to drop is through Box. - let mut scope = unsafe { bumpalo::boxed::Box::from_raw(scopeptr) }; - scope.reset(); - drop(scope); - } + // Simply drop this scope which drops all of its children + self.drop_scope(ScopeId(0)); } } diff --git a/packages/core/tests/.rustfmt.toml b/packages/core/tests/.rustfmt.toml new file mode 100644 index 000000000..01c43e05f --- /dev/null +++ b/packages/core/tests/.rustfmt.toml @@ -0,0 +1,2 @@ +struct_variant_width = 100 +struct_lit_width = 80 diff --git a/packages/dioxus/tests/README.md b/packages/core/tests/README.md similarity index 90% rename from packages/dioxus/tests/README.md rename to packages/core/tests/README.md index 63293c741..eea7cec3a 100644 --- a/packages/dioxus/tests/README.md +++ b/packages/core/tests/README.md @@ -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 diff --git a/packages/core/tests/attr_cleanup.rs b/packages/core/tests/attr_cleanup.rs new file mode 100644 index 000000000..c0aa9a1da --- /dev/null +++ b/packages/core/tests/attr_cleanup.rs @@ -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 } + ] + ); +} diff --git a/packages/core/tests/boolattrs.rs b/packages/core/tests/boolattrs.rs new file mode 100644 index 000000000..5cd2ad1ad --- /dev/null +++ b/packages/core/tests/boolattrs.rs @@ -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) }, + ] + ) +} diff --git a/packages/core/tests/borrowedstate.rs b/packages/core/tests/borrowedstate.rs new file mode 100644 index 000000000..0aace9c97 --- /dev/null +++ b/packages/core/tests/borrowedstate.rs @@ -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}!" })) +} diff --git a/packages/core/tests/bubble_error.rs b/packages/core/tests/bubble_error.rs new file mode 100644 index 000000000..3dbfe80a9 --- /dev/null +++ b/packages/core/tests/bubble_error.rs @@ -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::()?; + + 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(); +} diff --git a/packages/core/tests/context_api.rs b/packages/core/tests/context_api.rs new file mode 100644 index 000000000..696810cb0 --- /dev/null +++ b/packages/core/tests/context_api.rs @@ -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::().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::().unwrap(), 1); + + dom.mark_dirty(ScopeId(0)); + _ = dom.render_immediate(); + assert_eq!(dom.base_scope().consume_context::().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,) },] + ); +} diff --git a/packages/core/tests/create_dom.rs b/packages/core/tests/create_dom.rs new file mode 100644 index 000000000..5e5005d3f --- /dev/null +++ b/packages/core/tests/create_dom.rs @@ -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) } + ] + ) +} diff --git a/packages/core/tests/create_element.rs b/packages/core/tests/create_element.rs new file mode 100644 index 000000000..a4497ba74 --- /dev/null +++ b/packages/core/tests/create_element.rs @@ -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 } + // ] + // ) +} diff --git a/packages/core/tests/create_fragments.rs b/packages/core/tests/create_fragments.rs new file mode 100644 index 000000000..ee312f34b --- /dev/null +++ b/packages/core/tests/create_fragments.rs @@ -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 } + ); +} diff --git a/packages/core/tests/create_lists.rs b/packages/core/tests/create_lists.rs new file mode 100644 index 000000000..ec3e8ecf2 --- /dev/null +++ b/packages/core/tests/create_lists.rs @@ -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) } + ], + ) +} diff --git a/packages/core/tests/create_passthru.rs b/packages/core/tests/create_passthru.rs new file mode 100644 index 000000000..b329fe51a --- /dev/null +++ b/packages/core/tests/create_passthru.rs @@ -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 } + ] + ) +} diff --git a/packages/core/tests/cycle.rs b/packages/core/tests/cycle.rs new file mode 100644 index 000000000..133b04658 --- /dev/null +++ b/packages/core/tests/cycle.rs @@ -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 }, + ] + ); +} diff --git a/packages/core/tests/diff_component.rs b/packages/core/tests/diff_component.rs new file mode 100644 index 000000000..2b74f8302 --- /dev/null +++ b/packages/core/tests/diff_component.rs @@ -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 } + ] + ); +} diff --git a/packages/core/tests/diff_element.rs b/packages/core/tests/diff_element.rs new file mode 100644 index 000000000..3ab9b284a --- /dev/null +++ b/packages/core/tests/diff_element.rs @@ -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 }, + ] + ); +} diff --git a/packages/core/tests/diff_keyed_list.rs b/packages/core/tests/diff_keyed_list.rs new file mode 100644 index 000000000..81013849d --- /dev/null +++ b/packages/core/tests/diff_keyed_list.rs @@ -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 } + ] + ); +} diff --git a/packages/core/tests/diff_unkeyed_list.rs b/packages/core/tests/diff_unkeyed_list.rs new file mode 100644 index 000000000..415aa79c0 --- /dev/null +++ b/packages/core/tests/diff_unkeyed_list.rs @@ -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 }, + ] + ) +} diff --git a/packages/core/tests/hotreloading.rs b/packages/core/tests/hotreloading.rs new file mode 100644 index 000000000..8e9f3400b --- /dev/null +++ b/packages/core/tests/hotreloading.rs @@ -0,0 +1 @@ +//! It should be possible to swap out templates at runtime, enabling hotreloading diff --git a/packages/core/tests/kitchen_sink.rs b/packages/core/tests/kitchen_sink.rs new file mode 100644 index 000000000..78ef608cc --- /dev/null +++ b/packages/core/tests/kitchen_sink.rs @@ -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 } + ], + ); +} diff --git a/packages/core/tests/lifecycle.rs b/packages/core/tests/lifecycle.rs new file mode 100644 index 000000000..04f6bc3cf --- /dev/null +++ b/packages/core/tests/lifecycle.rs @@ -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 = Arc>; + +#[test] +fn manual_diffing() { + struct AppProps { + value: Shared<&'static str>, + } + + fn app(cx: Scope) -> 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) }, +// ] +// ); +// } diff --git a/packages/core/tests/miri_full_app.rs b/packages/core/tests/miri_full_app.rs new file mode 100644 index 000000000..c1c6d52cc --- /dev/null +++ b/packages/core/tests/miri_full_app.rs @@ -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}" + } + }) +} diff --git a/packages/core/tests/miri_simple.rs b/packages/core/tests/miri_simple.rs new file mode 100644 index 000000000..19d3a44a0 --- /dev/null +++ b/packages/core/tests/miri_simple.rs @@ -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::().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(); +} diff --git a/packages/core/tests/miri_stress.rs b/packages/core/tests/miri_stress.rs new file mode 100644 index 000000000..2d9760e5d --- /dev/null +++ b/packages/core/tests/miri_stress.rs @@ -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) -> 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, + } + + fn app(cx: Scope) -> Element { + let name: &AppProps = cx.use_hook(|| cx.props.clone()); + render!(child_component { inner: name.inner.clone() }) + } + + fn child_component(cx: Scope) -> 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) -> 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) -> 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); +// } +// } diff --git a/packages/core/tests/safety.rs b/packages/core/tests/safety.rs new file mode 100644 index 000000000..8c125f4c6 --- /dev/null +++ b/packages/core/tests/safety.rs @@ -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::>().is_some()); +} diff --git a/packages/core/tests/suspense.rs b/packages/core/tests/suspense.rs new file mode 100644 index 000000000..9fac13937 --- /dev/null +++ b/packages/core/tests/suspense.rs @@ -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::>().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}" } )) +} diff --git a/packages/core/tests/task.rs b/packages/core/tests/task.rs new file mode 100644 index 000000000..2704c5eb2 --- /dev/null +++ b/packages/core/tests/task.rs @@ -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!(())) +} diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index ee087e7b1..58e947574 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -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" } diff --git a/packages/desktop/src/controller.rs b/packages/desktop/src/controller.rs index f59874f85..7139c083f 100644 --- a/packages/desktop/src/controller.rs +++ b/packages/desktop/src/controller.rs @@ -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, - pub(super) sender: futures_channel::mpsc::UnboundedSender, pub(super) pending_edits: Arc>>, pub(super) quit_app_on_close: bool, pub(super) is_ready: Arc, + pub(super) proxy: EventLoopProxy, + pub(super) event_tx: UnboundedSender, + #[cfg(target_os = "ios")] pub(super) views: Vec<*mut Object>, } @@ -33,64 +38,61 @@ impl DesktopController { proxy: EventLoopProxy, ) -> Self { let edit_queue = Arc::new(Mutex::new(Vec::new())); - let (sender, receiver) = futures_channel::mpsc::unbounded::(); + 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::(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") + } } diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index f4d19bb68..f2bc2b3dc 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -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 { + let desktop = use_window(cx).clone(); + + &*cx.use_hook(|| Rc::new(move |script| desktop.eval(script)) as Rc) +} + /// 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(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}; diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index cf2c49727..b82d6f749 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -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 { } } -#[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 + })* + _ => 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 { - match name { - "copy" | "cut" | "paste" => { - // - Arc::new(ClipboardData {}) - } - "compositionend" | "compositionstart" | "compositionupdate" => { - Arc::new(serde_json::from_value::(val).unwrap()) - } - "keydown" | "keypress" | "keyup" => { - let evt = serde_json::from_value::(val).unwrap(); - Arc::new(evt) - } - "focus" | "blur" | "focusout" | "focusin" => { - // - Arc::new(FocusData {}) - } +pub fn decode_event(value: EventMessage) -> Option> { + 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::(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::(val).unwrap()) - } - "pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture" - | "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => { - Arc::new(serde_json::from_value::(val).unwrap()) - } - "select" => { - // - Arc::new(serde_json::from_value::(val).unwrap()) - } - - "touchcancel" | "touchend" | "touchmove" | "touchstart" => { - Arc::new(serde_json::from_value::(val).unwrap()) - } - - "scroll" => Arc::new(ScrollData {}), - - "wheel" => Arc::new(serde_json::from_value::(val).unwrap()), - - "animationstart" | "animationend" | "animationiteration" => { - Arc::new(serde_json::from_value::(val).unwrap()) - } - - "transitionend" => Arc::new(serde_json::from_value::(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) } diff --git a/packages/desktop/src/hot_reload.rs b/packages/desktop/src/hot_reload.rs index 86322f4bf..e2628cdd4 100644 --- a/packages/desktop/src/hot_reload.rs +++ b/packages/desktop/src/hot_reload.rs @@ -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) -> Option>>> = 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 { diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index e218570f4..585a7ff51 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -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(root: Component

, 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, + ) { + 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, + is_ready: Arc, + proxy: tao::event_loop::EventLoopProxy, + event_tx: UnboundedSender, +) -> 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(root: Component

, 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() } diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index b8e5cc341..cc084b403 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -7,7 +7,7 @@ use wry::{ const MODULE_LOADER: &str = r#" "#; diff --git a/packages/dioxus/Cargo.toml b/packages/dioxus/Cargo.toml index a86e4231a..801ef7ac5 100644 --- a/packages/dioxus/Cargo.toml +++ b/packages/dioxus/Cargo.toml @@ -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" diff --git a/packages/dioxus/benches/create.rs b/packages/dioxus/benches/create.rs deleted file mode 100644 index f328e4d9d..000000000 --- a/packages/dioxus/benches/create.rs +++ /dev/null @@ -1 +0,0 @@ -fn main() {} diff --git a/packages/dioxus/benches/jsframework.rs b/packages/dioxus/benches/jsframework.rs index 3a7198012..24ca2fe69 100644 --- a/packages/dioxus/benches/jsframework.rs +++ b/packages/dioxus/benches/jsframework.rs @@ -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); }) diff --git a/packages/dioxus/examples/stress.rs b/packages/dioxus/examples/stress.rs new file mode 100644 index 000000000..22560543e --- /dev/null +++ b/packages/dioxus/examples/stress.rs @@ -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) -> 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", +]; diff --git a/packages/dioxus/src/lib.rs b/packages/dioxus/src/lib.rs index 779369662..86adae336 100644 --- a/packages/dioxus/src/lib.rs +++ b/packages/dioxus/src/lib.rs @@ -7,7 +7,7 @@ pub use dioxus_hooks as hooks; pub mod events { #[cfg(feature = "html")] - pub use dioxus_html::{on::*, KeyCode}; + pub use dioxus_html::prelude::*; } #[cfg(feature = "html")] @@ -32,5 +32,5 @@ pub mod prelude { pub use dioxus_html as dioxus_elements; #[cfg(feature = "html")] - pub use dioxus_elements::{GlobalAttributes, SvgAttributes}; + pub use dioxus_elements::{prelude::*, GlobalAttributes, SvgAttributes}; } diff --git a/packages/dioxus/tests/.rustfmt.toml b/packages/dioxus/tests/.rustfmt.toml deleted file mode 100644 index 70a9483a8..000000000 --- a/packages/dioxus/tests/.rustfmt.toml +++ /dev/null @@ -1 +0,0 @@ -struct_lit_width = 80 diff --git a/packages/dioxus/tests/borrowedstate.rs b/packages/dioxus/tests/borrowedstate.rs deleted file mode 100644 index 905339793..000000000 --- a/packages/dioxus/tests/borrowedstate.rs +++ /dev/null @@ -1,46 +0,0 @@ -#![allow(non_snake_case)] - -use dioxus::prelude::*; - -#[test] -fn test_borrowed_state() { - let _ = VirtualDom::new(Parent); -} - -fn Parent(cx: Scope) -> Element { - let value = cx.use_hook(String::new); - - cx.render(rsx! { - div { - Child { name: value } - Child { name: value } - Child { name: value } - Child { name: value } - } - }) -} - -#[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}!" } - }) -} diff --git a/packages/dioxus/tests/create_dom.rs b/packages/dioxus/tests/create_dom.rs deleted file mode 100644 index d1136539c..000000000 --- a/packages/dioxus/tests/create_dom.rs +++ /dev/null @@ -1,248 +0,0 @@ -#![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::prelude::*; - -use dioxus_core::DomEdit::*; - -fn new_dom(app: Component

, props: P) -> VirtualDom { - VirtualDom::new_with_props(app, props) -} - -#[test] -fn test_original_diff() { - static APP: Component = |cx| { - cx.render(rsx! { - div { - div { - "Hello, world!" - } - } - }) - }; - - let mut dom = new_dom(APP, ()); - let mutations = dom.rebuild(); - assert_eq!( - mutations.edits, - [ - // create template - CreateElement { root: Some(1), tag: "template", children: 1 }, - CreateElement { root: None, tag: "div", children: 1 }, - CreateElement { root: None, tag: "div", children: 1 }, - CreateTextNode { root: None, text: "Hello, world!" }, - // clone template - CloneNodeChildren { id: Some(1), new_ids: vec![2] }, - // add to root - AppendChildren { root: Some(0), children: vec![2] }, - ] - ); -} - -#[test] -fn create() { - static APP: Component = |cx| { - cx.render(rsx! { - div { - div { - "Hello, world!" - div { - div { - Fragment { - "hello" - "world" - } - } - } - } - } - }) - }; - - let mut dom = new_dom(APP, ()); - let mutations = dom.rebuild(); - - assert_eq!( - mutations.edits, - [ - // create template - CreateElement { root: Some(1), tag: "template", children: 1 }, - CreateElement { root: None, tag: "div", children: 1 }, - CreateElement { root: None, tag: "div", children: 2 }, - CreateTextNode { root: None, text: "Hello, world!" }, - CreateElement { root: None, tag: "div", children: 1 }, - CreateElement { root: None, tag: "div", children: 0 }, - // clone template - CloneNodeChildren { id: Some(1), new_ids: vec![2] }, - SetLastNode { id: 2 }, - FirstChild {}, - StoreWithId { id: 3 }, - FirstChild {}, - NextSibling {}, - StoreWithId { id: 4 }, - FirstChild {}, - StoreWithId { id: 5 }, - CreateTextNode { root: Some(6), text: "hello" }, - CreateTextNode { root: Some(7), text: "world" }, - SetLastNode { id: 5 }, - AppendChildren { root: None, children: vec![6, 7] }, - AppendChildren { root: Some(0), children: vec![2] } - ] - ); -} - -#[test] -fn create_list() { - static APP: Component = |cx| { - cx.render(rsx! { - {(0..3).map(|f| rsx!{ div { - "hello" - }})} - }) - }; - - let mut dom = new_dom(APP, ()); - let mutations = dom.rebuild(); - - assert_eq!( - mutations.edits, - [ - // create template - CreateElement { root: Some(1), tag: "template", children: 1 }, - CreateElement { root: None, tag: "div", children: 1 }, - CreateTextNode { root: None, text: "hello" }, - // clone template - CloneNodeChildren { id: Some(1), new_ids: vec![2] }, - CloneNodeChildren { id: Some(1), new_ids: vec![3] }, - CloneNodeChildren { id: Some(1), new_ids: vec![4] }, - // add to root - AppendChildren { root: Some(0), children: vec![2, 3, 4] }, - ] - ); -} - -#[test] -fn create_simple() { - static APP: Component = |cx| { - cx.render(rsx! { - div {} - div {} - div {} - div {} - }) - }; - - let mut dom = new_dom(APP, ()); - let mutations = dom.rebuild(); - - assert_eq!( - mutations.edits, - [ - // create template - CreateElement { root: Some(1), tag: "template", children: 4 }, - CreateElement { root: None, tag: "div", children: 0 }, - CreateElement { root: None, tag: "div", children: 0 }, - CreateElement { root: None, tag: "div", children: 0 }, - CreateElement { root: None, tag: "div", children: 0 }, - // clone template - CloneNodeChildren { id: Some(1), new_ids: vec![2, 3, 4, 5] }, - // add to root - AppendChildren { root: Some(0), children: vec![2, 3, 4, 5] }, - ] - ); -} -#[test] -fn create_components() { - static App: Component = |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 mut dom = new_dom(App, ()); - let mutations = dom.rebuild(); - - assert_eq!( - mutations.edits, - [ - // create template - CreateElement { root: Some(1), tag: "template", children: 3 }, - CreateElement { root: None, tag: "h1", children: 0 }, - CreateElement { root: None, tag: "div", children: 0 }, - CreateElement { root: None, tag: "p", children: 0 }, - // clone template - CloneNodeChildren { id: Some(1), new_ids: vec![2, 3, 4] }, - // update template - SetLastNode { id: 2 }, - NextSibling {}, - CreateTextNode { root: Some(5), text: "abc1" }, - SetLastNode { id: 3 }, - AppendChildren { root: None, children: vec![5] }, - // clone template - CloneNodeChildren { id: Some(1), new_ids: vec![6, 7, 8] }, - SetLastNode { id: 6 }, - NextSibling {}, - // update template - CreateTextNode { root: Some(9), text: "abc2" }, - SetLastNode { id: 7 }, - AppendChildren { root: None, children: vec![9] }, - // clone template - CloneNodeChildren { id: Some(1), new_ids: vec![10, 11, 12] }, - // update template - SetLastNode { id: 10 }, - NextSibling {}, - CreateTextNode { root: Some(13), text: "abc3" }, - SetLastNode { id: 11 }, - AppendChildren { root: None, children: vec![13] }, - // add to root - AppendChildren { root: Some(0), children: vec![2, 3, 4, 6, 7, 8, 10, 11, 12] } - ] - ); -} - -#[test] -fn anchors() { - static App: Component = |cx| { - cx.render(rsx! { - {true.then(|| rsx!{ div { "hello" } })} - {false.then(|| rsx!{ div { "goodbye" } })} - }) - }; - - let mut dom = new_dom(App, ()); - let mutations = dom.rebuild(); - - assert_eq!( - mutations.edits, - [ - // create template - CreateElement { root: Some(1), tag: "template", children: 1 }, - CreateElement { root: None, tag: "div", children: 1 }, - CreateTextNode { root: None, text: "hello" }, - // clone template - CloneNodeChildren { id: Some(1), new_ids: vec![2] }, - CreatePlaceholder { root: Some(3) }, - // add to root - AppendChildren { root: Some(0), children: vec![2, 3] }, - ] - ); -} diff --git a/packages/dioxus/tests/diffing.rs b/packages/dioxus/tests/diffing.rs deleted file mode 100644 index 0f807151f..000000000 --- a/packages/dioxus/tests/diffing.rs +++ /dev/null @@ -1,831 +0,0 @@ -#![allow(unused, non_upper_case_globals)] - -//! 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_macro::rsx_without_templates, prelude::*}; - -fn new_dom() -> VirtualDom { - VirtualDom::new(|cx| cx.render(rsx_without_templates!("hi"))) -} - -use dioxus_core::DomEdit::*; - -/// Should push the text node onto the stack and modify it -#[test] -fn html_and_rsx_generate_the_same_output() { - let dom = new_dom(); - let (create, change) = dom.diff_lazynodes( - rsx_without_templates! ( div { "Hello world" } ), - rsx_without_templates! ( div { "Goodbye world" } ), - ); - assert_eq!( - create.edits, - [ - CreateElement { root: Some(1,), tag: "div", children: 0 }, - CreateTextNode { root: Some(2,), text: "Hello world" }, - AppendChildren { root: Some(1,), children: vec![2] }, - AppendChildren { root: Some(0,), children: vec![1] }, - ] - ); - - assert_eq!( - change.edits, - [SetText { root: Some(2,), text: "Goodbye world" },] - ); -} - -/// Should result in 3 elements on the stack -#[test] -fn fragments_create_properly() { - let dom = new_dom(); - - let create = dom.create_vnodes(rsx_without_templates! { - div { "Hello a" } - div { "Hello b" } - div { "Hello c" } - }); - - assert_eq!( - create.edits, - [ - CreateElement { root: Some(1,), tag: "div", children: 0 }, - CreateTextNode { root: Some(2,), text: "Hello a" }, - AppendChildren { root: Some(1,), children: vec![2,] }, - CreateElement { root: Some(3,), tag: "div", children: 0 }, - CreateTextNode { root: Some(4,), text: "Hello b" }, - AppendChildren { root: Some(3,), children: vec![4,] }, - CreateElement { root: Some(5,), tag: "div", children: 0 }, - CreateTextNode { root: Some(6,), text: "Hello c" }, - AppendChildren { root: Some(5,), children: vec![6,] }, - AppendChildren { root: Some(0,), children: vec![1, 3, 5,] }, - ] - ); -} - -/// Should result in the creation of an anchor (placeholder) and then a replacewith -#[test] -fn empty_fragments_create_anchors() { - let dom = new_dom(); - - let left = rsx_without_templates!({ (0..0).map(|_f| rsx_without_templates! { div {}}) }); - let right = rsx_without_templates!({ (0..1).map(|_f| rsx_without_templates! { div {}}) }); - - let (create, change) = dom.diff_lazynodes(left, right); - - assert_eq!( - create.edits, - [ - CreatePlaceholder { root: Some(1,) }, - AppendChildren { root: Some(0,), children: vec![1,] }, - ] - ); - assert_eq!( - change.edits, - [ - CreateElement { root: Some(2,), tag: "div", children: 0 }, - ReplaceWith { root: Some(1,), nodes: vec![2,] }, - ] - ); -} - -/// Should result in the creation of an anchor (placeholder) and then a replacewith m=5 -#[test] -fn empty_fragments_create_many_anchors() { - let dom = new_dom(); - - let left = rsx_without_templates!({ (0..0).map(|_f| rsx_without_templates! { div {}}) }); - let right = rsx_without_templates!({ (0..5).map(|_f| rsx_without_templates! { div {}}) }); - - let (create, change) = dom.diff_lazynodes(left, right); - - assert_eq!( - create.edits, - [ - CreatePlaceholder { root: Some(1,) }, - AppendChildren { root: Some(0,), children: vec![1,] }, - ] - ); - - assert_eq!( - change.edits, - [ - CreateElement { root: Some(2,), tag: "div", children: 0 }, - CreateElement { root: Some(3,), tag: "div", children: 0 }, - CreateElement { root: Some(4,), tag: "div", children: 0 }, - CreateElement { root: Some(5,), tag: "div", children: 0 }, - CreateElement { root: Some(6,), tag: "div", children: 0 }, - ReplaceWith { root: Some(1,), nodes: vec![2, 3, 4, 5, 6,] }, - ] - ); -} - -/// Should result in the creation of an anchor (placeholder) and then a replacewith -/// Includes child nodes inside the fragment -#[test] -fn empty_fragments_create_anchors_with_many_children() { - let dom = new_dom(); - - let left = rsx_without_templates!({ (0..0).map(|_| rsx_without_templates! { div {} }) }); - let right = rsx_without_templates!({ - (0..3).map(|f| { - rsx_without_templates! { div { "hello: {f}" }} - }) - }); - - let (create, change) = dom.diff_lazynodes(left, right); - - assert_eq!( - create.edits, - [ - CreatePlaceholder { root: Some(1,) }, - AppendChildren { root: Some(0,), children: vec![1,] }, - ] - ); - - assert_eq!( - change.edits, - [ - CreateElement { root: Some(2,), tag: "div", children: 0 }, - CreateTextNode { root: Some(3,), text: "hello: 0" }, - AppendChildren { root: Some(2,), children: vec![3,] }, - CreateElement { root: Some(4,), tag: "div", children: 0 }, - CreateTextNode { root: Some(5,), text: "hello: 1" }, - AppendChildren { root: Some(4,), children: vec![5,] }, - CreateElement { root: Some(6,), tag: "div", children: 0 }, - CreateTextNode { root: Some(7,), text: "hello: 2" }, - AppendChildren { root: Some(6,), children: vec![7,] }, - ReplaceWith { root: Some(1,), nodes: vec![2, 4, 6,] }, - ] - ); -} - -/// Should result in every node being pushed and then replaced with an anchor -#[test] -fn many_items_become_fragment() { - let dom = new_dom(); - - let left = rsx_without_templates!({ - (0..2).map(|_| { - rsx_without_templates! { div { "hello" }} - }) - }); - let right = rsx_without_templates!({ (0..0).map(|_| rsx_without_templates! { div {} }) }); - - let (create, change) = dom.diff_lazynodes(left, right); - - assert_eq!( - create.edits, - [ - CreateElement { root: Some(1,), tag: "div", children: 0 }, - CreateTextNode { root: Some(2,), text: "hello" }, - AppendChildren { root: Some(1,), children: vec![2,] }, - CreateElement { root: Some(3,), tag: "div", children: 0 }, - CreateTextNode { root: Some(4,), text: "hello" }, - AppendChildren { root: Some(3,), children: vec![4,] }, - AppendChildren { root: Some(0,), children: vec![1, 3,] }, - ] - ); - - assert_eq!( - change.edits, - [ - CreatePlaceholder { root: Some(5,) }, - ReplaceWith { root: Some(1,), nodes: vec![5,] }, - Remove { root: Some(3,) }, - ] - ); -} - -/// Should result in no edits -#[test] -fn two_equal_fragments_are_equal() { - let dom = new_dom(); - - let left = rsx_without_templates!({ - (0..2).map(|_| { - rsx_without_templates! { div { "hello" }} - }) - }); - let right = rsx_without_templates!({ - (0..2).map(|_| { - rsx_without_templates! { div { "hello" }} - }) - }); - - let (_create, change) = dom.diff_lazynodes(left, right); - assert!(change.edits.is_empty()); -} - -/// Should result the creation of more nodes appended after the old last node -#[test] -fn two_fragments_with_differrent_elements_are_differet() { - let dom = new_dom(); - - let left = rsx_without_templates!( - { (0..2).map(|_| rsx_without_templates! { div { }} ) } - p {} - ); - let right = rsx_without_templates!( - { (0..5).map(|_| rsx_without_templates! (h1 { }) ) } - p {} - ); - - let (_create, changes) = dom.diff_lazynodes(left, right); - assert_eq!( - changes.edits, - [ - CreateElement { root: Some(4,), tag: "h1", children: 0 }, - CreateElement { root: Some(5,), tag: "h1", children: 0 }, - CreateElement { root: Some(6,), tag: "h1", children: 0 }, - InsertAfter { root: Some(2,), nodes: vec![4, 5, 6,] }, - CreateElement { root: Some(7,), tag: "h1", children: 0 }, - ReplaceWith { root: Some(1,), nodes: vec![7,] }, // notice how 1 gets re-used - CreateElement { root: Some(1,), tag: "h1", children: 0 }, - ReplaceWith { root: Some(2,), nodes: vec![1,] }, - ] - ); -} - -/// Should result in multiple nodes destroyed - with changes to the first nodes -#[test] -fn two_fragments_with_differrent_elements_are_differet_shorter() { - let dom = new_dom(); - - let left = rsx_without_templates!( - {(0..5).map(|f| {rsx_without_templates! { div { }}})} - p {} - ); - let right = rsx_without_templates!( - {(0..2).map(|f| {rsx_without_templates! { h1 { }}})} - p {} - ); - - let (create, change) = dom.diff_lazynodes(left, right); - - assert_eq!( - create.edits, - [ - CreateElement { root: Some(1,), tag: "div", children: 0 }, - CreateElement { root: Some(2,), tag: "div", children: 0 }, - CreateElement { root: Some(3,), tag: "div", children: 0 }, - CreateElement { root: Some(4,), tag: "div", children: 0 }, - CreateElement { root: Some(5,), tag: "div", children: 0 }, - CreateElement { root: Some(6,), tag: "p", children: 0 }, - AppendChildren { root: Some(0,), children: vec![1, 2, 3, 4, 5, 6,] }, - ] - ); - - // note: key reuse is always the last node that got used - // slab maintains a linked list, essentially - assert_eq!( - change.edits, - [ - Remove { root: Some(3,) }, - Remove { root: Some(4,) }, - Remove { root: Some(5,) }, - CreateElement { root: Some(5,), tag: "h1", children: 0 }, // 5 gets reused - ReplaceWith { root: Some(1,), nodes: vec![5,] }, // 1 gets deleted - CreateElement { root: Some(1,), tag: "h1", children: 0 }, // 1 gets reused - ReplaceWith { root: Some(2,), nodes: vec![1,] }, - ] - ); -} - -/// Should result in multiple nodes destroyed - with no changes -#[test] -fn two_fragments_with_same_elements_are_differet() { - let dom = new_dom(); - - let left = rsx_without_templates!( - {(0..2).map(|f| rsx_without_templates! { div { }})} - p {} - ); - let right = rsx_without_templates!( - {(0..5).map(|f| rsx_without_templates! { div { }})} - p {} - ); - - let (create, change) = dom.diff_lazynodes(left, right); - - assert_eq!( - create.edits, - [ - CreateElement { root: Some(1,), tag: "div", children: 0 }, - CreateElement { root: Some(2,), tag: "div", children: 0 }, - CreateElement { root: Some(3,), tag: "p", children: 0 }, - AppendChildren { root: Some(0,), children: vec![1, 2, 3,] }, - ] - ); - assert_eq!( - change.edits, - [ - CreateElement { root: Some(4,), tag: "div", children: 0 }, - CreateElement { root: Some(5,), tag: "div", children: 0 }, - CreateElement { root: Some(6,), tag: "div", children: 0 }, - InsertAfter { root: Some(2,), nodes: vec![4, 5, 6,] }, - ] - ); -} - -/// should result in the removal of elements -#[test] -fn keyed_diffing_order() { - let dom = new_dom(); - - let left = rsx_without_templates!( - {(0..5).map(|f| {rsx_without_templates! { div { key: "{f}" }}})} - p {"e"} - ); - let right = rsx_without_templates!( - {(0..2).map(|f| rsx_without_templates! { div { key: "{f}" }})} - p {"e"} - ); - - let (create, change) = dom.diff_lazynodes(left, right); - - assert_eq!( - change.edits, - [ - Remove { root: Some(3,) }, - Remove { root: Some(4,) }, - Remove { root: Some(5,) }, - ] - ); -} - -/// Should result in moves, but not removals or additions -#[test] -fn keyed_diffing_out_of_order() { - let dom = new_dom(); - - let left = rsx_without_templates!({ - [0, 1, 2, 3, /**/ 4, 5, 6, /**/ 7, 8, 9].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) - }); - - let right = rsx_without_templates!({ - [0, 1, 2, 3, /**/ 6, 4, 5, /**/ 7, 8, 9].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) - }); - - let (_, changes) = dom.diff_lazynodes(left, right); - - assert_eq!( - changes.edits, - [InsertBefore { root: Some(5,), nodes: vec![7,] },] - ); -} - -/// Should result in moves only -#[test] -fn keyed_diffing_out_of_order_adds() { - let dom = new_dom(); - - let left = rsx_without_templates!({ - [/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) - }); - - let right = rsx_without_templates!({ - [/**/ 8, 7, 4, 5, 6 /**/].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) - }); - - let (_, change) = dom.diff_lazynodes(left, right); - - assert_eq!( - change.edits, - [InsertBefore { root: Some(1,), nodes: vec![5, 4,] },] - ); -} -/// Should result in moves only -#[test] -fn keyed_diffing_out_of_order_adds_2() { - let dom = new_dom(); - - let left = rsx_without_templates!({ - [/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) - }); - - let right = rsx_without_templates!({ - [/**/ 7, 8, 4, 5, 6 /**/].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) - }); - - let (_, change) = dom.diff_lazynodes(left, right); - - assert_eq!( - change.edits, - [InsertBefore { root: Some(1,), nodes: vec![4, 5,] },] - ); -} - -/// Should result in moves onl -#[test] -fn keyed_diffing_out_of_order_adds_3() { - let dom = new_dom(); - - let left = rsx_without_templates!({ - [/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) - }); - - let right = rsx_without_templates!({ - [/**/ 4, 8, 7, 5, 6 /**/].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) - }); - - let (_, change) = dom.diff_lazynodes(left, right); - - assert_eq!( - change.edits, - [InsertBefore { root: Some(2,), nodes: vec![5, 4,] },] - ); -} - -/// Should result in moves onl -#[test] -fn keyed_diffing_out_of_order_adds_4() { - let dom = new_dom(); - - let left = rsx_without_templates!({ - [/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) - }); - - let right = rsx_without_templates!({ - [/**/ 4, 5, 8, 7, 6 /**/].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) - }); - - let (_, change) = dom.diff_lazynodes(left, right); - - assert_eq!( - change.edits, - [InsertBefore { root: Some(3), nodes: vec![5, 4,] },] - ); -} - -/// Should result in moves onl -#[test] -fn keyed_diffing_out_of_order_adds_5() { - let dom = new_dom(); - - let left = rsx_without_templates!({ - [/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) - }); - - let right = rsx_without_templates!({ - [/**/ 4, 5, 6, 8, 7 /**/].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) - }); - - let (_, change) = dom.diff_lazynodes(left, right); - assert_eq!( - change.edits, - [InsertBefore { root: Some(4), nodes: vec![5] }] - ); -} - -#[test] -fn keyed_diffing_additions() { - let dom = new_dom(); - - let left = rsx_without_templates!({ - [/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) - }); - - let right = rsx_without_templates!({ - [/**/ 4, 5, 6, 7, 8, 9, 10 /**/].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) - }); - - let (_, change) = dom.diff_lazynodes(left, right); - - assert_eq!( - change.edits, - [ - CreateElement { root: Some(6,), tag: "div", children: 0 }, - CreateElement { root: Some(7,), tag: "div", children: 0 }, - InsertAfter { root: Some(5,), nodes: vec![6, 7,] }, - ] - ); -} - -#[test] -fn keyed_diffing_additions_and_moves_on_ends() { - let dom = new_dom(); - - let left = rsx_without_templates!({ - [/**/ 4, 5, 6, 7 /**/].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) - }); - - let right = rsx_without_templates!({ - [/**/ 7, 4, 5, 6, 11, 12 /**/].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) - }); - - let (_, change) = dom.diff_lazynodes(left, right); - println!("{:?}", change); - assert_eq!( - change.edits, - [ - // create 11, 12 - CreateElement { root: Some(5), tag: "div", children: 0 }, - CreateElement { root: Some(6), tag: "div", children: 0 }, - InsertAfter { root: Some(3), nodes: vec![5, 6] }, - // // move 7 to the front - InsertBefore { root: Some(1), nodes: vec![4] } - ] - ); -} - -#[test] -fn keyed_diffing_additions_and_moves_in_middle() { - let dom = new_dom(); - - let left = rsx_without_templates!({ - [/**/ 1, 2, 3, 4 /**/].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) - }); - - let right = rsx_without_templates!({ - [/**/ 4, 1, 7, 8, 2, 5, 6, 3 /**/].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) - }); - - // LIS: 4, 5, 6 - let (_, change) = dom.diff_lazynodes(left, right); - println!("{:#?}", change); - assert_eq!( - change.edits, - [ - // create 5, 6 - CreateElement { root: Some(5,), tag: "div", children: 0 }, - CreateElement { root: Some(6,), tag: "div", children: 0 }, - InsertBefore { root: Some(3,), nodes: vec![5, 6,] }, - // create 7, 8 - CreateElement { root: Some(7,), tag: "div", children: 0 }, - CreateElement { root: Some(8,), tag: "div", children: 0 }, - InsertBefore { root: Some(2,), nodes: vec![7, 8,] }, - // move 7 - InsertBefore { root: Some(1,), nodes: vec![4,] }, - ] - ); -} - -#[test] -fn controlled_keyed_diffing_out_of_order() { - let dom = new_dom(); - - let left = rsx_without_templates!({ - [4, 5, 6, 7].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) - }); - - let right = rsx_without_templates!({ - [0, 5, 9, 6, 4].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) - }); - - // LIS: 5, 6 - let (_, changes) = dom.diff_lazynodes(left, right); - println!("{:#?}", &changes); - assert_eq!( - changes.edits, - [ - // remove 7 - Remove { root: Some(4,) }, - // move 4 to after 6 - InsertAfter { root: Some(3,), nodes: vec![1,] }, - // create 9 and insert before 6 - CreateElement { root: Some(4,), tag: "div", children: 0 }, - InsertBefore { root: Some(3,), nodes: vec![4,] }, - // create 0 and insert before 5 - CreateElement { root: Some(5,), tag: "div", children: 0 }, - InsertBefore { root: Some(2,), nodes: vec![5,] }, - ] - ); -} - -#[test] -fn controlled_keyed_diffing_out_of_order_max_test() { - let dom = new_dom(); - - let left = rsx_without_templates!({ - [0, 1, 2, 3, 4].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) - }); - - let right = rsx_without_templates!({ - [3, 0, 1, 10, 2].iter().map(|f| { - rsx_without_templates! { div { key: "{f}" }} - }) - }); - - let (_, changes) = dom.diff_lazynodes(left, right); - println!("{:#?}", &changes); - assert_eq!( - changes.edits, - [ - Remove { root: Some(5,) }, - CreateElement { root: Some(5,), tag: "div", children: 0 }, - InsertBefore { root: Some(3,), nodes: vec![5,] }, - InsertBefore { root: Some(1,), nodes: vec![4,] }, - ] - ); -} - -// noticed some weird behavior in the desktop interpreter -// just making sure it doesnt happen in the core implementation -#[test] -fn remove_list() { - let dom = new_dom(); - - let left = rsx_without_templates!({ - (0..10).rev().take(5).map(|i| { - rsx_without_templates! { Fragment { key: "{i}", "{i}" }} - }) - }); - - let right = rsx_without_templates!({ - (0..10).rev().take(2).map(|i| { - rsx_without_templates! { Fragment { key: "{i}", "{i}" }} - }) - }); - - let (create, changes) = dom.diff_lazynodes(left, right); - - // dbg!(create); - // dbg!(changes); - - assert_eq!( - changes.edits, - // remove 5, 4, 3 - [ - Remove { root: Some(3) }, - Remove { root: Some(4) }, - Remove { root: Some(5) } - ] - ); -} - -// noticed some weird behavior in the desktop interpreter -// just making sure it doesnt happen in the core implementation -#[test] -fn remove_list_nokeyed() { - let dom = new_dom(); - - let left = rsx_without_templates!({ - (0..10).rev().take(5).map(|i| { - rsx_without_templates! { Fragment { "{i}" }} - }) - }); - - let right = rsx_without_templates!({ - (0..10).rev().take(2).map(|i| { - rsx_without_templates! { Fragment { "{i}" }} - }) - }); - - let (create, changes) = dom.diff_lazynodes(left, right); - - assert_eq!( - changes.edits, - [ - // remove 5, 4, 3 - Remove { root: Some(3) }, - Remove { root: Some(4) }, - Remove { root: Some(5) }, - ] - ); -} - -#[test] -fn add_nested_elements() { - let vdom = new_dom(); - - let (_create, change) = vdom.diff_lazynodes( - rsx_without_templates! { - div{} - }, - rsx_without_templates! { - div{ - div{} - } - }, - ); - - assert_eq!( - change.edits, - [ - CreateElement { root: Some(2), tag: "div", children: 0 }, - AppendChildren { root: Some(1), children: vec![2] }, - ] - ); -} - -#[test] -fn add_listeners() { - let vdom = new_dom(); - - let (_create, change) = vdom.diff_lazynodes( - rsx_without_templates! { - div{} - }, - rsx_without_templates! { - div{ - onkeyup: |_| {}, - onkeydown: |_| {}, - } - }, - ); - - assert_eq!( - change.edits, - [ - NewEventListener { event_name: "keyup", scope: ScopeId(0), root: Some(1) }, - NewEventListener { event_name: "keydown", scope: ScopeId(0), root: Some(1) }, - ] - ); -} - -#[test] -fn remove_listeners() { - let vdom = new_dom(); - - let (_create, change) = vdom.diff_lazynodes( - rsx_without_templates! { - div{ - onkeyup: |_| {}, - onkeydown: |_| {}, - } - }, - rsx_without_templates! { - div{} - }, - ); - - assert_eq!( - change.edits, - [ - RemoveEventListener { event: "keyup", root: Some(1) }, - RemoveEventListener { event: "keydown", root: Some(1) }, - ] - ); -} - -#[test] -fn diff_listeners() { - let vdom = new_dom(); - - let (_create, change) = vdom.diff_lazynodes( - rsx_without_templates! { - div{ - onkeydown: |_| {}, - } - }, - rsx_without_templates! { - div{ - onkeyup: |_| {}, - } - }, - ); - - assert_eq!( - change.edits, - [ - RemoveEventListener { root: Some(1), event: "keydown" }, - NewEventListener { event_name: "keyup", scope: ScopeId(0), root: Some(1) } - ] - ); -} diff --git a/packages/dioxus/tests/earlyabort.rs b/packages/dioxus/tests/earlyabort.rs deleted file mode 100644 index 11bf3e2ca..000000000 --- a/packages/dioxus/tests/earlyabort.rs +++ /dev/null @@ -1,67 +0,0 @@ -#![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::prelude::*; - -use dioxus_core::{DomEdit::*, ScopeId}; - -const IS_LOGGING_ENABLED: bool = false; - -fn new_dom(app: Component

, props: P) -> VirtualDom { - VirtualDom::new_with_props(app, props) -} - -/// This test ensures that if a component aborts early, it is replaced with a placeholder. -/// In debug, this should also toss a warning. -#[test] -fn test_early_abort() { - const app: Component = |cx| { - let val = cx.use_hook(|| 0); - - *val += 1; - - if *val == 2 { - return None; - } - - render!(div { "Hello, world!" }) - }; - - let mut dom = new_dom(app, ()); - - let edits = dom.rebuild(); - assert_eq!( - edits.edits, - [ - // create template - CreateElement { root: Some(1), tag: "template", children: 1 }, - CreateElement { root: None, tag: "div", children: 1 }, - CreateTextNode { root: None, text: "Hello, world!" }, - // clone template - CloneNodeChildren { id: Some(1), new_ids: vec![2] }, - AppendChildren { root: Some(0), children: vec![2] } - ] - ); - - let edits = dom.hard_diff(ScopeId(0)); - assert_eq!( - edits.edits, - [ - CreatePlaceholder { root: Some(3) }, - ReplaceWith { root: Some(2), nodes: vec![3] } - ] - ); - - let edits = dom.hard_diff(ScopeId(0)); - assert_eq!( - edits.edits, - [ - CloneNodeChildren { id: Some(1), new_ids: vec![2] }, - ReplaceWith { root: Some(3), nodes: vec![2] } - ] - ); -} diff --git a/packages/dioxus/tests/lifecycle.rs b/packages/dioxus/tests/lifecycle.rs deleted file mode 100644 index 17d198597..000000000 --- a/packages/dioxus/tests/lifecycle.rs +++ /dev/null @@ -1,264 +0,0 @@ -#![allow(unused, non_upper_case_globals)] -#![allow(non_snake_case)] - -//! Tests for the lifecycle of components. -use dioxus::{core_macro::rsx_without_templates, prelude::*}; -use dioxus_core::DomEdit::*; -use std::sync::{Arc, Mutex}; - -type Shared = Arc>; - -#[test] -fn manual_diffing() { - struct AppProps { - value: Shared<&'static str>, - } - - static App: Component = |cx| { - let val = cx.props.value.lock().unwrap(); - cx.render(rsx_without_templates! { 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"; - - let edits = dom.rebuild(); - - println!("edits: {:?}", edits); -} - -#[test] -fn events_generate() { - fn app(cx: Scope) -> Element { - let count = cx.use_hook(|| 0); - - let inner = match *count { - 0 => { - rsx_without_templates! { - div { - onclick: move |_| *count += 1, - div { - "nested" - } - "Click me!" - } - } - } - _ => todo!(), - }; - - cx.render(inner) - }; - - let mut dom = VirtualDom::new(app); - let mut channel = dom.get_scheduler_channel(); - assert!(dom.has_work()); - - let edits = dom.rebuild(); - assert_eq!( - edits.edits, - [ - CreateElement { root: Some(1), tag: "div", children: 0 }, - NewEventListener { event_name: "click", scope: ScopeId(0), root: Some(1) }, - CreateElement { root: Some(2), tag: "div", children: 0 }, - CreateTextNode { root: Some(3), text: "nested" }, - AppendChildren { root: Some(2), children: vec![3] }, - CreateTextNode { root: Some(4), text: "Click me!" }, - AppendChildren { root: Some(1), children: vec![2, 4] }, - AppendChildren { root: Some(0), children: vec![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) }, - ] - ); -} - -#[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_without_templates!( - div { - NavBar {} - Dashboard {} - } - ), - 1 => rsx_without_templates!( - div { - NavBar {} - Results {} - } - ), - 2 => rsx_without_templates!( - div { - NavBar {} - Dashboard {} - } - ), - 3 => rsx_without_templates!( - div { - NavBar {} - Results {} - } - ), - 4 => rsx_without_templates!( - div { - NavBar {} - Dashboard {} - } - ), - _ => rsx_without_templates!("blah"), - }) - }; - - static NavBar: Component = |cx| { - println!("running navbar"); - cx.render(rsx_without_templates! { - h1 { - "NavBar" - {(0..3).map(|f| rsx_without_templates!(NavLink {}))} - } - }) - }; - - static NavLink: Component = |cx| { - println!("running navlink"); - cx.render(rsx_without_templates! { - h1 { - "NavLink" - } - }) - }; - - static Dashboard: Component = |cx| { - println!("running dashboard"); - cx.render(rsx_without_templates! { - div { - "dashboard" - } - }) - }; - - static Results: Component = |cx| { - println!("running results"); - cx.render(rsx_without_templates! { - div { - "results" - } - }) - }; - - let mut dom = VirtualDom::new(app); - let edits = dom.rebuild(); - dbg!(&edits); - - let edits = dom.work_with_deadline(|| false); - dbg!(&edits); - let edits = dom.work_with_deadline(|| false); - dbg!(&edits); - let edits = dom.work_with_deadline(|| false); - dbg!(&edits); - let edits = dom.work_with_deadline(|| false); - dbg!(&edits); -} diff --git a/packages/dioxus/tests/miri_stress.rs b/packages/dioxus/tests/miri_stress.rs deleted file mode 100644 index 11f7d06ad..000000000 --- a/packages/dioxus/tests/miri_stress.rs +++ /dev/null @@ -1,413 +0,0 @@ -#![allow(non_snake_case)] -/* -Stress Miri as much as possible. - -Prove that we don't leak memory and that our methods are safe. - -Specifically: -- [ ] VirtualDom drops memory safely -- [ ] Borrowed components don't expose invalid pointers -- [ ] Async isn't busted -*/ - -use dioxus::prelude::*; -use dioxus_core::SchedulerMsg; - -fn new_dom(app: Component

, props: P) -> VirtualDom { - VirtualDom::new_with_props(app, props) -} - -/// This test ensures that if a component aborts early, it is replaced with a placeholder. -/// In debug, this should also toss a warning. -#[test] -#[ignore] -fn test_memory_leak() { - fn app(cx: Scope) -> Element { - let val = cx.use_hook(|| 0); - - *val += 1; - - if *val == 2 || *val == 4 { - return None; - } - - let name = cx.use_hook(|| String::from("asd")); - - cx.render(rsx!( - div { "Hello, world!" } - Child {} - Child {} - Child {} - Child {} - Child {} - Child {} - BorrowedChild { na: name } - BorrowedChild { na: name } - BorrowedChild { na: name } - BorrowedChild { na: name } - BorrowedChild { na: name } - )) - } - - #[derive(Props)] - struct BorrowedProps<'a> { - na: &'a str, - } - - fn BorrowedChild<'a>(cx: Scope<'a, BorrowedProps<'a>>) -> Element { - render!(div { - "goodbye {cx.props.na}" - Child {} - Child {} - }) - } - - fn Child(cx: Scope) -> Element { - render!(div { "goodbye world" }) - } - - let mut dom = new_dom(app, ()); - - dom.rebuild(); - 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 memo_works_properly() { - fn app(cx: Scope) -> Element { - let val = cx.use_hook(|| 0); - - *val += 1; - - if *val == 2 || *val == 4 { - return None; - } - - 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) -> Element { - render!(div { "goodbye world" }) - } - - let mut dom = new_dom(app, ()); - - dom.rebuild(); - 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_props() { - fn app(cx: Scope) -> Element { - cx.render(rsx! { - Child { a: "alpha"} - Child { a: "beta"} - Child { a: "gamma"} - Child { a: "delta"} - }) - } - - #[derive(Props, PartialEq)] - struct ChildProps { - a: &'static str, - } - - fn Child(cx: Scope) -> Element { - render!("child {cx.props.a}") - } - - struct Custom { - val: String, - } - - impl Drop for Custom { - fn drop(&mut self) { - dbg!("dropped! {}", &self.val); - } - } - - let mut dom = new_dom(app, Custom { val: String::from("asd") }); - dom.rebuild(); -} - -#[test] -fn free_works_on_borrowed() { - fn app(cx: Scope) -> Element { - cx.render(rsx! { - Child { a: "alpha", b: "asd".to_string() } - }) - } - #[derive(Props)] - struct ChildProps<'a> { - a: &'a str, - b: String, - } - - fn Child<'a>(cx: Scope<'a, ChildProps<'a>>) -> Element { - dbg!("rendering child"); - render!("child {cx.props.a}, {cx.props.b}") - } - - impl Drop for ChildProps<'_> { - fn drop(&mut self) { - dbg!("dropped child!"); - } - } - - let mut dom = new_dom(app, ()); - let _ = dom.rebuild(); -} - -#[test] -fn free_works_on_root_hooks() { - /* - On Drop, scopearena drops all the hook contents. - */ - - struct Droppable(T); - impl Drop for Droppable { - fn drop(&mut self) { - dbg!("dropping droppable"); - } - } - - fn app(cx: Scope) -> Element { - let name = cx.use_hook(|| Droppable(String::from("asd"))); - render!(div { "{name.0}" }) - } - - let mut dom = new_dom(app, ()); - let _ = dom.rebuild(); -} - -#[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) -> 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) -> 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); - } -} diff --git a/packages/dioxus/tests/passthru.rs b/packages/dioxus/tests/passthru.rs deleted file mode 100644 index ee9667c97..000000000 --- a/packages/dioxus/tests/passthru.rs +++ /dev/null @@ -1,100 +0,0 @@ -#![allow(unused, non_upper_case_globals)] -#![allow(non_snake_case)] - -//! 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::prelude::*; - -fn new_dom() -> VirtualDom { - VirtualDom::new(|cx| render!("hi")) -} - -use dioxus_core::DomEdit::*; - -/// Should push the text node onto the stack and modify it -#[test] -fn nested_passthru_creates() { - fn app(cx: Scope) -> Element { - cx.render(rsx! { - Child { - Child { - Child { - div { - "hi" - } - } - } - } - }) - }; - - #[inline_props] - fn Child<'a>(cx: Scope, children: Element<'a>) -> Element { - cx.render(rsx! { children }) - }; - - let mut dom = VirtualDom::new(app); - let mut channel = dom.get_scheduler_channel(); - assert!(dom.has_work()); - - let edits = dom.rebuild(); - assert_eq!( - edits.edits, - [ - CreateElement { tag: "div", root: Some(1), children: 0 }, - CreateTextNode { text: "hi", root: Some(2) }, - AppendChildren { root: Some(1), children: vec![2] }, - AppendChildren { root: Some(0), children: vec![1] }, - ] - ) -} - -/// Should push the text node onto the stack and modify it -#[test] -fn nested_passthru_creates_add() { - fn app(cx: Scope) -> Element { - cx.render(rsx! { - Child { - "1" - Child { - "2" - Child { - "3" - div { - "hi" - } - } - } - } - }) - }; - - #[inline_props] - fn Child<'a>(cx: Scope, children: Element<'a>) -> Element { - cx.render(rsx! { - children - }) - }; - - let mut dom = VirtualDom::new(app); - let mut channel = dom.get_scheduler_channel(); - assert!(dom.has_work()); - - let edits = dom.rebuild(); - assert_eq!( - edits.edits, - [ - CreateTextNode { text: "1", root: Some(1) }, - CreateTextNode { text: "2", root: Some(2) }, - CreateTextNode { text: "3", root: Some(3) }, - CreateElement { tag: "div", root: Some(4), children: 0 }, - CreateTextNode { text: "hi", root: Some(5) }, - AppendChildren { root: Some(4), children: vec![5] }, - AppendChildren { root: Some(0), children: vec![1, 2, 3, 4] }, - ] - ) -} diff --git a/packages/dioxus/tests/scheduler.rs b/packages/dioxus/tests/scheduler.rs deleted file mode 100644 index f058dfe4c..000000000 --- a/packages/dioxus/tests/scheduler.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! Tests for the scheduler. -//! -//! TODO -//! - priority lanes -//! - periodic checking -//! diff --git a/packages/dioxus/tests/sharedstate.rs b/packages/dioxus/tests/sharedstate.rs deleted file mode 100644 index 3b6f3559c..000000000 --- a/packages/dioxus/tests/sharedstate.rs +++ /dev/null @@ -1,126 +0,0 @@ -#![allow(unused, non_upper_case_globals, non_snake_case)] - -use dioxus::{core_macro::rsx_without_templates, prelude::*}; -use dioxus_core::{DomEdit, Mutations, SchedulerMsg, ScopeId}; -use std::rc::Rc; -use DomEdit::*; - -#[test] -fn shared_state_test() { - struct MySharedState(&'static str); - - static App: Component = |cx| { - cx.provide_context(Rc::new(MySharedState("world!"))); - cx.render(rsx_without_templates!(Child {})) - }; - - static Child: Component = |cx| { - let shared = cx.consume_context::>()?; - cx.render(rsx_without_templates!("Hello, {shared.0}")) - }; - - let mut dom = VirtualDom::new(App); - let Mutations { edits, .. } = dom.rebuild(); - - assert_eq!( - edits, - [ - CreateTextNode { root: Some(1), text: "Hello, world!" }, - AppendChildren { root: Some(0), children: vec![1] }, - ] - ); -} - -#[test] -fn swap_test() { - struct MySharedState(&'static str); - - fn app(cx: Scope) -> Element { - let val = cx.use_hook(|| 0); - *val += 1; - - cx.provide_context(Rc::new(MySharedState("world!"))); - - let child = match *val % 2 { - 0 => rsx_without_templates!( - Child1 { - Child1 { } - Child2 { } - } - ), - _ => rsx_without_templates!( - Child2 { - Child2 { } - Child2 { } - } - ), - }; - - cx.render(rsx_without_templates!( - Router { - div { child } - } - )) - } - - #[inline_props] - fn Router<'a>(cx: Scope, children: Element<'a>) -> Element<'a> { - cx.render(rsx_without_templates!(div { children })) - } - - #[inline_props] - fn Child1<'a>(cx: Scope, children: Element<'a>) -> Element { - let shared = cx.consume_context::>().unwrap(); - println!("Child1: {}", shared.0); - cx.render(rsx_without_templates! { - div { - "{shared.0}", - children - } - }) - } - - #[inline_props] - fn Child2<'a>(cx: Scope, children: Element<'a>) -> Element { - let shared = cx.consume_context::>().unwrap(); - println!("Child2: {}", shared.0); - cx.render(rsx_without_templates! { - h1 { - "{shared.0}", - children - } - }) - } - - let mut dom = VirtualDom::new(app); - let Mutations { edits, .. } = 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); - 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(2))); - // dom.work_with_deadline(|| false); - - // dom.handle_message(SchedulerMsg::Immediate(ScopeId(3))); - // 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(0))); - // dom.work_with_deadline(|| false); -} diff --git a/packages/dioxus/tests/task.rs b/packages/dioxus/tests/task.rs deleted file mode 100644 index f328e4d9d..000000000 --- a/packages/dioxus/tests/task.rs +++ /dev/null @@ -1 +0,0 @@ -fn main() {} diff --git a/packages/dioxus/tests/vdom_rebuild.rs b/packages/dioxus/tests/vdom_rebuild.rs deleted file mode 100644 index 5d19de48e..000000000 --- a/packages/dioxus/tests/vdom_rebuild.rs +++ /dev/null @@ -1,104 +0,0 @@ -#![allow(unused, non_upper_case_globals)] - -//! Rebuilding tests -//! ---------------- -//! -//! This tests module ensures that the initial build of the virtualdom is correct. -//! This does not include dynamic tests or the diffing algorithm itself. -//! -//! It does prove that mounting works properly and the correct edit streams are generated. -//! -//! Don't have a good way to validate, everything is done manually ATM - -use dioxus::prelude::*; -use dioxus_core::DomEdit::*; - -#[test] -fn app_runs() { - static App: Component = |cx| render!(div{"hello"} ); - - let mut vdom = VirtualDom::new(App); - let edits = vdom.rebuild(); -} - -#[test] -fn fragments_work() { - static App: Component = |cx| { - cx.render(rsx!( - div{"hello"} - div{"goodbye"} - )) - }; - let mut vdom = VirtualDom::new(App); - let edits = vdom.rebuild(); - // should result in a final "appendchildren n=2" - dbg!(edits); -} - -#[test] -fn lists_work() { - static App: Component = |cx| { - cx.render(rsx!( - h1 {"hello"} - (0..6).map(|f| rsx!(span{ "{f}" })) - )) - }; - let mut vdom = VirtualDom::new(App); - let edits = vdom.rebuild(); - dbg!(edits); -} - -#[test] -#[ignore] -fn conditional_rendering() { - static App: Component = |cx| { - cx.render(rsx!( - h1 {"hello"} - {true.then(|| rsx!(span{ "a" }))} - {false.then(|| rsx!(span{ "b" }))} - )) - }; - let mut vdom = VirtualDom::new(App); - - let mutations = vdom.rebuild(); - assert_eq!( - mutations.edits, - [ - CreateElement { root: Some(1), tag: "template", children: 3 }, - CreateElement { root: None, tag: "h1", children: 1 }, - CreateTextNode { root: None, text: "hello" }, - CreatePlaceholder { root: None }, - CreatePlaceholder { root: None }, - CloneNodeChildren { id: Some(1), new_ids: vec![2, 3, 4] }, - CreateElement { root: Some(5), tag: "template", children: 1 }, - CreateElement { root: None, tag: "span", children: 1 }, - CreateTextNode { root: None, text: "a" }, - CloneNodeChildren { id: Some(5), new_ids: vec![6] }, - SetLastNode { id: 3 }, - ReplaceWith { root: None, nodes: vec![6] }, - CreatePlaceholder { root: Some(7) }, - SetLastNode { id: 4 }, - ReplaceWith { root: None, nodes: vec![7] }, - AppendChildren { root: Some(0), children: vec![2, 3, 4] } - ] - ) -} - -#[test] -fn child_components() { - static App: Component = |cx| { - cx.render(rsx!( - {true.then(|| rsx!(Child { }))} - {false.then(|| rsx!(Child { }))} - )) - }; - static Child: Component = |cx| { - cx.render(rsx!( - h1 {"hello"} - h1 {"goodbye"} - )) - }; - let mut vdom = VirtualDom::new(App); - let edits = vdom.rebuild(); - dbg!(edits); -} diff --git a/packages/fermi/src/hooks/atom_root.rs b/packages/fermi/src/hooks/atom_root.rs index f9eb07bc9..24fd70093 100644 --- a/packages/fermi/src/hooks/atom_root.rs +++ b/packages/fermi/src/hooks/atom_root.rs @@ -1,11 +1,12 @@ +use std::rc::Rc; + use crate::AtomRoot; use dioxus_core::ScopeState; -use std::rc::Rc; // Returns the atom root, initiaizing it at the root of the app if it does not exist. pub fn use_atom_root(cx: &ScopeState) -> &Rc { cx.use_hook(|| match cx.consume_context::>() { Some(root) => root, - None => cx.provide_root_context(Rc::new(AtomRoot::new(cx.schedule_update_any()))), + None => panic!("No atom root found in context. Did you forget place an AtomRoot component at the top of your app?"), }) } diff --git a/packages/hooks/src/lib.rs b/packages/hooks/src/lib.rs index 1790eff1d..9feac8f13 100644 --- a/packages/hooks/src/lib.rs +++ b/packages/hooks/src/lib.rs @@ -1,5 +1,28 @@ -// #![deny(missing_docs)] -//! Useful foundational hooks for Dioxus +#[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(); + )*} +} + +mod usecontext; +pub use usecontext::*; mod usestate; pub use usestate::{use_state, UseState}; @@ -7,8 +30,8 @@ pub use usestate::{use_state, UseState}; mod useref; pub use useref::*; -mod use_shared_state; -pub use use_shared_state::*; +// mod use_shared_state; +// pub use use_shared_state::*; mod usecoroutine; pub use usecoroutine::*; @@ -19,5 +42,5 @@ pub use usefuture::*; mod useeffect; pub use useeffect::*; -// mod usesuspense; -// pub use usesuspense::*; +mod usecallback; +pub use usecallback::*; diff --git a/packages/hooks/src/use_shared_state.rs b/packages/hooks/src/use_shared_state.rs index 9c4048b0e..ebb848143 100644 --- a/packages/hooks/src/use_shared_state.rs +++ b/packages/hooks/src/use_shared_state.rs @@ -63,7 +63,7 @@ impl ProvidedStateInner { pub fn use_context(cx: &ScopeState) -> Option> { let state = cx.use_hook(|| { let scope_id = cx.scope_id(); - let root = cx.consume_context::>(); + let root = cx.consume_context::>().cloned(); if let Some(root) = root.as_ref() { root.borrow_mut().consumers.insert(scope_id); @@ -179,6 +179,7 @@ pub fn use_context_provider(cx: &ScopeState, f: impl FnOnce() -> T) notify_any: cx.schedule_update_any(), consumers: HashSet::new(), })); - cx.provide_context(state) + + cx.provide_context(state); }); } diff --git a/packages/hooks/src/usecallback.rs b/packages/hooks/src/usecallback.rs new file mode 100644 index 000000000..3f223b581 --- /dev/null +++ b/packages/hooks/src/usecallback.rs @@ -0,0 +1,47 @@ +use dioxus_core::ScopeState; +use std::future::Future; + +#[macro_export] +macro_rules! use_callback { + // ($cx:ident, || || $($rest:tt)*) => { use_callback( $cx, (), |_| $($rest)* ) }; + // ($cx:ident, || || $($rest:tt)*) => { use_callback( $cx, (), |_| $($rest)* ) }; + ($cx:ident, || $($rest:tt)*) => { + use_callback( + $cx, + move || $($rest)* + ) + }; + ($cx:ident, |$($args:tt),* | $($rest:tt)*) => { + use_callback( + $cx, + move || $($rest)* + ) + }; + ($cx:ident, $($rest:tt)*) => { + use_callback( + $cx, + move || $($rest)* + ) + }; +} +pub fn use_callback(cx: &ScopeState, make: impl FnOnce() -> R) -> impl FnMut(T) + '_ +where + R: FnMut(T) -> F + 'static, + F: Future + 'static, +{ + let mut hook = make(); + + move |evt| cx.spawn(hook(evt)) +} + +fn _it_works(cx: &ScopeState) { + let _p = use_callback(cx, || { + |()| async { + // + } + }); + + // let p = use_callback!(cx, || |evt| async { + // // + // }); +} diff --git a/packages/hooks/src/usecontext.rs b/packages/hooks/src/usecontext.rs new file mode 100644 index 000000000..74f7cea93 --- /dev/null +++ b/packages/hooks/src/usecontext.rs @@ -0,0 +1,17 @@ +use dioxus_core::ScopeState; + +/// Consume some context in the tree, providing a sharable handle to the value +pub fn use_context(cx: &ScopeState) -> Option<&T> { + cx.use_hook(|| cx.consume_context::()).as_ref() +} + +/// Provide some context via the tree and return a reference to it +/// +/// Once the context has been provided, it is immutable. Mutations should be done via interior mutability. +pub fn use_context_provider(cx: &ScopeState, f: impl FnOnce() -> T) -> &T { + cx.use_hook(|| { + let val = f(); + cx.provide_context(val.clone()); + val + }) +} diff --git a/packages/hooks/src/usecoroutine.rs b/packages/hooks/src/usecoroutine.rs index 503c0e91a..ec9472022 100644 --- a/packages/hooks/src/usecoroutine.rs +++ b/packages/hooks/src/usecoroutine.rs @@ -2,6 +2,8 @@ use dioxus_core::{ScopeState, TaskId}; pub use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender}; use std::future::Future; +use crate::{use_context, use_context_provider}; + /// Maintain a handle over a future that can be paused, resumed, and canceled. /// /// This is an upgraded form of [`use_future`] with an integrated channel system. @@ -59,33 +61,33 @@ use std::future::Future; /// } /// }) /// ``` -pub fn use_coroutine(cx: &ScopeState, init: G) -> &CoroutineHandle +pub fn use_coroutine(cx: &ScopeState, init: G) -> &Coroutine where M: 'static, G: FnOnce(UnboundedReceiver) -> F, F: Future + 'static, { - cx.use_hook(|| { + use_context_provider(cx, || { let (tx, rx) = futures_channel::mpsc::unbounded(); let task = cx.push_future(init(rx)); - cx.provide_context(CoroutineHandle { tx, task }) + Coroutine { tx, task } }) } /// Get a handle to a coroutine higher in the tree /// /// See the docs for [`use_coroutine`] for more details. -pub fn use_coroutine_handle(cx: &ScopeState) -> Option<&CoroutineHandle> { - cx.use_hook(|| cx.consume_context::>()) - .as_ref() +pub fn use_coroutine_handle(cx: &ScopeState) -> Option<&Coroutine> { + use_context::>(cx) } -pub struct CoroutineHandle { +pub struct Coroutine { tx: UnboundedSender, task: TaskId, } -impl Clone for CoroutineHandle { +// for use in futures +impl Clone for Coroutine { fn clone(&self) -> Self { Self { tx: self.tx.clone(), @@ -94,7 +96,7 @@ impl Clone for CoroutineHandle { } } -impl CoroutineHandle { +impl Coroutine { /// Get the ID of this coroutine #[must_use] pub fn task_id(&self) -> TaskId { @@ -112,8 +114,8 @@ mod tests { #![allow(unused)] use super::*; - use dioxus_core::exports::futures_channel::mpsc::unbounded; use dioxus_core::prelude::*; + use futures_channel::mpsc::unbounded; use futures_util::StreamExt; fn app(cx: Scope, name: String) -> Element { @@ -127,7 +129,7 @@ mod tests { let task3 = use_coroutine(&cx, |rx| complex_task(rx, 10)); - None + todo!() } async fn view_task(mut rx: UnboundedReceiver) { diff --git a/packages/hooks/src/useeffect.rs b/packages/hooks/src/useeffect.rs index e82335222..dd062587f 100644 --- a/packages/hooks/src/useeffect.rs +++ b/packages/hooks/src/useeffect.rs @@ -86,7 +86,7 @@ mod tests { // }); - None + todo!() } } } diff --git a/packages/hooks/src/usefuture.rs b/packages/hooks/src/usefuture.rs index 1aa7800ec..df34a6433 100644 --- a/packages/hooks/src/usefuture.rs +++ b/packages/hooks/src/usefuture.rs @@ -1,6 +1,12 @@ #![allow(missing_docs)] use dioxus_core::{ScopeState, TaskId}; -use std::{any::Any, cell::Cell, future::Future, rc::Rc, sync::Arc}; +use std::{ + any::Any, + cell::{Cell, RefCell}, + future::{Future, IntoFuture}, + rc::Rc, + sync::Arc, +}; /// A future that resolves to a value. /// @@ -28,16 +34,13 @@ where let state = cx.use_hook(move || UseFuture { update: cx.schedule_update(), needs_regen: Cell::new(true), - slot: Rc::new(Cell::new(None)), - value: None, + values: Default::default(), task: Cell::new(None), dependencies: Vec::new(), + waker: Default::default(), }); - if let Some(value) = state.slot.take() { - state.value = Some(value); - state.task.set(None); - } + *state.waker.borrow_mut() = None; if dependencies.clone().apply(&mut state.dependencies) || state.needs_regen.get() { // We don't need regen anymore @@ -47,8 +50,9 @@ where let fut = future(dependencies.out()); // Clone in our cells - let slot = state.slot.clone(); + let values = state.values.clone(); let schedule_update = state.update.clone(); + let waker = state.waker.clone(); // Cancel the current future if let Some(current) = state.task.take() { @@ -57,8 +61,13 @@ where state.task.set(Some(cx.push_future(async move { let res = fut.await; - slot.set(Some(res)); - schedule_update(); + values.borrow_mut().push(Box::leak(Box::new(res))); + + // if there's a waker, we dont re-render the component. Instead we just progress that future + match waker.borrow().as_ref() { + Some(waker) => waker.wake_by_ref(), + None => schedule_update(), + } }))); } @@ -74,10 +83,10 @@ pub enum FutureState<'a, T> { pub struct UseFuture { update: Arc, needs_regen: Cell, - value: Option, - slot: Rc>>, task: Cell>, dependencies: Vec>, + waker: Rc>>, + values: Rc>>, } pub enum UseFutureState<'a, T> { @@ -105,22 +114,28 @@ impl UseFuture { // clears the value in the future slot without starting the future over pub fn clear(&self) -> Option { - (self.update)(); - self.slot.replace(None) + todo!() + // (self.update)(); + // self.slot.replace(None) } // Manually set the value in the future slot without starting the future over - pub fn set(&self, new_value: T) { - self.slot.set(Some(new_value)); - self.needs_regen.set(true); - (self.update)(); + pub fn set(&self, _new_value: T) { + // self.slot.set(Some(new_value)); + // self.needs_regen.set(true); + // (self.update)(); + todo!() } /// Return any value, even old values if the future has not yet resolved. /// /// If the future has never completed, the returned value will be `None`. pub fn value(&self) -> Option<&T> { - self.value.as_ref() + self.values + .borrow_mut() + .last() + .cloned() + .map(|x| unsafe { &*x }) } /// Get the ID of the future in Dioxus' internal scheduler @@ -130,18 +145,48 @@ impl UseFuture { /// Get the current stateof the future. pub fn state(&self) -> UseFutureState { - match (&self.task.get(), &self.value) { - // If we have a task and an existing value, we're reloading - (Some(_), Some(val)) => UseFutureState::Reloading(val), + todo!() + // match (&self.task.get(), &self.value) { + // // If we have a task and an existing value, we're reloading + // (Some(_), Some(val)) => UseFutureState::Reloading(val), - // no task, but value - we're done - (None, Some(val)) => UseFutureState::Complete(val), + // // no task, but value - we're done + // (None, Some(val)) => UseFutureState::Complete(val), - // no task, no value - something's wrong? return pending - (None, None) => UseFutureState::Pending, + // // no task, no value - something's wrong? return pending + // (None, None) => UseFutureState::Pending, - // Task, no value - we're still pending - (Some(_), None) => UseFutureState::Pending, + // // Task, no value - we're still pending + // (Some(_), None) => UseFutureState::Pending, + // } + } +} + +impl<'a, T> IntoFuture for &'a UseFuture { + type Output = &'a T; + type IntoFuture = UseFutureAwait<'a, T>; + fn into_future(self) -> Self::IntoFuture { + UseFutureAwait { hook: self } + } +} + +pub struct UseFutureAwait<'a, T> { + hook: &'a UseFuture, +} + +impl<'a, T> Future for UseFutureAwait<'a, T> { + type Output = &'a T; + + fn poll( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll { + match self.hook.values.borrow_mut().last().cloned() { + Some(value) => std::task::Poll::Ready(unsafe { &*value }), + None => { + self.hook.waker.replace(Some(cx.waker().clone())); + std::task::Poll::Pending + } } } } @@ -239,6 +284,19 @@ impl_dep!(A = a, B = b, C = c, D = d, E = e, F = f,); impl_dep!(A = a, B = b, C = c, D = d, E = e, F = f, G = g,); impl_dep!(A = a, B = b, C = c, D = d, E = e, F = f, G = g, H = h,); +/// A helper macro that merges uses the closure syntax to elaborate the dependency array +#[macro_export] +macro_rules! use_future { + ($cx:ident, || $($rest:tt)*) => { use_future( $cx, (), |_| $($rest)* ) }; + ($cx:ident, | $($args:tt),* | $($rest:tt)*) => { + use_future( + $cx, + ($($args),*), + |($($args),*)| $($rest)* + ) + }; +} + #[cfg(test)] mod tests { use super::*; @@ -256,23 +314,33 @@ mod tests { e: i32, } - fn app(cx: Scope) -> Element { + async fn app(cx: Scope<'_, MyProps>) -> Element { // should only ever run once - let fut = use_future(&cx, (), |_| async move { - // - }); + let fut = use_future(cx, (), |_| async move {}); // runs when a is changed - let fut = use_future(&cx, (&cx.props.a,), |(a,)| async move { - // - }); + let fut = use_future(cx, (&cx.props.a,), |(a,)| async move {}); // runs when a or b is changed - let fut = use_future(&cx, (&cx.props.a, &cx.props.b), |(a, b)| async move { - // + let fut = use_future(cx, (&cx.props.a, &cx.props.b), |(a, b)| async move { 123 }); + + let a = use_future!(cx, || async move { + // do the thing! }); - None + let b = &123; + let c = &123; + + let a = use_future!(cx, |b, c| async move { + let a = b + c; + let blah = "asd"; + }); + + let g2 = a.await; + + let g = fut.await; + + todo!() } } } diff --git a/packages/hooks/src/usestate.rs b/packages/hooks/src/usestate.rs index 78cf643b1..fe9f566bd 100644 --- a/packages/hooks/src/usestate.rs +++ b/packages/hooks/src/usestate.rs @@ -39,7 +39,7 @@ pub fn use_state( let update_callback = cx.schedule_update(); let slot = Rc::new(RefCell::new(current_val.clone())); let setter = Rc::new({ - dioxus_core::to_owned![update_callback, slot]; + to_owned![update_callback, slot]; move |new| { { let mut slot = slot.borrow_mut(); @@ -466,12 +466,14 @@ fn api_makes_sense() { } cx.spawn({ - dioxus_core::to_owned![val]; + to_owned![val]; async move { val.modify(|f| f + 1); } }); - cx.render(LazyNodes::new(|f| f.static_text("asd"))) + // cx.render(LazyNodes::new(|f| f.static_text("asd"))) + + todo!() } } diff --git a/packages/hooks/src/usesuspense.rs b/packages/hooks/src/usesuspense.rs deleted file mode 100644 index d4405bd88..000000000 --- a/packages/hooks/src/usesuspense.rs +++ /dev/null @@ -1,41 +0,0 @@ -use std::{cell::Cell, future::Future, rc::Rc}; - -use dioxus_core::{Element, ScopeState, TaskId}; - -pub fn use_suspense + 'static>( - cx: &ScopeState, - create_future: impl FnOnce() -> F, - render: impl FnOnce(&R) -> Element, -) -> Element { - let sus = cx.use_hook(|| { - let fut = create_future(); - - let wip_value: Rc>> = Default::default(); - - let wip = wip_value.clone(); - let new_fut = async move { - let val = fut.await; - wip.set(Some(val)); - }; - - let task = cx.push_future(new_fut); - SuspenseInner { - _task: task, - value: None, - _wip_value: wip_value, - } - }); - - if let Some(value) = sus.value.as_ref() { - render(value) - } else { - // generate a placeholder node if the future isnt ready - None - } -} - -struct SuspenseInner { - _task: TaskId, - _wip_value: Rc>>, - value: Option, -} diff --git a/packages/html/Cargo.toml b/packages/html/Cargo.toml index 8464189ea..446da9303 100644 --- a/packages/html/Cargo.toml +++ b/packages/html/Cargo.toml @@ -18,6 +18,7 @@ wasm-bindgen = { version = "0.2.79", optional = true } euclid = "0.22.7" enumset = "1.0.11" keyboard-types = "0.6.2" +async-trait = "0.1.58" [dependencies.web-sys] optional = true diff --git a/packages/html/src/elements.rs b/packages/html/src/elements.rs index 1b73dba80..efbb4d797 100644 --- a/packages/html/src/elements.rs +++ b/packages/html/src/elements.rs @@ -1,5 +1,8 @@ +#![allow(non_upper_case_globals)] + use crate::{GlobalAttributes, SvgAttributes}; -use dioxus_core::*; + +pub type AttributeDiscription = (&'static str, Option<&'static str>, bool); macro_rules! builder_constructors { ( @@ -18,23 +21,17 @@ macro_rules! builder_constructors { $(#[$attr])* pub struct $name; - impl DioxusElement for $name { - const TAG_NAME: &'static str = stringify!($name); - const NAME_SPACE: Option<&'static str> = None; + + impl $name { + pub const TAG_NAME: &'static str = stringify!($name); + pub const NAME_SPACE: Option<&'static str> = None; + + $( + pub const $fil: AttributeDiscription = (stringify!($fil), None, false); + )* } impl GlobalAttributes for $name {} - - impl $name { - $( - #[allow(non_upper_case_globals)] - pub const $fil: AttributeDiscription = AttributeDiscription{ - name: stringify!($fil), - namespace: None, - volatile: false - }; - )* - } )* }; @@ -49,22 +46,14 @@ macro_rules! builder_constructors { $(#[$attr])* pub struct $name; - impl DioxusElement for $name { - const TAG_NAME: &'static str = stringify!($name); - const NAME_SPACE: Option<&'static str> = Some($namespace); - } - impl SvgAttributes for $name {} impl $name { - $( + pub const TAG_NAME: &'static str = stringify!($name); + pub const NAME_SPACE: Option<&'static str> = Some($namespace); - #[allow(non_upper_case_globals)] - pub const $fil: AttributeDiscription = AttributeDiscription{ - name: stringify!($fil), - namespace: Some(stringify!($namespace)), - volatile: false - }; + $( + pub const $fil: AttributeDiscription = (stringify!($fil), Some(stringify!($namespace)), false) )* } )* @@ -1068,19 +1057,10 @@ impl input { /// - `time` /// - `url` /// - `week` - #[allow(non_upper_case_globals)] - pub const r#type: AttributeDiscription = AttributeDiscription { - name: "type", - namespace: None, - volatile: false, - }; - #[allow(non_upper_case_globals)] - pub const value: AttributeDiscription = AttributeDiscription { - name: "value", - namespace: None, - volatile: true, - }; + pub const r#type: AttributeDiscription = ("type", None, false); + + pub const value: AttributeDiscription = ("value", None, true); } /* @@ -1090,63 +1070,29 @@ volatile attributes impl script { // r#async: Bool, // r#type: String, // TODO could be an enum - #[allow(non_upper_case_globals)] - pub const r#type: AttributeDiscription = AttributeDiscription { - name: "type", - namespace: None, - volatile: false, - }; - #[allow(non_upper_case_globals)] - pub const r#script: AttributeDiscription = AttributeDiscription { - name: "script", - namespace: None, - volatile: false, - }; + pub const r#type: AttributeDiscription = ("type", None, false); + + pub const r#script: AttributeDiscription = ("script", None, false); } impl button { - #[allow(non_upper_case_globals)] - pub const r#type: AttributeDiscription = AttributeDiscription { - name: "type", - namespace: None, - volatile: false, - }; + pub const r#type: AttributeDiscription = ("type", None, false); } impl select { - #[allow(non_upper_case_globals)] - pub const value: AttributeDiscription = AttributeDiscription { - name: "value", - namespace: None, - volatile: true, - }; + pub const value: AttributeDiscription = ("value", None, true); } impl option { - #[allow(non_upper_case_globals)] - pub const selected: AttributeDiscription = AttributeDiscription { - name: "selected", - namespace: None, - volatile: true, - }; + pub const selected: AttributeDiscription = ("selected", None, true); } impl textarea { - #[allow(non_upper_case_globals)] - pub const value: AttributeDiscription = AttributeDiscription { - name: "value", - namespace: None, - volatile: true, - }; + pub const value: AttributeDiscription = ("value", None, true); } impl label { - #[allow(non_upper_case_globals)] - pub const r#for: AttributeDiscription = AttributeDiscription { - name: "for", - namespace: None, - volatile: false, - }; + pub const r#for: AttributeDiscription = ("for", None, false); } builder_constructors! { diff --git a/packages/html/src/events.rs b/packages/html/src/events.rs index 8fa7df2cd..29a535107 100644 --- a/packages/html/src/events.rs +++ b/packages/html/src/events.rs @@ -1,1487 +1,64 @@ -use bumpalo::boxed::Box as BumpBox; -use dioxus_core::exports::bumpalo; -use dioxus_core::*; +mod keys; +pub use keys::*; -pub mod on { - //! Input events and associated data - - use crate::geometry::{ - ClientPoint, Coordinates, ElementPoint, LinesVector, PagePoint, PagesVector, PixelsVector, - ScreenPoint, WheelDelta, - }; - use crate::input_data::{ - decode_key_location, decode_mouse_button_set, encode_key_location, encode_mouse_button_set, - MouseButton, MouseButtonSet, - }; - use euclid::UnknownUnit; - use keyboard_types::{Code, Key, Location, Modifiers}; - use std::collections::HashMap; - use std::convert::TryInto; - use std::fmt::{Debug, Formatter}; - use std::str::FromStr; - - use super::*; - macro_rules! event_directory { - ( $( +macro_rules! impl_event { + ( + $data:ty; + $( $( #[$attr:meta] )* - $wrapper:ident($data:ident): [ - $( - $( #[$method_attr:meta] )* - $name:ident - )* - ]; - )* ) => { - $( - $( - $(#[$method_attr])* - pub fn $name<'a>( - factory: NodeFactory<'a>, - mut callback: impl FnMut($wrapper) + 'a, - // mut callback: impl FnMut(UiEvent<$data>) + 'a, - ) -> Listener<'a> - { - let bump = &factory.bump(); - - - use dioxus_core::{AnyEvent}; - // we can't allocate unsized in bumpalo's box, so we need to craft the box manually - // safety: this is essentially the same as calling Box::new() but manually - // The box is attached to the lifetime of the bumpalo allocator - let cb: &mut dyn FnMut(AnyEvent) = bump.alloc(move |evt: AnyEvent| { - let event = evt.downcast::<$data>().unwrap(); - callback(event) - }); - - let callback: BumpBox = unsafe { BumpBox::from_raw(cb) }; - - // ie oncopy - let event_name = stringify!($name); - - // ie copy - let shortname: &'static str = &event_name[2..]; - - let handler = bump.alloc(std::cell::RefCell::new(Some(callback))); - factory.listener(shortname, handler) - } - )* - )* - }; - } - - // The Dioxus Synthetic event system - // todo: move these into the html event system. dioxus accepts *any* event, so having these here doesn't make sense. - event_directory! { - ClipboardEvent(ClipboardData): [ - /// Called when "copy" - oncopy - - /// oncut - oncut - - /// onpaste - onpaste - ]; - - CompositionEvent(CompositionData): [ - /// oncompositionend - oncompositionend - - /// oncompositionstart - oncompositionstart - - /// oncompositionupdate - oncompositionupdate - ]; - - KeyboardEvent(KeyboardData): [ - /// onkeydown - onkeydown - - /// onkeypress - onkeypress - - /// onkeyup - onkeyup - ]; - - FocusEvent(FocusData): [ - /// onfocus - onfocus - - // onfocusout - onfocusout - - // onfocusin - onfocusin - - /// onblur - onblur - ]; - - FormEvent(FormData): [ - /// onchange - onchange - - /// oninput handler - oninput - - /// oninvalid - oninvalid - - /// onreset - onreset - - /// onsubmit - onsubmit - ]; - - /// A synthetic event that wraps a web-style [`MouseEvent`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) - /// - /// - /// The MouseEvent interface represents events that occur due to the user interacting with a pointing device (such as a mouse). - /// - /// ## Trait implementation: - /// ```rust, ignore - /// fn alt_key(&self) -> bool; - /// fn button(&self) -> i16; - /// fn buttons(&self) -> u16; - /// fn client_x(&self) -> i32; - /// fn client_y(&self) -> i32; - /// fn ctrl_key(&self) -> bool; - /// fn meta_key(&self) -> bool; - /// fn page_x(&self) -> i32; - /// fn page_y(&self) -> i32; - /// fn screen_x(&self) -> i32; - /// fn screen_y(&self) -> i32; - /// fn shift_key(&self) -> bool; - /// fn get_modifier_state(&self, key_code: &str) -> bool; - /// ``` - /// - /// ## Event Handlers - /// - [`onclick`] - /// - [`oncontextmenu`] - /// - [`ondoubleclick`] - /// - [`ondrag`] - /// - [`ondragend`] - /// - [`ondragenter`] - /// - [`ondragexit`] - /// - [`ondragleave`] - /// - [`ondragover`] - /// - [`ondragstart`] - /// - [`ondrop`] - /// - [`onmousedown`] - /// - [`onmouseenter`] - /// - [`onmouseleave`] - /// - [`onmousemove`] - /// - [`onmouseout`] - /// - [`onmouseover`] - /// - [`onmouseup`] - MouseEvent(MouseData): [ - /// Execute a callback when a button is clicked. - /// - /// ## Description - /// - /// An element receives a click event when a pointing device button (such as a mouse's primary mouse button) - /// is both pressed and released while the pointer is located inside the element. - /// - /// - Bubbles: Yes - /// - Cancelable: Yes - /// - Interface(InteData): [`MouseEvent`] - /// - /// If the button is pressed on one element and the pointer is moved outside the element before the button - /// is released, the event is fired on the most specific ancestor element that contained both elements. - /// `click` fires after both the `mousedown` and `mouseup` events have fired, in that order. - /// - /// ## Example - /// ```rust, ignore - /// rsx!( button { "click me", onclick: move |_| log::info!("Clicked!`") } ) - /// ``` - /// - /// ## Reference - /// - - /// - - onclick - - /// oncontextmenu - oncontextmenu - - /// ondoubleclick - ondoubleclick - - /// ondoubleclick - ondblclick - - /// ondrag - ondrag - - /// ondragend - ondragend - - /// ondragenter - ondragenter - - /// ondragexit - ondragexit - - /// ondragleave - ondragleave - - /// ondragover - ondragover - - /// ondragstart - ondragstart - - /// ondrop - ondrop - - /// onmousedown - onmousedown - - /// onmouseenter - onmouseenter - - /// onmouseleave - onmouseleave - - /// onmousemove - onmousemove - - /// onmouseout - onmouseout - - /// onmouseover - /// - /// Triggered when the users's mouse hovers over an element. - onmouseover - - /// onmouseup - onmouseup - ]; - - ScrollEvent(ScrollData): [ - /// - onscroll - ]; - - PointerEvent(PointerData): [ - /// pointerdown - onpointerdown - - /// pointermove - onpointermove - - /// pointerup - onpointerup - - /// pointercancel - onpointercancel - - /// gotpointercapture - ongotpointercapture - - /// lostpointercapture - onlostpointercapture - - /// pointerenter - onpointerenter - - /// pointerleave - onpointerleave - - /// pointerover - onpointerover - - /// pointerout - onpointerout - ]; - - SelectionEvent(SelectionData): [ - /// onselect - onselect - ]; - - TouchEvent(TouchData): [ - /// ontouchcancel - ontouchcancel - - /// ontouchend - ontouchend - - /// ontouchmove - ontouchmove - - /// ontouchstart - ontouchstart - ]; - - WheelEvent(WheelData): [ - /// - onwheel - ]; - - MediaEvent(MediaData): [ - ///abort - onabort - - ///canplay - oncanplay - - ///canplaythrough - oncanplaythrough - - ///durationchange - ondurationchange - - ///emptied - onemptied - - ///encrypted - onencrypted - - ///ended - onended - - ///error - onerror - - ///loadeddata - onloadeddata - - ///loadedmetadata - onloadedmetadata - - ///loadstart - onloadstart - - ///pause - onpause - - ///play - onplay - - ///playing - onplaying - - ///progress - onprogress - - ///ratechange - onratechange - - ///seeked - onseeked - - ///seeking - onseeking - - ///stalled - onstalled - - ///suspend - onsuspend - - ///timeupdate - ontimeupdate - - ///volumechange - onvolumechange - - ///waiting - onwaiting - ]; - - AnimationEvent(AnimationData): [ - /// onanimationstart - onanimationstart - - /// onanimationend - onanimationend - - /// onanimationiteration - onanimationiteration - ]; - - TransitionEvent(TransitionData): [ - /// - ontransitionend - ]; - - ToggleEvent(ToggleData): [ - /// - ontoggle - ]; - } - - pub type ClipboardEvent = UiEvent; - #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] - #[derive(Debug, Clone)] - pub struct ClipboardData { - // DOMDataTransfer clipboardData - } - - pub type CompositionEvent = UiEvent; - #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] - #[derive(Debug, Clone)] - pub struct CompositionData { - pub data: String, - } - - pub type KeyboardEvent = UiEvent; - #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] - #[derive(Clone)] - pub struct KeyboardData { - #[deprecated( - since = "0.3.0", - note = "This may not work in all environments. Use key() instead." - )] - pub char_code: u32, - - /// Identify which "key" was entered. - #[deprecated(since = "0.3.0", note = "use key() instead")] - pub key: String, - - /// Get the key code as an enum Variant. - #[deprecated( - since = "0.3.0", - note = "This may not work in all environments. Use code() instead." - )] - pub key_code: KeyCode, - - /// the physical key on the keyboard - code: Code, - - /// Indicate if the `alt` modifier key was pressed during this keyboard event - #[deprecated(since = "0.3.0", note = "use modifiers() instead")] - pub alt_key: bool, - - /// Indicate if the `ctrl` modifier key was pressed during this keyboard event - #[deprecated(since = "0.3.0", note = "use modifiers() instead")] - pub ctrl_key: bool, - - /// Indicate if the `meta` modifier key was pressed during this keyboard event - #[deprecated(since = "0.3.0", note = "use modifiers() instead")] - pub meta_key: bool, - - /// Indicate if the `shift` modifier key was pressed during this keyboard event - #[deprecated(since = "0.3.0", note = "use modifiers() instead")] - pub shift_key: bool, - - #[deprecated(since = "0.3.0", note = "use location() instead")] - pub location: usize, - - #[deprecated(since = "0.3.0", note = "use is_auto_repeating() instead")] - pub repeat: bool, - - #[deprecated(since = "0.3.0", note = "use code() or key() instead")] - pub which: usize, - } - - impl KeyboardData { - pub fn new( - key: Key, - code: Code, - location: Location, - is_auto_repeating: bool, - modifiers: Modifiers, - ) -> Self { - #[allow(deprecated)] - KeyboardData { - char_code: key.legacy_charcode(), - key: key.to_string(), - key_code: KeyCode::from_raw_code( - key.legacy_keycode() - .try_into() - .expect("could not convert keycode to u8"), - ), - code, - alt_key: modifiers.contains(Modifiers::ALT), - ctrl_key: modifiers.contains(Modifiers::CONTROL), - meta_key: modifiers.contains(Modifiers::META), - shift_key: modifiers.contains(Modifiers::SHIFT), - location: encode_key_location(location), - repeat: is_auto_repeating, - which: key - .legacy_charcode() - .try_into() - .expect("could not convert charcode to usize"), - } - } - - /// The value of the key pressed by the user, taking into consideration the state of modifier keys such as Shift as well as the keyboard locale and layout. - pub fn key(&self) -> Key { - #[allow(deprecated)] - FromStr::from_str(&self.key).expect("could not parse") - } - - /// A physical key on the keyboard (as opposed to the character generated by pressing the key). In other words, this property returns a value that isn't altered by keyboard layout or the state of the modifier keys. - pub fn code(&self) -> Code { - self.code - } - - /// The set of modifier keys which were pressed when the event occurred - pub fn modifiers(&self) -> Modifiers { - let mut modifiers = Modifiers::empty(); - - #[allow(deprecated)] - { - if self.alt_key { - modifiers.insert(Modifiers::ALT); - } - if self.ctrl_key { - modifiers.insert(Modifiers::CONTROL); - } - if self.meta_key { - modifiers.insert(Modifiers::META); - } - if self.shift_key { - modifiers.insert(Modifiers::SHIFT); + $name:ident + )* + ) => { + $( + $( #[$attr] )* + pub fn $name<'a>(_cx: &'a ::dioxus_core::ScopeState, _f: impl FnMut(::dioxus_core::Event<$data>) + 'a) -> ::dioxus_core::Attribute<'a> { + ::dioxus_core::Attribute { + name: stringify!($name), + value: _cx.listener(_f), + namespace: None, + mounted_element: Default::default(), + volatile: false, } } - - modifiers - } - - /// The location of the key on the keyboard or other input device. - pub fn location(&self) -> Location { - #[allow(deprecated)] - decode_key_location(self.location) - } - - /// `true` iff the key is being held down such that it is automatically repeating. - pub fn is_auto_repeating(&self) -> bool { - #[allow(deprecated)] - self.repeat - } - } - - impl Debug for KeyboardData { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("KeyboardData") - .field("key", &self.key()) - .field("code", &self.code()) - .field("modifiers", &self.modifiers()) - .field("location", &self.location()) - .field("is_auto_repeating", &self.is_auto_repeating()) - .finish() - } - } - - pub type FocusEvent = UiEvent; - #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] - #[derive(Debug, Clone)] - pub struct FocusData {/* DOMEventInner: Send + SyncTarget relatedTarget */} - - pub type FormEvent = UiEvent; - #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] - #[derive(Debug, Clone)] - pub struct FormData { - pub value: String, - pub values: HashMap, - /* DOMEvent: Send + SyncTarget relatedTarget */ - } - - pub type MouseEvent = UiEvent; - #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] - #[derive(Clone)] - /// Data associated with a mouse event - /// - /// Do not use the deprecated fields; they may change or become private in the future. - pub struct MouseData { - /// True if the alt key was down when the mouse event was fired. - #[deprecated(since = "0.3.0", note = "use modifiers() instead")] - pub alt_key: bool, - /// The button number that was pressed (if applicable) when the mouse event was fired. - #[deprecated(since = "0.3.0", note = "use trigger_button() instead")] - pub button: i16, - /// Indicates which buttons are pressed on the mouse (or other input device) when a mouse event is triggered. - /// - /// Each button that can be pressed is represented by a given number (see below). If more than one button is pressed, the button values are added together to produce a new number. For example, if the secondary (2) and auxiliary (4) buttons are pressed simultaneously, the value is 6 (i.e., 2 + 4). - /// - /// - 1: Primary button (usually the left button) - /// - 2: Secondary button (usually the right button) - /// - 4: Auxiliary button (usually the mouse wheel button or middle button) - /// - 8: 4th button (typically the "Browser Back" button) - /// - 16 : 5th button (typically the "Browser Forward" button) - #[deprecated(since = "0.3.0", note = "use held_buttons() instead")] - pub buttons: u16, - /// The horizontal coordinate within the application's viewport at which the event occurred (as opposed to the coordinate within the page). - /// - /// For example, clicking on the left edge of the viewport will always result in a mouse event with a clientX value of 0, regardless of whether the page is scrolled horizontally. - #[deprecated(since = "0.3.0", note = "use client_coordinates() instead")] - pub client_x: i32, - /// The vertical coordinate within the application's viewport at which the event occurred (as opposed to the coordinate within the page). - /// - /// For example, clicking on the top edge of the viewport will always result in a mouse event with a clientY value of 0, regardless of whether the page is scrolled vertically. - #[deprecated(since = "0.3.0", note = "use client_coordinates() instead")] - pub client_y: i32, - /// True if the control key was down when the mouse event was fired. - #[deprecated(since = "0.3.0", note = "use modifiers() instead")] - pub ctrl_key: bool, - /// True if the meta key was down when the mouse event was fired. - #[deprecated(since = "0.3.0", note = "use modifiers() instead")] - pub meta_key: bool, - /// The offset in the X coordinate of the mouse pointer between that event and the padding edge of the target node. - #[deprecated(since = "0.3.0", note = "use element_coordinates() instead")] - pub offset_x: i32, - /// The offset in the Y coordinate of the mouse pointer between that event and the padding edge of the target node. - #[deprecated(since = "0.3.0", note = "use element_coordinates() instead")] - pub offset_y: i32, - /// The X (horizontal) coordinate (in pixels) of the mouse, relative to the left edge of the entire document. This includes any portion of the document not currently visible. - /// - /// Being based on the edge of the document as it is, this property takes into account any horizontal scrolling of the page. For example, if the page is scrolled such that 200 pixels of the left side of the document are scrolled out of view, and the mouse is clicked 100 pixels inward from the left edge of the view, the value returned by pageX will be 300. - #[deprecated(since = "0.3.0", note = "use page_coordinates() instead")] - pub page_x: i32, - /// The Y (vertical) coordinate in pixels of the event relative to the whole document. - /// - /// See `page_x`. - #[deprecated(since = "0.3.0", note = "use page_coordinates() instead")] - pub page_y: i32, - /// The X coordinate of the mouse pointer in global (screen) coordinates. - #[deprecated(since = "0.3.0", note = "use screen_coordinates() instead")] - pub screen_x: i32, - /// The Y coordinate of the mouse pointer in global (screen) coordinates. - #[deprecated(since = "0.3.0", note = "use screen_coordinates() instead")] - pub screen_y: i32, - /// True if the shift key was down when the mouse event was fired. - #[deprecated(since = "0.3.0", note = "use modifiers() instead")] - pub shift_key: bool, - // fn get_modifier_state(&self, key_code: &str) -> bool; - } - - impl MouseData { - /// Construct MouseData with the specified properties - /// - /// Note: the current implementation truncates coordinates. In the future, when we change the internal representation, it may also support a fractional part. - pub fn new( - coordinates: Coordinates, - trigger_button: Option, - held_buttons: MouseButtonSet, - modifiers: Modifiers, - ) -> Self { - let alt_key = modifiers.contains(Modifiers::ALT); - let ctrl_key = modifiers.contains(Modifiers::CONTROL); - let meta_key = modifiers.contains(Modifiers::META); - let shift_key = modifiers.contains(Modifiers::SHIFT); - - let [client_x, client_y]: [i32; 2] = coordinates.client().cast().into(); - let [offset_x, offset_y]: [i32; 2] = coordinates.element().cast().into(); - let [page_x, page_y]: [i32; 2] = coordinates.page().cast().into(); - let [screen_x, screen_y]: [i32; 2] = coordinates.screen().cast().into(); - - #[allow(deprecated)] - Self { - alt_key, - ctrl_key, - meta_key, - shift_key, - - button: trigger_button.map_or(0, |b| b.into_web_code()), - buttons: encode_mouse_button_set(held_buttons), - - client_x, - client_y, - offset_x, - offset_y, - page_x, - page_y, - screen_x, - screen_y, - } - } - - /// The event's coordinates relative to the application's viewport (as opposed to the coordinate within the page). - /// - /// For example, clicking in the top left corner of the viewport will always result in a mouse event with client coordinates (0., 0.), regardless of whether the page is scrolled horizontally. - pub fn client_coordinates(&self) -> ClientPoint { - #[allow(deprecated)] - ClientPoint::new(self.client_x.into(), self.client_y.into()) - } - - /// The event's coordinates relative to the padding edge of the target element - /// - /// For example, clicking in the top left corner of an element will result in element coordinates (0., 0.) - pub fn element_coordinates(&self) -> ElementPoint { - #[allow(deprecated)] - ElementPoint::new(self.offset_x.into(), self.offset_y.into()) - } - - /// The event's coordinates relative to the entire document. This includes any portion of the document not currently visible. - /// - /// For example, if the page is scrolled 200 pixels to the right and 300 pixels down, clicking in the top left corner of the viewport would result in page coordinates (200., 300.) - pub fn page_coordinates(&self) -> PagePoint { - #[allow(deprecated)] - PagePoint::new(self.page_x.into(), self.page_y.into()) - } - - /// The event's coordinates relative to the entire screen. This takes into account the window's offset. - pub fn screen_coordinates(&self) -> ScreenPoint { - #[allow(deprecated)] - ScreenPoint::new(self.screen_x.into(), self.screen_y.into()) - } - - pub fn coordinates(&self) -> Coordinates { - Coordinates::new( - self.screen_coordinates(), - self.client_coordinates(), - self.element_coordinates(), - self.page_coordinates(), - ) - } - - /// The set of modifier keys which were pressed when the event occurred - pub fn modifiers(&self) -> Modifiers { - let mut modifiers = Modifiers::empty(); - - #[allow(deprecated)] - { - if self.alt_key { - modifiers.insert(Modifiers::ALT); - } - if self.ctrl_key { - modifiers.insert(Modifiers::CONTROL); - } - if self.meta_key { - modifiers.insert(Modifiers::META); - } - if self.shift_key { - modifiers.insert(Modifiers::SHIFT); - } - } - - modifiers - } - - /// The set of mouse buttons which were held when the event occurred. - pub fn held_buttons(&self) -> MouseButtonSet { - #[allow(deprecated)] - decode_mouse_button_set(self.buttons) - } - - /// The mouse button that triggered the event - /// - // todo the following is kind of bad; should we just return None when the trigger_button is unreliable (and frankly irrelevant)? i guess we would need the event_type here - /// This is only guaranteed to indicate which button was pressed during events caused by pressing or releasing a button. As such, it is not reliable for events such as mouseenter, mouseleave, mouseover, mouseout, or mousemove. For example, a value of MouseButton::Primary may also indicate that no button was pressed. - pub fn trigger_button(&self) -> Option { - #[allow(deprecated)] - Some(MouseButton::from_web_code(self.button)) - } - } - - impl Debug for MouseData { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("MouseData") - .field("coordinates", &self.coordinates()) - .field("modifiers", &self.modifiers()) - .field("held_buttons", &self.held_buttons()) - .field("trigger_button", &self.trigger_button()) - .finish() - } - } - - pub type PointerEvent = UiEvent; - #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] - #[derive(Debug, Clone)] - pub struct PointerData { - // Mouse only - pub alt_key: bool, - pub button: i16, - pub buttons: u16, - pub client_x: i32, - pub client_y: i32, - pub ctrl_key: bool, - pub meta_key: bool, - pub page_x: i32, - pub page_y: i32, - pub screen_x: i32, - pub screen_y: i32, - pub shift_key: bool, - pub pointer_id: i32, - pub width: i32, - pub height: i32, - pub pressure: f32, - pub tangential_pressure: f32, - pub tilt_x: i32, - pub tilt_y: i32, - pub twist: i32, - pub pointer_type: String, - pub is_primary: bool, - // pub get_modifier_state: bool, - } - - pub type SelectionEvent = UiEvent; - #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] - #[derive(Debug, Clone)] - pub struct SelectionData {} - - pub type TouchEvent = UiEvent; - #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] - #[derive(Debug, Clone)] - pub struct TouchData { - pub alt_key: bool, - pub ctrl_key: bool, - pub meta_key: bool, - pub shift_key: bool, - // get_modifier_state: bool, - // changedTouches: DOMTouchList, - // targetTouches: DOMTouchList, - // touches: DOMTouchList, - } - - pub type WheelEvent = UiEvent; - #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] - #[derive(Clone)] - pub struct WheelData { - #[deprecated(since = "0.3.0", note = "use delta() instead")] - pub delta_mode: u32, - #[deprecated(since = "0.3.0", note = "use delta() instead")] - pub delta_x: f64, - #[deprecated(since = "0.3.0", note = "use delta() instead")] - pub delta_y: f64, - #[deprecated(since = "0.3.0", note = "use delta() instead")] - pub delta_z: f64, - } - - impl WheelData { - /// Construct a new WheelData with the specified wheel movement delta - pub fn new(delta: WheelDelta) -> Self { - let (delta_mode, vector) = match delta { - WheelDelta::Pixels(v) => (0, v.cast_unit::()), - WheelDelta::Lines(v) => (1, v.cast_unit::()), - WheelDelta::Pages(v) => (2, v.cast_unit::()), - }; - - #[allow(deprecated)] - WheelData { - delta_mode, - delta_x: vector.x, - delta_y: vector.y, - delta_z: vector.z, - } - } - - /// Construct from the attributes of the web wheel event - pub fn from_web_attributes( - delta_mode: u32, - delta_x: f64, - delta_y: f64, - delta_z: f64, - ) -> Self { - #[allow(deprecated)] - Self { - delta_mode, - delta_x, - delta_y, - delta_z, - } - } - - /// The amount of wheel movement - #[allow(deprecated)] - pub fn delta(&self) -> WheelDelta { - let x = self.delta_x; - let y = self.delta_y; - let z = self.delta_z; - match self.delta_mode { - 0 => WheelDelta::Pixels(PixelsVector::new(x, y, z)), - 1 => WheelDelta::Lines(LinesVector::new(x, y, z)), - 2 => WheelDelta::Pages(PagesVector::new(x, y, z)), - _ => panic!("Invalid delta mode, {:?}", self.delta_mode), - } - } - } - - impl Debug for WheelData { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("WheelData") - .field("delta", &self.delta()) - .finish() - } - } - - pub type ScrollEvent = UiEvent; - #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] - #[derive(Debug, Clone)] - pub struct ScrollData {} - - pub type MediaEvent = UiEvent; - #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] - #[derive(Debug, Clone)] - pub struct MediaData {} - - pub type ImageEvent = UiEvent; - #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] - #[derive(Debug, Clone)] - pub struct ImageData { - pub load_error: bool, - } - - pub type AnimationEvent = UiEvent; - #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] - #[derive(Debug, Clone)] - pub struct AnimationData { - pub animation_name: String, - pub pseudo_element: String, - pub elapsed_time: f32, - } - - pub type TransitionEvent = UiEvent; - #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] - #[derive(Debug, Clone)] - pub struct TransitionData { - pub property_name: String, - pub pseudo_element: String, - pub elapsed_time: f32, - } - - pub type ToggleEvent = UiEvent; - #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] - #[derive(Debug, Clone)] - pub struct ToggleData {} + )* + }; } -#[cfg_attr( - feature = "serialize", - derive(serde_repr::Serialize_repr, serde_repr::Deserialize_repr) -)] -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -#[repr(u8)] -pub enum KeyCode { - // That key has no keycode, = 0 - // break, = 3 - // backspace / delete, = 8 - // tab, = 9 - // clear, = 12 - // enter, = 13 - // shift, = 16 - // ctrl, = 17 - // alt, = 18 - // pause/break, = 19 - // caps lock, = 20 - // hangul, = 21 - // hanja, = 25 - // escape, = 27 - // conversion, = 28 - // non-conversion, = 29 - // spacebar, = 32 - // page up, = 33 - // page down, = 34 - // end, = 35 - // home, = 36 - // left arrow, = 37 - // up arrow, = 38 - // right arrow, = 39 - // down arrow, = 40 - // select, = 41 - // print, = 42 - // execute, = 43 - // Print Screen, = 44 - // insert, = 45 - // delete, = 46 - // help, = 47 - // 0, = 48 - // 1, = 49 - // 2, = 50 - // 3, = 51 - // 4, = 52 - // 5, = 53 - // 6, = 54 - // 7, = 55 - // 8, = 56 - // 9, = 57 - // :, = 58 - // semicolon (firefox), equals, = 59 - // <, = 60 - // equals (firefox), = 61 - // ß, = 63 - // @ (firefox), = 64 - // a, = 65 - // b, = 66 - // c, = 67 - // d, = 68 - // e, = 69 - // f, = 70 - // g, = 71 - // h, = 72 - // i, = 73 - // j, = 74 - // k, = 75 - // l, = 76 - // m, = 77 - // n, = 78 - // o, = 79 - // p, = 80 - // q, = 81 - // r, = 82 - // s, = 83 - // t, = 84 - // u, = 85 - // v, = 86 - // w, = 87 - // x, = 88 - // y, = 89 - // z, = 90 - // Windows Key / Left ⌘ / Chromebook Search key, = 91 - // right window key, = 92 - // Windows Menu / Right ⌘, = 93 - // sleep, = 95 - // numpad 0, = 96 - // numpad 1, = 97 - // numpad 2, = 98 - // numpad 3, = 99 - // numpad 4, = 100 - // numpad 5, = 101 - // numpad 6, = 102 - // numpad 7, = 103 - // numpad 8, = 104 - // numpad 9, = 105 - // multiply, = 106 - // add, = 107 - // numpad period (firefox), = 108 - // subtract, = 109 - // decimal point, = 110 - // divide, = 111 - // f1, = 112 - // f2, = 113 - // f3, = 114 - // f4, = 115 - // f5, = 116 - // f6, = 117 - // f7, = 118 - // f8, = 119 - // f9, = 120 - // f10, = 121 - // f11, = 122 - // f12, = 123 - // f13, = 124 - // f14, = 125 - // f15, = 126 - // f16, = 127 - // f17, = 128 - // f18, = 129 - // f19, = 130 - // f20, = 131 - // f21, = 132 - // f22, = 133 - // f23, = 134 - // f24, = 135 - // f25, = 136 - // f26, = 137 - // f27, = 138 - // f28, = 139 - // f29, = 140 - // f30, = 141 - // f31, = 142 - // f32, = 143 - // num lock, = 144 - // scroll lock, = 145 - // airplane mode, = 151 - // ^, = 160 - // !, = 161 - // ؛ (arabic semicolon), = 162 - // #, = 163 - // $, = 164 - // ù, = 165 - // page backward, = 166 - // page forward, = 167 - // refresh, = 168 - // closing paren (AZERTY), = 169 - // *, = 170 - // ~ + * key, = 171 - // home key, = 172 - // minus (firefox), mute/unmute, = 173 - // decrease volume level, = 174 - // increase volume level, = 175 - // next, = 176 - // previous, = 177 - // stop, = 178 - // play/pause, = 179 - // e-mail, = 180 - // mute/unmute (firefox), = 181 - // decrease volume level (firefox), = 182 - // increase volume level (firefox), = 183 - // semi-colon / ñ, = 186 - // equal sign, = 187 - // comma, = 188 - // dash, = 189 - // period, = 190 - // forward slash / ç, = 191 - // grave accent / ñ / æ / ö, = 192 - // ?, / or °, = 193 - // numpad period (chrome), = 194 - // open bracket, = 219 - // back slash, = 220 - // close bracket / å, = 221 - // single quote / ø / ä, = 222 - // `, = 223 - // left or right ⌘ key (firefox), = 224 - // altgr, = 225 - // < /git >, left back slash, = 226 - // GNOME Compose Key, = 230 - // ç, = 231 - // XF86Forward, = 233 - // XF86Back, = 234 - // non-conversion, = 235 - // alphanumeric, = 240 - // hiragana/katakana, = 242 - // half-width/full-width, = 243 - // kanji, = 244 - // unlock trackpad (Chrome/Edge), = 251 - // toggle touchpad, = 255 - NA = 0, - Break = 3, - Backspace = 8, - Tab = 9, - Clear = 12, - Enter = 13, - Shift = 16, - Ctrl = 17, - Alt = 18, - Pause = 19, - CapsLock = 20, - // hangul, = 21 - // hanja, = 25 - Escape = 27, - // conversion, = 28 - // non-conversion, = 29 - Space = 32, - PageUp = 33, - PageDown = 34, - End = 35, - Home = 36, - LeftArrow = 37, - UpArrow = 38, - RightArrow = 39, - DownArrow = 40, - // select, = 41 - // print, = 42 - // execute, = 43 - // Print Screen, = 44 - Insert = 45, - Delete = 46, - // help, = 47 - Num0 = 48, - Num1 = 49, - Num2 = 50, - Num3 = 51, - Num4 = 52, - Num5 = 53, - Num6 = 54, - Num7 = 55, - Num8 = 56, - Num9 = 57, - // :, = 58 - // semicolon (firefox), equals, = 59 - // <, = 60 - // equals (firefox), = 61 - // ß, = 63 - // @ (firefox), = 64 - A = 65, - B = 66, - C = 67, - D = 68, - E = 69, - F = 70, - G = 71, - H = 72, - I = 73, - J = 74, - K = 75, - L = 76, - M = 77, - N = 78, - O = 79, - P = 80, - Q = 81, - R = 82, - S = 83, - T = 84, - U = 85, - V = 86, - W = 87, - X = 88, - Y = 89, - Z = 90, - LeftWindow = 91, - RightWindow = 92, - SelectKey = 93, - Numpad0 = 96, - Numpad1 = 97, - Numpad2 = 98, - Numpad3 = 99, - Numpad4 = 100, - Numpad5 = 101, - Numpad6 = 102, - Numpad7 = 103, - Numpad8 = 104, - Numpad9 = 105, - Multiply = 106, - Add = 107, - Subtract = 109, - DecimalPoint = 110, - Divide = 111, - F1 = 112, - F2 = 113, - F3 = 114, - F4 = 115, - F5 = 116, - F6 = 117, - F7 = 118, - F8 = 119, - F9 = 120, - F10 = 121, - F11 = 122, - F12 = 123, - // f13, = 124 - // f14, = 125 - // f15, = 126 - // f16, = 127 - // f17, = 128 - // f18, = 129 - // f19, = 130 - // f20, = 131 - // f21, = 132 - // f22, = 133 - // f23, = 134 - // f24, = 135 - // f25, = 136 - // f26, = 137 - // f27, = 138 - // f28, = 139 - // f29, = 140 - // f30, = 141 - // f31, = 142 - // f32, = 143 - NumLock = 144, - ScrollLock = 145, - // airplane mode, = 151 - // ^, = 160 - // !, = 161 - // ؛ (arabic semicolon), = 162 - // #, = 163 - // $, = 164 - // ù, = 165 - // page backward, = 166 - // page forward, = 167 - // refresh, = 168 - // closing paren (AZERTY), = 169 - // *, = 170 - // ~ + * key, = 171 - // home key, = 172 - // minus (firefox), mute/unmute, = 173 - // decrease volume level, = 174 - // increase volume level, = 175 - // next, = 176 - // previous, = 177 - // stop, = 178 - // play/pause, = 179 - // e-mail, = 180 - // mute/unmute (firefox), = 181 - // decrease volume level (firefox), = 182 - // increase volume level (firefox), = 183 - Semicolon = 186, - EqualSign = 187, - Comma = 188, - Dash = 189, - Period = 190, - ForwardSlash = 191, - GraveAccent = 192, - // ?, / or °, = 193 - // numpad period (chrome), = 194 - OpenBracket = 219, - BackSlash = 220, - CloseBraket = 221, - SingleQuote = 222, - // `, = 223 - // left or right ⌘ key (firefox), = 224 - // altgr, = 225 - // < /git >, left back slash, = 226 - // GNOME Compose Key, = 230 - // ç, = 231 - // XF86Forward, = 233 - // XF86Back, = 234 - // non-conversion, = 235 - // alphanumeric, = 240 - // hiragana/katakana, = 242 - // half-width/full-width, = 243 - // kanji, = 244 - // unlock trackpad (Chrome/Edge), = 251 - // toggle touchpad, = 255 - #[cfg_attr(feature = "serialize", serde(other))] - Unknown, -} +mod animation; +mod clipboard; +mod composition; +mod drag; +mod focus; +mod form; +mod image; +mod keyboard; +mod media; +mod mouse; +mod pointer; +mod scroll; +mod selection; +mod toggle; +mod touch; +mod transition; +mod wheel; -impl KeyCode { - pub fn from_raw_code(i: u8) -> Self { - use KeyCode::*; - match i { - 8 => Backspace, - 9 => Tab, - 13 => Enter, - 16 => Shift, - 17 => Ctrl, - 18 => Alt, - 19 => Pause, - 20 => CapsLock, - 27 => Escape, - 33 => PageUp, - 34 => PageDown, - 35 => End, - 36 => Home, - 37 => LeftArrow, - 38 => UpArrow, - 39 => RightArrow, - 40 => DownArrow, - 45 => Insert, - 46 => Delete, - 48 => Num0, - 49 => Num1, - 50 => Num2, - 51 => Num3, - 52 => Num4, - 53 => Num5, - 54 => Num6, - 55 => Num7, - 56 => Num8, - 57 => Num9, - 65 => A, - 66 => B, - 67 => C, - 68 => D, - 69 => E, - 70 => F, - 71 => G, - 72 => H, - 73 => I, - 74 => J, - 75 => K, - 76 => L, - 77 => M, - 78 => N, - 79 => O, - 80 => P, - 81 => Q, - 82 => R, - 83 => S, - 84 => T, - 85 => U, - 86 => V, - 87 => W, - 88 => X, - 89 => Y, - 90 => Z, - 91 => LeftWindow, - 92 => RightWindow, - 93 => SelectKey, - 96 => Numpad0, - 97 => Numpad1, - 98 => Numpad2, - 99 => Numpad3, - 100 => Numpad4, - 101 => Numpad5, - 102 => Numpad6, - 103 => Numpad7, - 104 => Numpad8, - 105 => Numpad9, - 106 => Multiply, - 107 => Add, - 109 => Subtract, - 110 => DecimalPoint, - 111 => Divide, - 112 => F1, - 113 => F2, - 114 => F3, - 115 => F4, - 116 => F5, - 117 => F6, - 118 => F7, - 119 => F8, - 120 => F9, - 121 => F10, - 122 => F11, - 123 => F12, - 144 => NumLock, - 145 => ScrollLock, - 186 => Semicolon, - 187 => EqualSign, - 188 => Comma, - 189 => Dash, - 190 => Period, - 191 => ForwardSlash, - 192 => GraveAccent, - 219 => OpenBracket, - 220 => BackSlash, - 221 => CloseBraket, - 222 => SingleQuote, - _ => Unknown, - } - } - - // get the raw code - pub fn raw_code(&self) -> u32 { - *self as u32 - } -} - -pub(crate) fn _event_meta(event: &UserEvent) -> (bool, EventPriority) { - use EventPriority::*; - - match event.name { - // clipboard - "copy" | "cut" | "paste" => (true, Medium), - - // Composition - "compositionend" | "compositionstart" | "compositionupdate" => (true, Low), - - // Keyboard - "keydown" | "keypress" | "keyup" => (true, High), - - // Focus - "focus" | "blur" | "focusout" | "focusin" => (true, Low), - - // Form - "change" | "input" | "invalid" | "reset" | "submit" => (true, Medium), - - // Mouse - "click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit" - | "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter" - | "mouseleave" | "mouseout" | "mouseover" | "mouseup" => (true, High), - - "mousemove" => (false, Medium), - - // Pointer - "pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture" - | "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => { - (true, Medium) - } - - // Selection - "select" | "touchcancel" | "touchend" => (true, Medium), - - // Touch - "touchmove" | "touchstart" => (true, Medium), - - // Wheel - "scroll" | "wheel" => (false, Medium), - - // Media - "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted" - | "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play" - | "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend" - | "timeupdate" | "volumechange" | "waiting" => (true, Medium), - - // Animation - "animationstart" | "animationend" | "animationiteration" => (true, Medium), - - // Transition - "transitionend" => (true, Medium), - - // Toggle - "toggle" => (true, Medium), - - _ => (true, Low), - } -} +pub use animation::*; +pub use clipboard::*; +pub use composition::*; +pub use drag::*; +pub use focus::*; +pub use form::*; +pub use image::*; +pub use keyboard::*; +pub use media::*; +pub use mouse::*; +pub use pointer::*; +pub use scroll::*; +pub use selection::*; +pub use toggle::*; +pub use touch::*; +pub use transition::*; +pub use wheel::*; pub fn event_bubbles(evt: &str) -> bool { match evt { @@ -1567,6 +144,6 @@ pub fn event_bubbles(evt: &str) -> bool { "animationiteration" => true, "transitionend" => true, "toggle" => true, - _ => panic!("unsupported event type {:?}", evt), + _ => true, } } diff --git a/packages/html/src/events/animation.rs b/packages/html/src/events/animation.rs new file mode 100644 index 000000000..fb6dc64ca --- /dev/null +++ b/packages/html/src/events/animation.rs @@ -0,0 +1,24 @@ +use dioxus_core::Event; + +pub type AnimationEvent = Event; + +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone)] +pub struct AnimationData { + pub animation_name: String, + pub pseudo_element: String, + pub elapsed_time: f32, +} + +impl_event! [ + AnimationData; + + /// onanimationstart + onanimationstart + + /// onanimationend + onanimationend + + /// onanimationiteration + onanimationiteration +]; diff --git a/packages/html/src/events/clipboard.rs b/packages/html/src/events/clipboard.rs new file mode 100644 index 000000000..e7031d061 --- /dev/null +++ b/packages/html/src/events/clipboard.rs @@ -0,0 +1,21 @@ +use dioxus_core::Event; + +pub type ClipboardEvent = Event; +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone)] +pub struct ClipboardData { + // DOMDataTransfer clipboardData +} + +impl_event![ + ClipboardData; + + /// oncopy + oncopy + + /// oncut + oncut + + /// onpaste + onpaste +]; diff --git a/packages/html/src/events/composition.rs b/packages/html/src/events/composition.rs new file mode 100644 index 000000000..59009c1f0 --- /dev/null +++ b/packages/html/src/events/composition.rs @@ -0,0 +1,21 @@ +use dioxus_core::Event; + +pub type CompositionEvent = Event; +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone)] +pub struct CompositionData { + pub data: String, +} + +impl_event! [ + CompositionData; + + /// oncompositionstart + oncompositionstart + + /// oncompositionend + oncompositionend + + /// oncompositionupdate + oncompositionupdate +]; diff --git a/packages/html/src/events/drag.rs b/packages/html/src/events/drag.rs new file mode 100644 index 000000000..5dd7f5c71 --- /dev/null +++ b/packages/html/src/events/drag.rs @@ -0,0 +1,47 @@ +use std::any::Any; + +use dioxus_core::Event; + +use crate::MouseData; + +pub type DragEvent = Event; + +/// The DragEvent interface is a DOM event that represents a drag and drop interaction. The user initiates a drag by +/// placing a pointer device (such as a mouse) on the touch surface and then dragging the pointer to a new location +/// (such as another DOM element). Applications are free to interpret a drag and drop interaction in an +/// application-specific way. +pub struct DragData { + /// Inherit mouse data + pub mouse: MouseData, + + /// And then add the rest of the drag data + pub data: Box, +} + +impl_event! { + DragData; + + /// ondrag + ondrag + + /// ondragend + ondragend + + /// ondragenter + ondragenter + + /// ondragexit + ondragexit + + /// ondragleave + ondragleave + + /// ondragover + ondragover + + /// ondragstart + ondragstart + + /// ondrop + ondrop +} diff --git a/packages/html/src/events/focus.rs b/packages/html/src/events/focus.rs new file mode 100644 index 000000000..b2a4f5205 --- /dev/null +++ b/packages/html/src/events/focus.rs @@ -0,0 +1,23 @@ +use dioxus_core::Event; + +pub type FocusEvent = Event; + +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone)] +pub struct FocusData {/* DOMEventInner: Send + SyncTarget relatedTarget */} + +impl_event! [ + FocusData; + + /// onfocus + onfocus + + // onfocusout + onfocusout + + // onfocusin + onfocusin + + /// onblur + onblur +]; diff --git a/packages/html/src/events/form.rs b/packages/html/src/events/form.rs new file mode 100644 index 000000000..5e0b6a17e --- /dev/null +++ b/packages/html/src/events/form.rs @@ -0,0 +1,57 @@ +use std::{collections::HashMap, fmt::Debug, sync::Arc}; + +use dioxus_core::Event; + +pub type FormEvent = Event; + +/* DOMEvent: Send + SyncTarget relatedTarget */ +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone)] +pub struct FormData { + pub value: String, + + pub values: HashMap, + + #[cfg_attr(feature = "serialize", serde(skip))] + pub files: Option>, +} + +impl Debug for FormData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FormEvent") + .field("value", &self.value) + .field("values", &self.values) + .finish() + } +} + +#[async_trait::async_trait(?Send)] +pub trait FileEngine { + // get a list of file names + fn files(&self) -> Vec; + + // read a file to bytes + async fn read_file(&self, file: &str) -> Option>; + + // read a file to string + async fn read_file_to_string(&self, file: &str) -> Option; +} + +impl_event! { + FormData; + + /// onchange + onchange + + /// oninput handler + oninput + + /// oninvalid + oninvalid + + /// onreset + onreset + + /// onsubmit + onsubmit +} diff --git a/packages/html/src/events/image.rs b/packages/html/src/events/image.rs new file mode 100644 index 000000000..06430c4c2 --- /dev/null +++ b/packages/html/src/events/image.rs @@ -0,0 +1,18 @@ +use dioxus_core::Event; + +pub type ImageEvent = Event; +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone)] +pub struct ImageData { + pub load_error: bool, +} + +impl_event! [ + ImageData; + + /// onerror + onerror + + /// onload + onload +]; diff --git a/packages/html/src/events/keyboard.rs b/packages/html/src/events/keyboard.rs new file mode 100644 index 000000000..ddf720770 --- /dev/null +++ b/packages/html/src/events/keyboard.rs @@ -0,0 +1,642 @@ +use crate::input_data::{decode_key_location, encode_key_location}; +use dioxus_core::Event; +use keyboard_types::{Code, Key, Location, Modifiers}; +use std::convert::TryInto; +use std::fmt::{Debug, Formatter}; +use std::str::FromStr; + +pub type KeyboardEvent = Event; +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone)] +pub struct KeyboardData { + #[deprecated( + since = "0.3.0", + note = "This may not work in all environments. Use key() instead." + )] + pub char_code: u32, + + /// Identify which "key" was entered. + #[deprecated(since = "0.3.0", note = "use key() instead")] + pub key: String, + + /// Get the key code as an enum Variant. + #[deprecated( + since = "0.3.0", + note = "This may not work in all environments. Use code() instead." + )] + pub key_code: KeyCode, + + /// the physical key on the keyboard + code: Code, + + /// Indicate if the `alt` modifier key was pressed during this keyboard event + #[deprecated(since = "0.3.0", note = "use modifiers() instead")] + pub alt_key: bool, + + /// Indicate if the `ctrl` modifier key was pressed during this keyboard event + #[deprecated(since = "0.3.0", note = "use modifiers() instead")] + pub ctrl_key: bool, + + /// Indicate if the `meta` modifier key was pressed during this keyboard event + #[deprecated(since = "0.3.0", note = "use modifiers() instead")] + pub meta_key: bool, + + /// Indicate if the `shift` modifier key was pressed during this keyboard event + #[deprecated(since = "0.3.0", note = "use modifiers() instead")] + pub shift_key: bool, + + #[deprecated(since = "0.3.0", note = "use location() instead")] + pub location: usize, + + #[deprecated(since = "0.3.0", note = "use is_auto_repeating() instead")] + pub repeat: bool, + + #[deprecated(since = "0.3.0", note = "use code() or key() instead")] + pub which: usize, +} + +impl_event! { + KeyboardData; + + /// onkeydown + onkeydown + + /// onkeypress + onkeypress + + /// onkeyup + onkeyup +} + +impl KeyboardData { + pub fn new( + key: Key, + code: Code, + location: Location, + is_auto_repeating: bool, + modifiers: Modifiers, + ) -> Self { + #[allow(deprecated)] + KeyboardData { + char_code: key.legacy_charcode(), + key: key.to_string(), + key_code: KeyCode::from_raw_code( + key.legacy_keycode() + .try_into() + .expect("could not convert keycode to u8"), + ), + code, + alt_key: modifiers.contains(Modifiers::ALT), + ctrl_key: modifiers.contains(Modifiers::CONTROL), + meta_key: modifiers.contains(Modifiers::META), + shift_key: modifiers.contains(Modifiers::SHIFT), + location: encode_key_location(location), + repeat: is_auto_repeating, + which: key + .legacy_charcode() + .try_into() + .expect("could not convert charcode to usize"), + } + } + + /// The value of the key pressed by the user, taking into consideration the state of modifier keys such as Shift as well as the keyboard locale and layout. + pub fn key(&self) -> Key { + #[allow(deprecated)] + FromStr::from_str(&self.key).expect("could not parse") + } + + /// A physical key on the keyboard (as opposed to the character generated by pressing the key). In other words, this property returns a value that isn't altered by keyboard layout or the state of the modifier keys. + pub fn code(&self) -> Code { + self.code + } + + /// The set of modifier keys which were pressed when the event occurred + pub fn modifiers(&self) -> Modifiers { + let mut modifiers = Modifiers::empty(); + + #[allow(deprecated)] + { + if self.alt_key { + modifiers.insert(Modifiers::ALT); + } + if self.ctrl_key { + modifiers.insert(Modifiers::CONTROL); + } + if self.meta_key { + modifiers.insert(Modifiers::META); + } + if self.shift_key { + modifiers.insert(Modifiers::SHIFT); + } + } + + modifiers + } + + /// The location of the key on the keyboard or other input device. + pub fn location(&self) -> Location { + #[allow(deprecated)] + decode_key_location(self.location) + } + + /// `true` iff the key is being held down such that it is automatically repeating. + pub fn is_auto_repeating(&self) -> bool { + #[allow(deprecated)] + self.repeat + } +} + +impl Debug for KeyboardData { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("KeyboardData") + .field("key", &self.key()) + .field("code", &self.code()) + .field("modifiers", &self.modifiers()) + .field("location", &self.location()) + .field("is_auto_repeating", &self.is_auto_repeating()) + .finish() + } +} + +#[cfg_attr( + feature = "serialize", + derive(serde_repr::Serialize_repr, serde_repr::Deserialize_repr) +)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u8)] +pub enum KeyCode { + // That key has no keycode, = 0 + // break, = 3 + // backspace / delete, = 8 + // tab, = 9 + // clear, = 12 + // enter, = 13 + // shift, = 16 + // ctrl, = 17 + // alt, = 18 + // pause/break, = 19 + // caps lock, = 20 + // hangul, = 21 + // hanja, = 25 + // escape, = 27 + // conversion, = 28 + // non-conversion, = 29 + // spacebar, = 32 + // page up, = 33 + // page down, = 34 + // end, = 35 + // home, = 36 + // left arrow, = 37 + // up arrow, = 38 + // right arrow, = 39 + // down arrow, = 40 + // select, = 41 + // print, = 42 + // execute, = 43 + // Print Screen, = 44 + // insert, = 45 + // delete, = 46 + // help, = 47 + // 0, = 48 + // 1, = 49 + // 2, = 50 + // 3, = 51 + // 4, = 52 + // 5, = 53 + // 6, = 54 + // 7, = 55 + // 8, = 56 + // 9, = 57 + // :, = 58 + // semicolon (firefox), equals, = 59 + // <, = 60 + // equals (firefox), = 61 + // ß, = 63 + // @ (firefox), = 64 + // a, = 65 + // b, = 66 + // c, = 67 + // d, = 68 + // e, = 69 + // f, = 70 + // g, = 71 + // h, = 72 + // i, = 73 + // j, = 74 + // k, = 75 + // l, = 76 + // m, = 77 + // n, = 78 + // o, = 79 + // p, = 80 + // q, = 81 + // r, = 82 + // s, = 83 + // t, = 84 + // u, = 85 + // v, = 86 + // w, = 87 + // x, = 88 + // y, = 89 + // z, = 90 + // Windows Key / Left ⌘ / Chromebook Search key, = 91 + // right window key, = 92 + // Windows Menu / Right ⌘, = 93 + // sleep, = 95 + // numpad 0, = 96 + // numpad 1, = 97 + // numpad 2, = 98 + // numpad 3, = 99 + // numpad 4, = 100 + // numpad 5, = 101 + // numpad 6, = 102 + // numpad 7, = 103 + // numpad 8, = 104 + // numpad 9, = 105 + // multiply, = 106 + // add, = 107 + // numpad period (firefox), = 108 + // subtract, = 109 + // decimal point, = 110 + // divide, = 111 + // f1, = 112 + // f2, = 113 + // f3, = 114 + // f4, = 115 + // f5, = 116 + // f6, = 117 + // f7, = 118 + // f8, = 119 + // f9, = 120 + // f10, = 121 + // f11, = 122 + // f12, = 123 + // f13, = 124 + // f14, = 125 + // f15, = 126 + // f16, = 127 + // f17, = 128 + // f18, = 129 + // f19, = 130 + // f20, = 131 + // f21, = 132 + // f22, = 133 + // f23, = 134 + // f24, = 135 + // f25, = 136 + // f26, = 137 + // f27, = 138 + // f28, = 139 + // f29, = 140 + // f30, = 141 + // f31, = 142 + // f32, = 143 + // num lock, = 144 + // scroll lock, = 145 + // airplane mode, = 151 + // ^, = 160 + // !, = 161 + // ؛ (arabic semicolon), = 162 + // #, = 163 + // $, = 164 + // ù, = 165 + // page backward, = 166 + // page forward, = 167 + // refresh, = 168 + // closing paren (AZERTY), = 169 + // *, = 170 + // ~ + * key, = 171 + // home key, = 172 + // minus (firefox), mute/unmute, = 173 + // decrease volume level, = 174 + // increase volume level, = 175 + // next, = 176 + // previous, = 177 + // stop, = 178 + // play/pause, = 179 + // e-mail, = 180 + // mute/unmute (firefox), = 181 + // decrease volume level (firefox), = 182 + // increase volume level (firefox), = 183 + // semi-colon / ñ, = 186 + // equal sign, = 187 + // comma, = 188 + // dash, = 189 + // period, = 190 + // forward slash / ç, = 191 + // grave accent / ñ / æ / ö, = 192 + // ?, / or °, = 193 + // numpad period (chrome), = 194 + // open bracket, = 219 + // back slash, = 220 + // close bracket / å, = 221 + // single quote / ø / ä, = 222 + // `, = 223 + // left or right ⌘ key (firefox), = 224 + // altgr, = 225 + // < /git >, left back slash, = 226 + // GNOME Compose Key, = 230 + // ç, = 231 + // XF86Forward, = 233 + // XF86Back, = 234 + // non-conversion, = 235 + // alphanumeric, = 240 + // hiragana/katakana, = 242 + // half-width/full-width, = 243 + // kanji, = 244 + // unlock trackpad (Chrome/Edge), = 251 + // toggle touchpad, = 255 + NA = 0, + Break = 3, + Backspace = 8, + Tab = 9, + Clear = 12, + Enter = 13, + Shift = 16, + Ctrl = 17, + Alt = 18, + Pause = 19, + CapsLock = 20, + // hangul, = 21 + // hanja, = 25 + Escape = 27, + // conversion, = 28 + // non-conversion, = 29 + Space = 32, + PageUp = 33, + PageDown = 34, + End = 35, + Home = 36, + LeftArrow = 37, + UpArrow = 38, + RightArrow = 39, + DownArrow = 40, + // select, = 41 + // print, = 42 + // execute, = 43 + // Print Screen, = 44 + Insert = 45, + Delete = 46, + // help, = 47 + Num0 = 48, + Num1 = 49, + Num2 = 50, + Num3 = 51, + Num4 = 52, + Num5 = 53, + Num6 = 54, + Num7 = 55, + Num8 = 56, + Num9 = 57, + // :, = 58 + // semicolon (firefox), equals, = 59 + // <, = 60 + // equals (firefox), = 61 + // ß, = 63 + // @ (firefox), = 64 + A = 65, + B = 66, + C = 67, + D = 68, + E = 69, + F = 70, + G = 71, + H = 72, + I = 73, + J = 74, + K = 75, + L = 76, + M = 77, + N = 78, + O = 79, + P = 80, + Q = 81, + R = 82, + S = 83, + T = 84, + U = 85, + V = 86, + W = 87, + X = 88, + Y = 89, + Z = 90, + LeftWindow = 91, + RightWindow = 92, + SelectKey = 93, + Numpad0 = 96, + Numpad1 = 97, + Numpad2 = 98, + Numpad3 = 99, + Numpad4 = 100, + Numpad5 = 101, + Numpad6 = 102, + Numpad7 = 103, + Numpad8 = 104, + Numpad9 = 105, + Multiply = 106, + Add = 107, + Subtract = 109, + DecimalPoint = 110, + Divide = 111, + F1 = 112, + F2 = 113, + F3 = 114, + F4 = 115, + F5 = 116, + F6 = 117, + F7 = 118, + F8 = 119, + F9 = 120, + F10 = 121, + F11 = 122, + F12 = 123, + // f13, = 124 + // f14, = 125 + // f15, = 126 + // f16, = 127 + // f17, = 128 + // f18, = 129 + // f19, = 130 + // f20, = 131 + // f21, = 132 + // f22, = 133 + // f23, = 134 + // f24, = 135 + // f25, = 136 + // f26, = 137 + // f27, = 138 + // f28, = 139 + // f29, = 140 + // f30, = 141 + // f31, = 142 + // f32, = 143 + NumLock = 144, + ScrollLock = 145, + // airplane mode, = 151 + // ^, = 160 + // !, = 161 + // ؛ (arabic semicolon), = 162 + // #, = 163 + // $, = 164 + // ù, = 165 + // page backward, = 166 + // page forward, = 167 + // refresh, = 168 + // closing paren (AZERTY), = 169 + // *, = 170 + // ~ + * key, = 171 + // home key, = 172 + // minus (firefox), mute/unmute, = 173 + // decrease volume level, = 174 + // increase volume level, = 175 + // next, = 176 + // previous, = 177 + // stop, = 178 + // play/pause, = 179 + // e-mail, = 180 + // mute/unmute (firefox), = 181 + // decrease volume level (firefox), = 182 + // increase volume level (firefox), = 183 + Semicolon = 186, + EqualSign = 187, + Comma = 188, + Dash = 189, + Period = 190, + ForwardSlash = 191, + GraveAccent = 192, + // ?, / or °, = 193 + // numpad period (chrome), = 194 + OpenBracket = 219, + BackSlash = 220, + CloseBraket = 221, + SingleQuote = 222, + // `, = 223 + // left or right ⌘ key (firefox), = 224 + // altgr, = 225 + // < /git >, left back slash, = 226 + // GNOME Compose Key, = 230 + // ç, = 231 + // XF86Forward, = 233 + // XF86Back, = 234 + // non-conversion, = 235 + // alphanumeric, = 240 + // hiragana/katakana, = 242 + // half-width/full-width, = 243 + // kanji, = 244 + // unlock trackpad (Chrome/Edge), = 251 + // toggle touchpad, = 255 + #[cfg_attr(feature = "serialize", serde(other))] + Unknown, +} + +impl KeyCode { + pub fn from_raw_code(i: u8) -> Self { + use KeyCode::*; + match i { + 8 => Backspace, + 9 => Tab, + 13 => Enter, + 16 => Shift, + 17 => Ctrl, + 18 => Alt, + 19 => Pause, + 20 => CapsLock, + 27 => Escape, + 33 => PageUp, + 34 => PageDown, + 35 => End, + 36 => Home, + 37 => LeftArrow, + 38 => UpArrow, + 39 => RightArrow, + 40 => DownArrow, + 45 => Insert, + 46 => Delete, + 48 => Num0, + 49 => Num1, + 50 => Num2, + 51 => Num3, + 52 => Num4, + 53 => Num5, + 54 => Num6, + 55 => Num7, + 56 => Num8, + 57 => Num9, + 65 => A, + 66 => B, + 67 => C, + 68 => D, + 69 => E, + 70 => F, + 71 => G, + 72 => H, + 73 => I, + 74 => J, + 75 => K, + 76 => L, + 77 => M, + 78 => N, + 79 => O, + 80 => P, + 81 => Q, + 82 => R, + 83 => S, + 84 => T, + 85 => U, + 86 => V, + 87 => W, + 88 => X, + 89 => Y, + 90 => Z, + 91 => LeftWindow, + 92 => RightWindow, + 93 => SelectKey, + 96 => Numpad0, + 97 => Numpad1, + 98 => Numpad2, + 99 => Numpad3, + 100 => Numpad4, + 101 => Numpad5, + 102 => Numpad6, + 103 => Numpad7, + 104 => Numpad8, + 105 => Numpad9, + 106 => Multiply, + 107 => Add, + 109 => Subtract, + 110 => DecimalPoint, + 111 => Divide, + 112 => F1, + 113 => F2, + 114 => F3, + 115 => F4, + 116 => F5, + 117 => F6, + 118 => F7, + 119 => F8, + 120 => F9, + 121 => F10, + 122 => F11, + 123 => F12, + 144 => NumLock, + 145 => ScrollLock, + 186 => Semicolon, + 187 => EqualSign, + 188 => Comma, + 189 => Dash, + 190 => Period, + 191 => ForwardSlash, + 192 => GraveAccent, + 219 => OpenBracket, + 220 => BackSlash, + 221 => CloseBraket, + 222 => SingleQuote, + _ => Unknown, + } + } + + // get the raw code + pub fn raw_code(&self) -> u32 { + *self as u32 + } +} diff --git a/packages/html/src/events/keys.rs b/packages/html/src/events/keys.rs new file mode 100644 index 000000000..8130875f2 --- /dev/null +++ b/packages/html/src/events/keys.rs @@ -0,0 +1,370 @@ +#[cfg_attr( + feature = "serialize", + derive(serde_repr::Serialize_repr, serde_repr::Deserialize_repr) +)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u8)] +pub enum KeyCode { + // That key has no keycode, = 0 + // break, = 3 + // backspace / delete, = 8 + // tab, = 9 + // clear, = 12 + // enter, = 13 + // shift, = 16 + // ctrl, = 17 + // alt, = 18 + // pause/break, = 19 + // caps lock, = 20 + // hangul, = 21 + // hanja, = 25 + // escape, = 27 + // conversion, = 28 + // non-conversion, = 29 + // spacebar, = 32 + // page up, = 33 + // page down, = 34 + // end, = 35 + // home, = 36 + // left arrow, = 37 + // up arrow, = 38 + // right arrow, = 39 + // down arrow, = 40 + // select, = 41 + // print, = 42 + // execute, = 43 + // Print Screen, = 44 + // insert, = 45 + // delete, = 46 + // help, = 47 + // 0, = 48 + // 1, = 49 + // 2, = 50 + // 3, = 51 + // 4, = 52 + // 5, = 53 + // 6, = 54 + // 7, = 55 + // 8, = 56 + // 9, = 57 + // :, = 58 + // semicolon (firefox), equals, = 59 + // <, = 60 + // equals (firefox), = 61 + // ß, = 63 + // @ (firefox), = 64 + // a, = 65 + // b, = 66 + // c, = 67 + // d, = 68 + // e, = 69 + // f, = 70 + // g, = 71 + // h, = 72 + // i, = 73 + // j, = 74 + // k, = 75 + // l, = 76 + // m, = 77 + // n, = 78 + // o, = 79 + // p, = 80 + // q, = 81 + // r, = 82 + // s, = 83 + // t, = 84 + // u, = 85 + // v, = 86 + // w, = 87 + // x, = 88 + // y, = 89 + // z, = 90 + // Windows Key / Left ⌘ / Chromebook Search key, = 91 + // right window key, = 92 + // Windows Menu / Right ⌘, = 93 + // sleep, = 95 + // numpad 0, = 96 + // numpad 1, = 97 + // numpad 2, = 98 + // numpad 3, = 99 + // numpad 4, = 100 + // numpad 5, = 101 + // numpad 6, = 102 + // numpad 7, = 103 + // numpad 8, = 104 + // numpad 9, = 105 + // multiply, = 106 + // add, = 107 + // numpad period (firefox), = 108 + // subtract, = 109 + // decimal point, = 110 + // divide, = 111 + // f1, = 112 + // f2, = 113 + // f3, = 114 + // f4, = 115 + // f5, = 116 + // f6, = 117 + // f7, = 118 + // f8, = 119 + // f9, = 120 + // f10, = 121 + // f11, = 122 + // f12, = 123 + // f13, = 124 + // f14, = 125 + // f15, = 126 + // f16, = 127 + // f17, = 128 + // f18, = 129 + // f19, = 130 + // f20, = 131 + // f21, = 132 + // f22, = 133 + // f23, = 134 + // f24, = 135 + // f25, = 136 + // f26, = 137 + // f27, = 138 + // f28, = 139 + // f29, = 140 + // f30, = 141 + // f31, = 142 + // f32, = 143 + // num lock, = 144 + // scroll lock, = 145 + // airplane mode, = 151 + // ^, = 160 + // !, = 161 + // ؛ (arabic semicolon), = 162 + // #, = 163 + // $, = 164 + // ù, = 165 + // page backward, = 166 + // page forward, = 167 + // refresh, = 168 + // closing paren (AZERTY), = 169 + // *, = 170 + // ~ + * key, = 171 + // home key, = 172 + // minus (firefox), mute/unmute, = 173 + // decrease volume level, = 174 + // increase volume level, = 175 + // next, = 176 + // previous, = 177 + // stop, = 178 + // play/pause, = 179 + // e-mail, = 180 + // mute/unmute (firefox), = 181 + // decrease volume level (firefox), = 182 + // increase volume level (firefox), = 183 + // semi-colon / ñ, = 186 + // equal sign, = 187 + // comma, = 188 + // dash, = 189 + // period, = 190 + // forward slash / ç, = 191 + // grave accent / ñ / æ / ö, = 192 + // ?, / or °, = 193 + // numpad period (chrome), = 194 + // open bracket, = 219 + // back slash, = 220 + // close bracket / å, = 221 + // single quote / ø / ä, = 222 + // `, = 223 + // left or right ⌘ key (firefox), = 224 + // altgr, = 225 + // < /git >, left back slash, = 226 + // GNOME Compose Key, = 230 + // ç, = 231 + // XF86Forward, = 233 + // XF86Back, = 234 + // non-conversion, = 235 + // alphanumeric, = 240 + // hiragana/katakana, = 242 + // half-width/full-width, = 243 + // kanji, = 244 + // unlock trackpad (Chrome/Edge), = 251 + // toggle touchpad, = 255 + NA = 0, + Break = 3, + Backspace = 8, + Tab = 9, + Clear = 12, + Enter = 13, + Shift = 16, + Ctrl = 17, + Alt = 18, + Pause = 19, + CapsLock = 20, + // hangul, = 21 + // hanja, = 25 + Escape = 27, + // conversion, = 28 + // non-conversion, = 29 + Space = 32, + PageUp = 33, + PageDown = 34, + End = 35, + Home = 36, + LeftArrow = 37, + UpArrow = 38, + RightArrow = 39, + DownArrow = 40, + // select, = 41 + // print, = 42 + // execute, = 43 + // Print Screen, = 44 + Insert = 45, + Delete = 46, + // help, = 47 + Num0 = 48, + Num1 = 49, + Num2 = 50, + Num3 = 51, + Num4 = 52, + Num5 = 53, + Num6 = 54, + Num7 = 55, + Num8 = 56, + Num9 = 57, + // :, = 58 + // semicolon (firefox), equals, = 59 + // <, = 60 + // equals (firefox), = 61 + // ß, = 63 + // @ (firefox), = 64 + A = 65, + B = 66, + C = 67, + D = 68, + E = 69, + F = 70, + G = 71, + H = 72, + I = 73, + J = 74, + K = 75, + L = 76, + M = 77, + N = 78, + O = 79, + P = 80, + Q = 81, + R = 82, + S = 83, + T = 84, + U = 85, + V = 86, + W = 87, + X = 88, + Y = 89, + Z = 90, + LeftWindow = 91, + RightWindow = 92, + SelectKey = 93, + Numpad0 = 96, + Numpad1 = 97, + Numpad2 = 98, + Numpad3 = 99, + Numpad4 = 100, + Numpad5 = 101, + Numpad6 = 102, + Numpad7 = 103, + Numpad8 = 104, + Numpad9 = 105, + Multiply = 106, + Add = 107, + Subtract = 109, + DecimalPoint = 110, + Divide = 111, + F1 = 112, + F2 = 113, + F3 = 114, + F4 = 115, + F5 = 116, + F6 = 117, + F7 = 118, + F8 = 119, + F9 = 120, + F10 = 121, + F11 = 122, + F12 = 123, + // f13, = 124 + // f14, = 125 + // f15, = 126 + // f16, = 127 + // f17, = 128 + // f18, = 129 + // f19, = 130 + // f20, = 131 + // f21, = 132 + // f22, = 133 + // f23, = 134 + // f24, = 135 + // f25, = 136 + // f26, = 137 + // f27, = 138 + // f28, = 139 + // f29, = 140 + // f30, = 141 + // f31, = 142 + // f32, = 143 + NumLock = 144, + ScrollLock = 145, + // airplane mode, = 151 + // ^, = 160 + // !, = 161 + // ؛ (arabic semicolon), = 162 + // #, = 163 + // $, = 164 + // ù, = 165 + // page backward, = 166 + // page forward, = 167 + // refresh, = 168 + // closing paren (AZERTY), = 169 + // *, = 170 + // ~ + * key, = 171 + // home key, = 172 + // minus (firefox), mute/unmute, = 173 + // decrease volume level, = 174 + // increase volume level, = 175 + // next, = 176 + // previous, = 177 + // stop, = 178 + // play/pause, = 179 + // e-mail, = 180 + // mute/unmute (firefox), = 181 + // decrease volume level (firefox), = 182 + // increase volume level (firefox), = 183 + Semicolon = 186, + EqualSign = 187, + Comma = 188, + Dash = 189, + Period = 190, + ForwardSlash = 191, + GraveAccent = 192, + // ?, / or °, = 193 + // numpad period (chrome), = 194 + OpenBracket = 219, + BackSlash = 220, + CloseBraket = 221, + SingleQuote = 222, + // `, = 223 + // left or right ⌘ key (firefox), = 224 + // altgr, = 225 + // < /git >, left back slash, = 226 + // GNOME Compose Key, = 230 + // ç, = 231 + // XF86Forward, = 233 + // XF86Back, = 234 + // non-conversion, = 235 + // alphanumeric, = 240 + // hiragana/katakana, = 242 + // half-width/full-width, = 243 + // kanji, = 244 + // unlock trackpad (Chrome/Edge), = 251 + // toggle touchpad, = 255 + #[cfg_attr(feature = "serialize", serde(other))] + Unknown, +} diff --git a/packages/html/src/events/media.rs b/packages/html/src/events/media.rs new file mode 100644 index 000000000..0b2fe70e0 --- /dev/null +++ b/packages/html/src/events/media.rs @@ -0,0 +1,81 @@ +use dioxus_core::Event; + +pub type MediaEvent = Event; +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone)] +pub struct MediaData {} + +impl_event! [ + MediaData; + + ///abort + onabort + + ///canplay + oncanplay + + ///canplaythrough + oncanplaythrough + + ///durationchange + ondurationchange + + ///emptied + onemptied + + ///encrypted + onencrypted + + ///ended + onended + + // todo: this conflicts with image events + // neither have data, so it's okay + // ///error + // onerror + + ///loadeddata + onloadeddata + + ///loadedmetadata + onloadedmetadata + + ///loadstart + onloadstart + + ///pause + onpause + + ///play + onplay + + ///playing + onplaying + + ///progress + onprogress + + ///ratechange + onratechange + + ///seeked + onseeked + + ///seeking + onseeking + + ///stalled + onstalled + + ///suspend + onsuspend + + ///timeupdate + ontimeupdate + + ///volumechange + onvolumechange + + ///waiting + onwaiting +]; diff --git a/packages/html/src/events/mouse.rs b/packages/html/src/events/mouse.rs new file mode 100644 index 000000000..a0fda7be6 --- /dev/null +++ b/packages/html/src/events/mouse.rs @@ -0,0 +1,280 @@ +use crate::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint}; +use crate::input_data::{ + decode_mouse_button_set, encode_mouse_button_set, MouseButton, MouseButtonSet, +}; +use dioxus_core::Event; +use keyboard_types::Modifiers; +use std::fmt::{Debug, Formatter}; + +pub type MouseEvent = Event; + +/// A synthetic event that wraps a web-style [`MouseEvent`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Default)] +/// Data associated with a mouse event +/// +/// Do not use the deprecated fields; they may change or become private in the future. +pub struct MouseData { + /// True if the alt key was down when the mouse event was fired. + #[deprecated(since = "0.3.0", note = "use modifiers() instead")] + pub alt_key: bool, + + /// The button number that was pressed (if applicable) when the mouse event was fired. + #[deprecated(since = "0.3.0", note = "use trigger_button() instead")] + pub button: i16, + + /// Indicates which buttons are pressed on the mouse (or other input device) when a mouse event is triggered. + /// + /// Each button that can be pressed is represented by a given number (see below). If more than one button is pressed, the button values are added together to produce a new number. For example, if the secondary (2) and auxiliary (4) buttons are pressed simultaneously, the value is 6 (i.e., 2 + 4). + /// + /// - 1: Primary button (usually the left button) + /// - 2: Secondary button (usually the right button) + /// - 4: Auxiliary button (usually the mouse wheel button or middle button) + /// - 8: 4th button (typically the "Browser Back" button) + /// - 16 : 5th button (typically the "Browser Forward" button) + #[deprecated(since = "0.3.0", note = "use held_buttons() instead")] + pub buttons: u16, + + /// The horizontal coordinate within the application's viewport at which the event occurred (as opposed to the coordinate within the page). + /// + /// For example, clicking on the left edge of the viewport will always result in a mouse event with a clientX value of 0, regardless of whether the page is scrolled horizontally. + #[deprecated(since = "0.3.0", note = "use client_coordinates() instead")] + pub client_x: i32, + + /// The vertical coordinate within the application's viewport at which the event occurred (as opposed to the coordinate within the page). + /// + /// For example, clicking on the top edge of the viewport will always result in a mouse event with a clientY value of 0, regardless of whether the page is scrolled vertically. + #[deprecated(since = "0.3.0", note = "use client_coordinates() instead")] + pub client_y: i32, + + /// True if the control key was down when the mouse event was fired. + #[deprecated(since = "0.3.0", note = "use modifiers() instead")] + pub ctrl_key: bool, + + /// True if the meta key was down when the mouse event was fired. + #[deprecated(since = "0.3.0", note = "use modifiers() instead")] + pub meta_key: bool, + + /// The offset in the X coordinate of the mouse pointer between that event and the padding edge of the target node. + #[deprecated(since = "0.3.0", note = "use element_coordinates() instead")] + pub offset_x: i32, + + /// The offset in the Y coordinate of the mouse pointer between that event and the padding edge of the target node. + #[deprecated(since = "0.3.0", note = "use element_coordinates() instead")] + pub offset_y: i32, + + /// The X (horizontal) coordinate (in pixels) of the mouse, relative to the left edge of the entire document. This includes any portion of the document not currently visible. + /// + /// Being based on the edge of the document as it is, this property takes into account any horizontal scrolling of the page. For example, if the page is scrolled such that 200 pixels of the left side of the document are scrolled out of view, and the mouse is clicked 100 pixels inward from the left edge of the view, the value returned by pageX will be 300. + #[deprecated(since = "0.3.0", note = "use page_coordinates() instead")] + pub page_x: i32, + + /// The Y (vertical) coordinate in pixels of the event relative to the whole document. + /// + /// See `page_x`. + #[deprecated(since = "0.3.0", note = "use page_coordinates() instead")] + pub page_y: i32, + + /// The X coordinate of the mouse pointer in global (screen) coordinates. + #[deprecated(since = "0.3.0", note = "use screen_coordinates() instead")] + pub screen_x: i32, + + /// The Y coordinate of the mouse pointer in global (screen) coordinates. + #[deprecated(since = "0.3.0", note = "use screen_coordinates() instead")] + pub screen_y: i32, + + /// True if the shift key was down when the mouse event was fired. + #[deprecated(since = "0.3.0", note = "use modifiers() instead")] + pub shift_key: bool, +} + +impl_event! { + MouseData; + + /// Execute a callback when a button is clicked. + /// + /// ## Description + /// + /// An element receives a click event when a pointing device button (such as a mouse's primary mouse button) + /// is both pressed and released while the pointer is located inside the element. + /// + /// - Bubbles: Yes + /// - Cancelable: Yes + /// - Interface(InteData): [`MouseEvent`] + /// + /// If the button is pressed on one element and the pointer is moved outside the element before the button + /// is released, the event is fired on the most specific ancestor element that contained both elements. + /// `click` fires after both the `mousedown` and `mouseup` events have fired, in that order. + /// + /// ## Example + /// ```rust, ignore + /// rsx!( button { "click me", onclick: move |_| log::info!("Clicked!`") } ) + /// ``` + /// + /// ## Reference + /// - + /// - + onclick + + /// oncontextmenu + oncontextmenu + + /// ondoubleclick + ondoubleclick + + /// ondoubleclick + ondblclick + + /// onmousedown + onmousedown + + /// onmouseenter + onmouseenter + + /// onmouseleave + onmouseleave + + /// onmousemove + onmousemove + + /// onmouseout + onmouseout + + /// onmouseover + /// + /// Triggered when the users's mouse hovers over an element. + onmouseover + + /// onmouseup + onmouseup +} + +impl MouseData { + /// Construct MouseData with the specified properties + /// + /// Note: the current implementation truncates coordinates. In the future, when we change the internal representation, it may also support a fractional part. + pub fn new( + coordinates: Coordinates, + trigger_button: Option, + held_buttons: MouseButtonSet, + modifiers: Modifiers, + ) -> Self { + let alt_key = modifiers.contains(Modifiers::ALT); + let ctrl_key = modifiers.contains(Modifiers::CONTROL); + let meta_key = modifiers.contains(Modifiers::META); + let shift_key = modifiers.contains(Modifiers::SHIFT); + + let [client_x, client_y]: [i32; 2] = coordinates.client().cast().into(); + let [offset_x, offset_y]: [i32; 2] = coordinates.element().cast().into(); + let [page_x, page_y]: [i32; 2] = coordinates.page().cast().into(); + let [screen_x, screen_y]: [i32; 2] = coordinates.screen().cast().into(); + + #[allow(deprecated)] + Self { + alt_key, + ctrl_key, + meta_key, + shift_key, + + button: trigger_button.map_or(0, |b| b.into_web_code()), + buttons: encode_mouse_button_set(held_buttons), + + client_x, + client_y, + offset_x, + offset_y, + page_x, + page_y, + screen_x, + screen_y, + } + } + + /// The event's coordinates relative to the application's viewport (as opposed to the coordinate within the page). + /// + /// For example, clicking in the top left corner of the viewport will always result in a mouse event with client coordinates (0., 0.), regardless of whether the page is scrolled horizontally. + pub fn client_coordinates(&self) -> ClientPoint { + #[allow(deprecated)] + ClientPoint::new(self.client_x.into(), self.client_y.into()) + } + + /// The event's coordinates relative to the padding edge of the target element + /// + /// For example, clicking in the top left corner of an element will result in element coordinates (0., 0.) + pub fn element_coordinates(&self) -> ElementPoint { + #[allow(deprecated)] + ElementPoint::new(self.offset_x.into(), self.offset_y.into()) + } + + /// The event's coordinates relative to the entire document. This includes any portion of the document not currently visible. + /// + /// For example, if the page is scrolled 200 pixels to the right and 300 pixels down, clicking in the top left corner of the viewport would result in page coordinates (200., 300.) + pub fn page_coordinates(&self) -> PagePoint { + #[allow(deprecated)] + PagePoint::new(self.page_x.into(), self.page_y.into()) + } + + /// The event's coordinates relative to the entire screen. This takes into account the window's offset. + pub fn screen_coordinates(&self) -> ScreenPoint { + #[allow(deprecated)] + ScreenPoint::new(self.screen_x.into(), self.screen_y.into()) + } + + pub fn coordinates(&self) -> Coordinates { + Coordinates::new( + self.screen_coordinates(), + self.client_coordinates(), + self.element_coordinates(), + self.page_coordinates(), + ) + } + + /// The set of modifier keys which were pressed when the event occurred + pub fn modifiers(&self) -> Modifiers { + let mut modifiers = Modifiers::empty(); + + #[allow(deprecated)] + { + if self.alt_key { + modifiers.insert(Modifiers::ALT); + } + if self.ctrl_key { + modifiers.insert(Modifiers::CONTROL); + } + if self.meta_key { + modifiers.insert(Modifiers::META); + } + if self.shift_key { + modifiers.insert(Modifiers::SHIFT); + } + } + + modifiers + } + + /// The set of mouse buttons which were held when the event occurred. + pub fn held_buttons(&self) -> MouseButtonSet { + #[allow(deprecated)] + decode_mouse_button_set(self.buttons) + } + + /// The mouse button that triggered the event + /// + // todo the following is kind of bad; should we just return None when the trigger_button is unreliable (and frankly irrelevant)? i guess we would need the event_type here + /// This is only guaranteed to indicate which button was pressed during events caused by pressing or releasing a button. As such, it is not reliable for events such as mouseenter, mouseleave, mouseover, mouseout, or mousemove. For example, a value of MouseButton::Primary may also indicate that no button was pressed. + pub fn trigger_button(&self) -> Option { + #[allow(deprecated)] + Some(MouseButton::from_web_code(self.button)) + } +} + +impl Debug for MouseData { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("MouseData") + .field("coordinates", &self.coordinates()) + .field("modifiers", &self.modifiers()) + .field("held_buttons", &self.held_buttons()) + .field("trigger_button", &self.trigger_button()) + .finish() + } +} diff --git a/packages/html/src/events/pointer.rs b/packages/html/src/events/pointer.rs new file mode 100644 index 000000000..23b0aba71 --- /dev/null +++ b/packages/html/src/events/pointer.rs @@ -0,0 +1,64 @@ +use dioxus_core::Event; + +pub type PointerEvent = Event; +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone)] +pub struct PointerData { + // Mouse only + pub alt_key: bool, + pub button: i16, + pub buttons: u16, + pub client_x: i32, + pub client_y: i32, + pub ctrl_key: bool, + pub meta_key: bool, + pub page_x: i32, + pub page_y: i32, + pub screen_x: i32, + pub screen_y: i32, + pub shift_key: bool, + pub pointer_id: i32, + pub width: i32, + pub height: i32, + pub pressure: f32, + pub tangential_pressure: f32, + pub tilt_x: i32, + pub tilt_y: i32, + pub twist: i32, + pub pointer_type: String, + pub is_primary: bool, + // pub get_modifier_state: bool, +} + +impl_event![ + PointerData; + /// pointerdown + onpointerdown + + /// pointermove + onpointermove + + /// pointerup + onpointerup + + /// pointercancel + onpointercancel + + /// gotpointercapture + ongotpointercapture + + /// lostpointercapture + onlostpointercapture + + /// pointerenter + onpointerenter + + /// pointerleave + onpointerleave + + /// pointerover + onpointerover + + /// pointerout + onpointerout +]; diff --git a/packages/html/src/events/scroll.rs b/packages/html/src/events/scroll.rs new file mode 100644 index 000000000..0c2dd3143 --- /dev/null +++ b/packages/html/src/events/scroll.rs @@ -0,0 +1,13 @@ +use dioxus_core::Event; + +pub type ScrollEvent = Event; +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone)] +pub struct ScrollData {} + +impl_event! { + ScrollData; + + /// onscroll + onscroll +} diff --git a/packages/html/src/events/selection.rs b/packages/html/src/events/selection.rs new file mode 100644 index 000000000..ff1496c96 --- /dev/null +++ b/packages/html/src/events/selection.rs @@ -0,0 +1,19 @@ +use dioxus_core::Event; + +pub type SelectionEvent = Event; +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone)] +pub struct SelectionData {} + +impl_event! [ + SelectionData; + + /// select + onselect + + /// selectstart + onselectstart + + /// selectionchange + onselectionchange +]; diff --git a/packages/html/src/events/toggle.rs b/packages/html/src/events/toggle.rs new file mode 100644 index 000000000..1f0d3f6e7 --- /dev/null +++ b/packages/html/src/events/toggle.rs @@ -0,0 +1,13 @@ +use dioxus_core::Event; + +pub type ToggleEvent = Event; +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone)] +pub struct ToggleData {} + +impl_event! { + ToggleData; + + /// ontoggle + ontoggle +} diff --git a/packages/html/src/events/touch.rs b/packages/html/src/events/touch.rs new file mode 100644 index 000000000..1f219b22f --- /dev/null +++ b/packages/html/src/events/touch.rs @@ -0,0 +1,30 @@ +use dioxus_core::Event; + +pub type TouchEvent = Event; +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone)] +pub struct TouchData { + pub alt_key: bool, + pub ctrl_key: bool, + pub meta_key: bool, + pub shift_key: bool, + // get_modifier_state: bool, + // changedTouches: DOMTouchList, + // targetTouches: DOMTouchList, + // touches: DOMTouchList, +} + +impl_event! { + TouchData; + /// touchstart + ontouchstart + + /// touchmove + ontouchmove + + /// touchend + ontouchend + + /// touchcancel + ontouchcancel +} diff --git a/packages/html/src/events/transition.rs b/packages/html/src/events/transition.rs new file mode 100644 index 000000000..095496664 --- /dev/null +++ b/packages/html/src/events/transition.rs @@ -0,0 +1,17 @@ +use dioxus_core::Event; + +pub type TransitionEvent = Event; +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone)] +pub struct TransitionData { + pub property_name: String, + pub pseudo_element: String, + pub elapsed_time: f32, +} + +impl_event! { + TransitionData; + + /// transitionend + ontransitionend +} diff --git a/packages/html/src/events/wheel.rs b/packages/html/src/events/wheel.rs new file mode 100644 index 000000000..e6e06501b --- /dev/null +++ b/packages/html/src/events/wheel.rs @@ -0,0 +1,78 @@ +use dioxus_core::Event; +use euclid::UnknownUnit; +use std::fmt::{Debug, Formatter}; + +use crate::geometry::{LinesVector, PagesVector, PixelsVector, WheelDelta}; + +pub type WheelEvent = Event; +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone)] +pub struct WheelData { + #[deprecated(since = "0.3.0", note = "use delta() instead")] + pub delta_mode: u32, + #[deprecated(since = "0.3.0", note = "use delta() instead")] + pub delta_x: f64, + #[deprecated(since = "0.3.0", note = "use delta() instead")] + pub delta_y: f64, + #[deprecated(since = "0.3.0", note = "use delta() instead")] + pub delta_z: f64, +} + +impl_event![ + WheelData; + + /// Called when the mouse wheel is rotated over an element. + onwheel +]; + +impl WheelData { + /// Construct a new WheelData with the specified wheel movement delta + pub fn new(delta: WheelDelta) -> Self { + let (delta_mode, vector) = match delta { + WheelDelta::Pixels(v) => (0, v.cast_unit::()), + WheelDelta::Lines(v) => (1, v.cast_unit::()), + WheelDelta::Pages(v) => (2, v.cast_unit::()), + }; + + #[allow(deprecated)] + WheelData { + delta_mode, + delta_x: vector.x, + delta_y: vector.y, + delta_z: vector.z, + } + } + + /// Construct from the attributes of the web wheel event + pub fn from_web_attributes(delta_mode: u32, delta_x: f64, delta_y: f64, delta_z: f64) -> Self { + #[allow(deprecated)] + Self { + delta_mode, + delta_x, + delta_y, + delta_z, + } + } + + /// The amount of wheel movement + #[allow(deprecated)] + pub fn delta(&self) -> WheelDelta { + let x = self.delta_x; + let y = self.delta_y; + let z = self.delta_z; + match self.delta_mode { + 0 => WheelDelta::Pixels(PixelsVector::new(x, y, z)), + 1 => WheelDelta::Lines(LinesVector::new(x, y, z)), + 2 => WheelDelta::Pages(PagesVector::new(x, y, z)), + _ => panic!("Invalid delta mode, {:?}", self.delta_mode), + } + } +} + +impl Debug for WheelData { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WheelData") + .field("delta", &self.delta()) + .finish() + } +} diff --git a/packages/html/src/global_attributes.rs b/packages/html/src/global_attributes.rs index 41382e07c..5c4ac42e8 100644 --- a/packages/html/src/global_attributes.rs +++ b/packages/html/src/global_attributes.rs @@ -1,4 +1,6 @@ -use dioxus_core::*; +#![allow(non_upper_case_globals)] + +use crate::AttributeDiscription; macro_rules! no_namespace_trait_methods { ( @@ -8,12 +10,12 @@ macro_rules! no_namespace_trait_methods { )* ) => { $( - #[allow(non_upper_case_globals)] - const $name: AttributeDiscription = AttributeDiscription{ - name: stringify!($name), - namespace: None, - volatile: false - }; + $(#[$attr])* + const $name: AttributeDiscription = ( + stringify!($name), + None, + false + ); )* }; } @@ -25,12 +27,12 @@ macro_rules! style_trait_methods { )* ) => { $( - #[allow(non_upper_case_globals)] - const $name: AttributeDiscription = AttributeDiscription{ - name: $lit, - namespace: Some("style"), - volatile: false - }; + $(#[$attr])* + const $name: AttributeDiscription = ( + $lit, + Some("style"), + false + ); )* }; } @@ -42,12 +44,12 @@ macro_rules! aria_trait_methods { )* ) => { $( - #[allow(non_upper_case_globals)] - const $name: AttributeDiscription = AttributeDiscription{ - name: $lit, - namespace: None, - volatile: false - }; + $(#[$attr])* + const $name: AttributeDiscription = ( + $lit, + None, + false + ); )* }; } @@ -57,12 +59,8 @@ pub trait GlobalAttributes { /// /// For more information, see the MDN docs: /// - #[allow(non_upper_case_globals)] - const prevent_default: AttributeDiscription = AttributeDiscription { - name: "dioxus-prevent-default", - namespace: None, - volatile: false, - }; + + const prevent_default: AttributeDiscription = ("dioxus-prevent-default", None, false); no_namespace_trait_methods! { accesskey; @@ -604,12 +602,8 @@ pub trait SvgAttributes { /// /// For more information, see the MDN docs: /// - #[allow(non_upper_case_globals)] - const prevent_default: AttributeDiscription = AttributeDiscription { - name: "dioxus-prevent-default", - namespace: None, - volatile: false, - }; + const prevent_default: AttributeDiscription = ("dioxus-prevent-default", None, false); + aria_trait_methods! { accent_height: "accent-height", accumulate: "accumulate", diff --git a/packages/html/src/lib.rs b/packages/html/src/lib.rs index cb356af7e..ab2e49f1e 100644 --- a/packages/html/src/lib.rs +++ b/packages/html/src/lib.rs @@ -14,13 +14,19 @@ //! Currently, we don't validate for structures, but do validate attributes. mod elements; -mod events; +pub mod events; pub mod geometry; mod global_attributes; pub mod input_data; +mod render_template; #[cfg(feature = "wasm-bind")] mod web_sys_bind; pub use elements::*; pub use events::*; pub use global_attributes::*; +pub use render_template::*; + +pub mod prelude { + pub use crate::events::*; +} diff --git a/packages/html/src/render_template.rs b/packages/html/src/render_template.rs new file mode 100644 index 000000000..e608f6aa1 --- /dev/null +++ b/packages/html/src/render_template.rs @@ -0,0 +1,41 @@ +use dioxus_core::{Template, TemplateAttribute, TemplateNode}; +use std::fmt::Write; + +/// Render a template to an HTML string +/// +/// Useful for sending over the wire. Can be used to with innerHtml to create templates with little work +pub fn render_template_to_html(template: &Template) -> String { + let mut out = String::new(); + + for root in template.roots { + render_template_node(root, &mut out).unwrap(); + } + + out +} + +fn render_template_node(node: &TemplateNode, out: &mut String) -> std::fmt::Result { + match node { + TemplateNode::Element { + tag, + attrs, + children, + .. + } => { + write!(out, "<{tag}")?; + for attr in *attrs { + if let TemplateAttribute::Static { name, value, .. } = attr { + write!(out, "{}=\"{}\"", name, value)?; + } + } + for child in *children { + render_template_node(child, out)?; + } + write!(out, "")?; + } + TemplateNode::Text { text: t } => write!(out, "{t}")?, + TemplateNode::Dynamic { id: _ } => write!(out, "