From cccbc53d516662d1cfeba46381b99d0a7c73e5f3 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Sat, 13 Jul 2024 11:30:33 +0200 Subject: [PATCH 01/17] chore: Fix clippy --- examples/custom_tokio_rt.rs | 4 ++-- examples/render_canvas.rs | 2 +- examples/shader_editor.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/custom_tokio_rt.rs b/examples/custom_tokio_rt.rs index 3fe010705..e90fbfc71 100644 --- a/examples/custom_tokio_rt.rs +++ b/examples/custom_tokio_rt.rs @@ -3,8 +3,6 @@ windows_subsystem = "windows" )] -use freya::prelude::*; - #[cfg(not(feature = "custom-tokio-rt"))] fn main() { panic!("Run this example without the `custom-tokio-rt` feature."); @@ -12,6 +10,8 @@ fn main() { #[cfg(feature = "custom-tokio-rt")] fn main() { + use freya::prelude::*; + let rt = tokio::runtime::Builder::new_multi_thread() .enable_all() .build() diff --git a/examples/render_canvas.rs b/examples/render_canvas.rs index 56d0f257e..18e1da6a4 100644 --- a/examples/render_canvas.rs +++ b/examples/render_canvas.rs @@ -27,7 +27,7 @@ fn app() -> Element { }); let canvas = use_canvas(move || { - let state = state.read().clone(); + let state = *state.read(); Box::new(move |canvas, font_collection, region, _| { canvas.translate((region.min_x(), region.min_y())); diff --git a/examples/shader_editor.rs b/examples/shader_editor.rs index 5d70cf599..9086311dd 100644 --- a/examples/shader_editor.rs +++ b/examples/shader_editor.rs @@ -207,7 +207,7 @@ fn ShaderView(editable: UseEditable) -> Element { UniformValue::Float(instant.elapsed().as_secs_f32()), ); - let uniforms = Data::new_copy(&builder.build(&runtime_effect)); + let uniforms = Data::new_copy(&builder.build(runtime_effect)); let shader = runtime_effect.make_shader(uniforms, &[], None).unwrap(); From 272f799053e28becf91b5bd3c1e584f2669468ac Mon Sep 17 00:00:00 2001 From: marc2332 Date: Sat, 13 Jul 2024 11:31:12 +0200 Subject: [PATCH 02/17] chore: Add CODEOWNERS --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..b13e8e18d --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @marc2332 \ No newline at end of file From 1cfd8633429d0dd822fd829e62eca276bddb4ff4 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Sat, 13 Jul 2024 13:26:21 +0200 Subject: [PATCH 03/17] docs: Fix color syntax link in background.md --- crates/elements/src/_docs/attributes/background.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/elements/src/_docs/attributes/background.md b/crates/elements/src/_docs/attributes/background.md index 876f51429..1a289e2f4 100644 --- a/crates/elements/src/_docs/attributes/background.md +++ b/crates/elements/src/_docs/attributes/background.md @@ -1,6 +1,6 @@ Specify a color as the background of an element. -You can learn about the syntax of this attribute [here](#color-syntax). +You can learn about the syntax of this attribute in [`Color Syntax`](crate::_docs::color_syntax). ### Example From 9ffa03594c2c9078b0bc15cea65d4e46ad00a628 Mon Sep 17 00:00:00 2001 From: Marc Espin Date: Sat, 13 Jul 2024 16:19:29 +0200 Subject: [PATCH 04/17] fix: Prevent opacity from clipping the node bounds (#764) * fix: Prevent opacity from clipping the node bounds * clean up * clean up * clean up again * mock restore_to_count * update mock of save --- crates/core/src/skia/skia_renderer.rs | 13 ++++++++++--- crates/engine/src/mocked.rs | 6 +++++- crates/renderer/src/app.rs | 18 ++++++++++++++++-- crates/testing/src/test_handler.rs | 1 + 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/crates/core/src/skia/skia_renderer.rs b/crates/core/src/skia/skia_renderer.rs index 0ba900c36..c02dace40 100644 --- a/crates/core/src/skia/skia_renderer.rs +++ b/crates/core/src/skia/skia_renderer.rs @@ -12,6 +12,7 @@ use freya_node_state::{ ViewportState, }; use torin::prelude::{ + Area, LayoutNode, Torin, }; @@ -27,6 +28,7 @@ use crate::{ }; pub struct SkiaRenderer<'a> { + pub canvas_area: Area, pub canvas: &'a Canvas, pub font_collection: &'a mut FontCollection, pub font_manager: &'a FontMgr, @@ -53,7 +55,7 @@ impl SkiaRenderer<'_> { return; }; - self.canvas.save(); + let initial_layer = self.canvas.save(); let node_transform = &*node_ref.get::().unwrap(); let node_style = &*node_ref.get::().unwrap(); @@ -90,7 +92,12 @@ impl SkiaRenderer<'_> { for (opacity, nodes) in self.opacities.iter_mut() { if nodes.contains(&node_ref.id()) { self.canvas.save_layer_alpha_f( - Rect::new(area.min_x(), area.min_y(), area.max_x(), area.max_y()), + Rect::new( + self.canvas_area.min_x(), + self.canvas_area.min_y(), + self.canvas_area.max_x(), + self.canvas_area.max_y(), + ), *opacity, ); @@ -131,7 +138,7 @@ impl SkiaRenderer<'_> { wireframe_renderer::render_wireframe(self.canvas, &area); } - self.canvas.restore(); + self.canvas.restore_to_count(initial_layer); } } } diff --git a/crates/engine/src/mocked.rs b/crates/engine/src/mocked.rs index 987ca94a7..247ab1c65 100644 --- a/crates/engine/src/mocked.rs +++ b/crates/engine/src/mocked.rs @@ -954,7 +954,7 @@ pub struct PlaceholderStyle; pub struct Canvas; impl Canvas { - pub fn save(&self) { + pub fn save(&self) -> usize { unimplemented!("This is mocked") } @@ -962,6 +962,10 @@ impl Canvas { unimplemented!("This is mocked") } + pub fn restore_to_count(&self, layer: usize) { + unimplemented!("This is mocked") + } + pub fn concat(&self, _matrix: &Matrix) { unimplemented!("This is mocked") } diff --git a/crates/renderer/src/app.rs b/crates/renderer/src/app.rs index 30c36bd29..007034eb6 100644 --- a/crates/renderer/src/app.rs +++ b/crates/renderer/src/app.rs @@ -270,7 +270,12 @@ impl Application { freya_dom: &self.sdom.get(), }); - self.start_render(hovered_node, canvas, window.scale_factor() as f32); + self.start_render( + hovered_node, + canvas, + window.inner_size(), + window.scale_factor() as f32, + ); self.accessibility .render_accessibility(window.title().as_str()); @@ -362,13 +367,22 @@ impl Application { } /// Start rendering the RealDOM to Window - pub fn start_render(&mut self, hovered_node: &HoveredNode, canvas: &Canvas, scale_factor: f32) { + pub fn start_render( + &mut self, + hovered_node: &HoveredNode, + canvas: &Canvas, + windows_size: PhysicalSize, + scale_factor: f32, + ) { let fdom = self.sdom.get(); let matrices: Vec<(Matrix, Vec)> = Vec::default(); let opacities: Vec<(f32, Vec)> = Vec::default(); let mut skia_renderer = SkiaRenderer { + canvas_area: Area::from_size( + (windows_size.width as f32, windows_size.height as f32).into(), + ), canvas, font_collection: &mut self.font_collection, font_manager: &self.font_mgr, diff --git a/crates/testing/src/test_handler.rs b/crates/testing/src/test_handler.rs index 9d3b84b64..4cbb8d66a 100644 --- a/crates/testing/src/test_handler.rs +++ b/crates/testing/src/test_handler.rs @@ -291,6 +291,7 @@ impl TestingHandler { surface.canvas().clear(Color::WHITE); let mut skia_renderer = SkiaRenderer { + canvas_area: Area::from_size((width as f32, height as f32).into()), canvas: surface.canvas(), font_collection: &mut self.font_collection, font_manager: &self.font_mgr, From 1cd1d2cb16a1f94a8a3633e2287f846f463a92b3 Mon Sep 17 00:00:00 2001 From: Marc Espin Date: Sun, 14 Jul 2024 13:37:15 +0200 Subject: [PATCH 05/17] feat: Scroll controller (#772) * feat: Scroll controller * clean up and support virtual scroll view * feat: scroll_to * clean up * docs * test --- crates/components/src/scroll_views/mod.rs | 2 + .../src/scroll_views/scroll_view.rs | 65 +++- .../src/scroll_views/use_scroll_controller.rs | 278 ++++++++++++++++++ .../src/scroll_views/virtual_scroll_view.rs | 44 ++- examples/controlled_scroll.rs | 77 +++++ 5 files changed, 454 insertions(+), 12 deletions(-) create mode 100644 crates/components/src/scroll_views/use_scroll_controller.rs create mode 100644 examples/controlled_scroll.rs diff --git a/crates/components/src/scroll_views/mod.rs b/crates/components/src/scroll_views/mod.rs index b7fcf8f2c..d543830b4 100644 --- a/crates/components/src/scroll_views/mod.rs +++ b/crates/components/src/scroll_views/mod.rs @@ -1,6 +1,7 @@ mod scroll_bar; mod scroll_thumb; mod scroll_view; +mod use_scroll_controller; mod virtual_scroll_view; use freya_elements::events::{ @@ -10,6 +11,7 @@ use freya_elements::events::{ pub use scroll_bar::*; pub use scroll_thumb::*; pub use scroll_view::*; +pub use use_scroll_controller::*; pub use virtual_scroll_view::*; // Holding alt while scrolling makes it 5x faster (VSCode behavior). diff --git a/crates/components/src/scroll_views/scroll_view.rs b/crates/components/src/scroll_views/scroll_view.rs index e513a761a..6e7243a0f 100644 --- a/crates/components/src/scroll_views/scroll_view.rs +++ b/crates/components/src/scroll_views/scroll_view.rs @@ -16,6 +16,7 @@ use freya_hooks::{ ScrollViewThemeWith, }; +use super::use_scroll_controller::ScrollController; use crate::{ get_container_size, get_corrected_scroll_position, @@ -24,6 +25,10 @@ use crate::{ get_scrollbar_pos_and_size, is_scrollbar_visible, manage_key_event, + scroll_views::use_scroll_controller::{ + use_scroll_controller, + ScrollConfig, + }, Axis, ScrollBar, ScrollThumb, @@ -48,6 +53,8 @@ pub struct ScrollViewProps { /// Enable scrolling with arrow keys. #[props(default = true, into)] pub scroll_with_arrows: bool, + + pub scroll_controller: Option, } /// Scrollable area with bidirectional support and scrollbars. @@ -59,28 +66,66 @@ pub struct ScrollViewProps { /// fn app() -> Element { /// rsx!( /// ScrollView { -/// theme: theme_with!(ScrollViewTheme { -/// width: "100%".into(), -/// height: "300".into(), -/// }), -/// show_scrollbar: true, -/// rect { +/// rect { /// background: "blue", -/// height: "500", +/// height: "400", +/// width: "100%" +/// } +/// rect { +/// background: "red", +/// height: "400", /// width: "100%" /// } /// } /// ) /// } /// ``` +/// +/// # With a Scroll Controller +/// +/// ```no_run +/// # use freya::prelude::*; +/// fn app() -> Element { +/// let mut scroll_controller = use_scroll_controller(|| ScrollConfig::default()); +/// +/// rsx!( +/// ScrollView { +/// scroll_controller, +/// rect { +/// background: "blue", +/// height: "400", +/// width: "100%" +/// } +/// Button { +/// label { +/// onclick: move |_| { +/// scroll_controller.scroll_to(ScrollPosition::Start, ScrollDirection::Vertical); +/// }, +/// label { +/// "Scroll up" +/// } +/// } +/// } +/// rect { +/// background: "red", +/// height: "400", +/// width: "100%" +/// } +/// } +/// ) +/// } +/// ``` #[allow(non_snake_case)] pub fn ScrollView(props: ScrollViewProps) -> Element { let mut clicking_scrollbar = use_signal::>(|| None); let mut clicking_shift = use_signal(|| false); let mut clicking_alt = use_signal(|| false); - let mut scrolled_y = use_signal(|| 0); - let mut scrolled_x = use_signal(|| 0); + let mut scroll_controller = props + .scroll_controller + .unwrap_or_else(|| use_scroll_controller(ScrollConfig::default)); + let (mut scrolled_x, mut scrolled_y) = scroll_controller.into(); let (node_ref, size) = use_node(); + let mut focus = use_focus(); let theme = use_applied_theme!(&props.theme, scroll_view); let scrollbar_theme = use_applied_theme!(&props.scrollbar_theme, scroll_bar); @@ -92,6 +137,8 @@ pub fn ScrollView(props: ScrollViewProps) -> Element { let show_scrollbar = props.show_scrollbar; let scroll_with_arrows = props.scroll_with_arrows; + scroll_controller.use_apply(size.inner.width, size.inner.height); + let direction_is_vertical = user_direction == "vertical"; let vertical_scrollbar_is_visible = diff --git a/crates/components/src/scroll_views/use_scroll_controller.rs b/crates/components/src/scroll_views/use_scroll_controller.rs new file mode 100644 index 000000000..ea3ccac66 --- /dev/null +++ b/crates/components/src/scroll_views/use_scroll_controller.rs @@ -0,0 +1,278 @@ +use std::collections::HashSet; + +use dioxus::prelude::{ + current_scope_id, + schedule_update_any, + use_drop, + use_hook, + Readable, + ScopeId, + Signal, + Writable, + WritableVecExt, +}; + +#[derive(Default, PartialEq, Eq)] +pub enum ScrollPosition { + #[default] + Start, + End, + // Specific +} + +#[derive(Default, PartialEq, Eq)] +pub enum ScrollDirection { + #[default] + Vertical, + Horizontal, +} + +#[derive(Default)] +pub struct ScrollConfig { + pub default_vertical_position: ScrollPosition, + pub default_horizontal_position: ScrollPosition, +} + +pub struct ScrollRequest { + pub(crate) position: ScrollPosition, + pub(crate) direction: ScrollDirection, + pub(crate) init: bool, + pub(crate) applied_by: HashSet, +} + +impl ScrollRequest { + pub fn new(position: ScrollPosition, direction: ScrollDirection) -> ScrollRequest { + ScrollRequest { + position, + direction, + init: false, + applied_by: HashSet::default(), + } + } +} + +#[derive(PartialEq, Eq, Clone, Copy)] +pub struct ScrollController { + requests_subscribers: Signal>, + requests: Signal>, + x: Signal, + y: Signal, +} + +impl From for (Signal, Signal) { + fn from(val: ScrollController) -> Self { + (val.x, val.y) + } +} + +impl ScrollController { + pub fn new(x: i32, y: i32, initial_requests: Vec) -> Self { + Self { + x: Signal::new(x), + y: Signal::new(y), + requests_subscribers: Signal::new(HashSet::new()), + requests: Signal::new(initial_requests), + } + } + + pub fn use_apply(&mut self, width: f32, height: f32) { + let scope_id = current_scope_id().unwrap(); + + if !self.requests_subscribers.peek().contains(&scope_id) { + self.requests_subscribers.write().insert(scope_id); + } + + let mut requests_subscribers = self.requests_subscribers; + use_drop(move || { + requests_subscribers.write().remove(&scope_id); + }); + + self.requests.write().retain_mut(|request| { + if request.applied_by.contains(&scope_id) { + return true; + } + + match request { + ScrollRequest { + position: ScrollPosition::Start, + direction: ScrollDirection::Vertical, + .. + } => { + *self.y.write() = 0; + } + ScrollRequest { + position: ScrollPosition::Start, + direction: ScrollDirection::Horizontal, + .. + } => { + *self.x.write() = 0; + } + ScrollRequest { + position: ScrollPosition::End, + direction: ScrollDirection::Vertical, + init, + .. + } => { + if *init && height == 0. { + return true; + } + *self.y.write() = -height as i32; + } + ScrollRequest { + position: ScrollPosition::End, + direction: ScrollDirection::Horizontal, + init, + .. + } => { + if *init && width == 0. { + return true; + } + *self.x.write() = -width as i32; + } + } + + request.applied_by.insert(scope_id); + + *self.requests_subscribers.peek() != request.applied_by + }); + } + + pub fn scroll_to_x(&mut self, to: i32) { + self.x.set(to); + } + + pub fn scroll_to_y(&mut self, to: i32) { + self.y.set(to); + } + + pub fn scroll_to( + &mut self, + scroll_position: ScrollPosition, + scroll_direction: ScrollDirection, + ) { + self.requests + .push(ScrollRequest::new(scroll_position, scroll_direction)); + let schedule = schedule_update_any(); + for scope_id in self.requests_subscribers.read().iter() { + schedule(*scope_id); + } + } +} + +pub fn use_scroll_controller(init: impl FnOnce() -> ScrollConfig) -> ScrollController { + use_hook(|| { + let config = init(); + ScrollController::new( + 0, + 0, + vec![ + ScrollRequest { + position: config.default_vertical_position, + direction: ScrollDirection::Vertical, + init: true, + applied_by: HashSet::default(), + }, + ScrollRequest { + position: config.default_horizontal_position, + direction: ScrollDirection::Horizontal, + init: true, + applied_by: HashSet::default(), + }, + ], + ) + }) +} + +#[cfg(test)] +mod test { + use freya::prelude::*; + use freya_testing::prelude::*; + + #[tokio::test] + pub async fn controlled_scroll_view() { + fn scroll_view_app() -> Element { + let mut scroll_controller = use_scroll_controller(|| ScrollConfig { + default_vertical_position: ScrollPosition::End, + ..Default::default() + }); + + rsx!( + ScrollView { + scroll_controller, + Button { + onclick: move |_| { + scroll_controller.scroll_to(ScrollPosition::End, ScrollDirection::Vertical); + }, + label { + "Scroll Down" + } + } + rect { + height: "200", + width: "200", + }, + rect { + height: "200", + width: "200", + }, + rect { + height: "200", + width: "200", + } + rect { + height: "200", + width: "200", + } + Button { + onclick: move |_| { + scroll_controller.scroll_to(ScrollPosition::Start, ScrollDirection::Vertical); + }, + label { + "Scroll up" + } + } + } + ) + } + + let mut utils = launch_test(scroll_view_app); + let root = utils.root(); + let content = root.get(0).get(0).get(0); + utils.wait_for_update().await; + + // Only the last three items are visible + assert!(!content.get(1).is_visible()); + assert!(content.get(2).is_visible()); + assert!(content.get(3).is_visible()); + assert!(content.get(4).is_visible()); + + // Click on the button to scroll up + utils.push_event(PlatformEvent::Mouse { + name: EventName::Click, + cursor: (15., 480.).into(), + button: Some(MouseButton::Left), + }); + utils.wait_for_update().await; + + // Only the first three items are visible + assert!(content.get(1).is_visible()); + assert!(content.get(2).is_visible()); + assert!(content.get(3).is_visible()); + assert!(!content.get(4).is_visible()); + + // Click on the button to scroll down + utils.push_event(PlatformEvent::Mouse { + name: EventName::Click, + cursor: (15., 15.).into(), + button: Some(MouseButton::Left), + }); + + utils.wait_for_update().await; + + // Only the first three items are visible + assert!(!content.get(1).is_visible()); + assert!(content.get(2).is_visible()); + assert!(content.get(3).is_visible()); + assert!(content.get(4).is_visible()); + } +} diff --git a/crates/components/src/scroll_views/virtual_scroll_view.rs b/crates/components/src/scroll_views/virtual_scroll_view.rs index 77afcf6df..c3610def0 100644 --- a/crates/components/src/scroll_views/virtual_scroll_view.rs +++ b/crates/components/src/scroll_views/virtual_scroll_view.rs @@ -28,8 +28,11 @@ use crate::{ get_scrollbar_pos_and_size, is_scrollbar_visible, manage_key_event, + scroll_views::use_scroll_controller, Axis, ScrollBar, + ScrollConfig, + ScrollController, ScrollThumb, SCROLL_SPEED_MULTIPLIER, }; @@ -66,6 +69,8 @@ pub struct VirtualScrollViewProps< /// Default is `true`. #[props(default = true, into)] pub cache_elements: bool, + + pub scroll_controller: Option, } impl< @@ -81,6 +86,7 @@ impl< && self.show_scrollbar == other.show_scrollbar && self.scroll_with_arrows == other.scroll_with_arrows && self.builder_args == other.builder_args + && self.scroll_controller == other.scroll_controller } } @@ -115,7 +121,6 @@ fn get_render_range( /// # use std::rc::Rc; /// fn app() -> Element { /// rsx!(VirtualScrollView { -/// show_scrollbar: true, /// length: 5, /// item_size: 80.0, /// direction: "vertical", @@ -131,6 +136,35 @@ fn get_render_range( /// }) /// } /// ``` +/// +/// # With a Scroll Controller +/// +/// ```no_run +/// # use freya::prelude::*; +/// # use std::rc::Rc; +/// fn app() -> Element { +/// let mut scroll_controller = use_scroll_controller(|| ScrollConfig::default()); +/// +/// rsx!(VirtualScrollView { +/// scroll_controller, +/// length: 5, +/// item_size: 80.0, +/// direction: "vertical", +/// builder: move |i, _other_args: &Option<()>| { +/// rsx! { +/// label { +/// key: "{i}", +/// height: "80", +/// onclick: move |_| { +/// scroll_controller.scroll_to(ScrollPosition::Start, ScrollDirection::Vertical); +/// }, +/// "Number {i}" +/// } +/// } +/// } +/// }) +/// } +/// ``` #[allow(non_snake_case)] pub fn VirtualScrollView< Builder: Clone + Fn(usize, &Option) -> Element, @@ -141,8 +175,10 @@ pub fn VirtualScrollView< let mut clicking_scrollbar = use_signal::>(|| None); let mut clicking_shift = use_signal(|| false); let mut clicking_alt = use_signal(|| false); - let mut scrolled_y = use_signal(|| 0); - let mut scrolled_x = use_signal(|| 0); + let mut scroll_controller = props + .scroll_controller + .unwrap_or_else(|| use_scroll_controller(ScrollConfig::default)); + let (mut scrolled_x, mut scrolled_y) = scroll_controller.into(); let (node_ref, size) = use_node(); let mut focus = use_focus(); let theme = use_applied_theme!(&props.theme, scroll_view); @@ -161,6 +197,8 @@ pub fn VirtualScrollView< let inner_size = items_size + (items_size * items_length as f32); + scroll_controller.use_apply(inner_size, inner_size); + let vertical_scrollbar_is_visible = user_direction != "horizontal" && is_scrollbar_visible(show_scrollbar, inner_size, size.area.height()); let horizontal_scrollbar_is_visible = user_direction != "vertical" diff --git a/examples/controlled_scroll.rs b/examples/controlled_scroll.rs new file mode 100644 index 000000000..8318559dd --- /dev/null +++ b/examples/controlled_scroll.rs @@ -0,0 +1,77 @@ +#![cfg_attr( + all(not(debug_assertions), target_os = "windows"), + windows_subsystem = "windows" +)] + +use freya::prelude::*; + +fn main() { + launch_with_props(app, "Controlled Example", (600.0, 600.0)); +} + +fn app() -> Element { + let mut scroll_controller = use_scroll_controller(|| ScrollConfig { + default_vertical_position: ScrollPosition::End, + ..Default::default() + }); + + let scroll_to_top = move |_| { + scroll_controller.scroll_to(ScrollPosition::Start, ScrollDirection::Vertical); + }; + + let scroll_to_bottom = move |_| { + scroll_controller.scroll_to(ScrollPosition::End, ScrollDirection::Vertical); + }; + + rsx!( + rect { + height: "fill", + width: "fill", + direction: "horizontal", + ScrollView { + scroll_controller, + theme: theme_with!(ScrollViewTheme { + width: "50%".into(), + }), + Button { + onclick: scroll_to_bottom, + label { + "Scroll to Bottom" + } + } + Card {} + Card {} + Card {} + } + ScrollView { + scroll_controller, + theme: theme_with!(ScrollViewTheme { + width: "50%".into(), + }), + Card {} + Card {} + Card {} + Button { + onclick: scroll_to_top, + label { + "Scroll to Top" + } + } + } + } + ) +} + +#[component] +fn Card() -> Element { + rsx!( + rect { + border: "15 solid rgb(43,106,208)", + height: "220", + width: "420", + background: "white", + padding: "25", + label { "Scroll..." } + } + ) +} From c3821a39c0c41f4e3cb9931a2d1d37da9f47df86 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Tue, 16 Jul 2024 17:17:05 +0200 Subject: [PATCH 06/17] hotfix: Compile error for attribute parsing in --release --- crates/state/src/parsing.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/state/src/parsing.rs b/crates/state/src/parsing.rs index 43d239c36..20c022ac5 100644 --- a/crates/state/src/parsing.rs +++ b/crates/state/src/parsing.rs @@ -31,7 +31,7 @@ pub trait ParseAttribute: Sized { } #[cfg(not(debug_assertions))] - self.parse_attribute(attr).ok() + self.parse_attribute(attr).ok(); } } From 8ad268b099badb446b36cc9566011d2a12c730ac Mon Sep 17 00:00:00 2001 From: Marc Espin Date: Tue, 16 Jul 2024 18:05:38 +0200 Subject: [PATCH 07/17] fix: Last frame of animations was not always applied (#798) --- crates/hooks/src/use_animation.rs | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/crates/hooks/src/use_animation.rs b/crates/hooks/src/use_animation.rs index 35371d945..f0a132a6d 100644 --- a/crates/hooks/src/use_animation.rs +++ b/crates/hooks/src/use_animation.rs @@ -200,17 +200,17 @@ impl AnimatedValue for AnimColor { match direction { AnimDirection::Forward => { index > self.time.as_millis() as i32 - && self.value.r() >= self.destination.r() - && self.value.g() >= self.destination.g() - && self.value.b() >= self.destination.b() - && self.value.a() >= self.destination.a() + && self.value.r() == self.destination.r() + && self.value.g() == self.destination.g() + && self.value.b() == self.destination.b() + && self.value.a() == self.destination.a() } AnimDirection::Reverse => { index > self.time.as_millis() as i32 - && self.value.r() <= self.origin.r() - && self.value.g() <= self.origin.g() - && self.value.b() <= self.origin.b() - && self.value.a() >= self.origin.a() + && self.value.r() == self.origin.r() + && self.value.g() == self.origin.g() + && self.value.b() == self.origin.b() + && self.value.a() == self.origin.a() } } } @@ -524,6 +524,14 @@ impl UseAnimator { let is_finished = values .iter() .all(|value| value.peek().is_finished(index, direction)); + + // Advance the animations + for value in values.iter_mut() { + value.write().advance(index, direction); + } + + prev_frame = Instant::now(); + if is_finished { if OnFinish::Reverse == on_finish { // Toggle direction @@ -544,13 +552,6 @@ impl UseAnimator { } } } - - // Advance the animations - for value in values.iter_mut() { - value.write().advance(index, direction); - } - - prev_frame = Instant::now(); } is_running.set(false); From 4db506dcc7cf7bf955f14e52aa700e043eb76f98 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Tue, 16 Jul 2024 18:12:01 +0200 Subject: [PATCH 08/17] hotfix: Adjust root element height of user app in devtools --- crates/renderer/src/devtools.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/renderer/src/devtools.rs b/crates/renderer/src/devtools.rs index 23c8f5641..1ee7110a2 100644 --- a/crates/renderer/src/devtools.rs +++ b/crates/renderer/src/devtools.rs @@ -38,7 +38,7 @@ impl Devtools { rdom.traverse_depth_first(|node| { let height = node.height(); - if height == 3 { + if height == 4 { if !root_found { root_found = true; } else { From ab0b0e0bb7d268899102bd77e850ba8279a4753a Mon Sep 17 00:00:00 2001 From: Marc Espin Date: Tue, 16 Jul 2024 21:40:57 +0200 Subject: [PATCH 09/17] docs: Add `widndows-gnu` note on README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0f67bb20e..f44c3b7b5 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,9 @@ Thanks to my sponsors for supporting this project! 😄 ### Want to try it? 🤔 -⚠️ First, see [Setup guide](https://book.freyaui.dev/setup.html). +👋 Make sure to check the [Setup guide](https://book.freyaui.dev/setup.html) first. + +> ⚠️ If you happen to be on Windows using `windows-gnu` and get compile errors, maybe go check this [issue](https://github.com/marc2332/freya/issues/794). Clone this repo and run: From 0dd173381aaabe38b38e4ccc8e218d4984040e8b Mon Sep 17 00:00:00 2001 From: Marc Espin Date: Tue, 16 Jul 2024 21:42:14 +0200 Subject: [PATCH 10/17] docs: Add a Fedora section in Setup --- book/src/setup.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/book/src/setup.md b/book/src/setup.md index 766fc181c..6ac4f7856 100644 --- a/book/src/setup.md +++ b/book/src/setup.md @@ -22,6 +22,15 @@ Install these packages: sudo pacman -S base-devel openssl cmake gtk3 clang ``` +#### Fedora + +Install these packages: + +```sh +sudo dnf install openssl-devel pkgconf cmake gtk3-devel clang-devel -y +sudo dnf groupinstall "Development Tools" "C Development Tools and Libraries" -y +``` + Don't hesitate to contribute so other distros can be added here. ### MacOS @@ -34,4 +43,4 @@ The following custom linkers are not supported at the moment: - `mold` -If there is another one not supported don't hesitate to add it here. \ No newline at end of file +If there is another one not supported don't hesitate to add it here. From c93048ba2aeea33a823107c2fea97c10666c507c Mon Sep 17 00:00:00 2001 From: marc2332 Date: Fri, 19 Jul 2024 16:11:04 +0200 Subject: [PATCH 11/17] hotfix: Allow `none` for non-text colors attributes --- crates/state/src/values/color.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/state/src/values/color.rs b/crates/state/src/values/color.rs index a04fe748f..f14682516 100644 --- a/crates/state/src/values/color.rs +++ b/crates/state/src/values/color.rs @@ -23,7 +23,7 @@ impl Parse for Color { "gray" => Ok(Color::GRAY), "white" => Ok(Color::WHITE), "orange" => Ok(Color::from_rgb(255, 165, 0)), - "transparent" => Ok(Color::TRANSPARENT), + "transparent" | "none" => Ok(Color::TRANSPARENT), _ => { if value.starts_with("hsl(") { parse_hsl(value) From 3eefd73dd1cb80d4134a56fc2ea80e6e2e8fb797 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Fri, 19 Jul 2024 15:33:40 +0000 Subject: [PATCH 12/17] =?UTF-8?q?Deploying=20to=20main=20from=20@=20marc23?= =?UTF-8?q?32/freya@c93048ba2aeea33a823107c2fea97c10666c507c=20?= =?UTF-8?q?=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f44c3b7b5..0e3a5db09 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ fn app() -> Element { Thanks to my sponsors for supporting this project! 😄 -Alberto +AlbertoAlbin Ekblom ### Want to try it? 🤔 From f4d176e47713242b4f2833f4307955f4602bfffa Mon Sep 17 00:00:00 2001 From: Savchenko Ivan <73419411+Aiving@users.noreply.github.com> Date: Sat, 3 Aug 2024 12:33:09 +0500 Subject: [PATCH 13/17] feat: Add missing gradient functions (#776) * Add radial-gradient and conic-gradient support * Format code using nightly channel * fix mocked engine * my brain is going explode * i forgot code formatting * add example for every gradient * all gradient examples have been moved into one unified * i forgor change text for conic gradient button * update example * clean up devtools --------- Co-authored-by: Marc Espin --- .vscode/settings.json | 2 +- crates/core/src/elements/rect.rs | 18 +++ crates/core/src/node.rs | 6 +- crates/devtools/src/property.rs | 2 +- crates/devtools/src/tabs/style.rs | 6 +- crates/engine/src/mocked.rs | 28 ++++ crates/state/src/values/fill.rs | 12 +- crates/state/src/values/gradient.rs | 197 ++++++++++++++++++++++++--- crates/state/tests/parse_gradient.rs | 163 +++++++++++++++++++++- examples/gradient.rs | 35 ++++- 10 files changed, 435 insertions(+), 34 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index d01413707..67fc9e44e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,4 +4,4 @@ "log", "use_camera" ] -} \ No newline at end of file +} diff --git a/crates/core/src/elements/rect.rs b/crates/core/src/elements/rect.rs index 9cd457bd3..64761b18c 100644 --- a/crates/core/src/elements/rect.rs +++ b/crates/core/src/elements/rect.rs @@ -96,6 +96,12 @@ impl ElementUtils for RectElement { Fill::LinearGradient(gradient) => { paint.set_shader(gradient.into_shader(area)); } + Fill::RadialGradient(gradient) => { + paint.set_shader(gradient.into_shader(area)); + } + Fill::ConicGradient(gradient) => { + paint.set_shader(gradient.into_shader(area)); + } } let mut radius = node_style.corner_radius; @@ -137,6 +143,12 @@ impl ElementUtils for RectElement { Fill::LinearGradient(gradient) => { shadow_paint.set_shader(gradient.into_shader(area)); } + Fill::RadialGradient(gradient) => { + shadow_paint.set_shader(gradient.into_shader(area)); + } + Fill::ConicGradient(gradient) => { + shadow_paint.set_shader(gradient.into_shader(area)); + } } // Shadows can be either outset or inset @@ -213,6 +225,12 @@ impl ElementUtils for RectElement { 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_with); diff --git a/crates/core/src/node.rs b/crates/core/src/node.rs index 6fe85a586..5584c6c2e 100644 --- a/crates/core/src/node.rs +++ b/crates/core/src/node.rs @@ -128,7 +128,9 @@ impl<'a> Iterator for NodeStateIterator<'a> { let background = &self.state.style.background; let fill = match *background { Fill::Color(_) => AttributeType::Color(background.clone()), - Fill::LinearGradient(_) => AttributeType::LinearGradient(background.clone()), + Fill::LinearGradient(_) => AttributeType::Gradient(background.clone()), + Fill::RadialGradient(_) => AttributeType::Gradient(background.clone()), + Fill::ConicGradient(_) => AttributeType::Gradient(background.clone()), }; Some(("background", fill)) } @@ -197,7 +199,7 @@ impl<'a> Iterator for NodeStateIterator<'a> { pub enum AttributeType<'a> { Color(Fill), - LinearGradient(Fill), + Gradient(Fill), Size(&'a Size), Measure(f32), Measures(Gaps), diff --git a/crates/devtools/src/property.rs b/crates/devtools/src/property.rs index a58f7b753..38b9f5847 100644 --- a/crates/devtools/src/property.rs +++ b/crates/devtools/src/property.rs @@ -41,7 +41,7 @@ pub fn Property(name: String, value: String) -> Element { #[allow(non_snake_case)] #[component] -pub fn LinearGradientProperty(name: String, fill: Fill) -> Element { +pub fn GradientProperty(name: String, fill: Fill) -> Element { rsx!( rect { padding: "5 10", diff --git a/crates/devtools/src/tabs/style.rs b/crates/devtools/src/tabs/style.rs index 40965c5c0..d53ad6358 100644 --- a/crates/devtools/src/tabs/style.rs +++ b/crates/devtools/src/tabs/style.rs @@ -12,7 +12,7 @@ use crate::{ property::{ BorderProperty, ColorProperty, - LinearGradientProperty, + GradientProperty, Property, ShadowProperty, TextShadowProperty, @@ -82,9 +82,9 @@ pub fn NodeInspectorStyle(node_id: String) -> Element { } } } - AttributeType::LinearGradient(fill) => { + AttributeType::Gradient(fill) => { rsx!{ - LinearGradientProperty { + GradientProperty { key: "{i}", name: "{name}", fill: fill.clone() diff --git a/crates/engine/src/mocked.rs b/crates/engine/src/mocked.rs index 247ab1c65..0699b42cc 100644 --- a/crates/engine/src/mocked.rs +++ b/crates/engine/src/mocked.rs @@ -140,6 +140,30 @@ impl Shader { ) -> Option { unimplemented!("This is mocked") } + + pub fn radial_gradient<'a>( + _center: impl Into, + _radius: f32, + _colors: impl Into>, + _pos: impl Into>, + _mode: TileMode, + _flags: impl Into>, + _local_matrix: impl Into>, + ) -> Option { + unimplemented!("This is mocked") + } + + pub fn sweep_gradient<'a>( + _center: impl Into, + _colors: impl Into>, + _pos: impl Into>, + _mode: TileMode, + _angles: impl Into>, + _flags: impl Into>, + _local_matrix: impl Into>, + ) -> Option { + unimplemented!("This is mocked") + } } pub enum TileMode { @@ -171,6 +195,10 @@ impl Matrix { pub fn set_rotate(&mut self, _degrees: f32, _pivot: impl Into>) -> &mut Self { unimplemented!("This is mocked") } + + pub fn rotate_deg_pivot(_degrees: f32, _pivot: impl Into) -> Self { + unimplemented!("This is mocked") + } } #[repr(C)] diff --git a/crates/state/src/values/fill.rs b/crates/state/src/values/fill.rs index db31befd9..bf285bbf5 100644 --- a/crates/state/src/values/fill.rs +++ b/crates/state/src/values/fill.rs @@ -3,18 +3,20 @@ use std::fmt; use freya_engine::prelude::Color; use crate::{ + ConicGradient, DisplayColor, LinearGradient, Parse, ParseError, + RadialGradient, }; #[derive(Clone, Debug, PartialEq)] pub enum Fill { Color(Color), LinearGradient(LinearGradient), - // RadialGradient(RadialGradient), - // ConicGradient(ConicGradient), + RadialGradient(RadialGradient), + ConicGradient(ConicGradient), } impl Default for Fill { @@ -33,6 +35,10 @@ impl Parse for Fill { fn parse(value: &str) -> Result { Ok(if value.starts_with("linear-gradient(") { Self::LinearGradient(LinearGradient::parse(value).map_err(|_| ParseError)?) + } else if value.starts_with("radial-gradient(") { + Self::RadialGradient(RadialGradient::parse(value).map_err(|_| ParseError)?) + } else if value.starts_with("conic-gradient(") { + Self::ConicGradient(ConicGradient::parse(value).map_err(|_| ParseError)?) } else { Self::Color(Color::parse(value).map_err(|_| ParseError)?) }) @@ -44,6 +50,8 @@ impl fmt::Display for Fill { match self { Self::Color(color) => color.fmt_rgb(f), Self::LinearGradient(gradient) => gradient.fmt(f), + Self::RadialGradient(gradient) => gradient.fmt(f), + Self::ConicGradient(gradient) => gradient.fmt(f), } } } diff --git a/crates/state/src/values/gradient.rs b/crates/state/src/values/gradient.rs index e5011d7f4..ec7051ede 100644 --- a/crates/state/src/values/gradient.rs +++ b/crates/state/src/values/gradient.rs @@ -60,26 +60,20 @@ impl LinearGradient { let colors: Vec = self.stops.iter().map(|stop| stop.color).collect(); let offsets: Vec = self.stops.iter().map(|stop| stop.offset).collect(); - let (dx, dy) = (-self.angle).sin_cos(); - let farthest_corner = Point::new( - if dx > 0.0 { bounds.width() } else { 0.0 }, - if dy > 0.0 { bounds.height() } else { 0.0 }, - ); - let delta = farthest_corner - Point::new(bounds.width(), bounds.height()) / 2.0; - let u = delta.x * dy - delta.y * dx; - let endpoint = farthest_corner + Point::new(-u * dy, u * dx); - - let origin = Point::new(bounds.min_x(), bounds.min_y()); + let center = bounds.center(); + + let matrix = Matrix::rotate_deg_pivot(self.angle, (center.x, center.y)); + Shader::linear_gradient( ( - Point::new(bounds.width(), bounds.height()) - endpoint + origin, - endpoint + origin, + (bounds.min_x(), bounds.min_y()), + (bounds.max_x(), bounds.max_y()), ), GradientShaderColors::Colors(&colors[..]), Some(&offsets[..]), TileMode::Clamp, None, - None, + Some(&matrix), ) } } @@ -100,18 +94,16 @@ impl Parse for LinearGradient { if angle_or_first_stop.ends_with("deg") { if let Ok(angle) = angle_or_first_stop.replacen("deg", "", 1).parse::() { - gradient.angle = angle.to_radians(); + gradient.angle = angle; } } else { gradient .stops - .push(GradientStop::parse(angle_or_first_stop).map_err(|_| ParseError)?); + .push(GradientStop::parse(angle_or_first_stop)?); } for stop in split { - gradient - .stops - .push(GradientStop::parse(stop).map_err(|_| ParseError)?); + gradient.stops.push(GradientStop::parse(stop)?); } Ok(gradient) @@ -123,7 +115,174 @@ impl fmt::Display for LinearGradient { write!( f, "linear-gradient({}deg, {})", - self.angle.to_degrees(), + self.angle, + self.stops + .iter() + .map(|stop| stop.to_string()) + .collect::>() + .join(", ") + ) + } +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct RadialGradient { + pub stops: Vec, +} + +impl RadialGradient { + pub fn into_shader(&self, bounds: Rect) -> Option { + let colors: Vec = self.stops.iter().map(|stop| stop.color).collect(); + let offsets: Vec = self.stops.iter().map(|stop| stop.offset).collect(); + + let center = bounds.center(); + + Shader::radial_gradient( + Point::new(center.x, center.y), + bounds.width().max(bounds.height()), + GradientShaderColors::Colors(&colors[..]), + Some(&offsets[..]), + TileMode::Clamp, + None, + None, + ) + } +} + +impl Parse for RadialGradient { + fn parse(value: &str) -> Result { + if !value.starts_with("radial-gradient(") || !value.ends_with(')') { + return Err(ParseError); + } + + let mut gradient = RadialGradient::default(); + let mut value = value.replacen("radial-gradient(", "", 1); + + value.remove(value.rfind(')').ok_or(ParseError)?); + + for stop in value.split_excluding_group(',', '(', ')') { + gradient.stops.push(GradientStop::parse(stop)?); + } + + Ok(gradient) + } +} + +impl fmt::Display for RadialGradient { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "radial-gradient({})", + self.stops + .iter() + .map(|stop| stop.to_string()) + .collect::>() + .join(", ") + ) + } +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct ConicGradient { + pub stops: Vec, + pub angles: Option<(f32, f32)>, + pub angle: Option, +} + +impl ConicGradient { + pub fn into_shader(&self, bounds: Rect) -> Option { + let colors: Vec = self.stops.iter().map(|stop| stop.color).collect(); + let offsets: Vec = self.stops.iter().map(|stop| stop.offset).collect(); + + let center = bounds.center(); + + let matrix = + Matrix::rotate_deg_pivot(-90.0 + self.angle.unwrap_or(0.0), (center.x, center.y)); + + Shader::sweep_gradient( + (center.x, center.y), + GradientShaderColors::Colors(&colors[..]), + Some(&offsets[..]), + TileMode::Clamp, + self.angles, + None, + Some(&matrix), + ) + } +} + +impl Parse for ConicGradient { + fn parse(value: &str) -> Result { + if !value.starts_with("conic-gradient(") || !value.ends_with(')') { + return Err(ParseError); + } + + let mut gradient = ConicGradient::default(); + let mut value = value.replacen("conic-gradient(", "", 1); + + value.remove(value.rfind(')').ok_or(ParseError)?); + + let mut split = value.split_excluding_group(',', '(', ')'); + + let angle_or_first_stop = split.next().ok_or(ParseError)?.trim(); + + if angle_or_first_stop.ends_with("deg") { + if let Ok(angle) = angle_or_first_stop.replacen("deg", "", 1).parse::() { + gradient.angle = Some(angle); + } + } else { + gradient + .stops + .push(GradientStop::parse(angle_or_first_stop).map_err(|_| ParseError)?); + } + + if let Some(angles_or_second_stop) = split.next().map(str::trim) { + if angles_or_second_stop.starts_with("from ") && angles_or_second_stop.ends_with("deg") + { + if let Some(start) = angles_or_second_stop + .find("deg") + .and_then(|index| angles_or_second_stop.get(5..index)) + .and_then(|slice| slice.parse::().ok()) + { + let end = angles_or_second_stop + .find(" to ") + .and_then(|index| angles_or_second_stop.get(index + 4..)) + .and_then(|slice| slice.find("deg").and_then(|index| slice.get(0..index))) + .and_then(|slice| slice.parse::().ok()) + .unwrap_or(360.0); + + gradient.angles = Some((start, end)); + } + } else { + gradient + .stops + .push(GradientStop::parse(angles_or_second_stop)?); + } + } + + for stop in split { + gradient.stops.push(GradientStop::parse(stop)?); + } + + Ok(gradient) + } +} + +impl fmt::Display for ConicGradient { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "conic-gradient(")?; + + if let Some(angle) = self.angle { + write!(f, "{angle}deg, ")?; + } + + if let Some((start, end)) = self.angles { + write!(f, "from {start}deg to {end}deg, ")?; + } + + write!( + f, + "{})", self.stops .iter() .map(|stop| stop.to_string()) diff --git a/crates/state/tests/parse_gradient.rs b/crates/state/tests/parse_gradient.rs index f85cdd2c1..e4b5b0aae 100644 --- a/crates/state/tests/parse_gradient.rs +++ b/crates/state/tests/parse_gradient.rs @@ -1,12 +1,14 @@ use freya_engine::prelude::*; use freya_node_state::{ + ConicGradient, GradientStop, LinearGradient, Parse, + RadialGradient, }; #[test] -fn parse_basic_gradient() { +fn parse_basic_linear_gradient() { assert_eq!( LinearGradient::parse("linear-gradient(red 0%, blue 100%)"), Ok(LinearGradient { @@ -25,6 +27,103 @@ fn parse_basic_gradient() { ); } +#[test] +fn parse_basic_radial_gradient() { + assert_eq!( + RadialGradient::parse("radial-gradient(red 0%, blue 100%)"), + Ok(RadialGradient { + stops: vec![ + GradientStop { + color: Color::RED, + offset: 0.0, + }, + GradientStop { + color: Color::BLUE, + offset: 1.0, + } + ] + }) + ); +} + +#[test] +fn parse_basic_conic_gradient() { + assert_eq!( + ConicGradient::parse("conic-gradient(red 0%, blue 100%)"), + Ok(ConicGradient { + angle: None, + angles: None, + stops: vec![ + GradientStop { + color: Color::RED, + offset: 0.0, + }, + GradientStop { + color: Color::BLUE, + offset: 1.0, + } + ] + }) + ); +} + +#[test] +fn parse_conic_gradient_variants() { + assert_eq!( + ConicGradient::parse("conic-gradient(45deg, red 0%, blue 100%)"), + Ok(ConicGradient { + angle: Some(45.0), + angles: None, + stops: vec![ + GradientStop { + color: Color::RED, + offset: 0.0, + }, + GradientStop { + color: Color::BLUE, + offset: 1.0, + } + ] + }) + ); + + assert_eq!( + ConicGradient::parse("conic-gradient(45deg, from 40deg, red 0%, blue 100%)"), + Ok(ConicGradient { + angle: Some(45.0), + angles: Some((40.0, 360.0)), + stops: vec![ + GradientStop { + color: Color::RED, + offset: 0.0, + }, + GradientStop { + color: Color::BLUE, + offset: 1.0, + } + ] + }) + ); + + assert_eq!( + ConicGradient::parse("conic-gradient(45deg, from 40deg to 120deg, red 0%, blue 100%)"), + Ok(ConicGradient { + angle: Some(45.0), + angles: Some((40.0, 120.0)), + stops: vec![ + GradientStop { + color: Color::RED, + offset: 0.0, + }, + GradientStop { + color: Color::BLUE, + offset: 1.0, + } + ] + }) + ); +} + #[test] fn parse_rgb_hsl_gradient() { assert_eq!( @@ -50,7 +149,7 @@ fn parse_gradient_angle() { assert_eq!( LinearGradient::parse("linear-gradient(45deg, red 0%, blue 100%)"), Ok(LinearGradient { - angle: f32::to_radians(45.0), + angle: 45.0, stops: vec![ GradientStop { color: Color::from_rgb(255, 0, 0), @@ -66,7 +165,7 @@ fn parse_gradient_angle() { } #[test] -fn invalid_gradients() { +fn invalid_linear_gradients() { let incorrect_name = LinearGradient::parse("lkdsjfalkasdasdjaslkfjsdklfs(red 0%, blue 100%)"); let extra_lparen = LinearGradient::parse("linear-gradient((red 0%, blue 100%)"); let extra_rparen = LinearGradient::parse("linear-gradient(red 0%, blue 100%))"); @@ -75,7 +174,7 @@ fn invalid_gradients() { let extra_commas = LinearGradient::parse("linear-gradient(red 0%, blue 100%,)"); let extra_stop_component = LinearGradient::parse("linear-gradient(red 0% something, blue 100%)"); - let bad_angle_unit = LinearGradient::parse("linear-gradient(45ft, red 0%, blue 100%,)"); + let bad_angle_unit = LinearGradient::parse("linear-gradient(45ft, red 0%, blue 100%)"); let bad_offset_unit = LinearGradient::parse("linear-gradient(45deg, red 0atm, blue 100kpa)"); let missing_color = LinearGradient::parse("linear-gradient(45deg, 0%, blue 100%)"); let missing_offset = LinearGradient::parse("linear-gradient(45deg, red, blue 100%)"); @@ -92,3 +191,59 @@ fn invalid_gradients() { assert!(missing_color.is_err()); assert!(missing_offset.is_err()); } + +#[test] +fn invalid_radial_gradients() { + let incorrect_name = RadialGradient::parse("lkdsjfalkasdasdjaslkfjsdklfs(red 0%, blue 100%)"); + let extra_lparen = RadialGradient::parse("radial-gradient((red 0%, blue 100%)"); + let extra_rparen = RadialGradient::parse("radial-gradient(red 0%, blue 100%))"); + let missing_rparen = RadialGradient::parse("radial-gradient(red 0%, blue 100%"); + let missing_commas = RadialGradient::parse("radial-gradient(red 0% blue 100%)"); + let extra_commas = RadialGradient::parse("radial-gradient(red 0%, blue 100%,)"); + let extra_stop_component = + RadialGradient::parse("radial-gradient(red 0% something, blue 100%)"); + let bad_offset_unit = RadialGradient::parse("radial-gradient(red 0atm, blue 100kpa)"); + let missing_color = RadialGradient::parse("radial-gradient(0%, blue 100%)"); + let missing_offset = RadialGradient::parse("radial-gradient(red, blue 100%)"); + + assert!(incorrect_name.is_err()); + assert!(extra_lparen.is_err()); + assert!(extra_rparen.is_err()); + assert!(missing_rparen.is_err()); + assert!(missing_commas.is_err()); + assert!(extra_commas.is_err()); + assert!(extra_stop_component.is_err()); + assert!(bad_offset_unit.is_err()); + assert!(missing_color.is_err()); + assert!(missing_offset.is_err()); +} + +#[test] +fn invalid_conic_gradients() { + let incorrect_name = ConicGradient::parse("lkdsjfalkasdasdjaslkfjsdklfs(red 0%, blue 100%)"); + let extra_lparen = ConicGradient::parse("conic-gradient((red 0%, blue 100%)"); + let extra_rparen = ConicGradient::parse("conic-gradient(red 0%, blue 100%))"); + let missing_rparen = ConicGradient::parse("conic-gradient(red 0%, blue 100%"); + let missing_commas = ConicGradient::parse("conic-gradient(red 0% blue 100%)"); + let extra_commas = ConicGradient::parse("conic-gradient(red 0%, blue 100%,)"); + let extra_stop_component = ConicGradient::parse("conic-gradient(red 0% something, blue 100%)"); + let bad_angle_unit = ConicGradient::parse("conic-gradient(45ft, red 0%, blue 100%)"); + let bad_offset_unit = ConicGradient::parse("conic-gradient(red 0atm, blue 100kpa)"); + let bad_angles = + ConicGradient::parse("conic-gradient(45deg, from 60rft to 90fft, red 0%, blue 100%)"); + let missing_color = ConicGradient::parse("conic-gradient(0%, blue 100%)"); + let missing_offset = ConicGradient::parse("conic-gradient(red, blue 100%)"); + + assert!(incorrect_name.is_err()); + assert!(extra_lparen.is_err()); + assert!(extra_rparen.is_err()); + assert!(missing_rparen.is_err()); + assert!(missing_commas.is_err()); + assert!(extra_commas.is_err()); + assert!(extra_stop_component.is_err()); + assert!(bad_angle_unit.is_err()); + assert!(bad_offset_unit.is_err()); + assert!(bad_angles.is_err()); + assert!(missing_color.is_err()); + assert!(missing_offset.is_err()); +} diff --git a/examples/gradient.rs b/examples/gradient.rs index 8f2f0526b..1e01f2fbc 100644 --- a/examples/gradient.rs +++ b/examples/gradient.rs @@ -9,11 +9,42 @@ fn main() { launch(app); } +enum GradientExample { + Linear, + Radial, + Conic, +} + fn app() -> Element { + let mut gradient = use_signal(|| GradientExample::Linear); + + let background = match *gradient.read() { + GradientExample::Linear => { + "linear-gradient(250deg, orange 15%, rgb(255, 0, 0) 50%, rgb(255, 192, 203) 80%)" + } + GradientExample::Radial => { + "radial-gradient(orange 15%, rgb(255, 0, 0) 50%, rgb(255, 192, 203) 80%)" + } + GradientExample::Conic => { + "conic-gradient(250deg, orange 15%, rgb(255, 0, 0) 50%, rgb(255, 192, 203) 80%)" + } + }; + rsx!(rect { height: "100%", width: "100%", - background: - "linear-gradient(250deg, orange 15%, rgb(255, 0, 0) 50%, rgb(255, 192, 203) 80%)", + background, + Button { + onclick: move |_| gradient.set(GradientExample::Linear), + label { "Linear Gradient" } + } + Button { + onclick: move |_| gradient.set(GradientExample::Radial), + label { "Radial Gradient" } + } + Button { + onclick: move |_| gradient.set(GradientExample::Conic), + label { "Conic Gradient" } + } }) } From fc1c1cc136acb42d92bd162f1647697c9dabde39 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Sat, 3 Aug 2024 09:40:57 +0200 Subject: [PATCH 14/17] fix: Use hotfix patch for nokhwa --- crates/hooks/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/hooks/Cargo.toml b/crates/hooks/Cargo.toml index a8e9c0daf..301dc163d 100644 --- a/crates/hooks/Cargo.toml +++ b/crates/hooks/Cargo.toml @@ -37,7 +37,7 @@ winit = { workspace = true } uuid = { workspace = true } easer = "0.3.0" ropey = "1.6.0" -nokhwa = { version = "0.10.4", features = ["input-native"], optional = true } +nokhwa = { git = "https://github.com/tactile-eng/nokhwa", branch = "update-mozjpeg", features = ["input-native"], optional = true } paste = "1.0.14" bitflags = "2.4.1" bytes = "1.5.0" From 2312a96d61fbd8049afdd31e1f223c99bbdbcef2 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Sat, 3 Aug 2024 09:45:05 +0200 Subject: [PATCH 15/17] fix: Use `ImageReader` for icon loading in windows --- crates/renderer/src/config.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/renderer/src/config.rs b/crates/renderer/src/config.rs index 05dc9d50f..5d647e08a 100644 --- a/crates/renderer/src/config.rs +++ b/crates/renderer/src/config.rs @@ -12,7 +12,7 @@ use freya_core::{ }; use freya_engine::prelude::Color; use freya_node_state::Parse; -use image::io::Reader; +use image::ImageReader; use winit::window::{ Icon, Window, @@ -95,7 +95,7 @@ impl<'a, T: Clone> LaunchConfig<'a, T> { impl LaunchConfig<'_, ()> { pub fn load_icon(icon: &[u8]) -> Icon { - let reader = Reader::new(Cursor::new(icon)) + let reader = ImageReader::new(Cursor::new(icon)) .with_guessed_format() .expect("Cursor io never fails"); let image = reader From 8177800b4bca0089e29465984cda0410370987ca Mon Sep 17 00:00:00 2001 From: Marc Espin Date: Sat, 3 Aug 2024 09:59:15 +0200 Subject: [PATCH 16/17] fix: Show missing attributes in devtools (#801) * fix: Show missing attributes in devtools * fmt --- crates/core/src/node.rs | 162 +++++++++++----------------- crates/devtools/src/tabs/style.rs | 20 +++- crates/torin/src/values/content.rs | 9 ++ crates/torin/src/values/position.rs | 15 +++ 4 files changed, 104 insertions(+), 102 deletions(-) diff --git a/crates/core/src/node.rs b/crates/core/src/node.rs index 5584c6c2e..11509017d 100644 --- a/crates/core/src/node.rs +++ b/crates/core/src/node.rs @@ -17,6 +17,10 @@ use torin::{ alignment::Alignment, direction::DirectionMode, gaps::Gaps, + prelude::{ + Content, + Position, + }, size::Size, }; @@ -75,125 +79,79 @@ pub fn get_node_state(node: &DioxusNode) -> NodeState { } impl NodeState { - pub fn iter(&self) -> NodeStateIterator { - NodeStateIterator { - state: self, - curr: 0, - } - } -} - -pub struct NodeStateIterator<'a> { - state: &'a NodeState, - curr: usize, -} - -impl<'a> Iterator for NodeStateIterator<'a> { - type Item = (&'a str, AttributeType<'a>); - - fn nth(&mut self, n: usize) -> Option { - match n { - 0 => Some(("width", AttributeType::Size(&self.state.size.width))), - 1 => Some(("height", AttributeType::Size(&self.state.size.height))), - 2 => Some(( - "min_width", - AttributeType::Size(&self.state.size.minimum_width), - )), - 3 => Some(( - "min_height", - AttributeType::Size(&self.state.size.minimum_height), - )), - 4 => Some(( - "max_width", - AttributeType::Size(&self.state.size.maximum_width), - )), - 5 => Some(( - "max_height", - AttributeType::Size(&self.state.size.maximum_height), - )), - 6 => Some(( - "direction", - AttributeType::Direction(&self.state.size.direction), - )), - 7 => Some(("padding", AttributeType::Measures(self.state.size.padding))), - 8 => Some(( + pub fn attributes(&self) -> Vec<(&str, AttributeType)> { + let mut attributes = vec![ + ("width", AttributeType::Size(&self.size.width)), + ("height", AttributeType::Size(&self.size.height)), + ("min_width", AttributeType::Size(&self.size.minimum_width)), + ("min_height", AttributeType::Size(&self.size.minimum_height)), + ("max_width", AttributeType::Size(&self.size.maximum_width)), + ("max_height", AttributeType::Size(&self.size.maximum_height)), + ("direction", AttributeType::Direction(&self.size.direction)), + ("padding", AttributeType::Measures(self.size.padding)), + ("margin", AttributeType::Measures(self.size.margin)), + ("position", AttributeType::Position(&self.size.position)), + ( "main_alignment", - AttributeType::Alignment(&self.state.size.main_alignment), - )), - 9 => Some(( + AttributeType::Alignment(&self.size.main_alignment), + ), + ( "cross_alignment", - AttributeType::Alignment(&self.state.size.cross_alignment), - )), - 10 => { - let background = &self.state.style.background; + AttributeType::Alignment(&self.size.cross_alignment), + ), + { + let background = &self.style.background; let fill = match *background { Fill::Color(_) => AttributeType::Color(background.clone()), Fill::LinearGradient(_) => AttributeType::Gradient(background.clone()), Fill::RadialGradient(_) => AttributeType::Gradient(background.clone()), Fill::ConicGradient(_) => AttributeType::Gradient(background.clone()), }; - Some(("background", fill)) - } - 11 => Some(("border", AttributeType::Border(&self.state.style.border))), - 12 => Some(( + ("background", fill) + }, + ("border", AttributeType::Border(&self.style.border)), + ( "corner_radius", - AttributeType::CornerRadius(self.state.style.corner_radius), - )), - 13 => Some(( - "color", - AttributeType::Color(self.state.font_style.color.into()), - )), - 14 => Some(( + AttributeType::CornerRadius(self.style.corner_radius), + ), + ("color", AttributeType::Color(self.font_style.color.into())), + ( "font_family", - AttributeType::Text(self.state.font_style.font_family.join(",")), - )), - 15 => Some(( + AttributeType::Text(self.font_style.font_family.join(",")), + ), + ( "font_size", - AttributeType::Measure(self.state.font_style.font_size), - )), - 16 => Some(( + AttributeType::Measure(self.font_style.font_size), + ), + ( "line_height", - AttributeType::Measure(self.state.font_style.line_height), - )), - 17 => Some(( + AttributeType::Measure(self.font_style.line_height), + ), + ( "text_align", - AttributeType::TextAlignment(&self.state.font_style.text_align), - )), - 18 => Some(( + AttributeType::TextAlignment(&self.font_style.text_align), + ), + ( "text_overflow", - AttributeType::TextOverflow(&self.state.font_style.text_overflow), - )), - 19 => Some(( - "offset_x", - AttributeType::Measure(self.state.size.offset_x.get()), - )), - 20 => Some(( - "offset_y", - AttributeType::Measure(self.state.size.offset_y.get()), - )), - n => { - let shadows = &self.state.style.shadows; - let shadow = shadows - .get(n - 21) - .map(|shadow| ("shadow", AttributeType::Shadow(shadow))); + AttributeType::TextOverflow(&self.font_style.text_overflow), + ), + ("offset_x", AttributeType::Measure(self.size.offset_x.get())), + ("offset_y", AttributeType::Measure(self.size.offset_y.get())), + ("content", AttributeType::Content(&self.size.content)), + ]; - if shadow.is_some() { - shadow - } else { - let text_shadows = &self.state.font_style.text_shadows; - text_shadows - .get(n - 21 + shadows.len()) - .map(|text_shadow| ("text_shadow", AttributeType::TextShadow(text_shadow))) - } - } + let shadows = &self.style.shadows; + for shadow in shadows { + attributes.push(("shadow", AttributeType::Shadow(shadow))); } - } - fn next(&mut self) -> Option { - let current = self.curr; - self.curr += 1; + let text_shadows = &self.font_style.text_shadows; + + for text_shadow in text_shadows { + attributes.push(("text_shadow", AttributeType::TextShadow(text_shadow))); + } - self.nth(current) + attributes } } @@ -205,6 +163,8 @@ pub enum AttributeType<'a> { Measures(Gaps), CornerRadius(CornerRadius), Direction(&'a DirectionMode), + Position(&'a Position), + Content(&'a Content), Alignment(&'a Alignment), Shadow(&'a Shadow), TextShadow(&'a TextShadow), diff --git a/crates/devtools/src/tabs/style.rs b/crates/devtools/src/tabs/style.rs index d53ad6358..c90f288f4 100644 --- a/crates/devtools/src/tabs/style.rs +++ b/crates/devtools/src/tabs/style.rs @@ -35,7 +35,7 @@ pub fn NodeInspectorStyle(node_id: String) -> Element { width: "100%".into(), } ), - {node.state.iter().enumerate().map(|(i, (name, attr))| { + {node.state.attributes().into_iter().enumerate().map(|(i, (name, attr))| { match attr { AttributeType::Measure(measure) => { rsx!{ @@ -118,6 +118,24 @@ pub fn NodeInspectorStyle(node_id: String) -> Element { } } } + AttributeType::Position(position) => { + rsx!{ + Property { + key: "{i}", + name: "{name}", + value: position.pretty() + } + } + } + AttributeType::Content(content) => { + rsx!{ + Property { + key: "{i}", + name: "{name}", + value: content.pretty() + } + } + } AttributeType::Alignment(alignment) => { rsx!{ Property { diff --git a/crates/torin/src/values/content.rs b/crates/torin/src/values/content.rs index 4215c7b73..a6193e8b8 100644 --- a/crates/torin/src/values/content.rs +++ b/crates/torin/src/values/content.rs @@ -10,3 +10,12 @@ impl Content { self == &Self::Fit } } + +impl Content { + pub fn pretty(&self) -> String { + match self { + Self::Normal => "normal".to_owned(), + Self::Fit => "fit".to_owned(), + } + } +} diff --git a/crates/torin/src/values/position.rs b/crates/torin/src/values/position.rs index fec95b901..99b13846f 100644 --- a/crates/torin/src/values/position.rs +++ b/crates/torin/src/values/position.rs @@ -147,3 +147,18 @@ impl Scaled for Position { } } } + +impl Position { + pub fn pretty(&self) -> String { + match self { + Self::Stacked => "horizontal".to_string(), + Self::Absolute(positions) => format!( + "{}, {}, {}, {}", + positions.top.unwrap_or_default(), + positions.right.unwrap_or_default(), + positions.bottom.unwrap_or_default(), + positions.left.unwrap_or_default() + ), + } + } +} From 741ef655258267255b51b8333c8c6570928f00b0 Mon Sep 17 00:00:00 2001 From: Marc Espin Date: Sat, 3 Aug 2024 10:15:52 +0200 Subject: [PATCH 17/17] feat: `import_svg` macro (#790) * feat: `import_svg` macro * feat: Allow to optionally specify width and height overrides in the generated component * docs: Add docs for `import_svg` * fix: Update `import_svg` doc example --- crates/components/src/lib.rs | 1 + crates/components/src/svg.rs | 42 ++++++++++++++++++++++++++++++++++++ examples/import_svg.rs | 7 ++++++ 3 files changed, 50 insertions(+) create mode 100644 crates/components/src/svg.rs create mode 100644 examples/import_svg.rs diff --git a/crates/components/src/lib.rs b/crates/components/src/lib.rs index 6a7a0e42d..a34e9b21a 100644 --- a/crates/components/src/lib.rs +++ b/crates/components/src/lib.rs @@ -29,6 +29,7 @@ mod scroll_views; mod sidebar; mod slider; mod snackbar; +mod svg; mod switch; mod table; mod tabs; diff --git a/crates/components/src/svg.rs b/crates/components/src/svg.rs new file mode 100644 index 000000000..08d201a0e --- /dev/null +++ b/crates/components/src/svg.rs @@ -0,0 +1,42 @@ +/// Generate a Dioxus component rendering the specified SVG. +/// +/// Example: +/// +/// ```no_run +/// # use freya::prelude::*; +/// +/// import_svg!(Ferris, "../../../examples/ferris.svg", "100%", "100%"); +/// +/// fn app() -> Element { +/// rsx!(Ferris {}) +/// } +/// +/// fn another_app() -> Element { +/// rsx!(Ferris { +/// width: "150", +/// height: "40%", +/// }) +/// } +/// ``` +#[macro_export] +macro_rules! import_svg { + ($component_name:ident, $path:expr, $width: expr, $height: expr) => { + use dioxus::prelude::component; + // Generate a function with the name derived from the file name + #[allow(non_snake_case)] + #[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 svg_content = include_str!($path); + + rsx!(svg { + width, + height, + svg_content + }) + } + }; +} diff --git a/examples/import_svg.rs b/examples/import_svg.rs new file mode 100644 index 000000000..bbaad50d7 --- /dev/null +++ b/examples/import_svg.rs @@ -0,0 +1,7 @@ +use freya::prelude::*; + +import_svg!(Ferris, "./ferris.svg", "100%", "100%"); + +fn main() { + launch(|| rsx!(Ferris {})) +}