diff --git a/Cargo.toml b/Cargo.toml index d66fe50bf..5445544e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ hot-reload = ["freya/hot-reload"] custom-tokio-rt = ["freya/custom-tokio-rt"] performance-overlay = ["freya/performance-overlay"] fade-cached-incremental-areas = ["freya/fade-cached-incremental-areas"] +disable-zoom-shortcuts = ["freya/disable-zoom-shortcuts"] [patch.crates-io] # dioxus = { git = "https://github.com/DioxusLabs/dioxus", rev = "7beacdf9c76ae5412d3c2bcd55f7c5d87f486a0f" } @@ -94,6 +95,7 @@ tree-sitter-highlight = "0.23.0" tree-sitter-rust = "0.23.0" rfd = "0.14.1" bytes = "1.5.0" +dioxus-sdk = { workspace = true } [profile.release] lto = true diff --git a/README.md b/README.md index 3b1cef692..9dcc95683 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ fn app() -> Element { let mut count = use_signal(|| 0); - render!( + rsx!( rect { height: "50%", width: "100%", diff --git a/book/src/setup.md b/book/src/setup.md index 6ac4f7856..f42e8a079 100644 --- a/book/src/setup.md +++ b/book/src/setup.md @@ -31,6 +31,10 @@ sudo dnf install openssl-devel pkgconf cmake gtk3-devel clang-devel -y sudo dnf groupinstall "Development Tools" "C Development Tools and Libraries" -y ``` +#### NixOS + +Copy this [flake.nix](https://github.com/kuba375/freya-flake) into your project root. Then you can enter a dev shell by `nix develop`. + Don't hesitate to contribute so other distros can be added here. ### MacOS diff --git a/crates/components/src/accordion.rs b/crates/components/src/accordion.rs index 5e8e41e1c..80b4899a4 100644 --- a/crates/components/src/accordion.rs +++ b/crates/components/src/accordion.rs @@ -100,7 +100,7 @@ pub fn Accordion(props: AccordionProps) -> Element { height: "auto", background: "{background}", onclick, - border: "1 solid {border_fill}", + border: "1 inner {border_fill}", {&props.summary} rect { overflow: "clip", diff --git a/crates/components/src/button.rs b/crates/components/src/button.rs index 779bc9ea8..eeafc1b21 100644 --- a/crates/components/src/button.rs +++ b/crates/components/src/button.rs @@ -186,9 +186,9 @@ pub fn Button( ButtonStatus::Idle => background, }; let border = if focus.is_selected() { - format!("2 solid {focus_border_fill}") + format!("2 inner {focus_border_fill}") } else { - format!("1 solid {border_fill}") + format!("1 inner {border_fill}") }; rsx!( @@ -212,7 +212,6 @@ pub fn Button( text_align: "center", main_align: "center", cross_align: "center", - line_height: "1.1", {&children} } ) diff --git a/crates/components/src/checkbox.rs b/crates/components/src/checkbox.rs index fb1aad579..8370af041 100644 --- a/crates/components/src/checkbox.rs +++ b/crates/components/src/checkbox.rs @@ -1,7 +1,11 @@ use dioxus::prelude::*; -use freya_elements::elements as dioxus_elements; +use freya_elements::{ + elements as dioxus_elements, + events::KeyboardEvent, +}; use freya_hooks::{ use_applied_theme, + use_focus, CheckboxTheme, CheckboxThemeWith, }; @@ -68,30 +72,45 @@ pub fn Checkbox( /// Theme override. theme: Option, ) -> Element { + let focus = use_focus(); let CheckboxTheme { + border_fill, unselected_fill, selected_fill, selected_icon_fill, } = use_applied_theme!(&theme, checkbox); - let (fill, border) = if selected { + let (inner_fill, outer_fill) = if selected { (selected_fill.as_ref(), selected_fill.as_ref()) } else { ("transparent", unselected_fill.as_ref()) }; + let border = if focus.is_selected() { + format!("4 outer {}", border_fill) + } else { + "none".to_string() + }; + + let onkeydown = move |_: KeyboardEvent| {}; rsx!( rect { - width: "18", - height: "18", - padding: "4", - main_align: "center", - cross_align: "center", + border, corner_radius: "4", - border: "2 solid {border}", - background: "{fill}", - if selected { - TickIcon { - fill: selected_icon_fill + 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 + } } } } @@ -172,29 +191,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).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()); + 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()); utils.click_cursor((20., 50.)).await; - 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()); + 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()); utils.click_cursor((10., 90.)).await; utils.wait_for_update().await; - 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()); + 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()); 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).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()); + 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()); } } diff --git a/crates/components/src/dropdown.rs b/crates/components/src/dropdown.rs index 86eadbf78..0ead97289 100644 --- a/crates/components/src/dropdown.rs +++ b/crates/components/src/dropdown.rs @@ -274,7 +274,7 @@ where color: "{font_theme.color}", corner_radius: "8", padding: "8 16", - border: "1 solid {border_fill}", + border: "1 inner {border_fill}", shadow: "0 4 5 0 rgb(0, 0, 0, 0.1)", direction: "horizontal", main_align: "center", @@ -299,9 +299,9 @@ where rect { onglobalclick, onglobalkeydown, - layer: "-99", + layer: "-1000", margin: "{margin}", - border: "1 solid {border_fill}", + border: "1 inner {border_fill}", overflow: "clip", corner_radius: "8", background: "{dropdown_background}", @@ -379,7 +379,7 @@ mod test { utils.click_cursor((15., 15.)).await; // Click on the second option - utils.click_cursor((45., 100.)).await; + utils.click_cursor((45., 90.)).await; utils.wait_for_update().await; utils.wait_for_update().await; diff --git a/crates/components/src/image.rs b/crates/components/src/image.rs new file mode 100644 index 000000000..bc4257e4d --- /dev/null +++ b/crates/components/src/image.rs @@ -0,0 +1,57 @@ +/// Generate a Dioxus component rendering the specified image. +/// +/// Example: +/// +/// ```no_run +/// # use freya::prelude::*; +/// +/// import_svg!(Ferris, "../../../examples/rust_logo.png", "100%", "100%"); +/// import_svg!(FerrisWithRequiredSize, "../../../examples/rust_logo.png"); +/// +/// fn app() -> Element { +/// rsx!(Ferris {}) +/// } +/// +/// fn another_app() -> Element { +/// rsx!(FerrisWithRequiredSize { +/// width: "150", +/// height: "40%", +/// }) +/// } +/// ``` +#[macro_export] +macro_rules! import_image { + ($component_name:ident, $path:expr, $width: expr, $height: expr) => { + // Generate a function with the name derived from the file name + #[allow(non_snake_case)] + #[dioxus::prelude::component] + pub fn $component_name( + #[props(default = $width.to_string())] width: String, + #[props(default = $height.to_string())] height: String, + ) -> freya::prelude::Element { + use freya::prelude::*; + let image_data = static_bytes(include_bytes!($path)); + + rsx!(image { + width, + height, + image_data + }) + } + }; + ($component_name:ident, $path:expr) => { + // 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 { + use freya::prelude::*; + let image_data = static_bytes(include_bytes!($path)); + + rsx!(image { + width, + height, + image_data + }) + } + }; +} diff --git a/crates/components/src/input.rs b/crates/components/src/input.rs index e5b85843a..f29017ab0 100644 --- a/crates/components/src/input.rs +++ b/crates/components/src/input.rs @@ -176,7 +176,7 @@ pub fn Input( let (background, cursor_char) = if focus.is_focused() { ( theme.hover_background, - editable.editor().read().visible_cursor_pos().to_string(), + editable.editor().read().cursor_pos().to_string(), ) } else { (theme.background, "none".to_string()) @@ -210,7 +210,7 @@ pub fn Input( direction: "vertical", color: "{color}", background: "{background}", - border: "1 solid {border_fill}", + border: "1 inner {border_fill}", shadow: "{shadow}", corner_radius: "{corner_radius}", margin: "{margin}", @@ -221,6 +221,7 @@ pub fn Input( a11y_auto_focus: "{auto_focus}", onkeydown, onkeyup, + overflow: "clip", paragraph { margin: "8 12", onglobalclick, diff --git a/crates/components/src/lib.rs b/crates/components/src/lib.rs index a34e9b21a..28a05e90d 100644 --- a/crates/components/src/lib.rs +++ b/crates/components/src/lib.rs @@ -15,6 +15,7 @@ mod gesture_area; mod graph; mod hooks; mod icons; +mod image; mod input; mod link; mod loader; diff --git a/crates/components/src/link.rs b/crates/components/src/link.rs index cb3effc60..1aa6db3b9 100644 --- a/crates/components/src/link.rs +++ b/crates/components/src/link.rs @@ -15,7 +15,10 @@ use freya_hooks::{ }; use winit::event::MouseButton; -use crate::Tooltip; +use crate::{ + Tooltip, + TooltipContainer, +}; /// Tooltip configuration for the [`Link`] component. #[derive(Clone, PartialEq)] @@ -159,7 +162,7 @@ pub fn Link( Some(LinkTooltip::Custom(str)) => Some(str), }; - let main_rect = rsx! { + let link = rsx! { rect { onmouseenter, onmouseleave, @@ -169,27 +172,19 @@ pub fn Link( } }; - let Some(tooltip) = tooltip else { - return rsx!({ main_rect }); - }; - - rsx! { - rect { - {main_rect} - rect { - height: "0", - width: "0", - layer: "-999", - rect { - width: "100v", - if *is_hovering.read() { - Tooltip { - url: tooltip - } + if let Some(tooltip) = tooltip { + rsx!( + TooltipContainer { + tooltip: rsx!( + Tooltip { + text: tooltip } - } + ) + {link} } - } + ) + } else { + link } } diff --git a/crates/components/src/popup.rs b/crates/components/src/popup.rs index 4a255daa2..68885634b 100644 --- a/crates/components/src/popup.rs +++ b/crates/components/src/popup.rs @@ -27,7 +27,7 @@ pub fn PopupBackground(children: Element) -> Element { position: "absolute", position_top: "0", position_left: "0", - layer: "-99", + layer: "-2000", main_align: "center", cross_align: "center", {children} diff --git a/crates/components/src/progress_bar.rs b/crates/components/src/progress_bar.rs index f05cca583..df797f144 100644 --- a/crates/components/src/progress_bar.rs +++ b/crates/components/src/progress_bar.rs @@ -47,6 +47,8 @@ pub fn ProgressBar( height, } = use_applied_theme!(&theme, progress_bar); + let progress = progress.clamp(0., 100.); + rsx!( rect { width: "{width}", @@ -59,8 +61,7 @@ pub fn ProgressBar( background: "{background}", font_size: "13", direction: "horizontal", - border: "1 solid {background}", - border_align: "outer", + border: "1 outer {background}", rect { corner_radius: "999", width: "{progress}%", diff --git a/crates/components/src/radio.rs b/crates/components/src/radio.rs index 5f0847c40..58af74e11 100644 --- a/crates/components/src/radio.rs +++ b/crates/components/src/radio.rs @@ -69,7 +69,7 @@ pub fn Radio( unselected_fill }; let border = if focus.is_selected() { - format!("4 solid {}", border_fill) + format!("4 outer {}", border_fill) } else { "none".to_string() }; @@ -79,13 +79,12 @@ pub fn Radio( rsx!( rect { border, - border_align: "outer", corner_radius: "99", rect { a11y_id: focus.attribute(), width: "18", height: "18", - border: "2 solid {fill}", + border: "2 inner {fill}", padding: "4", main_align: "center", cross_align: "center", diff --git a/crates/components/src/slider.rs b/crates/components/src/slider.rs index 1e023a7ca..ce3a9d3b3 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::{ + KeyboardEvent, MouseEvent, WheelEvent, }, @@ -22,11 +23,13 @@ pub struct SliderProps { pub theme: Option, /// Handler for the `onmoved` event. pub onmoved: EventHandler, - /// Width of the Slider. + /// Size of the Slider. #[props(into, default = "100%".to_string())] - pub width: String, + pub size: String, /// Height of the Slider. pub value: f64, + #[props(default = "horizontal".to_string())] + pub direction: String, } #[inline] @@ -73,7 +76,7 @@ pub enum SliderStatus { /// "Value: {percentage}" /// } /// Slider { -/// width: "50%", +/// size: "50%", /// value: *percentage.read(), /// onmoved: move |p| { /// percentage.set(p); @@ -88,7 +91,8 @@ pub fn Slider( value, onmoved, theme, - width, + size, + direction, }: SliderProps, ) -> Element { let theme = use_applied_theme!(&theme, slider); @@ -96,8 +100,9 @@ pub fn Slider( let mut status = use_signal(SliderStatus::default); let mut clicking = use_signal(|| false); let platform = use_platform(); - let (node_reference, size) = use_node(); + let (node_reference, node_size) = use_node(); + let direction_is_vertical = direction == "vertical"; let value = ensure_correct_slider_range(value); let a11y_id = focus.attribute(); @@ -107,6 +112,30 @@ pub fn Slider( } }); + let onkeydown = move |e: KeyboardEvent| match e.key { + Key::ArrowLeft if !direction_is_vertical => { + e.stop_propagation(); + let percentage = (value - 4.).clamp(0.0, 100.0); + onmoved.call(percentage); + } + Key::ArrowRight if !direction_is_vertical => { + e.stop_propagation(); + let percentage = (value + 4.).clamp(0.0, 100.0); + onmoved.call(percentage); + } + Key::ArrowUp if direction_is_vertical => { + e.stop_propagation(); + let percentage = (value + 4.).clamp(0.0, 100.0); + onmoved.call(percentage); + } + Key::ArrowDown if direction_is_vertical => { + e.stop_propagation(); + let percentage = (value - 4.).clamp(0.0, 100.0); + onmoved.call(percentage); + } + _ => {} + }; + let onmouseleave = move |e: MouseEvent| { e.stop_propagation(); *status.write() = SliderStatus::Idle; @@ -125,8 +154,13 @@ pub fn Slider( e.stop_propagation(); if *clicking.peek() { let coordinates = e.get_element_coordinates(); - let x = coordinates.x - size.area.min_x() as f64 - 6.0; - let percentage = x / (size.area.width() as f64 - 15.0) * 100.0; + let percentage = if direction_is_vertical { + let y = coordinates.y - node_size.area.min_y() as f64 - 6.0; + 100. - (y / (node_size.area.height() as f64 - 15.0) * 100.0) + } else { + let x = coordinates.x - node_size.area.min_x() as f64 - 6.0; + x / (node_size.area.width() as f64 - 15.0) * 100.0 + }; let percentage = percentage.clamp(0.0, 100.0); onmoved.call(percentage); @@ -141,8 +175,13 @@ pub fn Slider( focus.focus(); clicking.set(true); let coordinates = e.get_element_coordinates(); - let x = coordinates.x - 6.0; - let percentage = x / (size.area.width() as f64 - 15.0) * 100.0; + let percentage = if direction_is_vertical { + let y = coordinates.y - 6.0; + 100. - (y / (node_size.area.height() as f64 - 15.0) * 100.0) + } else { + let x = coordinates.x - 6.0; + x / (node_size.area.width() as f64 - 15.0) * 100.0 + }; let percentage = percentage.clamp(0.0, 100.0); onmoved.call(percentage); @@ -162,18 +201,83 @@ pub fn Slider( onmoved.call(percentage); }; - let inner_width = (size.area.width() - 15.0) * (value / 100.0) as f32; let border = if focus.is_selected() { - format!("2 solid {}", theme.border_fill) + format!("2 inner {}", theme.border_fill) } else { "none".to_string() }; + let ( + width, + height, + container_width, + container_height, + inner_width, + inner_height, + main_align, + offset_x, + offset_y, + ) = if direction_is_vertical { + let inner_height = (node_size.area.height() - 15.0) * (value / 100.0) as f32; + ( + "20", + size.as_str(), + "6", + "100%", + "100%".to_string(), + inner_height.to_string(), + "end", + -6, + 3, + ) + } else { + let inner_width = (node_size.area.width() - 15.0) * (value / 100.0) as f32; + ( + size.as_str(), + "20", + "100%", + "6", + inner_width.to_string(), + "100%".to_string(), + "start", + -3, + -6, + ) + }; + + let inner_fill = rsx!(rect { + background: "{theme.thumb_inner_background}", + width: "{inner_width}", + height: "{inner_height}", + corner_radius: "50" + }); + + let thumb = rsx!( + rect { + width: "fill", + offset_x: "{offset_x}", + offset_y: "{offset_y}", + rect { + background: "{theme.thumb_background}", + width: "18", + height: "18", + corner_radius: "50", + padding: "4", + rect { + height: "100%", + width: "100%", + background: "{theme.thumb_inner_background}", + corner_radius: "50" + } + } + } + ); + rsx!( rect { reference: node_reference, width: "{width}", - height: "20", + height: "{height}", onmousedown, onglobalclick: onclick, a11y_id, @@ -181,40 +285,24 @@ pub fn Slider( onglobalmousemove: onmousemove, onmouseleave, onwheel: onwheel, + onkeydown, main_align: "center", cross_align: "center", border: "{border}", corner_radius: "8", rect { background: "{theme.background}", - width: "100%", - height: "6", - direction: "horizontal", + width: "{container_width}", + height: "{container_height}", + main_align: "{main_align}", + direction: "{direction}", corner_radius: "50", - rect { - background: "{theme.thumb_inner_background}", - width: "{inner_width}", - height: "100%", - corner_radius: "50" - } - rect { - width: "fill", - height: "100%", - offset_y: "-6", - offset_x: "-3", - rect { - background: "{theme.thumb_background}", - width: "18", - height: "18", - corner_radius: "50", - padding: "4", - rect { - height: "100%", - width: "100%", - background: "{theme.thumb_inner_background}", - corner_radius: "50" - } - } + if direction_is_vertical { + {thumb} + {inner_fill} + } else { + {inner_fill} + {thumb} } } } diff --git a/crates/components/src/snackbar.rs b/crates/components/src/snackbar.rs index a33770f01..826b1e201 100644 --- a/crates/components/src/snackbar.rs +++ b/crates/components/src/snackbar.rs @@ -20,18 +20,18 @@ use freya_hooks::{ /// ```no_run /// # use freya::prelude::*; /// fn app() -> Element { -/// let mut show = use_signal(|| false); +/// let mut open = use_signal(|| false); /// /// rsx!( /// rect { /// height: "100%", /// width: "100%", /// Button { -/// onpress: move |_| show.toggle(), +/// onpress: move |_| open.toggle(), /// label { "Open" } /// } /// SnackBar { -/// show, +/// open, /// label { /// "Hello, World!" /// } @@ -45,8 +45,8 @@ use freya_hooks::{ pub fn SnackBar( /// Inner children of the SnackBar. children: Element, - /// Signal to show the snackbar or not. - show: Signal, + /// Open or not the SnackBar. You can pass a [ReadOnlySignal] as well. + open: ReadOnlySignal, /// Theme override. theme: Option, ) -> Element { @@ -60,7 +60,7 @@ pub fn SnackBar( }); use_effect(move || { - if *show.read() { + if open() { animation.start(); } else if animation.peek_has_run_yet() { animation.reverse(); @@ -100,6 +100,7 @@ pub fn SnackBarBox(children: Element, theme: Option) -> Eleme padding: "10", color: "{color}", direction: "horizontal", + layer: "-1000", {children} } ) @@ -117,18 +118,18 @@ mod test { #[tokio::test] pub async fn snackbar() { fn snackbar_app() -> Element { - let mut show = use_signal(|| false); + let mut open = use_signal(|| false); rsx!( rect { height: "100%", width: "100%", Button { - onpress: move |_| show.toggle(), + onpress: move |_| open.toggle(), label { "Open" } } SnackBar { - show, + open, label { "Hello, World!" } @@ -148,7 +149,7 @@ mod test { // Open the snackbar by clicking at the button utils.click_cursor((15., 15.)).await; - // Wait a bit for the snackbar to show up + // Wait a bit for the snackbar to open up utils.wait_for_update().await; sleep(Duration::from_millis(15)).await; utils.wait_for_update().await; diff --git a/crates/components/src/switch.rs b/crates/components/src/switch.rs index 0b66f6fea..580cc2196 100644 --- a/crates/components/src/switch.rs +++ b/crates/components/src/switch.rs @@ -186,9 +186,9 @@ pub fn Switch(props: SwitchProps) -> Element { let border = if focus.is_selected() { if props.enabled { - format!("2 solid {}", theme.enabled_focus_border_fill) + format!("2 inner {}", theme.enabled_focus_border_fill) } else { - format!("2 solid {}", theme.focus_border_fill) + format!("2 inner {}", theme.focus_border_fill) } } else { "none".to_string() diff --git a/crates/components/src/tooltip.rs b/crates/components/src/tooltip.rs index 6ec61d01a..52b3ea94c 100644 --- a/crates/components/src/tooltip.rs +++ b/crates/components/src/tooltip.rs @@ -1,7 +1,11 @@ use dioxus::prelude::*; -use freya_elements::elements as dioxus_elements; +use freya_elements::{ + elements as dioxus_elements, + events::MouseEvent, +}; use freya_hooks::{ use_applied_theme, + use_node_signal, TooltipTheme, TooltipThemeWith, }; @@ -11,8 +15,8 @@ use freya_hooks::{ pub struct TooltipProps { /// Theme override. pub theme: Option, - /// Url as the Tooltip destination. - pub url: String, + /// Text to show in the [Tooltip]. + pub text: String, } /// `Tooltip` component @@ -20,7 +24,7 @@ pub struct TooltipProps { /// # Styling /// Inherits the [`TooltipTheme`](freya_hooks::TooltipTheme) #[allow(non_snake_case)] -pub fn Tooltip(TooltipProps { url, theme }: TooltipProps) -> Element { +pub fn Tooltip(TooltipProps { text, theme }: TooltipProps) -> Element { let theme = use_applied_theme!(&theme, tooltip); let TooltipTheme { background, @@ -31,12 +35,80 @@ pub fn Tooltip(TooltipProps { url, theme }: TooltipProps) -> Element { rsx!( rect { padding: "4 10", - shadow: "0 4 5 0 rgb(0, 0, 0, 0.1)", - border: "1 solid {border_fill}", - corner_radius: "10", + shadow: "0 0 4 1 rgb(0, 0, 0, 0.1)", + border: "1 inner {border_fill}", + corner_radius: "8", background: "{background}", - main_align: "center", - label { max_lines: "1", color: "{color}", "{url}" } + label { max_lines: "1", font_size: "14", color: "{color}", "{text}" } + } + ) +} + +#[derive(PartialEq, Clone, Copy, Debug)] +pub enum TooltipPosition { + Besides, + Below, +} + +/// `TooltipContainer` component. +/// +/// Provides a hoverable area where to show a [Tooltip]. +/// +/// # Example +#[component] +pub fn TooltipContainer( + tooltip: Element, + children: Element, + #[props(default = TooltipPosition::Below, into)] position: TooltipPosition, +) -> Element { + let mut is_hovering = use_signal(|| false); + let (reference, size) = use_node_signal(); + + let onmouseenter = move |_: MouseEvent| { + is_hovering.set(true); + }; + + let onmouseleave = move |_: MouseEvent| { + is_hovering.set(false); + }; + + let direction = match position { + TooltipPosition::Below => "vertical", + TooltipPosition::Besides => "horizontal", + }; + + rsx!( + rect { + direction, + reference, + onmouseenter, + onmouseleave, + {children}, + rect { + height: "0", + width: "0", + layer: "-1500", + if *is_hovering.read() { + match position { + TooltipPosition::Below => rsx!( + rect { + width: "{size.read().area.width()}", + cross_align: "center", + padding: "5 0 0 0", + {tooltip} + } + ), + TooltipPosition::Besides => rsx!( + rect { + height: "{size.read().area.height()}", + main_align: "center", + padding: "0 0 0 5", + {tooltip} + } + ), + } + } + } } ) } diff --git a/crates/core/src/accessibility/tree.rs b/crates/core/src/accessibility/tree.rs index ef5b1bfd9..d427b22d2 100644 --- a/crates/core/src/accessibility/tree.rs +++ b/crates/core/src/accessibility/tree.rs @@ -163,8 +163,13 @@ impl AccessibilityTree { } // Mark the still existing ancenstors as modified - for (node_id, ancestor_node_id) in removed_ids { - added_or_updated_ids.insert(ancestor_node_id); + for (_, ancestor_node_id) in removed_ids.iter() { + added_or_updated_ids.insert(*ancestor_node_id); + } + + // Remove all the deleted noeds from the added_or_update list + for (node_id, _) in removed_ids { + added_or_updated_ids.remove(&node_id); self.map.retain(|_, id| *id != node_id); } diff --git a/crates/core/src/elements/paragraph.rs b/crates/core/src/elements/paragraph.rs index d18e0c354..2b595aa21 100644 --- a/crates/core/src/elements/paragraph.rs +++ b/crates/core/src/elements/paragraph.rs @@ -138,7 +138,7 @@ impl ElementUtils for ParagraphElement { }; if node_cursor_state.position.is_some() { - let (paragraph, _) = create_paragraph( + let paragraph = create_paragraph( node_ref, &area.size, font_collection, diff --git a/crates/core/src/elements/rect.rs b/crates/core/src/elements/rect.rs index 3be23687c..55bc235c0 100644 --- a/crates/core/src/elements/rect.rs +++ b/crates/core/src/elements/rect.rs @@ -1,9 +1,10 @@ use freya_engine::prelude::*; use freya_native_core::real_dom::NodeImmutable; use freya_node_state::{ + Border, BorderAlignment, - BorderStyle, CanvasRunnerContext, + CornerRadius, Fill, ReferencesState, ShadowPosition, @@ -12,7 +13,6 @@ use freya_node_state::{ use torin::{ prelude::{ Area, - AreaModel, CursorPoint, LayoutNode, Point2D, @@ -24,6 +24,11 @@ use torin::{ use super::utils::ElementUtils; use crate::dom::DioxusNode; +enum BorderShape { + DRRect(RRect, RRect), + Path(Path), +} + pub struct RectElement; impl RectElement { @@ -48,6 +53,207 @@ 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 { @@ -87,10 +293,9 @@ impl ElementUtils for RectElement { ) { let node_style = &*node_ref.get::().unwrap(); - let mut paint = Paint::default(); - let mut path = Path::new(); let area = layout_node.visible_area().to_f32(); - + let mut path = Path::new(); + let mut paint = Paint::default(); paint.set_anti_alias(true); paint.set_style(PaintStyle::Fill); @@ -109,22 +314,22 @@ impl ElementUtils for RectElement { } } - let mut radius = node_style.corner_radius; - radius.scale(scale_factor); + let mut corner_radius = node_style.corner_radius; + corner_radius.scale(scale_factor); let rounded_rect = RRect::new_rect_radii( Rect::new(area.min_x(), area.min_y(), area.max_x(), area.max_y()), &[ - (radius.top_left, radius.top_left).into(), - (radius.top_right, radius.top_right).into(), - (radius.bottom_right, radius.bottom_right).into(), - (radius.bottom_left, radius.bottom_left).into(), + (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(), ], ); - if radius.smoothing > 0.0 { + if corner_radius.smoothing > 0.0 { path.add_path( - &radius.smoothed_path(rounded_rect), + &corner_radius.smoothed_path(rounded_rect), (area.min_x(), area.min_y()), None, ); @@ -138,8 +343,10 @@ impl ElementUtils for RectElement { for mut shadow in node_style.shadows.clone().into_iter() { if shadow.fill != Fill::Color(Color::TRANSPARENT) { shadow.scale(scale_factor); - let mut shadow_paint = paint.clone(); + let mut shadow_path = Path::new(); + let mut shadow_paint = Paint::default(); + shadow_paint.set_anti_alias(true); match &shadow.fill { Fill::Color(color) => { @@ -181,7 +388,7 @@ impl ElementUtils for RectElement { } // Add either the RRect or smoothed path based on whether smoothing is used. - if radius.smoothing > 0.0 { + if corner_radius.smoothing > 0.0 { shadow_path.add_path( &node_style .corner_radius @@ -212,56 +419,39 @@ impl ElementUtils for RectElement { } // Borders - if node_style.border.width > 0.0 && node_style.border.style != BorderStyle::None { - let mut border_width = node_style.border.width; - border_width *= scale_factor; - - // Create a new paint and path - let mut border_paint = paint.clone(); - let mut border_path = Path::new(); - - // Setup paint params - border_paint.set_anti_alias(true); - border_paint.set_style(PaintStyle::Stroke); - match &node_style.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)); + for mut border in node_style.borders.clone().into_iter() { + 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)); + } } - } - border_paint.set_stroke_width(border_width); - - // Skia draws strokes centered on the edge of the path. This means that half of the stroke is inside the path, and half outside. - // For Inner and Outer borders, we need to grow or shrink the stroke path by half the border width. - let outset = Point::new(border_width / 2.0, border_width / 2.0) - * match node_style.border.alignment { - BorderAlignment::Center => 0.0, - BorderAlignment::Inner => -1.0, - BorderAlignment::Outer => 1.0, - }; - // Add either the RRect or smoothed path based on whether smoothing is used. - if radius.smoothing > 0.0 { - border_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 { - border_path.add_rrect(rounded_rect.with_outset(outset), None); + 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); + } + } } - - canvas.draw_path(&border_path, &border_paint); } let references = node_ref.get::().unwrap(); @@ -281,8 +471,7 @@ impl ElementUtils for RectElement { fn element_needs_cached_area(&self, node_ref: &DioxusNode) -> bool { let node_style = &*node_ref.get::().unwrap(); - node_style.border.is_visible() && node_style.border.alignment != BorderAlignment::Inner - || !node_style.shadows.is_empty() + !node_style.borders.is_empty() || !node_style.shadows.is_empty() } fn element_drawing_area( @@ -294,10 +483,7 @@ impl ElementUtils for RectElement { let node_style = &*node_ref.get::().unwrap(); let mut area = layout_node.visible_area(); - if !node_style.border.is_visible() - && node_style.border.alignment != BorderAlignment::Inner - && node_style.shadows.is_empty() - { + if node_style.borders.is_empty() && node_style.shadows.is_empty() { return area; } @@ -370,49 +556,22 @@ impl ElementUtils for RectElement { } } - if node_style.border.width > 0.0 && node_style.border.style != BorderStyle::None { - let mut border_width = node_style.border.width; - border_width *= scale_factor; + for mut border in node_style.borders.clone().into_iter() { + if border.is_visible() { + border.scale(scale_factor); - // Create a new paint and path - let mut border_path = Path::new(); - - // Skia draws strokes centered on the edge of the path. This means that half of the stroke is inside the path, and half outside. - // For Inner and Outer borders, we need to grow or shrink the stroke path by half the border width. - let outset = Point::new(border_width / 2.0, border_width / 2.0) - * match node_style.border.alignment { - BorderAlignment::Center => 0.0, - BorderAlignment::Inner => -1.0, - BorderAlignment::Outer => 1.0, + let border_shape = + Self::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(), }; - - // Add either the RRect or smoothed path based on whether smoothing is used. - if radius.smoothing > 0.0 { - border_path.add_path( - &node_style - .corner_radius - .smoothed_path(rounded_rect.with_outset(outset)), - Point::new(area.min_x(), area.min_y()) - outset, - None, + let border_area = Area::new( + Point2D::new(border_bounds.x(), border_bounds.y()), + Size2D::new(border_bounds.width(), border_bounds.height()), ); - } else { - border_path.add_rrect(rounded_rect.with_outset(outset), None); - } - let border_bounds = border_path.bounds(); - let border_area = Area::new( - Point2D::new(border_bounds.x(), border_bounds.y()), - Size2D::new(border_bounds.width(), border_bounds.height()), - ); - area = area.union(&border_area.round_out()); - - match node_style.border.alignment { - BorderAlignment::Outer => area.expand(&Size2D::new( - node_style.border.width, - node_style.border.width, - )), - BorderAlignment::Center => area.expand(&Size2D::new(border_width, border_width)), - _ => {} + area = area.union(&border_area.round_out()); } } diff --git a/crates/core/src/node.rs b/crates/core/src/node.rs index 11509017d..ef616509e 100644 --- a/crates/core/src/node.rs +++ b/crates/core/src/node.rs @@ -109,7 +109,6 @@ impl NodeState { }; ("background", fill) }, - ("border", AttributeType::Border(&self.style.border)), ( "corner_radius", AttributeType::CornerRadius(self.style.corner_radius), @@ -125,7 +124,7 @@ impl NodeState { ), ( "line_height", - AttributeType::Measure(self.font_style.line_height), + AttributeType::OptionalMeasure(self.font_style.line_height), ), ( "text_align", @@ -145,6 +144,11 @@ impl NodeState { attributes.push(("shadow", AttributeType::Shadow(shadow))); } + let borders = &self.style.borders; + for border in borders { + attributes.push(("border", AttributeType::Border(border))); + } + let text_shadows = &self.font_style.text_shadows; for text_shadow in text_shadows { @@ -160,6 +164,7 @@ pub enum AttributeType<'a> { Gradient(Fill), Size(&'a Size), Measure(f32), + OptionalMeasure(Option), Measures(Gaps), CornerRadius(CornerRadius), Direction(&'a DirectionMode), diff --git a/crates/core/src/render/pipeline.rs b/crates/core/src/render/pipeline.rs index 429342891..a477aaabb 100644 --- a/crates/core/src/render/pipeline.rs +++ b/crates/core/src/render/pipeline.rs @@ -138,7 +138,7 @@ impl RenderPipeline<'_> { // Render the dirty nodes for (_, nodes) in sorted(rendering_layers.iter()) { - 'elements: for node_id in nodes { + 'elements: for node_id in sorted(nodes) { let node_ref = self.rdom.get(*node_id).unwrap(); let node_viewports = node_ref.get::().unwrap(); let layout_node = self.layout.get(*node_id); diff --git a/crates/core/src/render/skia_measurer.rs b/crates/core/src/render/skia_measurer.rs index dd98f41a8..d79d347cc 100644 --- a/crates/core/src/render/skia_measurer.rs +++ b/crates/core/src/render/skia_measurer.rs @@ -26,7 +26,6 @@ use torin::prelude::{ Alignment, Area, LayoutMeasurer, - LayoutNode, Node, Size2D, }; @@ -69,21 +68,21 @@ impl<'a> LayoutMeasurer for SkiaMeasurer<'a> { match &*node_type { NodeType::Element(ElementNode { tag, .. }) if tag == &TagName::Label => { - let (label, paragraph_font_height) = create_label( + let label = create_label( &node, area_size, self.font_collection, self.default_fonts, self.scale_factor, ); - - let res = Size2D::new(label.longest_line(), label.height()); + let height = label.height(); + let res = Size2D::new(label.longest_line(), height); let mut map = SendAnyMap::new(); - map.insert(CachedParagraph(label, paragraph_font_height)); + map.insert(CachedParagraph(label, height)); Some((res, Arc::new(map))) } NodeType::Element(ElementNode { tag, .. }) if tag == &TagName::Paragraph => { - let (paragraph, paragraph_font_height) = create_paragraph( + let paragraph = create_paragraph( &node, area_size, self.font_collection, @@ -91,9 +90,10 @@ impl<'a> LayoutMeasurer for SkiaMeasurer<'a> { self.default_fonts, self.scale_factor, ); - let res = Size2D::new(paragraph.longest_line(), paragraph.height()); + let height = paragraph.height(); + let res = Size2D::new(paragraph.longest_line(), height); let mut map = SendAnyMap::new(); - map.insert(CachedParagraph(paragraph, paragraph_font_height)); + map.insert(CachedParagraph(paragraph, height)); Some((res, Arc::new(map))) } _ => None, @@ -110,14 +110,14 @@ impl<'a> LayoutMeasurer for SkiaMeasurer<'a> { .unwrap_or_default() } - fn notify_layout_references(&self, node_id: NodeId, layout_node: &LayoutNode) { + fn notify_layout_references(&self, node_id: NodeId, area: Area, inner_sizes: Size2D) { let node = self.rdom.get(node_id).unwrap(); let size_state = &*node.get::().unwrap(); if let Some(reference) = &size_state.node_ref { let mut node_layout = NodeReferenceLayout { - area: layout_node.area, - inner: layout_node.inner_sizes, + area, + inner: inner_sizes, }; node_layout.div(self.scale_factor); reference.0.send(node_layout).ok(); @@ -131,7 +131,7 @@ pub fn create_label( font_collection: &FontCollection, default_font_family: &[String], scale_factor: f32, -) -> (Paragraph, f32) { +) -> Paragraph { let font_style = &*node.get::().unwrap(); let mut paragraph_style = ParagraphStyle::default(); @@ -143,7 +143,7 @@ pub fn create_label( paragraph_style.set_ellipsis(ellipsis); } - let text_style = font_style.text_style(default_font_family, scale_factor, true); + 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); @@ -155,18 +155,15 @@ pub fn create_label( } let mut paragraph = paragraph_builder.build(); - paragraph.layout(area_size.width + 1.0); - - // Measure the actual text height, ignoring the line height - let mut height = paragraph.height(); - for line in paragraph.get_line_metrics() { - for (_, text) in line.get_style_metrics(0..1) { - let text_height = -(text.font_metrics.ascent - text.font_metrics.descent); - height = height.max(text_height); - } - } - - (paragraph, height) + paragraph.layout( + if font_style.max_lines == Some(1) && font_style.text_align == TextAlign::default() { + f32::MAX + } else { + area_size.width + 1.0 + }, + ); + + paragraph } /// Align the Y axis of the highlights and cursor of a paragraph @@ -224,7 +221,7 @@ pub fn create_paragraph( is_rendering: bool, default_font_family: &[String], scale_factor: f32, -) -> (Paragraph, f32) { +) -> Paragraph { let font_style = &*node.get::().unwrap(); let mut paragraph_style = ParagraphStyle::default(); @@ -238,13 +235,10 @@ pub fn create_paragraph( let mut paragraph_builder = ParagraphBuilder::new(¶graph_style, font_collection); - let text_style = font_style.text_style(default_font_family, scale_factor, true); + let text_style = font_style.text_style(default_font_family, scale_factor); paragraph_builder.push_style(&text_style); - let node_children = node.children(); - let node_children_len = node_children.len(); - - for text_span in node_children { + for text_span in node.children() { if let NodeType::Element(ElementNode { tag: TagName::Text, .. }) = &*text_span.node_type() @@ -253,7 +247,7 @@ pub fn create_paragraph( 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, true); + 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 { @@ -268,16 +262,13 @@ pub fn create_paragraph( } let mut paragraph = paragraph_builder.build(); - paragraph.layout(area_size.width + 1.0); - - // Measure the actual text height, ignoring the line height - let mut height = paragraph.height(); - for line in paragraph.get_line_metrics() { - for (_, text) in line.get_style_metrics(0..node_children_len) { - let text_height = -(text.font_metrics.ascent - text.font_metrics.descent); - height = height.max(text_height); - } - } - - (paragraph, height) + paragraph.layout( + if font_style.max_lines == Some(1) && font_style.text_align == TextAlign::default() { + f32::MAX + } else { + area_size.width + 1.0 + }, + ); + + paragraph } diff --git a/crates/devtools/src/property.rs b/crates/devtools/src/property.rs index 38b9f5847..6b283cb08 100644 --- a/crates/devtools/src/property.rs +++ b/crates/devtools/src/property.rs @@ -195,7 +195,7 @@ pub fn BorderProperty(name: String, border: Border) -> Element { text { font_size: "15", color: "rgb(252,181,172)", - "{border.width} {border.style:?} {border.alignment:?}" + "{border.width} {border.alignment:?}" } } rect { diff --git a/crates/devtools/src/tabs/style.rs b/crates/devtools/src/tabs/style.rs index d99e75e06..c8186db47 100644 --- a/crates/devtools/src/tabs/style.rs +++ b/crates/devtools/src/tabs/style.rs @@ -38,6 +38,15 @@ pub fn NodeInspectorStyle(node_id: String) -> Element { } } } + AttributeType::OptionalMeasure(measure) => { + rsx!{ + Property { + key: "{i}", + name: "{name}", + value: measure.map(|measure| measure.to_string()).unwrap_or_else(|| "inherit".to_string()) + } + } + } AttributeType::Measures(measures) => { rsx!{ Property { diff --git a/crates/elements/src/_docs/attributes/border.md b/crates/elements/src/_docs/attributes/border.md index b653171db..74a95f3c4 100644 --- a/crates/elements/src/_docs/attributes/border.md +++ b/crates/elements/src/_docs/attributes/border.md @@ -1,17 +1,65 @@ -### border & border_align +### border -You can add a border to an element using the `border` and `border_align` attributes. -- `border` syntax: `[width] [color]`. -- `border_align` syntax: ``. +You can add borders to an element using the `border` attribute. +- `border` syntax: `[width] [width?] [width?] [width?] [fill]`. + +1-4 width values should be provided with the `border` attribute. Widths will be applied to different sides of a `rect` depending on the number of values provided: +- One value: `all` +- Two values: `vertical`, `horizontal` +- Three values: `top` `horizontal` `bottom` +- Four values: `top` `right` `bottom` `left` + +*Border alignment* determines how the border is positioned relative to the element's edge. Alignment can be `inner`, `outer`, or `center`. + +### Examples + +A solid, black border with a width of 2 pixels on every side. Border is aligned to the inside of the rect's edge. + +```rust, no_run +# use freya::prelude::*; +fn app() -> Element { + rsx!( + rect { + border: "2 inner black", + } + ) +} +``` + +A solid, red border with different widths on each side. Border is aligned to the center of the rect's edge. + +```rust, no_run +# use freya::prelude::*; +fn app() -> Element { + rsx!( + rect { + border: "1 2 3 4 center red", + } + ) +} +``` + +Borders can take any valid fill type, including gradients. + +```rust, no_run +# use freya::prelude::*; +fn app() -> Element { + rsx!( + rect { + border: "1 inner linear-gradient(red, green, yellow 40%, blue)", + } + ) +} +``` + +Similarly to the `shadow` attribute, multiple borders can be drawn on a single element when separated by a comma. Borders specified later in the list are drawn on top of previous ones. -### Example ```rust, no_run # use freya::prelude::*; fn app() -> Element { rsx!( rect { - border: "2 solid black", - border_align: "inner" + border: "6 outer red, 5 outer orange, 4 outer yellow, 3 outer green, 2 outer blue, 1 outer purple", } ) } diff --git a/crates/elements/src/_docs/attributes/main_align_cross_align.md b/crates/elements/src/_docs/attributes/main_align_cross_align.md index d49ef4b75..5dce81502 100644 --- a/crates/elements/src/_docs/attributes/main_align_cross_align.md +++ b/crates/elements/src/_docs/attributes/main_align_cross_align.md @@ -2,7 +2,7 @@ Control how the inner elements are positioned inside the element. You can combine it with the `direction` attribute to create complex flows. -Accepted values for both attributes are: +Accepted values for `main_align`: - `start` (default): At the begining of the axis - `center`: At the center of the axis @@ -11,6 +11,12 @@ Accepted values for both attributes are: - `space-around` (only for `main_align`): Distributed among the available space with small margins in the sides - `space-evenly` (only for `main_align`): Distributed among the available space with the same size of margins in the sides and in between the elements. +Accepted values for `cross_align`: + +- `start` (default): At the begining of the axis (same as in `main_align`) +- `center`: At the center of the axis (same as in `main_align`) +- `end`: At the end of the axis (same as in `main_align`) + When using the `vertical` direction, `main_align` will be the Y axis and `cross_align` will be the X axis. But when using the `horizontal` direction, the `main_align` will be the X axis and the `cross_align` will be the Y axis. diff --git a/crates/elements/src/definitions.rs b/crates/elements/src/definitions.rs index e297c857f..ad5c9bdcb 100644 --- a/crates/elements/src/definitions.rs +++ b/crates/elements/src/definitions.rs @@ -187,7 +187,6 @@ builder_constructors! { background: String, #[doc = include_str!("_docs/attributes/border.md")] border: String, - border_align: String, #[doc = include_str!("_docs/attributes/direction.md")] direction: String, #[doc = include_str!("_docs/attributes/shadow.md")] diff --git a/crates/engine/src/mocked.rs b/crates/engine/src/mocked.rs index a55c8f8bd..cfeb00df0 100644 --- a/crates/engine/src/mocked.rs +++ b/crates/engine/src/mocked.rs @@ -1041,6 +1041,15 @@ impl Canvas { unimplemented!("This is mocked") } + pub fn draw_drrect( + &self, + outer: impl AsRef, + inner: impl AsRef, + paint: &Paint, + ) -> &Self { + unimplemented!("This is mocked") + } + pub fn draw_path(&self, _path: &Path, _paint: &Paint) -> &Self { unimplemented!("This is mocked") } @@ -1333,6 +1342,10 @@ impl Path { pub fn offset(&mut self, _d: impl Into) -> &mut Self { unimplemented!("This is mocked") } + + pub fn set_fill_type(&mut self, _ft: PathFillType) -> &mut Self { + unimplemented!("This is mocked") + } } #[repr(i32)] @@ -1357,6 +1370,14 @@ impl RRect { unimplemented!("This is mocked") } + pub fn rect(&self) -> &Rect { + unimplemented!("This is mocked") + } + + pub fn bounds(&self) -> &Rect { + unimplemented!("This is mocked") + } + pub fn width(&self) -> f32 { unimplemented!("This is mocked") } @@ -1394,6 +1415,15 @@ pub enum Corner { LowerLeft = 3, } +#[repr(i32)] +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] +pub enum PathFillType { + Winding = 0, + EvenOdd = 1, + InverseWinding = 2, + InverseEvenOdd = 3, +} + #[repr(i32)] #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] pub enum PathDirection { diff --git a/crates/engine/src/skia.rs b/crates/engine/src/skia.rs index f3524415a..c9c73a9b5 100644 --- a/crates/engine/src/skia.rs +++ b/crates/engine/src/skia.rs @@ -78,6 +78,7 @@ pub use skia_safe::{ PaintStyle, Path, PathDirection, + PathFillType, Point, RRect, Rect, diff --git a/crates/freya/Cargo.toml b/crates/freya/Cargo.toml index a8f3dfdc8..8797c7eda 100644 --- a/crates/freya/Cargo.toml +++ b/crates/freya/Cargo.toml @@ -26,6 +26,7 @@ mocked-engine-development = ["freya-engine/mocked-engine"] # This is just for th default = ["skia"] performance-overlay = [] fade-cached-incremental-areas = ["freya-core/fade-cached-incremental-areas"] +disable-zoom-shortcuts = ["freya-renderer/disable-zoom-shortcuts"] [dependencies] freya-devtools = { workspace = true, optional = true } diff --git a/crates/hooks/src/editor_history.rs b/crates/hooks/src/editor_history.rs index e10bdd638..2569c9c0b 100644 --- a/crates/hooks/src/editor_history.rs +++ b/crates/hooks/src/editor_history.rs @@ -2,9 +2,21 @@ use ropey::Rope; #[derive(Clone)] pub enum HistoryChange { - InsertChar { idx: usize, char: char }, - InsertText { idx: usize, text: String }, - Remove { idx: usize, text: String }, + InsertChar { + idx: usize, + len: usize, + ch: char, + }, + InsertText { + idx: usize, + len: usize, + text: String, + }, + Remove { + idx: usize, + len: usize, + text: String, + }, } #[derive(Default, Clone)] @@ -55,16 +67,21 @@ impl EditorHistory { let last_change = self.changes.get(self.current_change - 1); if let Some(last_change) = last_change { let idx_end = match last_change { - HistoryChange::Remove { idx, text } => { - rope.insert(*idx, text); - idx + text.chars().count() + HistoryChange::Remove { idx, text, len } => { + let start = rope.utf16_cu_to_char(*idx); + rope.insert(start, text); + *idx + len } - HistoryChange::InsertChar { idx, char: ch } => { - rope.remove(*idx..*idx + ch.len_utf8()); + HistoryChange::InsertChar { idx, len, .. } => { + let start = rope.utf16_cu_to_char(*idx); + let end = rope.utf16_cu_to_char(*idx + len); + rope.remove(start..end); *idx } - HistoryChange::InsertText { idx, text } => { - rope.remove(*idx..idx + text.len()); + HistoryChange::InsertText { idx, len, .. } => { + let start = rope.utf16_cu_to_char(*idx); + let end = rope.utf16_cu_to_char(*idx + len); + rope.remove(start..end); *idx } }; @@ -84,17 +101,21 @@ impl EditorHistory { let next_change = self.changes.get(self.current_change); if let Some(next_change) = next_change { let idx_end = match next_change { - HistoryChange::Remove { idx, text } => { - rope.remove(*idx..idx + text.chars().count()); + HistoryChange::Remove { idx, len, .. } => { + let start = rope.utf16_cu_to_char(*idx); + let end = rope.utf16_cu_to_char(*idx + len); + rope.remove(start..end); *idx } - HistoryChange::InsertChar { idx, char: ch } => { - rope.insert_char(*idx, *ch); - idx + 1 + HistoryChange::InsertChar { idx, ch, len } => { + let start = rope.utf16_cu_to_char(*idx); + rope.insert_char(start, *ch); + *idx + len } - HistoryChange::InsertText { idx, text, .. } => { - rope.insert(*idx, text); - idx + text.chars().count() + HistoryChange::InsertText { idx, text, len } => { + let start = rope.utf16_cu_to_char(*idx); + rope.insert(start, text); + *idx + len } }; self.current_change += 1; @@ -131,6 +152,7 @@ mod test { history.push_change(HistoryChange::InsertText { idx: 11, text: "\n!!!!".to_owned(), + len: "\n!!!!".len(), }); assert!(history.can_undo()); @@ -149,16 +171,19 @@ mod test { history.push_change(HistoryChange::InsertText { idx: 11, text: "\n!!!!".to_owned(), + len: "\n!!!!".len(), }); rope.insert(16, "\n!!!!"); history.push_change(HistoryChange::InsertText { idx: 16, text: "\n!!!!".to_owned(), + len: "\n!!!!".len(), }); rope.insert(21, "\n!!!!"); history.push_change(HistoryChange::InsertText { idx: 21, text: "\n!!!!".to_owned(), + len: "\n!!!!".len(), }); assert_eq!(history.any_pending_changes(), 0); @@ -202,7 +227,11 @@ mod test { // Dischard any changes that could have been redone rope.insert_char(0, '.'); - history.push_change(HistoryChange::InsertChar { idx: 0, char: '.' }); + history.push_change(HistoryChange::InsertChar { + idx: 0, + ch: '.', + len: 1, + }); assert_eq!(history.any_pending_changes(), 0); } } diff --git a/crates/hooks/src/rope_editor.rs b/crates/hooks/src/rope_editor.rs index 3eeceb6f8..017e95e0f 100644 --- a/crates/hooks/src/rope_editor.rs +++ b/crates/hooks/src/rope_editor.rs @@ -66,27 +66,60 @@ impl TextEditor for RopeEditor { LinesIterator { lines } } - fn insert_char(&mut self, char: char, idx: usize) { - self.history - .push_change(HistoryChange::InsertChar { idx, char }); - self.rope.insert_char(idx, char); + fn insert_char(&mut self, ch: char, idx: usize) -> usize { + let idx_utf8 = self.utf16_cu_to_char(idx); + + let len_before_insert = self.rope.len_utf16_cu(); + self.rope.insert_char(idx_utf8, ch); + let len_after_insert = self.rope.len_utf16_cu(); + + let inserted_text_len = len_after_insert - len_before_insert; + + self.history.push_change(HistoryChange::InsertChar { + idx, + ch, + len: inserted_text_len, + }); + + inserted_text_len } - fn insert(&mut self, text: &str, idx: usize) { + fn insert(&mut self, text: &str, idx: usize) -> usize { + let idx_utf8 = self.utf16_cu_to_char(idx); + + let len_before_insert = self.rope.len_utf16_cu(); + self.rope.insert(idx_utf8, text); + let len_after_insert = self.rope.len_utf16_cu(); + + let inserted_text_len = len_after_insert - len_before_insert; + self.history.push_change(HistoryChange::InsertText { idx, text: text.to_owned(), + len: inserted_text_len, }); - self.rope.insert(idx, text); + + inserted_text_len } - fn remove(&mut self, range: Range) { + fn remove(&mut self, range_utf16: Range) -> usize { + let range = + self.utf16_cu_to_char(range_utf16.start)..self.utf16_cu_to_char(range_utf16.end); let text = self.rope.slice(range.clone()).to_string(); + + let len_before_remove = self.rope.len_utf16_cu(); + self.rope.remove(range); + let len_after_remove = self.rope.len_utf16_cu(); + + let removed_text_len = len_before_remove - len_after_remove; + self.history.push_change(HistoryChange::Remove { - idx: range.start, + idx: range_utf16.end - removed_text_len, text, + len: removed_text_len, }); - self.rope.remove(range) + + removed_text_len } fn char_to_line(&self, char_idx: usize) -> usize { @@ -108,7 +141,10 @@ impl TextEditor for RopeEditor { fn line(&self, line_idx: usize) -> Option> { let line = self.rope.get_line(line_idx); - line.map(|line| Line { text: line.into() }) + line.map(|line| Line { + text: line.into(), + utf16_len: line.len_utf16_cu(), + }) } fn len_lines(&self) -> usize { @@ -119,6 +155,10 @@ impl TextEditor for RopeEditor { self.rope.len_chars() } + fn len_utf16_cu(&self) -> usize { + self.rope.len_utf16_cu() + } + fn cursor(&self) -> &TextCursor { &self.cursor } @@ -152,24 +192,21 @@ impl TextEditor for RopeEditor { let (selected_from, selected_to) = self.selected?; if self.mode == EditableMode::SingleLineMultipleEditors { - let selected_to_row = self.char_to_line(selected_to); - let selected_from_row = self.char_to_line(selected_from); - - let selected_to_line = self.char_to_line(selected_to); - let selected_from_line = self.char_to_line(selected_from); + let selected_from_row = self.char_to_line(self.utf16_cu_to_char(selected_from)); + let selected_to_row = self.char_to_line(self.utf16_cu_to_char(selected_to)); - let editor_row_idx = self.line_to_char(editor_id); - let selected_to_row_idx = self.line_to_char(selected_to_line); - let selected_from_row_idx = self.line_to_char(selected_from_line); + let editor_row_idx = self.char_to_utf16_cu(self.line_to_char(editor_id)); + let selected_from_row_idx = self.char_to_utf16_cu(self.line_to_char(selected_from_row)); + let selected_to_row_idx = self.char_to_utf16_cu(self.line_to_char(selected_to_row)); - let selected_to_col_idx = selected_to - selected_to_row_idx; let selected_from_col_idx = selected_from - selected_from_row_idx; + let selected_to_col_idx = selected_to - selected_to_row_idx; // Between starting line and endling line if (editor_id > selected_from_row && editor_id < selected_to_row) || (editor_id < selected_from_row && editor_id > selected_to_row) { - let len = self.line(editor_id).unwrap().len_chars(); + let len = self.line(editor_id).unwrap().utf16_len(); return Some((0, len)); } @@ -181,7 +218,7 @@ impl TextEditor for RopeEditor { Some((0, selected_from_col_idx)) } else if selected_to_row == editor_id { // Ending line - let len = self.line(selected_to_row).unwrap().len_chars(); + let len = self.line(selected_to_row).unwrap().utf16_len(); Some((selected_to_col_idx, len)) } else { None @@ -191,7 +228,7 @@ impl TextEditor for RopeEditor { Ordering::Less => { if selected_from_row == editor_id { // Starting line - let len = self.line(selected_from_row).unwrap().len_chars(); + let len = self.line(selected_from_row).unwrap().utf16_len(); Some((selected_from_col_idx, len)) } else if selected_to_row == editor_id { // Ending line @@ -207,12 +244,9 @@ impl TextEditor for RopeEditor { _ => None, }; - highlights.map(|(from, to)| (self.char_to_utf16_cu(from), self.char_to_utf16_cu(to))) + highlights } else { - Some(( - self.char_to_utf16_cu(selected_from), - self.char_to_utf16_cu(selected_to), - )) + Some((selected_from, selected_to)) } } @@ -231,6 +265,7 @@ impl TextEditor for RopeEditor { fn measure_new_selection(&self, from: usize, to: usize, editor_id: usize) -> (usize, usize) { if self.mode == EditableMode::SingleLineMultipleEditors { let row_idx = self.line_to_char(editor_id); + let row_idx = self.char_to_utf16_cu(row_idx); if let Some((start, _)) = self.selected { (start, row_idx + to) } else { @@ -246,7 +281,7 @@ impl TextEditor for RopeEditor { fn measure_new_cursor(&self, to: usize, editor_id: usize) -> TextCursor { if self.mode == EditableMode::SingleLineMultipleEditors { let row_char = self.line_to_char(editor_id); - let pos = row_char + to; + let pos = self.char_to_utf16_cu(row_char) + to; TextCursor::new(pos) } else { TextCursor::new(to) @@ -260,6 +295,9 @@ impl TextEditor for RopeEditor { fn get_selected_text(&self) -> Option { let (start, end) = self.get_selection_range()?; + let start = self.utf16_cu_to_char(start); + let end = self.utf16_cu_to_char(end); + Some(self.rope().get_slice(start..end)?.to_string()) } @@ -300,6 +338,9 @@ impl<'a> Iterator for LinesIterator<'a> { fn next(&mut self) -> Option { let line = self.lines.next(); - line.map(|line| Line { text: line.into() }) + line.map(|line| Line { + text: line.into(), + utf16_len: line.len_utf16_cu(), + }) } } diff --git a/crates/hooks/src/text_editor.rs b/crates/hooks/src/text_editor.rs index cd0ce6c92..936c3f13f 100644 --- a/crates/hooks/src/text_editor.rs +++ b/crates/hooks/src/text_editor.rs @@ -42,15 +42,13 @@ impl TextCursor { #[derive(Clone)] pub struct Line<'a> { pub text: Cow<'a, str>, + pub utf16_len: usize, } impl Line<'_> { /// Get the length of the line - pub fn len_chars(&self) -> usize { - self.text - .chars() - .filter(|c| c != &'\r' && c != &'\n') - .count() + pub fn utf16_len(&self) -> usize { + self.utf16_len } } @@ -85,13 +83,13 @@ pub trait TextEditor { fn lines(&self) -> Self::LinesIterator<'_>; /// Insert a character in the text in the given position. - fn insert_char(&mut self, char: char, char_idx: usize); + fn insert_char(&mut self, char: char, char_idx: usize) -> usize; /// Insert a string in the text in the given position. - fn insert(&mut self, text: &str, char_idx: usize); + fn insert(&mut self, text: &str, char_idx: usize) -> usize; /// Remove a part of the text. - fn remove(&mut self, range: Range); + fn remove(&mut self, range: Range) -> usize; /// Get line from the given char fn char_to_line(&self, char_idx: usize) -> usize; @@ -112,6 +110,9 @@ pub trait TextEditor { /// Total of chars fn len_chars(&self) -> usize; + /// Total of utf16 code units + fn len_utf16_cu(&self) -> usize; + /// Get a readable cursor fn cursor(&self) -> &TextCursor; @@ -121,14 +122,17 @@ pub trait TextEditor { /// Get the cursor row fn cursor_row(&self) -> usize { let pos = self.cursor_pos(); - self.char_to_line(pos) + let pos_utf8 = self.utf16_cu_to_char(pos); + self.char_to_line(pos_utf8) } /// Get the cursor column fn cursor_col(&self) -> usize { let pos = self.cursor_pos(); - let line = self.char_to_line(pos); - let line_char = self.line_to_char(line); + let pos_utf8 = self.utf16_cu_to_char(pos); + let line = self.char_to_line(pos_utf8); + let line_char_utf8 = self.line_to_char(line); + let line_char = self.char_to_utf16_cu(line_char_utf8); pos - line_char } @@ -137,11 +141,6 @@ pub trait TextEditor { (self.cursor_row(), self.cursor_col()) } - /// Get the visible cursor position - fn visible_cursor_col(&self) -> usize { - self.char_to_utf16_cu(self.cursor_col()) - } - /// Move the cursor 1 line down fn cursor_down(&mut self) -> bool { let old_row = self.cursor_row(); @@ -151,15 +150,15 @@ pub trait TextEditor { Ordering::Less => { // One line below let new_row = old_row + 1; - let new_row_char = self.line_to_char(new_row); - let new_row_len = self.line(new_row).unwrap().len_chars(); + let new_row_char = self.char_to_utf16_cu(self.line_to_char(new_row)); + let new_row_len = self.line(new_row).unwrap().utf16_len(); let new_col = old_col.min(new_row_len); self.cursor_mut().set(new_row_char + new_col); true } Ordering::Equal => { - let end = self.len_chars(); + let end = self.len_utf16_cu(); // Reached max self.cursor_mut().set(end); @@ -186,7 +185,7 @@ pub trait TextEditor { } else { let new_row = old_row - 1; let new_row_char = self.line_to_char(new_row); - let new_row_len = self.line(new_row).unwrap().len_chars(); + let new_row_len = self.line(new_row).unwrap().utf16_len(); let new_col = old_col.min(new_row_len); self.cursor_mut().set(new_row_char + new_col); } @@ -199,7 +198,7 @@ pub trait TextEditor { /// Move the cursor 1 char to the right fn cursor_right(&mut self) -> bool { - if self.cursor_pos() < self.len_chars() { + if self.cursor_pos() < self.len_utf16_cu() { *self.cursor_mut().write() += 1; true @@ -224,11 +223,6 @@ pub trait TextEditor { self.cursor().pos() } - /// Get the cursor position - fn visible_cursor_pos(&self) -> usize { - self.char_to_utf16_cu(self.cursor_pos()) - } - /// Set the cursor position fn set_cursor_pos(&mut self, pos: usize) { self.cursor_mut().set(pos); @@ -338,31 +332,31 @@ pub trait TextEditor { } } Key::Backspace => { - let char_idx = self.line_to_char(self.cursor_row()) + self.cursor_col(); + let cursor_pos = self.cursor_pos(); let selection = self.get_selection_range(); if let Some((start, end)) = selection { self.remove(start..end); self.set_cursor_pos(start); event.insert(TextEvent::TEXT_CHANGED); - } else if char_idx > 0 { + } else if cursor_pos > 0 { // Remove the character to the left if there is any - self.remove(char_idx - 1..char_idx); - self.set_cursor_pos(char_idx - 1); + let removed_text_len = self.remove(cursor_pos - 1..cursor_pos); + self.set_cursor_pos(cursor_pos - removed_text_len); event.insert(TextEvent::TEXT_CHANGED); } } Key::Delete => { - let char_idx = self.line_to_char(self.cursor_row()) + self.cursor_col(); + let cursor_pos = self.cursor_pos(); let selection = self.get_selection_range(); if let Some((start, end)) = selection { self.remove(start..end); self.set_cursor_pos(start); event.insert(TextEvent::TEXT_CHANGED); - } else if char_idx < self.len_chars() { + } else if cursor_pos < self.len_utf16_cu() { // Remove the character to the right if there is any - self.remove(char_idx..char_idx + 1); + self.remove(cursor_pos..cursor_pos + 1); event.insert(TextEvent::TEXT_CHANGED); } } @@ -403,7 +397,7 @@ pub trait TextEditor { // Select all text Code::KeyA if meta_or_ctrl => { - let len = self.len_chars(); + let len = self.len_utf16_cu(); self.set_selection((0, len)); event.remove(TextEvent::SELECTION_CHANGED); } @@ -433,9 +427,9 @@ pub trait TextEditor { Code::KeyV if meta_or_ctrl => { let copied_text = self.get_clipboard().get(); if let Ok(copied_text) = copied_text { - let char_idx = self.line_to_char(self.cursor_row()) + self.cursor_col(); - self.insert(&copied_text, char_idx); - let last_idx = copied_text.len() + char_idx; + let cursor_pos = self.cursor_pos(); + self.insert(&copied_text, cursor_pos); + let last_idx = copied_text.encode_utf16().count() + cursor_pos; self.set_cursor_pos(last_idx); event.insert(TextEvent::TEXT_CHANGED); } @@ -453,9 +447,9 @@ pub trait TextEditor { // Redo last change Code::KeyY if meta_or_ctrl => { - let undo_result = self.redo(); + let redo_result = self.redo(); - if let Some(idx) = undo_result { + if let Some(idx) = redo_result { self.set_cursor_pos(idx); event.insert(TextEvent::TEXT_CHANGED); } @@ -473,15 +467,15 @@ pub trait TextEditor { if let Ok(ch) = character.parse::() { // Inserts a character let cursor_pos = self.cursor_pos(); - self.insert_char(ch, cursor_pos); - self.cursor_right(); + let inserted_text_len = self.insert_char(ch, cursor_pos); + self.set_cursor_pos(cursor_pos + inserted_text_len); event.insert(TextEvent::TEXT_CHANGED); } else { // Inserts a text let cursor_pos = self.cursor_pos(); - self.insert(character, cursor_pos); - self.set_cursor_pos(cursor_pos + character.chars().count()); + let inserted_text_len = self.insert(character, cursor_pos); + self.set_cursor_pos(cursor_pos + inserted_text_len); event.insert(TextEvent::TEXT_CHANGED); } diff --git a/crates/hooks/src/theming/base.rs b/crates/hooks/src/theming/base.rs new file mode 100644 index 000000000..6371c4eea --- /dev/null +++ b/crates/hooks/src/theming/base.rs @@ -0,0 +1,231 @@ +use crate::{ + cow_borrowed, + theming::*, +}; + +pub(crate) const BASE_THEME: Theme = Theme { + name: "base", + colors: ColorsSheet { + primary: cow_borrowed!(""), + secondary: cow_borrowed!(""), + tertiary: cow_borrowed!(""), + surface: cow_borrowed!(""), + secondary_surface: cow_borrowed!(""), + neutral_surface: cow_borrowed!(""), + focused_surface: cow_borrowed!(""), + opposite_surface: cow_borrowed!(""), + secondary_opposite_surface: cow_borrowed!(""), + tertiary_opposite_surface: cow_borrowed!(""), + background: cow_borrowed!(""), + focused_border: cow_borrowed!(""), + solid: cow_borrowed!(""), + color: cow_borrowed!(""), + placeholder_color: cow_borrowed!(""), + highlight_color: cow_borrowed!(""), + }, + body: BodyTheme { + background: cow_borrowed!("key(background)"), + color: cow_borrowed!("key(color)"), + padding: cow_borrowed!("none"), + }, + slider: SliderTheme { + background: cow_borrowed!("key(surface)"), + thumb_background: cow_borrowed!("key(secondary)"), + thumb_inner_background: cow_borrowed!("key(primary)"), + border_fill: cow_borrowed!("key(surface)"), + }, + button: ButtonTheme { + background: cow_borrowed!("key(neutral_surface)"), + hover_background: cow_borrowed!("key(focused_surface)"), + font_theme: FontTheme { + color: cow_borrowed!("key(color)"), + }, + border_fill: cow_borrowed!("key(surface)"), + 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"), + }, + input: InputTheme { + background: cow_borrowed!("key(neutral_surface)"), + hover_background: cow_borrowed!("key(focused_surface)"), + font_theme: FontTheme { + color: cow_borrowed!("key(color)"), + }, + placeholder_font_theme: FontTheme { + color: cow_borrowed!("key(placeholder_color)"), + }, + border_fill: cow_borrowed!("key(surface)"), + width: cow_borrowed!("150"), + margin: cow_borrowed!("0"), + corner_radius: cow_borrowed!("10"), + shadow: cow_borrowed!("0 4 5 0 rgb(0, 0, 0, 0.1)"), + }, + switch: SwitchTheme { + margin: cow_borrowed!("0"), + background: cow_borrowed!("key(secondary_surface)"), + thumb_background: cow_borrowed!("key(opposite_surface)"), + enabled_background: cow_borrowed!("key(secondary)"), + enabled_thumb_background: cow_borrowed!("key(primary)"), + focus_border_fill: cow_borrowed!("key(focused_border)"), + enabled_focus_border_fill: cow_borrowed!("key(focused_border)"), + }, + scroll_bar: ScrollBarTheme { + background: cow_borrowed!("key(secondary_surface)"), + thumb_background: cow_borrowed!("key(opposite_surface)"), + hover_thumb_background: cow_borrowed!("key(secondary_opposite_surface)"), + active_thumb_background: cow_borrowed!("key(tertiary_opposite_surface)"), + size: cow_borrowed!("15"), + }, + tooltip: TooltipTheme { + background: cow_borrowed!("key(neutral_surface)"), + color: cow_borrowed!("key(color)"), + border_fill: cow_borrowed!("key(surface)"), + }, + dropdown: DropdownTheme { + width: cow_borrowed!("auto"), + margin: cow_borrowed!("0"), + dropdown_background: cow_borrowed!("key(background)"), + background_button: cow_borrowed!("key(neutral_surface)"), + hover_background: cow_borrowed!("key(focused_surface)"), + font_theme: FontTheme { + color: cow_borrowed!("key(color)"), + }, + border_fill: cow_borrowed!("key(surface)"), + arrow_fill: cow_borrowed!("key(solid)"), + }, + dropdown_item: DropdownItemTheme { + background: cow_borrowed!("key(background)"), + select_background: cow_borrowed!("key(neutral_surface)"), + hover_background: cow_borrowed!("key(focused_surface)"), + font_theme: FontTheme { + color: cow_borrowed!("key(color)"), + }, + }, + accordion: AccordionTheme { + color: cow_borrowed!("key(color)"), + background: cow_borrowed!("key(neutral_surface)"), + border_fill: cow_borrowed!("key(surface)"), + }, + loader: LoaderTheme { + primary_color: cow_borrowed!("key(tertiary_opposite_surface)"), + }, + link: LinkTheme { + highlight_color: cow_borrowed!("key(highlight_color)"), + }, + progress_bar: ProgressBarTheme { + color: cow_borrowed!("white"), + background: cow_borrowed!("key(surface)"), + progress_background: cow_borrowed!("key(primary)"), + width: cow_borrowed!("fill"), + height: cow_borrowed!("20"), + }, + table: TableTheme { + font_theme: FontTheme { + color: cow_borrowed!("key(color)"), + }, + background: cow_borrowed!("key(background)"), + 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)"), + height: cow_borrowed!("auto"), + corner_radius: cow_borrowed!("6"), + shadow: cow_borrowed!("0 2 15 5 rgb(35, 35, 35, 70)"), + }, + canvas: CanvasTheme { + width: cow_borrowed!("300"), + height: cow_borrowed!("150"), + background: cow_borrowed!("white"), + }, + graph: GraphTheme { + width: cow_borrowed!("100%"), + height: cow_borrowed!("100%"), + }, + network_image: NetworkImageTheme { + width: cow_borrowed!("100%"), + height: cow_borrowed!("100%"), + }, + icon: IconTheme { + width: cow_borrowed!("10"), + height: cow_borrowed!("10"), + margin: cow_borrowed!("0"), + }, + sidebar: SidebarTheme { + spacing: cow_borrowed!("4"), + background: cow_borrowed!("key(neutral_surface)"), + font_theme: FontTheme { + color: cow_borrowed!("key(color)"), + }, + }, + sidebar_item: SidebarItemTheme { + margin: cow_borrowed!("0"), + background: cow_borrowed!("transparent"), + hover_background: cow_borrowed!("key(focused_surface)"), + font_theme: FontTheme { + color: cow_borrowed!("key(color)"), + }, + }, + tile: TileTheme { + padding: cow_borrowed!("4 6"), + }, + radio: RadioTheme { + unselected_fill: cow_borrowed!("key(solid)"), + selected_fill: cow_borrowed!("key(primary)"), + border_fill: cow_borrowed!("key(surface)"), + }, + checkbox: CheckboxTheme { + unselected_fill: cow_borrowed!("key(solid)"), + selected_fill: cow_borrowed!("key(primary)"), + selected_icon_fill: cow_borrowed!("key(secondary)"), + border_fill: cow_borrowed!("key(surface)"), + }, + menu_item: MenuItemTheme { + hover_background: cow_borrowed!("key(focused_surface)"), + corner_radius: cow_borrowed!("8"), + font_theme: FontTheme { + color: cow_borrowed!("key(color)"), + }, + }, + menu_container: MenuContainerTheme { + background: cow_borrowed!("key(neutral_surface)"), + padding: cow_borrowed!("4"), + shadow: cow_borrowed!("0 2 5 2 rgb(0, 0, 0, 0.1)"), + }, + snackbar: SnackBarTheme { + background: cow_borrowed!("key(focused_surface)"), + color: cow_borrowed!("key(color)"), + }, + popup: PopupTheme { + background: cow_borrowed!("key(background)"), + color: cow_borrowed!("key(color)"), + cross_fill: cow_borrowed!("key(solid)"), + width: cow_borrowed!("350"), + height: cow_borrowed!("200"), + }, + tab: TabTheme { + background: cow_borrowed!("key(neutral_surface)"), + hover_background: cow_borrowed!("key(focused_surface)"), + font_theme: FontTheme { + color: cow_borrowed!("key(color)"), + }, + border_fill: cow_borrowed!("none"), + focus_border_fill: cow_borrowed!("key(focused_border)"), + padding: cow_borrowed!("8 16"), + width: cow_borrowed!("auto"), + height: cow_borrowed!("auto"), + }, + bottom_tab: BottomTabTheme { + background: cow_borrowed!("transparent"), + hover_background: cow_borrowed!("key(secondary_surface)"), + font_theme: FontTheme { + color: cow_borrowed!("key(color)"), + }, + padding: cow_borrowed!("8 10"), + width: cow_borrowed!("auto"), + height: cow_borrowed!("auto"), + }, +}; diff --git a/crates/hooks/src/theming/dark.rs b/crates/hooks/src/theming/dark.rs deleted file mode 100644 index 4ffea4418..000000000 --- a/crates/hooks/src/theming/dark.rs +++ /dev/null @@ -1,212 +0,0 @@ -use crate::{ - cow_borrowed, - theming::*, -}; - -pub const DARK_THEME: Theme = Theme { - name: "dark", - body: BodyTheme { - background: cow_borrowed!("rgb(25, 25, 25)"), - color: cow_borrowed!("white"), - padding: LIGHT_THEME.body.padding, - }, - slider: SliderTheme { - background: cow_borrowed!("rgb(60, 60, 60)"), - thumb_background: cow_borrowed!("rgb(103, 80, 164)"), - thumb_inner_background: cow_borrowed!("rgb(202, 193, 227)"), - border_fill: cow_borrowed!("rgb(103, 80, 164)"), - }, - button: ButtonTheme { - background: cow_borrowed!("rgb(35, 35, 35)"), - hover_background: cow_borrowed!("rgb(45, 45, 45)"), - font_theme: FontTheme { - color: cow_borrowed!("white"), - }, - border_fill: cow_borrowed!("rgb(80, 80, 80)"), - focus_border_fill: cow_borrowed!("rgb(110, 110, 110)"), - shadow: cow_borrowed!("0 4 5 0 rgb(0, 0, 0, 0.1)"), - padding: LIGHT_THEME.button.padding, - margin: LIGHT_THEME.button.margin, - corner_radius: LIGHT_THEME.button.corner_radius, - width: LIGHT_THEME.button.width, - height: LIGHT_THEME.button.height, - }, - input: InputTheme { - background: cow_borrowed!("rgb(35, 35, 35)"), - hover_background: cow_borrowed!("rgb(45, 45, 45)"), - font_theme: FontTheme { - color: cow_borrowed!("white"), - }, - placeholder_font_theme: FontTheme { - color: cow_borrowed!("rgb(210, 210, 210)"), - }, - border_fill: cow_borrowed!("rgb(80, 80, 80)"), - width: LIGHT_THEME.input.width, - margin: LIGHT_THEME.input.margin, - corner_radius: LIGHT_THEME.input.corner_radius, - shadow: LIGHT_THEME.input.shadow, - }, - switch: SwitchTheme { - margin: LIGHT_THEME.input.margin, - background: cow_borrowed!("rgb(60, 60, 60)"), - thumb_background: cow_borrowed!("rgb(200, 200, 200)"), - enabled_background: cow_borrowed!("rgb(202, 193, 227)"), - enabled_thumb_background: cow_borrowed!("rgb(103, 80, 164)"), - focus_border_fill: cow_borrowed!("rgb(110, 110, 110)"), - enabled_focus_border_fill: cow_borrowed!("rgb(170, 170, 170)"), - }, - scroll_bar: ScrollBarTheme { - background: cow_borrowed!("rgb(35, 35, 35)"), - thumb_background: cow_borrowed!("rgb(100, 100, 100)"), - hover_thumb_background: cow_borrowed!("rgb(120, 120, 120)"), - active_thumb_background: cow_borrowed!("rgb(140, 140, 140)"), - size: LIGHT_THEME.scroll_bar.size, - }, - tooltip: TooltipTheme { - background: cow_borrowed!("rgb(35,35,35)"), - color: cow_borrowed!("rgb(240,240,240)"), - border_fill: cow_borrowed!("rgb(80, 80, 80)"), - }, - dropdown: DropdownTheme { - width: LIGHT_THEME.dropdown.width, - margin: LIGHT_THEME.dropdown.margin, - dropdown_background: cow_borrowed!("rgb(25, 25, 25)"), - background_button: cow_borrowed!("rgb(35, 35, 35)"), - hover_background: cow_borrowed!("rgb(45, 45, 45)"), - font_theme: FontTheme { - color: cow_borrowed!("white"), - }, - border_fill: cow_borrowed!("rgb(80, 80, 80)"), - arrow_fill: cow_borrowed!("rgb(150, 150, 150)"), - }, - dropdown_item: DropdownItemTheme { - background: cow_borrowed!("rgb(35, 35, 35)"), - select_background: cow_borrowed!("rgb(80, 80, 80)"), - hover_background: cow_borrowed!("rgb(55, 55, 55)"), - font_theme: FontTheme { - color: cow_borrowed!("white"), - }, - }, - accordion: AccordionTheme { - color: cow_borrowed!("white"), - background: cow_borrowed!("rgb(60, 60, 60)"), - border_fill: cow_borrowed!("rgb(80, 80, 80)"), - }, - loader: LoaderTheme { - primary_color: cow_borrowed!("rgb(150, 150, 150)"), - }, - link: LinkTheme { - highlight_color: cow_borrowed!("rgb(43,106,208)"), - }, - progress_bar: ProgressBarTheme { - color: cow_borrowed!("black"), - background: cow_borrowed!("rgb(60, 60, 60)"), - progress_background: cow_borrowed!("rgb(202, 193, 227)"), - width: LIGHT_THEME.progress_bar.width, - height: LIGHT_THEME.progress_bar.height, - }, - table: TableTheme { - font_theme: FontTheme { - color: cow_borrowed!("white"), - }, - background: cow_borrowed!("rgb(25, 25, 25)"), - arrow_fill: cow_borrowed!("rgb(150, 150, 150)"), - row_background: cow_borrowed!("transparent"), - alternate_row_background: cow_borrowed!("rgb(50, 50, 50)"), - divider_fill: cow_borrowed!("rgb(100, 100, 100)"), - height: LIGHT_THEME.table.height, - corner_radius: LIGHT_THEME.table.corner_radius, - shadow: LIGHT_THEME.table.shadow, - }, - canvas: CanvasTheme { - width: LIGHT_THEME.canvas.width, - height: LIGHT_THEME.canvas.height, - background: cow_borrowed!("white"), - }, - graph: GraphTheme { - width: LIGHT_THEME.graph.width, - height: LIGHT_THEME.graph.height, - }, - network_image: NetworkImageTheme { - width: LIGHT_THEME.network_image.width, - height: LIGHT_THEME.network_image.height, - }, - icon: IconTheme { - width: LIGHT_THEME.icon.width, - height: LIGHT_THEME.icon.height, - margin: LIGHT_THEME.icon.margin, - }, - sidebar: SidebarTheme { - spacing: LIGHT_THEME.sidebar.spacing, - background: cow_borrowed!("rgb(20, 20, 20)"), - font_theme: FontTheme { - color: cow_borrowed!("white"), - }, - }, - sidebar_item: SidebarItemTheme { - margin: LIGHT_THEME.sidebar_item.margin, - background: cow_borrowed!("transparent"), - hover_background: cow_borrowed!("rgb(45, 45, 45)"), - font_theme: FontTheme { - color: cow_borrowed!("white"), - }, - }, - tile: TileTheme { - padding: LIGHT_THEME.tile.padding, - }, - radio: RadioTheme { - unselected_fill: cow_borrowed!("rgb(245, 245, 245)"), - selected_fill: cow_borrowed!("rgb(202, 193, 227)"), - border_fill: cow_borrowed!("rgb(103, 80, 164)"), - }, - checkbox: CheckboxTheme { - unselected_fill: cow_borrowed!("rgb(245, 245, 245)"), - selected_fill: cow_borrowed!("rgb(202, 193, 227)"), - selected_icon_fill: cow_borrowed!("rgb(103, 80, 164)"), - }, - menu_item: MenuItemTheme { - hover_background: cow_borrowed!("rgb(45, 45, 45)"), - corner_radius: LIGHT_THEME.menu_item.corner_radius, - font_theme: FontTheme { - color: cow_borrowed!("white"), - }, - }, - menu_container: MenuContainerTheme { - background: cow_borrowed!("rgb(35, 35, 35)"), - padding: LIGHT_THEME.menu_container.padding, - shadow: LIGHT_THEME.menu_container.shadow, - }, - snackbar: SnackBarTheme { - background: cow_borrowed!("rgb(35, 35, 35)"), - color: cow_borrowed!("white"), - }, - popup: PopupTheme { - background: cow_borrowed!("rgb(25, 25, 25)"), - color: cow_borrowed!("white"), - cross_fill: cow_borrowed!("rgb(150, 150, 150)"), - width: LIGHT_THEME.popup.width, - height: LIGHT_THEME.popup.height, - }, - tab: TabTheme { - background: cow_borrowed!("rgb(35, 35, 35)"), - hover_background: cow_borrowed!("rgb(45, 45, 45)"), - font_theme: FontTheme { - color: cow_borrowed!("white"), - }, - border_fill: cow_borrowed!("none"), - focus_border_fill: cow_borrowed!("rgb(110, 110, 110)"), - padding: LIGHT_THEME.button.padding, - width: LIGHT_THEME.button.width, - height: LIGHT_THEME.button.height, - }, - bottom_tab: BottomTabTheme { - background: cow_borrowed!("transparent"), - hover_background: cow_borrowed!("rgb(45, 45, 45)"), - font_theme: FontTheme { - color: cow_borrowed!("white"), - }, - padding: LIGHT_THEME.button.padding, - width: LIGHT_THEME.button.width, - height: LIGHT_THEME.button.height, - }, -}; diff --git a/crates/hooks/src/theming/light.rs b/crates/hooks/src/theming/light.rs deleted file mode 100644 index 3ac94cd3e..000000000 --- a/crates/hooks/src/theming/light.rs +++ /dev/null @@ -1,212 +0,0 @@ -use crate::{ - cow_borrowed, - theming::*, -}; - -pub const LIGHT_THEME: Theme = Theme { - name: "light", - body: BodyTheme { - background: cow_borrowed!("white"), - color: cow_borrowed!("black"), - padding: cow_borrowed!("none"), - }, - slider: SliderTheme { - background: cow_borrowed!("rgb(210, 210, 210)"), - thumb_background: cow_borrowed!("rgb(202, 193, 227)"), - thumb_inner_background: cow_borrowed!("rgb(103, 80, 164)"), - border_fill: cow_borrowed!("rgb(210, 210, 210)"), - }, - button: ButtonTheme { - background: cow_borrowed!("rgb(245, 245, 245)"), - hover_background: cow_borrowed!("rgb(235, 235, 235)"), - font_theme: FontTheme { - color: cow_borrowed!("rgb(10, 10, 10)"), - }, - border_fill: cow_borrowed!("rgb(210, 210, 210)"), - focus_border_fill: cow_borrowed!("rgb(180, 180, 180)"), - 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"), - }, - input: InputTheme { - background: cow_borrowed!("rgb(245, 245, 245)"), - hover_background: cow_borrowed!("rgb(235, 235, 235)"), - font_theme: FontTheme { - color: cow_borrowed!("rgb(10, 10, 10)"), - }, - placeholder_font_theme: FontTheme { - color: cow_borrowed!("rgb(100, 100, 100)"), - }, - border_fill: cow_borrowed!("rgb(210, 210, 210)"), - width: cow_borrowed!("150"), - margin: cow_borrowed!("0"), - corner_radius: cow_borrowed!("10"), - shadow: cow_borrowed!("0 4 5 0 rgb(0, 0, 0, 0.1)"), - }, - switch: SwitchTheme { - margin: cow_borrowed!("0"), - background: cow_borrowed!("rgb(225, 225, 225)"), - thumb_background: cow_borrowed!("rgb(125, 125, 125)"), - enabled_background: cow_borrowed!("rgb(202, 193, 227)"), - enabled_thumb_background: cow_borrowed!("rgb(103, 80, 164)"), - focus_border_fill: cow_borrowed!("rgb(180, 180, 180)"), - enabled_focus_border_fill: cow_borrowed!("rgb(180, 180, 180)"), - }, - scroll_bar: ScrollBarTheme { - background: cow_borrowed!("rgb(225, 225, 225)"), - thumb_background: cow_borrowed!("rgb(135, 135, 135)"), - hover_thumb_background: cow_borrowed!("rgb(115, 115, 115)"), - active_thumb_background: cow_borrowed!("rgb(95, 95, 95)"), - size: cow_borrowed!("15"), - }, - tooltip: TooltipTheme { - background: cow_borrowed!("rgb(245, 245, 245)"), - color: cow_borrowed!("rgb(25,25,25)"), - border_fill: cow_borrowed!("rgb(210, 210, 210)"), - }, - dropdown: DropdownTheme { - width: cow_borrowed!("auto"), - margin: cow_borrowed!("0"), - dropdown_background: cow_borrowed!("white"), - background_button: cow_borrowed!("rgb(245, 245, 245)"), - hover_background: cow_borrowed!("rgb(235, 235, 235)"), - font_theme: FontTheme { - color: cow_borrowed!("rgb(10, 10, 10)"), - }, - border_fill: cow_borrowed!("rgb(210, 210, 210)"), - arrow_fill: cow_borrowed!("rgb(40, 40, 40)"), - }, - dropdown_item: DropdownItemTheme { - background: cow_borrowed!("white"), - select_background: cow_borrowed!("rgb(240, 240, 240)"), - hover_background: cow_borrowed!("rgb(220, 220, 220)"), - font_theme: FontTheme { - color: cow_borrowed!("rgb(10, 10, 10)"), - }, - }, - accordion: AccordionTheme { - color: cow_borrowed!("black"), - background: cow_borrowed!("rgb(245, 245, 245)"), - border_fill: cow_borrowed!("rgb(210, 210, 210)"), - }, - loader: LoaderTheme { - primary_color: cow_borrowed!("rgb(50, 50, 50)"), - }, - link: LinkTheme { - highlight_color: cow_borrowed!("rgb(43,106,208)"), - }, - progress_bar: ProgressBarTheme { - color: cow_borrowed!("white"), - background: cow_borrowed!("rgb(210, 210, 210)"), - progress_background: cow_borrowed!("rgb(103, 80, 164)"), - width: cow_borrowed!("fill"), - height: cow_borrowed!("20"), - }, - table: TableTheme { - font_theme: FontTheme { - color: cow_borrowed!("black"), - }, - background: cow_borrowed!("white"), - arrow_fill: cow_borrowed!("rgb(40, 40, 40)"), - row_background: cow_borrowed!("transparent"), - alternate_row_background: cow_borrowed!("rgb(240, 240, 240)"), - divider_fill: cow_borrowed!("rgb(200, 200, 200)"), - height: cow_borrowed!("auto"), - corner_radius: cow_borrowed!("6"), - shadow: cow_borrowed!("0 2 15 5 rgb(35, 35, 35, 70)"), - }, - canvas: CanvasTheme { - width: cow_borrowed!("300"), - height: cow_borrowed!("150"), - background: cow_borrowed!("white"), - }, - graph: GraphTheme { - width: cow_borrowed!("100%"), - height: cow_borrowed!("100%"), - }, - network_image: NetworkImageTheme { - width: cow_borrowed!("100%"), - height: cow_borrowed!("100%"), - }, - icon: IconTheme { - width: cow_borrowed!("10"), - height: cow_borrowed!("10"), - margin: cow_borrowed!("0"), - }, - sidebar: SidebarTheme { - spacing: cow_borrowed!("4"), - background: cow_borrowed!("rgb(245, 245, 245)"), - font_theme: FontTheme { - color: cow_borrowed!("rgb(10, 10, 10)"), - }, - }, - sidebar_item: SidebarItemTheme { - margin: cow_borrowed!("0"), - background: cow_borrowed!("transparent"), - hover_background: cow_borrowed!("rgb(230, 230, 230)"), - font_theme: FontTheme { - color: cow_borrowed!("rgb(10, 10, 10)"), - }, - }, - tile: TileTheme { - padding: cow_borrowed!("4 6"), - }, - radio: RadioTheme { - unselected_fill: cow_borrowed!("rgb(35, 35, 35)"), - selected_fill: cow_borrowed!("rgb(103, 80, 164)"), - border_fill: cow_borrowed!("rgb(210, 210, 210)"), - }, - checkbox: CheckboxTheme { - unselected_fill: cow_borrowed!("rgb(80, 80, 80)"), - selected_fill: cow_borrowed!("rgb(103, 80, 164)"), - selected_icon_fill: cow_borrowed!("rgb(202, 193, 227)"), - }, - menu_item: MenuItemTheme { - hover_background: cow_borrowed!("rgb(235, 235, 235)"), - corner_radius: cow_borrowed!("8"), - font_theme: FontTheme { - color: cow_borrowed!("rgb(10, 10, 10)"), - }, - }, - menu_container: MenuContainerTheme { - background: cow_borrowed!("rgb(245, 245, 245)"), - padding: cow_borrowed!("4"), - shadow: cow_borrowed!("0 2 5 2 rgb(0, 0, 0, 0.1)"), - }, - snackbar: SnackBarTheme { - background: cow_borrowed!("rgb(235, 235, 235)"), - color: cow_borrowed!("rgb(103, 80, 164)"), - }, - popup: PopupTheme { - background: cow_borrowed!("white"), - color: cow_borrowed!("black"), - cross_fill: cow_borrowed!("rgb(40, 40, 40)"), - width: cow_borrowed!("350"), - height: cow_borrowed!("200"), - }, - tab: TabTheme { - background: cow_borrowed!("rgb(245, 245, 245)"), - hover_background: cow_borrowed!("rgb(235, 235, 235)"), - font_theme: FontTheme { - color: cow_borrowed!("rgb(10, 10, 10)"), - }, - border_fill: cow_borrowed!("none"), - focus_border_fill: cow_borrowed!("rgb(180, 180, 180)"), - padding: cow_borrowed!("8 16"), - width: cow_borrowed!("auto"), - height: cow_borrowed!("auto"), - }, - bottom_tab: BottomTabTheme { - background: cow_borrowed!("transparent"), - hover_background: cow_borrowed!("rgb(230, 230, 230)"), - font_theme: FontTheme { - color: cow_borrowed!("rgb(10, 10, 10)"), - }, - padding: cow_borrowed!("8 10"), - width: cow_borrowed!("auto"), - height: cow_borrowed!("auto"), - }, -}; diff --git a/crates/hooks/src/theming/mod.rs b/crates/hooks/src/theming/mod.rs index e1033fc74..10d9d23e3 100644 --- a/crates/hooks/src/theming/mod.rs +++ b/crates/hooks/src/theming/mod.rs @@ -1,5 +1,5 @@ -mod dark; -mod light; +mod base; +mod themes; #[doc(hidden)] pub use ::core::default::Default; @@ -7,8 +7,7 @@ pub use ::core::default::Default; pub use ::paste::paste; #[doc(hidden)] pub use ::std::borrow::Cow; -pub use dark::*; -pub use light::*; +pub use themes::*; /// Alias for `Cow::Borrowed`, because that's used a million times so shortening it is nice. /// Makes the code more readable. @@ -31,13 +30,9 @@ macro_rules! cow_borrowed { /// # struct Foo; /// define_theme! { /// %[component] -/// pub Test<'a> { +/// pub Test { /// %[cows] /// cow_string: str, -/// %[borrowed] -/// borrowed_data: &'a Foo, -/// %[owned] -/// owned_data: Bar, /// %[subthemes] /// font_theme: FontTheme, /// } @@ -58,20 +53,6 @@ macro_rules! define_theme { $cow_field_name:ident: $cow_field_ty:ty, )* )? - $( - %[borrowed$($borrowed_attr_control:tt)?] - $( - $(#[$borrowed_field_attrs:meta])* - $borrowed_field_name:ident: $borrowed_field_ty:ty, - )* - )? - $( - %[owned$($owned_attr_control:tt)?] - $( - $(#[$owned_field_attrs:meta])* - $owned_field_name:ident: $owned_field_ty:ty, - )* - )? $( %[subthemes$($subthemes_attr_control:tt)?] $( @@ -82,22 +63,12 @@ macro_rules! define_theme { }) => { $crate::define_theme!(NOTHING=$($($component_attr_control)?)?); $crate::define_theme!(NOTHING=$($($cows_attr_control)?)?); - $crate::define_theme!(NOTHING=$($($borrowed_attr_control)?)?); - $crate::define_theme!(NOTHING=$($($owned_attr_control)?)?); $crate::define_theme!(NOTHING=$($($subthemes_attr_control)?)?); $crate::paste! { #[derive(Default, Clone, Debug, PartialEq, Eq)] #[doc = "You can use this to change a theme for only one component, with the `theme` property."] $(#[$attrs])* $vis struct [<$name ThemeWith>] $(<$lifetime>)? { - $($( - $(#[$borrowed_field_attrs])* - pub $borrowed_field_name: Option<$borrowed_field_ty>, - )*)? - $($( - $(#[$owned_field_attrs])* - pub $owned_field_name: Option<$owned_field_ty>, - )*)? $($( $(#[$subtheme_field_attrs])* pub $subtheme_field_name: Option< [<$subtheme_field_ty_name With>] $(<$subtheme_field_ty_lifetime>)? >, @@ -112,14 +83,6 @@ macro_rules! define_theme { $(#[doc = "Theming properties for the `" $name "` component."] $($component_attr_control)?)? $(#[$attrs])* $vis struct [<$name Theme>] $(<$lifetime>)? { - $($( - $(#[$borrowed_field_attrs])* - pub $borrowed_field_name: $borrowed_field_ty, - )*)? - $($( - $(#[$owned_field_attrs])* - pub $owned_field_name: $owned_field_ty, - )*)? $($( $(#[$subtheme_field_attrs])* pub $subtheme_field_name: $subtheme_field_ty_name $(<$subtheme_field_ty_lifetime>)?, @@ -131,20 +94,19 @@ macro_rules! define_theme { } impl $(<$lifetime>)? [<$name Theme>] $(<$lifetime>)? { - #[doc = "Checks each field in `optional` and if it's `Some`, it overwrites the corresponding `self` field."] - pub fn apply_optional(&mut self, optional: & $($lifetime)? [<$name ThemeWith>]) { + + pub fn apply_colors(&mut self, colors: &$crate::ColorsSheet) { $($( - if let Some($borrowed_field_name) = optional.$borrowed_field_name { - self.$borrowed_field_name = $borrowed_field_name; - } + self.$subtheme_field_name.apply_colors(colors); )*)? $($( - if let Some($owned_field_name) = &optional.$owned_field_name { - self.$owned_field_name = $owned_field_name.clone(); - } + self.$cow_field_name = colors.resolve(self.$cow_field_name.clone()); )*)? + } + #[doc = "Checks each field in `optional` and if it's `Some`, it overwrites the corresponding `self` field."] + pub fn apply_optional(&mut self, optional: & $($lifetime)? [<$name ThemeWith>]) { $($( if let Some($subtheme_field_name) = &optional.$subtheme_field_name { self.$subtheme_field_name.apply_optional($subtheme_field_name); @@ -520,6 +482,7 @@ define_theme! { unselected_fill: str, selected_fill: str, selected_icon_fill: str, + border_fill: str, } } @@ -565,9 +528,59 @@ define_theme! { } } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ColorsSheet { + pub primary: Cow<'static, str>, + pub secondary: Cow<'static, str>, + pub tertiary: Cow<'static, str>, + pub surface: Cow<'static, str>, + pub secondary_surface: Cow<'static, str>, + pub neutral_surface: Cow<'static, str>, + pub focused_surface: Cow<'static, str>, + pub opposite_surface: Cow<'static, str>, + pub secondary_opposite_surface: Cow<'static, str>, + pub tertiary_opposite_surface: Cow<'static, str>, + pub background: Cow<'static, str>, + pub focused_border: Cow<'static, str>, + pub solid: Cow<'static, str>, + pub color: Cow<'static, str>, + pub placeholder_color: Cow<'static, str>, + pub highlight_color: Cow<'static, str>, +} + +impl ColorsSheet { + pub fn resolve(&self, val: Cow<'static, str>) -> Cow<'static, str> { + if val.starts_with("key") { + let key_val = val.replace("key(", "").replace(")", ""); + match key_val.as_str() { + "primary" => self.primary.clone(), + "secondary" => self.secondary.clone(), + "tertiary" => self.tertiary.clone(), + "surface" => self.surface.clone(), + "secondary_surface" => self.secondary_surface.clone(), + "neutral_surface" => self.neutral_surface.clone(), + "focused_surface" => self.focused_surface.clone(), + "opposite_surface" => self.opposite_surface.clone(), + "secondary_opposite_surface" => self.secondary_opposite_surface.clone(), + "tertiary_opposite_surface" => self.tertiary_opposite_surface.clone(), + "background" => self.background.clone(), + "focused_border" => self.focused_border.clone(), + "solid" => self.solid.clone(), + "color" => self.color.clone(), + "placeholder_color" => self.placeholder_color.clone(), + "highlight_color" => self.highlight_color.clone(), + _ => self.primary.clone(), + } + } else { + val + } + } +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct Theme { pub name: &'static str, + pub colors: ColorsSheet, pub body: BodyTheme, pub button: ButtonTheme, pub switch: SwitchTheme, diff --git a/crates/hooks/src/theming/themes.rs b/crates/hooks/src/theming/themes.rs new file mode 100644 index 000000000..2bcf56946 --- /dev/null +++ b/crates/hooks/src/theming/themes.rs @@ -0,0 +1,74 @@ +use super::base::BASE_THEME; +use crate::{ + cow_borrowed, + theming::*, +}; + +pub const DARK_THEME: Theme = Theme { + name: "dark", + colors: ColorsSheet { + primary: cow_borrowed!("rgb(103, 80, 164)"), + secondary: cow_borrowed!("rgb(202, 193, 227)"), + tertiary: cow_borrowed!("white"), + 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)"), + 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)"), + placeholder_color: cow_borrowed!("rgb(210, 210, 210)"), + highlight_color: cow_borrowed!("rgb(96, 145, 224)"), + }, + ..BASE_THEME +}; + +pub const LIGHT_THEME: Theme = Theme { + name: "light", + colors: ColorsSheet { + primary: cow_borrowed!("rgb(103, 80, 164)"), + secondary: cow_borrowed!("rgb(202, 193, 227)"), + tertiary: cow_borrowed!("white"), + surface: cow_borrowed!("rgb(210, 210, 210)"), + secondary_surface: cow_borrowed!("rgb(225, 225, 225)"), + neutral_surface: cow_borrowed!("rgb(245, 245, 245)"), + focused_surface: cow_borrowed!("rgb(235, 235, 235)"), + opposite_surface: cow_borrowed!("rgb(125, 125, 125)"), + secondary_opposite_surface: cow_borrowed!("rgb(110, 110, 125)"), + tertiary_opposite_surface: cow_borrowed!("rgb(90, 90, 90)"), + background: cow_borrowed!("rgb(250, 250, 250)"), + solid: cow_borrowed!("rgb(35, 35, 35)"), + focused_border: cow_borrowed!("rgb(180, 180, 180)"), + color: cow_borrowed!("rgb(10, 10, 10)"), + placeholder_color: cow_borrowed!("rgb(100, 100, 100)"), + highlight_color: cow_borrowed!("rgb(38, 89, 170)"), + }, + ..BASE_THEME +}; + +pub const BANANA_THEME: Theme = Theme { + name: "banana", + colors: ColorsSheet { + primary: cow_borrowed!("rgb(240, 200, 50)"), + secondary: cow_borrowed!("rgb(255, 250, 160)"), + tertiary: cow_borrowed!("rgb(255, 255, 240)"), + surface: cow_borrowed!("rgb(240, 229, 189)"), + secondary_surface: cow_borrowed!("rgb(250, 240, 210)"), + neutral_surface: cow_borrowed!("rgb(255, 245, 220)"), + focused_surface: cow_borrowed!("rgb(255, 238, 170)"), + opposite_surface: cow_borrowed!("rgb(139, 69, 19)"), + secondary_opposite_surface: cow_borrowed!("rgb(120, 80, 20)"), + tertiary_opposite_surface: cow_borrowed!("rgb(90, 60, 10)"), + background: cow_borrowed!("rgb(255, 255, 224)"), + solid: cow_borrowed!("rgb(110, 70, 10)"), + focused_border: cow_borrowed!("rgb(255, 239, 151)"), + color: cow_borrowed!("rgb(85, 60, 5)"), + 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_editable.rs b/crates/hooks/src/use_editable.rs index befdc8226..a2af7ce88 100644 --- a/crates/hooks/src/use_editable.rs +++ b/crates/hooks/src/use_editable.rs @@ -5,7 +5,10 @@ use dioxus_core::{ use_hook, AttributeValue, }; -use dioxus_sdk::clipboard::use_clipboard; +use dioxus_sdk::clipboard::{ + use_clipboard, + UseClipboard, +}; use dioxus_signals::{ Readable, Signal, @@ -116,7 +119,7 @@ impl TextDragging { } } -/// Manage an editable content. +/// Manage an editable text. #[derive(Clone, Copy, PartialEq)] pub struct UseEditable { pub(crate) editor: Signal, @@ -127,6 +130,88 @@ pub struct UseEditable { } impl UseEditable { + /// Manually create an editable content instead of using [use_editable]. + pub fn new_in_hook( + clipboard: UseClipboard, + platform: UsePlatform, + config: EditableConfig, + mode: EditableMode, + ) -> Self { + let text_id = Uuid::new_v4(); + let mut editor = Signal::new(RopeEditor::new( + config.content, + config.cursor, + config.identation, + mode, + clipboard, + EditorHistory::new(), + )); + let dragging = Signal::new(TextDragging::None); + let (cursor_sender, mut cursor_receiver) = unbounded_channel::(); + let cursor_reference = CursorReference { + text_id, + cursor_sender, + }; + + spawn(async move { + while let Some(message) = cursor_receiver.recv().await { + match message { + // Update the cursor position calculated by the layout + CursorLayoutResponse::CursorPosition { position, id } => { + let mut text_editor = editor.write(); + let new_cursor = text_editor.measure_new_cursor(position, id); + + // Only update and clear the selection if the cursor has changed + if *text_editor.cursor() != new_cursor { + *text_editor.cursor_mut() = new_cursor; + if let TextDragging::FromCursorToPoint { cursor: from, .. } = + &*dragging.read() + { + let to = text_editor.cursor_pos(); + text_editor.set_selection((*from, to)); + } else { + text_editor.clear_selection(); + } + } + } + // Update the text selections calculated by the layout + CursorLayoutResponse::TextSelection { from, to, id } => { + let current_cursor = editor.peek().cursor().clone(); + let current_selection = editor.peek().get_selection(); + + let maybe_new_cursor = editor.peek().measure_new_cursor(to, id); + let maybe_new_selection = editor.peek().measure_new_selection(from, to, id); + + // Update the text selection if it has changed + if let Some(current_selection) = current_selection { + if current_selection != maybe_new_selection { + let mut text_editor = editor.write(); + text_editor.set_selection(maybe_new_selection); + } + } else { + let mut text_editor = editor.write(); + text_editor.set_selection(maybe_new_selection); + } + + // Update the cursor if it has changed + if current_cursor != maybe_new_cursor { + let mut text_editor = editor.write(); + *text_editor.cursor_mut() = maybe_new_cursor; + } + } + } + } + }); + + UseEditable { + editor, + cursor_reference: Signal::new(cursor_reference.clone()), + dragging, + platform, + allow_tabs: config.allow_tabs, + } + } + /// Reference to the editor. pub fn editor(&self) -> &Signal { &self.editor @@ -294,90 +379,10 @@ impl EditableConfig { } } -/// Create a virtual text editor with it's own cursor and rope. +/// Hook to create an editable text. For manual creation use [UseEditable::new_in_hook]. pub fn use_editable(initializer: impl Fn() -> EditableConfig, mode: EditableMode) -> UseEditable { let platform = use_platform(); let clipboard = use_clipboard(); - use_hook(|| { - let text_id = Uuid::new_v4(); - let config = initializer(); - let mut editor = Signal::new(RopeEditor::new( - config.content, - config.cursor, - config.identation, - mode, - clipboard, - EditorHistory::new(), - )); - let dragging = Signal::new(TextDragging::None); - let (cursor_sender, mut cursor_receiver) = unbounded_channel::(); - let cursor_reference = CursorReference { - text_id, - cursor_sender, - }; - - spawn(async move { - while let Some(message) = cursor_receiver.recv().await { - match message { - // Update the cursor position calculated by the layout - CursorLayoutResponse::CursorPosition { position, id } => { - let mut text_editor = editor.write(); - let new_cursor = text_editor - .measure_new_cursor(text_editor.utf16_cu_to_char(position), id); - - // Only update and clear the selection if the cursor has changed - if *text_editor.cursor() != new_cursor { - *text_editor.cursor_mut() = new_cursor; - if let TextDragging::FromCursorToPoint { cursor: from, .. } = - &*dragging.read() - { - let to = text_editor.cursor_pos(); - text_editor.set_selection((*from, to)); - } else { - text_editor.clear_selection(); - } - } - } - // Update the text selections calculated by the layout - CursorLayoutResponse::TextSelection { from, to, id } => { - let current_cursor = editor.peek().cursor().clone(); - let current_selection = editor.peek().get_selection(); - - let (from, to) = ( - editor.peek().utf16_cu_to_char(from), - editor.peek().utf16_cu_to_char(to), - ); - let maybe_new_cursor = editor.peek().measure_new_cursor(to, id); - let maybe_new_selection = editor.peek().measure_new_selection(from, to, id); - - // Update the text selection if it has changed - if let Some(current_selection) = current_selection { - if current_selection != maybe_new_selection { - let mut text_editor = editor.write(); - text_editor.set_selection(maybe_new_selection); - } - } else { - let mut text_editor = editor.write(); - text_editor.set_selection(maybe_new_selection); - } - - // Update the cursor if it has changed - if current_cursor != maybe_new_cursor { - let mut text_editor = editor.write(); - *text_editor.cursor_mut() = maybe_new_cursor; - } - } - } - } - }); - - UseEditable { - editor, - cursor_reference: Signal::new(cursor_reference.clone()), - dragging, - platform, - allow_tabs: config.allow_tabs, - } - }) + use_hook(|| UseEditable::new_in_hook(clipboard, platform, initializer(), mode)) } diff --git a/crates/hooks/src/use_init_native_platform.rs b/crates/hooks/src/use_init_native_platform.rs index 0e9f3a89c..bb5dffd64 100644 --- a/crates/hooks/src/use_init_native_platform.rs +++ b/crates/hooks/src/use_init_native_platform.rs @@ -152,7 +152,7 @@ mod test { pub async fn uncontrolled_focus_accessibility() { #[allow(non_snake_case)] fn OtherChild() -> Element { - let mut focus = use_focus(); + let focus = use_focus(); rsx!(rect { a11y_id: focus.attribute(), a11y_role: "genericContainer", @@ -218,8 +218,8 @@ mod test { #[tokio::test] pub async fn auto_focus_accessibility() { fn use_focus_app() -> Element { - let mut focus_1 = use_focus(); - let mut focus_2 = use_focus(); + let focus_1 = use_focus(); + let focus_2 = use_focus(); rsx!( rect { a11y_id: focus_1.attribute(), diff --git a/crates/hooks/src/use_theme.rs b/crates/hooks/src/use_theme.rs index c1e4f40c6..4f9d8db20 100644 --- a/crates/hooks/src/use_theme.rs +++ b/crates/hooks/src/use_theme.rs @@ -66,12 +66,15 @@ pub fn use_get_theme() -> Theme { #[macro_export] macro_rules! use_applied_theme { ($theme_prop:expr, $theme_name:ident) => {{ - let mut theme = ::freya_hooks::use_get_theme().$theme_name; + let mut theme = ::freya_hooks::use_get_theme(); + let mut requested_theme = theme.$theme_name; if let Some(theme_override) = $theme_prop { - theme.apply_optional(theme_override); + requested_theme.apply_optional(theme_override); } - theme + requested_theme.apply_colors(&theme.colors); + + requested_theme }}; } diff --git a/crates/hooks/tests/use_editable.rs b/crates/hooks/tests/use_editable.rs index 507b8e04b..3e699bcb3 100644 --- a/crates/hooks/tests/use_editable.rs +++ b/crates/hooks/tests/use_editable.rs @@ -16,7 +16,7 @@ pub async fn multiple_lines_single_editor() { ); let cursor_attr = editable.cursor_attr(); let editor = editable.editor().read(); - let cursor_pos = editor.visible_cursor_pos(); + let cursor_pos = editor.cursor_pos(); let onmousedown = move |e: MouseEvent| { editable.process_event(&EditableEvent::MouseDown(e.data, 0)); @@ -294,7 +294,7 @@ pub async fn highlight_multiple_lines_single_editor() { EditableMode::MultipleLinesSingleEditor, ); let editor = editable.editor().read(); - let cursor_pos = editor.visible_cursor_pos(); + let cursor_pos = editor.cursor_pos(); let cursor_reference = editable.cursor_attr(); let highlights = editable.highlights_attr(0); @@ -355,7 +355,7 @@ pub async fn highlight_multiple_lines_single_editor() { utils.wait_for_update().await; // Move cursor - utils.move_cursor((80., 20.)).await; + utils.move_cursor((80., 25.)).await; utils.wait_for_update().await; @@ -397,7 +397,7 @@ pub async fn highlights_single_line_multiple_editors() { // Only show the cursor in the active line let character_index = if is_line_selected { - editable.editor().read().visible_cursor_col().to_string() + editable.editor().read().cursor_col().to_string() } else { "none".to_string() }; @@ -462,7 +462,7 @@ pub async fn highlights_single_line_multiple_editors() { utils.wait_for_update().await; let highlights_1 = root.child(0).unwrap().state().cursor.highlights.clone(); - assert_eq!(highlights_1, Some(vec![(5, 16)])); + assert_eq!(highlights_1, Some(vec![(5, 17)])); let highlights_2 = root.child(1).unwrap().state().cursor.highlights.clone(); #[cfg(not(target_os = "macos"))] @@ -481,7 +481,7 @@ pub async fn special_text_editing() { ); let cursor_attr = editable.cursor_attr(); let editor = editable.editor().read(); - let cursor_pos = editor.visible_cursor_pos(); + let cursor_pos = editor.cursor_pos(); let onmousedown = move |e: MouseEvent| { editable.process_event(&EditableEvent::MouseDown(e.data, 0)); @@ -564,13 +564,13 @@ pub async fn special_text_editing() { #[cfg(not(target_os = "linux"))] { assert_eq!(content.text(), Some("你好🦀世界\n👋")); - assert_eq!(cursor.text(), Some("0:3")); + assert_eq!(cursor.text(), Some("0:4")); } #[cfg(target_os = "linux")] { assert_eq!(content.text(), Some("你好世界🦀\n👋")); - assert_eq!(cursor.text(), Some("0:5")); + assert_eq!(cursor.text(), Some("0:6")); } // Move cursor to the begining @@ -644,7 +644,7 @@ pub async fn special_text_editing() { utils.wait_for_update().await; let cursor = root.get(1).get(0); // Because there is not a third line, the cursor will be moved to the max right - assert_eq!(cursor.text(), Some("1:1")); + assert_eq!(cursor.text(), Some("1:2")); // Move cursor with arrow up, twice utils.push_event(PlatformEvent::Keyboard { @@ -674,7 +674,7 @@ pub async fn backspace_remove() { ); let cursor_attr = editable.cursor_attr(); let editor = editable.editor().read(); - let cursor_pos = editor.visible_cursor_pos(); + let cursor_pos = editor.cursor_pos(); let onmousedown = move |e: MouseEvent| { editable.process_event(&EditableEvent::MouseDown(e.data, 0)); @@ -752,7 +752,7 @@ pub async fn backspace_remove() { let cursor = root.get(1).get(0); let content = root.get(0).get(0).get(0); assert_eq!(content.text(), Some("Hello🦀 Rustaceans\nHello Rustaceans")); - assert_eq!(cursor.text(), Some("0:6")); + assert_eq!(cursor.text(), Some("0:7")); // Remove text utils.push_event(PlatformEvent::Keyboard { @@ -780,7 +780,7 @@ pub async fn highlight_shift_click_multiple_lines_single_editor() { EditableMode::MultipleLinesSingleEditor, ); let editor = editable.editor().read(); - let cursor_pos = editor.visible_cursor_pos(); + let cursor_pos = editor.cursor_pos(); let cursor_reference = editable.cursor_attr(); let highlights = editable.highlights_attr(0); @@ -849,7 +849,7 @@ pub async fn highlight_shift_click_multiple_lines_single_editor() { utils.wait_for_update().await; // Move and click cursor - utils.click_cursor((80., 20.)).await; + utils.click_cursor((80., 25.)).await; utils.wait_for_update().await; @@ -891,7 +891,7 @@ pub async fn highlights_shift_click_single_line_multiple_editors() { // Only show the cursor in the active line let character_index = if is_line_selected { - editable.editor().read().visible_cursor_col().to_string() + editable.editor().read().cursor_col().to_string() } else { "none".to_string() }; @@ -960,7 +960,7 @@ pub async fn highlights_shift_click_single_line_multiple_editors() { let highlights_1 = root.child(0).unwrap().state().cursor.highlights.clone(); - assert_eq!(highlights_1, Some(vec![(5, 16)])); + assert_eq!(highlights_1, Some(vec![(5, 17)])); let highlights_2 = root.child(1).unwrap().state().cursor.highlights.clone(); @@ -979,7 +979,7 @@ pub async fn highlight_all_text() { EditableMode::MultipleLinesSingleEditor, ); let editor = editable.editor().read(); - let cursor_pos = editor.visible_cursor_pos(); + let cursor_pos = editor.cursor_pos(); let cursor_reference = editable.cursor_attr(); let highlights = editable.highlights_attr(0); @@ -1068,7 +1068,7 @@ pub async fn replace_text() { ); let cursor_attr = editable.cursor_attr(); let editor = editable.editor().read(); - let cursor_pos = editor.visible_cursor_pos(); + let cursor_pos = editor.cursor_pos(); let highlights = editable.highlights_attr(0); let onmousedown = move |e: MouseEvent| { @@ -1170,12 +1170,12 @@ pub async fn replace_text() { #[cfg(not(target_os = "macos"))] { assert_eq!(content.text(), Some("Hello🦀ceans\nHello Rustaceans")); - assert_eq!(cursor.text(), Some("0:6")); + assert_eq!(cursor.text(), Some("0:7")); } #[cfg(target_os = "macos")] { assert_eq!(content.text(), Some("Hello🦀aceans\nHello Rustaceans")); - assert_eq!(cursor.text(), Some("0:6")); + assert_eq!(cursor.text(), Some("0:7")); } } diff --git a/crates/native-core/src/attributes.rs b/crates/native-core/src/attributes.rs index 5c2c6556c..f91f42fdc 100644 --- a/crates/native-core/src/attributes.rs +++ b/crates/native-core/src/attributes.rs @@ -11,7 +11,6 @@ pub enum AttributeName { Padding, Background, Border, - BorderAlign, Direction, Shadow, CornerRadius, @@ -84,7 +83,6 @@ impl FromStr for AttributeName { "padding" => Ok(AttributeName::Padding), "background" => Ok(AttributeName::Background), "border" => Ok(AttributeName::Border), - "border_align" => Ok(AttributeName::BorderAlign), "direction" => Ok(AttributeName::Direction), "shadow" => Ok(AttributeName::Shadow), "corner_radius" => Ok(AttributeName::CornerRadius), diff --git a/crates/renderer/Cargo.toml b/crates/renderer/Cargo.toml index ec243f9fe..f4c11b6ec 100644 --- a/crates/renderer/Cargo.toml +++ b/crates/renderer/Cargo.toml @@ -17,6 +17,7 @@ features = ["freya-engine/mocked-engine"] [features] hot-reload = [] skia-engine = ["freya-engine/skia-engine"] +disable-zoom-shortcuts = [] [dependencies] freya-node-state = { workspace = true } diff --git a/crates/renderer/src/app.rs b/crates/renderer/src/app.rs index 26c1ffdd7..2f6899bde 100644 --- a/crates/renderer/src/app.rs +++ b/crates/renderer/src/app.rs @@ -287,6 +287,7 @@ impl Application { surface: &mut Surface, dirty_surface: &mut Surface, window: &Window, + scale_factor: f64, ) { self.plugins.send( PluginEvent::BeforeRender { @@ -303,7 +304,7 @@ impl Application { surface, dirty_surface, window.inner_size(), - window.scale_factor() as f32, + scale_factor as f32, ); self.plugins.send( diff --git a/crates/renderer/src/config.rs b/crates/renderer/src/config.rs index 5d647e08a..4466ac4a4 100644 --- a/crates/renderer/src/config.rs +++ b/crates/renderer/src/config.rs @@ -38,6 +38,8 @@ pub struct WindowConfig { pub transparent: bool, /// Background color of the Window. pub background: Color, + /// Window visibility. Default to `true`. + pub visible: bool, /// The Icon of the Window. pub icon: Option, /// Setup callback. @@ -58,6 +60,7 @@ impl Default for WindowConfig { title: "Freya app", transparent: false, background: Color::WHITE, + visible: true, icon: None, on_setup: None, on_exit: None, @@ -159,6 +162,12 @@ impl<'a, T: Clone> LaunchConfig<'a, T> { self } + /// Specify the Window visibility at launch. + pub fn with_visible(mut self, visible: bool) -> Self { + self.window_config.visible = visible; + self + } + /// Embed a font. pub fn with_font(mut self, font_name: &'a str, font: &'a [u8]) -> Self { self.embedded_fonts.push((font_name, font)); diff --git a/crates/renderer/src/renderer.rs b/crates/renderer/src/renderer.rs index 1b2ec0f92..49fd15fa3 100644 --- a/crates/renderer/src/renderer.rs +++ b/crates/renderer/src/renderer.rs @@ -63,6 +63,7 @@ pub struct DesktopRenderer<'a, State: Clone + 'static> { pub(crate) mouse_state: ElementState, pub(crate) modifiers_state: ModifiersState, pub(crate) dropped_file_path: Option, + pub(crate) custom_scale_factor: f64, } impl<'a, State: Clone + 'static> DesktopRenderer<'a, State> { @@ -120,6 +121,7 @@ impl<'a, State: Clone + 'static> DesktopRenderer<'a, State> { mouse_state: ElementState::Released, modifiers_state: ModifiersState::default(), dropped_file_path: None, + custom_scale_factor: 0., } } @@ -135,7 +137,9 @@ impl<'a, State: Clone + 'static> DesktopRenderer<'a, State> { /// Get the current scale factor of the Window fn scale_factor(&self) -> f64 { match &self.state { - WindowState::Created(CreatedState { window, .. }) => window.scale_factor(), + WindowState::Created(CreatedState { window, .. }) => { + window.scale_factor() + self.custom_scale_factor + } _ => 0.0, } } @@ -191,8 +195,9 @@ impl<'a, State: Clone> ApplicationHandler for DesktopRenderer<'a, app.resize(window); window.request_redraw(); } - EventMessage::InvalidateArea(area) => { + EventMessage::InvalidateArea(mut area) => { let fdom = app.sdom.get(); + area.size *= scale_factor as f32; let mut compositor_dirty_area = fdom.compositor_dirty_area(); compositor_dirty_area.unite_or_insert(&area) } @@ -294,6 +299,7 @@ impl<'a, State: Clone> ApplicationHandler for DesktopRenderer<'a, surface, dirty_surface, window, + scale_factor, ); app.event_loop_tick(); @@ -357,6 +363,37 @@ impl<'a, State: Clone> ApplicationHandler for DesktopRenderer<'a, return; } + #[cfg(not(feature = "disable-zoom-shortcuts"))] + { + let is_control_pressed = { + if cfg!(target_os = "macos") { + self.modifiers_state.super_key() + } else { + self.modifiers_state.control_key() + } + }; + + if is_control_pressed && state == ElementState::Pressed { + let ch = logical_key.to_text(); + let render_with_new_scale_factor = if ch == Some("+") { + self.custom_scale_factor = + (self.custom_scale_factor + 0.10).clamp(-1.0, 5.0); + true + } else if ch == Some("-") { + self.custom_scale_factor = + (self.custom_scale_factor - 0.10).clamp(-1.0, 5.0); + true + } else { + false + }; + + if render_with_new_scale_factor { + app.resize(window); + window.request_redraw(); + } + } + } + let name = match state { ElementState::Pressed => EventName::KeyDown, ElementState::Released => EventName::KeyUp, diff --git a/crates/renderer/src/window_state.rs b/crates/renderer/src/window_state.rs index 1e54244d0..57a136264 100644 --- a/crates/renderer/src/window_state.rs +++ b/crates/renderer/src/window_state.rs @@ -101,12 +101,13 @@ impl<'a, State: Clone + 'a> WindowState<'a, State> { let (graphics_driver, window, mut surface) = GraphicsDriver::new(event_loop, window_attributes, &config); + if config.window_config.visible { + window.set_visible(true); + } + // Allow IME window.set_ime_allowed(true); - // Mak the window visible once built - window.set_visible(true); - let mut dirty_surface = surface .new_surface_with_dimensions(window.inner_size().to_skia()) .unwrap(); diff --git a/crates/state/src/font_style.rs b/crates/state/src/font_style.rs index 84ed74d4c..54f215a0a 100644 --- a/crates/state/src/font_style.rs +++ b/crates/state/src/font_style.rs @@ -38,7 +38,7 @@ pub struct FontStyleState { pub font_slant: Slant, pub font_weight: Weight, pub font_width: Width, - pub line_height: f32, // https://developer.mozilla.org/en-US/docs/Web/CSS/line-height, + pub line_height: Option, pub decoration: Decoration, pub word_spacing: f32, pub letter_spacing: f32, @@ -48,12 +48,7 @@ pub struct FontStyleState { } impl FontStyleState { - pub fn text_style( - &self, - default_font_family: &[String], - scale_factor: f32, - height_override: bool, - ) -> TextStyle { + pub fn text_style(&self, default_font_family: &[String], scale_factor: f32) -> TextStyle { let mut text_style = TextStyle::new(); let mut font_family = self.font_family.clone(); @@ -69,9 +64,11 @@ impl FontStyleState { .set_font_size(self.font_size * scale_factor) .set_font_families(&font_family) .set_word_spacing(self.word_spacing) - .set_letter_spacing(self.letter_spacing) - .set_height_override(height_override) - .set_height(self.line_height); + .set_letter_spacing(self.letter_spacing); + + if let Some(line_height) = self.line_height { + text_style.set_height_override(true).set_height(line_height); + } for text_shadow in self.text_shadows.iter() { text_style.add_shadow(*text_shadow); @@ -95,7 +92,7 @@ impl Default for FontStyleState { font_weight: Weight::NORMAL, font_slant: Slant::Upright, font_width: Width::NORMAL, - line_height: 1.2, + line_height: None, word_spacing: 0.0, letter_spacing: 0.0, decoration: Decoration { @@ -151,7 +148,7 @@ impl ParseAttribute for FontStyleState { AttributeName::LineHeight => { if let Some(value) = attr.value.as_text() { if let Ok(line_height) = value.parse::() { - self.line_height = line_height.max(1.0); + self.line_height = Some(line_height); } } } diff --git a/crates/state/src/style.rs b/crates/state/src/style.rs index defaa60d3..978a57162 100644 --- a/crates/state/src/style.rs +++ b/crates/state/src/style.rs @@ -23,7 +23,6 @@ use crate::{ parsing::ExtSplit, AttributesBytes, Border, - BorderAlignment, CornerRadius, CustomAttributeValues, Fill, @@ -37,7 +36,7 @@ use crate::{ #[derive(Default, Debug, Clone, PartialEq, Component)] pub struct StyleState { pub background: Fill, - pub border: Border, + pub borders: Vec, pub shadows: Vec, pub corner_radius: CornerRadius, pub image_data: Option, @@ -61,14 +60,10 @@ impl ParseAttribute for StyleState { } AttributeName::Border => { if let Some(value) = attr.value.as_text() { - let mut border = Border::parse(value)?; - border.alignment = self.border.alignment; - self.border = border; - } - } - AttributeName::BorderAlign => { - if let Some(value) = attr.value.as_text() { - self.border.alignment = BorderAlignment::parse(value)?; + self.borders = value + .split_excluding_group(',', '(', ')') + .map(|chunk| Border::parse(chunk).unwrap_or_default()) + .collect(); } } AttributeName::Shadow => { @@ -139,7 +134,6 @@ impl State for StyleState { AttributeName::Background, AttributeName::Layer, AttributeName::Border, - AttributeName::BorderAlign, AttributeName::Shadow, AttributeName::CornerRadius, AttributeName::CornerSmoothing, diff --git a/crates/state/src/values/border.rs b/crates/state/src/values/border.rs index a75de44f4..5ed6939d1 100644 --- a/crates/state/src/values/border.rs +++ b/crates/state/src/values/border.rs @@ -4,30 +4,54 @@ use freya_engine::prelude::Color; use torin::scaled::Scaled; use crate::{ + ExtSplit, Fill, Parse, ParseError, }; -#[derive(Default, Clone, Copy, Debug, PartialEq)] -pub enum BorderStyle { - #[default] - None, - Solid, -} - #[derive(Default, Clone, Debug, PartialEq)] pub struct Border { pub fill: Fill, - pub style: BorderStyle, - pub width: f32, + pub width: BorderWidth, pub alignment: BorderAlignment, } impl Border { #[inline] pub fn is_visible(&self) -> bool { - self.width > 0. && self.fill != Fill::Color(Color::TRANSPARENT) + !(self.width.top == 0.0 + && self.width.left == 0.0 + && self.width.bottom == 0.0 + && self.width.right == 0.0) + && self.fill != Fill::Color(Color::TRANSPARENT) + } +} + +#[derive(Default, Clone, Copy, Debug, PartialEq)] +pub struct BorderWidth { + pub top: f32, + pub right: f32, + pub bottom: f32, + pub left: f32, +} + +impl Scaled for BorderWidth { + fn scale(&mut self, scale_factor: f32) { + self.top *= scale_factor; + self.left *= scale_factor; + self.bottom *= scale_factor; + self.right *= scale_factor; + } +} + +impl fmt::Display for BorderWidth { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{} {} {} {}", + self.top, self.right, self.bottom, self.left, + ) } } @@ -60,42 +84,132 @@ impl fmt::Display for BorderAlignment { } } -impl fmt::Display for BorderStyle { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str(match self { - BorderStyle::Solid => "solid", - BorderStyle::None => "none", - }) - } -} - impl Parse for Border { fn parse(value: &str) -> Result { if value == "none" { return Ok(Self::default()); } - let mut border_values = value.split_ascii_whitespace(); - - Ok(Border { - width: border_values - .next() - .ok_or(ParseError)? - .parse::() - .map_err(|_| ParseError)?, - style: match border_values.next().ok_or(ParseError)? { - "solid" => BorderStyle::Solid, - _ => BorderStyle::None, + let mut border_values = value.split_ascii_whitespace_excluding_group('(', ')'); + + Ok(match border_values.clone().count() { + //