diff --git a/.github/overview.png b/.github/overview.png new file mode 100644 index 000000000..34ecc9cf7 Binary files /dev/null and b/.github/overview.png differ diff --git a/.github/workflows/sponsors.yml b/.github/workflows/sponsors.yml index 8ae389908..7b53d946a 100644 --- a/.github/workflows/sponsors.yml +++ b/.github/workflows/sponsors.yml @@ -5,6 +5,7 @@ on: - cron: 30 15 * * 0-6 jobs: deploy: + if: github.repository == 'marc2332/freya' runs-on: ubuntu-latest steps: - name: Checkout πŸ›ŽοΈ diff --git a/.gitignore b/.gitignore index 4c4071f5b..79f56b260 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ Cargo.lock .idea snapshot_before.png snapshot_after.png -documents_example \ No newline at end of file +documents_example +bacon.toml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b8831994e..6f35be597 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,9 +24,9 @@ cargo +nightly fmt --all -- --error-on-unformatted --unstable-features Freya is split in various crates, each with it's own meaning and purpose, here is the list sorted by their importance: - `freya`: Entrypoint to the library used by end users, mainly reexports the other crates and contains the launch methods. -- `renderer`: GUI Renderer using Winit and a Skia Canvas to render the app. -- `core`: Core logic for events, DOM processing, accessibility integration and text layout measurement is located here. -- `native-core`: DOM tree-like data structure to hold all the nodes with their attribute values and registered event handlers. +- `renderer`: Provides a winit event loop based execution for the app. +- `core`: Core logic for events, DOM processing, accessibility integration, element rendering and text layout measurement is located here. +- `native-core`: DOM data structure to hold all the nodes with their attribute values and registered event handlers. - `torin`: UI layout library specifically made for Freya, although it's agnostic. - `hooks`: Various Dioxus hooks to be used in Freya apps (text editing, animation, theming, etc) - `components`: Collection of built-in Dioxus components to be used out of the box with in Freya apps (Button, Switch, Slider, Table, ScrollView, etc) @@ -38,6 +38,8 @@ Freya is split in various crates, each with it's own meaning and purpose, here i - `native-core-macro`: Just some internal macros to be used in `states` so it can be integrated with `native-core`. - `common`: Some simple utilities used across the different Freya crates. +![Overview](./.github/overview.png) + ## Examples All important examples are located in the `./examples` folder although you might also find some in the form of docs comments in the code itself. diff --git a/Cargo.toml b/Cargo.toml index c05a7f57c..0deac8d48 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,9 +51,9 @@ dioxus-signals = { version = "0.5" } dioxus-core = { version = "0.5" } dioxus-hot-reload = { version = "0.5", features = ["file_watcher"], default-features = false } dioxus-router = { version = "0.5", default-features = false } -dioxus-sdk = { version = "0.5", features = ["clipboard"]} +dioxus-clipboard = "0.1" -skia-safe = { version = "0.75.0", features = ["gl", "textlayout", "svg"] } +skia-safe = { version = "0.80.0", features = ["gl", "textlayout", "svg"] } gl = "0.14.0" glutin = "0.32.0" @@ -79,6 +79,7 @@ skia-safe = { workspace = true } tokio = { workspace = true, features = ["fs"]} dioxus = { workspace = true } freya = { workspace = true } +freya-hooks = { workspace = true } freya-core = { workspace = true } freya-testing = { workspace = true } reqwest = { version = "0.12.0", features = ["json"] } @@ -95,7 +96,8 @@ tree-sitter-highlight = "0.23.0" tree-sitter-rust = "0.23.0" rfd = "0.14.1" bytes = "1.5.0" -dioxus-sdk = { workspace = true } +dioxus-clipboard = { workspace = true } +winit = { workspace = true } [profile.release] lto = true diff --git a/README.md b/README.md index 9dcc95683..c68d77b20 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,12 @@ [Website](https://freyaui.dev) | [Nightly Docs](https://docs.freyaui.dev/freya) | [Stable Docs](https://docs.rs/freya/latest/freya) | [Book](https://book.freyaui.dev) | [Discord](https://discord.gg/sYejxCdewG) -**Freya** is a cross-paltform GUI library for Rust powered by 🧬 [Dioxus](https://dioxuslabs.com) and 🎨 [Skia](https://skia.org/). +**Freya** is a cross-platform GUI library for Rust powered by 🧬 [Dioxus](https://dioxuslabs.com) and 🎨 [Skia](https://skia.org/). **It does not use any web tech**, check the [Differences with Dioxus](https://book.freyaui.dev/differences_with_dioxus.html). ⚠️ It's currently work in progress, but you can already play with it! You can join the [Discord](https://discord.gg/sYejxCdewG) server if you have any question or issue. -

@@ -66,12 +65,6 @@ fn app() -> Element {
-### Sponsors πŸ€— - -Thanks to my sponsors for supporting this project! πŸ˜„ - - - ### Want to try it? πŸ€” πŸ‘‹ Make sure to check the [Setup guide](https://book.freyaui.dev/setup.html) first. @@ -93,19 +86,21 @@ Add Freya and Dioxus as dependencies: freya = "0.2" dioxus = { version = "0.5", features = ["macro", "hooks"], default-features = false } ``` +### Contributing πŸ§™β€β™‚οΈ + +If you are interested in contributing please make sure to have read the [Contributing](CONTRIBUTING.md) guide first! ### Features ✨ - ⛏️ Built-in **components** (button, scroll views, switch and more) -- πŸš‡ Built-in **hooks** library (animations, text editing and more) -- πŸ” Built-in **devtools** panel +- πŸš‡ Built-in **hooks** (animations, text editing and more) +- πŸ” Built-in **developer tools** (tree inspection, fps overlay) - 🧰 Built-in **headless runner** to test UI -- 🎨 **Theming** support (not extensible yet ⚠️) +- 🎨 **Theming** support - πŸ›©οΈ **Cross-platform** (Windows, Linux, MacOS) - πŸ–ΌοΈ SKSL **Shaders** support -- πŸ”„οΈ Dioxus **Hot-reload** support - πŸ“’ Multi-line **text editing** -- 🦾 Basic **Accessibility** Support (experimental ⚠️) -- 🧩Compatible with dioxus-sdk and other Dioxus renderer-agnostic libraries +- 🦾 **Accessibility** support +- 🧩 Compatible with dioxus-sdk and other Dioxus renderer-agnostic libraries ### Goals 😁 - Performant and low memory usage @@ -115,6 +110,22 @@ dioxus = { version = "0.5", features = ["macro", "hooks"], default-features = fa - Useful testing APIs - Useful and extensible built-in components and hooks +### Support πŸ€— + +If you are interested in supporting the development of this project feel free to donate to my [Github Sponsor](https://github.com/sponsors/marc2332/) page. + +Thanks to my sponsors for supporting this project! πŸ˜„ + + + +### Special thanks πŸ’ͺ + +- [Jonathan Kelley](https://github.com/jkelleyrtp) and [Evan Almloff](https://github.com/ealmloff) for making [Dioxus](https://dioxuslabs.com/) and all their help, specially when I was still creating Freya. +- [Armin](https://github.com/pragmatrix) for making [rust-skia](https://github.com/rust-skia/rust-skia/) and all his help and making the favor of hosting prebuilt binaries of skia for the combo of features use by Freya. +- [geom3trik](https://github.com/geom3trik) for helping me figure out how to add incremental rendering. +- [Tropical](https://github.com/Tropix126) for this contributions to improving accessibility and rendering. +- And to the rest of contributors and anybody who gave me any kind of feedback! + ### 🀠 Projects [Valin](https://github.com/marc2332/valin) βš’οΈ is a Work-In-Progress cross-platform code editor, made with Freya πŸ¦€ and Rust, by me. diff --git a/crates/common/src/accessibility.rs b/crates/common/src/accessibility.rs index 2ee331f2b..a8d51a7b8 100644 --- a/crates/common/src/accessibility.rs +++ b/crates/common/src/accessibility.rs @@ -25,8 +25,8 @@ impl AccessibilityDirtyNodes { self.added_or_updated.insert(node_id); } - pub fn remove(&mut self, node_id: NodeId, ancestor_node_id: NodeId) { - self.removed.insert(node_id, ancestor_node_id); + pub fn remove(&mut self, node_id: NodeId, parent_id: NodeId) { + self.removed.insert(node_id, parent_id); } pub fn clear(&mut self) { diff --git a/crates/components/src/activable_route.rs b/crates/components/src/activable_route.rs index e07fbe36a..362adc2b6 100644 --- a/crates/components/src/activable_route.rs +++ b/crates/components/src/activable_route.rs @@ -5,7 +5,29 @@ use dioxus_router::{ }; use freya_hooks::ActivableRouteContext; -/// Provide a context to the inner components so they can know whether the passed route is the current router in the Router or not. +/// Sometimes you might want to know if a route is selected so you can style a specific UI element in a different way, +/// like a button with a different color. +/// To avoid cluttering your components with router-specific code you might instead want to wrap your component in an `ActivableRoute` +/// and inside your component call `use_activable_route`. +/// +/// This way, your component and all its desdendants will just know whether a route is activated or not, but not which one. +/// +/// ```rs +/// Link { +/// to: Route::Home, // Direction route +/// ActivableRoute { +/// route: Route::Home, // Activation route +/// SidebarItem { +/// // `SidebarItem` will now appear "activated" when the route is `Route::Home` +/// // `ActivableRoute` is letting it know whether `Route::Home` is enabled +/// // or not, without the need to add router-specific logic in `SidebarItem`. +/// label { +/// "Go to Hey ! πŸ‘‹" +/// } +/// }, +/// } +/// } +/// ``` #[allow(non_snake_case)] #[component] pub fn ActivableRoute( diff --git a/crates/components/src/button.rs b/crates/components/src/button.rs index f00946595..a31f07ba5 100644 --- a/crates/components/src/button.rs +++ b/crates/components/src/button.rs @@ -22,6 +22,112 @@ use winit::{ window::CursorIcon, }; +/// Properties for the [`Button`], [`FilledButton`] and [`OutlineButton`] components. +#[derive(Props, Clone, PartialEq)] +pub struct ButtonProps { + /// Theme override. + pub theme: Option, + /// Inner children for the button. + pub children: Element, + /// Event handler for when the button is pressed. + pub onpress: Option>, + /// Event handler for when the button is clicked. Not recommended, use `onpress` instead. + pub onclick: Option>, +} + +/// Clickable button. +/// +/// # Styling +/// Inherits the [`ButtonTheme`](freya_hooks::ButtonTheme) theme. +/// +/// # Example +/// +/// ```no_run +/// # use freya::prelude::*; +/// fn app() -> Element { +/// rsx!( +/// Button { +/// onpress: |_| println!("clicked"), +/// label { +/// "Click this" +/// } +/// } +/// ) +/// } +/// ``` +#[allow(non_snake_case)] +pub fn Button(props: ButtonProps) -> Element { + let theme = use_applied_theme!(&props.theme, button); + ButtonBase(BaseButtonProps { + theme, + children: props.children, + onpress: props.onpress, + onclick: props.onclick, + }) +} + +/// Clickable button with a solid fill color. +/// +/// # Styling +/// Inherits the filled [`ButtonTheme`](freya_hooks::ButtonTheme) theme. +/// +/// # Example +/// +/// ```no_run +/// # use freya::prelude::*; +/// fn app() -> Element { +/// rsx!( +/// FilledButton { +/// onpress: |_| println!("clicked"), +/// label { +/// "Click this" +/// } +/// } +/// ) +/// } +/// ``` +#[allow(non_snake_case)] +pub fn FilledButton(props: ButtonProps) -> Element { + let theme = use_applied_theme!(&props.theme, filled_button); + ButtonBase(BaseButtonProps { + theme, + children: props.children, + onpress: props.onpress, + onclick: props.onclick, + }) +} + +/// Clickable button with an outline style. +/// +/// # Styling +/// Inherits the outline [`ButtonTheme`](freya_hooks::ButtonTheme) theme. +/// +/// # Example +/// +/// ```no_run +/// # use freya::prelude::*; +/// fn app() -> Element { +/// rsx!( +/// OutlineButton { +/// onpress: |_| println!("clicked"), +/// label { +/// "Click this" +/// } +/// } +/// ) +/// } +/// ``` +#[allow(non_snake_case)] +pub fn OutlineButton(props: ButtonProps) -> Element { + let theme = use_applied_theme!(&props.theme, outline_button); + ButtonBase(BaseButtonProps { + theme, + children: props.children, + onpress: props.onpress, + onclick: props.onclick, + }) +} + pub enum PressEvent { Pointer(PointerEvent), Key(KeyboardEvent), @@ -38,10 +144,10 @@ impl PressEvent { /// Properties for the [`Button`] component. #[derive(Props, Clone, PartialEq)] -pub struct ButtonProps { - /// Theme override. - pub theme: Option, - /// Inner children for the Button. +pub struct BaseButtonProps { + /// Theme. + pub theme: ButtonTheme, + /// Inner children for the button. pub children: Element, /// Event handler for when the button is pressed. pub onpress: Option>, @@ -59,34 +165,14 @@ pub enum ButtonStatus { Hovering, } -/// Clickable button. -/// -/// # Styling -/// Inherits the [`ButtonTheme`](freya_hooks::ButtonTheme) theme. -/// -/// # Example -/// -/// ```no_run -/// # use freya::prelude::*; -/// fn app() -> Element { -/// rsx!( -/// Button { -/// onpress: |_| println!("clicked"), -/// label { -/// "Click this" -/// } -/// } -/// ) -/// } -/// ``` #[allow(non_snake_case)] -pub fn Button( - ButtonProps { +pub fn ButtonBase( + BaseButtonProps { onpress, children, theme, onclick, - }: ButtonProps, + }: BaseButtonProps, ) -> Element { let mut focus = use_focus(); let mut status = use_signal(ButtonStatus::default); @@ -106,7 +192,7 @@ pub fn Button( height, font_theme, shadow, - } = use_applied_theme!(&theme, button); + } = theme; let onpointerup = { to_owned![onpress, onclick]; @@ -187,7 +273,7 @@ pub fn Button( border: "{border}", corner_radius: "{corner_radius}", background: "{background}", - text_align: "center", + text_height: "disable-least-ascent", main_align: "center", cross_align: "center", {&children} diff --git a/crates/components/src/checkbox.rs b/crates/components/src/checkbox.rs index 8370af041..afb4dc9e6 100644 --- a/crates/components/src/checkbox.rs +++ b/crates/components/src/checkbox.rs @@ -85,32 +85,28 @@ pub fn Checkbox( ("transparent", unselected_fill.as_ref()) }; let border = if focus.is_selected() { - format!("4 outer {}", border_fill) + format!("2 inner {outer_fill}, 4 outer {border_fill}") } else { - "none".to_string() + format!("2 inner {outer_fill}") }; let onkeydown = move |_: KeyboardEvent| {}; rsx!( rect { - border, + a11y_id: focus.attribute(), + width: "18", + height: "18", + padding: "4", + main_align: "center", + cross_align: "center", corner_radius: "4", - rect { - a11y_id: focus.attribute(), - width: "18", - height: "18", - padding: "4", - main_align: "center", - cross_align: "center", - corner_radius: "4", - border: "2 inner {outer_fill}", - background: "{inner_fill}", - onkeydown, - if selected { - TickIcon { - fill: selected_icon_fill - } + border, + background: "{inner_fill}", + onkeydown, + if selected { + TickIcon { + fill: selected_icon_fill } } } @@ -191,29 +187,29 @@ mod test { utils.wait_for_update().await; // If the inner square exists it means that the Checkbox is selected, otherwise it isn't - assert!(root.get(0).get(0).get(0).get(0).get(0).is_placeholder()); - assert!(root.get(1).get(0).get(0).get(0).get(0).is_placeholder()); - assert!(root.get(2).get(0).get(0).get(0).get(0).is_placeholder()); + assert!(root.get(0).get(0).get(0).get(0).is_placeholder()); + assert!(root.get(1).get(0).get(0).get(0).is_placeholder()); + assert!(root.get(2).get(0).get(0).get(0).is_placeholder()); utils.click_cursor((20., 50.)).await; - assert!(root.get(0).get(0).get(0).get(0).get(0).is_placeholder()); - assert!(root.get(1).get(0).get(0).get(0).get(0).is_element()); - assert!(root.get(2).get(0).get(0).get(0).get(0).is_placeholder()); + assert!(root.get(0).get(0).get(0).get(0).is_placeholder()); + assert!(root.get(1).get(0).get(0).get(0).is_element()); + assert!(root.get(2).get(0).get(0).get(0).is_placeholder()); utils.click_cursor((10., 90.)).await; utils.wait_for_update().await; - assert!(root.get(0).get(0).get(0).get(0).get(0).is_placeholder()); - assert!(root.get(1).get(0).get(0).get(0).get(0).is_element()); - assert!(root.get(2).get(0).get(0).get(0).get(0).is_element()); + assert!(root.get(0).get(0).get(0).get(0).is_placeholder()); + assert!(root.get(1).get(0).get(0).get(0).is_element()); + assert!(root.get(2).get(0).get(0).get(0).is_element()); utils.click_cursor((10., 10.)).await; utils.click_cursor((10., 50.)).await; utils.wait_for_update().await; - assert!(root.get(0).get(0).get(0).get(0).get(0).is_element()); - assert!(root.get(1).get(0).get(0).get(0).get(0).is_placeholder()); - assert!(root.get(2).get(0).get(0).get(0).get(0).is_element()); + assert!(root.get(0).get(0).get(0).get(0).is_element()); + assert!(root.get(1).get(0).get(0).get(0).is_placeholder()); + assert!(root.get(2).get(0).get(0).get(0).is_element()); } } diff --git a/crates/components/src/input.rs b/crates/components/src/input.rs index f29017ab0..8eae33031 100644 --- a/crates/components/src/input.rs +++ b/crates/components/src/input.rs @@ -21,6 +21,8 @@ use freya_hooks::{ }; use winit::window::CursorIcon; +use crate::ScrollView; + /// Enum to declare is [`Input`] hidden. #[derive(Default, Clone, PartialEq)] pub enum InputMode { @@ -137,7 +139,15 @@ pub fn Input( editable.process_event(&EditableEvent::KeyUp(e.data)); }; + let oninputmousedown = move |e: MouseEvent| { + if !display_placeholder { + editable.process_event(&EditableEvent::MouseDown(e.data, 0)); + } + focus.focus(); + }; + let onmousedown = move |e: MouseEvent| { + e.stop_propagation(); if !display_placeholder { editable.process_event(&EditableEvent::MouseDown(e.data, 0)); } @@ -217,27 +227,33 @@ pub fn Input( main_align: "center", cursor_reference, a11y_id, - a11y_role: "textInput", + a11y_role: "text-input", a11y_auto_focus: "{auto_focus}", onkeydown, onkeyup, overflow: "clip", - paragraph { - margin: "8 12", - onglobalclick, - onmouseenter, - onmouseleave, - onmousedown, - onmousemove, - width: "100%", - cursor_id: "0", - cursor_index: "{cursor_char}", - cursor_mode: "editable", - cursor_color: "{color}", - max_lines: "1", - highlights, - text { - "{text}" + onmousedown: oninputmousedown, + onmouseenter, + onmouseleave, + ScrollView { + height: "auto", + direction: "horizontal", + show_scrollbar: false, + paragraph { + min_width: "1", + margin: "8 12", + onglobalclick, + onmousedown, + onmousemove, + cursor_id: "0", + cursor_index: "{cursor_char}", + cursor_mode: "editable", + cursor_color: "{color}", + max_lines: "1", + highlights, + text { + "{text}" + } } } } @@ -264,7 +280,7 @@ mod test { let mut utils = launch_test(input_app); let root = utils.root(); - let text = root.get(0).get(0).get(0); + let text = root.get(0).get(0).get(0).get(0).get(0).get(0); utils.wait_for_update().await; // Default value diff --git a/crates/components/src/native_container.rs b/crates/components/src/native_container.rs index 82d2b6db8..dc06f6f54 100644 --- a/crates/components/src/native_container.rs +++ b/crates/components/src/native_container.rs @@ -2,7 +2,13 @@ use dioxus::prelude::*; use freya_core::prelude::EventMessage; use freya_elements::{ elements as dioxus_elements, - events::KeyboardEvent, + events::{ + keyboard::{ + Key, + Modifiers, + }, + KeyboardEvent, + }, }; use freya_hooks::{ use_init_native_platform, diff --git a/crates/components/src/network_image.rs b/crates/components/src/network_image.rs index 45d096d47..54d60b776 100644 --- a/crates/components/src/network_image.rs +++ b/crates/components/src/network_image.rs @@ -124,7 +124,7 @@ pub fn NetworkImage(props: NetworkImageProps) -> Element { a11y_id, image_data, a11y_role: "image", - a11y_alt: alt + a11y_name: alt }) } else if *status.read() == ImageState::Loading { if let Some(loading_element) = &props.loading { diff --git a/crates/components/src/popup.rs b/crates/components/src/popup.rs index 2399e8fea..eea08d929 100644 --- a/crates/components/src/popup.rs +++ b/crates/components/src/popup.rs @@ -1,7 +1,10 @@ use dioxus::prelude::*; use freya_elements::{ elements as dioxus_elements, - events::KeyboardEvent, + events::{ + keyboard::Key, + KeyboardEvent, + }, }; use freya_hooks::{ theme_with, use_animation, use_applied_theme, AnimNum, ButtonThemeWith, Ease, Function, PopupTheme, PopupThemeWith diff --git a/crates/components/src/progress_bar.rs b/crates/components/src/progress_bar.rs index df797f144..c26adc492 100644 --- a/crates/components/src/progress_bar.rs +++ b/crates/components/src/progress_bar.rs @@ -76,6 +76,7 @@ pub fn ProgressBar( width: "100%", color: "{color}", max_lines: "1", + text_height: "disable-least-ascent", "{progress.floor()}%" } } diff --git a/crates/components/src/radio.rs b/crates/components/src/radio.rs index 58af74e11..2d07728c0 100644 --- a/crates/components/src/radio.rs +++ b/crates/components/src/radio.rs @@ -69,34 +69,30 @@ pub fn Radio( unselected_fill }; let border = if focus.is_selected() { - format!("4 outer {}", border_fill) + format!("2 inner {fill}, 4 outer {border_fill}") } else { - "none".to_string() + format!("2 inner {fill}") }; let onkeydown = move |_: KeyboardEvent| {}; rsx!( rect { + a11y_id: focus.attribute(), + width: "18", + height: "18", border, + padding: "4", + main_align: "center", + cross_align: "center", corner_radius: "99", - rect { - a11y_id: focus.attribute(), - width: "18", - height: "18", - border: "2 inner {fill}", - padding: "4", - main_align: "center", - cross_align: "center", - corner_radius: "99", - onkeydown, - if selected { - rect { - width: "10", - height: "10", - background: "{fill}", - corner_radius: "99", - } + onkeydown, + if selected { + rect { + width: "10", + height: "10", + background: "{fill}", + corner_radius: "99", } } } @@ -157,26 +153,26 @@ mod test { utils.wait_for_update().await; // If the inner circle exists it means that the Radio is activated, otherwise it isn't - assert!(root.get(0).get(0).get(0).get(0).get(0).is_element()); - assert!(root.get(1).get(0).get(0).get(0).get(0).is_placeholder()); - assert!(root.get(2).get(0).get(0).get(0).get(0).is_placeholder()); + assert!(root.get(0).get(0).get(0).get(0).is_element()); + assert!(root.get(1).get(0).get(0).get(0).is_placeholder()); + assert!(root.get(2).get(0).get(0).get(0).is_placeholder()); utils.click_cursor((20., 50.)).await; - assert!(root.get(0).get(0).get(0).get(0).get(0).is_placeholder()); - assert!(root.get(1).get(0).get(0).get(0).get(0).is_element()); - assert!(root.get(2).get(0).get(0).get(0).get(0).is_placeholder()); + assert!(root.get(0).get(0).get(0).get(0).is_placeholder()); + assert!(root.get(1).get(0).get(0).get(0).is_element()); + assert!(root.get(2).get(0).get(0).get(0).is_placeholder()); utils.click_cursor((10., 90.)).await; - assert!(root.get(0).get(0).get(0).get(0).get(0).is_placeholder()); - assert!(root.get(1).get(0).get(0).get(0).get(0).is_placeholder()); - assert!(root.get(2).get(0).get(0).get(0).get(0).is_element()); + assert!(root.get(0).get(0).get(0).get(0).is_placeholder()); + assert!(root.get(1).get(0).get(0).get(0).is_placeholder()); + assert!(root.get(2).get(0).get(0).get(0).is_element()); utils.click_cursor((10., 10.)).await; - assert!(root.get(0).get(0).get(0).get(0).get(0).is_element()); - assert!(root.get(1).get(0).get(0).get(0).get(0).is_placeholder()); - assert!(root.get(2).get(0).get(0).get(0).get(0).is_placeholder()); + assert!(root.get(0).get(0).get(0).get(0).is_element()); + assert!(root.get(1).get(0).get(0).get(0).is_placeholder()); + assert!(root.get(2).get(0).get(0).get(0).is_placeholder()); } } diff --git a/crates/components/src/scroll_views/scroll_bar.rs b/crates/components/src/scroll_views/scroll_bar.rs index 80c0ba7da..e726ca9a2 100644 --- a/crates/components/src/scroll_views/scroll_bar.rs +++ b/crates/components/src/scroll_views/scroll_bar.rs @@ -56,7 +56,7 @@ pub fn ScrollBar( rsx!( rect { overflow: "clip", - a11y_role:"scrollBar", + a11y_role: "scroll-bar", width: "{width}", height: "{height}", offset_x: "{offset_x}", diff --git a/crates/components/src/scroll_views/scroll_view.rs b/crates/components/src/scroll_views/scroll_view.rs index d82d768d7..671c1a06e 100644 --- a/crates/components/src/scroll_views/scroll_view.rs +++ b/crates/components/src/scroll_views/scroll_view.rs @@ -159,10 +159,16 @@ pub fn ScrollView( let direction_is_vertical = direction == "vertical"; - let vertical_scrollbar_is_visible = - is_scrollbar_visible(show_scrollbar, size.inner.height, size.area.height()); - let horizontal_scrollbar_is_visible = - is_scrollbar_visible(show_scrollbar, size.inner.width, size.area.width()); + let vertical_scrollbar_is_visible = is_scrollbar_visible( + show_scrollbar, + size.inner.height.floor(), + size.area.height().floor(), + ); + let horizontal_scrollbar_is_visible = is_scrollbar_visible( + show_scrollbar, + size.inner.width.floor(), + size.area.width().floor(), + ); let (container_width, content_width) = get_container_size( &width, @@ -220,8 +226,6 @@ pub fn ScrollView( if *scrolled_y.peek() != scroll_position_y { e.stop_propagation(); *scrolled_y.write() = scroll_position_y; - } else { - return; } } else { let scroll_position_x = get_scroll_position_from_wheel( @@ -235,12 +239,8 @@ pub fn ScrollView( if *scrolled_x.peek() != scroll_position_x { e.stop_propagation(); *scrolled_x.write() = scroll_position_x; - } else { - return; } } - - focus.focus(); }; // Drag the scrollbars @@ -372,7 +372,7 @@ pub fn ScrollView( rsx!( rect { - a11y_role:"scrollView", + a11y_role:"scroll-view", overflow: "clip", direction: "horizontal", width, @@ -382,6 +382,7 @@ pub fn ScrollView( onglobalkeydown, onglobalkeyup, a11y_id, + a11y_focusable: "false", rect { direction: "vertical", width: "{container_width}", diff --git a/crates/components/src/scroll_views/use_scroll_controller.rs b/crates/components/src/scroll_views/use_scroll_controller.rs index c20b3b5b1..8e5736b93 100644 --- a/crates/components/src/scroll_views/use_scroll_controller.rs +++ b/crates/components/src/scroll_views/use_scroll_controller.rs @@ -75,6 +75,14 @@ impl ScrollController { } } + pub fn x(&self) -> Signal { + self.x + } + + pub fn y(&self) -> Signal { + self.y + } + pub fn use_apply(&mut self, width: f32, height: f32) { let scope_id = current_scope_id().unwrap(); diff --git a/crates/components/src/scroll_views/virtual_scroll_view.rs b/crates/components/src/scroll_views/virtual_scroll_view.rs index 74f9d51ff..2365ed697 100644 --- a/crates/components/src/scroll_views/virtual_scroll_view.rs +++ b/crates/components/src/scroll_views/virtual_scroll_view.rs @@ -447,7 +447,7 @@ pub fn VirtualScrollView< rsx!( rect { - a11y_role:"scrollView", + a11y_role: "scroll-view", overflow: "clip", direction: "horizontal", width: "{width}", diff --git a/crates/components/src/slider.rs b/crates/components/src/slider.rs index ce3a9d3b3..e656e66fe 100644 --- a/crates/components/src/slider.rs +++ b/crates/components/src/slider.rs @@ -2,6 +2,7 @@ use dioxus::prelude::*; use freya_elements::{ elements as dioxus_elements, events::{ + keyboard::Key, KeyboardEvent, MouseEvent, WheelEvent, diff --git a/crates/components/src/svg.rs b/crates/components/src/svg.rs index 7ff681521..70036bb60 100644 --- a/crates/components/src/svg.rs +++ b/crates/components/src/svg.rs @@ -28,11 +28,17 @@ macro_rules! import_svg { pub fn $component_name( #[props(default = $width.to_string())] width: String, #[props(default = $height.to_string())] height: String, + color: Option, + fill: Option, + stroke: Option, ) -> freya::prelude::Element { use freya::prelude::*; let svg_data = static_bytes(include_bytes!($path)); rsx!(svg { + color, + fill, + stroke, width, height, svg_data @@ -43,11 +49,20 @@ macro_rules! import_svg { // Generate a function with the name derived from the file name #[allow(non_snake_case)] #[dioxus::prelude::component] - pub fn $component_name(width: String, height: String) -> freya::prelude::Element { + pub fn $component_name( + width: String, + height: String, + color: Option, + fill: Option, + stroke: Option, + ) -> freya::prelude::Element { use freya::prelude::*; let svg_data = static_bytes(include_bytes!($path)); rsx!(svg { + color, + fill, + stroke, width, height, svg_data diff --git a/crates/components/src/switch.rs b/crates/components/src/switch.rs index 925a2998a..4d2dc344c 100644 --- a/crates/components/src/switch.rs +++ b/crates/components/src/switch.rs @@ -15,6 +15,7 @@ use freya_hooks::{ AnimNum, Ease, Function, + OnDepsChange, SwitchThemeWith, }; use winit::window::CursorIcon; @@ -67,6 +68,7 @@ pub enum SwitchStatus { pub fn Switch(props: SwitchProps) -> Element { let theme = use_applied_theme!(&props.theme, switch); let animation = use_animation_with_dependencies(&theme, |ctx, theme| { + ctx.on_deps_change(OnDepsChange::Run); ( ctx.with( AnimNum::new(2., 22.) diff --git a/crates/components/src/tabs.rs b/crates/components/src/tabs.rs index b056dcd62..bbcb7d8ef 100644 --- a/crates/components/src/tabs.rs +++ b/crates/components/src/tabs.rs @@ -146,7 +146,6 @@ pub fn Tab( a11y_role:"tab", color: "{font_theme.color}", background: "{background}", - text_align: "center", content: "fit", rect { padding: "{padding}", @@ -257,7 +256,6 @@ pub fn BottomTab(children: Element, theme: Option) -> Elemen a11y_role:"tab", color: "{font_theme.color}", background: "{background}", - text_align: "center", padding: "{padding}", main_align: "center", cross_align: "center", diff --git a/crates/components/src/tile.rs b/crates/components/src/tile.rs index 17cd1aa52..c4563ad0d 100644 --- a/crates/components/src/tile.rs +++ b/crates/components/src/tile.rs @@ -2,6 +2,7 @@ use dioxus::prelude::*; use freya_elements::{ elements as dioxus_elements, events::{ + keyboard::Key, KeyboardEvent, MouseEvent, }, @@ -42,8 +43,6 @@ pub fn Tile( onselect: Option>, /// Theme override. theme: Option, - - a11y_name: Option, ) -> Element { let mut status = use_signal(TileStatus::default); let platform = use_platform(); diff --git a/crates/core/src/accessibility/mod.rs b/crates/core/src/accessibility/mod.rs index 52f3cc600..e590e541e 100644 --- a/crates/core/src/accessibility/mod.rs +++ b/crates/core/src/accessibility/mod.rs @@ -4,6 +4,7 @@ use freya_native_core::{ real_dom::NodeImmutable, }; use freya_node_state::AccessibilityNodeState; +use itertools::Itertools; pub use tree::*; use crate::{ @@ -54,7 +55,9 @@ impl NodeAccessibility for DioxusNode<'_> { /// Collect all descendant accessibility node ids fn get_accessibility_children(&self) -> Vec { - let node_accessibility = &*self.get::().unwrap(); - node_accessibility.descencent_accessibility_ids.clone() + self.children() + .into_iter() + .filter_map(|child| child.get_accessibility_id()) + .collect_vec() } } diff --git a/crates/core/src/accessibility/tree.rs b/crates/core/src/accessibility/tree.rs index ef5b1bfd9..1c2f424f4 100644 --- a/crates/core/src/accessibility/tree.rs +++ b/crates/core/src/accessibility/tree.rs @@ -149,25 +149,28 @@ impl AccessibilityTree { ); } + // Remove all the removed nodes from the update list + for (node_id, _) in removed_ids.iter() { + added_or_updated_ids.remove(node_id); + self.map.retain(|_, id| id != node_id); + } + + // Mark the parent of the removed nodes as updated + for (_, parent_id) in removed_ids.iter() { + if !removed_ids.contains_key(parent_id) { + added_or_updated_ids.insert(*parent_id); + } + } + // Mark the ancestors as modified for node_id in added_or_updated_ids.clone() { let node_ref = rdom.get(node_id).unwrap(); - let node_accessibility_state = node_ref.get::().unwrap(); - added_or_updated_ids.insert( - node_accessibility_state - .closest_accessibility_node_id - .unwrap_or(rdom.root_id()), - ); + let node_ref_parent = node_ref.parent_id().unwrap_or(rdom.root_id()); + added_or_updated_ids.insert(node_ref_parent); self.map .insert(node_ref.get_accessibility_id().unwrap(), node_id); } - // Mark the still existing ancenstors as modified - for (node_id, ancestor_node_id) in removed_ids { - added_or_updated_ids.insert(ancestor_node_id); - self.map.retain(|_, id| *id != node_id); - } - // Create the updated nodes let mut nodes = Vec::new(); for node_id in added_or_updated_ids { @@ -180,7 +183,6 @@ impl AccessibilityTree { { let accessibility_node = Self::create_node(&node_ref, layout_node, node_accessibility_state); - let accessibility_id = node_ref.get_accessibility_id().unwrap(); nodes.push((accessibility_id, accessibility_node)); @@ -332,7 +334,18 @@ impl AccessibilityTree { let transform_state = &*node_ref.get::().unwrap(); let node_type = node_ref.node_type(); - let mut builder = NodeBuilder::new(Role::default()); + let mut builder = match node_type.tag() { + // Make the root accessibility node. + Some(&TagName::Root) => NodeBuilder::new(Role::Window), + + // All other node types will either don't have a builder (but don't support + // accessibility attributes like with `text`) or have their builder made for + // them already. + Some(_) => node_accessibility.builder.clone().unwrap(), + + // Tag-less nodes can't have accessibility state + None => unreachable!(), + }; // Set children let children = node_ref.get_accessibility_children(); @@ -347,6 +360,14 @@ impl AccessibilityTree { y1: area.max_y(), }); + if let NodeType::Element(node) = &*node_type { + if matches!(node.tag, TagName::Label | TagName::Paragraph) && builder.name().is_none() { + if let Some(inner_text) = node_ref.get_inner_texts() { + builder.set_name(inner_text); + } + } + } + // Set focusable action // This will cause assistive technology to offer the user an option // to focus the current element if it supports it. @@ -453,28 +474,6 @@ impl AccessibilityTree { )); } - // Set text value - if let Some(alt) = &node_accessibility.a11y_alt { - builder.set_value(alt.to_owned()); - } else if let Some(value) = node_ref.get_inner_texts() { - builder.set_value(value); - builder.set_role(Role::Label); - } - - // Set name - if let Some(name) = &node_accessibility.a11y_name { - builder.set_name(name.to_owned()); - } - - // Set role - if let Some(role) = node_accessibility.a11y_role { - builder.set_role(role); - } - // Set root role - if node_ref.id() == node_ref.real_dom().root_id() { - builder.set_role(Role::Window); - } - builder.build() } } diff --git a/crates/core/src/dom/doms.rs b/crates/core/src/dom/doms.rs index d93c5cf17..c1e597046 100644 --- a/crates/core/src/dom/doms.rs +++ b/crates/core/src/dom/doms.rs @@ -251,7 +251,7 @@ impl FreyaDOM { ctx.insert(self.accessibility_generator.clone()); // Update the Node's states - let (_, diff) = self.rdom.update_state(ctx); + let diff = self.rdom.update_state(ctx); let must_repaint = !diff.is_empty(); let must_relayout = !self.layout().get_dirty_nodes().is_empty(); diff --git a/crates/core/src/dom/mutations_writer.rs b/crates/core/src/dom/mutations_writer.rs index 8db949c0b..84ccf29ba 100644 --- a/crates/core/src/dom/mutations_writer.rs +++ b/crates/core/src/dom/mutations_writer.rs @@ -17,7 +17,6 @@ use freya_native_core::{ NodeId, }; use freya_node_state::{ - AccessibilityNodeState, CursorState, CustomAttributeValues, LayerState, @@ -91,12 +90,10 @@ impl<'a> MutationsWriter<'a> { // Remove from the accessibility tree if node.get_accessibility_id().is_some() { - let node_accessibility_state = node.get::().unwrap(); - let closed_accessibility_node_id = node_accessibility_state - .closest_accessibility_node_id + let parent_id = node + .parent_id() .unwrap_or(self.native_writer.rdom.root_id()); - self.accessibility_dirty_nodes - .remove(node.id(), closed_accessibility_node_id); + self.accessibility_dirty_nodes.remove(node.id(), parent_id); } // Unite the removed area with the dirty area diff --git a/crates/core/src/elements/label.rs b/crates/core/src/elements/label.rs index 0c66114d2..186cf107d 100644 --- a/crates/core/src/elements/label.rs +++ b/crates/core/src/elements/label.rs @@ -11,9 +11,9 @@ use torin::prelude::{ }; use super::utils::ElementUtils; -use crate::prelude::{ - align_main_align_paragraph, - DioxusNode, +use crate::{ + prelude::DioxusNode, + render::align_main_align_paragraph, }; pub struct LabelElement; diff --git a/crates/core/src/elements/paragraph.rs b/crates/core/src/elements/paragraph.rs index 2b595aa21..a637dad03 100644 --- a/crates/core/src/elements/paragraph.rs +++ b/crates/core/src/elements/paragraph.rs @@ -31,12 +31,14 @@ use torin::{ use super::utils::ElementUtils; use crate::{ dom::DioxusNode, - prelude::{ - align_highlights_and_cursor_paragraph, + prelude::TextGroupMeasurement, + render::{ align_main_align_paragraph, - TextGroupMeasurement, + create_paragraph, + draw_cursor, + draw_cursor_highlights, + ParagraphData, }, - render::create_paragraph, }; pub struct ParagraphElement; @@ -138,7 +140,7 @@ impl ElementUtils for ParagraphElement { }; if node_cursor_state.position.is_some() { - let paragraph = create_paragraph( + let ParagraphData { paragraph, .. } = create_paragraph( node_ref, &area.size, font_collection, @@ -176,7 +178,7 @@ impl ElementUtils for ParagraphElement { false } - fn drawing_area( + fn element_drawing_area( &self, layout_node: &LayoutNode, node_ref: &DioxusNode, @@ -230,80 +232,3 @@ impl ElementUtils for ParagraphElement { area } } - -fn draw_cursor_highlights( - area: &Area, - paragraph: &Paragraph, - canvas: &Canvas, - node_ref: &DioxusNode, -) -> Option<()> { - let node_cursor_state = &*node_ref.get::().unwrap(); - - let highlights = node_cursor_state.highlights.as_ref()?; - let highlight_color = node_cursor_state.highlight_color; - - for (from, to) in highlights.iter() { - let (from, to) = { - if from < to { - (from, to) - } else { - (to, from) - } - }; - let cursor_rects = paragraph.get_rects_for_range( - *from..*to, - RectHeightStyle::Tight, - RectWidthStyle::Tight, - ); - for cursor_rect in cursor_rects { - let rect = align_highlights_and_cursor_paragraph( - node_ref, - area, - paragraph, - &cursor_rect, - None, - ); - - let mut paint = Paint::default(); - paint.set_anti_alias(true); - paint.set_style(PaintStyle::Fill); - paint.set_color(highlight_color); - - canvas.draw_rect(rect, &paint); - } - } - - Some(()) -} - -fn draw_cursor( - area: &Area, - paragraph: &Paragraph, - canvas: &Canvas, - node_ref: &DioxusNode, -) -> Option<()> { - let node_cursor_state = &*node_ref.get::().unwrap(); - - let cursor = node_cursor_state.position?; - let cursor_color = node_cursor_state.color; - let cursor_position = cursor as usize; - - let cursor_rects = paragraph.get_rects_for_range( - cursor_position..cursor_position + 1, - RectHeightStyle::Tight, - RectWidthStyle::Tight, - ); - let cursor_rect = cursor_rects.first()?; - - let rect = - align_highlights_and_cursor_paragraph(node_ref, area, paragraph, cursor_rect, Some(1.0)); - - let mut paint = Paint::default(); - paint.set_anti_alias(true); - paint.set_style(PaintStyle::Fill); - paint.set_color(cursor_color); - - canvas.draw_rect(rect, &paint); - - Some(()) -} diff --git a/crates/core/src/elements/rect.rs b/crates/core/src/elements/rect.rs index 55bc235c0..b033ee415 100644 --- a/crates/core/src/elements/rect.rs +++ b/crates/core/src/elements/rect.rs @@ -1,10 +1,7 @@ use freya_engine::prelude::*; use freya_native_core::real_dom::NodeImmutable; use freya_node_state::{ - Border, - BorderAlignment, CanvasRunnerContext, - CornerRadius, Fill, ReferencesState, ShadowPosition, @@ -22,12 +19,15 @@ use torin::{ }; use super::utils::ElementUtils; -use crate::dom::DioxusNode; - -enum BorderShape { - DRRect(RRect, RRect), - Path(Path), -} +use crate::{ + dom::DioxusNode, + render::{ + border_shape, + render_border, + render_shadow, + BorderShape, + }, +}; pub struct RectElement; @@ -53,207 +53,6 @@ impl RectElement { ], ) } - - fn outer_border_path_corner_radius( - alignment: BorderAlignment, - corner_radius: f32, - width_1: f32, - width_2: f32, - ) -> f32 { - if alignment == BorderAlignment::Inner || corner_radius == 0.0 { - return corner_radius; - } - - let mut offset = if width_1 == 0.0 { - width_2 - } else if width_2 == 0.0 { - width_1 - } else { - width_1.min(width_2) - }; - - if alignment == BorderAlignment::Center { - offset *= 0.5; - } - - corner_radius + offset - } - - fn inner_border_path_corner_radius( - alignment: BorderAlignment, - corner_radius: f32, - width_1: f32, - width_2: f32, - ) -> f32 { - if alignment == BorderAlignment::Outer || corner_radius == 0.0 { - return corner_radius; - } - - let mut offset = if width_1 == 0.0 { - width_2 - } else if width_2 == 0.0 { - width_1 - } else { - width_1.min(width_2) - }; - - if alignment == BorderAlignment::Center { - offset *= 0.5; - } - - corner_radius - offset - } - - /// Returns a `Path` that will draw a [`Border`] around a base rectangle. - /// - /// We don't use Skia's stroking API here, since we might need different widths for each side. - fn border_shape( - base_rect: Rect, - base_corner_radius: CornerRadius, - border: &Border, - ) -> BorderShape { - let border_alignment = border.alignment; - let border_width = border.width; - - // First we create a path that is outset from the rect by a certain amount on each side. - // - // Let's call this the outer border path. - let (outer_rrect, outer_corner_radius) = { - // Calculuate the outer corner radius for the border. - let corner_radius = CornerRadius { - top_left: Self::outer_border_path_corner_radius( - border_alignment, - base_corner_radius.top_left, - border_width.top, - border_width.left, - ), - top_right: Self::outer_border_path_corner_radius( - border_alignment, - base_corner_radius.top_right, - border_width.top, - border_width.right, - ), - bottom_left: Self::outer_border_path_corner_radius( - border_alignment, - base_corner_radius.bottom_left, - border_width.bottom, - border_width.left, - ), - bottom_right: Self::outer_border_path_corner_radius( - border_alignment, - base_corner_radius.bottom_right, - border_width.bottom, - border_width.right, - ), - smoothing: base_corner_radius.smoothing, - }; - - let rrect = RRect::new_rect_radii( - { - let mut rect = base_rect; - let alignment_scale = match border_alignment { - BorderAlignment::Outer => 1.0, - BorderAlignment::Center => 0.5, - BorderAlignment::Inner => 0.0, - }; - - rect.left -= border_width.left * alignment_scale; - rect.top -= border_width.top * alignment_scale; - rect.right += border_width.right * alignment_scale; - rect.bottom += border_width.bottom * alignment_scale; - - rect - }, - &[ - (corner_radius.top_left, corner_radius.top_left).into(), - (corner_radius.top_right, corner_radius.top_right).into(), - (corner_radius.bottom_right, corner_radius.bottom_right).into(), - (corner_radius.bottom_left, corner_radius.bottom_left).into(), - ], - ); - - (rrect, corner_radius) - }; - - // After the outer path, we will then move to the inner bounds of the border. - let (inner_rrect, inner_corner_radius) = { - // Calculuate the inner corner radius for the border. - let corner_radius = CornerRadius { - top_left: Self::inner_border_path_corner_radius( - border_alignment, - base_corner_radius.top_left, - border_width.top, - border_width.left, - ), - top_right: Self::inner_border_path_corner_radius( - border_alignment, - base_corner_radius.top_right, - border_width.top, - border_width.right, - ), - bottom_left: Self::inner_border_path_corner_radius( - border_alignment, - base_corner_radius.bottom_left, - border_width.bottom, - border_width.left, - ), - bottom_right: Self::inner_border_path_corner_radius( - border_alignment, - base_corner_radius.bottom_right, - border_width.bottom, - border_width.right, - ), - smoothing: base_corner_radius.smoothing, - }; - - let rrect = RRect::new_rect_radii( - { - let mut rect = base_rect; - let alignment_scale = match border_alignment { - BorderAlignment::Outer => 0.0, - BorderAlignment::Center => 0.5, - BorderAlignment::Inner => 1.0, - }; - - rect.left += border_width.left * alignment_scale; - rect.top += border_width.top * alignment_scale; - rect.right -= border_width.right * alignment_scale; - rect.bottom -= border_width.bottom * alignment_scale; - - rect - }, - &[ - (corner_radius.top_left, corner_radius.top_left).into(), - (corner_radius.top_right, corner_radius.top_right).into(), - (corner_radius.bottom_right, corner_radius.bottom_right).into(), - (corner_radius.bottom_left, corner_radius.bottom_left).into(), - ], - ); - - (rrect, corner_radius) - }; - - if base_corner_radius.smoothing > 0.0 { - let mut path = Path::new(); - path.set_fill_type(PathFillType::EvenOdd); - - path.add_path( - &outer_corner_radius.smoothed_path(outer_rrect), - Point::new(outer_rrect.rect().x(), outer_rrect.rect().y()), - None, - ); - - path.add_path( - &inner_corner_radius.smoothed_path(inner_rrect), - Point::new(inner_rrect.rect().x(), inner_rrect.rect().y()), - None, - ); - - BorderShape::Path(path) - } else { - BorderShape::DRRect(outer_rrect, inner_rrect) - } - } } impl ElementUtils for RectElement { @@ -299,24 +98,12 @@ impl ElementUtils for RectElement { paint.set_anti_alias(true); paint.set_style(PaintStyle::Fill); - match &node_style.background { - Fill::Color(color) => { - paint.set_color(*color); - } - Fill::LinearGradient(gradient) => { - paint.set_shader(gradient.into_shader(area)); - } - Fill::RadialGradient(gradient) => { - paint.set_shader(gradient.into_shader(area)); - } - Fill::ConicGradient(gradient) => { - paint.set_shader(gradient.into_shader(area)); - } - } + node_style.background.apply_to_paint(&mut paint, area); let mut corner_radius = node_style.corner_radius; corner_radius.scale(scale_factor); + // Container let rounded_rect = RRect::new_rect_radii( Rect::new(area.min_x(), area.min_y(), area.max_x(), area.max_y()), &[ @@ -326,7 +113,6 @@ impl ElementUtils for RectElement { (corner_radius.bottom_left, corner_radius.bottom_left).into(), ], ); - if corner_radius.smoothing > 0.0 { path.add_path( &corner_radius.smoothed_path(rounded_rect), @@ -336,7 +122,6 @@ impl ElementUtils for RectElement { } else { path.add_rrect(rounded_rect, None); } - canvas.draw_path(&path, &paint); // Shadows @@ -344,77 +129,15 @@ impl ElementUtils for RectElement { if shadow.fill != Fill::Color(Color::TRANSPARENT) { shadow.scale(scale_factor); - let mut shadow_path = Path::new(); - let mut shadow_paint = Paint::default(); - shadow_paint.set_anti_alias(true); - - match &shadow.fill { - Fill::Color(color) => { - shadow_paint.set_color(*color); - } - Fill::LinearGradient(gradient) => { - shadow_paint.set_shader(gradient.into_shader(area)); - } - Fill::RadialGradient(gradient) => { - shadow_paint.set_shader(gradient.into_shader(area)); - } - Fill::ConicGradient(gradient) => { - shadow_paint.set_shader(gradient.into_shader(area)); - } - } - - // Shadows can be either outset or inset - // If they are outset, we fill a copy of the path outset by spread_radius, and blur it. - // Otherwise, we draw a stroke with the inner portion being spread_radius width, and the outer portion being blur_radius width. - let outset: Point = match shadow.position { - ShadowPosition::Normal => { - shadow_paint.set_style(PaintStyle::Fill); - (shadow.spread, shadow.spread).into() - } - ShadowPosition::Inset => { - shadow_paint.set_style(PaintStyle::Stroke); - shadow_paint.set_stroke_width(shadow.blur / 2.0 + shadow.spread); - (-shadow.spread / 2.0, -shadow.spread / 2.0).into() - } - }; - - // Apply gassuan blur to the copied path. - if shadow.blur > 0.0 { - shadow_paint.set_mask_filter(MaskFilter::blur( - BlurStyle::Normal, - shadow.blur / 2.0, - false, - )); - } - - // Add either the RRect or smoothed path based on whether smoothing is used. - if corner_radius.smoothing > 0.0 { - shadow_path.add_path( - &node_style - .corner_radius - .smoothed_path(rounded_rect.with_outset(outset)), - Point::new(area.min_x(), area.min_y()) - outset, - None, - ); - } else { - shadow_path.add_rrect(rounded_rect.with_outset(outset), None); - } - - // Offset our path by the shadow's x and y coordinates. - shadow_path.offset((shadow.x, shadow.y)); - - // Exclude the original path bounds from the shadow using a clip, then draw the shadow. - canvas.save(); - canvas.clip_path( - &path, - match shadow.position { - ShadowPosition::Normal => ClipOp::Difference, - ShadowPosition::Inset => ClipOp::Intersect, - }, - true, + render_shadow( + canvas, + node_style, + &mut path, + rounded_rect, + area, + shadow, + corner_radius, ); - canvas.draw_path(&shadow_path, &shadow_paint); - canvas.restore(); } } @@ -423,39 +146,12 @@ impl ElementUtils for RectElement { if border.is_visible() { border.scale(scale_factor); - // Create a new paint - let mut border_paint = Paint::default(); - border_paint.set_style(PaintStyle::Fill); - border_paint.set_anti_alias(true); - - match &border.fill { - Fill::Color(color) => { - border_paint.set_color(*color); - } - Fill::LinearGradient(gradient) => { - border_paint.set_shader(gradient.into_shader(area)); - } - Fill::RadialGradient(gradient) => { - border_paint.set_shader(gradient.into_shader(area)); - } - Fill::ConicGradient(gradient) => { - border_paint.set_shader(gradient.into_shader(area)); - } - } - - match Self::border_shape(*rounded_rect.rect(), corner_radius, &border) { - BorderShape::DRRect(outer, inner) => { - canvas.draw_drrect(outer, inner, &border_paint); - } - BorderShape::Path(path) => { - canvas.draw_path(&path, &border_paint); - } - } + render_border(canvas, rounded_rect, area, &border, corner_radius); } } + // Layout references let references = node_ref.get::().unwrap(); - if let Some(canvas_ref) = &references.canvas_ref { let mut ctx = CanvasRunnerContext { canvas, @@ -561,7 +257,7 @@ impl ElementUtils for RectElement { border.scale(scale_factor); let border_shape = - Self::border_shape(*rounded_rect.rect(), node_style.corner_radius, &border); + border_shape(*rounded_rect.rect(), node_style.corner_radius, &border); let border_bounds = match border_shape { BorderShape::DRRect(ref outer, _) => outer.bounds(), BorderShape::Path(ref path) => path.bounds(), diff --git a/crates/core/src/elements/svg.rs b/crates/core/src/elements/svg.rs index e0685d816..8d6b1c5d7 100644 --- a/crates/core/src/elements/svg.rs +++ b/crates/core/src/elements/svg.rs @@ -1,6 +1,9 @@ use freya_engine::prelude::*; use freya_native_core::real_dom::NodeImmutable; -use freya_node_state::StyleState; +use freya_node_state::{ + FontStyleState, + StyleState, +}; use torin::prelude::LayoutNode; use super::utils::ElementUtils; @@ -21,15 +24,27 @@ impl ElementUtils for SvgElement { ) { let area = layout_node.visible_area(); let node_style = &*node_ref.get::().unwrap(); + let font_style = &*node_ref.get::().unwrap(); let x = area.min_x(); let y = area.min_y(); if let Some(svg_data) = &node_style.svg_data { - let svg_dom = svg::Dom::from_bytes(svg_data.as_slice(), font_manager); + let resource_provider = LocalResourceProvider::new(font_manager); + let svg_dom = svg::Dom::from_bytes(svg_data.as_slice(), resource_provider); if let Ok(mut svg_dom) = svg_dom { canvas.save(); canvas.translate((x, y)); svg_dom.set_container_size((area.width() as i32, area.height() as i32)); + let mut root = svg_dom.root(); + root.set_width(svg::Length::new(100.0, svg::LengthUnit::Percentage)); + root.set_height(svg::Length::new(100.0, svg::LengthUnit::Percentage)); + root.set_color(font_style.color); + if let Some(color) = node_style.svg_stroke.as_ref() { + root.set_fill(svg::Paint::from_color(*color)); + } + if let Some(color) = node_style.svg_fill.as_ref() { + root.set_stroke(svg::Paint::from_color(*color)); + } svg_dom.render(canvas); canvas.restore(); } diff --git a/crates/core/src/elements/utils.rs b/crates/core/src/elements/utils.rs index 9d3d8e446..4575e653b 100644 --- a/crates/core/src/elements/utils.rs +++ b/crates/core/src/elements/utils.rs @@ -85,7 +85,8 @@ pub trait ElementUtils { } } - Some(drawing_area) + // Inflate the area by 1px in each side to cover potential off-bounds rendering caused by antialising + Some(drawing_area.inflate(1.0, 1.0)) } /// Measure the area for this element considering other diff --git a/crates/core/src/layout.rs b/crates/core/src/layout.rs index 63f5f6a18..b81ff25b9 100644 --- a/crates/core/src/layout.rs +++ b/crates/core/src/layout.rs @@ -35,22 +35,23 @@ pub fn process_layout( let mut compositor_dirty_area = fdom.compositor_dirty_area(); let mut buffer = layout.dirty.iter().copied().collect_vec(); while let Some(node_id) = buffer.pop() { - if let Some(area) = Compositor::get_drawing_area(node_id, &layout, rdom, scale_factor) { - // Unite the invalidated area with the dirty area - compositor_dirty_area.unite_or_insert(&area); + if let Some(node) = rdom.get(node_id) { + if let Some(area) = + Compositor::get_drawing_area(node_id, &layout, rdom, scale_factor) + { + // Unite the invalidated area with the dirty area + compositor_dirty_area.unite_or_insert(&area); - // Mark these elements as dirty for the compositor - compositor_dirty_nodes.insert(node_id); + // Mark these elements as dirty for the compositor + compositor_dirty_nodes.insert(node_id); - // Continue iterating in the children of this node - if let Some(node) = rdom.get(node_id) { // Mark as invalidated this node as its layout has changed if node.get_accessibility_id().is_some() { dirty_accessibility_tree.add_or_update(node_id); } - - buffer.extend(node.child_ids()); } + // Continue iterating in the children of this node + buffer.extend(node.child_ids()); } } let root_id = fdom.rdom().root_id(); diff --git a/crates/core/src/node.rs b/crates/core/src/node.rs index ef616509e..c01a2822d 100644 --- a/crates/core/src/node.rs +++ b/crates/core/src/node.rs @@ -137,6 +137,14 @@ impl NodeState { ("offset_x", AttributeType::Measure(self.size.offset_x.get())), ("offset_y", AttributeType::Measure(self.size.offset_y.get())), ("content", AttributeType::Content(&self.size.content)), + ( + "fill", + AttributeType::OptionalColor(self.style.svg_fill.map(|color| color.into())), + ), + ( + "svg_stroke", + AttributeType::OptionalColor(self.style.svg_stroke.map(|color| color.into())), + ), ]; let shadows = &self.style.shadows; @@ -161,6 +169,7 @@ impl NodeState { pub enum AttributeType<'a> { Color(Fill), + OptionalColor(Option), Gradient(Fill), Size(&'a Size), Measure(f32), diff --git a/crates/core/src/render/compositor.rs b/crates/core/src/render/compositor.rs index e7a1e503c..734cc725f 100644 --- a/crates/core/src/render/compositor.rs +++ b/crates/core/src/render/compositor.rs @@ -257,7 +257,7 @@ mod test { use itertools::sorted; fn run_compositor( - utils: &TestingHandler, + utils: &TestingHandler<()>, compositor: &mut Compositor, ) -> (Layers, Layers, usize) { let sdom = utils.sdom(); @@ -273,10 +273,10 @@ mod test { // Process what nodes need to be rendered let rendering_layers = compositor.run( - &mut *compositor_dirty_nodes, - &mut *compositor_dirty_area, + &mut compositor_dirty_nodes, + &mut compositor_dirty_area, &mut compositor_cache, - &*layers, + &layers, &mut dirty_layers, &layout, rdom, @@ -367,6 +367,7 @@ mod test { height: "100", width: "200", background: "red", + margin: "0 0 2 0", onclick: move |_| height += 10, } rect { @@ -374,6 +375,7 @@ mod test { width: "200", background: "green", shadow: "0 {shadow} 8 0 rgb(0, 0, 0, 0.5)", + margin: "0 0 2 0", onclick: move |_| height -= 10, } rect { @@ -418,7 +420,7 @@ mod test { let (_, _, painted_nodes) = run_compositor(&utils, &mut compositor); - // Root + First + Second rect + Third rect + // Root + First rect + Second rect + Third Rect assert_eq!(painted_nodes, 4); } @@ -440,6 +442,7 @@ mod test { height: "200", width: "200", direction: "horizontal", + spacing: "2", rect { onclick: move |_| msg_state.toggle(), height: "200", diff --git a/crates/core/src/render/mod.rs b/crates/core/src/render/mod.rs index 81a0379a0..35d6d787c 100644 --- a/crates/core/src/render/mod.rs +++ b/crates/core/src/render/mod.rs @@ -1,8 +1,10 @@ pub mod compositor; pub mod pipeline; pub mod skia_measurer; +pub mod utils; mod wireframe_renderer; pub use compositor::*; pub use pipeline::*; pub use skia_measurer::*; +pub use utils::*; diff --git a/crates/core/src/render/skia_measurer.rs b/crates/core/src/render/skia_measurer.rs index a3278bc04..55eec4ce1 100644 --- a/crates/core/src/render/skia_measurer.rs +++ b/crates/core/src/render/skia_measurer.rs @@ -15,22 +15,22 @@ use freya_native_core::{ tags::TagName, NodeId, }; -use freya_node_state::{ - CursorState, - FontStyleState, - HighlightMode, - LayoutState, - TextOverflow, -}; +use freya_node_state::LayoutState; use torin::prelude::{ - Alignment, Area, LayoutMeasurer, Node, Size2D, }; -use crate::dom::*; +use super::{ + create_label, + create_paragraph, +}; +use crate::{ + dom::*, + render::ParagraphData, +}; /// Provides Text measurements using Skia APIs like SkParagraph pub struct SkiaMeasurer<'a> { @@ -68,21 +68,19 @@ impl<'a> LayoutMeasurer for SkiaMeasurer<'a> { match &*node_type { NodeType::Element(ElementNode { tag, .. }) if tag == &TagName::Label => { - let label = create_label( + let ParagraphData { paragraph, size } = create_label( &node, area_size, self.font_collection, self.default_fonts, self.scale_factor, ); - let height = label.height(); - let res = Size2D::new(label.longest_line(), height); let mut map = SendAnyMap::new(); - map.insert(CachedParagraph(label, height)); - Some((res, Arc::new(map))) + map.insert(CachedParagraph(paragraph, size.height)); + Some((size, Arc::new(map))) } NodeType::Element(ElementNode { tag, .. }) if tag == &TagName::Paragraph => { - let paragraph = create_paragraph( + let ParagraphData { paragraph, size } = create_paragraph( &node, area_size, self.font_collection, @@ -90,11 +88,9 @@ impl<'a> LayoutMeasurer for SkiaMeasurer<'a> { self.default_fonts, self.scale_factor, ); - let height = paragraph.height(); - let res = Size2D::new(paragraph.longest_line(), height); let mut map = SendAnyMap::new(); - map.insert(CachedParagraph(paragraph, height)); - Some((res, Arc::new(map))) + map.insert(CachedParagraph(paragraph, size.height)); + Some((size, Arc::new(map))) } _ => None, } @@ -124,151 +120,3 @@ impl<'a> LayoutMeasurer for SkiaMeasurer<'a> { } } } - -pub fn create_label( - node: &DioxusNode, - area_size: &Size2D, - font_collection: &FontCollection, - default_font_family: &[String], - scale_factor: f32, -) -> Paragraph { - let font_style = &*node.get::().unwrap(); - - let mut paragraph_style = ParagraphStyle::default(); - paragraph_style.set_text_align(font_style.text_align); - paragraph_style.set_max_lines(font_style.max_lines); - paragraph_style.set_replace_tab_characters(true); - - if let Some(ellipsis) = font_style.text_overflow.get_ellipsis() { - paragraph_style.set_ellipsis(ellipsis); - } - - let text_style = font_style.text_style(default_font_family, scale_factor); - paragraph_style.set_text_style(&text_style); - - let mut paragraph_builder = ParagraphBuilder::new(¶graph_style, font_collection); - - for child in node.children() { - if let NodeType::Text(text) = &*child.node_type() { - paragraph_builder.add_text(text); - } - } - - let mut paragraph = paragraph_builder.build(); - paragraph.layout( - if font_style.max_lines == Some(1) && font_style.text_align == TextAlign::Start { - f32::MAX - } else { - area_size.width + 1.0 - }, - ); - - paragraph -} - -/// Align the Y axis of the highlights and cursor of a paragraph -pub fn align_highlights_and_cursor_paragraph( - node: &DioxusNode, - area: &Area, - paragraph: &Paragraph, - cursor_rect: &TextBox, - width: Option, -) -> Rect { - let cursor_state = node.get::().unwrap(); - - let left = area.min_x() + cursor_rect.rect.left; - let right = left + width.unwrap_or(cursor_rect.rect.right - cursor_rect.rect.left); - - match cursor_state.highlight_mode { - HighlightMode::Fit => { - let top = (area.min_y() - + align_main_align_paragraph(node, area, paragraph) - + cursor_rect.rect.top) - .clamp(area.min_y(), area.max_y()); - let bottom = (top + (cursor_rect.rect.bottom - cursor_rect.rect.top)) - .clamp(area.min_y(), area.max_y()); - - Rect::new(left, top, right, bottom) - } - HighlightMode::Expanded => { - let top = area.min_y(); - let bottom = area.max_y(); - - Rect::new(left, top, right, bottom) - } - } -} - -/// Align the main alignment of a paragraph -pub fn align_main_align_paragraph(node: &DioxusNode, area: &Area, paragraph: &Paragraph) -> f32 { - let layout = node.get::().unwrap(); - - match layout.main_alignment { - Alignment::Start => 0., - Alignment::Center => (area.height() / 2.0) - (paragraph.height() / 2.0), - Alignment::End => area.height() - paragraph.height(), - Alignment::SpaceBetween => 0., - Alignment::SpaceEvenly => 0., - Alignment::SpaceAround => 0., - } -} - -/// Compose a new SkParagraph -pub fn create_paragraph( - node: &DioxusNode, - area_size: &Size2D, - font_collection: &FontCollection, - is_rendering: bool, - default_font_family: &[String], - scale_factor: f32, -) -> Paragraph { - let font_style = &*node.get::().unwrap(); - - let mut paragraph_style = ParagraphStyle::default(); - paragraph_style.set_text_align(font_style.text_align); - paragraph_style.set_max_lines(font_style.max_lines); - paragraph_style.set_replace_tab_characters(true); - - if font_style.text_overflow == TextOverflow::Ellipsis { - paragraph_style.set_ellipsis("…"); - } - - let mut paragraph_builder = ParagraphBuilder::new(¶graph_style, font_collection); - - let text_style = font_style.text_style(default_font_family, scale_factor); - paragraph_builder.push_style(&text_style); - - for text_span in node.children() { - if let NodeType::Element(ElementNode { - tag: TagName::Text, .. - }) = &*text_span.node_type() - { - let text_nodes = text_span.children(); - let text_node = *text_nodes.first().unwrap(); - let text_node_type = &*text_node.node_type(); - let font_style = text_span.get::().unwrap(); - let text_style = font_style.text_style(default_font_family, scale_factor); - paragraph_builder.push_style(&text_style); - - if let NodeType::Text(text) = text_node_type { - paragraph_builder.add_text(text); - } - } - } - - if is_rendering { - // This is very tricky, but it works! It allows freya to render the cursor at the end of a line. - paragraph_builder.add_text(" "); - } - - let mut paragraph = paragraph_builder.build(); - paragraph.layout( - if font_style.max_lines == Some(1) && font_style.text_align == TextAlign::Start { - f32::MAX - } else { - area_size.width + 1.0 - }, - ); - - paragraph -} diff --git a/crates/core/src/render/utils/borders.rs b/crates/core/src/render/utils/borders.rs new file mode 100644 index 000000000..3ced2b54c --- /dev/null +++ b/crates/core/src/render/utils/borders.rs @@ -0,0 +1,237 @@ +use freya_engine::prelude::*; +use freya_node_state::{ + Border, + BorderAlignment, + CornerRadius, +}; +use torin::prelude::Area; + +pub enum BorderShape { + DRRect(RRect, RRect), + Path(Path), +} + +pub fn render_border( + canvas: &Canvas, + rounded_rect: RRect, + area: Area, + border: &Border, + corner_radius: CornerRadius, +) { + // Create a new paint + let mut border_paint = Paint::default(); + border_paint.set_style(PaintStyle::Fill); + border_paint.set_anti_alias(true); + + border.fill.apply_to_paint(&mut border_paint, area); + + match border_shape(*rounded_rect.rect(), corner_radius, border) { + BorderShape::DRRect(outer, inner) => { + canvas.draw_drrect(outer, inner, &border_paint); + } + BorderShape::Path(path) => { + canvas.draw_path(&path, &border_paint); + } + } +} + +/// Returns a `Path` that will draw a [`Border`] around a base rectangle. +/// +/// We don't use Skia's stroking API here, since we might need different widths for each side. +pub fn border_shape( + base_rect: Rect, + base_corner_radius: CornerRadius, + border: &Border, +) -> BorderShape { + let border_alignment = border.alignment; + let border_width = border.width; + + // First we create a path that is outset from the rect by a certain amount on each side. + // + // Let's call this the outer border path. + let (outer_rrect, outer_corner_radius) = { + // Calculuate the outer corner radius for the border. + let corner_radius = CornerRadius { + top_left: outer_border_path_corner_radius( + border_alignment, + base_corner_radius.top_left, + border_width.top, + border_width.left, + ), + top_right: outer_border_path_corner_radius( + border_alignment, + base_corner_radius.top_right, + border_width.top, + border_width.right, + ), + bottom_left: outer_border_path_corner_radius( + border_alignment, + base_corner_radius.bottom_left, + border_width.bottom, + border_width.left, + ), + bottom_right: outer_border_path_corner_radius( + border_alignment, + base_corner_radius.bottom_right, + border_width.bottom, + border_width.right, + ), + smoothing: base_corner_radius.smoothing, + }; + + let rrect = RRect::new_rect_radii( + { + let mut rect = base_rect; + let alignment_scale = match border_alignment { + BorderAlignment::Outer => 1.0, + BorderAlignment::Center => 0.5, + BorderAlignment::Inner => 0.0, + }; + + rect.left -= border_width.left * alignment_scale; + rect.top -= border_width.top * alignment_scale; + rect.right += border_width.right * alignment_scale; + rect.bottom += border_width.bottom * alignment_scale; + + rect + }, + &[ + (corner_radius.top_left, corner_radius.top_left).into(), + (corner_radius.top_right, corner_radius.top_right).into(), + (corner_radius.bottom_right, corner_radius.bottom_right).into(), + (corner_radius.bottom_left, corner_radius.bottom_left).into(), + ], + ); + + (rrect, corner_radius) + }; + + // After the outer path, we will then move to the inner bounds of the border. + let (inner_rrect, inner_corner_radius) = { + // Calculuate the inner corner radius for the border. + let corner_radius = CornerRadius { + top_left: inner_border_path_corner_radius( + border_alignment, + base_corner_radius.top_left, + border_width.top, + border_width.left, + ), + top_right: inner_border_path_corner_radius( + border_alignment, + base_corner_radius.top_right, + border_width.top, + border_width.right, + ), + bottom_left: inner_border_path_corner_radius( + border_alignment, + base_corner_radius.bottom_left, + border_width.bottom, + border_width.left, + ), + bottom_right: inner_border_path_corner_radius( + border_alignment, + base_corner_radius.bottom_right, + border_width.bottom, + border_width.right, + ), + smoothing: base_corner_radius.smoothing, + }; + + let rrect = RRect::new_rect_radii( + { + let mut rect = base_rect; + let alignment_scale = match border_alignment { + BorderAlignment::Outer => 0.0, + BorderAlignment::Center => 0.5, + BorderAlignment::Inner => 1.0, + }; + + rect.left += border_width.left * alignment_scale; + rect.top += border_width.top * alignment_scale; + rect.right -= border_width.right * alignment_scale; + rect.bottom -= border_width.bottom * alignment_scale; + + rect + }, + &[ + (corner_radius.top_left, corner_radius.top_left).into(), + (corner_radius.top_right, corner_radius.top_right).into(), + (corner_radius.bottom_right, corner_radius.bottom_right).into(), + (corner_radius.bottom_left, corner_radius.bottom_left).into(), + ], + ); + + (rrect, corner_radius) + }; + + if base_corner_radius.smoothing > 0.0 { + let mut path = Path::new(); + path.set_fill_type(PathFillType::EvenOdd); + + path.add_path( + &outer_corner_radius.smoothed_path(outer_rrect), + Point::new(outer_rrect.rect().x(), outer_rrect.rect().y()), + None, + ); + + path.add_path( + &inner_corner_radius.smoothed_path(inner_rrect), + Point::new(inner_rrect.rect().x(), inner_rrect.rect().y()), + None, + ); + + BorderShape::Path(path) + } else { + BorderShape::DRRect(outer_rrect, inner_rrect) + } +} + +fn outer_border_path_corner_radius( + alignment: BorderAlignment, + corner_radius: f32, + width_1: f32, + width_2: f32, +) -> f32 { + if alignment == BorderAlignment::Inner || corner_radius == 0.0 { + return corner_radius; + } + + let mut offset = if width_1 == 0.0 { + width_2 + } else if width_2 == 0.0 { + width_1 + } else { + width_1.min(width_2) + }; + + if alignment == BorderAlignment::Center { + offset *= 0.5; + } + + corner_radius + offset +} + +fn inner_border_path_corner_radius( + alignment: BorderAlignment, + corner_radius: f32, + width_1: f32, + width_2: f32, +) -> f32 { + if alignment == BorderAlignment::Outer || corner_radius == 0.0 { + return corner_radius; + } + + let mut offset = if width_1 == 0.0 { + width_2 + } else if width_2 == 0.0 { + width_1 + } else { + width_1.min(width_2) + }; + + if alignment == BorderAlignment::Center { + offset *= 0.5; + } + + corner_radius - offset +} diff --git a/crates/core/src/render/utils/label.rs b/crates/core/src/render/utils/label.rs new file mode 100644 index 000000000..bc20297a1 --- /dev/null +++ b/crates/core/src/render/utils/label.rs @@ -0,0 +1,61 @@ +use freya_engine::prelude::*; +use freya_native_core::{ + prelude::NodeType, + real_dom::NodeImmutable, +}; +use freya_node_state::FontStyleState; +use torin::prelude::Size2D; + +use super::ParagraphData; +use crate::dom::*; + +pub fn create_label( + node: &DioxusNode, + area_size: &Size2D, + font_collection: &FontCollection, + default_font_family: &[String], + scale_factor: f32, +) -> ParagraphData { + let font_style = &*node.get::().unwrap(); + + let mut paragraph_style = ParagraphStyle::default(); + paragraph_style.set_text_align(font_style.text_align); + paragraph_style.set_max_lines(font_style.max_lines); + paragraph_style.set_replace_tab_characters(true); + paragraph_style.set_text_height_behavior(font_style.text_height); + + if let Some(ellipsis) = font_style.text_overflow.get_ellipsis() { + paragraph_style.set_ellipsis(ellipsis); + } + + let text_style = + font_style.text_style(default_font_family, scale_factor, font_style.text_height); + paragraph_style.set_text_style(&text_style); + + let mut paragraph_builder = ParagraphBuilder::new(¶graph_style, font_collection); + + for child in node.children() { + if let NodeType::Text(text) = &*child.node_type() { + paragraph_builder.add_text(text); + } + } + + let mut paragraph = paragraph_builder.build(); + paragraph.layout( + if font_style.max_lines == Some(1) && font_style.text_align == TextAlign::default() { + f32::MAX + } else { + area_size.width + 1.0 + }, + ); + + let width = match font_style.text_align { + TextAlign::Start | TextAlign::Left => paragraph.longest_line(), + _ => paragraph.max_width(), + }; + + ParagraphData { + size: Size2D::new(width, paragraph.height()), + paragraph, + } +} diff --git a/crates/core/src/render/utils/mod.rs b/crates/core/src/render/utils/mod.rs new file mode 100644 index 000000000..5a3a0fbf8 --- /dev/null +++ b/crates/core/src/render/utils/mod.rs @@ -0,0 +1,9 @@ +mod borders; +mod label; +mod paragraph; +mod shadows; + +pub use borders::*; +pub use label::*; +pub use paragraph::*; +pub use shadows::*; diff --git a/crates/core/src/render/utils/paragraph.rs b/crates/core/src/render/utils/paragraph.rs new file mode 100644 index 000000000..64c9c966a --- /dev/null +++ b/crates/core/src/render/utils/paragraph.rs @@ -0,0 +1,223 @@ +use freya_engine::prelude::*; +use freya_native_core::{ + node::ElementNode, + prelude::NodeType, + real_dom::NodeImmutable, + tags::TagName, +}; +use freya_node_state::{ + CursorState, + FontStyleState, + HighlightMode, + LayoutState, +}; +use torin::prelude::{ + Alignment, + Area, + Size2D, +}; + +use crate::dom::DioxusNode; + +pub struct ParagraphData { + pub paragraph: Paragraph, + pub size: Size2D, +} + +/// Compose a new SkParagraph +pub fn create_paragraph( + node: &DioxusNode, + area_size: &Size2D, + font_collection: &FontCollection, + is_rendering: bool, + default_font_family: &[String], + scale_factor: f32, +) -> ParagraphData { + let font_style = &*node.get::().unwrap(); + + let mut paragraph_style = ParagraphStyle::default(); + paragraph_style.set_text_align(font_style.text_align); + paragraph_style.set_max_lines(font_style.max_lines); + paragraph_style.set_replace_tab_characters(true); + paragraph_style.set_text_height_behavior(font_style.text_height); + + if let Some(ellipsis) = font_style.text_overflow.get_ellipsis() { + paragraph_style.set_ellipsis(ellipsis); + } + + let mut paragraph_builder = ParagraphBuilder::new(¶graph_style, font_collection); + + let text_style = + font_style.text_style(default_font_family, scale_factor, font_style.text_height); + paragraph_builder.push_style(&text_style); + + for text_span in node.children() { + if let NodeType::Element(ElementNode { + tag: TagName::Text, .. + }) = &*text_span.node_type() + { + let text_nodes = text_span.children(); + let text_node = *text_nodes.first().unwrap(); + let text_node_type = &*text_node.node_type(); + let text_font_style = text_span.get::().unwrap(); + let text_style = text_font_style.text_style( + default_font_family, + scale_factor, + font_style.text_height, + ); + paragraph_builder.push_style(&text_style); + + if let NodeType::Text(text) = text_node_type { + paragraph_builder.add_text(text); + } + } + } + + if is_rendering { + // This is very tricky, but it works! It allows freya to render the cursor at the end of a line. + paragraph_builder.add_text(" "); + } + + let mut paragraph = paragraph_builder.build(); + paragraph.layout( + if font_style.max_lines == Some(1) && font_style.text_align == TextAlign::default() { + f32::MAX + } else { + area_size.width + 1.0 + }, + ); + + let width = match font_style.text_align { + TextAlign::Start | TextAlign::Left => paragraph.longest_line(), + _ => paragraph.max_width(), + }; + + ParagraphData { + size: Size2D::new(width, paragraph.height()), + paragraph, + } +} + +pub fn draw_cursor_highlights( + area: &Area, + paragraph: &Paragraph, + canvas: &Canvas, + node_ref: &DioxusNode, +) -> Option<()> { + let node_cursor_state = &*node_ref.get::().unwrap(); + + let highlights = node_cursor_state.highlights.as_ref()?; + let highlight_color = node_cursor_state.highlight_color; + + for (from, to) in highlights.iter() { + let (from, to) = { + if from < to { + (from, to) + } else { + (to, from) + } + }; + let cursor_rects = paragraph.get_rects_for_range( + *from..*to, + RectHeightStyle::Tight, + RectWidthStyle::Tight, + ); + for cursor_rect in cursor_rects { + let rect = align_highlights_and_cursor_paragraph( + node_ref, + area, + paragraph, + &cursor_rect, + None, + ); + + let mut paint = Paint::default(); + paint.set_anti_alias(true); + paint.set_style(PaintStyle::Fill); + paint.set_color(highlight_color); + + canvas.draw_rect(rect, &paint); + } + } + + Some(()) +} + +pub fn draw_cursor( + area: &Area, + paragraph: &Paragraph, + canvas: &Canvas, + node_ref: &DioxusNode, +) -> Option<()> { + let node_cursor_state = &*node_ref.get::().unwrap(); + + let cursor = node_cursor_state.position?; + let cursor_color = node_cursor_state.color; + let cursor_position = cursor as usize; + + let cursor_rects = paragraph.get_rects_for_range( + cursor_position..cursor_position + 1, + RectHeightStyle::Tight, + RectWidthStyle::Tight, + ); + let cursor_rect = cursor_rects.first()?; + + let rect = + align_highlights_and_cursor_paragraph(node_ref, area, paragraph, cursor_rect, Some(1.0)); + + let mut paint = Paint::default(); + paint.set_anti_alias(true); + paint.set_style(PaintStyle::Fill); + paint.set_color(cursor_color); + + canvas.draw_rect(rect, &paint); + + Some(()) +} + +/// Align the Y axis of the highlights and cursor of a paragraph +pub fn align_highlights_and_cursor_paragraph( + node: &DioxusNode, + area: &Area, + paragraph: &Paragraph, + cursor_rect: &TextBox, + width: Option, +) -> Rect { + let cursor_state = node.get::().unwrap(); + + let left = area.min_x() + cursor_rect.rect.left; + let right = left + width.unwrap_or(cursor_rect.rect.right - cursor_rect.rect.left); + + match cursor_state.highlight_mode { + HighlightMode::Fit => { + let top = (area.min_y() + + align_main_align_paragraph(node, area, paragraph) + + cursor_rect.rect.top) + .clamp(area.min_y(), area.max_y()); + let bottom = (top + (cursor_rect.rect.bottom - cursor_rect.rect.top)) + .clamp(area.min_y(), area.max_y()); + + Rect::new(left, top, right, bottom) + } + HighlightMode::Expanded => { + let top = area.min_y(); + let bottom = area.max_y(); + + Rect::new(left, top, right, bottom) + } + } +} + +/// Align the main alignment of a paragraph +pub fn align_main_align_paragraph(node: &DioxusNode, area: &Area, paragraph: &Paragraph) -> f32 { + let layout = node.get::().unwrap(); + + match layout.main_alignment { + Alignment::Start => 0., + Alignment::Center => (area.height() / 2.0) - (paragraph.height() / 2.0), + Alignment::End => area.height() - paragraph.height(), + Alignment::SpaceBetween => 0., + Alignment::SpaceEvenly => 0., + Alignment::SpaceAround => 0., + } +} diff --git a/crates/core/src/render/utils/shadows.rs b/crates/core/src/render/utils/shadows.rs new file mode 100644 index 000000000..ea83c183e --- /dev/null +++ b/crates/core/src/render/utils/shadows.rs @@ -0,0 +1,77 @@ +use freya_engine::prelude::*; +use freya_node_state::{ + CornerRadius, + Shadow, + ShadowPosition, + StyleState, +}; +use torin::prelude::Area; + +pub fn render_shadow( + canvas: &Canvas, + node_style: &StyleState, + path: &mut Path, + rounded_rect: RRect, + area: Area, + shadow: Shadow, + corner_radius: CornerRadius, +) { + let mut shadow_path = Path::new(); + let mut shadow_paint = Paint::default(); + shadow_paint.set_anti_alias(true); + + shadow.fill.apply_to_paint(&mut shadow_paint, area); + + // Shadows can be either outset or inset + // If they are outset, we fill a copy of the path outset by spread_radius, and blur it. + // Otherwise, we draw a stroke with the inner portion being spread_radius width, and the outer portion being blur_radius width. + let outset: Point = match shadow.position { + ShadowPosition::Normal => { + shadow_paint.set_style(PaintStyle::Fill); + (shadow.spread, shadow.spread).into() + } + ShadowPosition::Inset => { + shadow_paint.set_style(PaintStyle::Stroke); + shadow_paint.set_stroke_width(shadow.blur / 2.0 + shadow.spread); + (-shadow.spread / 2.0, -shadow.spread / 2.0).into() + } + }; + + // Apply gassuan blur to the copied path. + if shadow.blur > 0.0 { + shadow_paint.set_mask_filter(MaskFilter::blur( + BlurStyle::Normal, + shadow.blur / 2.0, + false, + )); + } + + // Add either the RRect or smoothed path based on whether smoothing is used. + if corner_radius.smoothing > 0.0 { + shadow_path.add_path( + &node_style + .corner_radius + .smoothed_path(rounded_rect.with_outset(outset)), + Point::new(area.min_x(), area.min_y()) - outset, + None, + ); + } else { + shadow_path.add_rrect(rounded_rect.with_outset(outset), None); + } + + // Offset our path by the shadow's x and y coordinates. + shadow_path.offset((shadow.x, shadow.y)); + + // Exclude the original path bounds from the shadow using a clip, then draw the shadow. + canvas.save(); + canvas.clip_path( + path, + match shadow.position { + ShadowPosition::Normal => ClipOp::Difference, + ShadowPosition::Inset => ClipOp::Intersect, + }, + true, + ); + canvas.draw_path(&shadow_path, &shadow_paint); + canvas.restore(); +} diff --git a/crates/devtools/src/lib.rs b/crates/devtools/src/lib.rs index 5c651e353..f79b7b510 100644 --- a/crates/devtools/src/lib.rs +++ b/crates/devtools/src/lib.rs @@ -12,6 +12,7 @@ use freya_components::*; use freya_core::prelude::EventMessage; use freya_elements::elements as dioxus_elements; use freya_hooks::{ + use_applied_theme, use_init_theme, use_platform, DARK_THEME, @@ -116,15 +117,15 @@ impl PartialEq for DevToolsProps { #[allow(non_snake_case)] pub fn DevTools(props: DevToolsProps) -> Element { - let theme = use_init_theme(|| DARK_THEME); + use_init_theme(|| DARK_THEME); use_init_radio_station::(|| DevtoolsState { hovered_node: props.hovered_node.clone(), devtools_receiver: props.devtools_receiver.clone(), devtools_tree: HashSet::default(), }); - let theme = theme.read(); - let color = &theme.body.color; + let theme = use_applied_theme!(None, body); + let color = &theme.color; rsx!( rect { diff --git a/crates/devtools/src/property.rs b/crates/devtools/src/property.rs index 6b283cb08..b4a5ad784 100644 --- a/crates/devtools/src/property.rs +++ b/crates/devtools/src/property.rs @@ -13,10 +13,8 @@ pub fn Property(name: String, value: String) -> Element { rsx!( rect { overflow: "clip", - height: "30", width: "100%", direction: "horizontal", - padding: "10", paragraph { width: "100%", text { @@ -43,25 +41,22 @@ pub fn Property(name: String, value: String) -> Element { #[component] pub fn GradientProperty(name: String, fill: Fill) -> Element { rsx!( - rect { - padding: "5 10", - paragraph { - line_height: "1.9", - text { - font_size: "15", - color: "rgb(71, 180, 240)", - "{name}" - } - text { - font_size: "15", - color: "rgb(215, 215, 215)", - ": " - } - text { - font_size: "15", - color: "rgb(252,181,172)", - "{fill}", - } + paragraph { + line_height: "1.9", + text { + font_size: "15", + color: "rgb(71, 180, 240)", + "{name}" + } + text { + font_size: "15", + color: "rgb(215, 215, 215)", + ": " + } + text { + font_size: "15", + color: "rgb(252,181,172)", + "{fill}", } } ) @@ -73,10 +68,8 @@ pub fn ColorProperty(name: String, fill: Fill) -> Element { rsx!( rect { overflow: "clip", - height: "30", width: "100%", direction: "horizontal", - padding: "10", label { font_size: "15", color: "rgb(71, 180, 240)", @@ -121,10 +114,8 @@ pub fn ShadowProperty(name: String, shadow: Shadow) -> Element { rsx!( rect { overflow: "clip", - height: "30", width: "100%", direction: "horizontal", - padding: "10", paragraph { text { font_size: "15", @@ -177,10 +168,8 @@ pub fn BorderProperty(name: String, border: Border) -> Element { rsx!( rect { overflow: "clip", - height: "30", width: "100%", direction: "horizontal", - padding: "10", paragraph { text { font_size: "15", @@ -233,10 +222,8 @@ pub fn TextShadowProperty(name: String, text_shadow: TextShadow) -> Element { rsx!( rect { overflow: "clip", - height: "30", width: "100%", direction: "horizontal", - padding: "10", paragraph { text { font_size: "15", diff --git a/crates/devtools/src/tabs/style.rs b/crates/devtools/src/tabs/style.rs index c8186db47..b802b8b59 100644 --- a/crates/devtools/src/tabs/style.rs +++ b/crates/devtools/src/tabs/style.rs @@ -25,10 +25,12 @@ pub fn NodeInspectorStyle(node_id: String) -> Element { rsx!( ScrollView { show_scrollbar: true, - height : "calc(100% - 35)", + height : "fill", width: "100%", - {node.state.attributes().into_iter().enumerate().map(|(i, (name, attr))| { - match attr { + spacing: "6", + padding: "8 16", + {node.state.attributes().into_iter().enumerate().filter_map(|(i, (name, attr))| { + Some(match attr { AttributeType::Measure(measure) => { rsx!{ Property { @@ -83,6 +85,19 @@ pub fn NodeInspectorStyle(node_id: String) -> Element { } } } + AttributeType::OptionalColor(fill) => { + if let Some(fill) = fill { + rsx!{ + ColorProperty { + key: "{i}", + name: "{name}", + fill: fill.clone() + } + } + } else { + return None; + } + } AttributeType::Gradient(fill) => { rsx!{ GradientProperty { @@ -182,7 +197,7 @@ pub fn NodeInspectorStyle(node_id: String) -> Element { } } } - } + }) })} } ) diff --git a/crates/elements/src/_docs/attributes/content.md b/crates/elements/src/_docs/attributes/content.md index a26b154ed..d1d723c00 100644 --- a/crates/elements/src/_docs/attributes/content.md +++ b/crates/elements/src/_docs/attributes/content.md @@ -4,10 +4,12 @@ Accepted values: - `normal` (default): Uses parent bounds. - `fit`: Uses parent bounds but later shrunks to the size of the biggest element inside. +- `flex`: Marks the container as flex container, children of this element will be able to use `size`/`size(n)` in their `width` and `height` attributes. -The `fit` mode will allow the inner elements using `width: fill-min` to expand to the biggest element inside this element. -### Example +### `fit` + +The `fit` mode will allow the inner elements using `width: fill-min` to expand to the biggest element inside this element. ```rust, no_run # use freya::prelude::*; diff --git a/crates/elements/src/_docs/attributes/fill_stroke.md b/crates/elements/src/_docs/attributes/fill_stroke.md new file mode 100644 index 000000000..04bfe0e30 --- /dev/null +++ b/crates/elements/src/_docs/attributes/fill_stroke.md @@ -0,0 +1,21 @@ +The `fill` and `stroke` attributes allows you to specify the fill or stroke color for the `svg`. + +You can learn about the syntax of this attribute in [`Color Syntax`](crate::_docs::color_syntax). + +### Example + +```rust, no_run +# use freya::prelude::*; +fn app() -> Element { + let svg_content = include_str!("../../../examples/settings.svg"); + + rsx!( + svg { + fill: "red", + width: "100%", + height: "100%", + svg_content, + } + ) +} +``` diff --git a/crates/elements/src/_docs/attributes/text_height.md b/crates/elements/src/_docs/attributes/text_height.md new file mode 100644 index 000000000..270d7450a --- /dev/null +++ b/crates/elements/src/_docs/attributes/text_height.md @@ -0,0 +1,22 @@ +Specify the text height behavior. + +Accepted values: + +- `disable-all` (default) +- `all` +- `disable-first-ascent` +- `disable-least-ascent` + +### Example + +```rust, no_run +# use freya::prelude::*; +fn app() -> Element { + rsx!( + label { + text_height: "disable-all", + "Hello, World!" + } + ) +} +``` \ No newline at end of file diff --git a/crates/elements/src/_docs/size_unit.rs b/crates/elements/src/_docs/size_unit.rs index 15a341bdb..ac2de49a9 100644 --- a/crates/elements/src/_docs/size_unit.rs +++ b/crates/elements/src/_docs/size_unit.rs @@ -102,3 +102,30 @@ //! ) //! } //! ``` +//! +//! #### Flex Factor +//! +//! When being a children of an element with `content: flex` you may change the growth factor of the size attributes. +//! +//! ```rust, no_run +//! # use freya::prelude::*; +//! fn app() -> Element { +//! rsx!( +//! rect { +//! content: "flex", +//! width: "200", +//! height: "200", +//! rect { +//! height: "flex(1)", +//! width: "100%", +//! background: "red" +//! } +//! rect { +//! height: "flex(3)", +//! width: "100%", +//! background: "blue" +//! } +//! } +//! ) +//! } +//! ``` diff --git a/crates/elements/src/definitions.rs b/crates/elements/src/definitions.rs index 9a259a3b8..cbf62a1b5 100644 --- a/crates/elements/src/definitions.rs +++ b/crates/elements/src/definitions.rs @@ -172,8 +172,7 @@ builder_constructors! { /// } /// ``` rect { - #[doc = include_str!("_docs/attributes/padding.md")] - padding: String, + // Layout #[doc = include_str!("_docs/attributes/width_height.md")] height: String, width: String, @@ -183,17 +182,45 @@ builder_constructors! { #[doc = include_str!("_docs/attributes/max_width_max_height.md")] max_height: String, max_width: String, + #[doc = include_str!("_docs/attributes/margin.md")] + margin: String, + #[doc = include_str!("_docs/attributes/padding.md")] + padding: String, + #[doc = include_str!("_docs/attributes/position.md")] + position: String, + position_top: String, + position_right: String, + position_bottom: String, + position_left: String, + layer: String, + + // Children layout + #[doc = include_str!("_docs/attributes/direction.md")] + direction: String, + #[doc = include_str!("_docs/attributes/content.md")] + content: String, + #[doc = include_str!("_docs/attributes/main_align_cross_align.md")] + main_align: String, + cross_align: String, + #[doc = include_str!("_docs/attributes/spacing.md")] + spacing: String, + #[doc = include_str!("_docs/attributes/overflow.md")] + overflow: String, + offset_x: String, + offset_y: String, + + // Style #[doc = include_str!("_docs/attributes/background.md")] background: String, #[doc = include_str!("_docs/attributes/border.md")] border: String, - #[doc = include_str!("_docs/attributes/direction.md")] - direction: String, #[doc = include_str!("_docs/attributes/shadow.md")] shadow: String, #[doc = include_str!("_docs/attributes/corner.md")] corner_radius: String, corner_smoothing: String, + + // Font style #[doc = include_str!("_docs/attributes/color.md")] color: String, #[doc = include_str!("_docs/attributes/font_size.md")] @@ -206,46 +233,110 @@ builder_constructors! { font_weight: String, #[doc = include_str!("_docs/attributes/font_width.md")] font_width: String, - #[doc = include_str!("_docs/attributes/main_align_cross_align.md")] - main_align: String, - cross_align: String, #[doc = include_str!("_docs/attributes/text_align.md")] text_align: String, + #[doc = include_str!("_docs/attributes/line_height.md")] + line_height: String, + #[doc = include_str!("_docs/attributes/text_shadow.md")] + text_shadow: String, + #[doc = include_str!("_docs/attributes/max_lines.md")] + max_lines: String, + #[doc = include_str!("_docs/attributes/decoration.md")] + decoration: String, + #[doc = include_str!("_docs/attributes/decoration_style.md")] + decoration_style: String, + #[doc = include_str!("_docs/attributes/decoration_color.md")] + decoration_color: String, + #[doc = include_str!("_docs/attributes/text_overflow.md")] + text_overflow: String, + #[doc = include_str!("_docs/attributes/letter_spacing.md")] + letter_spacing: String, + #[doc = include_str!("_docs/attributes/word_spacing.md")] + word_spacing: String, + #[doc = include_str!("_docs/attributes/text_height.md")] + text_height: String, + + // Transform #[doc = include_str!("_docs/attributes/rotate.md")] rotate: String, - #[doc = include_str!("_docs/attributes/overflow.md")] - overflow: String, - #[doc = include_str!("_docs/attributes/margin.md")] - margin: String, - #[doc = include_str!("_docs/attributes/position.md")] - position: String, - position_top: String, - position_right: String, - position_bottom: String, - position_left: String, #[doc = include_str!("_docs/attributes/opacity.md")] opacity: String, - #[doc = include_str!("_docs/attributes/content.md")] - content: String, - #[doc = include_str!("_docs/attributes/line_height.md")] - line_height: String, - #[doc = include_str!("_docs/attributes/spacing.md")] - spacing: String, scale: String, - - a11y_auto_focus: String, - a11y_name: String, - a11y_role:String, - a11y_id: AccessibilityId, - a11y_alt: String, - a11y_focusable: String, + // Reference canvas_reference: String, - layer: String, - offset_y: String, - offset_x: String, reference: Reference, cursor_reference: CursorReference, + + // Accessibility + a11y_id: String, + a11y_focusable: String, + a11y_auto_focus: String, + a11y_name: String, + a11y_description: String, + a11y_value: String, + a11y_access_key: String, + a11y_author_id: String, + a11y_keyboard_shortcut: String, + a11y_language: String, + a11y_placeholder: String, + a11y_role_description: String, + a11y_state_description: String, + a11y_tooltip: String, + a11y_url: String, + a11y_row_index_text: String, + a11y_column_index_text: String, + a11y_scroll_x: String, + a11y_scroll_x_min: String, + a11y_scroll_x_max: String, + a11y_scroll_y: String, + a11y_scroll_y_min: String, + a11y_scroll_y_max: String, + a11y_numeric_value: String, + a11y_min_numeric_value: String, + a11y_max_numeric_value: String, + a11y_numeric_value_step: String, + a11y_numeric_value_jump: String, + a11y_row_count: String, + a11y_column_count: String, + a11y_row_index: String, + a11y_column_index: String, + a11y_row_span: String, + a11y_column_span: String, + a11y_level: String, + a11y_size_of_set: String, + a11y_position_in_set: String, + a11y_color_value: String, + a11y_expanded: String, + a11y_selected: String, + a11y_hovered: String, + a11y_hidden: String, + a11y_linked: String, + a11y_multiselectable: String, + a11y_required: String, + a11y_visited: String, + a11y_busy: String, + a11y_live_atomic: String, + a11y_modal: String, + a11y_touch_transparent: String, + a11y_read_only: String, + a11y_disabled: String, + a11y_is_spelling_error: String, + a11y_is_grammar_error: String, + a11y_is_search_match: String, + a11y_is_suggestion: String, + a11y_role: String, + a11y_invalid: String, + a11y_toggled: String, + a11y_live: String, + a11y_default_action_verb: String, + a11y_orientation: String, + a11y_sort_direction: String, + a11y_current: String, + a11y_auto_complete: String, + a11y_has_popup: String, + a11y_list_style: String, + a11y_vertical_offset: String, }; /// `label` simply let's you display some text. /// @@ -262,15 +353,33 @@ builder_constructors! { /// } /// ``` label { - #[doc = include_str!("_docs/attributes/color.md")] - color: String, - #[doc = include_str!("_docs/attributes/text_shadow.md")] - text_shadow: String, + // Layout #[doc = include_str!("_docs/attributes/width_height.md")] height: String, width: String, + #[doc = include_str!("_docs/attributes/min_width_min_height.md")] + min_height: String, + min_width: String, + #[doc = include_str!("_docs/attributes/max_width_max_height.md")] + max_height: String, + max_width: String, + #[doc = include_str!("_docs/attributes/margin.md")] + margin: String, + #[doc = include_str!("_docs/attributes/position.md")] + position: String, + position_top: String, + position_right: String, + position_bottom: String, + position_left: String, + layer: String, + + // Children layout #[doc = include_str!("_docs/attributes/main_align_cross_align.md")] main_align: String, + + // Font style + #[doc = include_str!("_docs/attributes/color.md")] + color: String, #[doc = include_str!("_docs/attributes/font_size.md")] font_size: String, #[doc = include_str!("_docs/attributes/font_family.md")] @@ -283,16 +392,12 @@ builder_constructors! { font_width: String, #[doc = include_str!("_docs/attributes/text_align.md")] text_align: String, - #[doc = include_str!("_docs/attributes/max_lines.md")] - max_lines: String, - #[doc = include_str!("_docs/attributes/rotate.md")] - rotate: String, #[doc = include_str!("_docs/attributes/line_height.md")] line_height: String, - #[doc = include_str!("_docs/attributes/letter_spacing.md")] - letter_spacing: String, - #[doc = include_str!("_docs/attributes/word_spacing.md")] - word_spacing: String, + #[doc = include_str!("_docs/attributes/text_shadow.md")] + text_shadow: String, + #[doc = include_str!("_docs/attributes/max_lines.md")] + max_lines: String, #[doc = include_str!("_docs/attributes/decoration.md")] decoration: String, #[doc = include_str!("_docs/attributes/decoration_style.md")] @@ -301,18 +406,88 @@ builder_constructors! { decoration_color: String, #[doc = include_str!("_docs/attributes/text_overflow.md")] text_overflow: String, - #[doc = include_str!("_docs/attributes/margin.md")] - margin: String, + #[doc = include_str!("_docs/attributes/letter_spacing.md")] + letter_spacing: String, + #[doc = include_str!("_docs/attributes/word_spacing.md")] + word_spacing: String, + #[doc = include_str!("_docs/attributes/text_height.md")] + text_height: String, + + // Transform + #[doc = include_str!("_docs/attributes/rotate.md")] + rotate: String, #[doc = include_str!("_docs/attributes/opacity.md")] opacity: String, - layer: String, + // Accessibility + a11y_id: String, a11y_auto_focus: String, - a11y_name: String, - a11y_role:String, - a11y_id: AccessibilityId, - a11y_alt: String, a11y_focusable: String, + a11y_name: String, + a11y_description: String, + a11y_value: String, + a11y_access_key: String, + a11y_author_id: String, + a11y_keyboard_shortcut: String, + a11y_language: String, + a11y_placeholder: String, + a11y_role_description: String, + a11y_state_description: String, + a11y_tooltip: String, + a11y_url: String, + a11y_row_index_text: String, + a11y_column_index_text: String, + a11y_scroll_x: String, + a11y_scroll_x_min: String, + a11y_scroll_x_max: String, + a11y_scroll_y: String, + a11y_scroll_y_min: String, + a11y_scroll_y_max: String, + a11y_numeric_value: String, + a11y_min_numeric_value: String, + a11y_max_numeric_value: String, + a11y_numeric_value_step: String, + a11y_numeric_value_jump: String, + a11y_row_count: String, + a11y_column_count: String, + a11y_row_index: String, + a11y_column_index: String, + a11y_row_span: String, + a11y_column_span: String, + a11y_level: String, + a11y_size_of_set: String, + a11y_position_in_set: String, + a11y_color_value: String, + a11y_expanded: String, + a11y_selected: String, + a11y_hovered: String, + a11y_hidden: String, + a11y_linked: String, + a11y_multiselectable: String, + a11y_required: String, + a11y_visited: String, + a11y_busy: String, + a11y_live_atomic: String, + a11y_modal: String, + a11y_touch_transparent: String, + a11y_read_only: String, + a11y_disabled: String, + a11y_is_spelling_error: String, + a11y_is_grammar_error: String, + a11y_is_search_match: String, + a11y_is_suggestion: String, + a11y_role: String, + a11y_invalid: String, + a11y_toggled: String, + a11y_live: String, + a11y_default_action_verb: String, + a11y_orientation: String, + a11y_sort_direction: String, + a11y_current: String, + a11y_auto_complete: String, + a11y_has_popup: String, + a11y_list_style: String, + a11y_vertical_offset: String, }; /// `paragraph` element let's you build texts with different styles. /// @@ -336,6 +511,7 @@ builder_constructors! { /// } /// ``` paragraph { + // Layout #[doc = include_str!("_docs/attributes/width_height.md")] height: String, width: String, @@ -345,12 +521,23 @@ builder_constructors! { #[doc = include_str!("_docs/attributes/max_width_max_height.md")] max_height: String, max_width: String, + #[doc = include_str!("_docs/attributes/margin.md")] + margin: String, + #[doc = include_str!("_docs/attributes/position.md")] + position: String, + position_top: String, + position_right: String, + position_bottom: String, + position_left: String, + layer: String, + + // Children layout #[doc = include_str!("_docs/attributes/main_align_cross_align.md")] main_align: String, - #[doc = include_str!("_docs/attributes/text_align.md")] - text_align: String, - #[doc = include_str!("_docs/attributes/rotate.md")] - rotate: String, + + // Font style + #[doc = include_str!("_docs/attributes/color.md")] + color: String, #[doc = include_str!("_docs/attributes/font_size.md")] font_size: String, #[doc = include_str!("_docs/attributes/font_family.md")] @@ -361,51 +548,120 @@ builder_constructors! { font_weight: String, #[doc = include_str!("_docs/attributes/font_width.md")] font_width: String, + #[doc = include_str!("_docs/attributes/text_align.md")] + text_align: String, #[doc = include_str!("_docs/attributes/line_height.md")] line_height: String, - #[doc = include_str!("_docs/attributes/letter_spacing.md")] - letter_spacing: String, - #[doc = include_str!("_docs/attributes/word_spacing.md")] - word_spacing: String, + #[doc = include_str!("_docs/attributes/text_shadow.md")] + text_shadow: String, + #[doc = include_str!("_docs/attributes/max_lines.md")] + max_lines: String, #[doc = include_str!("_docs/attributes/decoration.md")] decoration: String, #[doc = include_str!("_docs/attributes/decoration_style.md")] decoration_style: String, #[doc = include_str!("_docs/attributes/decoration_color.md")] + decoration_color: String, + #[doc = include_str!("_docs/attributes/text_overflow.md")] text_overflow: String, - #[doc = include_str!("_docs/attributes/overflow.md")] - overflow: String, - #[doc = include_str!("_docs/attributes/margin.md")] - margin: String, + #[doc = include_str!("_docs/attributes/letter_spacing.md")] + letter_spacing: String, + #[doc = include_str!("_docs/attributes/word_spacing.md")] + word_spacing: String, + #[doc = include_str!("_docs/attributes/text_height.md")] + text_height: String, + + // Transform + #[doc = include_str!("_docs/attributes/rotate.md")] + rotate: String, #[doc = include_str!("_docs/attributes/opacity.md")] opacity: String, - layer: String, + // Text Editing cursor_index: String, - max_lines: String, cursor_color: String, cursor_mode: String, cursor_id: String, - a11y_auto_focus: String, - a11y_name: String, - a11y_role:String, - a11y_id: AccessibilityId, - a11y_alt: String, - a11y_focusable: String, highlights: String, highlight_color: String, highlight_mode: String, + + // Accessibility + a11y_id: String, + a11y_focusable: String, + a11y_auto_focus: String, + a11y_name: String, + a11y_description: String, + a11y_value: String, + a11y_access_key: String, + a11y_author_id: String, + a11y_keyboard_shortcut: String, + a11y_language: String, + a11y_placeholder: String, + a11y_role_description: String, + a11y_state_description: String, + a11y_tooltip: String, + a11y_url: String, + a11y_row_index_text: String, + a11y_column_index_text: String, + a11y_scroll_x: String, + a11y_scroll_x_min: String, + a11y_scroll_x_max: String, + a11y_scroll_y: String, + a11y_scroll_y_min: String, + a11y_scroll_y_max: String, + a11y_numeric_value: String, + a11y_min_numeric_value: String, + a11y_max_numeric_value: String, + a11y_numeric_value_step: String, + a11y_numeric_value_jump: String, + a11y_row_count: String, + a11y_column_count: String, + a11y_row_index: String, + a11y_column_index: String, + a11y_row_span: String, + a11y_column_span: String, + a11y_level: String, + a11y_size_of_set: String, + a11y_position_in_set: String, + a11y_color_value: String, + a11y_expanded: String, + a11y_selected: String, + a11y_hovered: String, + a11y_hidden: String, + a11y_linked: String, + a11y_multiselectable: String, + a11y_required: String, + a11y_visited: String, + a11y_busy: String, + a11y_live_atomic: String, + a11y_modal: String, + a11y_touch_transparent: String, + a11y_read_only: String, + a11y_disabled: String, + a11y_is_spelling_error: String, + a11y_is_grammar_error: String, + a11y_is_search_match: String, + a11y_is_suggestion: String, + a11y_role: String, + a11y_invalid: String, + a11y_toggled: String, + a11y_live: String, + a11y_default_action_verb: String, + a11y_orientation: String, + a11y_sort_direction: String, + a11y_current: String, + a11y_auto_complete: String, + a11y_has_popup: String, + a11y_list_style: String, + a11y_vertical_offset: String, }; /// `text` element is simply a text span used for the `paragraph` element. text { + // Font style #[doc = include_str!("_docs/attributes/color.md")] color: String, #[doc = include_str!("_docs/attributes/font_size.md")] - text_shadow: String, - #[doc = include_str!("_docs/attributes/width_height.md")] - height: String, - width: String, - #[doc = include_str!("_docs/attributes/font_size.md")] font_size: String, #[doc = include_str!("_docs/attributes/font_family.md")] font_family: String, @@ -415,21 +671,27 @@ builder_constructors! { font_weight: String, #[doc = include_str!("_docs/attributes/font_width.md")] font_width: String, + #[doc = include_str!("_docs/attributes/text_align.md")] + text_align: String, #[doc = include_str!("_docs/attributes/line_height.md")] line_height: String, - #[doc = include_str!("_docs/attributes/letter_spacing.md")] - letter_spacing: String, - #[doc = include_str!("_docs/attributes/word_spacing.md")] - word_spacing: String, + #[doc = include_str!("_docs/attributes/text_shadow.md")] + text_shadow: String, #[doc = include_str!("_docs/attributes/decoration.md")] decoration: String, #[doc = include_str!("_docs/attributes/decoration_style.md")] decoration_style: String, #[doc = include_str!("_docs/attributes/decoration_color.md")] decoration_color: String, + #[doc = include_str!("_docs/attributes/letter_spacing.md")] + letter_spacing: String, + #[doc = include_str!("_docs/attributes/word_spacing.md")] + word_spacing: String, }; /// `image` element let's you show an image. /// + /// For dynamic Images you may use `dynamic_bytes`. + /// /// ### Example /// /// ```rust, ignore, no_run @@ -441,33 +703,116 @@ builder_constructors! { /// rsx!( /// image { /// image_data: image_data, - /// width: "{size}", - /// height: "{size}", + /// width: "100%", // You must specify size otherwhise it will default to 0 + /// height: "100%", /// } /// ) /// } /// ``` image { - #[doc = include_str!("_docs/attributes/width_height.md")] + // Layout + #[doc = include_str!("_docs/attributes/width_height.md")] height: String, width: String, + #[doc = include_str!("_docs/attributes/min_width_min_height.md")] + min_height: String, + min_width: String, + #[doc = include_str!("_docs/attributes/max_width_max_height.md")] + max_height: String, + max_width: String, + #[doc = include_str!("_docs/attributes/margin.md")] + margin: String, + #[doc = include_str!("_docs/attributes/position.md")] + position: String, + position_top: String, + position_right: String, + position_bottom: String, + position_left: String, + layer: String, + + // Transform #[doc = include_str!("_docs/attributes/rotate.md")] rotate: String, #[doc = include_str!("_docs/attributes/opacity.md")] opacity: String, + // Image image_data: String, image_reference: String, + + // Accessibility + a11y_id: String, + a11y_focusable: String, a11y_auto_focus: String, a11y_name: String, - a11y_role:String, - a11y_id: AccessibilityId, - a11y_alt: String, + a11y_description: String, + a11y_value: String, + a11y_access_key: String, + a11y_author_id: String, + a11y_keyboard_shortcut: String, + a11y_language: String, + a11y_placeholder: String, + a11y_role_description: String, + a11y_state_description: String, + a11y_tooltip: String, + a11y_url: String, + a11y_row_index_text: String, + a11y_column_index_text: String, + a11y_scroll_x: String, + a11y_scroll_x_min: String, + a11y_scroll_x_max: String, + a11y_scroll_y: String, + a11y_scroll_y_min: String, + a11y_scroll_y_max: String, + a11y_numeric_value: String, + a11y_min_numeric_value: String, + a11y_max_numeric_value: String, + a11y_numeric_value_step: String, + a11y_numeric_value_jump: String, + a11y_row_count: String, + a11y_column_count: String, + a11y_row_index: String, + a11y_column_index: String, + a11y_row_span: String, + a11y_column_span: String, + a11y_level: String, + a11y_size_of_set: String, + a11y_position_in_set: String, + a11y_color_value: String, + a11y_expanded: String, + a11y_selected: String, + a11y_hovered: String, + a11y_hidden: String, + a11y_linked: String, + a11y_multiselectable: String, + a11y_required: String, + a11y_visited: String, + a11y_busy: String, + a11y_live_atomic: String, + a11y_modal: String, + a11y_touch_transparent: String, + a11y_read_only: String, + a11y_disabled: String, + a11y_is_spelling_error: String, + a11y_is_grammar_error: String, + a11y_is_search_match: String, + a11y_is_suggestion: String, + a11y_role: String, + a11y_invalid: String, + a11y_toggled: String, + a11y_live: String, + a11y_default_action_verb: String, + a11y_orientation: String, + a11y_sort_direction: String, + a11y_current: String, + a11y_auto_complete: String, + a11y_has_popup: String, + a11y_list_style: String, + a11y_vertical_offset: String, }; /// `svg` element let's you display SVG code. /// - /// You will need to use the [`dynamic_bytes`](https://docs.freyaui.dev/freya/prelude/fn.dynamic_bytes.html) - /// to transform the bytes into data the element can recognize. + /// For dynamic SVGs you may use `dynamic_bytes`. /// /// ### Example /// @@ -476,33 +821,121 @@ builder_constructors! { /// static FERRIS: &[u8] = include_bytes!("./ferris.svg"); /// /// fn app() -> Element { - /// let ferris = dynamic_bytes(FERRIS); + /// let ferris = static_bytes(FERRIS); /// rsx!( /// svg { /// svg_data: ferris, + /// width: "100%", // You must specify size otherwhise it will default to 0 + /// height: "100%", /// } /// ) /// } /// ``` svg { - #[doc = include_str!("_docs/attributes/margin.md")] - margin: String, - #[doc = include_str!("_docs/attributes/width_height.md")] + // Layout + #[doc = include_str!("_docs/attributes/width_height.md")] height: String, width: String, + #[doc = include_str!("_docs/attributes/min_width_min_height.md")] + min_height: String, + min_width: String, + #[doc = include_str!("_docs/attributes/max_width_max_height.md")] + max_height: String, + max_width: String, + #[doc = include_str!("_docs/attributes/margin.md")] + margin: String, + #[doc = include_str!("_docs/attributes/position.md")] + position: String, + position_top: String, + position_right: String, + position_bottom: String, + position_left: String, + layer: String, + + // Transform #[doc = include_str!("_docs/attributes/rotate.md")] rotate: String, #[doc = include_str!("_docs/attributes/opacity.md")] opacity: String, + // Svg + #[doc = include_str!("_docs/attributes/color.md")] + color: String, svg_data: String, svg_content: String, + #[doc = include_str!("_docs/attributes/fill_stroke.md")] + fill: String, + stroke: String, + + // Accessibility + a11y_id: String, + a11y_focusable: String, a11y_auto_focus: String, a11y_name: String, - a11y_role:String, - a11y_id: AccessibilityId, - a11y_alt: String, - a11y_focusable: String, + a11y_description: String, + a11y_value: String, + a11y_access_key: String, + a11y_author_id: String, + a11y_keyboard_shortcut: String, + a11y_language: String, + a11y_placeholder: String, + a11y_role_description: String, + a11y_state_description: String, + a11y_tooltip: String, + a11y_url: String, + a11y_row_index_text: String, + a11y_column_index_text: String, + a11y_scroll_x: String, + a11y_scroll_x_min: String, + a11y_scroll_x_max: String, + a11y_scroll_y: String, + a11y_scroll_y_min: String, + a11y_scroll_y_max: String, + a11y_numeric_value: String, + a11y_min_numeric_value: String, + a11y_max_numeric_value: String, + a11y_numeric_value_step: String, + a11y_numeric_value_jump: String, + a11y_row_count: String, + a11y_column_count: String, + a11y_row_index: String, + a11y_column_index: String, + a11y_row_span: String, + a11y_column_span: String, + a11y_level: String, + a11y_size_of_set: String, + a11y_position_in_set: String, + a11y_color_value: String, + a11y_expanded: String, + a11y_selected: String, + a11y_hovered: String, + a11y_hidden: String, + a11y_linked: String, + a11y_multiselectable: String, + a11y_required: String, + a11y_visited: String, + a11y_busy: String, + a11y_live_atomic: String, + a11y_modal: String, + a11y_touch_transparent: String, + a11y_read_only: String, + a11y_disabled: String, + a11y_is_spelling_error: String, + a11y_is_grammar_error: String, + a11y_is_search_match: String, + a11y_is_suggestion: String, + a11y_role: String, + a11y_invalid: String, + a11y_toggled: String, + a11y_live: String, + a11y_default_action_verb: String, + a11y_orientation: String, + a11y_sort_direction: String, + a11y_current: String, + a11y_auto_complete: String, + a11y_has_popup: String, + a11y_list_style: String, + a11y_vertical_offset: String, }; } diff --git a/crates/engine/src/mocked.rs b/crates/engine/src/mocked.rs index cfeb00df0..608bc7bda 100644 --- a/crates/engine/src/mocked.rs +++ b/crates/engine/src/mocked.rs @@ -9,6 +9,9 @@ use std::ops::*; use bitflags::bitflags; use glutin::context::PossiblyCurrentContext; +#[derive(Default, Debug)] +pub struct SaveLayerRec; + #[derive(Clone, Debug, PartialEq, Copy, Eq)] pub struct Color(u32); @@ -613,6 +616,18 @@ impl TextStyle { pub fn set_placeholder(&mut self) -> &mut Self { unimplemented!("This is mocked") } + + pub fn set_height_behavior(&mut self, behavior: TextHeightBehavior) { + unimplemented!("This is mocked") + } +} + +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] +pub enum TextHeightBehavior { + All = 0, + DisableFirstAscent = 1, + DisableLastDescent = 2, + DisableAll = 3, } pub struct Typeface; @@ -637,6 +652,10 @@ impl Paint { unimplemented!("This is mocked") } + pub fn set_blend_mode(&mut self, _mode: BlendMode) -> &mut Self { + unimplemented!("This is mocked") + } + pub fn set_style(&mut self, _style: PaintStyle) -> &mut Self { unimplemented!("This is mocked") } @@ -988,8 +1007,6 @@ impl From<&FontCollection> for FontCollection { pub struct StrutStyle; -pub struct TextHeightBehavior; - #[repr(i32)] #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] pub enum TextDirection { @@ -1075,6 +1092,10 @@ impl Canvas { unimplemented!("This is mocked") } + pub fn draw_paint(&self, _: &Paint) -> &Self { + unimplemented!("This is mocked") + } + pub fn draw_line(&self, _p1: impl Into, _p2: impl Into, _paint: &Paint) -> &Self { unimplemented!("This is mocked") } @@ -1083,6 +1104,10 @@ impl Canvas { unimplemented!("This is mocked") } + pub fn save_layer(&self, layer_rec: &SaveLayerRec) -> usize { + unimplemented!("This is mocked") + } + pub fn save_layer_alpha_f(&self, bounds: impl Into>, alpha: f32) -> usize { unimplemented!("This is mocked") } @@ -1443,6 +1468,40 @@ impl MaskFilter { } } +#[repr(i32)] +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] +pub enum BlendMode { + Clear = 0, + Src = 1, + Dst = 2, + SrcOver = 3, + DstOver = 4, + SrcIn = 5, + DstIn = 6, + SrcOut = 7, + DstOut = 8, + SrcATop = 9, + DstATop = 10, + Xor = 11, + Plus = 12, + Modulate = 13, + Screen = 14, + Overlay = 15, + Darken = 16, + Lighten = 17, + ColorDodge = 18, + ColorBurn = 19, + HardLight = 20, + SoftLight = 21, + Difference = 22, + Exclusion = 23, + Multiply = 24, + Hue = 25, + Saturation = 26, + Color = 27, + Luminosity = 28, +} + impl BlurStyle { pub const LastEnum: BlurStyle = BlurStyle::Inner; } @@ -1458,14 +1517,59 @@ pub enum BlurStyle { pub mod svg { use super::{ Canvas, - FontMgr, + Color, + LocalResourceProvider, Size, }; + pub enum LengthUnit { + Percentage, + } + + pub struct Length; + + impl Length { + pub fn new(value: f32, unit: LengthUnit) -> Self { + unimplemented!("This is mocked") + } + } + + pub struct Paint; + + impl Paint { + pub fn from_color(_color: Color) -> Self { + unimplemented!("This is mocked") + } + } + + pub struct SvgNode; + + impl SvgNode { + pub fn set_width(&mut self, _width: Length) { + unimplemented!("This is mocked") + } + + pub fn set_height(&mut self, _height: Length) { + unimplemented!("This is mocked") + } + + pub fn set_color(&mut self, _value: Color) { + unimplemented!("This is mocked") + } + + pub fn set_fill(&mut self, _value: Paint) { + unimplemented!("This is mocked") + } + + pub fn set_stroke(&mut self, _value: Paint) { + unimplemented!("This is mocked") + } + } + pub struct Dom; impl Dom { - pub fn from_bytes(_bytes: &[u8], font_mgr: &FontMgr) -> Result { + pub fn from_bytes(_bytes: &[u8], provider: LocalResourceProvider) -> Result { unimplemented!("This is mocked") } @@ -1476,6 +1580,10 @@ pub mod svg { pub fn render(&self, _canvas: &Canvas) { unimplemented!("This is mocked") } + + pub fn root(&self) -> SvgNode { + unimplemented!("This is mocked") + } } } @@ -1741,3 +1849,11 @@ pub enum EncodedImageFormat { AVIF = 12, JPEGXL = 13, } + +pub struct LocalResourceProvider; + +impl LocalResourceProvider { + pub fn new(font_mgr: &FontMgr) -> Self { + unimplemented!("This is mocked") + } +} diff --git a/crates/engine/src/skia.rs b/crates/engine/src/skia.rs index c9c73a9b5..bd50c53e1 100644 --- a/crates/engine/src/skia.rs +++ b/crates/engine/src/skia.rs @@ -1,4 +1,5 @@ pub use skia_safe::{ + canvas::SaveLayerRec, font_style::{ Slant, Weight, @@ -24,6 +25,7 @@ pub use skia_safe::{ set_resource_cache_total_bytes_limit, }, path::ArcSize, + resources::LocalResourceProvider, rrect::Corner, runtime_effect::Uniform, surfaces::raster_n32_premul, @@ -55,7 +57,9 @@ pub use skia_safe::{ TextStyle, TypefaceFontProvider, }, + wrapper::PointerWrapper, Bitmap, + BlendMode, BlurStyle, Canvas, ClipOp, diff --git a/crates/freya/src/plugins/performance_overlay.rs b/crates/freya/src/plugins/performance_overlay.rs index 11d257808..3d4b79852 100644 --- a/crates/freya/src/plugins/performance_overlay.rs +++ b/crates/freya/src/plugins/performance_overlay.rs @@ -82,7 +82,7 @@ impl FreyaPlugin for PerformanceOverlayPlugin { // FPS add_text( &mut paragraph_builder, - format!("{} \n", self.frames.len()), + format!("{} FPS\n", self.frames.len()), 30.0, ); diff --git a/crates/hooks/Cargo.toml b/crates/hooks/Cargo.toml index 301dc163d..7c581176b 100644 --- a/crates/hooks/Cargo.toml +++ b/crates/hooks/Cargo.toml @@ -29,7 +29,7 @@ torin = { workspace = true } dioxus-hooks = { workspace = true } dioxus-signals = { workspace = true } dioxus-core = { workspace = true } -dioxus-sdk = { workspace = true } +dioxus-clipboard = { workspace = true } tokio = { workspace = true } winit = { workspace = true } diff --git a/crates/hooks/src/rope_editor.rs b/crates/hooks/src/rope_editor.rs index 017e95e0f..b46d992d3 100644 --- a/crates/hooks/src/rope_editor.rs +++ b/crates/hooks/src/rope_editor.rs @@ -4,7 +4,7 @@ use std::{ ops::Range, }; -use dioxus_sdk::clipboard::UseClipboard; +use dioxus_clipboard::prelude::UseClipboard; use ropey::iter::Lines; pub use ropey::Rope; diff --git a/crates/hooks/src/text_editor.rs b/crates/hooks/src/text_editor.rs index ad932c235..bbb48fdaa 100644 --- a/crates/hooks/src/text_editor.rs +++ b/crates/hooks/src/text_editor.rs @@ -5,7 +5,7 @@ use std::{ ops::Range, }; -use dioxus_sdk::clipboard::UseClipboard; +use dioxus_clipboard::prelude::UseClipboard; use freya_elements::events::keyboard::{ Code, Key, @@ -459,9 +459,8 @@ pub trait TextEditor { // Remove selected text let selection = self.get_selection_range(); if let Some((start, end)) = selection { - let cursor_pos = self.cursor_pos(); - let removed_text_len = self.remove(start..end); - self.set_cursor_pos(cursor_pos - removed_text_len); + self.remove(start..end); + self.set_cursor_pos(start); event.insert(TextEvent::TEXT_CHANGED); } diff --git a/crates/hooks/src/theming/base.rs b/crates/hooks/src/theming/base.rs index d8ab036ee..d9ba77a18 100644 --- a/crates/hooks/src/theming/base.rs +++ b/crates/hooks/src/theming/base.rs @@ -20,7 +20,9 @@ pub(crate) const BASE_THEME: Theme = Theme { focused_border: cow_borrowed!(""), solid: cow_borrowed!(""), color: cow_borrowed!(""), + primary_color: cow_borrowed!(""), placeholder_color: cow_borrowed!(""), + highlight_color: cow_borrowed!(""), }, body: BodyTheme { background: cow_borrowed!("key(background)"), @@ -48,6 +50,36 @@ pub(crate) const BASE_THEME: Theme = Theme { width: cow_borrowed!("auto"), height: cow_borrowed!("auto"), }, + filled_button: ButtonTheme { + background: cow_borrowed!("key(primary)"), + hover_background: cow_borrowed!("key(tertiary)"), + font_theme: FontTheme { + color: cow_borrowed!("key(primary_color)"), + }, + border_fill: cow_borrowed!("none"), + focus_border_fill: cow_borrowed!("key(focused_border)"), + shadow: cow_borrowed!("0 4 5 0 rgb(0, 0, 0, 0.1)"), + padding: cow_borrowed!("8 12"), + margin: cow_borrowed!("0"), + corner_radius: cow_borrowed!("8"), + width: cow_borrowed!("auto"), + height: cow_borrowed!("auto"), + }, + outline_button: ButtonTheme { + background: cow_borrowed!("key(background)"), + hover_background: cow_borrowed!("key(neutral_surface)"), + font_theme: FontTheme { + color: cow_borrowed!("key(primary)"), + }, + border_fill: cow_borrowed!("key(surface)"), + focus_border_fill: cow_borrowed!("key(secondary_opposite_surface)"), + shadow: cow_borrowed!("none"), + padding: cow_borrowed!("8 12"), + margin: cow_borrowed!("0"), + corner_radius: cow_borrowed!("8"), + width: cow_borrowed!("auto"), + height: cow_borrowed!("auto"), + }, input: InputTheme { background: cow_borrowed!("key(neutral_surface)"), hover_background: cow_borrowed!("key(focused_surface)"), @@ -55,7 +87,7 @@ pub(crate) const BASE_THEME: Theme = Theme { color: cow_borrowed!("key(color)"), }, placeholder_font_theme: FontTheme { - color: cow_borrowed!("rgb(100, 100, 100)"), + color: cow_borrowed!("key(placeholder_color)"), }, border_fill: cow_borrowed!("key(surface)"), width: cow_borrowed!("150"), @@ -94,7 +126,7 @@ pub(crate) const BASE_THEME: Theme = Theme { color: cow_borrowed!("key(color)"), }, border_fill: cow_borrowed!("key(surface)"), - arrow_fill: cow_borrowed!("rgb(40, 40, 40)"), + arrow_fill: cow_borrowed!("key(solid)"), }, dropdown_item: DropdownItemTheme { background: cow_borrowed!("key(background)"), @@ -105,7 +137,7 @@ pub(crate) const BASE_THEME: Theme = Theme { }, }, accordion: AccordionTheme { - color: cow_borrowed!("black"), + color: cow_borrowed!("key(color)"), background: cow_borrowed!("key(neutral_surface)"), border_fill: cow_borrowed!("key(surface)"), }, @@ -113,7 +145,7 @@ pub(crate) const BASE_THEME: Theme = Theme { primary_color: cow_borrowed!("key(tertiary_opposite_surface)"), }, link: LinkTheme { - highlight_color: cow_borrowed!("rgb(43,106,208)"), + highlight_color: cow_borrowed!("key(highlight_color)"), }, progress_bar: ProgressBarTheme { color: cow_borrowed!("white"), @@ -124,10 +156,10 @@ pub(crate) const BASE_THEME: Theme = Theme { }, table: TableTheme { font_theme: FontTheme { - color: cow_borrowed!("black"), + color: cow_borrowed!("key(color)"), }, background: cow_borrowed!("key(background)"), - arrow_fill: cow_borrowed!("rgb(40, 40, 40)"), + arrow_fill: cow_borrowed!("key(solid)"), row_background: cow_borrowed!("transparent"), alternate_row_background: cow_borrowed!("key(neutral_surface)"), divider_fill: cow_borrowed!("key(secondary_surface)"), @@ -200,7 +232,7 @@ pub(crate) const BASE_THEME: Theme = Theme { }, popup: PopupTheme { background: cow_borrowed!("key(background)"), - color: cow_borrowed!("black"), + color: cow_borrowed!("key(color)"), cross_fill: cow_borrowed!("key(solid)"), width: cow_borrowed!("350"), height: cow_borrowed!("200"), diff --git a/crates/hooks/src/theming/mod.rs b/crates/hooks/src/theming/mod.rs index ae3f32f7d..f06c02ae8 100644 --- a/crates/hooks/src/theming/mod.rs +++ b/crates/hooks/src/theming/mod.rs @@ -544,7 +544,9 @@ pub struct ColorsSheet { pub focused_border: Cow<'static, str>, pub solid: Cow<'static, str>, pub color: Cow<'static, str>, + pub primary_color: Cow<'static, str>, pub placeholder_color: Cow<'static, str>, + pub highlight_color: Cow<'static, str>, } impl ColorsSheet { @@ -566,7 +568,9 @@ impl ColorsSheet { "focused_border" => self.focused_border.clone(), "solid" => self.solid.clone(), "color" => self.color.clone(), + "primary_color" => self.primary_color.clone(), "placeholder_color" => self.placeholder_color.clone(), + "highlight_color" => self.highlight_color.clone(), _ => self.primary.clone(), } } else { @@ -581,6 +585,8 @@ pub struct Theme { pub colors: ColorsSheet, pub body: BodyTheme, pub button: ButtonTheme, + pub filled_button: ButtonTheme, + pub outline_button: ButtonTheme, pub switch: SwitchTheme, pub scroll_bar: ScrollBarTheme, pub slider: SliderTheme, diff --git a/crates/hooks/src/theming/themes.rs b/crates/hooks/src/theming/themes.rs index 289bec04e..a33609237 100644 --- a/crates/hooks/src/theming/themes.rs +++ b/crates/hooks/src/theming/themes.rs @@ -9,19 +9,21 @@ pub const DARK_THEME: Theme = Theme { colors: ColorsSheet { primary: cow_borrowed!("rgb(103, 80, 164)"), secondary: cow_borrowed!("rgb(202, 193, 227)"), - tertiary: cow_borrowed!("white"), + tertiary: cow_borrowed!("rgb(79, 61, 130)"), surface: cow_borrowed!("rgb(60, 60, 60)"), secondary_surface: cow_borrowed!("rgb(45, 45, 45)"), neutral_surface: cow_borrowed!("rgb(25, 25, 25)"), focused_surface: cow_borrowed!("rgb(15, 15, 15)"), - opposite_surface: cow_borrowed!("rgb(210, 210, 210)"), - secondary_opposite_surface: cow_borrowed!("rgb(225, 225, 225)"), - tertiary_opposite_surface: cow_borrowed!("rgb(235, 235, 235)"), + opposite_surface: cow_borrowed!("rgb(125, 125, 125)"), + secondary_opposite_surface: cow_borrowed!("rgb(150, 150, 150)"), + tertiary_opposite_surface: cow_borrowed!("rgb(170, 170, 170)"), background: cow_borrowed!("rgb(20, 20, 20)"), focused_border: cow_borrowed!("rgb(110, 110, 110)"), solid: cow_borrowed!("rgb(240, 240, 240)"), color: cow_borrowed!("rgb(250, 250, 250)"), + primary_color: cow_borrowed!("white"), placeholder_color: cow_borrowed!("rgb(210, 210, 210)"), + highlight_color: cow_borrowed!("rgb(96, 145, 224)"), }, ..BASE_THEME }; @@ -31,7 +33,7 @@ pub const LIGHT_THEME: Theme = Theme { colors: ColorsSheet { primary: cow_borrowed!("rgb(103, 80, 164)"), secondary: cow_borrowed!("rgb(202, 193, 227)"), - tertiary: cow_borrowed!("white"), + tertiary: cow_borrowed!("rgb(79, 61, 130)"), surface: cow_borrowed!("rgb(210, 210, 210)"), secondary_surface: cow_borrowed!("rgb(225, 225, 225)"), neutral_surface: cow_borrowed!("rgb(245, 245, 245)"), @@ -43,7 +45,9 @@ pub const LIGHT_THEME: Theme = Theme { solid: cow_borrowed!("rgb(35, 35, 35)"), focused_border: cow_borrowed!("rgb(180, 180, 180)"), color: cow_borrowed!("rgb(10, 10, 10)"), + primary_color: cow_borrowed!("white"), placeholder_color: cow_borrowed!("rgb(100, 100, 100)"), + highlight_color: cow_borrowed!("rgb(38, 89, 170)"), }, ..BASE_THEME }; @@ -53,7 +57,7 @@ pub const BANANA_THEME: Theme = Theme { colors: ColorsSheet { primary: cow_borrowed!("rgb(240, 200, 50)"), secondary: cow_borrowed!("rgb(255, 250, 160)"), - tertiary: cow_borrowed!("rgb(255, 255, 240)"), + tertiary: cow_borrowed!("rgb(186, 153, 37)"), surface: cow_borrowed!("rgb(240, 229, 189)"), secondary_surface: cow_borrowed!("rgb(250, 240, 210)"), neutral_surface: cow_borrowed!("rgb(255, 245, 220)"), @@ -65,7 +69,9 @@ pub const BANANA_THEME: Theme = Theme { solid: cow_borrowed!("rgb(110, 70, 10)"), focused_border: cow_borrowed!("rgb(255, 239, 151)"), color: cow_borrowed!("rgb(85, 60, 5)"), + primary_color: cow_borrowed!("rgb(69, 49, 7)"), placeholder_color: cow_borrowed!("rgb(56, 44, 5)"), + highlight_color: cow_borrowed!("rgb(143, 114, 6)"), }, ..BASE_THEME }; diff --git a/crates/hooks/src/use_activable_route.rs b/crates/hooks/src/use_activable_route.rs index 35c5a60ec..024b8fea8 100644 --- a/crates/hooks/src/use_activable_route.rs +++ b/crates/hooks/src/use_activable_route.rs @@ -13,6 +13,7 @@ impl ActivableRouteContext { } } +/// Consume an activable Route, use in combination with `ActivableRoute`. pub fn use_activable_route() -> bool { let ctx = try_use_context::(); diff --git a/crates/hooks/src/use_animation.rs b/crates/hooks/src/use_animation.rs index b36336ddc..5f3004184 100644 --- a/crates/hooks/src/use_animation.rs +++ b/crates/hooks/src/use_animation.rs @@ -379,6 +379,7 @@ pub struct Context { animated_values: Vec>>, on_finish: OnFinish, auto_start: bool, + on_deps_change: OnDepsChange, } impl Context { @@ -398,6 +399,11 @@ impl Context { self.auto_start = auto_start; self } + + pub fn on_deps_change(&mut self, on_deps_change: OnDepsChange) -> &mut Self { + self.on_deps_change = on_deps_change; + self + } } /// Controls the direction of the animation. @@ -425,6 +431,14 @@ pub enum OnFinish { Restart, } +/// What to do once the animation dependencies change. By default it is [`Reset`](OnDepsChange::Reset) +#[derive(PartialEq, Clone, Copy, Default)] +pub enum OnDepsChange { + #[default] + Reset, + Run, +} + /// Animate your elements. Use [`use_animation`] to use this. #[derive(PartialEq, Clone)] pub struct UseAnimator { @@ -446,8 +460,11 @@ impl UseAnimator { /// Reset the animation to the default state. pub fn reset(&self) { + let mut has_run_yet = self.has_run_yet; let mut task = self.task; + has_run_yet.set(false); + if let Some(task) = task.write().take() { task.cancel(); } @@ -721,8 +738,8 @@ where }; use_memo(move || { - let _ = value_and_ctx.read(); - if *has_run_yet.peek() { + let value_and_ctx = value_and_ctx.read(); + if *has_run_yet.peek() && value_and_ctx.1.on_deps_change == OnDepsChange::Run { animator.run_update() } }); diff --git a/crates/hooks/src/use_editable.rs b/crates/hooks/src/use_editable.rs index a2af7ce88..d2008489a 100644 --- a/crates/hooks/src/use_editable.rs +++ b/crates/hooks/src/use_editable.rs @@ -1,14 +1,14 @@ use std::rc::Rc; +use dioxus_clipboard::prelude::{ + use_clipboard, + UseClipboard, +}; use dioxus_core::{ prelude::spawn, use_hook, AttributeValue, }; -use dioxus_sdk::clipboard::{ - use_clipboard, - UseClipboard, -}; use dioxus_signals::{ Readable, Signal, diff --git a/crates/hooks/src/use_init_native_platform.rs b/crates/hooks/src/use_init_native_platform.rs index bb5dffd64..ce98be2d1 100644 --- a/crates/hooks/src/use_init_native_platform.rs +++ b/crates/hooks/src/use_init_native_platform.rs @@ -118,7 +118,7 @@ mod test { let mut utils = launch_test_with_config( use_focus_app, - TestingConfig { + TestingConfig::<()> { size: (100.0, 100.0).into(), ..TestingConfig::default() }, @@ -155,7 +155,6 @@ mod test { let focus = use_focus(); rsx!(rect { a11y_id: focus.attribute(), - a11y_role: "genericContainer", width: "100%", height: "50%", }) @@ -174,7 +173,7 @@ mod test { let mut utils = launch_test_with_config( use_focus_app, - TestingConfig { + TestingConfig::<()> { size: (100.0, 100.0).into(), ..TestingConfig::default() }, @@ -223,12 +222,10 @@ mod test { rsx!( rect { a11y_id: focus_1.attribute(), - a11y_role: "genericContainer", a11y_auto_focus: "true", } rect { a11y_id: focus_2.attribute(), - a11y_role: "genericContainer", a11y_auto_focus: "true", } ) @@ -236,7 +233,7 @@ mod test { let mut utils = launch_test_with_config( use_focus_app, - TestingConfig { + TestingConfig::<()> { size: (100.0, 100.0).into(), ..TestingConfig::default() }, diff --git a/crates/hooks/src/use_node.rs b/crates/hooks/src/use_node.rs index 366541eab..fb5716dab 100644 --- a/crates/hooks/src/use_node.rs +++ b/crates/hooks/src/use_node.rs @@ -90,7 +90,7 @@ mod test { let mut utils = launch_test_with_config( use_node_app, - TestingConfig { + TestingConfig::<()> { size: (500.0, 800.0).into(), ..TestingConfig::default() }, diff --git a/crates/hooks/tests/use_focus.rs b/crates/hooks/tests/use_focus.rs index d62545d7a..f6552a842 100644 --- a/crates/hooks/tests/use_focus.rs +++ b/crates/hooks/tests/use_focus.rs @@ -37,7 +37,7 @@ pub async fn track_focus() { let mut utils = launch_test_with_config( use_focus_app, - TestingConfig { + TestingConfig::<()> { size: (100.0, 100.0).into(), ..TestingConfig::default() }, @@ -117,7 +117,7 @@ pub async fn block_focus() { let mut utils = launch_test_with_config( use_focus_app, - TestingConfig { + TestingConfig::<()> { size: (100.0, 100.0).into(), ..TestingConfig::default() }, diff --git a/crates/hooks/tests/use_platform_information.rs b/crates/hooks/tests/use_platform_information.rs index da26688bf..4bd7bbc61 100644 --- a/crates/hooks/tests/use_platform_information.rs +++ b/crates/hooks/tests/use_platform_information.rs @@ -1,5 +1,6 @@ use dioxus::prelude::*; use freya::prelude::use_platform_information; +use freya_elements::elements as dioxus_elements; use freya_testing::prelude::*; #[tokio::test] @@ -14,7 +15,7 @@ async fn window_size() { let mut utils = launch_test_with_config( use_animation_app, - TestingConfig { + TestingConfig::<()> { size: (333.0, 190.0).into(), ..TestingConfig::default() }, diff --git a/crates/native-core-macro/src/lib.rs b/crates/native-core-macro/src/lib.rs index 0935dec47..f866deaa3 100644 --- a/crates/native-core-macro/src/lib.rs +++ b/crates/native-core-macro/src/lib.rs @@ -345,7 +345,7 @@ pub fn partial_derive_state(_: TokenStream, input: TokenStream) -> TokenStream { let (#(#split_views,)*) = data; let tree = run_view.tree.clone(); let node_types = run_view.node_type.clone(); - freya_native_core::prelude::run_pass(type_id, dependants.clone(), pass_direction, run_view, |id, context, height| { + freya_native_core::prelude::run_pass(type_id, &dependants, pass_direction, run_view, |id, context, height| { let node_data: &NodeType<_> = node_types.get(id).unwrap_or_else(|err| panic!("Failed to get node type {:?}", err)); if node_data.is_text() { return false; diff --git a/crates/native-core/Cargo.toml b/crates/native-core/Cargo.toml index 84be2247e..9ba0ec154 100644 --- a/crates/native-core/Cargo.toml +++ b/crates/native-core/Cargo.toml @@ -16,9 +16,7 @@ dioxus-core = { workspace = true} smallvec = { workspace = true } rustc-hash = { workspace = true } -anymap = "1.0.0-beta.2" parking_lot = { version = "0.12.1", features = ["send_guard"] } -dashmap = "6.0.0" shipyard = { workspace = true } [dev-dependencies] diff --git a/crates/native-core/src/attributes.rs b/crates/native-core/src/attributes.rs index 5f812cb41..8d27a9f5d 100644 --- a/crates/native-core/src/attributes.rs +++ b/crates/native-core/src/attributes.rs @@ -16,6 +16,8 @@ pub enum AttributeName { CornerRadius, CornerSmoothing, Color, + Fill, + Stroke, FontSize, FontFamily, FontStyle, @@ -33,6 +35,7 @@ pub enum AttributeName { DecorationColor, DecorationStyle, TextOverflow, + TextHeight, Rotate, Overflow, Margin, @@ -43,12 +46,6 @@ pub enum AttributeName { PositionLeft, Opacity, Content, - A11YAutoFocus, - A11YName, - A11YFocusable, - A11YRole, - A11YId, - A11YAlt, CanvasReference, Layer, OffsetY, @@ -67,7 +64,161 @@ pub enum AttributeName { SvgData, SvgContent, Spacing, - Scale + Scale, + + // Focus + A11yId, + A11yFocusable, + A11yAutoFocus, + + // Some internal notes about these accessibility attributes: + // + // - These are mostly derived from AccessKit's [`Node`] struct, with minor + // modifications to fit Freya's needs. These modifications are documented. + // + // - Some properties are commented out, meaning they are yet to be implemented. + // This is typically due to it being unclear how to represent these in Freya's + // attribute system (such as the association types, which will likely need + // some kind of ID system). + // + // - Any AccessKit properties that can be automatically calculated from style + // attributes or measured from torin are not included here, and are instead + // added in Freya's [`AccessibilityManager`] struct. + + // Vec associations + // A11yControls, + // A11yDetails, + // A11yDescribedBy, + // A11yFlowTo, + // A11yLabelledBy, + // A11yOwns, + // A11yRadioGroup, + + // NodeId associations + // ActiveDescendant, + // A11yErrorMessage, + // A11yInPageLinkTarget, + // A11yMemberOf, + // A11yNextOnLine, + // A11yPreviousOnLine, + // A11yPopupFor, + + // String + A11yName, + A11yDescription, + A11yValue, + A11yAccessKey, + A11yAuthorId, + // These three attributes are intended for assistive tech that parse MathML, + // which we don't support at the moment anyways. Unlikely to be implemented. + // A11yClassName, + // A11yHtmlTag, + // A11yInnerHtml, + A11yKeyboardShortcut, + A11yLanguage, + A11yPlaceholder, + A11yRoleDescription, + A11yStateDescription, + A11yTooltip, + A11yUrl, + A11yRowIndexText, + A11yColumnIndexText, + + // f64 + A11yScrollX, + A11yScrollXMin, + A11yScrollXMax, + A11yScrollY, + A11yScrollYMin, + A11yScrollYMax, + A11yNumericValue, + A11yMinNumericValue, + A11yMaxNumericValue, + A11yNumericValueStep, + A11yNumericValueJump, + + // usize + A11yRowCount, + A11yColumnCount, + A11yRowIndex, + A11yColumnIndex, + A11yRowSpan, + A11yColumnSpan, + A11yLevel, + A11ySizeOfSet, + A11yPositionInSet, + + // Color + A11yColorValue, + + // TODO: The following two categories are for inline text. They should be implemented + // automatically in [`AccessibilityManager`] based on Skia text measurement on text. + // spans. These really shouldn't be here (they should never have to be manually provided + // as an attribute), but I've left them here as a reminder to implement inline text data. + // + // See AccessKit's documentation for inline text measurements here: + // - + // + // Chromium also has a good writeup on how it measures inline text spans: + // - + + // LengthSlice + // A11yCharacterLengths, + // A11yWordLengths, + + // CoordSlice + // A11yCharacterPositions, + // A11yCharacterWidths, + + // bool + A11yExpanded, + A11ySelected, + + // bitflag + // TODO: This might be able to be determined automatically, + // but i'm not sure what ARIA property it corresponds to + // or its actual purpose. + A11yHovered, + A11yHidden, + A11yLinked, + A11yMultiselectable, + A11yRequired, + A11yVisited, + A11yBusy, + A11yLiveAtomic, + A11yModal, + A11yTouchTransparent, + A11yReadOnly, + A11yDisabled, + A11yIsSpellingError, + A11yIsGrammarError, + A11yIsSearchMatch, + A11yIsSuggestion, + + // Unique enums + A11yRole, + A11yInvalid, + A11yToggled, + A11yLive, + A11yDefaultActionVerb, + A11yOrientation, + A11ySortDirection, + A11yCurrent, // called AriaCurrent in accesskit, but that's a pretty poor name + A11yAutoComplete, + A11yHasPopup, + // This one is kind of weird to include, given it's reflecting a CSS property + // not in Freya for the HTML
    /
  • tags, but it can maybe be useful for + // language-specific semantics. + A11yListStyle, + A11yVerticalOffset, + // Other + // This could probably be inferred from Freya's text editing hook, but it's also + // a little strange in the data it expects. + // A11yTextSelection, + // A11yCustomActions, // Needs a special syntax or custom attribute value' + + // TODO: Some way to specify builtin AccessKit actions, as well as a way to + // handle actions in the form of an event. } impl FromStr for AttributeName { @@ -89,6 +240,8 @@ impl FromStr for AttributeName { "corner_radius" => Ok(AttributeName::CornerRadius), "corner_smoothing" => Ok(AttributeName::CornerSmoothing), "color" => Ok(AttributeName::Color), + "fill" => Ok(AttributeName::Fill), + "stroke" => Ok(AttributeName::Stroke), "font_size" => Ok(AttributeName::FontSize), "font_family" => Ok(AttributeName::FontFamily), "font_style" => Ok(AttributeName::FontStyle), @@ -106,6 +259,7 @@ impl FromStr for AttributeName { "decoration_color" => Ok(AttributeName::DecorationColor), "decoration_style" => Ok(AttributeName::DecorationStyle), "text_overflow" => Ok(AttributeName::TextOverflow), + "text_height" => Ok(AttributeName::TextHeight), "rotate" => Ok(AttributeName::Rotate), "overflow" => Ok(AttributeName::Overflow), "margin" => Ok(AttributeName::Margin), @@ -116,12 +270,6 @@ impl FromStr for AttributeName { "position_left" => Ok(AttributeName::PositionLeft), "opacity" => Ok(AttributeName::Opacity), "content" => Ok(AttributeName::Content), - "a11y_auto_focus" => Ok(AttributeName::A11YAutoFocus), - "a11y_name" => Ok(AttributeName::A11YName), - "a11y_role" => Ok(AttributeName::A11YRole), - "a11y_id" => Ok(AttributeName::A11YId), - "a11y_alt" => Ok(AttributeName::A11YAlt), - "a11y_focusable" => Ok(AttributeName::A11YFocusable), "canvas_reference" => Ok(AttributeName::CanvasReference), "layer" => Ok(AttributeName::Layer), "offset_y" => Ok(AttributeName::OffsetY), @@ -141,6 +289,74 @@ impl FromStr for AttributeName { "svg_content" => Ok(AttributeName::SvgContent), "spacing" => Ok(AttributeName::Spacing), "scale" => Ok(AttributeName::Scale), + "a11y_id" => Ok(AttributeName::A11yId), + "a11y_focusable" => Ok(AttributeName::A11yFocusable), + "a11y_auto_focus" => Ok(AttributeName::A11yAutoFocus), + "a11y_name" => Ok(AttributeName::A11yName), + "a11y_description" => Ok(AttributeName::A11yDescription), + "a11y_value" => Ok(AttributeName::A11yValue), + "a11y_access_key" => Ok(AttributeName::A11yAccessKey), + "a11y_author_id" => Ok(AttributeName::A11yAuthorId), + "a11y_keyboard_shortcut" => Ok(AttributeName::A11yKeyboardShortcut), + "a11y_language" => Ok(AttributeName::A11yLanguage), + "a11y_placeholder" => Ok(AttributeName::A11yPlaceholder), + "a11y_role_description" => Ok(AttributeName::A11yRoleDescription), + "a11y_state_description" => Ok(AttributeName::A11yStateDescription), + "a11y_tooltip" => Ok(AttributeName::A11yTooltip), + "a11y_url" => Ok(AttributeName::A11yUrl), + "a11y_row_index_text" => Ok(AttributeName::A11yRowIndexText), + "a11y_column_index_text" => Ok(AttributeName::A11yColumnIndexText), + "a11y_scroll_x" => Ok(AttributeName::A11yScrollX), + "a11y_scroll_x_min" => Ok(AttributeName::A11yScrollXMin), + "a11y_scroll_x_max" => Ok(AttributeName::A11yScrollXMax), + "a11y_scroll_y" => Ok(AttributeName::A11yScrollY), + "a11y_scroll_y_min" => Ok(AttributeName::A11yScrollYMin), + "a11y_scroll_y_max" => Ok(AttributeName::A11yScrollYMax), + "a11y_numeric_value" => Ok(AttributeName::A11yNumericValue), + "a11y_min_numeric_value" => Ok(AttributeName::A11yMinNumericValue), + "a11y_max_numeric_value" => Ok(AttributeName::A11yMaxNumericValue), + "a11y_numeric_value_step" => Ok(AttributeName::A11yNumericValueStep), + "a11y_numeric_value_jump" => Ok(AttributeName::A11yNumericValueJump), + "a11y_row_count" => Ok(AttributeName::A11yRowCount), + "a11y_column_count" => Ok(AttributeName::A11yColumnCount), + "a11y_row_index" => Ok(AttributeName::A11yRowIndex), + "a11y_column_index" => Ok(AttributeName::A11yColumnIndex), + "a11y_row_span" => Ok(AttributeName::A11yRowSpan), + "a11y_column_span" => Ok(AttributeName::A11yColumnSpan), + "a11y_level" => Ok(AttributeName::A11yLevel), + "a11y_size_of_set" => Ok(AttributeName::A11ySizeOfSet), + "a11y_position_in_set" => Ok(AttributeName::A11yPositionInSet), + "a11y_color_value" => Ok(AttributeName::A11yColorValue), + "a11y_expanded" => Ok(AttributeName::A11yExpanded), + "a11y_selected" => Ok(AttributeName::A11ySelected), + "a11y_hovered" => Ok(AttributeName::A11yHovered), + "a11y_hidden" => Ok(AttributeName::A11yHidden), + "a11y_linked" => Ok(AttributeName::A11yLinked), + "a11y_multiselectable" => Ok(AttributeName::A11yMultiselectable), + "a11y_required" => Ok(AttributeName::A11yRequired), + "a11y_visited" => Ok(AttributeName::A11yVisited), + "a11y_busy" => Ok(AttributeName::A11yBusy), + "a11y_live_atomic" => Ok(AttributeName::A11yLiveAtomic), + "a11y_modal" => Ok(AttributeName::A11yModal), + "a11y_touch_transparent" => Ok(AttributeName::A11yTouchTransparent), + "a11y_read_only" => Ok(AttributeName::A11yReadOnly), + "a11y_disabled" => Ok(AttributeName::A11yDisabled), + "a11y_is_spelling_error" => Ok(AttributeName::A11yIsSpellingError), + "a11y_is_grammar_error" => Ok(AttributeName::A11yIsGrammarError), + "a11y_is_search_match" => Ok(AttributeName::A11yIsSearchMatch), + "a11y_is_suggestion" => Ok(AttributeName::A11yIsSuggestion), + "a11y_role" => Ok(AttributeName::A11yRole), + "a11y_invalid" => Ok(AttributeName::A11yInvalid), + "a11y_toggled" => Ok(AttributeName::A11yToggled), + "a11y_live" => Ok(AttributeName::A11yLive), + "a11y_default_action_verb" => Ok(AttributeName::A11yDefaultActionVerb), + "a11y_orientation" => Ok(AttributeName::A11yOrientation), + "a11y_sort_direction" => Ok(AttributeName::A11ySortDirection), + "a11y_current" => Ok(AttributeName::A11yCurrent), + "a11y_auto_complete" => Ok(AttributeName::A11yAutoComplete), + "a11y_has_popup" => Ok(AttributeName::A11yHasPopup), + "a11y_list_style" => Ok(AttributeName::A11yListStyle), + "a11y_vertical_offset" => Ok(AttributeName::A11yVerticalOffset), _ => Err(format!("{attr} not supported.")), } } diff --git a/crates/native-core/src/lib.rs b/crates/native-core/src/lib.rs index 2501f2df1..0cf2b033b 100644 --- a/crates/native-core/src/lib.rs +++ b/crates/native-core/src/lib.rs @@ -1,10 +1,9 @@ -use std::{ - any::Any, - hash::BuildHasherDefault, +use std::any::{ + Any, + TypeId, }; use node_ref::NodeMask; -use rustc_hash::FxHasher; pub mod attributes; pub mod dioxus; @@ -16,6 +15,7 @@ pub mod real_dom; pub mod tags; pub mod tree; +use rustc_hash::FxHashMap; pub use shipyard::EntityId as NodeId; pub mod exports { @@ -65,9 +65,32 @@ pub mod prelude { }; } -/// A map that can be sent between threads -pub type FxDashMap = dashmap::DashMap>; -/// A set that can be sent between threads -pub type FxDashSet = dashmap::DashSet>; /// A map of types that can be sent between threads -pub type SendAnyMap = anymap::Map; +#[derive(Debug)] +pub struct SendAnyMap { + map: FxHashMap>, +} + +impl Default for SendAnyMap { + fn default() -> Self { + Self::new() + } +} + +impl SendAnyMap { + pub fn new() -> Self { + Self { + map: FxHashMap::default(), + } + } + + pub fn get(&self) -> Option<&T> { + self.map + .get(&TypeId::of::()) + .and_then(|any| any.downcast_ref::()) + } + + pub fn insert(&mut self, value: T) { + self.map.insert(TypeId::of::(), Box::new(value)); + } +} diff --git a/crates/native-core/src/passes.rs b/crates/native-core/src/passes.rs index 11b4ca049..65de22f60 100644 --- a/crates/native-core/src/passes.rs +++ b/crates/native-core/src/passes.rs @@ -33,10 +33,7 @@ use crate::{ NodeMaskBuilder, NodeView, }, - real_dom::{ - DirtyNodesResult, - SendAnyMapWrapper, - }, + real_dom::SendAnyMapWrapper, tree::{ TreeRef, TreeRefView, @@ -61,9 +58,8 @@ impl DirtyNodes { } pub fn pop(&mut self) -> Option { - self.nodes_dirty.iter().next().copied().map(|id| { - self.nodes_dirty.remove(&id); - id + self.nodes_dirty.iter().next().copied().inspect(|id| { + self.nodes_dirty.remove(id); }) } } @@ -213,7 +209,6 @@ fn pass_direction>() -> PassDirection pub struct RunPassView<'a, V: FromAnyValue + Send + Sync = ()> { pub tree: TreeRefView<'a>, pub node_type: View<'a, NodeType>, - dirty_nodes_result: UniqueView<'a, DirtyNodesResult>, node_states: UniqueView<'a, DirtyNodeStates>, any_map: UniqueView<'a, SendAnyMapWrapper>, } @@ -223,14 +218,13 @@ pub struct RunPassView<'a, V: FromAnyValue + Send + Sync = ()> { #[doc(hidden)] pub fn run_pass( type_id: TypeId, - dependants: Arc, + dependants: &Dependants, pass_direction: PassDirection, view: RunPassView, mut update_node: impl FnMut(NodeId, &SendAnyMap, u16) -> bool, ) { let RunPassView { tree, - dirty_nodes_result: nodes_updated, node_states: dirty, any_map: ctx, .. @@ -240,7 +234,6 @@ pub fn run_pass( PassDirection::ParentToChild => { while let Some((height, id)) = dirty.pop_front(type_id) { if (update_node)(id, ctx, height) { - nodes_updated.insert(id); dependants.mark_dirty(&dirty, id, &tree, height); } } @@ -248,7 +241,6 @@ pub fn run_pass( PassDirection::ChildToParent => { while let Some((height, id)) = dirty.pop_back(type_id) { if (update_node)(id, ctx, height) { - nodes_updated.insert(id); dependants.mark_dirty(&dirty, id, &tree, height); } } @@ -256,7 +248,6 @@ pub fn run_pass( PassDirection::AnyOrder => { while let Some((height, id)) = dirty.pop_back(type_id) { if (update_node)(id, ctx, height) { - nodes_updated.insert(id); dependants.mark_dirty(&dirty, id, &tree, height); } } diff --git a/crates/native-core/src/real_dom.rs b/crates/native-core/src/real_dom.rs index 996b1b7c6..a2d16e841 100644 --- a/crates/native-core/src/real_dom.rs +++ b/crates/native-core/src/real_dom.rs @@ -57,7 +57,6 @@ use crate::{ TreeRef, TreeRefView, }, - FxDashSet, NodeId, SendAnyMap, }; @@ -74,18 +73,6 @@ impl Deref for SendAnyMapWrapper { } } -/// The nodes that were changed when updating the state of the RealDom -#[derive(Unique, Default)] -pub(crate) struct DirtyNodesResult(FxDashSet); - -impl Deref for DirtyNodesResult { - type Target = FxDashSet; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - /// The nodes that have been marked as dirty in the RealDom pub(crate) struct NodesDirty { passes_updated: FxHashMap>, @@ -330,10 +317,7 @@ impl RealDom { } /// Update the state of the dom, after appling some mutations. This will keep the nodes in the dom up to date with their VNode counterparts. - pub fn update_state( - &mut self, - ctx: SendAnyMap, - ) -> (FxDashSet, FxHashMap) { + pub fn update_state(&mut self, ctx: SendAnyMap) -> FxHashMap { let passes = std::mem::take(&mut self.dirty_nodes.passes_updated); let nodes_updated = std::mem::take(&mut self.dirty_nodes.nodes_updated); @@ -353,13 +337,10 @@ impl RealDom { let _ = self.world.remove_unique::(); self.world.add_unique(dirty_nodes); self.world.add_unique(SendAnyMapWrapper(ctx)); - self.world.add_unique(DirtyNodesResult::default()); self.workload.run_with_world(&self.world).unwrap(); - let dirty = self.world.remove_unique::().unwrap(); - - (dirty.0, nodes_updated) + nodes_updated } /// Traverses the dom in a depth first manner, diff --git a/crates/renderer/src/app.rs b/crates/renderer/src/app.rs index 2f6899bde..dce9a35af 100644 --- a/crates/renderer/src/app.rs +++ b/crates/renderer/src/app.rs @@ -69,9 +69,8 @@ impl Application { fonts_config: EmbeddedFonts, plugins: PluginsManager, default_fonts: Vec, + accessibility: AccessKitManager, ) -> Self { - let accessibility = AccessKitManager::new(window, proxy.clone()); - let mut font_collection = FontCollection::new(); let def_mgr = FontMgr::default(); diff --git a/crates/renderer/src/config.rs b/crates/renderer/src/config.rs index 4466ac4a4..226cf6e4d 100644 --- a/crates/renderer/src/config.rs +++ b/crates/renderer/src/config.rs @@ -8,18 +8,23 @@ use freya_core::{ FreyaPlugin, PluginsManager, }, + prelude::EventMessage, style::default_fonts, }; use freya_engine::prelude::Color; use freya_node_state::Parse; use image::ImageReader; -use winit::window::{ - Icon, - Window, - WindowAttributes, +use winit::{ + event_loop::EventLoopBuilder, + window::{ + Icon, + Window, + WindowAttributes, + }, }; -pub type WindowBuilderHook = Box WindowAttributes>; +pub type EventLoopBuilderHook = Box)>; +pub type WindowBuilderHook = Box WindowAttributes>; pub type EmbeddedFonts<'a> = Vec<(&'a str, &'a [u8])>; /// Configuration for a Window. @@ -48,6 +53,8 @@ pub struct WindowConfig { pub on_exit: Option, /// Hook function called with the Window Attributes. pub window_attributes_hook: Option, + /// Hook function called with the Event Loop Builder. + pub event_loop_builder_hook: Option, } impl Default for WindowConfig { @@ -65,6 +72,7 @@ impl Default for WindowConfig { on_setup: None, on_exit: None, window_attributes_hook: None, + event_loop_builder_hook: None, } } } @@ -213,9 +221,18 @@ impl<'a, T: Clone> LaunchConfig<'a, T> { /// Register a Window Attributes hook. pub fn with_window_attributes( mut self, - window_attributes_hook: impl Fn(WindowAttributes) -> WindowAttributes + 'static, + window_attributes_hook: impl FnOnce(WindowAttributes) -> WindowAttributes + 'static, ) -> Self { self.window_config.window_attributes_hook = Some(Box::new(window_attributes_hook)); self } + + /// Register an Event Loop Builder hook. + pub fn with_event_loop_builder( + mut self, + event_loop_builder_hook: impl FnOnce(&mut EventLoopBuilder) + 'static, + ) -> Self { + self.window_config.event_loop_builder_hook = Some(Box::new(event_loop_builder_hook)); + self + } } diff --git a/crates/renderer/src/renderer.rs b/crates/renderer/src/renderer.rs index 49fd15fa3..d5796e5b7 100644 --- a/crates/renderer/src/renderer.rs +++ b/crates/renderer/src/renderer.rs @@ -71,11 +71,16 @@ impl<'a, State: Clone + 'static> DesktopRenderer<'a, State> { pub fn launch( vdom: VirtualDom, sdom: SafeDOM, - config: LaunchConfig, + mut config: LaunchConfig, devtools: Option, hovered_node: HoveredNode, ) { - let event_loop = EventLoop::::with_user_event() + let mut event_loop_builder = EventLoop::::with_user_event(); + let event_loop_builder_hook = config.window_config.event_loop_builder_hook.take(); + if let Some(event_loop_builder_hook) = event_loop_builder_hook { + event_loop_builder_hook(&mut event_loop_builder); + } + let event_loop = event_loop_builder .build() .expect("Failed to create event loop."); let proxy = event_loop.create_proxy(); diff --git a/crates/renderer/src/window_state.rs b/crates/renderer/src/window_state.rs index 57a136264..f017d2583 100644 --- a/crates/renderer/src/window_state.rs +++ b/crates/renderer/src/window_state.rs @@ -16,6 +16,7 @@ use winit::{ }; use crate::{ + accessibility::AccessKitManager, app::Application, config::WindowConfig, devtools::Devtools, @@ -94,13 +95,15 @@ impl<'a, State: Clone + 'a> WindowState<'a, State> { window_attributes.with_max_inner_size(LogicalSize::::from(max_size)); } - if let Some(with_window_attributes) = &config.window_config.window_attributes_hook { + if let Some(with_window_attributes) = config.window_config.window_attributes_hook.take() { window_attributes = (with_window_attributes)(window_attributes); } let (graphics_driver, window, mut surface) = GraphicsDriver::new(event_loop, window_attributes, &config); + let accessibility = AccessKitManager::new(&window, event_loop_proxy.clone()); + if config.window_config.visible { window.set_visible(true); } @@ -135,6 +138,7 @@ impl<'a, State: Clone + 'a> WindowState<'a, State> { config.embedded_fonts, config.plugins, config.default_fonts, + accessibility, ); app.init_doms(scale_factor as f32, config.state.clone()); diff --git a/crates/state/Cargo.toml b/crates/state/Cargo.toml index 998ff2daf..82f152a67 100644 --- a/crates/state/Cargo.toml +++ b/crates/state/Cargo.toml @@ -29,9 +29,10 @@ dioxus-core = { workspace = true } tokio = { workspace = true } accesskit = { workspace = true } shipyard = { workspace = true } -rustc-hash= { workspace = true } +rustc-hash = { workspace = true } tracing = { workspace = true } uuid = { workspace = true } bytes = "1.5.0" serde_json = "1.0.107" +nom = "7.1.3" diff --git a/crates/state/src/accessibility.rs b/crates/state/src/accessibility.rs index 1fb90b874..751ee14ca 100644 --- a/crates/state/src/accessibility.rs +++ b/crates/state/src/accessibility.rs @@ -4,13 +4,26 @@ use std::sync::{ }; use accesskit::{ + AriaCurrent, + AutoComplete, + DefaultActionVerb, + HasPopup, + Invalid, + ListStyle, + Live, + NodeBuilder, NodeId as AccessibilityId, + Orientation, Role, + SortDirection, + Toggled, + VerticalOffset, }; use freya_common::{ AccessibilityDirtyNodes, AccessibilityGenerator, }; +use freya_engine::prelude::Color; use freya_native_core::{ attributes::AttributeName, exports::shipyard::Component, @@ -22,6 +35,7 @@ use freya_native_core::{ NodeMaskBuilder, State, }, + tags::TagName, NodeId, SendAnyMap, }; @@ -35,17 +49,13 @@ use crate::{ ParseError, }; -#[derive(Clone, Debug, PartialEq, Eq, Default, Component)] +#[derive(Clone, Debug, PartialEq, Default, Component)] pub struct AccessibilityNodeState { - pub closest_accessibility_node_id: Option, - pub descencent_accessibility_ids: Vec, pub node_id: NodeId, pub a11y_id: Option, - pub a11y_role: Option, - pub a11y_alt: Option, - pub a11y_name: Option, pub a11y_auto_focus: bool, pub a11y_focusable: Focusable, + pub builder: Option, } impl ParseAttribute for AccessibilityNodeState { @@ -54,47 +64,248 @@ impl ParseAttribute for AccessibilityNodeState { attr: freya_native_core::prelude::OwnedAttributeView, ) -> Result<(), crate::ParseError> { match attr.attribute { - AttributeName::A11YId => { + AttributeName::A11yId => { if let OwnedAttributeValue::Custom(CustomAttributeValues::AccessibilityId(id)) = attr.value { self.a11y_id = Some(*id); - // Enable focus on nodes that pass a custom a11y id if self.a11y_focusable.is_unknown() { self.a11y_focusable = Focusable::Enabled; } } } - AttributeName::A11YRole => { + AttributeName::A11yFocusable => { if let OwnedAttributeValue::Text(attr) = attr.value { - self.a11y_role = Some( - serde_json::from_str::(&format!("\"{attr}\"")) - .map_err(|_| ParseError)?, - ) - } - } - AttributeName::A11YAlt => { - if let OwnedAttributeValue::Text(attr) = attr.value { - self.a11y_alt = Some(attr.to_owned()) - } - } - AttributeName::A11YName => { - if let OwnedAttributeValue::Text(attr) = attr.value { - self.a11y_name = Some(attr.to_owned()) + self.a11y_focusable = Focusable::parse(attr)?; } } - AttributeName::A11YAutoFocus => { + AttributeName::A11yAutoFocus => { if let OwnedAttributeValue::Text(attr) = attr.value { self.a11y_auto_focus = attr.parse().unwrap_or_default() } } - AttributeName::A11YFocusable => { + a11y_attr => { if let OwnedAttributeValue::Text(attr) = attr.value { - self.a11y_focusable = Focusable::parse(attr)?; + if let Some(builder) = self.builder.as_mut() { + match a11y_attr { + AttributeName::A11yName => builder.set_name(attr.clone()), + AttributeName::A11yDescription => builder.set_description(attr.clone()), + AttributeName::A11yValue => builder.set_value(attr.clone()), + AttributeName::A11yAccessKey => builder.set_access_key(attr.clone()), + AttributeName::A11yAuthorId => builder.set_author_id(attr.clone()), + AttributeName::A11yKeyboardShortcut => { + builder.set_keyboard_shortcut(attr.clone()) + } + AttributeName::A11yLanguage => builder.set_language(attr.clone()), + AttributeName::A11yPlaceholder => builder.set_placeholder(attr.clone()), + AttributeName::A11yRoleDescription => { + builder.set_role_description(attr.clone()) + } + AttributeName::A11yStateDescription => { + builder.set_state_description(attr.clone()) + } + AttributeName::A11yTooltip => builder.set_tooltip(attr.clone()), + AttributeName::A11yUrl => builder.set_url(attr.clone()), + AttributeName::A11yRowIndexText => { + builder.set_row_index_text(attr.clone()) + } + AttributeName::A11yColumnIndexText => { + builder.set_column_index_text(attr.clone()) + } + AttributeName::A11yScrollX => { + builder.set_scroll_x(attr.parse().map_err(|_| ParseError)?) + } + AttributeName::A11yScrollXMin => { + builder.set_scroll_x_min(attr.parse().map_err(|_| ParseError)?) + } + AttributeName::A11yScrollXMax => { + builder.set_scroll_x_max(attr.parse().map_err(|_| ParseError)?) + } + AttributeName::A11yScrollY => { + builder.set_scroll_y(attr.parse().map_err(|_| ParseError)?) + } + AttributeName::A11yScrollYMin => { + builder.set_scroll_y_min(attr.parse().map_err(|_| ParseError)?) + } + AttributeName::A11yScrollYMax => { + builder.set_scroll_y_max(attr.parse().map_err(|_| ParseError)?) + } + AttributeName::A11yNumericValue => { + builder.set_numeric_value(attr.parse().map_err(|_| ParseError)?) + } + AttributeName::A11yMinNumericValue => { + builder.set_min_numeric_value(attr.parse().map_err(|_| ParseError)?) + } + AttributeName::A11yMaxNumericValue => { + builder.set_max_numeric_value(attr.parse().map_err(|_| ParseError)?) + } + AttributeName::A11yNumericValueStep => builder + .set_numeric_value_step(attr.parse().map_err(|_| ParseError)?), + AttributeName::A11yNumericValueJump => builder + .set_numeric_value_jump(attr.parse().map_err(|_| ParseError)?), + AttributeName::A11yRowCount => { + builder.set_row_count(attr.parse().map_err(|_| ParseError)?) + } + AttributeName::A11yColumnCount => { + builder.set_column_count(attr.parse().map_err(|_| ParseError)?) + } + AttributeName::A11yRowIndex => { + builder.set_row_index(attr.parse().map_err(|_| ParseError)?) + } + AttributeName::A11yColumnIndex => { + builder.set_column_index(attr.parse().map_err(|_| ParseError)?) + } + AttributeName::A11yRowSpan => { + builder.set_row_span(attr.parse().map_err(|_| ParseError)?) + } + AttributeName::A11yColumnSpan => { + builder.set_column_span(attr.parse().map_err(|_| ParseError)?) + } + AttributeName::A11yLevel => { + builder.set_level(attr.parse().map_err(|_| ParseError)?) + } + AttributeName::A11ySizeOfSet => { + builder.set_size_of_set(attr.parse().map_err(|_| ParseError)?) + } + AttributeName::A11yPositionInSet => { + builder.set_position_in_set(attr.parse().map_err(|_| ParseError)?) + } + AttributeName::A11yColorValue => { + let color = Color::parse(attr)?; + builder.set_color_value( + ((color.a() as u32) << 24) + | ((color.b() as u32) << 16) + | (((color.g() as u32) << 8) + (color.r() as u32)), + ); + } + AttributeName::A11yExpanded => { + builder.set_expanded(attr.parse::().map_err(|_| ParseError)?); + } + AttributeName::A11ySelected => { + builder.set_selected(attr.parse::().map_err(|_| ParseError)?); + } + AttributeName::A11yHovered => { + if attr.parse::().map_err(|_| ParseError)? { + builder.set_hovered(); + } + } + AttributeName::A11yHidden => { + if attr.parse::().map_err(|_| ParseError)? { + builder.set_hidden(); + } + } + AttributeName::A11yLinked => { + if attr.parse::().map_err(|_| ParseError)? { + builder.set_linked(); + } + } + AttributeName::A11yMultiselectable => { + if attr.parse::().map_err(|_| ParseError)? { + builder.set_multiselectable(); + } + } + AttributeName::A11yRequired => { + if attr.parse::().map_err(|_| ParseError)? { + builder.set_required(); + } + } + AttributeName::A11yVisited => { + if attr.parse::().map_err(|_| ParseError)? { + builder.set_visited(); + } + } + AttributeName::A11yBusy => { + if attr.parse::().map_err(|_| ParseError)? { + builder.set_busy(); + } + } + AttributeName::A11yLiveAtomic => { + if attr.parse::().map_err(|_| ParseError)? { + builder.set_live_atomic(); + } + } + AttributeName::A11yModal => { + if attr.parse::().map_err(|_| ParseError)? { + builder.set_modal(); + } + } + AttributeName::A11yTouchTransparent => { + if attr.parse::().map_err(|_| ParseError)? { + builder.set_touch_transparent(); + } + } + AttributeName::A11yReadOnly => { + if attr.parse::().map_err(|_| ParseError)? { + builder.set_read_only(); + } + } + AttributeName::A11yDisabled => { + if attr.parse::().map_err(|_| ParseError)? { + builder.set_disabled(); + } + } + AttributeName::A11yIsSpellingError => { + if attr.parse::().map_err(|_| ParseError)? { + builder.set_is_spelling_error(); + } + } + AttributeName::A11yIsGrammarError => { + if attr.parse::().map_err(|_| ParseError)? { + builder.set_is_grammar_error(); + } + } + AttributeName::A11yIsSearchMatch => { + if attr.parse::().map_err(|_| ParseError)? { + builder.set_is_search_match(); + } + } + AttributeName::A11yIsSuggestion => { + if attr.parse::().map_err(|_| ParseError)? { + builder.set_is_suggestion(); + } + } + AttributeName::A11yRole => { + builder.set_role(Role::parse(attr)?); + } + AttributeName::A11yInvalid => { + builder.set_invalid(Invalid::parse(attr)?); + } + AttributeName::A11yToggled => { + builder.set_toggled(Toggled::parse(attr)?); + } + AttributeName::A11yLive => { + builder.set_live(Live::parse(attr)?); + } + AttributeName::A11yDefaultActionVerb => { + builder.set_default_action_verb(DefaultActionVerb::parse(attr)?); + } + AttributeName::A11yOrientation => { + builder.set_orientation(Orientation::parse(attr)?); + } + AttributeName::A11ySortDirection => { + builder.set_sort_direction(SortDirection::parse(attr)?); + } + AttributeName::A11yCurrent => { + builder.set_aria_current(AriaCurrent::parse(attr)?); + } + AttributeName::A11yAutoComplete => { + builder.set_auto_complete(AutoComplete::parse(attr)?); + } + AttributeName::A11yHasPopup => { + builder.set_has_popup(HasPopup::parse(attr)?); + } + AttributeName::A11yListStyle => { + builder.set_list_style(ListStyle::parse(attr)?); + } + AttributeName::A11yVerticalOffset => { + builder.set_vertical_offset(VerticalOffset::parse(attr)?); + } + _ => {} + } + } } } - _ => {} } Ok(()) @@ -103,27 +314,91 @@ impl ParseAttribute for AccessibilityNodeState { #[partial_derive_state] impl State for AccessibilityNodeState { - type ParentDependencies = (Self,); + type ParentDependencies = (); - type ChildDependencies = (Self,); + type ChildDependencies = (); type NodeDependencies = (); - const NODE_MASK: NodeMaskBuilder<'static> = - NodeMaskBuilder::new().with_attrs(AttributeMaskBuilder::Some(&[ - AttributeName::A11YId, - AttributeName::A11YRole, - AttributeName::A11YAlt, - AttributeName::A11YName, - AttributeName::A11YAutoFocus, - ])); + const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new() + .with_attrs(AttributeMaskBuilder::Some(&[ + AttributeName::A11yId, + AttributeName::A11yFocusable, + AttributeName::A11yAutoFocus, + AttributeName::A11yName, + AttributeName::A11yDescription, + AttributeName::A11yValue, + AttributeName::A11yAccessKey, + AttributeName::A11yAuthorId, + AttributeName::A11yKeyboardShortcut, + AttributeName::A11yLanguage, + AttributeName::A11yPlaceholder, + AttributeName::A11yRoleDescription, + AttributeName::A11yStateDescription, + AttributeName::A11yTooltip, + AttributeName::A11yUrl, + AttributeName::A11yRowIndexText, + AttributeName::A11yColumnIndexText, + AttributeName::A11yScrollX, + AttributeName::A11yScrollXMin, + AttributeName::A11yScrollXMax, + AttributeName::A11yScrollY, + AttributeName::A11yScrollYMin, + AttributeName::A11yScrollYMax, + AttributeName::A11yNumericValue, + AttributeName::A11yMinNumericValue, + AttributeName::A11yMaxNumericValue, + AttributeName::A11yNumericValueStep, + AttributeName::A11yNumericValueJump, + AttributeName::A11yRowCount, + AttributeName::A11yColumnCount, + AttributeName::A11yRowIndex, + AttributeName::A11yColumnIndex, + AttributeName::A11yRowSpan, + AttributeName::A11yColumnSpan, + AttributeName::A11yLevel, + AttributeName::A11ySizeOfSet, + AttributeName::A11yPositionInSet, + AttributeName::A11yColorValue, + AttributeName::A11yExpanded, + AttributeName::A11ySelected, + AttributeName::A11yHovered, + AttributeName::A11yHidden, + AttributeName::A11yLinked, + AttributeName::A11yMultiselectable, + AttributeName::A11yRequired, + AttributeName::A11yVisited, + AttributeName::A11yBusy, + AttributeName::A11yLiveAtomic, + AttributeName::A11yModal, + AttributeName::A11yTouchTransparent, + AttributeName::A11yReadOnly, + AttributeName::A11yDisabled, + AttributeName::A11yIsSpellingError, + AttributeName::A11yIsGrammarError, + AttributeName::A11yIsSearchMatch, + AttributeName::A11yIsSuggestion, + AttributeName::A11yRole, + AttributeName::A11yInvalid, + AttributeName::A11yToggled, + AttributeName::A11yLive, + AttributeName::A11yDefaultActionVerb, + AttributeName::A11yOrientation, + AttributeName::A11ySortDirection, + AttributeName::A11yCurrent, + AttributeName::A11yAutoComplete, + AttributeName::A11yHasPopup, + AttributeName::A11yListStyle, + AttributeName::A11yVerticalOffset, + ])) + .with_tag(); fn update<'a>( &mut self, node_view: NodeView, _node: ::ElementBorrowed<'a>, - parent: Option<::ElementBorrowed<'a>>, - children: Vec<::ElementBorrowed<'a>>, + _parent: Option<::ElementBorrowed<'a>>, + _children: Vec<::ElementBorrowed<'a>>, context: &SendAnyMap, ) -> bool { let root_id = context.get::().unwrap(); @@ -134,6 +409,18 @@ impl State for AccessibilityNodeState { let mut accessibility = AccessibilityNodeState { node_id: node_view.node_id(), a11y_id: self.a11y_id, + builder: node_view.tag().and_then(|tag| { + match tag { + TagName::Image => Some(NodeBuilder::new(Role::Image)), + TagName::Label => Some(NodeBuilder::new(Role::Label)), + TagName::Paragraph => Some(NodeBuilder::new(Role::Paragraph)), + TagName::Rect => Some(NodeBuilder::new(Role::GenericContainer)), + TagName::Svg => Some(NodeBuilder::new(Role::GraphicsObject)), + TagName::Root => Some(NodeBuilder::new(Role::Window)), + // TODO: make this InlineTextBox and supply computed text span properties + TagName::Text => None, + } + }), ..Default::default() }; @@ -143,36 +430,19 @@ impl State for AccessibilityNodeState { } } - for (child,) in children { - if let Some(child_id) = child.a11y_id { - // Mark this child as descendent if it has an ID - accessibility.descencent_accessibility_ids.push(child_id) - } else { - // If it doesn't have an ID then use its descencent accessibility IDs - accessibility - .descencent_accessibility_ids - .extend(child.descencent_accessibility_ids.iter()); - } - } - - if let Some(parent) = parent { - // Mark the parent accessibility ID as the closest to this node or - // fallback to its closest ID. - accessibility.closest_accessibility_node_id = parent - .0 - .a11y_id - .map(|_| parent.0.node_id) - .or(parent.0.closest_accessibility_node_id); - } - let changed = &accessibility != self; let had_id = self.a11y_id.is_some(); *self = accessibility; - if changed { - // Assign an accessibility ID if none was passed but the node has a role - if self.a11y_id.is_none() && self.a11y_role.is_some() { + let is_orphan = node_view.height() == 0 && node_view.node_id() != *root_id; + + if changed && !is_orphan { + // Assign an accessibility ID if none was passed but the node has a valid builder + // + // In our case, builder will be `None` if the node's tag cannot be added to accessibility + // tree. + if self.a11y_id.is_none() && self.builder.is_some() { let id = AccessibilityId(accessibility_generator.new_id()); #[cfg(debug_assertions)] tracing::info!("Assigned {id:?} to {:?}", node_view.node_id()); diff --git a/crates/state/src/cursor.rs b/crates/state/src/cursor.rs index 6dcb5bbc2..f629507a0 100644 --- a/crates/state/src/cursor.rs +++ b/crates/state/src/cursor.rs @@ -21,6 +21,7 @@ use freya_native_core::{ State, }, tags::TagName, + NodeId, SendAnyMap, }; use freya_native_core_macro::partial_derive_state; @@ -151,6 +152,7 @@ impl State for CursorState { _children: Vec<::ElementBorrowed<'a>>, context: &SendAnyMap, ) -> bool { + let root_id = context.get::().unwrap(); let paragraphs = context.get::>>().unwrap(); let compositor_dirty_nodes = context.get::>>().unwrap(); let mut cursor = parent.map(|(p,)| p.clone()).unwrap_or_default(); @@ -162,7 +164,9 @@ impl State for CursorState { } let changed = &cursor != self; - if changed && CursorMode::Editable == cursor.mode { + let is_orphan = node_view.height() == 0 && node_view.node_id() != *root_id; + + if changed && CursorMode::Editable == cursor.mode && !is_orphan { if let Some((tag, cursor_ref)) = node_view.tag().zip(cursor.cursor_ref.as_ref()) { if *tag == TagName::Paragraph { paragraphs diff --git a/crates/state/src/font_style.rs b/crates/state/src/font_style.rs index 54f215a0a..18c033488 100644 --- a/crates/state/src/font_style.rs +++ b/crates/state/src/font_style.rs @@ -26,6 +26,7 @@ use crate::{ ExtSplit, Parse, ParseAttribute, + TextHeight, TextOverflow, }; @@ -45,10 +46,16 @@ pub struct FontStyleState { pub text_align: TextAlign, pub max_lines: Option, pub text_overflow: TextOverflow, + pub text_height: TextHeightBehavior, } impl FontStyleState { - pub fn text_style(&self, default_font_family: &[String], scale_factor: f32) -> TextStyle { + pub fn text_style( + &self, + default_font_family: &[String], + scale_factor: f32, + paragraph_text_height: TextHeightBehavior, + ) -> TextStyle { let mut text_style = TextStyle::new(); let mut font_family = self.font_family.clone(); @@ -66,6 +73,11 @@ impl FontStyleState { .set_word_spacing(self.word_spacing) .set_letter_spacing(self.letter_spacing); + if paragraph_text_height.needs_custom_height() { + text_style.set_height_override(true); + text_style.set_half_leading(true); + } + if let Some(line_height) = self.line_height { text_style.set_height_override(true).set_height(line_height); } @@ -102,6 +114,7 @@ impl Default for FontStyleState { text_align: TextAlign::default(), max_lines: None, text_overflow: TextOverflow::default(), + text_height: TextHeightBehavior::DisableAll, } } } @@ -234,6 +247,14 @@ impl ParseAttribute for FontStyleState { } } } + AttributeName::TextHeight => { + let value = attr.value.as_text(); + if let Some(value) = value { + if let Ok(text_height) = TextHeightBehavior::parse(value) { + self.text_height = text_height; + } + } + } _ => {} } @@ -267,6 +288,7 @@ impl State for FontStyleState { AttributeName::DecorationColor, AttributeName::DecorationStyle, AttributeName::TextOverflow, + AttributeName::TextHeight, ])); fn update<'a>( @@ -277,6 +299,7 @@ impl State for FontStyleState { _children: Vec<::ElementBorrowed<'a>>, context: &SendAnyMap, ) -> bool { + let root_id = context.get::().unwrap(); let torin_layout = context.get::>>>().unwrap(); let compositor_dirty_nodes = context.get::>>().unwrap(); @@ -290,7 +313,9 @@ impl State for FontStyleState { let changed = &font_style != self; - if changed { + let is_orphan = node_view.height() == 0 && node_view.node_id() != *root_id; + + if changed && !is_orphan { torin_layout.lock().unwrap().invalidate(node_view.node_id()); compositor_dirty_nodes .lock() diff --git a/crates/state/src/layer.rs b/crates/state/src/layer.rs index fc5d7c616..24bed3c99 100644 --- a/crates/state/src/layer.rs +++ b/crates/state/src/layer.rs @@ -14,6 +14,7 @@ use freya_native_core::{ NodeMaskBuilder, State, }, + NodeId, SendAnyMap, }; use freya_native_core_macro::partial_derive_state; @@ -75,6 +76,7 @@ impl State for LayerState { return false; } + let root_id = context.get::().unwrap(); let layers = context.get::>>().unwrap(); let inherited_layer = parent.map(|(p,)| p.layer_for_children).unwrap_or(0i16); @@ -91,7 +93,9 @@ impl State for LayerState { let changed = &layer_state != self; - if changed { + let is_orphan = node_view.height() == 0 && node_view.node_id() != *root_id; + + if changed && !is_orphan { layers .lock() .unwrap() diff --git a/crates/state/src/layout.rs b/crates/state/src/layout.rs index 939ebcd01..753123534 100644 --- a/crates/state/src/layout.rs +++ b/crates/state/src/layout.rs @@ -221,6 +221,7 @@ impl State for LayoutState { _children: Vec<::ElementBorrowed<'a>>, context: &SendAnyMap, ) -> bool { + let root_id = context.get::().unwrap(); let torin_layout = context.get::>>>().unwrap(); let compositor_dirty_nodes = context.get::>>().unwrap(); @@ -237,7 +238,9 @@ impl State for LayoutState { let changed = layout != *self; - if changed { + let is_orphan = node_view.height() == 0 && node_view.node_id() != *root_id; + + if changed && !is_orphan { torin_layout.lock().unwrap().invalidate(node_view.node_id()); compositor_dirty_nodes .lock() diff --git a/crates/state/src/style.rs b/crates/state/src/style.rs index 978a57162..576337936 100644 --- a/crates/state/src/style.rs +++ b/crates/state/src/style.rs @@ -4,6 +4,7 @@ use std::sync::{ }; use freya_common::CompositorDirtyNodes; +use freya_engine::prelude::Color; use freya_native_core::{ attributes::AttributeName, exports::shipyard::Component, @@ -36,6 +37,8 @@ use crate::{ #[derive(Default, Debug, Clone, PartialEq, Component)] pub struct StyleState { pub background: Fill, + pub svg_fill: Option, + pub svg_stroke: Option, pub borders: Vec, pub shadows: Vec, pub corner_radius: CornerRadius, @@ -58,6 +61,22 @@ impl ParseAttribute for StyleState { self.background = Fill::parse(value)?; } } + AttributeName::Fill => { + if let Some(value) = attr.value.as_text() { + if value == "none" { + return Ok(()); + } + self.svg_stroke = Some(Color::parse(value)?); + } + } + AttributeName::Stroke => { + if let Some(value) = attr.value.as_text() { + if value == "none" { + return Ok(()); + } + self.svg_fill = Some(Color::parse(value)?); + } + } AttributeName::Border => { if let Some(value) = attr.value.as_text() { self.borders = value @@ -132,6 +151,8 @@ impl State for StyleState { const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new().with_attrs(AttributeMaskBuilder::Some(&[ AttributeName::Background, + AttributeName::Fill, + AttributeName::Stroke, AttributeName::Layer, AttributeName::Border, AttributeName::Shadow, diff --git a/crates/state/src/transform.rs b/crates/state/src/transform.rs index 111740233..2020aea0c 100644 --- a/crates/state/src/transform.rs +++ b/crates/state/src/transform.rs @@ -93,6 +93,7 @@ impl State for TransformState { _children: Vec<::ElementBorrowed<'a>>, context: &SendAnyMap, ) -> bool { + let root_id = context.get::().unwrap(); let compositor_dirty_nodes = context.get::>>().unwrap(); let inherited_transform = parent.map(|(p,)| p.clone()).unwrap_or_default(); @@ -109,7 +110,9 @@ impl State for TransformState { let changed = transform_state != *self; - if changed { + let is_orphan = node_view.height() == 0 && node_view.node_id() != *root_id; + + if changed && !is_orphan { compositor_dirty_nodes .lock() .unwrap() diff --git a/crates/state/src/values/accessibility.rs b/crates/state/src/values/accessibility.rs new file mode 100644 index 000000000..35101fffb --- /dev/null +++ b/crates/state/src/values/accessibility.rs @@ -0,0 +1,349 @@ +use accesskit::{ + AriaCurrent, + AutoComplete, + DefaultActionVerb, + HasPopup, + Invalid, + ListStyle, + Live, + Orientation, + Role, + SortDirection, + Toggled, + VerticalOffset, +}; + +use crate::{ + Parse, + ParseError, +}; + +impl Parse for Role { + fn parse(value: &str) -> Result { + Ok(match value { + "unknown" => Self::Unknown, + "inline-text-box" => Self::InlineTextBox, + "cell" => Self::Cell, + "label" => Self::Label, + "image" => Self::Image, + "link" => Self::Link, + "row" => Self::Row, + "list-item" => Self::ListItem, + "list-marker" => Self::ListMarker, + "tree-item" => Self::TreeItem, + "list-box-option" => Self::ListBoxOption, + "menu-item" => Self::MenuItem, + "menu-list-option" => Self::MenuListOption, + "paragraph" => Self::Paragraph, + "generic-container" => Self::GenericContainer, + "check-box" => Self::CheckBox, + "radio-button" => Self::RadioButton, + "text-input" => Self::TextInput, + "button" => Self::Button, + "default-button" => Self::DefaultButton, + "pane" => Self::Pane, + "row-header" => Self::RowHeader, + "column-header" => Self::ColumnHeader, + "row-group" => Self::RowGroup, + "list" => Self::List, + "table" => Self::Table, + "layout-table-cell" => Self::LayoutTableCell, + "layout-table-row" => Self::LayoutTableRow, + "layout-table" => Self::LayoutTable, + "switch" => Self::Switch, + "menu" => Self::Menu, + "multiline-text-input" => Self::MultilineTextInput, + "search-input" => Self::SearchInput, + "date-input" => Self::DateInput, + "date-time-input" => Self::DateTimeInput, + "week-input" => Self::WeekInput, + "month-input" => Self::MonthInput, + "time-input" => Self::TimeInput, + "email-input" => Self::EmailInput, + "number-input" => Self::NumberInput, + "password-input" => Self::PasswordInput, + "phone-number-input" => Self::PhoneNumberInput, + "url-input" => Self::UrlInput, + "abbr" => Self::Abbr, + "alert" => Self::Alert, + "alert-dialog" => Self::AlertDialog, + "application" => Self::Application, + "article" => Self::Article, + "audio" => Self::Audio, + "banner" => Self::Banner, + "blockquote" => Self::Blockquote, + "canvas" => Self::Canvas, + "caption" => Self::Caption, + "caret" => Self::Caret, + "code" => Self::Code, + "color-well" => Self::ColorWell, + "combo-box" => Self::ComboBox, + "editable-combo-box" => Self::EditableComboBox, + "complementary" => Self::Complementary, + "comment" => Self::Comment, + "content-deletion" => Self::ContentDeletion, + "content-insertion" => Self::ContentInsertion, + "content-info" => Self::ContentInfo, + "definition" => Self::Definition, + "description-list" => Self::DescriptionList, + "description-list-detail" => Self::DescriptionListDetail, + "description-list-term" => Self::DescriptionListTerm, + "details" => Self::Details, + "dialog" => Self::Dialog, + "directory" => Self::Directory, + "disclosure-triangle" => Self::DisclosureTriangle, + "document" => Self::Document, + "embedded-object" => Self::EmbeddedObject, + "emphasis" => Self::Emphasis, + "feed" => Self::Feed, + "figure-caption" => Self::FigureCaption, + "figure" => Self::Figure, + "footer" => Self::Footer, + "footer-as-non-landmark" => Self::FooterAsNonLandmark, + "form" => Self::Form, + "grid" => Self::Grid, + "group" => Self::Group, + "header" => Self::Header, + "header-as-non-landmark" => Self::HeaderAsNonLandmark, + "heading" => Self::Heading, + "iframe" => Self::Iframe, + "iframe-presentational" => Self::IframePresentational, + "ime-candidate" => Self::ImeCandidate, + "keyboard" => Self::Keyboard, + "legend" => Self::Legend, + "line-break" => Self::LineBreak, + "list-box" => Self::ListBox, + "log" => Self::Log, + "main" => Self::Main, + "mark" => Self::Mark, + "marquee" => Self::Marquee, + "math" => Self::Math, + "menu-bar" => Self::MenuBar, + "menu-item-check-box" => Self::MenuItemCheckBox, + "menu-item-radio" => Self::MenuItemRadio, + "menu-list-popup" => Self::MenuListPopup, + "meter" => Self::Meter, + "navigation" => Self::Navigation, + "note" => Self::Note, + "plugin-object" => Self::PluginObject, + "portal" => Self::Portal, + "pre" => Self::Pre, + "progress-indicator" => Self::ProgressIndicator, + "radio-group" => Self::RadioGroup, + "region" => Self::Region, + "root-web-area" => Self::RootWebArea, + "ruby" => Self::Ruby, + "ruby-annotation" => Self::RubyAnnotation, + "scroll-bar" => Self::ScrollBar, + "scroll-view" => Self::ScrollView, + "search" => Self::Search, + "section" => Self::Section, + "slider" => Self::Slider, + "spin-button" => Self::SpinButton, + "splitter" => Self::Splitter, + "status" => Self::Status, + "strong" => Self::Strong, + "suggestion" => Self::Suggestion, + "svg-root" => Self::SvgRoot, + "tab" => Self::Tab, + "tab-list" => Self::TabList, + "tab-panel" => Self::TabPanel, + "term" => Self::Term, + "time" => Self::Time, + "timer" => Self::Timer, + "title-bar" => Self::TitleBar, + "toolbar" => Self::Toolbar, + "tooltip" => Self::Tooltip, + "tree" => Self::Tree, + "tree-grid" => Self::TreeGrid, + "video" => Self::Video, + "web-view" => Self::WebView, + "window" => Self::Window, + "pdf-actionable-highlight" => Self::PdfActionableHighlight, + "pdf-root" => Self::PdfRoot, + "graphics-document" => Self::GraphicsDocument, + "graphics-object" => Self::GraphicsObject, + "graphics-symbol" => Self::GraphicsSymbol, + "doc-abstract" => Self::DocAbstract, + "doc-acknowledgements" => Self::DocAcknowledgements, + "doc-afterword" => Self::DocAfterword, + "doc-appendix" => Self::DocAppendix, + "doc-back-link" => Self::DocBackLink, + "doc-biblio-entry" => Self::DocBiblioEntry, + "doc-bibliography" => Self::DocBibliography, + "doc-biblio-ref" => Self::DocBiblioRef, + "doc-chapter" => Self::DocChapter, + "doc-colophon" => Self::DocColophon, + "doc-conclusion" => Self::DocConclusion, + "doc-cover" => Self::DocCover, + "doc-credit" => Self::DocCredit, + "doc-credits" => Self::DocCredits, + "doc-dedication" => Self::DocDedication, + "doc-endnote" => Self::DocEndnote, + "doc-endnotes" => Self::DocEndnotes, + "doc-epigraph" => Self::DocEpigraph, + "doc-epilogue" => Self::DocEpilogue, + "doc-errata" => Self::DocErrata, + "doc-example" => Self::DocExample, + "doc-footnote" => Self::DocFootnote, + "doc-foreword" => Self::DocForeword, + "doc-glossary" => Self::DocGlossary, + "doc-gloss-ref" => Self::DocGlossRef, + "doc-index" => Self::DocIndex, + "doc-introduction" => Self::DocIntroduction, + "doc-note-ref" => Self::DocNoteRef, + "doc-notice" => Self::DocNotice, + "doc-page-break" => Self::DocPageBreak, + "doc-page-footer" => Self::DocPageFooter, + "doc-page-header" => Self::DocPageHeader, + "doc-page-list" => Self::DocPageList, + "doc-part" => Self::DocPart, + "doc-preface" => Self::DocPreface, + "doc-prologue" => Self::DocPrologue, + "doc-pullquote" => Self::DocPullquote, + "doc-qna" => Self::DocQna, + "doc-subtitle" => Self::DocSubtitle, + "doc-tip" => Self::DocTip, + "doc-toc" => Self::DocToc, + "list-grid" => Self::ListGrid, + "terminal" => Self::Terminal, + _ => Err(ParseError)?, + }) + } +} + +impl Parse for Invalid { + fn parse(value: &str) -> Result { + Ok(match value { + "true" => Invalid::True, + "grammar" => Invalid::Grammar, + "spelling" => Invalid::Spelling, + _ => Err(ParseError)?, + }) + } +} + +impl Parse for Toggled { + fn parse(value: &str) -> Result { + Ok(match value { + "true" => Toggled::True, + "false" => Toggled::False, + "mixed" => Toggled::Mixed, + _ => Err(ParseError)?, + }) + } +} + +impl Parse for Live { + fn parse(value: &str) -> Result { + Ok(match value { + "assertive" => Live::Assertive, + "off" => Live::Off, + "polite" => Live::Polite, + _ => Err(ParseError)?, + }) + } +} + +impl Parse for DefaultActionVerb { + fn parse(value: &str) -> Result { + Ok(match value { + "click" => DefaultActionVerb::Click, + "focus" => DefaultActionVerb::Focus, + "check" => DefaultActionVerb::Check, + "uncheck" => DefaultActionVerb::Uncheck, + "click-ancestor" => DefaultActionVerb::ClickAncestor, + "jump" => DefaultActionVerb::Jump, + "open" => DefaultActionVerb::Open, + "press" => DefaultActionVerb::Press, + "select" => DefaultActionVerb::Select, + "unselect" => DefaultActionVerb::Unselect, + _ => Err(ParseError)?, + }) + } +} + +impl Parse for Orientation { + fn parse(value: &str) -> Result { + Ok(match value { + "horizontal" => Orientation::Horizontal, + "vertical" => Orientation::Vertical, + _ => Err(ParseError)?, + }) + } +} + +impl Parse for SortDirection { + fn parse(value: &str) -> Result { + Ok(match value { + "ascending" => SortDirection::Ascending, + "descending" => SortDirection::Descending, + "other" => SortDirection::Other, + _ => Err(ParseError)?, + }) + } +} + +impl Parse for AriaCurrent { + fn parse(value: &str) -> Result { + Ok(match value { + "false" => AriaCurrent::False, + "true" => AriaCurrent::True, + "page" => AriaCurrent::Page, + "step" => AriaCurrent::Step, + "location" => AriaCurrent::Location, + "date" => AriaCurrent::Date, + "time" => AriaCurrent::Time, + _ => Err(ParseError)?, + }) + } +} + +impl Parse for AutoComplete { + fn parse(value: &str) -> Result { + Ok(match value { + "inline" => AutoComplete::Inline, + "list" => AutoComplete::List, + "both" => AutoComplete::Both, + _ => Err(ParseError)?, + }) + } +} + +impl Parse for HasPopup { + fn parse(value: &str) -> Result { + Ok(match value { + "true" => HasPopup::True, + "menu" => HasPopup::Menu, + "listbox" => HasPopup::Listbox, + "tree" => HasPopup::Tree, + "grid" => HasPopup::Grid, + "dialog" => HasPopup::Dialog, + _ => Err(ParseError)?, + }) + } +} + +impl Parse for ListStyle { + fn parse(value: &str) -> Result { + Ok(match value { + "circle" => ListStyle::Circle, + "disc" => ListStyle::Disc, + "image" => ListStyle::Image, + "numeric" => ListStyle::Numeric, + "square" => ListStyle::Square, + "other" => ListStyle::Other, + _ => Err(ParseError)?, + }) + } +} + +impl Parse for VerticalOffset { + fn parse(value: &str) -> Result { + Ok(match value { + "subscript" => VerticalOffset::Subscript, + "superscript" => VerticalOffset::Superscript, + _ => Err(ParseError)?, + }) + } +} diff --git a/crates/state/src/values/content.rs b/crates/state/src/values/content.rs index 7cd26f0a1..099599ba5 100644 --- a/crates/state/src/values/content.rs +++ b/crates/state/src/values/content.rs @@ -9,6 +9,7 @@ impl Parse for Content { fn parse(value: &str) -> Result { Ok(match value { "fit" => Content::Fit, + "flex" => Content::Flex, _ => Content::Normal, }) } diff --git a/crates/state/src/values/fill.rs b/crates/state/src/values/fill.rs index bf285bbf5..84556f517 100644 --- a/crates/state/src/values/fill.rs +++ b/crates/state/src/values/fill.rs @@ -1,6 +1,10 @@ use std::fmt; -use freya_engine::prelude::Color; +use freya_engine::prelude::{ + Color, + Paint, +}; +use torin::prelude::Area; use crate::{ ConicGradient, @@ -19,6 +23,25 @@ pub enum Fill { ConicGradient(ConicGradient), } +impl Fill { + pub fn apply_to_paint(&self, paint: &mut Paint, area: Area) { + match &self { + Fill::Color(color) => { + paint.set_color(*color); + } + Fill::LinearGradient(gradient) => { + paint.set_shader(gradient.into_shader(area)); + } + Fill::RadialGradient(gradient) => { + paint.set_shader(gradient.into_shader(area)); + } + Fill::ConicGradient(gradient) => { + paint.set_shader(gradient.into_shader(area)); + } + } + } +} + impl Default for Fill { fn default() -> Self { Self::Color(Color::default()) diff --git a/crates/state/src/values/gradient.rs b/crates/state/src/values/gradient.rs index 5295a0e38..b3202ae43 100644 --- a/crates/state/src/values/gradient.rs +++ b/crates/state/src/values/gradient.rs @@ -148,7 +148,7 @@ impl RadialGradient { Shader::radial_gradient( Point::new(center.x, center.y), - bounds.width().max(bounds.height()), + bounds.width().max(bounds.height()) / 2.0, GradientShaderColors::Colors(&colors[..]), Some(&offsets[..]), TileMode::Clamp, diff --git a/crates/state/src/values/mod.rs b/crates/state/src/values/mod.rs index 4f2b435e4..83d564b48 100644 --- a/crates/state/src/values/mod.rs +++ b/crates/state/src/values/mod.rs @@ -1,3 +1,4 @@ +mod accessibility; mod alignment; mod border; mod color; @@ -15,6 +16,7 @@ mod overflow; mod position; mod shadow; mod size; +mod text_height; mod text_shadow; pub use border::*; @@ -29,3 +31,4 @@ pub use highlight::*; pub use overflow::*; pub use shadow::*; pub use size::*; +pub use text_height::*; diff --git a/crates/state/src/values/size.rs b/crates/state/src/values/size.rs index ae856e511..4caf55ddd 100644 --- a/crates/state/src/values/size.rs +++ b/crates/state/src/values/size.rs @@ -1,3 +1,16 @@ +use nom::{ + branch::alt, + bytes::complete::tag, + character::complete::multispace0, + combinator::map, + multi::many1, + number::complete::float, + sequence::{ + preceded, + tuple, + }, + IResult, +}; use torin::{ geometry::Length, size::{ @@ -15,6 +28,16 @@ impl Parse for Size { fn parse(value: &str) -> Result { if value == "auto" { Ok(Size::Inner) + } else if value == "flex" { + Ok(Size::Flex(Length::new(1.0))) + } else if value.contains("flex") { + Ok(Size::Flex(Length::new( + value + .replace("flex(", "") + .replace(')', "") + .parse::() + .map_err(|_| ParseError)?, + ))) } else if value == "fill" { Ok(Size::Fill) } else if value == "fill-min" { @@ -51,39 +74,34 @@ impl Parse for Size { } pub fn parse_calc(mut value: &str) -> Result, ParseError> { - let mut calcs = Vec::new(); + // No need to parse this using nom value = value .strip_prefix("calc(") .ok_or(ParseError)? .strip_suffix(')') .ok_or(ParseError)?; - - let values = value.split_whitespace(); - - for val in values { - if val.contains('%') { - calcs.push(DynamicCalculation::Percentage( - val.replace('%', "").parse().map_err(|_| ParseError)?, - )); - } else if val.contains('v') { - calcs.push(DynamicCalculation::RootPercentage( - val.replace('v', "").parse().map_err(|_| ParseError)?, - )); - } else if val == "+" { - calcs.push(DynamicCalculation::Add); - } else if val == "-" { - calcs.push(DynamicCalculation::Sub); - } else if val == "/" { - calcs.push(DynamicCalculation::Div); - } else if val == "*" { - calcs.push(DynamicCalculation::Mul); - } else { - calcs.push(DynamicCalculation::Pixels( - val.parse::().map_err(|_| ParseError)?, - )); - } + fn inner_parse(value: &str) -> IResult<&str, Vec> { + many1(preceded( + multispace0, + alt(( + map(tag("+"), |_| DynamicCalculation::Add), + map(tag("-"), |_| DynamicCalculation::Sub), + map(tag("*"), |_| DynamicCalculation::Mul), + map(tag("/"), |_| DynamicCalculation::Div), + map(tag("("), |_| DynamicCalculation::OpenParenthesis), + map(tag(")"), |_| DynamicCalculation::ClosedParenthesis), + map(tuple((float, tag("%"))), |(v, _)| { + DynamicCalculation::Percentage(v) + }), + map(tuple((float, tag("v"))), |(v, _)| { + DynamicCalculation::RootPercentage(v) + }), + map(float, DynamicCalculation::Pixels), + )), + ))(value) } + let tokens = inner_parse(value).map_err(|_| ParseError)?.1; - Ok(calcs) + Ok(tokens) } diff --git a/crates/state/src/values/text_height.rs b/crates/state/src/values/text_height.rs new file mode 100644 index 000000000..4977b383d --- /dev/null +++ b/crates/state/src/values/text_height.rs @@ -0,0 +1,31 @@ +use freya_engine::prelude::*; + +use crate::{ + Parse, + ParseError, +}; + +impl Parse for TextHeightBehavior { + fn parse(value: &str) -> Result { + match value { + "all" => Ok(TextHeightBehavior::All), + "disable-first-ascent" => Ok(TextHeightBehavior::DisableFirstAscent), + "disable-least-ascent" => Ok(TextHeightBehavior::DisableLastDescent), + "disable-all" => Ok(TextHeightBehavior::DisableAll), + _ => Err(ParseError), + } + } +} + +pub trait TextHeight { + fn needs_custom_height(&self) -> bool; +} + +impl TextHeight for TextHeightBehavior { + fn needs_custom_height(&self) -> bool { + matches!( + self, + Self::All | Self::DisableFirstAscent | Self::DisableLastDescent + ) + } +} diff --git a/crates/state/tests/parse_focusable.rs b/crates/state/tests/parse_focusable.rs index 62ae1d9c9..27832e9a7 100644 --- a/crates/state/tests/parse_focusable.rs +++ b/crates/state/tests/parse_focusable.rs @@ -1,4 +1,3 @@ -use freya_engine::prelude::*; use freya_node_state::{ Focusable, Parse, diff --git a/crates/state/tests/parse_size.rs b/crates/state/tests/parse_size.rs index 4e4a90084..d24129b42 100644 --- a/crates/state/tests/parse_size.rs +++ b/crates/state/tests/parse_size.rs @@ -27,7 +27,7 @@ fn parse_auto_size() { #[test] fn parse_calc_size() { - let size = Size::parse("calc(90% - 5% * 123.6 / 50v)"); + let size = Size::parse("calc(90%- 5%* 123.6/ 50v(5 + 6))"); assert_eq!( size, Ok(Size::DynamicCalculations(Box::new(vec![ @@ -37,7 +37,12 @@ fn parse_calc_size() { DynamicCalculation::Mul, DynamicCalculation::Pixels(123.6), DynamicCalculation::Div, - DynamicCalculation::RootPercentage(50.0) + DynamicCalculation::RootPercentage(50.0), + DynamicCalculation::OpenParenthesis, + DynamicCalculation::Pixels(5.0), + DynamicCalculation::Add, + DynamicCalculation::Pixels(6.0), + DynamicCalculation::ClosedParenthesis, ]))) ); } diff --git a/crates/testing/src/config.rs b/crates/testing/src/config.rs index 3b9d8d8bc..009c858a9 100644 --- a/crates/testing/src/config.rs +++ b/crates/testing/src/config.rs @@ -3,24 +3,26 @@ use std::time::Duration; use torin::geometry::Size2D; /// Configuration for [`crate::test_handler::TestingHandler`]. -#[derive(Clone, Copy)] -pub struct TestingConfig { +#[derive(Clone)] +pub struct TestingConfig { pub vdom_timeout: Duration, pub size: Size2D, pub event_loop_ticker: bool, + pub state: Option, } -impl Default for TestingConfig { +impl Default for TestingConfig { fn default() -> Self { Self { vdom_timeout: Duration::from_millis(16), size: Size2D::from((500.0, 500.0)), event_loop_ticker: true, + state: None, } } } -impl TestingConfig { +impl TestingConfig { pub fn new() -> Self { TestingConfig::default() } diff --git a/crates/testing/src/launch.rs b/crates/testing/src/launch.rs index 225dc4e64..4d934666b 100644 --- a/crates/testing/src/launch.rs +++ b/crates/testing/src/launch.rs @@ -32,12 +32,15 @@ use crate::{ /// Run a Component in a headless testing environment. /// /// Default size is `500x500`. -pub fn launch_test(root: AppComponent) -> TestingHandler { +pub fn launch_test(root: AppComponent) -> TestingHandler<()> { launch_test_with_config(root, TestingConfig::default()) } /// Run a Component in a headless testing environment -pub fn launch_test_with_config(root: AppComponent, config: TestingConfig) -> TestingHandler { +pub fn launch_test_with_config( + root: AppComponent, + config: TestingConfig, +) -> TestingHandler { let vdom = with_accessibility(root); let fdom = FreyaDOM::default(); let sdom = SafeDOM::new(fdom); @@ -76,6 +79,7 @@ pub fn launch_test_with_config(root: AppComponent, config: TestingConfig) -> Tes }; handler.init_dom(); + handler.resize(handler.config.size); handler } diff --git a/crates/testing/src/lib.rs b/crates/testing/src/lib.rs index 401d246b6..a92b65032 100644 --- a/crates/testing/src/lib.rs +++ b/crates/testing/src/lib.rs @@ -139,7 +139,7 @@ //! //! let mut utils = launch_test_with_config( //! our_component, -//! TestingConfig { +//! TestingConfig::<()> { //! size: (500.0, 800.0).into(), //! ..TestingConfig::default() //! }, diff --git a/crates/testing/src/test_handler.rs b/crates/testing/src/test_handler.rs index 3b4876622..db2e6e339 100644 --- a/crates/testing/src/test_handler.rs +++ b/crates/testing/src/test_handler.rs @@ -15,6 +15,7 @@ use freya_core::prelude::{ use freya_engine::prelude::{ raster_n32_premul, Color, + Data, EncodedImageFormat, FontCollection, FontMgr, @@ -53,7 +54,7 @@ use crate::{ }; /// Manages the lifecycle of your tests. -pub struct TestingHandler { +pub struct TestingHandler { pub(crate) vdom: VirtualDom, pub(crate) utils: TestUtils, pub(crate) event_emitter: EventEmitter, @@ -67,12 +68,12 @@ pub struct TestingHandler { pub(crate) font_collection: FontCollection, pub(crate) font_mgr: FontMgr, pub(crate) accessibility_tree: SharedAccessibilityTree, - pub(crate) config: TestingConfig, + pub(crate) config: TestingConfig, pub(crate) ticker_sender: broadcast::Sender<()>, pub(crate) cursor_icon: CursorIcon, } -impl TestingHandler { +impl TestingHandler { /// Init the DOM. pub(crate) fn init_dom(&mut self) { self.provide_vdom_contexts(); @@ -82,7 +83,7 @@ impl TestingHandler { } /// Get a mutable reference to the current [`TestingConfig`]. - pub fn config(&mut self) -> &mut TestingConfig { + pub fn config(&mut self) -> &mut TestingConfig { &mut self.config } @@ -101,6 +102,10 @@ impl TestingHandler { }; self.vdom .insert_any_root_context(Box::new(accessibility_generator)); + + if let Some(state) = self.config.state.clone() { + self.vdom.insert_any_root_context(Box::new(state)); + } } /// Wait and apply new changes @@ -287,6 +292,11 @@ impl TestingHandler { state.information.viewport_size = size; }); self.utils.sdom().get_mut().layout().reset(); + self.utils + .sdom() + .get_mut() + .compositor_dirty_area() + .unite_or_insert(&Area::new((0.0, 0.0).into(), size)); } /// Get the current [CursorIcon]. @@ -299,8 +309,8 @@ impl TestingHandler { self.utils.sdom() } - /// Render the app into a canvas and save it into a file. - pub fn save_snapshot(&mut self, snapshot_path: impl Into) { + /// Render the app into a canvas and make a snapshot of it. + pub fn create_snapshot(&mut self) -> Data { let fdom = self.utils.sdom.get(); let (width, height) = self.config.size.to_i32().to_tuple(); @@ -341,16 +351,19 @@ impl TestingHandler { // Capture snapshot let image = surface.image_snapshot(); let mut context = surface.direct_context(); - let snapshot_data = image + image .encode(context.as_mut(), EncodedImageFormat::PNG, None) - .expect("Failed to encode the snapshot."); + .expect("Failed to encode the snapshot.") + } - // Save snapshot + /// Render the app into a canvas and save it into a file. + pub fn save_snapshot(&mut self, snapshot_path: impl Into) { let mut snapshot_file = File::create(snapshot_path.into()).expect("Failed to create the snapshot file."); - let snapshot_bytes = snapshot_data.as_bytes(); + let snapshot_data = self.create_snapshot(); + snapshot_file - .write_all(snapshot_bytes) + .write_all(&snapshot_data) .expect("Failed to save the snapshot file."); } diff --git a/crates/torin/src/measure.rs b/crates/torin/src/measure.rs index 4ba77a479..78e17611e 100644 --- a/crates/torin/src/measure.rs +++ b/crates/torin/src/measure.rs @@ -20,6 +20,7 @@ use crate::{ AreaModel, DirectionMode, LayoutMetadata, + Length, Torin, }, }; @@ -308,8 +309,9 @@ where ) { let children = self.dom_adapter.children_of(parent_node_id); + let mut initial_phase_flex_grows = FxHashMap::default(); let mut initial_phase_sizes = FxHashMap::default(); - let mut initial_phase_inner_sizes = *inner_sizes; + let mut initial_phase_inner_sizes = Size2D::default(); // Used to calculate the spacing and some alignments let (non_absolute_children_len, first_child, last_child) = if parent_node.spacing.get() > 0. @@ -342,16 +344,18 @@ where ) }; - // Initial phase: Measure the size and position of the children if the parent has a - // non-start cross alignment, non-start main aligment of a fit-content. - if parent_node.cross_alignment.is_not_start() + let needs_initial_phase = parent_node.cross_alignment.is_not_start() || parent_node.main_alignment.is_not_start() || parent_node.content.is_fit() - { - let mut initial_phase_area = *area; - let mut initial_phase_inner_area = *inner_area; - let mut initial_phase_available_area = *available_area; + || parent_node.content.is_flex(); + let mut initial_phase_area = *area; + let mut initial_phase_inner_area = *inner_area; + let mut initial_phase_available_area = *available_area; + + // Initial phase: Measure the size and position of the children if the parent has a + // non-start cross alignment, non-start main aligment of a fit-content. + if needs_initial_phase { // Measure the children for child_id in children.iter() { let Some(child_data) = self.dom_adapter.get_node(child_id) else { @@ -382,11 +386,13 @@ where Self::stack_child( &mut initial_phase_available_area, parent_node, + &child_data, &mut initial_phase_area, &mut initial_phase_inner_area, &mut initial_phase_inner_sizes, &child_areas.area, is_last_child, + Phase::Initial, ); if parent_node.cross_alignment.is_not_start() @@ -394,8 +400,60 @@ where { initial_phase_sizes.insert(*child_id, child_areas.area.size); } + + if parent_node.content.is_flex() { + match parent_node.direction { + DirectionMode::Vertical => { + if let Some(ff) = child_data.height.flex_grow() { + initial_phase_flex_grows.insert(*child_id, ff); + } + } + DirectionMode::Horizontal => { + if let Some(ff) = child_data.width.flex_grow() { + initial_phase_flex_grows.insert(*child_id, ff); + } + } + } + } } + } + + let initial_available_area = *available_area; + let flex_grows = initial_phase_flex_grows + .values() + .cloned() + .reduce(|acc, v| acc + v) + .unwrap_or_default() + .max(Length::new(1.0)); + + let flex_axis = AlignAxis::new(&parent_node.direction, AlignmentDirection::Main); + + let flex_available_width = initial_available_area.width() - initial_phase_inner_sizes.width; + let flex_available_height = + initial_available_area.height() - initial_phase_inner_sizes.height; + + let initial_phase_inner_sizes_with_flex = + initial_phase_flex_grows + .values() + .fold(initial_phase_inner_sizes, |mut acc, f| { + let flex_grow_per = f.get() / flex_grows.get() * 100.; + + match flex_axis { + AlignAxis::Height => { + let size = flex_available_height / 100. * flex_grow_per; + acc.height += size; + } + AlignAxis::Width => { + let size = flex_available_width / 100. * flex_grow_per; + acc.width += size; + } + } + + acc + }); + + if needs_initial_phase { if parent_node.main_alignment.is_not_start() { // Adjust the available and inner areas of the Main axis Self::shrink_area_to_fit_when_unbounded( @@ -410,7 +468,7 @@ where Self::align_content( available_area, &initial_phase_inner_area, - &initial_phase_inner_sizes, + &initial_phase_inner_sizes_with_flex, &parent_node.main_alignment, &parent_node.direction, AlignmentDirection::Main, @@ -442,6 +500,25 @@ where let mut adapted_available_area = *available_area; + if parent_node.content.is_flex() { + let flex_grow = initial_phase_flex_grows.get(&child_id); + + if let Some(flex_grow) = flex_grow { + let flex_grow_per = flex_grow.get() / flex_grows.get() * 100.; + + match flex_axis { + AlignAxis::Height => { + let size = flex_available_height / 100. * flex_grow_per; + adapted_available_area.size.height = size; + } + AlignAxis::Width => { + let size = flex_available_width / 100. * flex_grow_per; + adapted_available_area.size.width = size; + } + } + } + } + // Only the stacked children will be aligned if parent_node.main_alignment.is_spaced() && !child_data.position.is_absolute() { // Align the Main axis if necessary @@ -449,7 +526,7 @@ where AlignmentDirection::Main, &mut adapted_available_area, &initial_available_area, - &initial_phase_inner_sizes, + &initial_phase_inner_sizes_with_flex, &parent_node.main_alignment, &parent_node.direction, non_absolute_children_len, @@ -492,11 +569,13 @@ where Self::stack_child( available_area, parent_node, + &child_data, area, inner_area, inner_sizes, &child_areas.area, is_last_child, + Phase::Final, ); } @@ -612,11 +691,13 @@ where fn stack_child( available_area: &mut Area, parent_node: &Node, + child_node: &Node, parent_area: &mut Area, inner_area: &mut Area, inner_sizes: &mut Size2D, child_area: &Area, is_last_sibiling: bool, + phase: Phase, ) { // Only apply the spacing to elements after `i > 0` and `i < len - 1` let spacing = (!is_last_sibiling) @@ -630,7 +711,10 @@ where available_area.size.width -= child_area.size.width + spacing.get(); inner_sizes.height = child_area.height().max(inner_sizes.height); - inner_sizes.width += child_area.width() + spacing.get(); + inner_sizes.width += spacing.get(); + if !child_node.width.is_flex() || phase == Phase::Final { + inner_sizes.width += child_area.width(); + } // Keep the biggest height if parent_node.height.inner_sized() { @@ -656,7 +740,10 @@ where available_area.size.height -= child_area.size.height + spacing.get(); inner_sizes.width = child_area.width().max(inner_sizes.width); - inner_sizes.height += child_area.height() + spacing.get(); + inner_sizes.height += spacing.get(); + if !child_node.height.is_flex() || phase == Phase::Final { + inner_sizes.height += child_area.height(); + } // Keep the biggest width if parent_node.width.inner_sized() { diff --git a/crates/torin/src/values/content.rs b/crates/torin/src/values/content.rs index a6193e8b8..c96b38afb 100644 --- a/crates/torin/src/values/content.rs +++ b/crates/torin/src/values/content.rs @@ -3,12 +3,17 @@ pub enum Content { #[default] Normal, Fit, + Flex, } impl Content { pub fn is_fit(&self) -> bool { self == &Self::Fit } + + pub fn is_flex(&self) -> bool { + self == &Self::Flex + } } impl Content { @@ -16,6 +21,7 @@ impl Content { match self { Self::Normal => "normal".to_owned(), Self::Fit => "fit".to_owned(), + Self::Flex => "flex".to_owned(), } } } diff --git a/crates/torin/src/values/size.rs b/crates/torin/src/values/size.rs index 2c1c347b2..9afce5792 100644 --- a/crates/torin/src/values/size.rs +++ b/crates/torin/src/values/size.rs @@ -21,6 +21,7 @@ pub enum Size { RootPercentage(Length), InnerPercentage(Length), DynamicCalculations(Box>), + Flex(Length), } impl Default for Size { @@ -30,6 +31,17 @@ impl Default for Size { } impl Size { + pub fn flex_grow(&self) -> Option { + match self { + Self::Flex(f) => Some(*f), + _ => None, + } + } + + pub fn is_flex(&self) -> bool { + matches!(self, Self::Flex(_)) + } + pub fn inner_sized(&self) -> bool { matches!( self, @@ -58,6 +70,7 @@ impl Size { Size::FillMinimum => "fill-min".to_string(), Size::RootPercentage(p) => format!("{}% of root", p.get()), Size::InnerPercentage(p) => format!("{}% of auto", p.get()), + Size::Flex(f) => format!("flex({}", f.get()), } } @@ -76,14 +89,9 @@ impl Size { run_calculations(calculations.deref(), parent_value, root_value).unwrap_or(0.0), ), Size::Fill => Some(available_parent_value), - Size::FillMinimum => { - if phase == Phase::Initial { - None - } else { - Some(available_parent_value) - } - } + Size::FillMinimum if phase == Phase::Final => Some(available_parent_value), Size::RootPercentage(per) => Some(root_value / 100.0 * per.get()), + Size::Flex(_) if phase == Phase::Final => Some(available_parent_value), _ => None, } } @@ -171,6 +179,8 @@ pub enum DynamicCalculation { Mul, Div, Add, + OpenParenthesis, + ClosedParenthesis, Percentage(f32), RootPercentage(f32), Pixels(f32), @@ -191,6 +201,8 @@ impl std::fmt::Display for DynamicCalculation { DynamicCalculation::Mul => f.write_str("*"), DynamicCalculation::Div => f.write_str("/"), DynamicCalculation::Add => f.write_str("+"), + DynamicCalculation::OpenParenthesis => f.write_str("("), + DynamicCalculation::ClosedParenthesis => f.write_str(")"), DynamicCalculation::Percentage(p) => f.write_fmt(format_args!("{p}%")), DynamicCalculation::RootPercentage(p) => f.write_fmt(format_args!("{p}v")), DynamicCalculation::Pixels(s) => f.write_fmt(format_args!("{s}")), @@ -235,7 +247,7 @@ impl<'a> DynamicCalculationEvaluator<'a> { fn parse_expression(&mut self, min_precedence: usize) -> Option { // Parse left-hand side value self.current = self.calcs.next(); - let mut lhs = self.parse_value()?; + let mut lhs = self.parse_term()?; while let Some(operator_precedence) = self.operator_precedence() { // Return if minimal precedence is reached. @@ -267,35 +279,88 @@ impl<'a> DynamicCalculationEvaluator<'a> { Some(lhs) } - /// Parse and evaluate the value with the following grammar: + /// Parse and evaluate the term, implements implicit multiplication. only parenthesis count as + /// a seperator, so syntax like 50 50 isnt correct, but 50(50) is because the parenthesis act + /// as a seperator + fn parse_term(&mut self) -> Option { + let prefix = self.parse_prefix()?; + let mut lhs = None; + // set to true so that the first value is multiplied and counts as normal syntax + let mut last_is_separator = true; + + while let Some((rhs, seperator)) = self.parse_value() { + if last_is_separator || seperator { + lhs = Some(lhs.unwrap_or(1.0) * rhs); + } else { + return None; + } + last_is_separator = seperator; + } + if let Some(prefix) = prefix { + match prefix { + DynamicCalculation::Add => lhs, + DynamicCalculation::Sub => lhs.map(|v| v * -1.0), + _ => unreachable!("make sure to add the prefix here"), + } + } else { + lhs + } + } + /// parse and evaluate the value with the following grammar: /// ```ebnf /// value = percentage | pixels ; /// percentage = number, "%" ; /// pixels = number ; - /// ``` - fn parse_value(&mut self) -> Option { + /// ` + fn parse_value(&mut self) -> Option<(f32, bool)> { match self.current? { DynamicCalculation::Percentage(value) => { self.current = self.calcs.next(); - Some((self.parent_value / 100.0 * value).round()) + Some(((self.parent_value / 100.0 * value).round(), false)) } DynamicCalculation::RootPercentage(value) => { self.current = self.calcs.next(); - Some((self.root_value / 100.0 * value).round()) + Some(((self.root_value / 100.0 * value).round(), false)) } DynamicCalculation::Pixels(value) => { self.current = self.calcs.next(); - Some(*value) + Some((*value, false)) + } + DynamicCalculation::OpenParenthesis => { + // function should return on DynamicCalculation::ClosedParenthesis because it does + // not have a precedence, thats how it actually works + let val = self.parse_expression(0); + if self.current != Some(&DynamicCalculation::ClosedParenthesis) { + return None; + } + self.current = self.calcs.next(); + Some((val?, true)) } _ => None, } } + /// parses out the prefix, like a + or - + fn parse_prefix(&mut self) -> Option> { + match self.current? { + DynamicCalculation::Add => { + self.current = self.calcs.next(); + Some(Some(DynamicCalculation::Add)) + } + DynamicCalculation::Sub => { + self.current = self.calcs.next(); + Some(Some(DynamicCalculation::Sub)) + } + _ => Some(None), + } + } + /// Get the precedence of the operator if current token is an operator or None otherwise. fn operator_precedence(&self) -> Option { match self.current? { DynamicCalculation::Add | DynamicCalculation::Sub => Some(1), DynamicCalculation::Mul | DynamicCalculation::Div => Some(2), + DynamicCalculation::OpenParenthesis => Some(0), _ => None, } } diff --git a/crates/torin/tests/alignment.rs b/crates/torin/tests/alignment.rs index 40889d4c1..536a807cc 100644 --- a/crates/torin/tests/alignment.rs +++ b/crates/torin/tests/alignment.rs @@ -627,7 +627,7 @@ pub fn alignment_with_absolute_child() { Node::from_size_and_position( Size::Pixels(Length::new(100.)), Size::Pixels(Length::new(100.)), - Position::Absolute(Box::new(AbsolutePosition::default())), + Position::Absolute(Box::default()), ), ); mocked_dom.add( diff --git a/crates/torin/tests/flex.rs b/crates/torin/tests/flex.rs new file mode 100644 index 000000000..d5fcd0902 --- /dev/null +++ b/crates/torin/tests/flex.rs @@ -0,0 +1,321 @@ +use euclid::Length; +use torin::{ + prelude::*, + test_utils::*, +}; + +#[test] +pub fn flex_generic() { + let (mut layout, mut measurer) = test_utils(); + + let mut mocked_dom = TestingDOM::default(); + mocked_dom.add( + 0, + None, + vec![1, 2, 3, 4], + Node::from_size_and_content( + Size::Pixels(Length::new(200.0)), + Size::Pixels(Length::new(200.0)), + Content::Flex, + ), + ); + mocked_dom.add( + 1, + Some(0), + vec![], + Node::from_size_and_direction( + Size::Pixels(Length::new(100.0)), + Size::Percentage(Length::new(10.)), + DirectionMode::Vertical, + ), + ); + mocked_dom.add( + 2, + Some(0), + vec![], + Node::from_size_and_direction( + Size::Pixels(Length::new(100.0)), + Size::Flex(Length::new(1.0)), + DirectionMode::Vertical, + ), + ); + mocked_dom.add( + 3, + Some(0), + vec![], + Node::from_size_and_direction( + Size::Pixels(Length::new(100.0)), + Size::Pixels(Length::new(50.0)), + DirectionMode::Vertical, + ), + ); + mocked_dom.add( + 4, + Some(0), + vec![], + Node::from_size_and_direction( + Size::Pixels(Length::new(100.0)), + Size::Flex(Length::new(3.0)), + DirectionMode::Vertical, + ), + ); + + layout.measure( + 0, + Rect::new(Point2D::new(0.0, 0.0), Size2D::new(1000.0, 1000.0)), + &mut measurer, + &mut mocked_dom, + ); + + assert_eq!( + layout.get(0).unwrap().area, + Rect::new(Point2D::new(0.0, 0.0), Size2D::new(200.0, 200.0)), + ); + + assert_eq!( + layout.get(1).unwrap().area, + Rect::new(Point2D::new(0.0, 0.0), Size2D::new(100.0, 20.0)), + ); + assert_eq!( + layout.get(2).unwrap().area, + Rect::new(Point2D::new(0.0, 20.0), Size2D::new(100.0, 32.5)), + ); + assert_eq!( + layout.get(3).unwrap().area, + Rect::new(Point2D::new(0.0, 52.5), Size2D::new(100.0, 50.0)), + ); + assert_eq!( + layout.get(4).unwrap().area, + Rect::new(Point2D::new(0.0, 102.5), Size2D::new(100.0, 97.5)), + ); +} + +#[test] +pub fn flex_under_1_flex_grow() { + let (mut layout, mut measurer) = test_utils(); + + let mut mocked_dom = TestingDOM::default(); + mocked_dom.add( + 0, + None, + vec![1, 2], + Node::from_size_and_content( + Size::Pixels(Length::new(200.0)), + Size::Pixels(Length::new(200.0)), + Content::Flex, + ), + ); + mocked_dom.add( + 1, + Some(0), + vec![], + Node::from_size_and_direction( + Size::Pixels(Length::new(100.0)), + Size::Flex(Length::new(0.2)), + DirectionMode::Vertical, + ), + ); + mocked_dom.add( + 2, + Some(0), + vec![], + Node::from_size_and_direction( + Size::Pixels(Length::new(100.0)), + Size::Flex(Length::new(0.5)), + DirectionMode::Vertical, + ), + ); + + layout.measure( + 0, + Rect::new(Point2D::new(0.0, 0.0), Size2D::new(1000.0, 1000.0)), + &mut measurer, + &mut mocked_dom, + ); + + assert_eq!( + layout.get(0).unwrap().area, + Rect::new(Point2D::new(0.0, 0.0), Size2D::new(200.0, 200.0)), + ); + + assert_eq!( + layout.get(1).unwrap().area, + Rect::new(Point2D::new(0.0, 0.0), Size2D::new(100.0, 40.0)), + ); + assert_eq!( + layout.get(2).unwrap().area, + Rect::new(Point2D::new(0.0, 40.0), Size2D::new(100.0, 100.0)), + ); +} + +#[test] +pub fn flex_grow_balance() { + let (mut layout, mut measurer) = test_utils(); + + let mut mocked_dom = TestingDOM::default(); + mocked_dom.add( + 0, + None, + vec![1, 2, 3, 4], + Node::from_size_and_content( + Size::Pixels(Length::new(200.0)), + Size::Pixels(Length::new(200.0)), + Content::Flex, + ), + ); + mocked_dom.add( + 1, + Some(0), + vec![], + Node::from_size_and_direction( + Size::Pixels(Length::new(100.0)), + Size::Flex(Length::new(1.0)), + DirectionMode::Vertical, + ), + ); + mocked_dom.add( + 2, + Some(0), + vec![], + Node::from_size_and_direction( + Size::Pixels(Length::new(100.0)), + Size::Flex(Length::new(2.0)), + DirectionMode::Vertical, + ), + ); + mocked_dom.add( + 3, + Some(0), + vec![], + Node::from_size_and_direction( + Size::Pixels(Length::new(100.0)), + Size::Flex(Length::new(3.0)), + DirectionMode::Vertical, + ), + ); + mocked_dom.add( + 4, + Some(0), + vec![], + Node::from_size_and_direction( + Size::Pixels(Length::new(100.0)), + Size::Flex(Length::new(4.0)), + DirectionMode::Vertical, + ), + ); + + layout.measure( + 0, + Rect::new(Point2D::new(0.0, 0.0), Size2D::new(1000.0, 1000.0)), + &mut measurer, + &mut mocked_dom, + ); + + assert_eq!( + layout.get(0).unwrap().area, + Rect::new(Point2D::new(0.0, 0.0), Size2D::new(200.0, 200.0)), + ); + + assert_eq!( + layout.get(1).unwrap().area, + Rect::new(Point2D::new(0.0, 0.0), Size2D::new(100.0, 20.0)), + ); + assert_eq!( + layout.get(2).unwrap().area, + Rect::new(Point2D::new(0.0, 20.0), Size2D::new(100.0, 40.0)), + ); + assert_eq!( + layout.get(3).unwrap().area.round(), + Rect::new(Point2D::new(0.0, 60.0), Size2D::new(100.0, 60.0)), + ); + assert_eq!( + layout.get(4).unwrap().area.round(), + Rect::new(Point2D::new(0.0, 120.0), Size2D::new(100.0, 80.0)), + ); +} + +#[test] +pub fn flex_large_grow_balance() { + let (mut layout, mut measurer) = test_utils(); + + let mut mocked_dom = TestingDOM::default(); + mocked_dom.add( + 0, + None, + vec![1, 2, 3, 4], + Node::from_size_and_content( + Size::Pixels(Length::new(200.0)), + Size::Pixels(Length::new(200.0)), + Content::Flex, + ), + ); + mocked_dom.add( + 1, + Some(0), + vec![], + Node::from_size_and_direction( + Size::Pixels(Length::new(100.0)), + Size::Flex(Length::new(5.0)), + DirectionMode::Vertical, + ), + ); + mocked_dom.add( + 2, + Some(0), + vec![], + Node::from_size_and_direction( + Size::Pixels(Length::new(100.0)), + Size::Flex(Length::new(65.0)), + DirectionMode::Vertical, + ), + ); + mocked_dom.add( + 3, + Some(0), + vec![], + Node::from_size_and_direction( + Size::Pixels(Length::new(100.0)), + Size::Flex(Length::new(30.0)), + DirectionMode::Vertical, + ), + ); + mocked_dom.add( + 4, + Some(0), + vec![], + Node::from_size_and_direction( + Size::Pixels(Length::new(100.0)), + Size::Flex(Length::new(100.0)), + DirectionMode::Vertical, + ), + ); + + layout.measure( + 0, + Rect::new(Point2D::new(0.0, 0.0), Size2D::new(1000.0, 1000.0)), + &mut measurer, + &mut mocked_dom, + ); + + assert_eq!( + layout.get(0).unwrap().area, + Rect::new(Point2D::new(0.0, 0.0), Size2D::new(200.0, 200.0)), + ); + + assert_eq!( + layout.get(1).unwrap().area, + Rect::new(Point2D::new(0.0, 0.0), Size2D::new(100.0, 5.0)), + ); + assert_eq!( + layout.get(2).unwrap().area, + Rect::new(Point2D::new(0.0, 5.0), Size2D::new(100.0, 65.0)), + ); + assert_eq!( + layout.get(3).unwrap().area.round(), + Rect::new(Point2D::new(0.0, 70.0), Size2D::new(100.0, 30.0)), + ); + assert_eq!( + layout.get(4).unwrap().area.round(), + Rect::new(Point2D::new(0.0, 100.0), Size2D::new(100.0, 100.0)), + ); +} diff --git a/crates/torin/tests/size.rs b/crates/torin/tests/size.rs index f7e89a96a..585dd1d9a 100644 --- a/crates/torin/tests/size.rs +++ b/crates/torin/tests/size.rs @@ -784,7 +784,7 @@ pub fn test_calc() { assert_eq!( run_calculations( - &vec![DynamicCalculation::Pixels(10.0)], + &[DynamicCalculation::Pixels(10.0)], PARENT_VALUE, PARENT_VALUE ), @@ -793,7 +793,7 @@ pub fn test_calc() { assert_eq!( run_calculations( - &vec![DynamicCalculation::Percentage(87.5)], + &[DynamicCalculation::Percentage(87.5)], PARENT_VALUE, PARENT_VALUE ), @@ -802,12 +802,12 @@ pub fn test_calc() { assert_eq!( run_calculations( - &vec![ + &[ DynamicCalculation::Pixels(10.0), DynamicCalculation::Add, DynamicCalculation::Pixels(20.0), DynamicCalculation::Mul, - DynamicCalculation::Percentage(50.0), + DynamicCalculation::Percentage(50.0) ], PARENT_VALUE, PARENT_VALUE @@ -817,7 +817,7 @@ pub fn test_calc() { assert_eq!( run_calculations( - &vec![ + &[ DynamicCalculation::Pixels(10.0), DynamicCalculation::Add, DynamicCalculation::Percentage(10.0), @@ -828,7 +828,7 @@ pub fn test_calc() { DynamicCalculation::Add, DynamicCalculation::Pixels(75.0), DynamicCalculation::Mul, - DynamicCalculation::Pixels(2.0), + DynamicCalculation::Pixels(2.0) ], PARENT_VALUE, PARENT_VALUE @@ -838,9 +838,9 @@ pub fn test_calc() { assert_eq!( run_calculations( - &vec![ + &[ DynamicCalculation::Pixels(10.0), - DynamicCalculation::Pixels(20.0), + DynamicCalculation::Pixels(20.0) ], PARENT_VALUE, PARENT_VALUE @@ -850,7 +850,7 @@ pub fn test_calc() { assert_eq!( run_calculations( - &vec![DynamicCalculation::Pixels(10.0), DynamicCalculation::Add], + &[DynamicCalculation::Pixels(10.0), DynamicCalculation::Add], PARENT_VALUE, PARENT_VALUE ), @@ -859,30 +859,32 @@ pub fn test_calc() { assert_eq!( run_calculations( - &vec![DynamicCalculation::Add, DynamicCalculation::Pixels(10.0)], + &[DynamicCalculation::Add, DynamicCalculation::Pixels(10.0)], PARENT_VALUE, PARENT_VALUE ), - None + // Because +10 is just 10 + Some(10.0) ); assert_eq!( run_calculations( - &vec![ + &[ DynamicCalculation::Pixels(10.0), DynamicCalculation::Add, + // counts as a prefix DynamicCalculation::Add, DynamicCalculation::Pixels(10.0) ], PARENT_VALUE, PARENT_VALUE ), - None + Some(20.0) ); assert_eq!( run_calculations( - &vec![ + &[ DynamicCalculation::Percentage(50.0), DynamicCalculation::Sub, DynamicCalculation::RootPercentage(20.0) @@ -892,4 +894,67 @@ pub fn test_calc() { ), Some((PARENT_VALUE * 0.5) - (PARENT_VALUE * 0.20)) ); + + assert_eq!( + run_calculations( + &[ + DynamicCalculation::OpenParenthesis, + DynamicCalculation::Pixels(10.0), + DynamicCalculation::ClosedParenthesis + ], + PARENT_VALUE, + PARENT_VALUE + ), + Some(10.0) + ); + + assert_eq!( + run_calculations( + &[ + DynamicCalculation::Pixels(10.0), + DynamicCalculation::OpenParenthesis, + DynamicCalculation::Pixels(10.0), + DynamicCalculation::Add, + DynamicCalculation::Pixels(20.0), + DynamicCalculation::ClosedParenthesis, + DynamicCalculation::Pixels(10.0), + DynamicCalculation::Add, + DynamicCalculation::Pixels(10.0), + DynamicCalculation::OpenParenthesis, + DynamicCalculation::Pixels(10.0), + DynamicCalculation::ClosedParenthesis, + DynamicCalculation::Pixels(10.0) + ], + PARENT_VALUE, + PARENT_VALUE + ), + Some((10.0 * (10.0 + 20.0) * 10.0) + (10.0 * (10.0) * 10.0)) + ); + + assert_eq!( + run_calculations( + &[ + DynamicCalculation::Sub, + DynamicCalculation::OpenParenthesis, + DynamicCalculation::Pixels(10.0), + DynamicCalculation::ClosedParenthesis, + DynamicCalculation::Pixels(20.0) + ], + PARENT_VALUE, + PARENT_VALUE + ), + Some(-1.0 * 10.0 * 20.0) + ); + + assert_eq!( + run_calculations( + &[ + DynamicCalculation::OpenParenthesis, + DynamicCalculation::Pixels(10.0) + ], + PARENT_VALUE, + PARENT_VALUE + ), + None + ); } diff --git a/examples/accessibility.rs b/examples/accessibility.rs index 56f07101c..27aa9d0cc 100644 --- a/examples/accessibility.rs +++ b/examples/accessibility.rs @@ -55,7 +55,7 @@ fn app() -> Element { width: "100%", height: "50%", a11y_role:"label", - a11y_alt: "This is a rectangle", + a11y_name: "This is a rectangle", onclick: move |_| { focus_b.focus(); }, diff --git a/examples/button.rs b/examples/button.rs index 00861d214..029e7ddbd 100644 --- a/examples/button.rs +++ b/examples/button.rs @@ -11,16 +11,25 @@ fn main() { fn app() -> Element { rsx!( - Button { - onclick: move |_| println!("Button Clicked!"), - label { "Button A" } - } - Button { - onpress: move |_| println!("Button Pressed!"), - label { "Button B" } - } - Button { - label { "Button C" } + Body { + rect { + width: "fill", + height: "fill", + spacing: "10", + main_align: "center", + cross_align: "center", + Button { + onclick: move |_| println!("Button Clicked!"), + label { "Button A" } + } + FilledButton { + onpress: move |_| println!("Button Pressed!"), + label { "Button B" } + } + OutlineButton { + label { "Button C" } + } + } } ) } diff --git a/examples/cloned_editor.rs b/examples/cloned_editor.rs index e611f8833..6fb3edda7 100644 --- a/examples/cloned_editor.rs +++ b/examples/cloned_editor.rs @@ -21,8 +21,7 @@ fn app() -> Element { #[allow(non_snake_case)] fn Body() -> Element { - let theme = use_theme(); - let theme = theme.read(); + let theme = use_applied_theme!(None, body); let mut editable = use_editable( || { @@ -55,7 +54,7 @@ fn Body() -> Element { cursor_reference, direction: "horizontal", onglobalclick: onclick, - background: "{theme.body.background}", + background: "{theme.background}", VirtualScrollView { width: "50%", length: editor.len_lines(), diff --git a/examples/documents_editor.rs b/examples/documents_editor.rs index 3929f5cb3..eb0be866f 100644 --- a/examples/documents_editor.rs +++ b/examples/documents_editor.rs @@ -266,7 +266,7 @@ fn DocumentView(path: ReadOnlySignal) -> Element { } } -static LOREM_IPSUM: &'static str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."; +static LOREM_IPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."; #[component] fn DocumentEdit(path: String) -> Element { diff --git a/examples/flex.rs b/examples/flex.rs new file mode 100644 index 000000000..a091c705b --- /dev/null +++ b/examples/flex.rs @@ -0,0 +1,53 @@ +#![cfg_attr( + all(not(debug_assertions), target_os = "windows"), + windows_subsystem = "windows" +)] + +use freya::prelude::*; + +fn main() { + launch(app); +} + +fn app() -> Element { + rsx!( + rect { + height: "100%", + width: "100%", + direction: "horizontal", + content: "flex", + spacing: "4", + padding: "4", + rect { + height: "100%", + width: "10%", + background: "red", + } + rect { + width: "flex", + height: "100%", + background: "orange", + } + rect { + height: "100%", + width: "25", + background: "black", + } + rect { + width: "flex(3)", + height: "100%", + background: "yellow", + } + rect { + width: "flex", + height: "100%", + background: "green", + } + rect { + height: "100%", + width: "30%", + background: "blue", + } + } + ) +} diff --git a/examples/flex_2.rs b/examples/flex_2.rs new file mode 100644 index 000000000..d07c3126e --- /dev/null +++ b/examples/flex_2.rs @@ -0,0 +1,34 @@ +#![cfg_attr( + all(not(debug_assertions), target_os = "windows"), + windows_subsystem = "windows" +)] + +use freya::prelude::*; + +fn main() { + launch(app); +} + +fn app() -> Element { + rsx!( + rect { + width: "100%", + height: "fill", + direction: "horizontal", + main_align: "space-around", + content: "flex", + + rect { + width: "flex(0.5)", + height: "fill", + background: "red", + } + + rect { + width: "120", + height: "fill", + background: "orange", + } + } + ) +} diff --git a/examples/gamepad_focus.rs b/examples/gamepad_focus.rs index eb436e691..405b95972 100644 --- a/examples/gamepad_focus.rs +++ b/examples/gamepad_focus.rs @@ -20,10 +20,7 @@ use gilrs::{ }; fn main() { - launch_cfg( - app, - LaunchConfig::<()>::new().with_plugin(GamePadPlugin::default()), - ) + launch_cfg(app, LaunchConfig::<()>::new().with_plugin(GamePadPlugin)) } #[derive(Default)] @@ -38,32 +35,29 @@ impl GamePadPlugin { loop { while let Some(ev) = gilrs_instance.next_event() { - match ev.event { - EventType::ButtonReleased(_, code) => { - // NOTE: You might need to tweak these codes - match code.into_u32() { - 4 => { - handle.send_event_loop_event( - EventMessage::FocusPrevAccessibilityNode, - ); - } - 6 => { - handle.send_event_loop_event( - EventMessage::FocusNextAccessibilityNode, - ); - } - 13 => { - handle.send_platform_event(PlatformEvent::Keyboard { - name: EventName::KeyDown, - key: Key::Enter, - code: Code::Enter, - modifiers: Modifiers::default(), - }); - } - _ => {} + if let EventType::ButtonReleased(_, code) = ev.event { + // NOTE: You might need to tweak these codes + match code.into_u32() { + 4 => { + handle.send_event_loop_event( + EventMessage::FocusPrevAccessibilityNode, + ); + } + 6 => { + handle.send_event_loop_event( + EventMessage::FocusNextAccessibilityNode, + ); } + 13 => { + handle.send_platform_event(PlatformEvent::Keyboard { + name: EventName::KeyDown, + key: Key::Enter, + code: Code::Enter, + modifiers: Modifiers::default(), + }); + } + _ => {} } - _ => {} } } } @@ -73,11 +67,8 @@ impl GamePadPlugin { impl FreyaPlugin for GamePadPlugin { fn on_event(&mut self, event: &PluginEvent, handle: PluginHandle) { - match event { - PluginEvent::WindowCreated(_) => { - Self::listen_gamepad(handle); - } - _ => {} + if let PluginEvent::WindowCreated(_) = event { + Self::listen_gamepad(handle); } } } diff --git a/examples/gamepad_trace.rs b/examples/gamepad_trace.rs index 038a8349e..3dd20ce70 100644 --- a/examples/gamepad_trace.rs +++ b/examples/gamepad_trace.rs @@ -26,10 +26,7 @@ use gilrs::{ }; fn main() { - launch_cfg( - app, - LaunchConfig::<()>::new().with_plugin(GamePadPlugin::default()), - ) + launch_cfg(app, LaunchConfig::<()>::new().with_plugin(GamePadPlugin)) } #[derive(Default)] @@ -61,7 +58,7 @@ impl GamePadPlugin { } if diff_x != 0.0 { - x += diff_x as f64 * 10.; + x += diff_x * 10.; handle.send_platform_event(PlatformEvent::Mouse { name: EventName::MouseMove, cursor: (x, y).into(), @@ -70,7 +67,7 @@ impl GamePadPlugin { } if diff_x != 0.0 { - y -= diff_y as f64 * 10.; + y -= diff_y * 10.; handle.send_platform_event(PlatformEvent::Mouse { name: EventName::MouseMove, cursor: (x, y).into(), @@ -95,11 +92,8 @@ impl GamePadPlugin { impl FreyaPlugin for GamePadPlugin { fn on_event(&mut self, event: &PluginEvent, handle: PluginHandle) { - match event { - PluginEvent::WindowCreated(_) => { - Self::listen_gamepad(handle); - } - _ => {} + if let PluginEvent::WindowCreated(_) = event { + Self::listen_gamepad(handle); } } } diff --git a/examples/input.rs b/examples/input.rs index 79bfb88b5..6893b417f 100644 --- a/examples/input.rs +++ b/examples/input.rs @@ -18,13 +18,14 @@ fn app() -> Element { padding: "7", width: "100%", height: "100%", - font_size: "10", + spacing: "4", label { color: "black", "Your name:" } Input { value: values.read().0.clone(), + placeholder: "Name", onchange: move |txt| { values.write().0 = txt; } @@ -35,17 +36,11 @@ fn app() -> Element { } Input { value: values.read().1.clone(), + placeholder: "Age", onchange: move |txt| { values.write().1 = txt; } }, - rect { - background: "red", - label { - color: "black", - "You are {values.read().0} and you are {values.read().1} years old." - } - } } ) } diff --git a/examples/mouse.rs b/examples/mouse.rs index 3d07ae9f5..8018a5450 100644 --- a/examples/mouse.rs +++ b/examples/mouse.rs @@ -10,8 +10,8 @@ fn main() { } fn app() -> Element { - let mut cursor_pos_over = use_signal(|| CursorPoint::default()); - let mut cursor_pos_click = use_signal(|| CursorPoint::default()); + let mut cursor_pos_over = use_signal(CursorPoint::default); + let mut cursor_pos_click = use_signal(CursorPoint::default); let onmousemove = move |e: MouseEvent| { let cursor_pos = e.get_screen_coordinates(); diff --git a/examples/settings.svg b/examples/settings.svg new file mode 100644 index 000000000..cea27111e --- /dev/null +++ b/examples/settings.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/examples/shadow.rs b/examples/shadow.rs index 1d7ee720e..bf0756c1c 100644 --- a/examples/shadow.rs +++ b/examples/shadow.rs @@ -17,29 +17,28 @@ fn app() -> Element { width: "100%", padding: "60", background: "rgb(224, 224, 224)", + spacing: "40", rect { direction: "horizontal", + spacing: "40", rect { shadow: "inset 0 0 8 red", height: "80", width: "80", background: "black", } - rect { width: "40" } rect { shadow: "24 24 8 0 rgb(0, 0, 0, 128), -24 -24 8 0 rgb(0, 255, 0, 128)", height: "80", width: "80", background: "black", } - rect { width: "40" } rect { shadow: "0 0 60 3 red, 0 0 50 3 orange, 0 0 40 3 yellow, 0 0 30 3 green, 0 0 20 3 blue, 0 0 10 3 rgb(255,0,255)", height: "80", width: "80", background: "black", } - rect { width: "40" } rect { shadow: "5 5 10 rgb(190, 190, 190), -5 -5 10 rgb(255, 255, 255)", height: "80", @@ -48,7 +47,6 @@ fn app() -> Element { background: "rgb(224, 224, 224)", } } - rect { height: "40" } label { text_shadow: "0 18 12 rgb(0, 0, 0), 0 0 20 orange", font_size: "80", diff --git a/examples/svg_fill.rs b/examples/svg_fill.rs new file mode 100644 index 000000000..1f8d66687 --- /dev/null +++ b/examples/svg_fill.rs @@ -0,0 +1,23 @@ +#![cfg_attr( + all(not(debug_assertions), target_os = "windows"), + windows_subsystem = "windows" +)] + +use freya::prelude::*; + +fn main() { + launch(app); +} + +static SETTINGS: &[u8] = include_bytes!("./settings.svg"); + +fn app() -> Element { + let svg_data = static_bytes(SETTINGS); + + rsx!(svg { + fill: "red", + width: "100%", + height: "100%", + svg_data, + }) +} diff --git a/examples/switch_theme.rs b/examples/switch_theme.rs index befb14012..137c0e987 100644 --- a/examples/switch_theme.rs +++ b/examples/switch_theme.rs @@ -6,7 +6,7 @@ use freya::prelude::*; fn main() { - launch(app); + launch_with_props(app, "Switch Theme", (600., 700.)); } #[component] @@ -57,7 +57,12 @@ fn ThemeChanger() -> Element { label { "Light" } } } - + Link { + to: "https://freyaui.dev", + label { + "https://freyaui.dev" + } + } ) } @@ -135,6 +140,12 @@ fn app() -> Element { "Set to 35%" } } + FilledButton { + onpress: move |_| value.set(75.), + label { + "Set to 75%" + } + } ThemeChanger { } } } diff --git a/examples/text_editors.rs b/examples/text_editors.rs index 8ca01547a..58535b08e 100644 --- a/examples/text_editors.rs +++ b/examples/text_editors.rs @@ -3,7 +3,7 @@ windows_subsystem = "windows" )] -use dioxus_sdk::clipboard::use_clipboard; +use dioxus_clipboard::prelude::use_clipboard; use freya::prelude::*; fn main() { diff --git a/examples/website.rs b/examples/website.rs index 6796ddcc1..cbb56cb29 100644 --- a/examples/website.rs +++ b/examples/website.rs @@ -353,7 +353,7 @@ fn Code() -> Element { .highlight(&rust_config, CODE.as_bytes(), None, |_| None) .unwrap(); - let rope = Rope::from_str(&CODE); + let rope = Rope::from_str(CODE); let mut syntax_blocks = SyntaxBlocks::default(); diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 94cbe48dd..8fc77ba56 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.80.1" +channel = "1.82.0" profile = "default" \ No newline at end of file