diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml index 984b4617d..fea1e7590 100644 --- a/.github/workflows/deploy_docs.yml +++ b/.github/workflows/deploy_docs.yml @@ -20,7 +20,7 @@ jobs: - name: Set up cargo cache uses: Swatinem/rust-cache@v2 with: - prefix-key: "2" + prefix-key: "3" - name: cargo doc run: cargo doc --no-deps --workspace @@ -30,4 +30,4 @@ jobs: with: project: freya-docs entrypoint: https://deno.land/std/http/file_server.ts - root: target/doc \ No newline at end of file + root: target/doc diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 8162790a8..c20a5e4cc 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -25,7 +25,7 @@ jobs: - name: Set up cargo cache uses: Swatinem/rust-cache@v2 with: - prefix-key: "5" + prefix-key: "7" - name: Install linux dependencies if: runner.os == 'Linux' run: | @@ -51,7 +51,7 @@ jobs: rustup component add llvm-tools-preview curl -L https://github.com/mozilla/grcov/releases/latest/download/grcov-x86_64-unknown-linux-gnu.tar.bz2 | tar jxf - ./grcov . --binary-path ./target/debug/deps -s . -t lcov --branch --ignore-not-existing --ignore "../*" --ignore "/*" -o cov.lcov - - uses: codecov/codecov-action@v3 + - uses: codecov/codecov-action@v4 if: runner.os == 'Linux' with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/Cargo.toml b/Cargo.toml index c54e543ed..797a46e9c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,18 +26,19 @@ freya-testing = { path = "crates/testing", version = "0.1" } freya-engine = { path = "crates/engine", version = "0.1" } torin = { path = "crates/torin", version = "0.1" } -dioxus = { git = "https://github.com/DioxusLabs/dioxus", rev = "53380c9956c7dda54d9251d3bc48eaa0ec4e89be", default-features = false, features = ["macro", "signals", "hooks", "hot-reload", "html"]} -dioxus-native-core-macro = { git = "https://github.com/DioxusLabs/dioxus", rev = "53380c9956c7dda54d9251d3bc48eaa0ec4e89be" } -dioxus-rsx = { git = "https://github.com/DioxusLabs/dioxus", rev = "53380c9956c7dda54d9251d3bc48eaa0ec4e89be", features = ["hot_reload"] } -dioxus-native-core = { git = "https://github.com/DioxusLabs/dioxus", rev = "53380c9956c7dda54d9251d3bc48eaa0ec4e89be", features = ["dioxus"] } -dioxus-core-macro = { git = "https://github.com/DioxusLabs/dioxus", rev = "53380c9956c7dda54d9251d3bc48eaa0ec4e89be" } -dioxus-hooks = { git = "https://github.com/DioxusLabs/dioxus", rev = "53380c9956c7dda54d9251d3bc48eaa0ec4e89be" } -dioxus-signals = { git = "https://github.com/DioxusLabs/dioxus", rev = "53380c9956c7dda54d9251d3bc48eaa0ec4e89be" } -dioxus-core = { git = "https://github.com/DioxusLabs/dioxus", rev = "53380c9956c7dda54d9251d3bc48eaa0ec4e89be" } -dioxus-hot-reload = { git = "https://github.com/DioxusLabs/dioxus", rev = "53380c9956c7dda54d9251d3bc48eaa0ec4e89be", features = ["file_watcher"], default-features = false } -dioxus-router = { git = "https://github.com/DioxusLabs/dioxus", rev = "53380c9956c7dda54d9251d3bc48eaa0ec4e89be", default-features = false } +dioxus = { git = "https://github.com/DioxusLabs/dioxus", rev = "a454f2c7ac239933bb4d5cd184c8a549190070f1", default-features = false, features = ["macro", "signals", "hooks", "hot-reload", "html"]} +dioxus-native-core-macro = { git = "https://github.com/DioxusLabs/dioxus", rev = "a454f2c7ac239933bb4d5cd184c8a549190070f1" } +dioxus-rsx = { git = "https://github.com/DioxusLabs/dioxus", rev = "a454f2c7ac239933bb4d5cd184c8a549190070f1", features = ["hot_reload"] } +dioxus-native-core = { git = "https://github.com/DioxusLabs/dioxus", rev = "a454f2c7ac239933bb4d5cd184c8a549190070f1", features = ["dioxus"] } +dioxus-core-macro = { git = "https://github.com/DioxusLabs/dioxus", rev = "a454f2c7ac239933bb4d5cd184c8a549190070f1" } +dioxus-hooks = { git = "https://github.com/DioxusLabs/dioxus", rev = "a454f2c7ac239933bb4d5cd184c8a549190070f1" } +dioxus-signals = { git = "https://github.com/DioxusLabs/dioxus", rev = "a454f2c7ac239933bb4d5cd184c8a549190070f1" } +dioxus-core = { git = "https://github.com/DioxusLabs/dioxus", rev = "a454f2c7ac239933bb4d5cd184c8a549190070f1" } +dioxus-hot-reload = { git = "https://github.com/DioxusLabs/dioxus", rev = "a454f2c7ac239933bb4d5cd184c8a549190070f1", features = ["file_watcher"], default-features = false } +dioxus-router = { git = "https://github.com/DioxusLabs/dioxus", rev = "a454f2c7ac239933bb4d5cd184c8a549190070f1", default-features = false } +dioxus-std = { git = "https://github.com/marc2332/dioxus-std", rev = "2e111cda95f816fe4232bae5e75515a58afa4a1e", features = ["clipboard"] } -skia-safe = { version = "0.67.0", features = ["gl", "textlayout", "svg"] } +skia-safe = { version = "0.70.0", features = ["gl", "textlayout", "svg"] } gl = "0.14.0" glutin = "0.31.2" @@ -47,12 +48,11 @@ winit = "0.29.9" tokio = { version = "1.33.0", features = ["sync", "rt-multi-thread", "time", "macros"] } accesskit = { version = "0.12.2", features = ["serde"]} accesskit_winit = "0.18.0" -zbus = "3.14.1" euclid = "0.22.9" uuid = { version = "1.4.1", features = ["v4"]} -futures = "0.3.28" -anymap = "0.12.1" +futures-util = "0.3.30" +futures-task = "0.3.30" tracing = "0.1" tracing-subscriber = "0.3.17" rustc-hash = "1.1.0" @@ -69,7 +69,7 @@ freya-core = { workspace = true } reqwest = { version = "0.11.22", features = ["json"] } serde = "1.0.189" tracing-subscriber = "0.3.17" -dioxus-std = { git = "https://github.com/DioxusLabs/dioxus-std", rev = "137b4149dc86a648119eef8f331e3a682c2c6b62", features = ["i18n"] } +dioxus-std = { git = "https://github.com/marc2332/dioxus-std", rev = "2e111cda95f816fe4232bae5e75515a58afa4a1e", features = ["i18n"] } rand = "0.8.5" dioxus-router = { workspace = true } itertools = "0.11.0" diff --git a/README.md b/README.md index bc49e1b2f..98902c637 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,10 @@ ```rust, no_run -fn app() -> Element { - let mut count = use_signal(|| 0); +fn app(cx: Scope) -> Element { + let mut count = use_state(cx, || 0); - rsx!( + render!( rect { height: "20%", width: "100%", @@ -59,10 +59,12 @@ fn app() -> Element { Thanks to my sponsors for supporting this project! 😄 -Alberto MendezStephen Andary +Alberto MendezAndar1an ### Want to try it? 🤔 +Note: `main` branch currently depends on Dioxus 0.5. + ⚠️ First, see [Environment setup](https://book.freyaui.dev/setup.html). Clone this repo and run: diff --git a/book/src/guides/animating.md b/book/src/guides/animating.md index 08e24d37a..9afdf8808 100644 --- a/book/src/guides/animating.md +++ b/book/src/guides/animating.md @@ -18,16 +18,16 @@ fn main() { launch(app); } - fn app() -> Element { - let mut animation = use_animation(|| 0.0); + fn app(cx: Scope) -> Element { + let animation = use_animation(cx, || 0.0); let progress = animation.value(); - use_hook(move || { + use_memo(cx, (), move |_| { animation.start(Animation::new_linear(0.0..=100.0, 50)); - }) + }); - rsx!(rect { + render!(rect { width: "{progress}", }) } @@ -53,8 +53,8 @@ fn main() { const TARGET: f64 = 500.0; -fn app() -> Element { - let mut animation = use_animation_transition(TransitionAnimation::new_sine_in_out(200), (), || { +fn app(cx: Scope) -> Element { + let animation = use_animation_transition(cx, TransitionAnimation::new_sine_in_out(200), (), || { vec![ Animate::new_size(0.0, TARGET), Animate::new_color("rgb(33, 158, 188)", "white"), @@ -72,7 +72,7 @@ fn app() -> Element { } }; - rsx!( + render!( rect { overflow: "clip", background: "black", @@ -83,7 +83,7 @@ fn app() -> Element { height: "100%", width: "200", background: "{background}", - onclick, + onclick: onclick, } } ) diff --git a/book/src/guides/effects.md b/book/src/guides/effects.md index 5f0c9703a..494fba317 100644 --- a/book/src/guides/effects.md +++ b/book/src/guides/effects.md @@ -11,8 +11,8 @@ The `rotate` attribute let's you rotate an element. Example: ```rust, no_run -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( label { rotate: "180deg", "Hello, World!" diff --git a/book/src/guides/elements.md b/book/src/guides/elements.md index 660c50e20..eb7afd058 100644 --- a/book/src/guides/elements.md +++ b/book/src/guides/elements.md @@ -16,8 +16,8 @@ You can specify things like [`width`](/guides/layout.html#width), [`paddings`](/ Example: ```rust, no_run -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( rect { direction: "vertical", label { "Hi!" } @@ -34,8 +34,8 @@ The `label` element simply shows some text. Example: ```rust, no_run -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( label { "Hello World" } @@ -53,9 +53,9 @@ Example: static FERRIS: &[u8] = include_bytes!("./ferris.svg"); -fn app() -> Element { - let ferris = bytes_to_data(FERRIS); - rsx!( +fn app(cx: Scope) -> Element { + let ferris = bytes_to_data(cx, FERRIS); + render!( svg { svg_data: ferris, } @@ -70,9 +70,9 @@ The `image` element, just like `svg` element, require you to pass the image byte ```rust, no_run static RUST_LOGO: &[u8] = include_bytes!("./rust_logo.png"); -fn app() -> Element { - let image_data = bytes_to_data(RUST_LOGO); - rsx!( +fn app(cx: Scope) -> Element { + let image_data = bytes_to_data(cx, RUST_LOGO); + render!( image { image_data: image_data, width: "{size}", @@ -87,8 +87,8 @@ fn app() -> Element { Both `paragraph` and `text` elements are used together. They will let you build texts with different styles. ``` rust -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( paragraph { text { font_size: "15", diff --git a/book/src/guides/font_style.md b/book/src/guides/font_style.md index d63cf10dd..9d889b1e9 100644 --- a/book/src/guides/font_style.md +++ b/book/src/guides/font_style.md @@ -29,8 +29,8 @@ You can learn about the syntax of this attribute in [`Color Syntax`](/guides/sty Example: ```rust, no_run -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( label { color: "green", "Hello, World!" @@ -42,8 +42,8 @@ fn app() -> Element { Another example showing [inheritance](#inheritance): ```rust, no_run -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( rect { color: "blue", label { @@ -66,8 +66,8 @@ Limitation: Only fonts installed in the system are supported for now. Example: ```rust, no_run -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( label { font_family: "Inter", "Hello, World!" @@ -85,8 +85,8 @@ You can specify the size of the text using `font_size`. Example: ```rust, no_run -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( label { font_size: "50", "Hellooooo!" @@ -106,8 +106,8 @@ Accepted values: `center`, `end`, `justify`, `left`, `right`, `start` Example ```rust, no_run -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( label { align: "right", "Hello, World!" @@ -127,8 +127,8 @@ Accepted values: `upright` (default), `italic` and `oblique`. Example: ```rust, no_run -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( label { font_style: "italic", "Hello, World!" @@ -170,8 +170,8 @@ Accepted values: Example: ```rust, no_run -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( label { font_weight: "bold", "Hello, World!" @@ -199,8 +199,8 @@ Accepted values: Example: ```rust, no_run -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( label { font_weight: "bold", "Hello, World!" @@ -220,8 +220,8 @@ Specify the height of the lines of the text. Example: ```rust, no_run -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( label { lines_height: "3", "Hello, World! \n Hello, again!" @@ -237,13 +237,13 @@ Determines the amount of lines that the text can have. It has unlimited lines by Example: ```rust, no_run -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( label { "Hello, World! \n Hello, World! \n Hello, world!" // Will show all three lines } label { - lines_height: "2", + max_lines: "2", "Hello, World! \n Hello, World! \n Hello, world!" // Will only show two lines } ) @@ -259,8 +259,8 @@ Specify the spacing between characters of the text. Example: ```rust, no_run -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( label { letter_spacing: "10", "Hello, World!" @@ -278,8 +278,8 @@ Specify the spacing between words of the text. Example: ```rust, no_run -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( label { word_spacing: "10", "Hello, World!" @@ -302,8 +302,8 @@ Accpted values: Example: ```rust, no_run -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( label { decoration: "line-through", "Hello, World!" @@ -328,8 +328,8 @@ Accpted values: Example: ```rust, no_run -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( label { decoration: "line-through", decoration_style: "dotted", @@ -350,8 +350,8 @@ You can learn about the syntax of this attribute in [`Color Syntax`](/guides/sty Example: ```rust, no_run -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( label { decoration: "line-through", decoration_color: "orange", @@ -372,8 +372,8 @@ Syntax: ` ` Example: ```rust, no_run -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( label { text_shadow: "0 18 12 rgb(0, 0, 0)", "Hello, World!" @@ -395,8 +395,8 @@ Accepted values: Example: ```rust, no_run -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( label { max_lines: "3", text_overflow: "ellipsis", diff --git a/book/src/guides/getting_started.md b/book/src/guides/getting_started.md index 3a303f5e9..3ce82c631 100644 --- a/book/src/guides/getting_started.md +++ b/book/src/guides/getting_started.md @@ -43,10 +43,10 @@ fn main() { launch(app); } -fn app() -> Element { - let mut count = use_signal(|| 0); +fn app(cx: Scope) -> Element { + let mut count = use_state(cx, || 0); - rsx!( + render!( rect { height: "100%", width: "100%", diff --git a/book/src/guides/hot_reload.md b/book/src/guides/hot_reload.md index a463f7bde..1d61f57dc 100644 --- a/book/src/guides/hot_reload.md +++ b/book/src/guides/hot_reload.md @@ -8,7 +8,7 @@ Before launching your app, you need to initialize the hot-reload context: ```rust, no_run use freya::prelude::*; -use freya::hot_reload::FreyaCtx; +use freya::hotreload::FreyaCtx; fn main() { dioxus_hot_reload::hot_reload_init!(Config::::default()); diff --git a/book/src/guides/layout.md b/book/src/guides/layout.md index bc5de08b3..ea49c7752 100644 --- a/book/src/guides/layout.md +++ b/book/src/guides/layout.md @@ -24,8 +24,8 @@ See syntax for [`Size Units`](#size-units). ##### Usage ```rust, no_run -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( rect { background: "red", width: "15", @@ -44,8 +44,8 @@ See syntax for [`Size Units`](#size-units). ##### Usage ```rust, no_run -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( rect { background: "red", min_width: "100", @@ -66,8 +66,8 @@ See syntax for [`Size Units`](#size-units). ##### Usage ```rust, no_run -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( rect { background: "red", max_width: "50%", @@ -85,8 +85,8 @@ fn app() -> Element { Will use it's inner children as size, so in this case, the `rect` width will be equivalent to the width of `label`: ```rust, no_run -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( rect { width: "auto", height: "33", @@ -101,8 +101,8 @@ fn app() -> Element { #### Logical pixels ```rust, no_run -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( rect { width: "50", height: "33" @@ -115,8 +115,8 @@ fn app() -> Element { Relative percentage to the parent equivalent value. ```rust, no_run -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( rect { width: "50%", // Half the parent height: "75%" // 3/4 the parent @@ -130,8 +130,8 @@ fn app() -> Element { For more complex logic you can use the `calc()` function. ```rust, no_run -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( rect { width: "calc(33% - 60 + 15%)", // (1/3 of the parent minus 60) plus 15% of parent height: "calc(100% - 10)" // 100% of the parent minus 10 @@ -147,8 +147,8 @@ Control how the inner elements will be stacked, possible values are `horizontal` ##### Usage ```rust, no_run -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( rect { width: "100%", height: "100%", @@ -173,8 +173,8 @@ fn app() -> Element { Specify the inner paddings of an element. You can do so by three different ways. ```rust, no_run -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( rect { padding: "25" // 25 in all sides padding: "100 50" // 100 in top and bottom, and 50 in left and right @@ -190,8 +190,8 @@ fn app() -> Element { Control how the inner elements are displayed, possible values are `normal` (default) or `center`. ```rust, no_run -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( rect { width: "100%", height: "100%", @@ -212,8 +212,8 @@ fn app() -> Element { Specify the margin of an element. You can do so by three different ways. ```rust, no_run -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( rect { margin: "25" // 25 in all sides margin: "100 50" // 100 in top and bottom, and 50 in left and right diff --git a/book/src/guides/style.md b/book/src/guides/style.md index ce9773c04..2053e54de 100644 --- a/book/src/guides/style.md +++ b/book/src/guides/style.md @@ -21,8 +21,8 @@ You can learn about the syntax of this attribute [here](#color-syntax). Example: ```rust, no_run -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( rect { background: "red" } @@ -42,8 +42,8 @@ Syntax: ` ` Example: ```rust, no_run -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( rect { shadow: "0 0 25 2 rgb(0, 0, 0, 120)" } @@ -60,8 +60,8 @@ The `corner_radius` attribute let's you smooth the corners of the element, with Example: ```rust, no_run -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( rect { corner_radius: "10", corner_smoothing: "75%" @@ -82,8 +82,8 @@ You can add a border to an element using the `border` and `border_align` attribu Example: ```rust, no_run -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( rect { border: "2 solid black", border_align: "inner" @@ -103,8 +103,8 @@ Accepted values: `clip | none`. Example: ```rust, no_run -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( rect { overflow: "clip" width: "100", diff --git a/book/src/guides/testing.md b/book/src/guides/testing.md index 296480191..0cf151773 100644 --- a/book/src/guides/testing.md +++ b/book/src/guides/testing.md @@ -12,8 +12,8 @@ For example, this will launch a state-less component and assert that it renders ```rust, no_run #[tokio::test] async fn test() { - fn our_component() -> Element { - rsx!( + fn our_component(cx: Scope) -> Element { + render!( label { "Hello World!" } @@ -41,12 +41,15 @@ Here, the component has a state that is `false` by default, but, once mounted it ```rust, no_run #[tokio::test] async fn dynamic_test() { - fn dynamic_component() -> Element { - let mut state = use_signal(|| false); + fn dynamic_component(cx: Scope) -> Element { + let state = use_state(cx, || false); - use_hook(move || state.set(true)); + use_effect(cx, (), |_| { + state.set(true); + async move { } + }); - rsx!( + render!( label { "Is enabled? {state}" } @@ -60,7 +63,7 @@ async fn dynamic_test() { assert_eq!(label.get(0).text(), Some("Is enabled? false")); - // This will run the `use_hook` and update the state. + // This will run the `use_effect` and update the state. utils.wait_for_update().await; assert_eq!(label.get(0).text(), Some("Is enabled? true")); @@ -74,14 +77,14 @@ We can also simulate events on the component, for example, we can simulate a cli ```rust, no_run #[tokio::test] async fn event_test() { - fn event_component() -> Element { - let mut enabled = use_signal(|| false); - rsx!( + fn event_component(cx: Scope) -> Element { + let enabled = use_state(cx, || false); + render!( rect { width: "100%", height: "100%", background: "red", - onclick: move |_| { + onclick: |_| { enabled.set(true); }, label { @@ -126,8 +129,8 @@ Here is an example of how to can set our custom window size: ```rust, no_run #[tokio::test] async fn test() { - fn our_component() -> Element { - rsx!( + fn our_component(cx: Scope) -> Element { + render!( label { "Hello World!" } diff --git a/book/src/guides/theming.md b/book/src/guides/theming.md index a172b218f..e192dc95f 100644 --- a/book/src/guides/theming.md +++ b/book/src/guides/theming.md @@ -9,8 +9,8 @@ Freya has built-in support for Theming. You can access the whole current theme via the `use_get_theme` hook. ```rust, no_run -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( ThemeProvider { Component { } } @@ -18,12 +18,12 @@ fn app() -> Element { } #[allow(non_snake_case)] -fn Component() -> Element { - let theme = use_get_theme(); +fn Component(cx: Scope) -> Element { + let theme = use_get_theme(cx); let button_theme = &theme.button; - rsx!( + render!( rect { background: "{button_theme.background}", } @@ -35,8 +35,8 @@ fn Component() -> Element { By default, the selected theme is `LIGHT_THEME`. You can use the alternative, `DARK_THEME`. ```rust, no_run -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( ThemeProvider { theme: LIGHT_THEME, Component { } @@ -45,12 +45,12 @@ fn app() -> Element { } #[allow(non_snake_case)] -fn Component() -> Element { - let theme = use_get_theme(); +fn Component(cx: Scope) -> Element { + let theme = use_get_theme(cx); let button_theme = &theme.button; - rsx!( + render!( rect { background: "{button_theme.background}", } @@ -63,8 +63,8 @@ fn Component() -> Element { Changing the selected theme at runtime is possible by using the `use_theme` hook. ```rust, no_run -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( ThemeProvider { Component { } } @@ -72,16 +72,16 @@ fn app() -> Element { } #[allow(non_snake_case)] -fn Component() -> Element { - let mut theme = use_theme(); +fn Component(cx: Scope) -> Element { + let theme = use_theme(cx); - let onclick = move |_| { + let onclick = |_| { *theme.write() = LIGHT_THEME; }; - rsx!( + render!( Button { - onclick, + onclick: onclick, label { "Use Light theme" } @@ -104,8 +104,8 @@ const CUSTOM_THEME: Theme = Theme { ..LIGHT_THEME }; -fn app() -> Element { - rsx!( +fn app(cx: Scope) -> Element { + render!( ThemeProvider { theme: CUSTOM_THEME, rect { diff --git a/book/src/guides/virtualizing.md b/book/src/guides/virtualizing.md index 5de8fd9f0..9a22020da 100644 --- a/book/src/guides/virtualizing.md +++ b/book/src/guides/virtualizing.md @@ -19,19 +19,19 @@ fn main() { launch(app); } -fn app() -> Element { - let values = use_signal(|| vec!["Hello World"].repeat(400)); +fn app(cx: Scope) -> Element { + let values = use_state(cx, || vec!["Hello World"].repeat(400)); - rsx!( + render!( VirtualScrollView { width: "100%", height: "100%", show_scrollbar: true, direction: "vertical", - length: values.read().len(), + length: values.get().len(), item_size: 25.0, - builder_values: values.read().clone(), - builder: Rc::new(move |(key, index, values)| { + builder_values: values.get(), + builder: Box::new(move |(key, index, _cx, values)| { let values = values.unwrap(); let value = values[index]; rsx! { @@ -72,4 +72,4 @@ Used to calculate how many elements can be fit in the viewport. Any data that you might need in the `builder` function #### `builder` -This is a function that dinamically creates an element for the given index in the list. It receives 4 arguments, a `key` for the element, the `index` of the element and the `builder_values` you previously passed. \ No newline at end of file +This is a function that dinamically creates an element for the given index in the list. It receives 4 arguments, a `key` for the element, the `index` of the element, the Scope (`cx`) and the `builder_values` you previously passed. \ No newline at end of file diff --git a/book/src/index.md b/book/src/index.md index 1d352c2cc..6eea72d1d 100644 --- a/book/src/index.md +++ b/book/src/index.md @@ -16,10 +16,10 @@ ```rust, no_run -fn app() -> Element { - let mut count = use_signal(|| 0); +fn app(cx: Scope) -> Element { + let mut count = use_state(cx, || 0); - rsx!( + render!( rect { height: "20%", width: "100%", diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index fbb139936..f73056ed4 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -14,15 +14,10 @@ categories = ["gui", "asynchronous"] [dependencies] torin = { workspace = true } -dioxus-rsx = { workspace = true } -dioxus-native-core = { workspace = true } -dioxus-core-macro = { workspace = true } -dioxus-hooks = { workspace = true } dioxus-core = { workspace = true } accesskit = { workspace = true } accesskit_winit = { workspace = true } winit = { workspace = true } -euclid = { workspace = true } uuid = { workspace = true } diff --git a/crates/common/src/event_messages.rs b/crates/common/src/event_messages.rs index bc4ac3d76..d4ab3479d 100644 --- a/crates/common/src/event_messages.rs +++ b/crates/common/src/event_messages.rs @@ -11,12 +11,8 @@ pub enum EventMessage { UpdateTemplate(Template), /// Pull the VirtualDOM PollVDOM, - /// Request a layout recalculation - RequestRelayout, /// Request a rerender RequestRerender, - /// Request a redraw - RequestRedraw, /// Remeasure a text elements group RemeasureTextGroup(Uuid), /// Change the cursor icon diff --git a/crates/components/Cargo.toml b/crates/components/Cargo.toml index 7d7ba7c3d..9f0e8221f 100644 --- a/crates/components/Cargo.toml +++ b/crates/components/Cargo.toml @@ -22,13 +22,12 @@ freya-elements = { workspace = true } freya-node-state = { workspace = true } freya-hooks = { workspace = true } freya-common = { workspace = true } -freya-core = { path = "../core", version = "0.1" } freya-engine = { path = "../engine", version = "0.1" } torin = { workspace = true } -dioxus = { workspace = true } dioxus-router = { workspace = true } -futures = { workspace = true } +dioxus = { workspace = true } +futures-util = { workspace = true } winit = { workspace = true } tokio = { workspace = true } @@ -39,4 +38,4 @@ reqwest = { version = "0.11.22", features = ["json"] } [dev-dependencies] freya = { path = "../freya" } -freya-testing = { path = "../testing" } \ No newline at end of file +freya-testing = { path = "../testing" } diff --git a/crates/components/src/accordion.rs b/crates/components/src/accordion.rs index 6f79a70ad..23e7c241b 100644 --- a/crates/components/src/accordion.rs +++ b/crates/components/src/accordion.rs @@ -3,8 +3,8 @@ use freya_elements::elements as dioxus_elements; use freya_elements::events::MouseEvent; use freya_hooks::{ - use_animation, use_applied_theme, use_node_signal, use_platform, AccordionTheme, - AccordionThemeWith, Animation, + use_animation_with_dependencies, use_applied_theme, use_node, use_platform, AccordionTheme, + AccordionThemeWith, AnimNum, }; use winit::window::CursorIcon; @@ -40,54 +40,40 @@ pub struct AccordionProps { #[allow(non_snake_case)] pub fn Accordion(props: AccordionProps) -> Element { let theme = use_applied_theme!(&props.theme, accordion); - let mut animation = use_animation(|| 0.0); let mut open = use_signal(|| false); - let (node_ref, size) = use_node_signal(); + let (node_ref, size) = use_node(); + + let animation = use_animation_with_dependencies(&size.area.height(), move |ctx, height| { + ctx.with(AnimNum::new(0., height).time(200)) + }); let mut status = use_signal(AccordionStatus::default); let platform = use_platform(); - let animation_value = animation.value(); + let animation_value = animation.read().get().read().as_f32(); let AccordionTheme { background, color, border_fill, } = theme; - // Adapt the accordion if the body size changes - use_memo({ - to_owned![animation]; - move || { - if (size().area.height() as f64) < animation.value() && !animation.is_animating() { - animation.set_value(size().area.height() as f64); - } - } - }); - let onclick = move |_: MouseEvent| { - let bodyHeight = size.peek().area.height() as f64; + open.toggle(); if *open.read() { - animation.start(Animation::new_sine_in_out(bodyHeight..=0.0, 200)); + animation.read().start(); } else { - animation.start(Animation::new_sine_in_out(0.0..=bodyHeight, 200)); + animation.read().reverse(); } - open.toggle(); }; - use_drop({ - to_owned![status, platform]; - move || { - if *status.read() == AccordionStatus::Hovering { - platform.set_cursor(CursorIcon::default()); - } + use_drop(move || { + if *status.read() == AccordionStatus::Hovering { + platform.set_cursor(CursorIcon::default()); } }); - let onmouseenter = { - to_owned![status, platform]; - move |_| { - platform.set_cursor(CursorIcon::Pointer); - status.set(AccordionStatus::Hovering); - } + let onmouseenter = move |_| { + platform.set_cursor(CursorIcon::Pointer); + status.set(AccordionStatus::Hovering); }; let onmouseleave = move |_| { @@ -162,3 +148,53 @@ pub fn AccordionBody(props: AccordionBodyProps) -> Element { {props.children} }) } + +#[cfg(test)] +mod test { + use freya::prelude::*; + use freya_testing::{launch_test, EventName, PlatformEvent}; + use winit::event::MouseButton; + + #[tokio::test] + pub async fn accordion() { + fn accordion_app() -> Element { + rsx!( + Accordion { + summary: rsx!(AccordionSummary { + label { + "Accordion Summary" + } + }), + AccordionBody { + label { + "Accordion Body" + } + } + } + ) + } + + let mut utils = launch_test(accordion_app); + + let root = utils.root(); + let content = root.get(0).get(1).get(0); + let label = content.get(0); + utils.wait_for_update().await; + utils.wait_for_update().await; + + // Accordion is closed, therefore label is hidden. + assert!(!label.is_visible()); + + // Click on the accordion + utils.push_event(PlatformEvent::Mouse { + name: EventName::Click, + cursor: (5., 5.).into(), + button: Some(MouseButton::Left), + }); + + utils.wait_for_update().await; + + // Accordion is open, therefore label is visible. + assert!(label.is_visible()); + } +} diff --git a/crates/components/src/button.rs b/crates/components/src/button.rs index 070062a46..2fea81fe2 100644 --- a/crates/components/src/button.rs +++ b/crates/components/src/button.rs @@ -52,7 +52,7 @@ pub enum ButtonStatus { /// #[allow(non_snake_case)] pub fn Button(props: ButtonProps) -> Element { - let focus = use_focus(); + let mut focus = use_focus(); let mut status = use_signal(ButtonStatus::default); let platform = use_platform(); @@ -73,7 +73,7 @@ pub fn Button(props: ButtonProps) -> Element { } = use_applied_theme!(&props.theme, button); let onclick = { - to_owned![focus, click]; + to_owned![click]; move |ev| { focus.focus(); if let Some(onclick) = &click { @@ -82,21 +82,15 @@ pub fn Button(props: ButtonProps) -> Element { } }; - use_drop({ - to_owned![status, platform]; - move || { - if *status.read() == ButtonStatus::Hovering { - platform.set_cursor(CursorIcon::default()); - } + use_drop(move || { + if *status.read() == ButtonStatus::Hovering { + platform.set_cursor(CursorIcon::default()); } }); - let onmouseenter = { - to_owned![status, platform]; - move |_| { - platform.set_cursor(CursorIcon::Pointer); - status.set(ButtonStatus::Hovering); - } + let onmouseenter = move |_| { + platform.set_cursor(CursorIcon::Pointer); + status.set(ButtonStatus::Hovering); }; let onmouseleave = move |_| { @@ -104,13 +98,10 @@ pub fn Button(props: ButtonProps) -> Element { status.set(ButtonStatus::default()); }; - let onkeydown = { - to_owned![focus]; - move |e: KeyboardEvent| { - if focus.validate_keydown(e) { - if let Some(onclick) = &props.onclick { - onclick.call(None) - } + let onkeydown = move |e: KeyboardEvent| { + if focus.validate_keydown(e) { + if let Some(onclick) = &props.onclick { + onclick.call(None) } } }; @@ -151,3 +142,43 @@ pub fn Button(props: ButtonProps) -> Element { } ) } + +#[cfg(test)] +mod test { + use dioxus::prelude::use_signal; + use freya::prelude::*; + use freya_testing::*; + + #[tokio::test] + pub async fn button() { + fn button_app() -> Element { + let mut state = use_signal(|| false); + + rsx!( + Button { + onclick: move |_| state.toggle(), + label { + "{state}" + } + } + ) + } + + let mut utils = launch_test(button_app); + let root = utils.root(); + let label = root.get(0).get(0); + utils.wait_for_update().await; + + assert_eq!(label.get(0).text(), Some("false")); + + utils.push_event(PlatformEvent::Mouse { + name: EventName::Click, + cursor: (5.0, 5.0).into(), + button: Some(MouseButton::Left), + }); + + utils.wait_for_update().await; + + assert_eq!(label.get(0).text(), Some("true")); + } +} diff --git a/crates/components/src/cursor_area.rs b/crates/components/src/cursor_area.rs index 6ec97df05..2c4873565 100644 --- a/crates/components/src/cursor_area.rs +++ b/crates/components/src/cursor_area.rs @@ -1,7 +1,7 @@ use dioxus::prelude::*; use freya_elements::elements as dioxus_elements; use freya_hooks::use_platform; -use winit::window::CursorIcon; +pub use winit::window::CursorIcon; /// [`CursorArea`] component properties. #[derive(Props, Clone, PartialEq)] @@ -39,22 +39,16 @@ pub struct CursorAreaProps { #[allow(non_snake_case)] pub fn CursorArea(CursorAreaProps { children, icon }: CursorAreaProps) -> Element { let platform = use_platform(); - let is_hovering = use_signal(|| false); + let mut is_hovering = use_signal(|| false); - let onmouseover = { - to_owned![platform, is_hovering]; - move |_| { - *is_hovering.write() = true; - platform.set_cursor(icon); - } + let onmouseover = move |_| { + *is_hovering.write() = true; + platform.set_cursor(icon); }; - let onmouseleave = { - to_owned![platform]; - move |_| { - *is_hovering.write() = false; - platform.set_cursor(CursorIcon::default()); - } + let onmouseleave = move |_| { + *is_hovering.write() = false; + platform.set_cursor(CursorIcon::default()); }; use_drop(move || { @@ -71,3 +65,70 @@ pub fn CursorArea(CursorAreaProps { children, icon }: CursorAreaProps) -> Elemen } ) } + +#[cfg(test)] +mod test { + use freya::prelude::*; + use freya_testing::*; + use winit::{event::MouseButton, window::CursorIcon}; + + #[tokio::test] + pub async fn cursor_area() { + fn cursor_area_app() -> Element { + rsx!( + CursorArea { + icon: CursorIcon::Progress, + rect { + height: "50%", + width: "100%", + } + } + CursorArea { + icon: CursorIcon::Pointer, + rect { + height: "50%", + width: "100%", + } + } + ) + } + + let mut utils = launch_test(cursor_area_app); + + // Initial cursor + assert_eq!(utils.cursor_icon(), CursorIcon::default()); + + utils.push_event(PlatformEvent::Mouse { + name: EventName::MouseOver, + cursor: (100., 100.).into(), + button: Some(MouseButton::Left), + }); + + utils.wait_for_update().await; + + // Cursor after hovering the first half + assert_eq!(utils.cursor_icon(), CursorIcon::Progress); + + utils.push_event(PlatformEvent::Mouse { + name: EventName::MouseOver, + cursor: (100., 300.).into(), + button: Some(MouseButton::Left), + }); + + utils.wait_for_update().await; + + // Cursor after hovering the second half + assert_eq!(utils.cursor_icon(), CursorIcon::Pointer); + + utils.push_event(PlatformEvent::Mouse { + name: EventName::MouseOver, + cursor: (-1., -1.).into(), + button: Some(MouseButton::Left), + }); + + utils.wait_for_update().await; + + // Cursor after leaving the window + assert_eq!(utils.cursor_icon(), CursorIcon::default()); + } +} diff --git a/crates/components/src/drag_drop.rs b/crates/components/src/drag_drop.rs index 461534f2b..ae866464e 100644 --- a/crates/components/src/drag_drop.rs +++ b/crates/components/src/drag_drop.rs @@ -24,7 +24,7 @@ pub fn DragProvider(DragProviderProps { children }: DragProviderProp /// [`DragZone`] component properties. #[derive(Props, Clone, PartialEq)] -pub struct DragZoneProps { +pub struct DragZoneProps { /// Element visible when dragging the element. This follows the cursor. drag_element: Element, /// Inner children for the DropZone. @@ -39,14 +39,14 @@ pub struct DragZoneProps { /// See [`DragZoneProps`]. /// #[allow(non_snake_case)] -pub fn DragZone( +pub fn DragZone( DragZoneProps { data, children, drag_element, }: DragZoneProps, ) -> Element { - let drags = use_context::>>(); + let mut drags = use_context::>>(); let mut dragging = use_signal(|| false); let mut pos = use_signal(CursorPoint::default); let (node_reference, size) = use_node_signal(); @@ -108,31 +108,22 @@ pub fn DragZone( } /// [`DropZone`] component properties. -#[derive(Props, PartialEq)] -pub struct DropZoneProps { +#[derive(Props, PartialEq, Clone)] +pub struct DropZoneProps { /// Inner children for the DropZone. children: Element, /// Handler for the `ondrop` event. ondrop: EventHandler, } -impl Clone for DropZoneProps { - fn clone(&self) -> Self { - Self { - children: self.children.clone(), - ondrop: self.ondrop.clone(), - } - } -} - /// Elements from [`DragZone`]s can be dropped here. /// /// # Props /// See [`DropZoneProps`]. /// #[allow(non_snake_case)] -pub fn DropZone(props: DropZoneProps) -> Element { - let drags = use_context::>>(); +pub fn DropZone(props: DropZoneProps) -> Element { + let mut drags = use_context::>>(); let onclick = move |_: MouseEvent| { if let Some(current_drags) = &*drags.read() { @@ -153,9 +144,8 @@ pub fn DropZone(props: DropZoneProps) -> Element { #[cfg(test)] mod test { - use dioxus::signals::use_signal; use freya::prelude::*; - use freya_testing::{events::pointer::MouseButton, launch_test, FreyaEvent}; + use freya_testing::{events::pointer::MouseButton, launch_test, EventName, PlatformEvent}; #[tokio::test] pub async fn drag_drop() { @@ -200,24 +190,24 @@ mod test { let root = utils.root(); utils.wait_for_update().await; - utils.push_event(FreyaEvent::Mouse { - name: "mousedown".to_string(), + utils.push_event(PlatformEvent::Mouse { + name: EventName::MouseDown, cursor: (5.0, 5.0).into(), button: Some(MouseButton::Left), }); utils.wait_for_update().await; - utils.push_event(FreyaEvent::Mouse { - name: "mouseover".to_string(), + utils.push_event(PlatformEvent::Mouse { + name: EventName::MouseOver, cursor: (5.0, 5.0).into(), button: Some(MouseButton::Left), }); utils.wait_for_update().await; - utils.push_event(FreyaEvent::Mouse { - name: "mouseover".to_string(), + utils.push_event(PlatformEvent::Mouse { + name: EventName::MouseOver, cursor: (5.0, 300.0).into(), button: Some(MouseButton::Left), }); @@ -226,8 +216,8 @@ mod test { assert_eq!(root.get(0).get(0).get(0).get(0).text(), Some("Moving")); - utils.push_event(FreyaEvent::Mouse { - name: "click".to_string(), + utils.push_event(PlatformEvent::Mouse { + name: EventName::Click, cursor: (5.0, 300.0).into(), button: Some(MouseButton::Left), }); diff --git a/crates/components/src/dropdown.rs b/crates/components/src/dropdown.rs index 7dc3a8d91..ac823cd2d 100644 --- a/crates/components/src/dropdown.rs +++ b/crates/components/src/dropdown.rs @@ -14,7 +14,7 @@ use winit::window::CursorIcon; /// [`DropdownItem`] component properties. #[derive(Props, Clone, PartialEq)] -pub struct DropdownItemProps { +pub struct DropdownItemProps { /// Theme override. pub theme: Option, /// Selectable items, like [`DropdownItem`] @@ -72,21 +72,15 @@ where }; let color = theme.font_theme.color; - use_drop({ - to_owned![status, platform]; - move || { - if *status.peek() == DropdownItemStatus::Hovering { - platform.set_cursor(CursorIcon::default()); - } + use_drop(move || { + if *status.peek() == DropdownItemStatus::Hovering { + platform.set_cursor(CursorIcon::default()); } }); - let onmouseenter = { - to_owned![platform]; - move |_| { - platform.set_cursor(CursorIcon::Pointer); - status.set(DropdownItemStatus::Hovering); - } + let onmouseenter = move |_| { + platform.set_cursor(CursorIcon::Pointer); + status.set(DropdownItemStatus::Hovering); }; let onmouseleave = move |_| { @@ -132,7 +126,7 @@ where /// [`Dropdown`] component properties. #[derive(Props, Clone, PartialEq)] -pub struct DropdownProps { +pub struct DropdownProps { /// Theme override. pub theme: Option, /// Selectable items, like [`DropdownItem`] @@ -188,7 +182,7 @@ pub fn Dropdown(props: DropdownProps) -> Element where T: PartialEq + Clone + Display + 'static, { - let selected = use_context_provider(|| Signal::new(props.value.clone())); + let mut selected = use_context_provider(|| Signal::new(props.value.clone())); let theme = use_applied_theme!(&props.theme, dropdown); let mut focus = use_focus(); let mut status = use_signal(DropdownStatus::default); @@ -204,12 +198,9 @@ where *selected.write() = value; }); - use_drop({ - to_owned![status, platform]; - move || { - if *status.peek() == DropdownStatus::Hovering { - platform.set_cursor(CursorIcon::default()); - } + use_drop(move || { + if *status.peek() == DropdownStatus::Hovering { + platform.set_cursor(CursorIcon::default()); } }); @@ -237,12 +228,9 @@ where } }; - let onmouseenter = { - to_owned![status, platform]; - move |_| { - platform.set_cursor(CursorIcon::Pointer); - status.set(DropdownStatus::Hovering); - } + let onmouseenter = move |_| { + platform.set_cursor(CursorIcon::Pointer); + status.set(DropdownStatus::Hovering); }; let onmouseleave = move |_| { @@ -315,3 +303,96 @@ where } ) } + +#[cfg(test)] +mod test { + use freya::prelude::*; + use freya_testing::*; + use winit::event::MouseButton; + + #[tokio::test] + pub async fn dropdown() { + fn dropdown_app() -> Element { + let values = use_hook(|| { + vec![ + "Value A".to_string(), + "Value B".to_string(), + "Value C".to_string(), + ] + }); + let mut selected_dropdown = use_signal(|| "Value A".to_string()); + + rsx!( + Dropdown { + value: selected_dropdown.read().clone(), + for ch in values { + DropdownItem { + value: ch.clone(), + onclick: { + to_owned![ch]; + move |_| selected_dropdown.set(ch.clone()) + }, + label { "{ch}" } + } + } + } + ) + } + + let mut utils = launch_test(dropdown_app); + let root = utils.root(); + let label = root.get(0).get(0); + utils.wait_for_update().await; + + // Currently closed + let start_size = utils.sdom().get().layout().size(); + + // Default value + assert_eq!(label.get(0).text(), Some("Value A")); + + // Open the dropdown + utils.push_event(PlatformEvent::Mouse { + name: EventName::Click, + cursor: (5.0, 5.0).into(), + button: Some(MouseButton::Left), + }); + utils.wait_for_update().await; + + // Now that the dropwdown is opened, there are more nodes in the layout + assert!(utils.sdom().get().layout().size() > start_size); + + // Close the dropdown by clicking outside of it + utils.push_event(PlatformEvent::Mouse { + name: EventName::Click, + cursor: (200.0, 200.0).into(), + button: Some(MouseButton::Left), + }); + utils.wait_for_update().await; + + // Now the layout size is like in the begining + assert_eq!(utils.sdom().get().layout().size(), start_size); + + // Open the dropdown again + utils.push_event(PlatformEvent::Mouse { + name: EventName::Click, + cursor: (5.0, 5.0).into(), + button: Some(MouseButton::Left), + }); + utils.wait_for_update().await; + + // Click on the second option + utils.push_event(PlatformEvent::Mouse { + name: EventName::Click, + cursor: (45.0, 100.0).into(), + button: Some(MouseButton::Left), + }); + utils.wait_for_update().await; + utils.wait_for_update().await; + + // Now the layout size is like in the begining, again + assert_eq!(utils.sdom().get().layout().size(), start_size); + + // The second optio was selected + assert_eq!(label.get(0).text(), Some("Value B")); + } +} diff --git a/crates/components/src/external_link.rs b/crates/components/src/external_link.rs deleted file mode 100644 index 408fa6f28..000000000 --- a/crates/components/src/external_link.rs +++ /dev/null @@ -1,102 +0,0 @@ -use dioxus::prelude::*; -use freya_elements::elements as dioxus_elements; -use freya_elements::events::MouseEvent; - -use freya_hooks::{use_applied_theme, ExternalLinkThemeWith}; - -use crate::Tooltip; - -/// [`ExternalLink`] component properties. -#[derive(Props, Clone, PartialEq)] -pub struct ExternalLinkProps { - /// Theme override. - pub theme: Option, - /// Inner children for the ExternalLink. - pub children: Element, - /// Handler for the `onerror` event. - pub onerror: Option>, - /// Whether to show a tooltip with the URL or not. - #[props(optional, default = true)] - pub show_tooltip: bool, - /// The ExternalLink destination URL. - #[props(into)] - pub url: String, -} - -/// `Link` for external locations, e.g websites. -/// -/// # Props -/// See [`ExternalLinkProps`]. -/// -/// # Styling -/// Inherits the [`ExternalLinkTheme`](freya_hooks::ExternalLinkTheme) theme. -/// -/// # Example -/// -/// ```no_run -/// # use freya::prelude::*; -/// fn app() -> Element { -/// rsx!( -/// ExternalLink { -/// url: "https://github.com", -/// label { -/// "GitHub" -/// } -/// } -/// ) -/// } -/// ``` -/// -#[allow(non_snake_case)] -pub fn ExternalLink(props: ExternalLinkProps) -> Element { - let theme = use_applied_theme!(&props.theme, external_link); - let is_hovering = use_signal(|| false); - - let onmouseover = move |_: MouseEvent| { - *is_hovering.write() = true; - }; - - let onmouseleave = move |_: MouseEvent| { - *is_hovering.write() = false; - }; - - let onclick = { - let url = props.url.clone(); - move |_: MouseEvent| { - let res = open::that(url.clone()); - if let (Err(_), Some(onerror)) = (res, props.onerror.as_ref()) { - onerror.call(()); - } - // TODO(marc2332): Log unhandled errors - } - }; - - let color = if *is_hovering.read() { - theme.highlight_color.as_ref() - } else { - "inherit" - }; - - rsx!( - rect { - onmouseover, - onmouseleave, - onclick, - color: "{color}", - {props.children} - } - rect { - height: "0", - width: "0", - layer: "-999", - rect { - width: "100v", - if *is_hovering.read() && props.show_tooltip { - Tooltip { - url: props.url.clone() - } - } - } - } - ) -} diff --git a/crates/components/src/gesture_area.rs b/crates/components/src/gesture_area.rs index 6fba5379b..5a5adbe3e 100644 --- a/crates/components/src/gesture_area.rs +++ b/crates/components/src/gesture_area.rs @@ -4,7 +4,7 @@ use std::time::Instant; use dioxus::prelude::*; use freya_elements::elements as dioxus_elements; use freya_elements::events::{touch::TouchPhase, TouchEvent}; -use futures::StreamExt; +use futures_util::StreamExt; /// Distance between the first tap and the second tap in `DoubleTap` gesture. const DOUBLE_TAP_DISTANCE: f64 = 100.0; @@ -173,10 +173,8 @@ pub fn GestureArea(props: GestureAreaProps) -> Element { mod test { use std::time::Duration; - use dioxus::signals::use_signal; use freya::prelude::*; - use freya_elements::events::touch::TouchPhase; - use freya_testing::{launch_test, FreyaEvent}; + use freya_testing::{events::touch::TouchPhase, launch_test, EventName, PlatformEvent}; use tokio::time::sleep; use crate::gesture_area::DOUBLE_TAP_MIN; @@ -198,8 +196,13 @@ mod test { rsx!( GestureArea { ongesture, - "{value}" + rect { + width: "100%", + height: "100%", + + } } + "{value}" ) } @@ -208,18 +211,18 @@ mod test { // Initial state utils.wait_for_update().await; - assert_eq!(utils.root().get(0).get(0).text(), Some("EMPTY")); + assert_eq!(utils.root().get(1).text(), Some("EMPTY")); - utils.push_event(FreyaEvent::Touch { - name: "touchstart".to_string(), + utils.push_event(PlatformEvent::Touch { + name: EventName::TouchStart, location: (1.0, 1.0).into(), phase: TouchPhase::Started, finger_id: 0, force: None, }); - utils.push_event(FreyaEvent::Touch { - name: "touchend".to_string(), + utils.push_event(PlatformEvent::Touch { + name: EventName::TouchEnd, location: (1.0, 1.0).into(), phase: TouchPhase::Ended, finger_id: 0, @@ -231,8 +234,8 @@ mod test { sleep(Duration::from_millis(DOUBLE_TAP_MIN as u64)).await; - utils.push_event(FreyaEvent::Touch { - name: "touchstart".to_string(), + utils.push_event(PlatformEvent::Touch { + name: EventName::TouchStart, location: (1.0, 1.0).into(), phase: TouchPhase::Started, finger_id: 0, @@ -242,7 +245,7 @@ mod test { utils.wait_for_update().await; utils.wait_for_update().await; - assert_eq!(utils.root().get(0).get(0).text(), Some("DoubleTap")); + assert_eq!(utils.root().get(1).text(), Some("DoubleTap")); } /// Simulates `TapUp` and `TapDown` gestures. @@ -258,8 +261,13 @@ mod test { rsx!( GestureArea { ongesture, - "{value}" + rect { + width: "100%", + height: "100%", + + } } + "{value}" ) } @@ -268,10 +276,10 @@ mod test { // Initial state utils.wait_for_update().await; - assert_eq!(utils.root().get(0).get(0).text(), Some("EMPTY")); + assert_eq!(utils.root().get(1).text(), Some("EMPTY")); - utils.push_event(FreyaEvent::Touch { - name: "touchstart".to_string(), + utils.push_event(PlatformEvent::Touch { + name: EventName::TouchStart, location: (1.0, 1.0).into(), phase: TouchPhase::Started, finger_id: 0, @@ -281,10 +289,10 @@ mod test { utils.wait_for_update().await; utils.wait_for_update().await; - assert_eq!(utils.root().get(0).get(0).text(), Some("TapDown")); + assert_eq!(utils.root().get(1).text(), Some("TapDown")); - utils.push_event(FreyaEvent::Touch { - name: "touchend".to_string(), + utils.push_event(PlatformEvent::Touch { + name: EventName::TouchEnd, location: (1.0, 1.0).into(), phase: TouchPhase::Ended, finger_id: 0, @@ -294,6 +302,6 @@ mod test { utils.wait_for_update().await; utils.wait_for_update().await; - assert_eq!(utils.root().get(0).get(0).text(), Some("TapUp")); + assert_eq!(utils.root().get(1).text(), Some("TapUp")); } } diff --git a/crates/components/src/graph.rs b/crates/components/src/graph.rs index e5e730970..5796ef8c8 100644 --- a/crates/components/src/graph.rs +++ b/crates/components/src/graph.rs @@ -140,7 +140,8 @@ pub fn Graph(props: GraphProps) -> Element { for (i, point) in x_labels.iter().enumerate() { let x = (space_x * i as f32) + start_x; - let mut paragrap_builder = ParagraphBuilder::new(¶graph_style, font_collection); + let mut paragrap_builder = + ParagraphBuilder::new(¶graph_style, font_collection.clone()); paragrap_builder.add_text(point); let mut text = paragrap_builder.build(); diff --git a/crates/components/src/input.rs b/crates/components/src/input.rs index e5862531f..7ab8d5604 100644 --- a/crates/components/src/input.rs +++ b/crates/components/src/input.rs @@ -88,13 +88,13 @@ pub fn Input( }: InputProps, ) -> Element { let platform = use_platform(); - let status = use_signal(InputStatus::default); + let mut status = use_signal(InputStatus::default); let mut editable = use_editable( || EditableConfig::new(value.to_string()), EditableMode::MultipleLinesSingleEditor, ); let theme = use_applied_theme!(&theme, input); - let focus = use_focus(); + let mut focus = use_focus(); if &value != editable.editor().read().rope() { editable.editor_mut().write().set(&value); @@ -105,67 +105,46 @@ pub fn Input( InputMode::Shown => value.clone(), }; - use_drop({ - to_owned![status, platform]; - move || { - if *status.peek() == InputStatus::Hovering { - platform.set_cursor(CursorIcon::default()); - } + use_drop(move || { + if *status.peek() == InputStatus::Hovering { + platform.set_cursor(CursorIcon::default()); } }); - let onkeydown = { - to_owned![editable, focus]; - move |e: Event| { - if focus.is_focused() && e.data.key != Key::Enter { - editable.process_event(&EditableEvent::KeyDown(e.data)); - onchange.call(editable.editor().peek().to_string()); - } + let onkeydown = move |e: Event| { + if focus.is_focused() && e.data.key != Key::Enter { + editable.process_event(&EditableEvent::KeyDown(e.data)); + onchange.call(editable.editor().peek().to_string()); } }; - let onmousedown = { - to_owned![editable, focus]; - move |e: MouseEvent| { - editable.process_event(&EditableEvent::MouseDown(e.data, 0)); - focus.focus(); - } + let onmousedown = move |e: MouseEvent| { + editable.process_event(&EditableEvent::MouseDown(e.data, 0)); + focus.focus(); }; - let onmouseover = { - to_owned![editable]; - move |e: MouseEvent| { - editable.process_event(&EditableEvent::MouseOver(e.data, 0)); - } + let onmouseover = move |e: MouseEvent| { + editable.process_event(&EditableEvent::MouseOver(e.data, 0)); }; - let onmouseenter = { - to_owned![platform, status]; - move |_| { - platform.set_cursor(CursorIcon::Text); - *status.write() = InputStatus::Hovering; - } + let onmouseenter = move |_| { + platform.set_cursor(CursorIcon::Text); + *status.write() = InputStatus::Hovering; }; - let onmouseleave = { - to_owned![platform, status]; - move |_| { - platform.set_cursor(CursorIcon::default()); - *status.write() = InputStatus::default(); - } + let onmouseleave = move |_| { + platform.set_cursor(CursorIcon::default()); + *status.write() = InputStatus::default(); }; - let onglobalclick = { - to_owned![editable, focus]; - move |_| match *status.read() { - InputStatus::Idle if focus.is_focused() => { - focus.unfocus(); - } - InputStatus::Hovering => { - editable.process_event(&EditableEvent::Click); - } - _ => {} + let onglobalclick = move |_| match *status.read() { + InputStatus::Idle if focus.is_focused() => { + focus.unfocus(); } + InputStatus::Hovering => { + editable.process_event(&EditableEvent::Click); + } + _ => {} }; let focus_id = focus.attribute(); @@ -184,6 +163,7 @@ pub fn Input( border_fill, width, margin, + corner_radius, font_theme: FontTheme { color }, .. } = theme; @@ -195,11 +175,12 @@ pub fn Input( color: "{color}", background: "{background}", border: "1 solid {border_fill}", - shadow: "0 3 15 0 rgb(0, 0, 0, 0.3)", - corner_radius: "10", + shadow: "0 4 5 0 rgb(0, 0, 0, 0.1)", + corner_radius: "{corner_radius}", margin: "{margin}", cursor_reference, focus_id, + focusable: "true", role: "textInput", main_align: "center", paragraph { @@ -224,3 +205,56 @@ pub fn Input( } ) } + +#[cfg(test)] +mod test { + use freya::prelude::*; + use freya_testing::*; + + #[tokio::test] + pub async fn input() { + fn input_app() -> Element { + let mut value = use_signal(|| "Hello, Worl".to_string()); + + rsx!(Input { + value: value.read().clone(), + onchange: move |new_value| { + value.set(new_value); + } + },) + } + + let mut utils = launch_test(input_app); + let root = utils.root(); + let text = root.get(0).get(0).get(0); + utils.wait_for_update().await; + + // Default value + assert_eq!(text.get(0).text(), Some("Hello, Worl")); + + assert_eq!(utils.focus_id(), ACCESSIBILITY_ROOT_ID); + + // Focus the input in the end of the text + utils.push_event(PlatformEvent::Mouse { + name: EventName::MouseDown, + cursor: (115., 10.).into(), + button: Some(MouseButton::Left), + }); + utils.wait_for_update().await; + utils.wait_for_update().await; + + assert_ne!(utils.focus_id(), ACCESSIBILITY_ROOT_ID); + + // Write "d" + utils.push_event(PlatformEvent::Keyboard { + name: EventName::KeyDown, + key: Key::Character("d".to_string()), + code: Code::KeyD, + modifiers: Modifiers::default(), + }); + utils.wait_for_update().await; + + // Check that "d" has been written into the input. + assert_eq!(text.get(0).text(), Some("Hello, World")); + } +} diff --git a/crates/components/src/lib.rs b/crates/components/src/lib.rs index e080e1ed5..7121cb937 100644 --- a/crates/components/src/lib.rs +++ b/crates/components/src/lib.rs @@ -8,15 +8,16 @@ mod canvas; mod cursor_area; mod drag_drop; mod dropdown; -mod external_link; mod gesture_area; mod graph; mod icons; mod input; +mod link; mod loader; mod network_image; mod progress_bar; mod scroll_views; +mod sidebar; mod slider; mod switch; mod table; @@ -31,15 +32,16 @@ pub use canvas::*; pub use cursor_area::*; pub use drag_drop::*; pub use dropdown::*; -pub use external_link::*; pub use gesture_area::*; pub use graph::*; pub use icons::*; pub use input::*; +pub use link::*; pub use loader::*; pub use network_image::*; pub use progress_bar::*; pub use scroll_views::*; +pub use sidebar::*; pub use slider::*; pub use switch::*; pub use table::*; diff --git a/crates/components/src/link.rs b/crates/components/src/link.rs new file mode 100644 index 000000000..7cf69ebdf --- /dev/null +++ b/crates/components/src/link.rs @@ -0,0 +1,286 @@ +use crate::Tooltip; +use dioxus::prelude::*; +use dioxus_router::prelude::{navigator, IntoRoutable}; +use freya_elements::elements as dioxus_elements; +use freya_elements::events::MouseEvent; +use freya_hooks::{use_applied_theme, LinkThemeWith}; +use std::borrow::Cow; +use winit::event::MouseButton; + +#[derive(Clone, PartialEq)] +pub enum LinkTooltip { + /// No tooltip at all. + None, + /// Default tooltip. + /// + /// - For a route, this is the same as [`None`](AnchorTooltip::None). + /// - For a URL, this is the value of that URL. + Default, + /// Custom tooltip to always show. + Custom(String), +} + +/// Similar to [`Link`](dioxus_router::components::Link), but you can use it in Freya. +/// Both internal routes (dioxus-router) and external links are supported. When using internal routes +/// make sure the Link is descendant of a [`Router`](dioxus_router::components::Router) component. +/// +/// # Styling +/// +/// Inherits the [`LinkTheme`](freya_hooks::LinkTheme) theme. +/// +/// # Example +/// +/// With Dioxus Router: +/// +/// ```rust +/// # use dioxus::prelude::*; +/// # use dioxus_router::prelude::*; +/// # use freya_elements::elements as dioxus_elements; +/// # use freya_components::Link; +/// # #[derive(Routable, Clone)] +/// # #[rustfmt::skip] +/// # enum AppRouter { +/// # #[route("/")] +/// # Settings, +/// # #[route("/..routes")] +/// # NotFound +/// # } +/// # #[component] +/// # fn Settings() -> Element { rsx!(rect { })} +/// # #[component] +/// # fn NotFound() -> Element { rsx!(rect { })} +/// # fn link_example_good() -> Element { +/// rsx! { +/// Link { +/// to: AppRouter::Settings, +/// label { "App Settings" } +/// } +/// } +/// # } +/// ``` +/// +/// With external routes: +/// +/// ```rust +/// # use dioxus::prelude::*; +/// # use freya_elements::elements as dioxus_elements; +/// # use freya_components::Link; +/// # fn link_example_good() -> Element { +/// rsx! { +/// Link { +/// to: "https://crates.io/crates/freya", +/// label { "Freya crates.io" } +/// } +/// } +/// # } +/// ``` +#[allow(non_snake_case)] +#[component] +pub fn Link( + /// Theme override. + #[props(optional)] + theme: Option, + /// The route or external URL string to navigate to. + #[props(into)] + to: IntoRoutable, + /// Inner children for the Link. + children: Element, + /// This event will be fired if opening an external link fails. + #[props(optional)] + onerror: Option>, + /// A little text hint to show when hovering over the anchor. + /// + /// Setting this to [`None`] is the same as [`LinkTooltip::Default`]. + /// To remove the tooltip, set this to [`LinkTooltip::None`]. + #[props(optional)] + tooltip: Option, +) -> Element { + let theme = use_applied_theme!(&theme, link); + let mut is_hovering = use_signal(|| false); + + let url = if let IntoRoutable::FromStr(ref url) = to { + Some(url.clone()) + } else { + None + }; + + let onmouseenter = move |_: MouseEvent| { + is_hovering.set(true); + }; + + let onmouseleave = move |_: MouseEvent| { + is_hovering.set(false); + }; + + let onclick = { + to_owned![url, to]; + move |event: MouseEvent| { + if !matches!(event.trigger_button, Some(MouseButton::Left)) { + return; + } + + // Open the url if there is any + // otherwise change the dioxus router route + if let Some(url) = &url { + let res = open::that(url); + + if let (Err(_), Some(onerror)) = (res, onerror.as_ref()) { + onerror.call(()); + } + + // TODO(marc2332): Log unhandled errors + } else { + let router = navigator(); + router.push(to.clone()); + } + } + }; + + let color = if *is_hovering.read() { + theme.highlight_color + } else { + Cow::Borrowed("inherit") + }; + + let tooltip = match tooltip { + None | Some(LinkTooltip::Default) => url.clone(), + Some(LinkTooltip::None) => None, + Some(LinkTooltip::Custom(str)) => Some(str), + }; + + let main_rect = rsx! { + rect { + onmouseenter, + onmouseleave, + onclick, + color: "{color}", + {children} + } + }; + + let Some(tooltip) = tooltip else { + return rsx!({ main_rect }); + }; + + rsx! { + {main_rect} + rect { + height: "0", + layer: "-999", + rect { + width: "100v", + if *is_hovering.read() { + Tooltip { + url: tooltip + } + } + } + } + } +} + +#[cfg(test)] +mod test { + use dioxus_router::prelude::{Outlet, Routable, Router}; + use freya::prelude::*; + use freya_testing::*; + + #[tokio::test] + pub async fn link() { + #[derive(Routable, Clone)] + #[rustfmt::skip] + enum Route { + #[layout(Layout)] + #[route("/")] + Home, + #[route("/somewhere")] + Somewhere, + #[route("/..routes")] + NotFound + } + + #[allow(non_snake_case)] + #[component] + fn NotFound() -> Element { + rsx! { + label { + "Not found" + } + } + } + + #[allow(non_snake_case)] + #[component] + fn Home() -> Element { + rsx! { + label { + "Home" + } + } + } + + #[allow(non_snake_case)] + #[component] + fn Somewhere() -> Element { + rsx! { + label { + "Somewhere" + } + } + } + + #[allow(non_snake_case)] + #[component] + fn Layout() -> Element { + rsx!( + Link { + to: Route::Home, + Button { + label { "Home" } + } + } + Link { + to: Route::Somewhere, + Button { + label { "Somewhere" } + } + } + Outlet:: {} + ) + } + + fn link_app() -> Element { + rsx!(Router:: {}) + } + + let mut utils = launch_test(link_app); + + utils.wait_for_update().await; + utils.wait_for_update().await; + + // Check route is Home + assert_eq!(utils.root().get(2).get(0).text(), Some("Home")); + + // Go to the "Somewhere" route + utils.push_event(PlatformEvent::Mouse { + name: EventName::Click, + cursor: (5., 70.).into(), + button: Some(MouseButton::Left), + }); + + utils.wait_for_update().await; + utils.wait_for_update().await; + + // Check route is Somewhere + assert_eq!(utils.root().get(2).get(0).text(), Some("Somewhere")); + + // Go to the "Home" route again + utils.push_event(PlatformEvent::Mouse { + name: EventName::Click, + cursor: (5., 5.).into(), + button: Some(MouseButton::Left), + }); + + utils.wait_for_update().await; + } +} diff --git a/crates/components/src/network_image.rs b/crates/components/src/network_image.rs index b9b0962ab..9189c0f7c 100644 --- a/crates/components/src/network_image.rs +++ b/crates/components/src/network_image.rs @@ -58,8 +58,8 @@ pub enum ImageStatus { #[allow(non_snake_case)] pub fn NetworkImage(props: NetworkImageProps) -> Element { let focus = use_focus(); - let status = use_signal(|| ImageStatus::Loading); - let image_bytes = use_signal::>>(|| None); + let mut status = use_signal(|| ImageStatus::Loading); + let mut image_bytes = use_signal::>>(|| None); let focus_id = focus.attribute(); let NetworkImageTheme { width, height } = use_applied_theme!(&props.theme, network_image); @@ -67,7 +67,6 @@ pub fn NetworkImage(props: NetworkImageProps) -> Element { // TODO: Waiting for a dependency-based use_effect let _ = use_memo_with_dependencies(&props.url, move |url| { - to_owned![image_bytes, status]; spawn(async move { // Loading image status.set(ImageStatus::Loading); diff --git a/crates/components/src/scroll_views/scroll_view.rs b/crates/components/src/scroll_views/scroll_view.rs index 5b92d599e..d0d8d252e 100644 --- a/crates/components/src/scroll_views/scroll_view.rs +++ b/crates/components/src/scroll_views/scroll_view.rs @@ -56,13 +56,13 @@ pub struct ScrollViewProps { /// #[allow(non_snake_case)] pub fn ScrollView(props: ScrollViewProps) -> Element { - let clicking_scrollbar = use_signal::>(|| None); + let mut clicking_scrollbar = use_signal::>(|| None); let mut clicking_shift = use_signal(|| false); let mut clicking_alt = use_signal(|| false); let mut scrolled_y = use_signal(|| 0); let mut scrolled_x = use_signal(|| 0); let (node_ref, size) = use_node(); - let focus = use_focus(); + let mut focus = use_focus(); let theme = use_applied_theme!(&props.theme, scroll_view); let padding = &theme.padding; @@ -97,92 +97,86 @@ pub fn ScrollView(props: ScrollViewProps) -> Element { get_scrollbar_pos_and_size(size.inner.width, size.area.width(), corrected_scrolled_x); // Moves the Y axis when the user scrolls in the container - let onwheel = { - to_owned![focus]; - move |e: WheelEvent| { - let speed_multiplier = if *clicking_alt.read() { - SCROLL_SPEED_MULTIPLIER - } else { - 1.0 - }; - - if !*clicking_shift.read() { - let wheel_y = e.get_delta_y() as f32 * speed_multiplier; - - let scroll_position_y = get_scroll_position_from_wheel( - wheel_y, - size.inner.height, - size.area.height(), - corrected_scrolled_y, - ); - - // Only scroll when there is still area to scroll - if *scrolled_y.peek() != scroll_position_y { - e.stop_propagation(); - *scrolled_y.write() = scroll_position_y; - } else { - return; - } - } + let onwheel = move |e: WheelEvent| { + let speed_multiplier = if *clicking_alt.peek() { + SCROLL_SPEED_MULTIPLIER + } else { + 1.0 + }; - let wheel_x = if *clicking_shift.read() { - e.get_delta_y() as f32 - } else { - e.get_delta_x() as f32 - } * speed_multiplier; + if !*clicking_shift.peek() { + let wheel_y = e.get_delta_y() as f32 * speed_multiplier; - let scroll_position_x = get_scroll_position_from_wheel( - wheel_x, - size.inner.width, - size.area.width(), - corrected_scrolled_x, + let scroll_position_y = get_scroll_position_from_wheel( + wheel_y, + size.inner.height, + size.area.height(), + corrected_scrolled_y, ); // Only scroll when there is still area to scroll - if *scrolled_x.peek() != scroll_position_x { + if *scrolled_y.peek() != scroll_position_y { e.stop_propagation(); - *scrolled_x.write() = scroll_position_x; + *scrolled_y.write() = scroll_position_y; } else { return; } + } - focus.focus(); + let wheel_x = if *clicking_shift.peek() { + e.get_delta_y() as f32 + } else { + e.get_delta_x() as f32 + } * speed_multiplier; + + let scroll_position_x = get_scroll_position_from_wheel( + wheel_x, + size.inner.width, + size.area.width(), + corrected_scrolled_x, + ); + + // Only scroll when there is still area to scroll + if *scrolled_x.peek() != scroll_position_x { + e.stop_propagation(); + *scrolled_x.write() = scroll_position_x; + } else { + return; } + + focus.focus(); }; // Drag the scrollbars - let onmouseover = { - to_owned![focus]; - move |e: MouseEvent| { - let clicking_scrollbar = clicking_scrollbar.read(); - - if let Some((Axis::Y, y)) = *clicking_scrollbar { - let coordinates = e.get_element_coordinates(); - let cursor_y = coordinates.y - y - size.area.min_y() as f64; - - let scroll_position = get_scroll_position_from_cursor( - cursor_y as f32, - size.inner.height, - size.area.height(), - ); + let onmouseover = move |e: MouseEvent| { + let clicking_scrollbar = clicking_scrollbar.peek(); - *scrolled_y.write() = scroll_position; - } else if let Some((Axis::X, x)) = *clicking_scrollbar { - let coordinates = e.get_element_coordinates(); - let cursor_x = coordinates.x - x - size.area.min_x() as f64; + if let Some((Axis::Y, y)) = *clicking_scrollbar { + let coordinates = e.get_element_coordinates(); + let cursor_y = coordinates.y - y - size.area.min_y() as f64; - let scroll_position = get_scroll_position_from_cursor( - cursor_x as f32, - size.inner.width, - size.area.width(), - ); + let scroll_position = get_scroll_position_from_cursor( + cursor_y as f32, + size.inner.height, + size.area.height(), + ); - *scrolled_x.write() = scroll_position; - } + *scrolled_y.write() = scroll_position; + } else if let Some((Axis::X, x)) = *clicking_scrollbar { + let coordinates = e.get_element_coordinates(); + let cursor_x = coordinates.x - x - size.area.min_x() as f64; - if clicking_scrollbar.is_some() { - focus.focus(); - } + let scroll_position = get_scroll_position_from_cursor( + cursor_x as f32, + size.inner.width, + size.area.width(), + ); + + *scrolled_x.write() = scroll_position; + } + + if clicking_scrollbar.is_some() { + focus.focus(); } }; @@ -252,7 +246,7 @@ pub fn ScrollView(props: ScrollViewProps) -> Element { // Unmark any scrollbar let onclick = move |_: MouseEvent| { - if clicking_scrollbar.read().is_some() { + if clicking_scrollbar.peek().is_some() { *clicking_scrollbar.write() = None; } }; @@ -334,3 +328,161 @@ pub fn ScrollView(props: ScrollViewProps) -> Element { } ) } + +#[cfg(test)] +mod test { + use freya::prelude::*; + use freya_testing::*; + + #[tokio::test] + pub async fn scroll_view_wheel() { + fn scroll_view_wheel_app() -> Element { + rsx!( + ScrollView { + rect { + height: "200", + width: "200", + }, + rect { + height: "200", + width: "200", + }, + rect { + height: "200", + width: "200", + } + rect { + height: "200", + width: "200", + } + } + ) + } + + let mut utils = launch_test(scroll_view_wheel_app); + let root = utils.root(); + let content = root.get(0).get(0).get(0); + utils.wait_for_update().await; + + // Only the first three items are visible + // Scrollview height is 500 and the user hasn't scrolled yet + assert!(content.get(0).is_visible()); // 1. 0 -> 200, 200 < 500 + assert!(content.get(1).is_visible()); // 2. 200 -> 400, 200 < 500 + assert!(content.get(2).is_visible()); // 3. 400 -> 600, 400 < 500 + assert!(!content.get(3).is_visible()); // 4. 600 -> 800, 600 is NOT < 500, which means it is not visible. + + utils.push_event(PlatformEvent::Wheel { + name: EventName::Wheel, + scroll: (0., -300.).into(), + cursor: (5., 5.).into(), + }); + + utils.wait_for_update().await; + + // Only the last three items are visible + // Scrollview height is 500 but the user has scrolled 300 pixels + assert!(!content.get(0).is_visible()); // 1. 0 -> 200, 200 is NOT > 300, which means it is not visible. + assert!(content.get(1).is_visible()); // 2. 200 -> 400, 400 > 300 + assert!(content.get(2).is_visible()); // 3. 400 -> 600, 600 > 300 + assert!(content.get(3).is_visible()); // 4. 600 -> 800, 800 > 300 + } + + #[tokio::test] + pub async fn scroll_view_scrollbar() { + fn scroll_view_scrollbar_app() -> Element { + rsx!( + ScrollView { + rect { + height: "200", + width: "200", + }, + rect { + height: "200", + width: "200", + }, + rect { + height: "200", + width: "200", + } + rect { + height: "200", + width: "200", + } + } + ) + } + + let mut utils = launch_test(scroll_view_scrollbar_app); + let root = utils.root(); + let content = root.get(0).get(0).get(0); + utils.wait_for_update().await; + + // Only the first three items are visible + // Scrollview height is 500 and the user hasn't scrolled yet + assert!(content.get(0).is_visible()); // 1. 0 -> 200, 200 < 500 + assert!(content.get(1).is_visible()); // 2. 200 -> 400, 200 < 500 + assert!(content.get(2).is_visible()); // 3. 400 -> 600, 400 < 500 + assert!(!content.get(3).is_visible()); // 4. 600 -> 800, 600 is NOT < 500, which means it is not visible. + + // Simulate the user dragging the scrollbar + utils.push_event(PlatformEvent::Mouse { + name: EventName::MouseOver, + cursor: (490., 20.).into(), + button: Some(MouseButton::Left), + }); + utils.push_event(PlatformEvent::Mouse { + name: EventName::MouseDown, + cursor: (490., 20.).into(), + button: Some(MouseButton::Left), + }); + utils.push_event(PlatformEvent::Mouse { + name: EventName::MouseOver, + cursor: (490., 320.).into(), + button: Some(MouseButton::Left), + }); + utils.push_event(PlatformEvent::Mouse { + name: EventName::Click, + cursor: (490., 320.).into(), + button: Some(MouseButton::Left), + }); + + utils.wait_for_update().await; + + // Only the last three items are visible + // Scrollview height is 500 but the user has dragged the scrollbar 300 pixels + assert!(!content.get(0).is_visible()); // 1. 0 -> 200, 200 is NOT > 300, which means it is not visible. + assert!(content.get(1).is_visible()); // 2. 200 -> 400, 400 > 300 + assert!(content.get(2).is_visible()); // 3. 400 -> 600, 600 > 300 + assert!(content.get(3).is_visible()); // 4. 600 -> 800, 800 > 300 + + // Scroll up with arrows + for _ in 0..5 { + utils.push_event(PlatformEvent::Keyboard { + name: EventName::KeyDown, + key: Key::ArrowUp, + code: Code::ArrowUp, + modifiers: Modifiers::default(), + }); + utils.wait_for_update().await; + } + + assert!(content.get(0).is_visible()); + assert!(content.get(1).is_visible()); + assert!(content.get(2).is_visible()); + assert!(!content.get(3).is_visible()); + + // Scroll to the bottom with arrows + utils.push_event(PlatformEvent::Keyboard { + name: EventName::KeyDown, + key: Key::End, + code: Code::End, + modifiers: Modifiers::default(), + }); + utils.wait_for_update().await; + + assert!(!content.get(0).is_visible()); + assert!(content.get(1).is_visible()); + assert!(content.get(2).is_visible()); + assert!(content.get(3).is_visible()); + } +} diff --git a/crates/components/src/scroll_views/virtual_scroll_view.rs b/crates/components/src/scroll_views/virtual_scroll_view.rs index 45940be5b..3de6a4ba8 100644 --- a/crates/components/src/scroll_views/virtual_scroll_view.rs +++ b/crates/components/src/scroll_views/virtual_scroll_view.rs @@ -5,7 +5,6 @@ use freya_elements::elements as dioxus_elements; use freya_elements::events::{keyboard::Key, KeyboardEvent, MouseEvent, WheelEvent}; use freya_hooks::{use_applied_theme, use_focus, use_node, ScrollViewThemeWith}; use std::ops::Range; -use std::rc::Rc; use crate::{ get_container_size, get_corrected_scroll_position, get_scroll_position_from_cursor, @@ -13,11 +12,12 @@ use crate::{ manage_key_event, Axis, ScrollBar, ScrollThumb, SCROLLBAR_SIZE, SCROLL_SPEED_MULTIPLIER, }; -type BuilderFunction = dyn Fn((usize, usize, &Option)) -> Element; - /// [`VirtualScrollView`] component properties. #[derive(Props, Clone)] -pub struct VirtualScrollViewProps { +pub struct VirtualScrollViewProps< + Builder: 'static + Clone + Fn(usize, &Option) -> Element, + BuilderArgs: Clone + 'static + PartialEq = (), +> { /// Theme override. pub theme: Option, /// Quantity of items in the VirtualScrollView. @@ -25,9 +25,10 @@ pub struct VirtualScrollViewProps { /// Size of the items, height for vertical direction and width for horizontal. pub item_size: f32, /// The item builder function. - pub builder: Rc>, - /// Custom values to pass to the builder function. - pub builder_values: Option, + pub builder: Builder, + /// The values for the item builder function. + #[props(into)] + pub builder_args: Option, /// Direction of the VirtualScrollView, `vertical` or `horizontal`. #[props(default = "vertical".to_string(), into)] pub direction: String, @@ -39,6 +40,22 @@ pub struct VirtualScrollViewProps { pub scroll_with_arrows: bool, } +impl< + BuilderArgs: Clone + PartialEq, + Builder: Clone + Fn(usize, &Option) -> Element, + > PartialEq for VirtualScrollViewProps +{ + fn eq(&self, other: &Self) -> bool { + self.theme == other.theme + && self.length == other.length + && self.item_size == other.item_size + && self.direction == other.direction + && self.show_scrollbar == other.show_scrollbar + && self.scroll_with_arrows == other.scroll_with_arrows + && self.builder_args == other.builder_args + } +} + fn get_render_range( viewport_size: f32, scroll_position: f32, @@ -74,30 +91,34 @@ fn get_render_range( /// show_scrollbar: true, /// length: 5, /// item_size: 80.0, -/// builder_values: (), /// direction: "vertical", -/// builder: Rc::new(move |(k, i, _)| { +/// builder: move |i, _other_args: &Option<()>| { /// rsx! { /// label { -/// key: "{k}", +/// key: "{i}", /// height: "80", /// "Number {i}" /// } /// } -/// }) +/// } /// } /// ) /// } /// ``` #[allow(non_snake_case)] -pub fn VirtualScrollView(props: VirtualScrollViewProps) -> Element { - let clicking_scrollbar = use_signal::>(|| None); +pub fn VirtualScrollView< + Builder: Clone + Fn(usize, &Option) -> Element, + BuilderArgs: Clone + PartialEq, +>( + props: VirtualScrollViewProps, +) -> Element { + let mut clicking_scrollbar = use_signal::>(|| None); let mut clicking_shift = use_signal(|| false); let mut clicking_alt = use_signal(|| false); let mut scrolled_y = use_signal(|| 0); let mut scrolled_x = use_signal(|| 0); let (node_ref, size) = use_node(); - let focus = use_focus(); + let mut focus = use_focus(); let theme = use_applied_theme!(&props.theme, scroll_view); let padding = &theme.padding; @@ -130,77 +151,80 @@ pub fn VirtualScrollView(props: VirtualScrollViewProps) -> Element get_scrollbar_pos_and_size(inner_size, size.area.width(), corrected_scrolled_x); // Moves the Y axis when the user scrolls in the container - let onwheel = { - to_owned![focus]; - move |e: WheelEvent| { - let speed_multiplier = if *clicking_alt.peek() { - SCROLL_SPEED_MULTIPLIER - } else { - 1.0 - }; + let onwheel = move |e: WheelEvent| { + let speed_multiplier = if *clicking_alt.peek() { + SCROLL_SPEED_MULTIPLIER + } else { + 1.0 + }; - if !*clicking_shift.peek() { - let wheel_y = e.get_delta_y() as f32 * speed_multiplier; + if !*clicking_shift.peek() { + let wheel_y = e.get_delta_y() as f32 * speed_multiplier; - let scroll_position_y = get_scroll_position_from_wheel( - wheel_y, - inner_size, - size.area.height(), - corrected_scrolled_y, - ); + let scroll_position_y = get_scroll_position_from_wheel( + wheel_y, + inner_size, + size.area.height(), + corrected_scrolled_y, + ); + // Only scroll when there is still area to scroll + if *scrolled_y.peek() != scroll_position_y { + e.stop_propagation(); *scrolled_y.write() = scroll_position_y; - } - - let wheel_x = if *clicking_shift.peek() { - e.get_delta_y() as f32 } else { - e.get_delta_x() as f32 - } * speed_multiplier; - - let scroll_position_x = get_scroll_position_from_wheel( - wheel_x, - inner_size, - size.area.width(), - corrected_scrolled_x, - ); + return; + } + } + let wheel_x = if *clicking_shift.peek() { + e.get_delta_y() as f32 + } else { + e.get_delta_x() as f32 + } * speed_multiplier; + + let scroll_position_x = get_scroll_position_from_wheel( + wheel_x, + inner_size, + size.area.width(), + corrected_scrolled_x, + ); + + // Only scroll when there is still area to scroll + if *scrolled_x.peek() != scroll_position_x { + e.stop_propagation(); *scrolled_x.write() = scroll_position_x; - - focus.focus(); + } else { + return; } + + focus.focus(); }; // Drag the scrollbars - let onmouseover = { - to_owned![focus]; - move |e: MouseEvent| { - let clicking_scrollbar = clicking_scrollbar.read(); - - if let Some((Axis::Y, y)) = *clicking_scrollbar { - let coordinates = e.get_element_coordinates(); - let cursor_y = coordinates.y - y - size.area.min_y() as f64; - - let scroll_position = get_scroll_position_from_cursor( - cursor_y as f32, - inner_size, - size.area.height(), - ); + let onmouseover = move |e: MouseEvent| { + let clicking_scrollbar = clicking_scrollbar.peek(); - *scrolled_y.write() = scroll_position; - } else if let Some((Axis::X, x)) = *clicking_scrollbar { - let coordinates = e.get_element_coordinates(); - let cursor_x = coordinates.x - x - size.area.min_x() as f64; + if let Some((Axis::Y, y)) = *clicking_scrollbar { + let coordinates = e.get_element_coordinates(); + let cursor_y = coordinates.y - y - size.area.min_y() as f64; - let scroll_position = - get_scroll_position_from_cursor(cursor_x as f32, inner_size, size.area.width()); + let scroll_position = + get_scroll_position_from_cursor(cursor_y as f32, inner_size, size.area.height()); - *scrolled_x.write() = scroll_position; - } + *scrolled_y.write() = scroll_position; + } else if let Some((Axis::X, x)) = *clicking_scrollbar { + let coordinates = e.get_element_coordinates(); + let cursor_x = coordinates.x - x - size.area.min_x() as f64; - if clicking_scrollbar.is_some() { - focus.focus(); - } + let scroll_position = + get_scroll_position_from_cursor(cursor_x as f32, inner_size, size.area.width()); + + *scrolled_x.write() = scroll_position; + } + + if clicking_scrollbar.is_some() { + focus.focus(); } }; @@ -270,7 +294,7 @@ pub fn VirtualScrollView(props: VirtualScrollViewProps) -> Element // Unmark any scrollbar let onclick = move |_: MouseEvent| { - if clicking_scrollbar.read().is_some() { + if clicking_scrollbar.peek().is_some() { *clicking_scrollbar.write() = None; } }; @@ -301,7 +325,14 @@ pub fn VirtualScrollView(props: VirtualScrollViewProps) -> Element items_length as f32, ); - let children = render_range.map(|i| (props.builder)((i + 1, i, &props.builder_values))); + let children = use_memo_with_dependencies( + (&render_range, &props.builder_args), + move |(render_range, builder_args)| { + render_range + .map(|i| (props.builder)(i, &builder_args)) + .collect::>() + }, + ); let is_scrolling_x = clicking_scrollbar .read() @@ -337,7 +368,7 @@ pub fn VirtualScrollView(props: VirtualScrollViewProps) -> Element direction: "{user_direction}", reference: node_ref, onwheel: onwheel, - {children} + {children.read().iter()} } ScrollBar { width: "100%", @@ -367,3 +398,193 @@ pub fn VirtualScrollView(props: VirtualScrollViewProps) -> Element } ) } + +#[cfg(test)] +mod test { + use freya::prelude::*; + use freya_testing::*; + + #[tokio::test] + pub async fn virtual_scroll_view_wheel() { + fn virtual_scroll_view_wheel_app() -> Element { + let values = use_signal(|| ["Hello, World!"].repeat(30)); + + rsx!(VirtualScrollView { + length: values.read().len(), + item_size: 50.0, + direction: "vertical", + builder: move |index, _: &Option<()>| { + let value = values.read()[index]; + rsx! { + label { + key: "{index}", + height: "50", + "{index} {value}" + } + } + } + }) + } + + let mut utils = launch_test(virtual_scroll_view_wheel_app); + let root = utils.root(); + + utils.wait_for_update().await; + utils.wait_for_update().await; + + let content = root.get(0).get(0).get(0); + assert_eq!(content.children_ids().len(), 10); + + // Check that visible items are from indexes 0 to 10, because 500 / 50 = 10. + for (n, i) in (0..10).enumerate() { + let child = content.get(n); + assert_eq!( + child.get(0).text(), + Some(format!("{i} Hello, World!").as_str()) + ); + } + + utils.push_event(PlatformEvent::Wheel { + name: EventName::Wheel, + scroll: (0., -300.).into(), + cursor: (5., 5.).into(), + }); + + utils.wait_for_update().await; + utils.wait_for_update().await; + + let content = root.get(0).get(0).get(0); + assert_eq!(content.children_ids().len(), 10); + + // It has scrolled 300 pixels, which equals to 6 items since because 300 / 50 = 6 + // So we must start checking from 6 to +10, 16 in this case because 6 + 10 = 16 + for (n, i) in (6..16).enumerate() { + let child = content.get(n); + assert_eq!( + child.get(0).text(), + Some(format!("{i} Hello, World!").as_str()) + ); + } + } + + #[tokio::test] + pub async fn virtual_scroll_view_scrollbar() { + fn virtual_scroll_view_scrollar_app() -> Element { + let values = use_signal(|| ["Hello, World!"].repeat(30)); + + rsx!(VirtualScrollView { + length: values.read().len(), + item_size: 50.0, + direction: "vertical", + builder: move |index, _: &Option<()>| { + let value = values.read()[index]; + rsx! { + label { + key: "{index}", + height: "50", + "{index} {value}" + } + } + } + }) + } + + let mut utils = launch_test(virtual_scroll_view_scrollar_app); + let root = utils.root(); + + utils.wait_for_update().await; + utils.wait_for_update().await; + + let content = root.get(0).get(0).get(0); + assert_eq!(content.children_ids().len(), 10); + + // Check that visible items are from indexes 0 to 10, because 500 / 50 = 10. + for (n, i) in (0..10).enumerate() { + let child = content.get(n); + assert_eq!( + child.get(0).text(), + Some(format!("{i} Hello, World!").as_str()) + ); + } + + // Simulate the user dragging the scrollbar + utils.push_event(PlatformEvent::Mouse { + name: EventName::MouseOver, + cursor: (490., 20.).into(), + button: Some(MouseButton::Left), + }); + utils.push_event(PlatformEvent::Mouse { + name: EventName::MouseDown, + cursor: (490., 20.).into(), + button: Some(MouseButton::Left), + }); + utils.push_event(PlatformEvent::Mouse { + name: EventName::MouseOver, + cursor: (490., 320.).into(), + button: Some(MouseButton::Left), + }); + utils.push_event(PlatformEvent::Mouse { + name: EventName::Click, + cursor: (490., 320.).into(), + button: Some(MouseButton::Left), + }); + + utils.wait_for_update().await; + utils.wait_for_update().await; + + let content = root.get(0).get(0).get(0); + assert_eq!(content.children_ids().len(), 10); + + // It has dragged the scrollbar 300 pixels + for (n, i) in (18..28).enumerate() { + let child = content.get(n); + assert_eq!( + child.get(0).text(), + Some(format!("{i} Hello, World!").as_str()) + ); + } + + // Scroll up with arrows + for _ in 0..10 { + utils.push_event(PlatformEvent::Keyboard { + name: EventName::KeyDown, + key: Key::ArrowUp, + code: Code::ArrowUp, + modifiers: Modifiers::default(), + }); + utils.wait_for_update().await; + } + + let content = root.get(0).get(0).get(0); + assert_eq!(content.children_ids().len(), 10); + + for (n, i) in (0..10).enumerate() { + let child = content.get(n); + assert_eq!( + child.get(0).text(), + Some(format!("{i} Hello, World!").as_str()) + ); + } + + // Scroll to the bottom with arrows + utils.push_event(PlatformEvent::Keyboard { + name: EventName::KeyDown, + key: Key::End, + code: Code::End, + modifiers: Modifiers::default(), + }); + utils.wait_for_update().await; + utils.wait_for_update().await; + + let content = root.get(0).get(0).get(0); + assert_eq!(content.children_ids().len(), 9); + + for (n, i) in (21..30).enumerate() { + let child = content.get(n); + assert_eq!( + child.get(0).text(), + Some(format!("{i} Hello, World!").as_str()) + ); + } + } +} diff --git a/crates/components/src/sidebar.rs b/crates/components/src/sidebar.rs new file mode 100644 index 000000000..3388d181e --- /dev/null +++ b/crates/components/src/sidebar.rs @@ -0,0 +1,124 @@ +#![cfg_attr( + all(not(debug_assertions), target_os = "windows"), + windows_subsystem = "windows" +)] + +use crate::{ButtonStatus, ScrollView}; +use dioxus::prelude::*; +use freya_elements::elements as dioxus_elements; +use freya_hooks::{ + theme_with, use_applied_theme, use_platform, ScrollViewThemeWith, SidebarItemTheme, + SidebarItemThemeWith, SidebarTheme, SidebarThemeWith, +}; +use winit::window::CursorIcon; + +#[allow(non_snake_case)] +#[component] +pub fn Sidebar( + /// Theme override. + theme: Option, + /// This is what is rendered next to the sidebar. + children: Element, + /// This is what is rendered in the sidebar. + sidebar: Element, +) -> Element { + let SidebarTheme { + font_theme, + background, + } = use_applied_theme!(&theme, sidebar); + + rsx!( + rect { + width: "100%", + height: "100%", + direction: "horizontal", + rect { + overflow: "clip", + width: "170", + height: "100%", + background: "{background}", + color: "{font_theme.color}", + shadow: "2 0 5 0 rgb(0, 0, 0, 30)", + ScrollView { + theme: theme_with!(ScrollViewTheme { + padding: "16".into(), + }), + {sidebar} + } + } + rect { + overflow: "clip", + width: "fill", + height: "100%", + color: "{font_theme.color}", + {children} + } + } + ) +} + +#[allow(non_snake_case)] +#[component] +pub fn SidebarItem( + /// Theme override. + theme: Option, + /// Inner content for the SidebarItem. + children: Element, + /// Optionally handle the `onclick` event in the SidebarItem. + onclick: Option>, +) -> Element { + let SidebarItemTheme { + hover_background, + background, + font_theme, + border_fill, + } = use_applied_theme!(&theme, sidebar_item); + let mut status = use_signal(ButtonStatus::default); + let platform = use_platform(); + + use_drop(move || { + if *status.read() == ButtonStatus::Hovering { + platform.set_cursor(CursorIcon::default()); + } + }); + + let onclick = move |_| { + if let Some(onclick) = &onclick { + onclick.call(()); + } + }; + + let onmouseenter = move |_| { + platform.set_cursor(CursorIcon::Pointer); + status.set(ButtonStatus::Hovering); + }; + + let onmouseleave = move |_| { + platform.set_cursor(CursorIcon::default()); + status.set(ButtonStatus::default()); + }; + + let background = match *status.read() { + ButtonStatus::Hovering => hover_background, + ButtonStatus::Idle => background, + }; + + rsx!( + rect { + overflow: "clip", + margin: "5 0", + onclick, + onmouseenter, + onmouseleave, + width: "100%", + height: "auto", + color: "{font_theme.color}", + border: "1 solid {border_fill}", + shadow: "0 4 5 0 rgb(0, 0, 0, 30)", + corner_radius: "8", + padding: "8", + background: "{background}", + {children} + } + ) +} diff --git a/crates/components/src/slider.rs b/crates/components/src/slider.rs index 10cb3f2f4..bec743297 100644 --- a/crates/components/src/slider.rs +++ b/crates/components/src/slider.rs @@ -84,8 +84,8 @@ pub fn Slider( }: SliderProps, ) -> Element { let theme = use_applied_theme!(&theme, slider); - let focus = use_focus(); - let status = use_signal(SliderStatus::default); + let mut focus = use_focus(); + let mut status = use_signal(SliderStatus::default); let mut clicking = use_signal(|| false); let platform = use_platform(); let (node_reference, size) = use_node(); @@ -93,35 +93,26 @@ pub fn Slider( let value = ensure_correct_slider_range(value); let focus_id = focus.attribute(); - use_drop({ - to_owned![status, platform]; - move || { - if *status.peek() == SliderStatus::Hovering { - platform.set_cursor(CursorIcon::default()); - } + use_drop(move || { + if *status.peek() == SliderStatus::Hovering { + platform.set_cursor(CursorIcon::default()); } }); - let onmouseleave = { - to_owned![platform, status]; - move |e: MouseEvent| { - e.stop_propagation(); - *status.write() = SliderStatus::Idle; - platform.set_cursor(CursorIcon::default()); - } + let onmouseleave = move |e: MouseEvent| { + e.stop_propagation(); + *status.write() = SliderStatus::Idle; + platform.set_cursor(CursorIcon::default()); }; - let onmouseenter = { - to_owned![status]; - move |e: MouseEvent| { - e.stop_propagation(); - *status.write() = SliderStatus::Hovering; - platform.set_cursor(CursorIcon::Pointer); - } + let onmouseenter = move |e: MouseEvent| { + e.stop_propagation(); + *status.write() = SliderStatus::Hovering; + platform.set_cursor(CursorIcon::Pointer); }; let onmouseover = { - to_owned![clicking, onmoved]; + to_owned![onmoved]; move |e: MouseEvent| { e.stop_propagation(); if *clicking.peek() { @@ -136,7 +127,7 @@ pub fn Slider( }; let onmousedown = { - to_owned![clicking, onmoved, focus]; + to_owned![onmoved]; move |e: MouseEvent| { e.stop_propagation(); focus.focus(); @@ -221,3 +212,61 @@ pub fn Slider( } ) } + +#[cfg(test)] +mod test { + use dioxus::prelude::use_signal; + use freya::prelude::*; + use freya_testing::*; + + #[tokio::test] + pub async fn slider() { + fn slider_app() -> Element { + let mut value = use_signal(|| 50.); + + rsx!( + Slider { + value: *value.read(), + onmoved: move |p| { + value.set(p); + } + } + label { + "{value}" + } + ) + } + + let mut utils = launch_test(slider_app); + let root = utils.root(); + let label = root.get(1); + utils.wait_for_update().await; + + assert_eq!(label.get(0).text(), Some("50")); + + utils.push_event(PlatformEvent::Mouse { + name: EventName::MouseOver, + cursor: (250.0, 7.0).into(), + button: Some(MouseButton::Left), + }); + utils.push_event(PlatformEvent::Mouse { + name: EventName::MouseDown, + cursor: (250.0, 7.0).into(), + button: Some(MouseButton::Left), + }); + utils.push_event(PlatformEvent::Mouse { + name: EventName::MouseOver, + cursor: (500.0, 7.0).into(), + button: Some(MouseButton::Left), + }); + utils.push_event(PlatformEvent::Mouse { + name: EventName::Click, + cursor: (500.0, 7.0).into(), + button: Some(MouseButton::Left), + }); + + utils.wait_for_update().await; + + assert_eq!(label.get(0).text(), Some("100")); + } +} diff --git a/crates/components/src/switch.rs b/crates/components/src/switch.rs index d8019163f..e20b27aa8 100644 --- a/crates/components/src/switch.rs +++ b/crates/components/src/switch.rs @@ -3,7 +3,7 @@ use freya_elements::elements as dioxus_elements; use freya_elements::events::{KeyboardEvent, MouseEvent}; use freya_hooks::{ - use_animation, use_applied_theme, use_focus, use_platform, Animation, SwitchThemeWith, + use_animation, use_applied_theme, use_focus, use_platform, AnimNum, SwitchThemeWith, }; use winit::window::CursorIcon; @@ -56,20 +56,17 @@ pub enum SwitchStatus { /// #[allow(non_snake_case)] pub fn Switch(props: SwitchProps) -> Element { - let mut animation = use_animation(|| 0.0); + let animation = use_animation(|ctx| ctx.with(AnimNum::new(0., 25.).time(200))); let theme = use_applied_theme!(&props.theme, switch); let platform = use_platform(); - let status = use_signal(SwitchStatus::default); - let focus = use_focus(); + let mut status = use_signal(SwitchStatus::default); + let mut focus = use_focus(); let focus_id = focus.attribute(); - use_drop({ - to_owned![status, platform]; - move || { - if *status.read() == SwitchStatus::Hovering { - platform.set_cursor(CursorIcon::default()); - } + use_drop(move || { + if *status.read() == SwitchStatus::Hovering { + platform.set_cursor(CursorIcon::default()); } }); @@ -77,13 +74,10 @@ pub fn Switch(props: SwitchProps) -> Element { e.stop_propagation(); }; - let onmouseleave = { - to_owned![platform]; - move |e: MouseEvent| { - e.stop_propagation(); - *status.write() = SwitchStatus::Idle; - platform.set_cursor(CursorIcon::default()); - } + let onmouseleave = move |e: MouseEvent| { + e.stop_propagation(); + *status.write() = SwitchStatus::Idle; + platform.set_cursor(CursorIcon::default()); }; let onmouseenter = move |e: MouseEvent| { @@ -94,7 +88,6 @@ pub fn Switch(props: SwitchProps) -> Element { let onclick = { let ontoggled = props.ontoggled.clone(); - to_owned![focus]; move |e: MouseEvent| { e.stop_propagation(); focus.focus(); @@ -102,24 +95,25 @@ pub fn Switch(props: SwitchProps) -> Element { } }; - let onkeydown = { - to_owned![focus]; - move |e: KeyboardEvent| { - if focus.validate_keydown(e) { - props.ontoggled.call(()); - } + let onkeydown = move |e: KeyboardEvent| { + if focus.validate_keydown(e) { + props.ontoggled.call(()); } }; let (offset_x, background, circle) = { if props.enabled { ( - animation.value(), + animation.read().get().read().as_f32(), theme.enabled_background, theme.enabled_thumb_background, ) } else { - (animation.value(), theme.background, theme.thumb_background) + ( + animation.read().get().read().as_f32(), + theme.background, + theme.thumb_background, + ) } }; let border = if focus.is_selected() { @@ -134,9 +128,9 @@ pub fn Switch(props: SwitchProps) -> Element { let _ = use_memo_with_dependencies(&props.enabled, move |enabled| { if enabled { - animation.start(Animation::new_sine_in_out(0.0..=25.0, 200)); - } else if animation.peek_value() > 0.0 { - animation.start(Animation::new_sine_in_out(25.0..=0.0, 200)); + animation.read().start(); + } else { + animation.read().reverse(); } }); @@ -171,3 +165,59 @@ pub fn Switch(props: SwitchProps) -> Element { } ) } + +#[cfg(test)] +mod test { + use dioxus::prelude::use_signal; + use freya::prelude::*; + use freya_testing::*; + + #[tokio::test] + pub async fn button() { + fn button_app() -> Element { + let mut enabled = use_signal(|| false); + + rsx!( + Switch { + enabled: *enabled.read(), + ontoggled: move |_| { + enabled.toggle(); + } + } + label { + "{enabled}" + } + ) + } + + let mut utils = launch_test(button_app); + let root = utils.root(); + let label = root.get(1); + utils.wait_for_update().await; + + // Default is false + assert_eq!(label.get(0).text(), Some("false")); + + utils.push_event(PlatformEvent::Mouse { + name: EventName::Click, + cursor: (5.0, 5.0).into(), + button: Some(MouseButton::Left), + }); + + utils.wait_for_update().await; + + // Check if after clicking it is now enabled + assert_eq!(label.get(0).text(), Some("true")); + + utils.push_event(PlatformEvent::Mouse { + name: EventName::Click, + cursor: (5.0, 5.0).into(), + button: Some(MouseButton::Left), + }); + + utils.wait_for_update().await; + + // Check if after clicking again it is now disabled + assert_eq!(label.get(0).text(), Some("false")); + } +} diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 6e6945693..da24d31e6 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -26,19 +26,14 @@ freya-dom = { workspace = true } freya-engine = { workspace = true } torin = { workspace = true } -dioxus-rsx = { workspace = true } dioxus-native-core = { workspace = true } -dioxus-core-macro = { workspace = true } -dioxus-hooks = { workspace = true } dioxus-core = { workspace = true } tokio = { workspace = true } winit = { workspace = true } accesskit = { workspace = true } -zbus = { workspace = true } rustc-hash= { workspace = true } -tracing = { workspace = true } uuid = { workspace = true } itertools = "0.11.0" smallvec = "1.11.2" diff --git a/crates/core/src/accessibility/accessibility_manager.rs b/crates/core/src/accessibility/accessibility_manager.rs index bbe14ec96..f7a947766 100644 --- a/crates/core/src/accessibility/accessibility_manager.rs +++ b/crates/core/src/accessibility/accessibility_manager.rs @@ -181,9 +181,9 @@ impl AccessibilityManager { // Find the previous Node if let Some(node_index) = node_index { if node_index == 0 { - self.nodes.get(node_index - 1) - } else { self.nodes.last() + } else { + self.nodes.get(node_index - 1) } } else { self.nodes.last() diff --git a/crates/core/src/events/dom_events.rs b/crates/core/src/events/dom_event.rs similarity index 68% rename from crates/core/src/events/dom_events.rs rename to crates/core/src/events/dom_event.rs index 96e170a71..3fa95695a 100644 --- a/crates/core/src/events/dom_events.rs +++ b/crates/core/src/events/dom_event.rs @@ -8,15 +8,19 @@ use freya_elements::{ }; use torin::prelude::*; -use crate::events::FreyaEvent; +use crate::{events::PlatformEvent, prelude::PotentialEvent}; + +use super::event_name::EventName; /// Event emitted to the DOM. #[derive(Debug, Clone, PartialEq)] pub struct DomEvent { - pub name: String, + pub name: EventName, pub node_id: NodeId, pub element_id: ElementId, pub data: DomEventData, + pub bubbles: bool, + pub layer: Option, } impl Eq for DomEvent {} @@ -29,72 +33,67 @@ impl PartialOrd for DomEvent { impl Ord for DomEvent { fn cmp(&self, other: &Self) -> std::cmp::Ordering { - match self.name.as_str() { - "mouseleave" | "pointerleave" => { - if self.name == other.name { - std::cmp::Ordering::Equal - } else { - std::cmp::Ordering::Less - } - } - _ => std::cmp::Ordering::Greater, - } + self.name.cmp(&other.name) } } impl DomEvent { - pub fn does_move_cursor(&self) -> bool { - return does_event_move_cursor(self.name.as_str()); - } - pub fn new( - node_id: NodeId, + PotentialEvent { + node_id, + layer, + event, + }: PotentialEvent, element_id: ElementId, - event: &FreyaEvent, node_area: Option, scale_factor: f64, ) -> Self { - let is_pointer_event = event.is_pointer_event(); - let event_name = event.get_name().to_string(); + let name = event.get_name(); + + let bubbles = name.does_bubble(); match event { - FreyaEvent::Mouse { cursor, button, .. } => { - let screen_coordinates = *cursor / scale_factor; + PlatformEvent::Mouse { cursor, button, .. } => { + let screen_coordinates = cursor / scale_factor; let element_x = (cursor.x - node_area.unwrap_or_default().min_x() as f64) / scale_factor; let element_y = (cursor.y - node_area.unwrap_or_default().min_y() as f64) / scale_factor; - let event_data = if is_pointer_event { + let event_data = if name.is_pointer() { DomEventData::Pointer(PointerData::new( screen_coordinates, (element_x, element_y).into(), PointerType::Mouse { - trigger_button: *button, + trigger_button: button, }, )) } else { DomEventData::Mouse(MouseData::new( screen_coordinates, (element_x, element_y).into(), - *button, + button, )) }; Self { node_id, element_id, - name: event_name, + name, data: event_data, + bubbles, + layer, } } - FreyaEvent::Wheel { scroll, .. } => Self { + PlatformEvent::Wheel { scroll, .. } => Self { node_id, element_id, - name: event_name, + name, data: DomEventData::Wheel(WheelData::new(scroll.x, scroll.y)), + bubbles, + layer, }, - FreyaEvent::Keyboard { + PlatformEvent::Keyboard { ref key, code, modifiers, @@ -102,10 +101,12 @@ impl DomEvent { } => Self { node_id, element_id, - name: event_name, - data: DomEventData::Keyboard(KeyboardData::new(key.clone(), *code, *modifiers)), + name, + data: DomEventData::Keyboard(KeyboardData::new(key.clone(), code, modifiers)), + bubbles, + layer, }, - FreyaEvent::Touch { + PlatformEvent::Touch { location, finger_id, phase, @@ -115,31 +116,33 @@ impl DomEvent { let element_x = location.x - node_area.unwrap_or_default().min_x() as f64; let element_y = location.y - node_area.unwrap_or_default().min_y() as f64; - let event_data = if is_pointer_event { + let event_data = if name.is_pointer() { DomEventData::Pointer(PointerData::new( - *location, + location, (element_x, element_y).into(), PointerType::Touch { - finger_id: *finger_id, - phase: *phase, - force: *force, + finger_id, + phase, + force, }, )) } else { DomEventData::Touch(TouchData::new( - *location, + location, (element_x, element_y).into(), - *finger_id, - *phase, - *force, + finger_id, + phase, + force, )) }; Self { node_id, element_id, - name: event_name, + name, data: event_data, + bubbles, + layer, } } } @@ -167,7 +170,3 @@ impl DomEventData { } } } - -pub fn does_event_move_cursor(event_name: &str) -> bool { - ["pointerover", "pointerenter", "mouseover", "mouseenter"].contains(&event_name) -} diff --git a/crates/core/src/events/elements_state.rs b/crates/core/src/events/elements_state.rs deleted file mode 100644 index 4c4832af5..000000000 --- a/crates/core/src/events/elements_state.rs +++ /dev/null @@ -1,124 +0,0 @@ -#![allow(clippy::type_complexity)] - -use dioxus_native_core::NodeId; -use rustc_hash::{FxHashMap, FxHashSet}; - -use crate::events::{does_event_move_cursor, DomEvent, FreyaEvent}; - -/// [`ElementsState`] stores the elements states given incoming events. -#[derive(Default)] -pub struct ElementsState { - hovered_elements: FxHashSet, -} - -impl ElementsState { - /// Update the Element states given the new events - pub fn process_events( - &mut self, - events_to_emit: &[DomEvent], - events: &[FreyaEvent], - ) -> (FxHashMap>, Vec) { - let mut new_events_to_emit = Vec::default(); - let mut new_events = FxHashMap::>::default(); - - let recent_mouse_movement_event = any_recent_mouse_movement(events); - - // Suggest emitting `mouseleave` in elements not being hovered anymore - self.hovered_elements.retain(|node_id| { - let no_recent_mouse_movement_on_me = - has_node_been_hovered_recently(events_to_emit, node_id); - - if no_recent_mouse_movement_on_me { - if let Some(FreyaEvent::Mouse { cursor, button, .. }) = recent_mouse_movement_event - { - let events = new_events.entry("mouseleave".to_string()).or_default(); - events.push(( - *node_id, - FreyaEvent::Mouse { - name: "mouseleave".to_string(), - cursor, - button, - }, - )); - - // Remove the node from the list of hovered elements - return false; - } - } - true - }); - - // All these events will mark the node as being hovered - // "mouseover" "mouseenter" "pointerover" "pointerenter" - - // We clone this here so events emitted in the same batch that mark an element as hovered will not affect the other events - let hovered_elements = self.hovered_elements.clone(); - - // Emit valid events - for event in events_to_emit { - let id = &event.node_id; - - let should_trigger = match event.name.as_str() { - name @ "mouseover" - | name @ "mouseenter" - | name @ "pointerover" - | name @ "pointerenter" => { - let is_hovered = hovered_elements.contains(id); - - if !is_hovered { - self.hovered_elements.insert(*id); - } - - if name == "mouseenter" || name == "pointerenter" { - // If the event is already being hovered then it's pointless to trigger the movement event - !is_hovered - } else { - true - } - } - _ => true, - }; - - if should_trigger { - new_events_to_emit.push(event.clone()); - } - } - - // Update the internal states of elements given the events - // e.g `mouseover` will mark the element as hovered. - for event in events_to_emit { - let id = &event.node_id; - if does_event_move_cursor(event.name.as_str()) && !self.hovered_elements.contains(id) { - self.hovered_elements.insert(*id); - } - } - - (new_events, new_events_to_emit) - } -} - -fn any_recent_mouse_movement(events: &[FreyaEvent]) -> Option { - events - .iter() - .find(|event| { - if let FreyaEvent::Mouse { name, .. } = event { - does_event_move_cursor(name) - } else { - false - } - }) - .cloned() -} - -fn has_node_been_hovered_recently(events_to_emit: &[DomEvent], element: &NodeId) -> bool { - events_to_emit - .iter() - .find_map(|event| { - if event.does_move_cursor() && &event.node_id == element { - Some(false) - } else { - None - } - }) - .unwrap_or(true) -} diff --git a/crates/core/src/events/event_name.rs b/crates/core/src/events/event_name.rs new file mode 100644 index 000000000..1517fd13a --- /dev/null +++ b/crates/core/src/events/event_name.rs @@ -0,0 +1,165 @@ +use smallvec::SmallVec; + +#[derive(Clone, Copy, PartialEq, Debug, Hash)] +pub enum EventName { + Click, + + MouseDown, + MouseOver, + MouseEnter, + MouseLeave, + + Wheel, + + PointerOver, + PointerDown, + PointerEnter, + PointerLeave, + PointerUp, + + KeyDown, + KeyUp, + + TouchCancel, + TouchStart, + TouchMove, + TouchEnd, + + GlobalClick, + GlobalMouseDown, + GlobalMouseOver, +} + +impl From for &str { + fn from(event: EventName) -> Self { + match event { + EventName::Click => "click", + EventName::MouseDown => "mousedown", + EventName::MouseOver => "mouseover", + EventName::MouseEnter => "mouseenter", + EventName::MouseLeave => "mouseleave", + EventName::Wheel => "wheel", + EventName::PointerOver => "pointerover", + EventName::PointerDown => "pointerdown", + EventName::PointerEnter => "pointerenter", + EventName::PointerLeave => "pointerleave", + EventName::PointerUp => "pointerup", + EventName::KeyDown => "keydown", + EventName::KeyUp => "keyup", + EventName::TouchCancel => "touchcancel", + EventName::TouchStart => "touchstart", + EventName::TouchMove => "touchmove", + EventName::TouchEnd => "touchend", + EventName::GlobalClick => "globalclick", + EventName::GlobalMouseDown => "globalmousedown", + EventName::GlobalMouseOver => "globalmouseover", + } + } +} + +impl Eq for EventName {} + +impl PartialOrd for EventName { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for EventName { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + match self { + // Always priorize leave events before anything else + Self::MouseLeave | Self::PointerLeave => { + if self == other { + std::cmp::Ordering::Equal + } else { + std::cmp::Ordering::Less + } + } + _ => std::cmp::Ordering::Greater, + } + } +} + +impl EventName { + /// Get the equivalent to a global event + pub fn get_global_event(&self) -> Option { + match self { + Self::Click => Some(Self::GlobalClick), + Self::MouseDown => Some(Self::GlobalMouseDown), + Self::MouseOver => Some(Self::GlobalMouseOver), + _ => None, + } + } + + /// Some events might cause other events, like for example: + /// A `mouseover` might also trigger a `mouseenter` + /// A `mousedown` or a `touchdown` might also trigger a `pointerdown` + pub fn get_colateral_events(&self) -> SmallVec<[Self; 4]> { + let mut events = SmallVec::new(); + + events.push(*self); + + match self { + Self::MouseOver | Self::TouchMove => { + events.extend([Self::MouseEnter, Self::PointerEnter, Self::PointerOver]) + } + Self::MouseDown | Self::TouchStart => events.push(Self::PointerDown), + Self::Click | Self::TouchEnd => events.push(Self::PointerUp), + Self::MouseLeave => events.push(Self::PointerLeave), + _ => {} + } + + events + } + + /// Check if the event event means that the pointer (e.g cursor) just entered a Node + pub fn is_enter(&self) -> bool { + matches!(&self, Self::MouseEnter | Self::PointerEnter) + } + + /// Check if it's one of the Pointer variants + pub fn is_pointer(&self) -> bool { + matches!( + &self, + Self::PointerEnter + | Self::PointerLeave + | Self::PointerOver + | Self::PointerDown + | Self::PointerUp + ) + } + + /// Check if the event means the cursor was moved + pub fn was_cursor_moved(&self) -> bool { + matches!( + &self, + Self::MouseOver | Self::MouseEnter | Self::PointerEnter | Self::PointerOver + ) + } + + // Bubble all events except: + // - Keyboard events + // - Mouse movements events + pub fn does_bubble(&self) -> bool { + !matches!( + self, + Self::KeyDown + | Self::KeyUp + | Self::MouseLeave + | Self::PointerLeave + | Self::MouseEnter + | Self::PointerEnter + | Self::MouseOver + | Self::PointerOver + ) + } + + // Check if this event can change the hover state of a Node. + pub fn can_change_hover_state(&self) -> bool { + matches!( + self, + Self::MouseOver | Self::MouseEnter | Self::PointerOver | Self::PointerEnter + ) + } +} diff --git a/crates/core/src/events/events_measurer.rs b/crates/core/src/events/events_measurer.rs index b0f83fbc6..896ebd76e 100644 --- a/crates/core/src/events/events_measurer.rs +++ b/crates/core/src/events/events_measurer.rs @@ -6,23 +6,24 @@ use freya_dom::{dom::DioxusDOM, prelude::FreyaDOM}; use freya_engine::prelude::*; use freya_node_state::{Fill, Style}; -use rustc_hash::FxHashMap; -pub use crate::events::{DomEvent, ElementsState, FreyaEvent}; +pub use crate::events::{DomEvent, NodesState, PlatformEvent}; use crate::types::{EventEmitter, EventsQueue, PotentialEvents}; +use super::potential_event::PotentialEvent; + /// Process the events and emit them to the VirtualDOM pub fn process_events( dom: &FreyaDOM, layers: &Layers, events: &mut EventsQueue, event_emitter: &EventEmitter, - elements_state: &mut ElementsState, + nodes_state: &mut NodesState, viewports: &Viewports, scale_factor: f64, ) { - // 1. Get global events created from the incominge vents + // 1. Get global events created from the incoming events let global_events = measure_global_events(events); // 2. Get potential events that could be emitted based on the elements layout and viewports @@ -31,11 +32,11 @@ pub fn process_events( // 3. Get what events can be actually emitted based on what elements are listening let dom_events = measure_dom_events(potential_events, dom, scale_factor); - // 4. Filter the dom events and get potential derived events, e.g mouseover -> mouseenter + // 4. Filter the dom events and get potential colateral events, e.g mouseover -> mouseenter let (potential_colateral_events, mut to_emit_dom_events) = - elements_state.process_events(&dom_events, events); + nodes_state.process_events(&dom_events, events); - // 5. Get what derived events can actually be emitted + // 5. Get what colateral events can actually be emitted let to_emit_dom_colateral_events = measure_dom_events(potential_colateral_events, dom, scale_factor); @@ -56,20 +57,15 @@ pub fn process_events( } /// Measure globale events -pub fn measure_global_events(events: &EventsQueue) -> Vec { +pub fn measure_global_events(events: &EventsQueue) -> Vec { let mut global_events = Vec::default(); for event in events { - let event_name = match event.get_name() { - "click" => Some("globalclick"), - "mousedown" => Some("globalmousedown"), - "mouseover" => Some("globalmouseover"), - _ => None, + let Some(event_name) = event.get_name().get_global_event() else { + continue; }; - if let Some(event_name) = event_name { - let mut global_event = event.clone(); - global_event.set_name(event_name.to_string()); - global_events.push(global_event); - } + let mut global_event = event.clone(); + global_event.set_name(event_name); + global_events.push(global_event); } global_events } @@ -81,27 +77,28 @@ pub fn measure_potential_event_listeners( viewports: &Viewports, fdom: &FreyaDOM, ) -> PotentialEvents { - let mut potential_events = FxHashMap::default(); + let mut potential_events = PotentialEvents::default(); let layout = fdom.layout(); // Propagate events from the top to the bottom - for (_, layer) in layers.layers() { - for node_id in layer { + for (layer, layer_nodes) in layers.layers() { + for node_id in layer_nodes { let areas = layout.get(*node_id); if let Some(areas) = areas { 'events: for event in events.iter() { - if let FreyaEvent::Keyboard { name, .. } = event { - let event_data = (*node_id, event.clone()); - potential_events - .entry(name.clone()) - .or_insert_with(|| vec![event_data.clone()]) - .push(event_data); + if let PlatformEvent::Keyboard { name, .. } = event { + let event_data = PotentialEvent { + node_id: *node_id, + layer: Some(*layer), + event: event.clone(), + }; + potential_events.entry(*name).or_default().push(event_data); } else { let data = match event { - FreyaEvent::Mouse { name, cursor, .. } => Some((name, cursor)), - FreyaEvent::Wheel { name, cursor, .. } => Some((name, cursor)), - FreyaEvent::Touch { name, location, .. } => Some((name, location)), + PlatformEvent::Mouse { name, cursor, .. } => Some((name, cursor)), + PlatformEvent::Wheel { name, cursor, .. } => Some((name, cursor)), + PlatformEvent::Touch { name, location, .. } => Some((name, location)), _ => None, }; if let Some((name, cursor)) = data { @@ -123,10 +120,14 @@ pub fn measure_potential_event_listeners( } } - let event_data = (*node_id, event.clone()); + let event_data = PotentialEvent { + node_id: *node_id, + layer: Some(*layer), + event: event.clone(), + }; potential_events - .entry(name.clone()) + .entry(*name) .or_insert_with(Vec::new) .push(event_data); } @@ -140,27 +141,6 @@ pub fn measure_potential_event_listeners( potential_events } -/// Some events might cause other events, like for example: -/// A `mouseover` might also trigger a `mouseenter` -/// A `mousedown` or a `touchdown` might also trigger a `pointerdown` -fn get_derivated_events(event_name: &str) -> Vec<&str> { - match event_name { - "mouseover" | "touchmove" => { - vec![event_name, "mouseenter", "pointerenter", "pointerover"] - } - "mousedown" | "touchstart" => { - vec![event_name, "pointerdown"] - } - "click" | "touchend" => { - vec![event_name, "pointerup"] - } - "mouseleave" => { - vec![event_name, "pointerleave"] - } - _ => vec![event_name], - } -} - fn is_node_parent_of(rdom: &DioxusDOM, node: NodeId, parent_node: NodeId) -> bool { let mut stack = vec![parent_node]; while let Some(id) = stack.pop() { @@ -191,18 +171,23 @@ fn measure_dom_events( // Iterate over all the events for (event_name, event_nodes) in potential_events { - let derivated_events = get_derivated_events(event_name.as_str()); + let colateral_events = event_name.get_colateral_events(); - let mut found_nodes: Vec<(&NodeId, FreyaEvent)> = Vec::new(); + let mut valid_events: Vec = Vec::new(); - // Iterate over the derivated event (including the source) - 'event: for derivated_event_name in derivated_events.iter() { + // Iterate over the colateral events (including the source) + 'event: for colateral_event in colateral_events { let mut child_node: Option = None; - let listeners = rdom.get_listening_sorted(derivated_event_name); + let listeners = rdom.get_listening_sorted(colateral_event.into()); // Iterate over the event nodes - for (node_id, event) in event_nodes.iter().rev() { + for PotentialEvent { + node_id, + event, + layer, + } in event_nodes.iter().rev() + { let Some(node) = rdom.get(*node_id) else { continue; }; @@ -218,11 +203,15 @@ fn measure_dom_events( if valid_node { let mut valid_event = event.clone(); - valid_event.set_name(derivated_event_name.to_string()); - found_nodes.push((node_id, valid_event)); - - // Only stop looking for valid nodes when the event isn't of type keyboard - if !event.is_keyboard_event() { + valid_event.set_name(colateral_event); + valid_events.push(PotentialEvent { + node_id: *node_id, + event: valid_event, + layer: *layer, + }); + + // Stack events that do not bubble up + if event.get_name().does_bubble() { continue 'event; } } @@ -231,24 +220,25 @@ fn measure_dom_events( let Style { background, .. } = &*node.get:: + \ No newline at end of file