From 8cc06faf37b7d96942145a9dee741c022c619f5c Mon Sep 17 00:00:00 2001 From: Marc Espin Date: Mon, 16 Sep 2024 14:24:38 +0200 Subject: [PATCH 01/39] fix: Dont trigger mouse enter on touch move (#888) * fix: Dont trigger mouse enter on touch move * fmt --- crates/native-core/src/events.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/native-core/src/events.rs b/crates/native-core/src/events.rs index cb2d1c120..1f537db73 100644 --- a/crates/native-core/src/events.rs +++ b/crates/native-core/src/events.rs @@ -166,9 +166,10 @@ impl EventName { events.push(*self); match self { - Self::MouseMove | Self::TouchMove => { + Self::MouseMove => { events.extend([Self::MouseEnter, Self::PointerEnter, Self::PointerOver]) } + Self::TouchMove => events.extend([Self::PointerEnter, Self::PointerOver]), Self::MouseDown | Self::TouchStart => events.push(Self::PointerDown), Self::MouseUp | Self::MiddleClick | Self::RightClick | Self::TouchEnd => { events.extend([Self::Click, Self::PointerUp]) From 732e03bf1d9b78c0195a49c809f0fec4704b79d0 Mon Sep 17 00:00:00 2001 From: Marc Espin Date: Mon, 16 Sep 2024 14:29:38 +0200 Subject: [PATCH 02/39] fix: Use collateral event to check if event is allowed (#890) * fix: Consder pointer and mouse up for pressed nodes * fixes * fixes --- crates/components/src/button.rs | 10 +++++++++- crates/components/src/native_router.rs | 4 ++-- crates/core/src/events/events_measurer.rs | 2 +- crates/core/src/events/nodes_state.rs | 12 ++++++------ crates/native-core/src/events.rs | 14 ++++++++++---- 5 files changed, 28 insertions(+), 14 deletions(-) diff --git a/crates/components/src/button.rs b/crates/components/src/button.rs index fe807baea..8ec8ef058 100644 --- a/crates/components/src/button.rs +++ b/crates/components/src/button.rs @@ -228,6 +228,15 @@ mod test { assert_eq!(label.get(0).text(), Some("true")); + utils.push_event(PlatformEvent::Touch { + name: EventName::TouchStart, + location: (15.0, 15.0).into(), + finger_id: 1, + phase: TouchPhase::Started, + force: None, + }); + utils.wait_for_update().await; + utils.push_event(PlatformEvent::Touch { name: EventName::TouchEnd, location: (15.0, 15.0).into(), @@ -235,7 +244,6 @@ mod test { phase: TouchPhase::Ended, force: None, }); - utils.wait_for_update().await; assert_eq!(label.get(0).text(), Some("false")); diff --git a/crates/components/src/native_router.rs b/crates/components/src/native_router.rs index f56898996..1cc47f28e 100644 --- a/crates/components/src/native_router.rs +++ b/crates/components/src/native_router.rs @@ -119,7 +119,7 @@ mod test { assert_eq!(utils.root().get(0).get(1).get(0).text(), Some("B")); utils.push_event(PlatformEvent::Mouse { - name: EventName::PointerUp, + name: EventName::MouseUp, cursor: (5.0, 5.0).into(), button: Some(MouseButton::Back), }); @@ -128,7 +128,7 @@ mod test { assert_eq!(utils.root().get(0).get(1).get(0).text(), Some("A")); utils.push_event(PlatformEvent::Mouse { - name: EventName::PointerUp, + name: EventName::MouseUp, cursor: (5.0, 5.0).into(), button: Some(MouseButton::Forward), }); diff --git a/crates/core/src/events/events_measurer.rs b/crates/core/src/events/events_measurer.rs index 5efc09c6b..8ec1fa790 100644 --- a/crates/core/src/events/events_measurer.rs +++ b/crates/core/src/events/events_measurer.rs @@ -252,7 +252,7 @@ fn measure_dom_events( true }; - let allowed_event = nodes_state.is_event_allowed(event, node_id); + let allowed_event = nodes_state.is_event_allowed(collateral_event, node_id); if valid_node && allowed_event { let mut valid_event = event.clone(); diff --git a/crates/core/src/events/nodes_state.rs b/crates/core/src/events/nodes_state.rs index d25d245e0..833130d6e 100644 --- a/crates/core/src/events/nodes_state.rs +++ b/crates/core/src/events/nodes_state.rs @@ -15,7 +15,7 @@ use crate::{ }, }; -#[derive(Clone)] +#[derive(Clone, Debug)] struct NodeMetadata { layer: Option, } @@ -30,8 +30,8 @@ pub struct NodesState { impl NodesState { /// Given the current state, event, and NodeID check if it is allowed to be emitted /// For example, it will not make sense to emit a Click event on an element that was not pressed before. - pub fn is_event_allowed(&self, event: &PlatformEvent, node_id: &NodeId) -> bool { - if event.get_name().is_click() { + pub fn is_event_allowed(&self, event_name: EventName, node_id: &NodeId) -> bool { + if event_name.is_pressed() { self.pressed_nodes.contains_key(node_id) } else { true @@ -116,9 +116,9 @@ impl NodesState { layer, } in events { - match event { + match event.get_name() { // Update hovered nodes state - PlatformEvent::Mouse { name, .. } if name.can_change_hover_state() => { + name if name.can_change_hover_state() => { let is_hovered = hovered_nodes.contains_key(node_id); // Mark the Node as hovered if it wasn't already @@ -139,7 +139,7 @@ impl NodesState { } // Update pressed nodes state - PlatformEvent::Mouse { name, .. } if name.can_change_press_state() => { + name if name.can_change_press_state() => { let is_pressed = pressed_nodes.contains_key(node_id); // Mark the Node as pressed if it wasn't already diff --git a/crates/native-core/src/events.rs b/crates/native-core/src/events.rs index 1f537db73..93d3a7813 100644 --- a/crates/native-core/src/events.rs +++ b/crates/native-core/src/events.rs @@ -240,19 +240,25 @@ impl EventName { /// Check if this event can change the press state of a Node. pub fn can_change_press_state(&self) -> bool { - matches!(self, Self::MouseDown | Self::PointerDown) + matches!(self, Self::MouseDown | Self::TouchStart | Self::PointerDown) } /// Check if the event means the cursor started or released a click pub fn was_cursor_pressed_or_released(&self) -> bool { matches!( &self, - Self::MouseDown | Self::PointerDown | Self::MouseUp | Self::Click | Self::PointerUp + Self::MouseDown + | Self::PointerDown + | Self::MouseUp + | Self::Click + | Self::PointerUp + | Self::TouchStart + | Self::TouchEnd ) } - /// Check if the event was a click - pub fn is_click(&self) -> bool { + /// Check if the event was pressed + pub fn is_pressed(&self) -> bool { matches!(&self, Self::Click) } } From bf1fcd2178121880cd7d3bfb4e8c7461e3c4a9ce Mon Sep 17 00:00:00 2001 From: Marc Espin Date: Wed, 18 Sep 2024 21:51:40 +0200 Subject: [PATCH 03/39] fix: Properly adjust accumulated sizes when using padding (#894) * fix: Properly adjust accumulate sizes when using padding * fmt * fix --- crates/torin/src/measure.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/torin/src/measure.rs b/crates/torin/src/measure.rs index bc0b7e6d4..88b8771f7 100644 --- a/crates/torin/src/measure.rs +++ b/crates/torin/src/measure.rs @@ -228,6 +228,9 @@ where ); } + inner_sizes.width += node.padding.horizontal(); + inner_sizes.height += node.padding.vertical(); + ( must_cache_children, LayoutNode { From ebb22ee1be3fdc0759a439b6e70360c09f33d11a Mon Sep 17 00:00:00 2001 From: Marc Espin Date: Thu, 19 Sep 2024 18:19:29 +0200 Subject: [PATCH 04/39] fix: Filter enter events properly, regression of #895 (#896) * fix: Filter enter events properly, regression of #895 * chore: Update test --- crates/core/src/events/events_measurer.rs | 11 ++++------ crates/core/src/events/nodes_state.rs | 26 ++++++++++++----------- crates/core/tests/pointer_events.rs | 14 ++++++++---- 3 files changed, 28 insertions(+), 23 deletions(-) diff --git a/crates/core/src/events/events_measurer.rs b/crates/core/src/events/events_measurer.rs index 8ec1fa790..1f4ae6588 100644 --- a/crates/core/src/events/events_measurer.rs +++ b/crates/core/src/events/events_measurer.rs @@ -38,15 +38,15 @@ pub fn process_events( let potential_events = measure_potential_event_listeners(events, dom, scale_factor, focus_id); // 3. Get what events can be actually emitted based on what elements are listening - let mut dom_events = measure_dom_events(&potential_events, dom, nodes_state, scale_factor); + let mut dom_events = measure_dom_events(&potential_events, dom, scale_factor); // 4. Get potential collateral events, e.g. mousemove -> mouseenter let potential_collateral_events = - nodes_state.process_collateral(&potential_events, &dom_events, events); + nodes_state.process_collateral(&potential_events, &mut dom_events, events); // 5. Get what collateral events can actually be emitted let to_emit_dom_collateral_events = - measure_dom_events(&potential_collateral_events, dom, nodes_state, scale_factor); + measure_dom_events(&potential_collateral_events, dom, scale_factor); let colateral_global_events = measure_colateral_global_events(&to_emit_dom_collateral_events); @@ -218,7 +218,6 @@ fn is_node_parent_of(rdom: &DioxusDOM, node: NodeId, parent_node: NodeId) -> boo fn measure_dom_events( potential_events: &PotentialEvents, fdom: &FreyaDOM, - nodes_state: &NodesState, scale_factor: f64, ) -> Vec { let mut new_events = Vec::new(); @@ -252,9 +251,7 @@ fn measure_dom_events( true }; - let allowed_event = nodes_state.is_event_allowed(collateral_event, node_id); - - if valid_node && allowed_event { + if valid_node { let mut valid_event = event.clone(); valid_event.set_name(collateral_event); valid_events.push(PotentialEvent { diff --git a/crates/core/src/events/nodes_state.rs b/crates/core/src/events/nodes_state.rs index 833130d6e..e7a45ea06 100644 --- a/crates/core/src/events/nodes_state.rs +++ b/crates/core/src/events/nodes_state.rs @@ -28,21 +28,11 @@ pub struct NodesState { } impl NodesState { - /// Given the current state, event, and NodeID check if it is allowed to be emitted - /// For example, it will not make sense to emit a Click event on an element that was not pressed before. - pub fn is_event_allowed(&self, event_name: EventName, node_id: &NodeId) -> bool { - if event_name.is_pressed() { - self.pressed_nodes.contains_key(node_id) - } else { - true - } - } - /// Update the node states given the new events and suggest potential collateral new events pub fn process_collateral( &mut self, pontential_events: &PotentialEvents, - events_to_emit: &[DomEvent], + dom_events: &mut Vec, events: &[PlatformEvent], ) -> PotentialEvents { let mut potential_events = PotentialEvents::default(); @@ -71,7 +61,7 @@ impl NodesState { self.hovered_nodes.retain(|node_id, metadata| { // Check if a DOM event that moves the cursor in this Node will get emitted let no_recently_hovered = - filter_dom_events_by(events_to_emit, node_id, |e| e.was_cursor_moved()); + filter_dom_events_by(dom_events, node_id, |e| e.was_cursor_moved()); if no_recently_hovered { // If there has been a mouse movement but a DOM event was not emitted to this node, then we safely assume @@ -156,6 +146,18 @@ impl NodesState { } } + dom_events.retain(|ev| { + match ev.name { + // Filter out enter events for nodes that were already hovered + _ if ev.name.is_enter() => !hovered_nodes.contains_key(&ev.node_id), + + // Filter out press events for nodes that were already pressed + _ if ev.name.is_pressed() => !pressed_nodes.contains_key(&ev.node_id), + + _ => true, + } + }); + // Order the events by their Nodes layer for events in potential_events.values_mut() { events.sort_by(|left, right| left.layer.cmp(&right.layer)) diff --git a/crates/core/tests/pointer_events.rs b/crates/core/tests/pointer_events.rs index fe0be7e27..20b3c679c 100644 --- a/crates/core/tests/pointer_events.rs +++ b/crates/core/tests/pointer_events.rs @@ -55,6 +55,12 @@ pub async fn pointer_events_from_mouse() { Some(format!("{:?}", vec!["enter", "over"]).as_str()) ); + utils.move_cursor((101., 100.)).await; + assert_eq!( + label.get(0).text(), + Some(format!("{:?}", vec!["enter", "over", "over"]).as_str()) + ); + utils.push_event(PlatformEvent::Mouse { name: EventName::MouseDown, cursor: CursorPoint::new(100.0, 100.0), @@ -63,7 +69,7 @@ pub async fn pointer_events_from_mouse() { utils.wait_for_update().await; assert_eq!( label.get(0).text(), - Some(format!("{:?}", vec!["enter", "over", "down"]).as_str()) + Some(format!("{:?}", vec!["enter", "over", "over", "down"]).as_str()) ); utils.push_event(PlatformEvent::Mouse { @@ -74,13 +80,13 @@ pub async fn pointer_events_from_mouse() { utils.wait_for_update().await; assert_eq!( label.get(0).text(), - Some(format!("{:?}", vec!["enter", "over", "down", "up"]).as_str()) + Some(format!("{:?}", vec!["enter", "over", "over", "down", "up"]).as_str()) ); utils.move_cursor((0., 0.)).await; assert_eq!( label.get(0).text(), - Some(format!("{:?}", vec!["enter", "over", "down", "up", "leave"]).as_str()) + Some(format!("{:?}", vec!["enter", "over", "over", "down", "up", "leave"]).as_str()) ); utils.push_event(PlatformEvent::Mouse { @@ -94,7 +100,7 @@ pub async fn pointer_events_from_mouse() { Some( format!( "{:?}", - vec!["enter", "over", "down", "up", "leave", "globalup"] + vec!["enter", "over", "over", "down", "up", "leave", "globalup"] ) .as_str() ) From 3ae218dd16eb0ad01cb5d855834863748bdfea1b Mon Sep 17 00:00:00 2001 From: Marc Espin Date: Thu, 19 Sep 2024 18:19:53 +0200 Subject: [PATCH 05/39] feat: Only focus focusable nodes (#884) * feat: Only focus focusable nodes * chore: Update tests * chore: Update tests * chore: Bring back a11y_focusable --- crates/components/src/button.rs | 1 - crates/components/src/input.rs | 1 - crates/components/src/menu.rs | 1 - crates/components/src/radio.rs | 1 - crates/components/src/tabs.rs | 2 -- crates/core/src/accessibility/tree.rs | 7 +++-- crates/elements/src/definitions.rs | 9 +++--- crates/hooks/src/use_init_native_platform.rs | 6 ++++ crates/native-core/src/attributes.rs | 2 +- crates/state/src/accessibility.rs | 20 +++++++----- crates/state/src/values/focusable.rs | 32 ++++++++++++++++++++ crates/state/src/values/mod.rs | 2 ++ crates/state/tests/parse_focusable.rs | 23 ++++++++++++++ 13 files changed, 86 insertions(+), 21 deletions(-) create mode 100644 crates/state/src/values/focusable.rs create mode 100644 crates/state/tests/parse_focusable.rs diff --git a/crates/components/src/button.rs b/crates/components/src/button.rs index 8ec8ef058..c9a9149ce 100644 --- a/crates/components/src/button.rs +++ b/crates/components/src/button.rs @@ -180,7 +180,6 @@ pub fn Button( height: "{height}", padding: "{padding}", margin: "{margin}", - a11y_focusable: "true", overflow: "clip", a11y_role:"button", color: "{font_theme.color}", diff --git a/crates/components/src/input.rs b/crates/components/src/input.rs index 7140b11a5..e5b85843a 100644 --- a/crates/components/src/input.rs +++ b/crates/components/src/input.rs @@ -217,7 +217,6 @@ pub fn Input( main_align: "center", cursor_reference, a11y_id, - a11y_focusable: "true", a11y_role: "textInput", a11y_auto_focus: "{auto_focus}", onkeydown, diff --git a/crates/components/src/menu.rs b/crates/components/src/menu.rs index 7d342b5d1..e1a1b8ae5 100644 --- a/crates/components/src/menu.rs +++ b/crates/components/src/menu.rs @@ -201,7 +201,6 @@ pub fn MenuItem( width: "fill-min", padding: "6", margin: "2", - a11y_focusable: "true", a11y_role:"button", color: "{font_theme.color}", corner_radius: "{corner_radius}", diff --git a/crates/components/src/radio.rs b/crates/components/src/radio.rs index b977f0840..5f0847c40 100644 --- a/crates/components/src/radio.rs +++ b/crates/components/src/radio.rs @@ -83,7 +83,6 @@ pub fn Radio( corner_radius: "99", rect { a11y_id: focus.attribute(), - a11y_focusable: "true", width: "18", height: "18", border: "2 solid {fill}", diff --git a/crates/components/src/tabs.rs b/crates/components/src/tabs.rs index acf49b569..b056dcd62 100644 --- a/crates/components/src/tabs.rs +++ b/crates/components/src/tabs.rs @@ -142,7 +142,6 @@ pub fn Tab( a11y_id, width: "{width}", height: "{height}", - a11y_focusable: "true", overflow: "clip", a11y_role:"tab", color: "{font_theme.color}", @@ -254,7 +253,6 @@ pub fn BottomTab(children: Element, theme: Option) -> Elemen a11y_id, width: "{width}", height: "{height}", - a11y_focusable: "true", overflow: "clip", a11y_role:"tab", color: "{font_theme.color}", diff --git a/crates/core/src/accessibility/tree.rs b/crates/core/src/accessibility/tree.rs index 83f8d9499..ef5b1bfd9 100644 --- a/crates/core/src/accessibility/tree.rs +++ b/crates/core/src/accessibility/tree.rs @@ -257,7 +257,10 @@ impl AccessibilityTree { let accessibility_id = node_ref.get_accessibility_id(); if let Some(accessibility_id) = accessibility_id { - nodes.push((accessibility_id, node_ref.id())) + let accessibility_state = node_ref.get::().unwrap(); + if accessibility_state.a11y_focusable.is_enabled() { + nodes.push((accessibility_id, node_ref.id())) + } } if let Some(tag) = node_ref.node_type().tag() { @@ -347,7 +350,7 @@ impl AccessibilityTree { // Set focusable action // This will cause assistive technology to offer the user an option // to focus the current element if it supports it. - if node_accessibility.a11y_focusable { + if node_accessibility.a11y_focusable.is_enabled() { builder.add_action(Action::Focus); } diff --git a/crates/elements/src/definitions.rs b/crates/elements/src/definitions.rs index 9ee27960a..294f9ab36 100644 --- a/crates/elements/src/definitions.rs +++ b/crates/elements/src/definitions.rs @@ -235,10 +235,10 @@ builder_constructors! { a11y_auto_focus: String, a11y_name: String, - a11y_focusable: String, a11y_role:String, a11y_id: AccessibilityId, a11y_alt: String, + a11y_focusable: String, canvas_reference: String, layer: String, offset_y: String, @@ -308,10 +308,10 @@ builder_constructors! { layer: String, a11y_auto_focus: String, a11y_name: String, - a11y_focusable: String, a11y_role:String, a11y_id: AccessibilityId, a11y_alt: String, + a11y_focusable: String, }; /// `paragraph` element let's you build texts with different styles. /// @@ -387,10 +387,10 @@ builder_constructors! { cursor_id: String, a11y_auto_focus: String, a11y_name: String, - a11y_focusable: String, a11y_role:String, a11y_id: AccessibilityId, a11y_alt: String, + a11y_focusable: String, highlights: String, highlight_color: String, highlight_mode: String, @@ -459,7 +459,6 @@ builder_constructors! { image_reference: String, a11y_auto_focus: String, a11y_name: String, - a11y_focusable: String, a11y_role:String, a11y_id: AccessibilityId, a11y_alt: String, @@ -499,10 +498,10 @@ builder_constructors! { svg_content: String, a11y_auto_focus: String, a11y_name: String, - a11y_focusable: String, a11y_role:String, a11y_id: AccessibilityId, a11y_alt: String, + a11y_focusable: String, }; } diff --git a/crates/hooks/src/use_init_native_platform.rs b/crates/hooks/src/use_init_native_platform.rs index 4f33f167d..0e9f3a89c 100644 --- a/crates/hooks/src/use_init_native_platform.rs +++ b/crates/hooks/src/use_init_native_platform.rs @@ -152,7 +152,9 @@ mod test { pub async fn uncontrolled_focus_accessibility() { #[allow(non_snake_case)] fn OtherChild() -> Element { + let mut focus = use_focus(); rsx!(rect { + a11y_id: focus.attribute(), a11y_role: "genericContainer", width: "100%", height: "50%", @@ -216,12 +218,16 @@ 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(); rsx!( rect { + a11y_id: focus_1.attribute(), a11y_role: "genericContainer", a11y_auto_focus: "true", } rect { + a11y_id: focus_2.attribute(), a11y_role: "genericContainer", a11y_auto_focus: "true", } diff --git a/crates/native-core/src/attributes.rs b/crates/native-core/src/attributes.rs index 6f248ac65..5c2c6556c 100644 --- a/crates/native-core/src/attributes.rs +++ b/crates/native-core/src/attributes.rs @@ -119,10 +119,10 @@ impl FromStr for AttributeName { "content" => Ok(AttributeName::Content), "a11y_auto_focus" => Ok(AttributeName::A11YAutoFocus), "a11y_name" => Ok(AttributeName::A11YName), - "a11y_focusable" => Ok(AttributeName::A11YFocusable), "a11y_role" => Ok(AttributeName::A11YRole), "a11y_id" => Ok(AttributeName::A11YId), "a11y_alt" => Ok(AttributeName::A11YAlt), + "a11y_focusable" => Ok(AttributeName::A11YFocusable), "canvas_reference" => Ok(AttributeName::CanvasReference), "layer" => Ok(AttributeName::Layer), "offset_y" => Ok(AttributeName::OffsetY), diff --git a/crates/state/src/accessibility.rs b/crates/state/src/accessibility.rs index 1e81eeae2..1fb90b874 100644 --- a/crates/state/src/accessibility.rs +++ b/crates/state/src/accessibility.rs @@ -29,6 +29,8 @@ use freya_native_core_macro::partial_derive_state; use crate::{ CustomAttributeValues, + Focusable, + Parse, ParseAttribute, ParseError, }; @@ -42,8 +44,8 @@ pub struct AccessibilityNodeState { pub a11y_role: Option, pub a11y_alt: Option, pub a11y_name: Option, - pub a11y_focusable: bool, pub a11y_auto_focus: bool, + pub a11y_focusable: Focusable, } impl ParseAttribute for AccessibilityNodeState { @@ -57,6 +59,11 @@ impl ParseAttribute for AccessibilityNodeState { attr.value { self.a11y_id = Some(*id); + + // Enable focus on nodes that pass a custom a11y id + if self.a11y_focusable.is_unknown() { + self.a11y_focusable = Focusable::Enabled; + } } } AttributeName::A11YRole => { @@ -77,14 +84,14 @@ impl ParseAttribute for AccessibilityNodeState { self.a11y_name = Some(attr.to_owned()) } } - AttributeName::A11YFocusable => { + AttributeName::A11YAutoFocus => { if let OwnedAttributeValue::Text(attr) = attr.value { - self.a11y_focusable = attr.parse().unwrap_or_default() + self.a11y_auto_focus = attr.parse().unwrap_or_default() } } - AttributeName::A11YAutoFocus => { + AttributeName::A11YFocusable => { if let OwnedAttributeValue::Text(attr) = attr.value { - self.a11y_auto_focus = attr.parse().unwrap_or_default() + self.a11y_focusable = Focusable::parse(attr)?; } } _ => {} @@ -108,7 +115,6 @@ impl State for AccessibilityNodeState { AttributeName::A11YRole, AttributeName::A11YAlt, AttributeName::A11YName, - AttributeName::A11YFocusable, AttributeName::A11YAutoFocus, ])); @@ -171,7 +177,7 @@ impl State for AccessibilityNodeState { #[cfg(debug_assertions)] tracing::info!("Assigned {id:?} to {:?}", node_view.node_id()); - self.a11y_id = Some(id) + self.a11y_id = Some(id); } let was_just_created = !had_id && self.a11y_id.is_some(); diff --git a/crates/state/src/values/focusable.rs b/crates/state/src/values/focusable.rs new file mode 100644 index 000000000..8f4e8d9c2 --- /dev/null +++ b/crates/state/src/values/focusable.rs @@ -0,0 +1,32 @@ +use crate::{ + Parse, + ParseError, +}; + +#[derive(Clone, Debug, PartialEq, Default, Eq)] +pub enum Focusable { + #[default] + Unknown, + Disabled, + Enabled, +} + +impl Focusable { + pub fn is_unknown(&self) -> bool { + matches!(self, Self::Unknown) + } + + pub fn is_enabled(&self) -> bool { + matches!(self, Self::Enabled) + } +} + +impl Parse for Focusable { + fn parse(value: &str) -> Result { + Ok(match value { + "true" => Self::Enabled, + "false" => Self::Disabled, + _ => Self::Unknown, + }) + } +} diff --git a/crates/state/src/values/mod.rs b/crates/state/src/values/mod.rs index 03c51ecd7..4f2b435e4 100644 --- a/crates/state/src/values/mod.rs +++ b/crates/state/src/values/mod.rs @@ -6,6 +6,7 @@ mod corner_radius; mod cursor; mod decoration; mod fill; +mod focusable; mod font; mod gaps; mod gradient; @@ -21,6 +22,7 @@ pub use color::*; pub use corner_radius::*; pub use cursor::*; pub use fill::*; +pub use focusable::*; pub use font::*; pub use gradient::*; pub use highlight::*; diff --git a/crates/state/tests/parse_focusable.rs b/crates/state/tests/parse_focusable.rs new file mode 100644 index 000000000..62ae1d9c9 --- /dev/null +++ b/crates/state/tests/parse_focusable.rs @@ -0,0 +1,23 @@ +use freya_engine::prelude::*; +use freya_node_state::{ + Focusable, + Parse, +}; + +#[test] +fn parse_focusable_enabled() { + let enabled = Focusable::parse("true"); + assert_eq!(enabled, Ok(Focusable::Enabled)); +} + +#[test] +fn parse_focusable_disabled() { + let disabled = Focusable::parse("false"); + assert_eq!(disabled, Ok(Focusable::Disabled)); +} + +#[test] +fn parse_focusable_fallback() { + let fallback = Focusable::parse("hello!!"); + assert_eq!(fallback, Ok(Focusable::Unknown)); +} From bdbcabfce55e4bee3b3d66e39a674ecda52a9066 Mon Sep 17 00:00:00 2001 From: Marc Espin Date: Thu, 19 Sep 2024 18:35:43 +0200 Subject: [PATCH 06/39] feat: Rename `pointerover` event to `pointermove` (#897) --- crates/core/tests/pointer_events.rs | 26 +++++++++---------- .../events/{pointerover.md => pointermove.md} | 6 ++--- crates/elements/src/definitions.rs | 4 +-- crates/native-core/src/events.rs | 4 +-- examples/pointer.rs | 4 +-- 5 files changed, 22 insertions(+), 22 deletions(-) rename crates/elements/src/_docs/events/{pointerover.md => pointermove.md} (64%) diff --git a/crates/core/tests/pointer_events.rs b/crates/core/tests/pointer_events.rs index 20b3c679c..59e2bcf73 100644 --- a/crates/core/tests/pointer_events.rs +++ b/crates/core/tests/pointer_events.rs @@ -12,7 +12,7 @@ pub async fn pointer_events_from_mouse() { let onpointerup = move |_| state.push("up".to_string()); - let onpointerover = move |_| state.push("over".to_string()); + let onpointermove = move |_| state.push("move".to_string()); let onpointerenter = move |_| state.push("enter".to_string()); @@ -30,7 +30,7 @@ pub async fn pointer_events_from_mouse() { width: "100%", onpointerdown, onpointerup, - onpointerover, + onpointermove, onpointerenter, onpointerleave, onglobalpointerup, @@ -52,13 +52,13 @@ pub async fn pointer_events_from_mouse() { utils.move_cursor((100., 100.)).await; assert_eq!( label.get(0).text(), - Some(format!("{:?}", vec!["enter", "over"]).as_str()) + Some(format!("{:?}", vec!["enter", "move"]).as_str()) ); utils.move_cursor((101., 100.)).await; assert_eq!( label.get(0).text(), - Some(format!("{:?}", vec!["enter", "over", "over"]).as_str()) + Some(format!("{:?}", vec!["enter", "move", "move"]).as_str()) ); utils.push_event(PlatformEvent::Mouse { @@ -69,7 +69,7 @@ pub async fn pointer_events_from_mouse() { utils.wait_for_update().await; assert_eq!( label.get(0).text(), - Some(format!("{:?}", vec!["enter", "over", "over", "down"]).as_str()) + Some(format!("{:?}", vec!["enter", "move", "move", "down"]).as_str()) ); utils.push_event(PlatformEvent::Mouse { @@ -80,13 +80,13 @@ pub async fn pointer_events_from_mouse() { utils.wait_for_update().await; assert_eq!( label.get(0).text(), - Some(format!("{:?}", vec!["enter", "over", "over", "down", "up"]).as_str()) + Some(format!("{:?}", vec!["enter", "move", "move", "down", "up"]).as_str()) ); utils.move_cursor((0., 0.)).await; assert_eq!( label.get(0).text(), - Some(format!("{:?}", vec!["enter", "over", "over", "down", "up", "leave"]).as_str()) + Some(format!("{:?}", vec!["enter", "move", "move", "down", "up", "leave"]).as_str()) ); utils.push_event(PlatformEvent::Mouse { @@ -100,7 +100,7 @@ pub async fn pointer_events_from_mouse() { Some( format!( "{:?}", - vec!["enter", "over", "over", "down", "up", "leave", "globalup"] + vec!["enter", "move", "move", "down", "up", "leave", "globalup"] ) .as_str() ) @@ -116,7 +116,7 @@ pub async fn pointer_events_from_touch() { let onpointerup = move |_| state.push("up".to_string()); - let onpointerover = move |_| state.push("over".to_string()); + let onpointermove = move |_| state.push("move".to_string()); let onpointerenter = move |_| state.push("enter".to_string()); @@ -130,7 +130,7 @@ pub async fn pointer_events_from_touch() { width: "100%", onpointerdown: onpointerdown, onpointerup: onpointerup, - onpointerover: onpointerover, + onpointermove: onpointermove, onpointerenter: onpointerenter, label { "{state:?}" } } @@ -156,7 +156,7 @@ pub async fn pointer_events_from_touch() { utils.wait_for_update().await; assert_eq!( label.get(0).text(), - Some(format!("{:?}", vec!["enter", "over"]).as_str()) + Some(format!("{:?}", vec!["enter", "move"]).as_str()) ); utils.push_event(PlatformEvent::Touch { @@ -169,7 +169,7 @@ pub async fn pointer_events_from_touch() { utils.wait_for_update().await; assert_eq!( label.get(0).text(), - Some(format!("{:?}", vec!["enter", "over", "down"]).as_str()) + Some(format!("{:?}", vec!["enter", "move", "down"]).as_str()) ); utils.push_event(PlatformEvent::Touch { @@ -182,6 +182,6 @@ pub async fn pointer_events_from_touch() { utils.wait_for_update().await; assert_eq!( label.get(0).text(), - Some(format!("{:?}", vec!["enter", "over", "down", "up"]).as_str()) + Some(format!("{:?}", vec!["enter", "move", "down", "up"]).as_str()) ); } diff --git a/crates/elements/src/_docs/events/pointerover.md b/crates/elements/src/_docs/events/pointermove.md similarity index 64% rename from crates/elements/src/_docs/events/pointerover.md rename to crates/elements/src/_docs/events/pointermove.md index 306251968..b6f1afd6d 100644 --- a/crates/elements/src/_docs/events/pointerover.md +++ b/crates/elements/src/_docs/events/pointermove.md @@ -1,6 +1,6 @@ -The `pointerover` event fires when the user hovers/touches over an element. +The `pointermove` event fires when the user moves the cursor or touches over an element. Unlike [`onpointerenter`](crate::elements::onpointerenter), this fires even if the user was already hovering over -the element. For that reason, it's less efficient. +the element. Event Data: [`PointerData`](crate::events::PointerData) @@ -14,7 +14,7 @@ fn app() -> Element { width: "100", height: "100", background: "red", - onpointerover: |_| println!("Hovering or touching!") + onpointermove: |_| println!("Moving or touching!") } ) } diff --git a/crates/elements/src/definitions.rs b/crates/elements/src/definitions.rs index 294f9ab36..e297c857f 100644 --- a/crates/elements/src/definitions.rs +++ b/crates/elements/src/definitions.rs @@ -647,8 +647,8 @@ pub mod events { onpointerup #[doc = include_str!("_docs/events/globalpointerup.md")] onglobalpointerup - #[doc = include_str!("_docs/events/pointerover.md")] - onpointerover + #[doc = include_str!("_docs/events/pointermove.md")] + onpointermove #[doc = include_str!("_docs/events/pointerenter.md")] onpointerenter #[doc = include_str!("_docs/events/pointerleave.md")] diff --git a/crates/native-core/src/events.rs b/crates/native-core/src/events.rs index 93d3a7813..d5be74793 100644 --- a/crates/native-core/src/events.rs +++ b/crates/native-core/src/events.rs @@ -56,7 +56,7 @@ impl FromStr for EventName { "mouseenter" => Ok(EventName::MouseEnter), "mouseleave" => Ok(EventName::MouseLeave), "wheel" => Ok(EventName::Wheel), - "pointerover" => Ok(EventName::PointerOver), + "pointermove" => Ok(EventName::PointerOver), "pointerdown" => Ok(EventName::PointerDown), "pointerenter" => Ok(EventName::PointerEnter), "pointerleave" => Ok(EventName::PointerLeave), @@ -93,7 +93,7 @@ impl From for &str { EventName::MouseEnter => "mouseenter", EventName::MouseLeave => "mouseleave", EventName::Wheel => "wheel", - EventName::PointerOver => "pointerover", + EventName::PointerOver => "pointermove", EventName::PointerDown => "pointerdown", EventName::PointerEnter => "pointerenter", EventName::PointerLeave => "pointerleave", diff --git a/examples/pointer.rs b/examples/pointer.rs index 7098ff4d1..14ae1b149 100644 --- a/examples/pointer.rs +++ b/examples/pointer.rs @@ -18,7 +18,7 @@ fn app() -> Element { println!("Up -> {:?}", ev.data.get_pointer_type()); }; - let onpointerover = move |ev: PointerEvent| { + let onpointermove = move |ev: PointerEvent| { println!("Over -> {:?}", ev.data.get_pointer_type()); }; @@ -50,7 +50,7 @@ fn app() -> Element { padding: "12", onpointerdown, onpointerup, - onpointerover, + onpointermove, onpointerenter, onpointerleave, onglobalpointerup, From 0f046764fd05a02e16f473b5ad8f10c40817b133 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Sat, 21 Sep 2024 20:14:31 +0200 Subject: [PATCH 07/39] fix: Pin mdbook to 0.4.32 --- .github/workflows/deploy_book.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/deploy_book.yml b/.github/workflows/deploy_book.yml index c40397d18..007599b46 100644 --- a/.github/workflows/deploy_book.yml +++ b/.github/workflows/deploy_book.yml @@ -19,6 +19,8 @@ jobs: - name: Setup mdBook uses: peaceiris/actions-mdbook@v2 + with: + mdbook-version: '0.4.32' - run: mdbook build working-directory: book From d4bc2dfbcbaafc8b526f9267d7b74dd742d225aa Mon Sep 17 00:00:00 2001 From: marc2332 Date: Sun, 22 Sep 2024 12:22:48 +0200 Subject: [PATCH 08/39] docs: Update differences with dioxus --- book/src/differences_with_dioxus.md | 33 ++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/book/src/differences_with_dioxus.md b/book/src/differences_with_dioxus.md index 29e0a36b5..0ae60f283 100644 --- a/book/src/differences_with_dioxus.md +++ b/book/src/differences_with_dioxus.md @@ -1,15 +1,32 @@ # Differences with Dioxus -**Freya** is built on top of the core crates from Dioxus, this means that you will effectively be creating Dioxus components, using RSX and hooks. But, you will **not** be using HTML, CSS, JS or any Web tech at all. +**Freya** is built on top of the **core** crates from Dioxus, this means that you will effectively be creating Dioxus components, using RSX and hooks. -Here you can find a list of the main differences between Freya and the official Dioxus renderers for Desktop (WebView and Blitz): +**But**, thanks to Dioxus being a renderer-agnostic library you will **NOT** be using JavaScript, HTML, OR CSS, or any other abstraction that ends up using one of those or anything close to web. + + Freya does everything on its own when it comes to: +- Elements +- Styling +- Layout +- Events +- Rendering +- Testing +- Built-in components and hooks, +- Editing +- Animating + +And more. Dioxus only is only used to run the app components (hooks, lifecycle, state, rsx), **everything else is managed by Freya**. + +**Freya is not mean to be drop-in alternative to Dioxus renderers but as GUI library on its own.** + +Here is the list of the main differences between Freya and the official Dioxus renderers for Desktop (WebView and Blitz): | Category | Freya | Dioxus Renderers | |--------------------------------------|------------------|---------------------------------| | **Elements, attributes and events** | Custom | HTML | -| **Layout** | [`Torin`](https://github.com/marc2332/freya/tree/main/crates/torin) | CSS or [`Taffy`](https://github.com/DioxusLabs/taffy) | -| **Styling** | Custom | CSS | -| **Renderer** | Skia | WebView or WGPU | -| **Components library** | Custom | None, but can use HTML elements and CSS libraries | -| **Devtools** | Custom | Provided in Webview | -| **Headless testing runner** | Custom | None, but there is Playwright and similar | +| **Layout** | Custom ([`Torin`](https://github.com/marc2332/freya/tree/main/crates/torin)) | CSS or [`Taffy`](https://github.com/DioxusLabs/taffy) | +| **Styling** | Custom | CSS | +| **Renderer** | Skia | WebView or WGPU | +| **Components library** | Custom ([`freya-comonents`](https://github.com/marc2332/freya/tree/main/crates/components)) | None, but can use HTML elements and CSS libraries | +| **Devtools** | Custom ([`freya-devtools`](https://github.com/marc2332/freya/tree/main/crates/devtools)) | Provided in Webview | +| **Headless testing runner** | Custom ([`freya-testing`](https://github.com/marc2332/freya/tree/main/crates/testing)) | None, but there is Playwright and similar | From 90f2bfbf60694e11d09ff091dc4685d4cb5d988c Mon Sep 17 00:00:00 2001 From: Marc Espin Date: Sun, 22 Sep 2024 12:52:00 +0200 Subject: [PATCH 09/39] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index caca0862f..3b1cef692 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ **Freya** is a cross-paltform GUI library for Rust powered by 🧬 [Dioxus](https://dioxuslabs.com) and 🎨 [Skia](https://skia.org/). +**It does not use any web tech**, check the [Differences with Dioxus](https://book.freyaui.dev/differences_with_dioxus.html). + ⚠️ It's currently work in progress, but you can already play with it! You can join the [Discord](https://discord.gg/sYejxCdewG) server if you have any question or issue.
From ad619b6a96797221f0a670fa35f9d3c84b7f1ed8 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Mon, 23 Sep 2024 15:07:48 +0200 Subject: [PATCH 10/39] fix: Invalidate area of canvs in opengl_rtt.rs example --- examples/opengl_rtt.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/opengl_rtt.rs b/examples/opengl_rtt.rs index 1e04f4601..9bb8b549b 100644 --- a/examples/opengl_rtt.rs +++ b/examples/opengl_rtt.rs @@ -531,13 +531,16 @@ fn app() -> Element { let mut r = use_signal(|| 100.0); let mut g: Signal = use_signal(|| 0.0); let mut b = use_signal(|| 0.0); + let platform = use_platform(); + let (reference, size) = use_node_signal(); let triangle_renderer = use_hook(|| Arc::new(Mutex::new(TriangleRenderer::new()))); let canvas = use_canvas(move || { let color = (*r.read(), *g.read(), *b.read()); let triangle_renderer = triangle_renderer.clone(); - + platform.invalidate_drawing_area(size.read().area); + platform.request_animation_frame(); Box::new(move |ctx| { ctx.canvas.translate((ctx.area.min_x(), ctx.area.min_y())); let mut renderer_guard = triangle_renderer.lock().unwrap(); @@ -557,6 +560,7 @@ fn app() -> Element { rsx!( rect { + reference, canvas_reference: canvas.attribute(), width: "100%", height: "100%", From 5ba5463a6f0146372eca7e030a4779fb1ce9754e Mon Sep 17 00:00:00 2001 From: kuba375 <114685362+kuba375@users.noreply.github.com> Date: Mon, 23 Sep 2024 15:10:45 +0200 Subject: [PATCH 11/39] Add NixOS setup (#911) adding NixOS setup via a flake.nix, as mentioned on Discord. --- book/src/setup.md | 4 ++++ 1 file changed, 4 insertions(+) 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 From dab31780609f76533314296ca8987c0b5ad0af3a Mon Sep 17 00:00:00 2001 From: Marc Espin Date: Mon, 23 Sep 2024 18:43:44 +0200 Subject: [PATCH 12/39] =?UTF-8?q?fix:=20Scale=20with=20window's=20scaled?= =?UTF-8?q?=20actor=20the=20areas=20requested=20for=20invalid=E2=80=A6=20(?= =?UTF-8?q?#912)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: Scale with window's scaled actor the areas requested for invalidation * fix lint --- crates/renderer/src/renderer.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/renderer/src/renderer.rs b/crates/renderer/src/renderer.rs index 1b2ec0f92..6eda7db36 100644 --- a/crates/renderer/src/renderer.rs +++ b/crates/renderer/src/renderer.rs @@ -191,8 +191,10 @@ 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(); + let sf = window.scale_factor() as f32; + area.size *= sf; let mut compositor_dirty_area = fdom.compositor_dirty_area(); compositor_dirty_area.unite_or_insert(&area) } From c872a0e226903a07dd59982ba561c374ca328547 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Fri, 27 Sep 2024 15:09:57 +0200 Subject: [PATCH 13/39] feat: Small improvements in `SnackBar` --- crates/components/src/snackbar.rs | 10 +++++----- examples/snackbar.rs | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/components/src/snackbar.rs b/crates/components/src/snackbar.rs index a33770f01..09897410d 100644 --- a/crates/components/src/snackbar.rs +++ b/crates/components/src/snackbar.rs @@ -20,7 +20,7 @@ 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 { @@ -31,7 +31,7 @@ use freya_hooks::{ /// 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(); diff --git a/examples/snackbar.rs b/examples/snackbar.rs index 456db6424..7c62dc16b 100644 --- a/examples/snackbar.rs +++ b/examples/snackbar.rs @@ -13,13 +13,13 @@ fn app() -> Element { let animation = use_animation(move |ctx| { ctx.with( AnimNum::new(0., 100.) - .time(1650) + .time(1850) .ease(Ease::Out) .function(Function::Sine), ) }); let progress = animation.get().read().as_f32(); - let mut show = use_signal(|| false); + let mut open = use_signal(|| false); rsx!( rect { @@ -30,8 +30,8 @@ fn app() -> Element { direction: "horizontal", Button { onpress: move |_| { - show.toggle(); - if *show.read() { + open.toggle(); + if open() { animation.start(); } else { animation.reset(); @@ -40,7 +40,7 @@ fn app() -> Element { label { "Install" } } SnackBar { - show, + open, ProgressBar { show_progress: true, progress: progress From 25b39128e14668f9f9e1037af4fb196a91c0ab2c Mon Sep 17 00:00:00 2001 From: marc2332 Date: Fri, 27 Sep 2024 15:11:53 +0200 Subject: [PATCH 14/39] docs: Update main_align_cross_align.md --- .../src/_docs/attributes/main_align_cross_align.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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. From 4e4afafca94aa62654f280ecf37420b7a08f4972 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Fri, 27 Sep 2024 15:24:59 +0200 Subject: [PATCH 15/39] chore: Fix snackbar tests --- crates/components/src/snackbar.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/components/src/snackbar.rs b/crates/components/src/snackbar.rs index 09897410d..dc6df4fa5 100644 --- a/crates/components/src/snackbar.rs +++ b/crates/components/src/snackbar.rs @@ -27,7 +27,7 @@ use freya_hooks::{ /// height: "100%", /// width: "100%", /// Button { -/// onpress: move |_| show.toggle(), +/// onpress: move |_| open.toggle(), /// label { "Open" } /// } /// SnackBar { @@ -117,18 +117,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 +148,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; From 9c9aae8922d5ee1038f3c717f8ff5eb6c64421a0 Mon Sep 17 00:00:00 2001 From: Marc Espin Date: Sat, 28 Sep 2024 10:30:54 +0200 Subject: [PATCH 16/39] feat: `TooltipContainer` (#900) --- crates/components/src/link.rs | 37 +++++----- crates/components/src/tooltip.rs | 89 ++++++++++++++++++++++--- crates/core/src/render/skia_measurer.rs | 12 +++- examples/switch_theme.rs | 45 +++++++++---- examples/tooltip.rs | 44 ++++++++++++ 5 files changed, 182 insertions(+), 45 deletions(-) create mode 100644 examples/tooltip.rs 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/tooltip.rs b/crates/components/src/tooltip.rs index 6ec61d01a..09f0ee603 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,81 @@ pub fn Tooltip(TooltipProps { url, theme }: TooltipProps) -> Element { rsx!( rect { padding: "4 10", - shadow: "0 4 5 0 rgb(0, 0, 0, 0.1)", + shadow: "0 0 4 1 rgb(0, 0, 0, 0.1)", border: "1 solid {border_fill}", - corner_radius: "10", + 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}, + if *is_hovering.read() { + rect { + height: "0", + width: "0", + layer: "-999", + 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/render/skia_measurer.rs b/crates/core/src/render/skia_measurer.rs index dd98f41a8..3ba11be03 100644 --- a/crates/core/src/render/skia_measurer.rs +++ b/crates/core/src/render/skia_measurer.rs @@ -155,7 +155,11 @@ pub fn create_label( } let mut paragraph = paragraph_builder.build(); - paragraph.layout(area_size.width + 1.0); + paragraph.layout(if font_style.max_lines == Some(1) { + f32::MAX + } else { + area_size.width + 1.0 + }); // Measure the actual text height, ignoring the line height let mut height = paragraph.height(); @@ -268,7 +272,11 @@ pub fn create_paragraph( } let mut paragraph = paragraph_builder.build(); - paragraph.layout(area_size.width + 1.0); + paragraph.layout(if font_style.max_lines == Some(1) { + f32::MAX + } else { + area_size.width + 1.0 + }); // Measure the actual text height, ignoring the line height let mut height = paragraph.height(); diff --git a/examples/switch_theme.rs b/examples/switch_theme.rs index fd288b317..e3c0e6b3c 100644 --- a/examples/switch_theme.rs +++ b/examples/switch_theme.rs @@ -14,24 +14,41 @@ fn ThemeChanger() -> Element { let mut theme = use_theme(); rsx!( - Tile { - onselect: move |_| theme.set(DARK_THEME), - leading: rsx!( - Radio { - selected: theme.read().name == "dark", - }, + TooltipContainer { + position: TooltipPosition::Besides, + tooltip: rsx!( + Tooltip { + text: "Switch to Dark theme" + } ), - label { "Dark" } + Tile { + onselect: move |_| theme.set(DARK_THEME), + leading: rsx!( + Radio { + selected: theme.read().name == "dark", + }, + ), + label { "Dark" } + } } - Tile { - onselect: move |_| theme.set(LIGHT_THEME), - leading: rsx!( - Radio { - selected: theme.read().name == "light", - }, + TooltipContainer { + position: TooltipPosition::Besides, + tooltip: rsx!( + Tooltip { + text: "Switch to Light theme" + } ), - label { "Light" } + Tile { + onselect: move |_| theme.set(LIGHT_THEME), + leading: rsx!( + Radio { + selected: theme.read().name == "light", + }, + ), + label { "Light" } + } } + ) } diff --git a/examples/tooltip.rs b/examples/tooltip.rs new file mode 100644 index 000000000..6b40fa7e8 --- /dev/null +++ b/examples/tooltip.rs @@ -0,0 +1,44 @@ +#![cfg_attr( + all(not(debug_assertions), target_os = "windows"), + windows_subsystem = "windows" +)] + +use freya::prelude::*; + +fn main() { + launch(app); +} + +fn app() -> Element { + rsx!( + rect { + height: "fill", + width: "fill", + main_align: "center", + cross_align: "center", + direction: "horizontal", + spacing: "10", + TooltipContainer { + tooltip: rsx!( + Tooltip { + text: "Hello, World!" + } + ), + Button { + label { "Hello!!" } + } + } + TooltipContainer { + position: TooltipPosition::Besides, + tooltip: rsx!( + Tooltip { + text: "Hello, World!" + } + ), + Button { + label { "Hello!!" } + } + } + } + ) +} From 407287fd3cfeb853b8881722a0cd984d603caa9d Mon Sep 17 00:00:00 2001 From: Marc Espin Date: Sat, 28 Sep 2024 10:33:20 +0200 Subject: [PATCH 17/39] fix: Various fixes when editing utf16-encoded text (#901) * fix: Various fixes when editing utf16-encoded text * more fixes --- crates/components/src/input.rs | 2 +- crates/hooks/src/editor_history.rs | 69 +++++++++++++++------ crates/hooks/src/rope_editor.rs | 99 +++++++++++++++++++++--------- crates/hooks/src/text_editor.rs | 85 ++++++++++++------------- crates/hooks/src/use_editable.rs | 7 +-- crates/hooks/tests/use_editable.rs | 34 +++++----- examples/cloned_editor.rs | 4 +- examples/documents_editor.rs | 2 +- examples/shader_editor.rs | 2 +- examples/simple_editor.rs | 2 +- 10 files changed, 184 insertions(+), 122 deletions(-) diff --git a/crates/components/src/input.rs b/crates/components/src/input.rs index e5b85843a..2287524a2 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()) diff --git a/crates/hooks/src/editor_history.rs b/crates/hooks/src/editor_history.rs index e10bdd638..3e807d3e4 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)] @@ -49,22 +61,28 @@ impl EditorHistory { pub fn undo(&mut self, rope: &mut Rope) -> Option { if !self.can_undo() { + println!("cant undo"); return None; } 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 } }; @@ -78,23 +96,28 @@ impl EditorHistory { pub fn redo(&mut self, rope: &mut Rope) -> Option { if !self.can_redo() { + println!("cant redo"); return None; } 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 +154,7 @@ mod test { history.push_change(HistoryChange::InsertText { idx: 11, text: "\n!!!!".to_owned(), + len: "\n!!!!".len(), }); assert!(history.can_undo()); @@ -149,16 +173,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 +229,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..ad932c235 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); } @@ -465,23 +459,24 @@ pub trait TextEditor { // Remove selected text let selection = self.get_selection_range(); if let Some((start, end)) = selection { - self.remove(start..end); - self.set_cursor_pos(start); + let cursor_pos = self.cursor_pos(); + let removed_text_len = self.remove(start..end); + self.set_cursor_pos(cursor_pos - removed_text_len); event.insert(TextEvent::TEXT_CHANGED); } 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/use_editable.rs b/crates/hooks/src/use_editable.rs index befdc8226..9827f7c85 100644 --- a/crates/hooks/src/use_editable.rs +++ b/crates/hooks/src/use_editable.rs @@ -323,8 +323,7 @@ pub fn use_editable(initializer: impl Fn() -> EditableConfig, mode: EditableMode // 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); + 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 { @@ -344,10 +343,6 @@ pub fn use_editable(initializer: impl Fn() -> EditableConfig, mode: EditableMode 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); diff --git a/crates/hooks/tests/use_editable.rs b/crates/hooks/tests/use_editable.rs index 507b8e04b..5ffc80256 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); @@ -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); @@ -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/examples/cloned_editor.rs b/examples/cloned_editor.rs index 77674cdb5..e611f8833 100644 --- a/examples/cloned_editor.rs +++ b/examples/cloned_editor.rs @@ -70,7 +70,7 @@ fn Body() -> Element { // Only show the cursor in the active line let character_index = if is_line_selected { - editor.visible_cursor_col().to_string() + editor.cursor_col().to_string() } else { "none".to_string() }; @@ -144,7 +144,7 @@ fn Body() -> Element { // Only show the cursor in the active line let character_index = if is_line_selected { - editor.visible_cursor_col().to_string() + editor.cursor_col().to_string() } else { "none".to_string() }; diff --git a/examples/documents_editor.rs b/examples/documents_editor.rs index 10d4b7f09..3929f5cb3 100644 --- a/examples/documents_editor.rs +++ b/examples/documents_editor.rs @@ -339,7 +339,7 @@ fn DocumentEditor(path: String, mut editable: UseEditable) -> Element { let cursor_reference = editable.cursor_attr(); let highlights = editable.highlights_attr(0); let editor = editable.editor().read(); - let cursor_char = editor.visible_cursor_pos(); + let cursor_char = editor.cursor_pos(); let onmousedown = move |e: MouseEvent| { editable.process_event(&EditableEvent::MouseDown(e.data, 0)); diff --git a/examples/shader_editor.rs b/examples/shader_editor.rs index c9482cb10..80acb30b6 100644 --- a/examples/shader_editor.rs +++ b/examples/shader_editor.rs @@ -105,7 +105,7 @@ fn ShaderEditor(editable: UseEditable) -> Element { // Only show the cursor in the active line let character_index = if is_line_selected { - editor.visible_cursor_col().to_string() + editor.cursor_col().to_string() } else { "none".to_string() }; diff --git a/examples/simple_editor.rs b/examples/simple_editor.rs index b3de972e6..9834b9255 100644 --- a/examples/simple_editor.rs +++ b/examples/simple_editor.rs @@ -26,7 +26,7 @@ fn app() -> Element { let cursor_reference = editable.cursor_attr(); let highlights = editable.highlights_attr(0); let editor = editable.editor().read(); - let cursor_char = editor.visible_cursor_pos(); + let cursor_char = editor.cursor_pos(); let onmousedown = move |e: MouseEvent| { editable.process_event(&EditableEvent::MouseDown(e.data, 0)); From a41c5f592aa4612bf607b0fac441a3305e0dfc3f Mon Sep 17 00:00:00 2001 From: Marc Espin Date: Sat, 28 Sep 2024 10:33:41 +0200 Subject: [PATCH 18/39] feat: `import_image` (#899) --- crates/components/src/image.rs | 57 ++++++++++++++++++++++++++++++++++ crates/components/src/lib.rs | 1 + examples/import_image.rs | 18 +++++++++++ 3 files changed, 76 insertions(+) create mode 100644 crates/components/src/image.rs create mode 100644 examples/import_image.rs 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/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/examples/import_image.rs b/examples/import_image.rs new file mode 100644 index 000000000..7534b1390 --- /dev/null +++ b/examples/import_image.rs @@ -0,0 +1,18 @@ +use freya::prelude::*; + +import_image!(Ferris, "./rust_logo.png", "70%", "50%"); +import_image!(FerrisWithRequiredSize, "./rust_logo.png"); + +fn main() { + launch(app); +} + +fn app() -> Element { + rsx!( + Ferris { } + FerrisWithRequiredSize { + width: "50%", + height: "50%", + } + ) +} From 90647c1eb9035853e68e7461acf0d2c175a80084 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Sat, 28 Sep 2024 10:34:49 +0200 Subject: [PATCH 19/39] chore: Clean up --- crates/hooks/src/editor_history.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/hooks/src/editor_history.rs b/crates/hooks/src/editor_history.rs index 3e807d3e4..2569c9c0b 100644 --- a/crates/hooks/src/editor_history.rs +++ b/crates/hooks/src/editor_history.rs @@ -61,7 +61,6 @@ impl EditorHistory { pub fn undo(&mut self, rope: &mut Rope) -> Option { if !self.can_undo() { - println!("cant undo"); return None; } @@ -96,7 +95,6 @@ impl EditorHistory { pub fn redo(&mut self, rope: &mut Rope) -> Option { if !self.can_redo() { - println!("cant redo"); return None; } From 31e42f5f69e91729d32c61456a98a7b8194371e4 Mon Sep 17 00:00:00 2001 From: Marc Espin Date: Sat, 28 Sep 2024 10:42:43 +0200 Subject: [PATCH 20/39] feat: Support RootPercentage in `calc()` (#907) * feat: Support RootPercentage in `calc()` * update tests --- crates/state/src/values/size.rs | 4 ++++ crates/state/tests/parse_size.rs | 6 ++++-- crates/torin/src/values/size.rs | 24 ++++++++++++++++++------ crates/torin/tests/size.rs | 31 +++++++++++++++++++++++++++++-- 4 files changed, 55 insertions(+), 10 deletions(-) diff --git a/crates/state/src/values/size.rs b/crates/state/src/values/size.rs index c92e7bdcf..ae856e511 100644 --- a/crates/state/src/values/size.rs +++ b/crates/state/src/values/size.rs @@ -66,6 +66,10 @@ pub fn parse_calc(mut value: &str) -> Result, ParseError calcs.push(DynamicCalculation::Percentage( val.replace('%', "").parse().map_err(|_| ParseError)?, )); + } else if val.contains('v') { + calcs.push(DynamicCalculation::RootPercentage( + val.replace('v', "").parse().map_err(|_| ParseError)?, + )); } else if val == "+" { calcs.push(DynamicCalculation::Add); } else if val == "-" { diff --git a/crates/state/tests/parse_size.rs b/crates/state/tests/parse_size.rs index b2a3740ee..4e4a90084 100644 --- a/crates/state/tests/parse_size.rs +++ b/crates/state/tests/parse_size.rs @@ -27,7 +27,7 @@ fn parse_auto_size() { #[test] fn parse_calc_size() { - let size = Size::parse("calc(90% - 5% * 123.6)"); + let size = Size::parse("calc(90% - 5% * 123.6 / 50v)"); assert_eq!( size, Ok(Size::DynamicCalculations(Box::new(vec![ @@ -35,7 +35,9 @@ fn parse_calc_size() { DynamicCalculation::Sub, DynamicCalculation::Percentage(5.0), DynamicCalculation::Mul, - DynamicCalculation::Pixels(123.6) + DynamicCalculation::Pixels(123.6), + DynamicCalculation::Div, + DynamicCalculation::RootPercentage(50.0) ]))) ); } diff --git a/crates/torin/src/values/size.rs b/crates/torin/src/values/size.rs index 3e05b869b..2c1c347b2 100644 --- a/crates/torin/src/values/size.rs +++ b/crates/torin/src/values/size.rs @@ -72,9 +72,9 @@ impl Size { match self { Size::Pixels(px) => Some(px.get() + parent_margin), Size::Percentage(per) => Some(parent_value / 100.0 * per.get()), - Size::DynamicCalculations(calculations) => { - Some(run_calculations(calculations.deref(), parent_value).unwrap_or(0.0)) - } + Size::DynamicCalculations(calculations) => Some( + run_calculations(calculations.deref(), parent_value, root_value).unwrap_or(0.0), + ), Size::Fill => Some(available_parent_value), Size::FillMinimum => { if phase == Phase::Initial { @@ -172,6 +172,7 @@ pub enum DynamicCalculation { Div, Add, Percentage(f32), + RootPercentage(f32), Pixels(f32), } @@ -191,6 +192,7 @@ impl std::fmt::Display for DynamicCalculation { DynamicCalculation::Div => f.write_str("/"), DynamicCalculation::Add => f.write_str("+"), DynamicCalculation::Percentage(p) => f.write_fmt(format_args!("{p}%")), + DynamicCalculation::RootPercentage(p) => f.write_fmt(format_args!("{p}v")), DynamicCalculation::Pixels(s) => f.write_fmt(format_args!("{s}")), } } @@ -200,14 +202,16 @@ impl std::fmt::Display for DynamicCalculation { struct DynamicCalculationEvaluator<'a> { calcs: Iter<'a, DynamicCalculation>, parent_value: f32, + root_value: f32, current: Option<&'a DynamicCalculation>, } impl<'a> DynamicCalculationEvaluator<'a> { - pub fn new(calcs: Iter<'a, DynamicCalculation>, parent_value: f32) -> Self { + pub fn new(calcs: Iter<'a, DynamicCalculation>, parent_value: f32, root_value: f32) -> Self { Self { calcs, parent_value, + root_value, current: None, } } @@ -275,6 +279,10 @@ impl<'a> DynamicCalculationEvaluator<'a> { self.current = self.calcs.next(); Some((self.parent_value / 100.0 * value).round()) } + DynamicCalculation::RootPercentage(value) => { + self.current = self.calcs.next(); + Some((self.root_value / 100.0 * value).round()) + } DynamicCalculation::Pixels(value) => { self.current = self.calcs.next(); Some(*value) @@ -295,6 +303,10 @@ impl<'a> DynamicCalculationEvaluator<'a> { /// Calculate dynamic expression with operator precedence. /// This value could be for example the width of a node's parent area. -pub fn run_calculations(calcs: &[DynamicCalculation], value: f32) -> Option { - DynamicCalculationEvaluator::new(calcs.iter(), value).evaluate() +pub fn run_calculations( + calcs: &[DynamicCalculation], + parent_value: f32, + root_value: f32, +) -> Option { + DynamicCalculationEvaluator::new(calcs.iter(), parent_value, root_value).evaluate() } diff --git a/crates/torin/tests/size.rs b/crates/torin/tests/size.rs index 13d6ea157..f7e89a96a 100644 --- a/crates/torin/tests/size.rs +++ b/crates/torin/tests/size.rs @@ -783,12 +783,20 @@ pub fn test_calc() { const PARENT_VALUE: f32 = 500.0; assert_eq!( - run_calculations(&vec![DynamicCalculation::Pixels(10.0)], PARENT_VALUE), + run_calculations( + &vec![DynamicCalculation::Pixels(10.0)], + PARENT_VALUE, + PARENT_VALUE + ), Some(10.0) ); assert_eq!( - run_calculations(&vec![DynamicCalculation::Percentage(87.5)], PARENT_VALUE), + run_calculations( + &vec![DynamicCalculation::Percentage(87.5)], + PARENT_VALUE, + PARENT_VALUE + ), Some((87.5 / 100.0 * PARENT_VALUE).round()) ); @@ -801,6 +809,7 @@ pub fn test_calc() { DynamicCalculation::Mul, DynamicCalculation::Percentage(50.0), ], + PARENT_VALUE, PARENT_VALUE ), Some(10.0 + 20.0 * (50.0 / 100.0 * PARENT_VALUE).round()) @@ -821,6 +830,7 @@ pub fn test_calc() { DynamicCalculation::Mul, DynamicCalculation::Pixels(2.0), ], + PARENT_VALUE, PARENT_VALUE ), Some(10.0 + (10.0 / 100.0 * PARENT_VALUE).round() + 30.0 * 10.0 + 75.0 * 2.0) @@ -832,6 +842,7 @@ pub fn test_calc() { DynamicCalculation::Pixels(10.0), DynamicCalculation::Pixels(20.0), ], + PARENT_VALUE, PARENT_VALUE ), None @@ -840,6 +851,7 @@ pub fn test_calc() { assert_eq!( run_calculations( &vec![DynamicCalculation::Pixels(10.0), DynamicCalculation::Add], + PARENT_VALUE, PARENT_VALUE ), None @@ -848,6 +860,7 @@ pub fn test_calc() { assert_eq!( run_calculations( &vec![DynamicCalculation::Add, DynamicCalculation::Pixels(10.0)], + PARENT_VALUE, PARENT_VALUE ), None @@ -861,8 +874,22 @@ pub fn test_calc() { DynamicCalculation::Add, DynamicCalculation::Pixels(10.0) ], + PARENT_VALUE, PARENT_VALUE ), None ); + + assert_eq!( + run_calculations( + &vec![ + DynamicCalculation::Percentage(50.0), + DynamicCalculation::Sub, + DynamicCalculation::RootPercentage(20.0) + ], + PARENT_VALUE, + PARENT_VALUE + ), + Some((PARENT_VALUE * 0.5) - (PARENT_VALUE * 0.20)) + ); } From a52ea1b9e1cc6e7bd37ed72625fca4c1973ed4a1 Mon Sep 17 00:00:00 2001 From: Tropical <42101043+Tropix126@users.noreply.github.com> Date: Sat, 28 Sep 2024 04:44:31 -0400 Subject: [PATCH 21/39] fix: linear gradient angles (#921) --- crates/state/src/values/gradient.rs | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/crates/state/src/values/gradient.rs b/crates/state/src/values/gradient.rs index ec7051ede..5295a0e38 100644 --- a/crates/state/src/values/gradient.rs +++ b/crates/state/src/values/gradient.rs @@ -1,4 +1,7 @@ -use std::fmt; +use std::{ + f32::consts::FRAC_PI_2, + fmt, +}; use freya_engine::prelude::*; use torin::{ @@ -60,20 +63,26 @@ 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 center = bounds.center(); - - let matrix = Matrix::rotate_deg_pivot(self.angle, (center.x, center.y)); + let (dy, dx) = (self.angle.to_radians() + FRAC_PI_2).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()); Shader::linear_gradient( ( - (bounds.min_x(), bounds.min_y()), - (bounds.max_x(), bounds.max_y()), + Point::new(bounds.width(), bounds.height()) - endpoint + origin, + endpoint + origin, ), GradientShaderColors::Colors(&colors[..]), Some(&offsets[..]), TileMode::Clamp, None, - Some(&matrix), + None, ) } } From c0b5ae5e588f5ebffce9a88e219fe5a64fbce87d Mon Sep 17 00:00:00 2001 From: Marc Espin Date: Sat, 28 Sep 2024 10:47:21 +0200 Subject: [PATCH 22/39] optimization: Avoid marking as dirty fixed-size nodes with non-start alignments or layout references if they haven't actually been modified (#919) * optimization: Avoid marking as dirty fixed-size nodes with non-center alignments * clean up * move contains_text --- crates/torin/src/measure.rs | 2 +- crates/torin/src/node.rs | 12 +++++++----- crates/torin/src/torin.rs | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/crates/torin/src/measure.rs b/crates/torin/src/measure.rs index 88b8771f7..476f05b1a 100644 --- a/crates/torin/src/measure.rs +++ b/crates/torin/src/measure.rs @@ -244,7 +244,7 @@ where } else { let layout_node = self.layout.get(node_id).unwrap().clone(); - let mut inner_sizes = layout_node.inner_sizes; + let mut inner_sizes = Size2D::default(); let mut available_area = layout_node.inner_area; let mut area = layout_node.area; let mut inner_area = layout_node.inner_area; diff --git a/crates/torin/src/node.rs b/crates/torin/src/node.rs index 8dbb450c7..bce0274f7 100644 --- a/crates/torin/src/node.rs +++ b/crates/torin/src/node.rs @@ -239,11 +239,13 @@ impl Node { /// Has properties that depend on the inner Nodes? pub fn does_depend_on_inner(&self) -> bool { - self.width.inner_sized() - || self.height.inner_sized() - || self.has_layout_references - || self.cross_alignment.is_not_start() + self.width.inner_sized() || self.height.inner_sized() || self.contains_text + } + + /// Has properties that make its children dependant on it? + pub fn do_inner_depend_on_parent(&self) -> bool { + self.cross_alignment.is_not_start() || self.main_alignment.is_not_start() - || self.contains_text + || self.has_layout_references } } diff --git a/crates/torin/src/torin.rs b/crates/torin/src/torin.rs index 4c396d79d..5171bd563 100644 --- a/crates/torin/src/torin.rs +++ b/crates/torin/src/torin.rs @@ -199,7 +199,7 @@ impl Torin { } // Try using the node's parent as root candidate if it has multiple children - if multiple_children { + if multiple_children || parent.do_inner_depend_on_parent() { self.root_node_candidate .propose_new_candidate(&parent_id, dom_adapter); } From 0068281a2a70bea3b3ee6d990ec99268c6d2a285 Mon Sep 17 00:00:00 2001 From: Marc Espin Date: Sat, 28 Sep 2024 10:47:37 +0200 Subject: [PATCH 23/39] feat: Deterministic order of rendering (#923) --- crates/core/src/render/pipeline.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); From d12309326ed87797737cc47341d6d255cb0021c3 Mon Sep 17 00:00:00 2001 From: Marc Espin Date: Sat, 28 Sep 2024 10:47:55 +0200 Subject: [PATCH 24/39] fix: Clamp progress bar progress (#927) --- crates/components/src/progress_bar.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/components/src/progress_bar.rs b/crates/components/src/progress_bar.rs index f05cca583..47cf040f6 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}", From bdedb2aa25b597af45ec08500a55f8ec37219bbb Mon Sep 17 00:00:00 2001 From: Marc Espin Date: Sat, 28 Sep 2024 10:48:09 +0200 Subject: [PATCH 25/39] feat: Vertical `direction` for `Slider` (#910) * feat: Vertical `direction` for `Slider` * improvements * fix examples * fix doc comment * improvements --- crates/components/src/slider.rs | 138 +++++++++++++++++++++++--------- examples/floating_editors.rs | 4 +- examples/opacity.rs | 2 +- examples/opengl_rtt.rs | 6 +- examples/progress_bar.rs | 2 +- examples/switch_theme.rs | 8 +- examples/text.rs | 2 +- 7 files changed, 115 insertions(+), 47 deletions(-) diff --git a/crates/components/src/slider.rs b/crates/components/src/slider.rs index 1e023a7ca..bf3b6c84a 100644 --- a/crates/components/src/slider.rs +++ b/crates/components/src/slider.rs @@ -22,11 +22,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 +75,7 @@ pub enum SliderStatus { /// "Value: {percentage}" /// } /// Slider { -/// width: "50%", +/// size: "50%", /// value: *percentage.read(), /// onmoved: move |p| { /// percentage.set(p); @@ -88,7 +90,8 @@ pub fn Slider( value, onmoved, theme, - width, + size, + direction, }: SliderProps, ) -> Element { let theme = use_applied_theme!(&theme, slider); @@ -96,8 +99,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(); @@ -125,8 +129,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 +150,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 +176,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) } 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, @@ -187,34 +266,17 @@ pub fn Slider( 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/examples/floating_editors.rs b/examples/floating_editors.rs index a1a930c1f..f10d3d746 100644 --- a/examples/floating_editors.rs +++ b/examples/floating_editors.rs @@ -207,7 +207,7 @@ fn Editor() -> Element { rect { cross_align: "center", Slider { - width: "130", + size: "130", value: font_size_percentage(), onmoved: move |p| { font_size_percentage.set(p); @@ -220,7 +220,7 @@ fn Editor() -> Element { rect { cross_align: "center", Slider { - width: "130", + size: "130", value: line_height_percentage(), onmoved: move |p| { line_height_percentage.set(p); diff --git a/examples/opacity.rs b/examples/opacity.rs index 188bd8957..3b89f1bc8 100644 --- a/examples/opacity.rs +++ b/examples/opacity.rs @@ -35,7 +35,7 @@ fn app() -> Element { } } Slider { - width: "100", + size: "100", value: *opacity.read(), onmoved: move |p| { opacity.set(p); diff --git a/examples/opengl_rtt.rs b/examples/opengl_rtt.rs index 9bb8b549b..397ced06e 100644 --- a/examples/opengl_rtt.rs +++ b/examples/opengl_rtt.rs @@ -565,17 +565,17 @@ fn app() -> Element { width: "100%", height: "100%", Slider { - width: "300", + size: "300", value: *r.read(), onmoved: move |value: f64| { r.set(value) } } Slider { - width: "300", + size: "300", value: *g.read(), onmoved: move |value: f64| { g.set(value) } } Slider { - width: "300", + size: "300", value: *b.read(), onmoved: move |value: f64| { b.set(value) } } diff --git a/examples/progress_bar.rs b/examples/progress_bar.rs index ad216f2c7..39638ca2e 100644 --- a/examples/progress_bar.rs +++ b/examples/progress_bar.rs @@ -83,7 +83,7 @@ fn app() -> Element { progress: progress * 0.20 } Slider { - width: "300", + size: "300", value: progress as f64, onmoved: onmoved } diff --git a/examples/switch_theme.rs b/examples/switch_theme.rs index e3c0e6b3c..705b8df7b 100644 --- a/examples/switch_theme.rs +++ b/examples/switch_theme.rs @@ -76,10 +76,16 @@ fn app() -> Element { } } Slider { - width: "fill", + size: "fill", value: value(), onmoved: move |e| value.set(e), } + Slider { + size: "200", + value: value(), + direction: "vertical", + onmoved: move |e| value.set(e), + } ProgressBar { show_progress: true, progress: value() as f32 diff --git a/examples/text.rs b/examples/text.rs index 2af9c1f69..b1fc76db8 100644 --- a/examples/text.rs +++ b/examples/text.rs @@ -30,7 +30,7 @@ fn app() -> Element { } } Slider { - width: "200", + size: "200", value: *percentage.read(), onmoved: move |p| { percentage.set(p); From ffca92a3af044f06ff18fc33135c9fb4eb1decba Mon Sep 17 00:00:00 2001 From: Marc Espin Date: Sat, 28 Sep 2024 10:48:38 +0200 Subject: [PATCH 26/39] feat: Move sliders with keyboard (#917) * feat: Vertical `direction` for `Slider` * improvements * fix examples * fix doc comment * faet: Move sliders with keyboard * improvements * swap direction --- crates/components/src/slider.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/crates/components/src/slider.rs b/crates/components/src/slider.rs index bf3b6c84a..c5c391f94 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, }, @@ -111,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; @@ -260,6 +285,7 @@ pub fn Slider( onglobalmousemove: onmousemove, onmouseleave, onwheel: onwheel, + onkeydown, main_align: "center", cross_align: "center", border: "{border}", From f916a9a10e68378fe2fecadd6ade6bc5e2a657b5 Mon Sep 17 00:00:00 2001 From: Marc Espin Date: Sat, 28 Sep 2024 10:50:00 +0200 Subject: [PATCH 27/39] feat: Ajust the custom layers of some built-in-components (#928) --- crates/components/src/dropdown.rs | 2 +- crates/components/src/popup.rs | 2 +- crates/components/src/snackbar.rs | 1 + crates/components/src/tooltip.rs | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/components/src/dropdown.rs b/crates/components/src/dropdown.rs index 86eadbf78..dd2440f08 100644 --- a/crates/components/src/dropdown.rs +++ b/crates/components/src/dropdown.rs @@ -299,7 +299,7 @@ where rect { onglobalclick, onglobalkeydown, - layer: "-99", + layer: "-1000", margin: "{margin}", border: "1 solid {border_fill}", overflow: "clip", 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/snackbar.rs b/crates/components/src/snackbar.rs index dc6df4fa5..826b1e201 100644 --- a/crates/components/src/snackbar.rs +++ b/crates/components/src/snackbar.rs @@ -100,6 +100,7 @@ pub fn SnackBarBox(children: Element, theme: Option) -> Eleme padding: "10", color: "{color}", direction: "horizontal", + layer: "-1000", {children} } ) diff --git a/crates/components/src/tooltip.rs b/crates/components/src/tooltip.rs index 09f0ee603..45f9d6909 100644 --- a/crates/components/src/tooltip.rs +++ b/crates/components/src/tooltip.rs @@ -88,7 +88,7 @@ pub fn TooltipContainer( rect { height: "0", width: "0", - layer: "-999", + layer: "-1500", match position { TooltipPosition::Below => rsx!( rect { From e924f27eae7d0b1d549dcddce1b2222a47d755ca Mon Sep 17 00:00:00 2001 From: Marc Espin Date: Sat, 28 Sep 2024 10:50:12 +0200 Subject: [PATCH 28/39] refactor: Remove `accesskit` window rendering workaround (#930) --- crates/renderer/src/window_state.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/renderer/src/window_state.rs b/crates/renderer/src/window_state.rs index 1e54244d0..8e7a0c081 100644 --- a/crates/renderer/src/window_state.rs +++ b/crates/renderer/src/window_state.rs @@ -75,7 +75,6 @@ impl<'a, State: Clone + 'a> WindowState<'a, State> { }; let mut window_attributes = Window::default_attributes() - .with_visible(false) .with_title(config.window_config.title) .with_decorations(config.window_config.decorations) .with_transparent(config.window_config.transparent) @@ -104,9 +103,6 @@ impl<'a, State: Clone + 'a> WindowState<'a, State> { // 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(); From 0507fcaa47cb583dbcd197206febbf52f68de986 Mon Sep 17 00:00:00 2001 From: Marc Espin Date: Sat, 28 Sep 2024 10:52:36 +0200 Subject: [PATCH 29/39] feat: Add `UseEditable::new_in_hook` for manual creation of editable content (#933) * feat: Add `UseEditable::new_in_hook` for manual creation of editable content * doc comment --- Cargo.toml | 1 + crates/hooks/src/use_editable.rs | 168 ++++++++++++++++--------------- examples/text_editors.rs | 122 ++++++++++++++++++++++ 3 files changed, 212 insertions(+), 79 deletions(-) create mode 100644 examples/text_editors.rs diff --git a/Cargo.toml b/Cargo.toml index 798941a6a..91ce951f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -94,6 +94,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/crates/hooks/src/use_editable.rs b/crates/hooks/src/use_editable.rs index 9827f7c85..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,85 +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(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, - } - }) + use_hook(|| UseEditable::new_in_hook(clipboard, platform, initializer(), mode)) } diff --git a/examples/text_editors.rs b/examples/text_editors.rs new file mode 100644 index 000000000..b79724cbb --- /dev/null +++ b/examples/text_editors.rs @@ -0,0 +1,122 @@ +#![cfg_attr( + all(not(debug_assertions), target_os = "windows"), + windows_subsystem = "windows" +)] + +use dioxus_sdk::clipboard::use_clipboard; +use freya::prelude::*; + +fn main() { + launch_with_props(app, "Simple editor", (900.0, 650.0)); +} + +fn app() -> Element { + let platform = use_platform(); + let clipboard = use_clipboard(); + + let text_editors = use_hook(|| { + vec![ + UseEditable::new_in_hook( + clipboard, + platform, + EditableConfig::new("Editor 1 -------------\n".repeat(4).trim().to_string()) + .with_allow_tabs(true), + EditableMode::MultipleLinesSingleEditor, + ), + UseEditable::new_in_hook( + clipboard, + platform, + EditableConfig::new("Editor 2 -------------\n".repeat(4).trim().to_string()) + .with_allow_tabs(true), + EditableMode::MultipleLinesSingleEditor, + ), + UseEditable::new_in_hook( + clipboard, + platform, + EditableConfig::new("Editor 3 -------------\n".repeat(4).trim().to_string()) + .with_allow_tabs(true), + EditableMode::MultipleLinesSingleEditor, + ), + UseEditable::new_in_hook( + clipboard, + platform, + EditableConfig::new("Editor 4 -------------\n".repeat(4).trim().to_string()) + .with_allow_tabs(true), + EditableMode::MultipleLinesSingleEditor, + ), + UseEditable::new_in_hook( + clipboard, + platform, + EditableConfig::new("Editor 5 -------------\n".repeat(4).trim().to_string()) + .with_allow_tabs(true), + EditableMode::MultipleLinesSingleEditor, + ), + ] + }); + + rsx!( + rect { + spacing: "10", + for (i, editable) in text_editors.into_iter().enumerate() { + TextEditor { + key: "{i}", + editable + } + } + } + ) +} + +#[component] +fn TextEditor(mut editable: UseEditable) -> Element { + let mut focus = use_focus(); + let cursor_reference = editable.cursor_attr(); + let highlights = editable.highlights_attr(0); + let editor = editable.editor().read(); + let cursor_char = editor.visible_cursor_pos(); + + let onmousedown = move |e: MouseEvent| { + focus.focus(); + editable.process_event(&EditableEvent::MouseDown(e.data, 0)); + }; + + let onmousemove = move |e: MouseEvent| { + editable.process_event(&EditableEvent::MouseMove(e.data, 0)); + }; + + let onglobalclick = move |_: MouseEvent| { + editable.process_event(&EditableEvent::Click); + }; + + let onkeydown = move |e: KeyboardEvent| { + editable.process_event(&EditableEvent::KeyDown(e.data)); + }; + + let onglobalkeyup = move |e: KeyboardEvent| { + editable.process_event(&EditableEvent::KeyUp(e.data)); + }; + + rsx!( + rect { + background: "rgb(235, 235, 235)", + cursor_reference, + paragraph { + a11y_id: focus.attribute(), + width: "100%", + cursor_id: "0", + cursor_index: "{cursor_char}", + cursor_mode: "editable", + cursor_color: "black", + highlights, + onglobalclick, + onmousemove, + onmousedown, + onkeydown, + onglobalkeyup, + text { + "{editable.editor()}" + } + } + } + ) +} From 315a056a24b05a476dabd4bcd905fbef6295a8d8 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Sat, 28 Sep 2024 11:08:36 +0200 Subject: [PATCH 30/39] chore: Update text_editors.rs example --- examples/text_editors.rs | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/examples/text_editors.rs b/examples/text_editors.rs index b79724cbb..8ca01547a 100644 --- a/examples/text_editors.rs +++ b/examples/text_editors.rs @@ -19,36 +19,31 @@ fn app() -> Element { UseEditable::new_in_hook( clipboard, platform, - EditableConfig::new("Editor 1 -------------\n".repeat(4).trim().to_string()) - .with_allow_tabs(true), + EditableConfig::new("Editor 1 -------------\n".repeat(4).trim().to_string()), EditableMode::MultipleLinesSingleEditor, ), UseEditable::new_in_hook( clipboard, platform, - EditableConfig::new("Editor 2 -------------\n".repeat(4).trim().to_string()) - .with_allow_tabs(true), + EditableConfig::new("Editor 2 -------------\n".repeat(4).trim().to_string()), EditableMode::MultipleLinesSingleEditor, ), UseEditable::new_in_hook( clipboard, platform, - EditableConfig::new("Editor 3 -------------\n".repeat(4).trim().to_string()) - .with_allow_tabs(true), + EditableConfig::new("Editor 3 -------------\n".repeat(4).trim().to_string()), EditableMode::MultipleLinesSingleEditor, ), UseEditable::new_in_hook( clipboard, platform, - EditableConfig::new("Editor 4 -------------\n".repeat(4).trim().to_string()) - .with_allow_tabs(true), + EditableConfig::new("Editor 4 -------------\n".repeat(4).trim().to_string()), EditableMode::MultipleLinesSingleEditor, ), UseEditable::new_in_hook( clipboard, platform, - EditableConfig::new("Editor 5 -------------\n".repeat(4).trim().to_string()) - .with_allow_tabs(true), + EditableConfig::new("Editor 5 -------------\n".repeat(4).trim().to_string()), EditableMode::MultipleLinesSingleEditor, ), ] @@ -73,7 +68,7 @@ fn TextEditor(mut editable: UseEditable) -> Element { let cursor_reference = editable.cursor_attr(); let highlights = editable.highlights_attr(0); let editor = editable.editor().read(); - let cursor_char = editor.visible_cursor_pos(); + let cursor_char = editor.cursor_pos(); let onmousedown = move |e: MouseEvent| { focus.focus(); From b4f05d4644f51a264d45840b08ce997ccb101fe9 Mon Sep 17 00:00:00 2001 From: Marc Espin Date: Sat, 28 Sep 2024 11:08:57 +0200 Subject: [PATCH 31/39] feat: Custom scale factor shortcuts (#931) * feat: Custom scale factor shortcuts * add feature to disable the shortcuts --- Cargo.toml | 1 + crates/freya/Cargo.toml | 1 + crates/renderer/Cargo.toml | 1 + crates/renderer/src/app.rs | 3 ++- crates/renderer/src/renderer.rs | 41 ++++++++++++++++++++++++++++++--- 5 files changed, 43 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 91ce951f0..c05a7f57c 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" } 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/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/renderer.rs b/crates/renderer/src/renderer.rs index 6eda7db36..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, } } @@ -193,8 +197,7 @@ impl<'a, State: Clone> ApplicationHandler for DesktopRenderer<'a, } EventMessage::InvalidateArea(mut area) => { let fdom = app.sdom.get(); - let sf = window.scale_factor() as f32; - area.size *= sf; + area.size *= scale_factor as f32; let mut compositor_dirty_area = fdom.compositor_dirty_area(); compositor_dirty_area.unite_or_insert(&area) } @@ -296,6 +299,7 @@ impl<'a, State: Clone> ApplicationHandler for DesktopRenderer<'a, surface, dirty_surface, window, + scale_factor, ); app.event_loop_tick(); @@ -359,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, From 198b95a772a70a3a46118272d6a3ed390b4b11d9 Mon Sep 17 00:00:00 2001 From: Marc Espin Date: Sat, 28 Sep 2024 15:12:59 +0200 Subject: [PATCH 32/39] fix: Layout references not triggering (#934) * fix: Layout references not triggering * lint --- crates/core/src/render/skia_measurer.rs | 7 ++--- crates/torin/src/custom_measurer.rs | 4 +-- crates/torin/src/dom_adapter.rs | 9 +----- crates/torin/src/measure.rs | 39 ++++++++++++++----------- crates/torin/src/torin.rs | 17 +++-------- 5 files changed, 32 insertions(+), 44 deletions(-) diff --git a/crates/core/src/render/skia_measurer.rs b/crates/core/src/render/skia_measurer.rs index 3ba11be03..a6abf8e34 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, }; @@ -110,14 +109,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(); diff --git a/crates/torin/src/custom_measurer.rs b/crates/torin/src/custom_measurer.rs index 432de764d..3dcd1ecf5 100644 --- a/crates/torin/src/custom_measurer.rs +++ b/crates/torin/src/custom_measurer.rs @@ -6,7 +6,7 @@ use crate::{ dom_adapter::NodeKey, geometry::Size2D, node::Node, - prelude::LayoutNode, + prelude::Area, }; pub trait LayoutMeasurer { @@ -19,5 +19,5 @@ pub trait LayoutMeasurer { fn should_measure_inner_children(&mut self, node_id: Key) -> bool; - fn notify_layout_references(&self, _node_id: Key, _layout_node: &LayoutNode) {} + fn notify_layout_references(&self, _node_id: Key, _area: Area, _inner_sizes: Size2D) {} } diff --git a/crates/torin/src/dom_adapter.rs b/crates/torin/src/dom_adapter.rs index bd76a3993..4cdff767b 100644 --- a/crates/torin/src/dom_adapter.rs +++ b/crates/torin/src/dom_adapter.rs @@ -4,10 +4,7 @@ pub use euclid::Rect; use freya_native_core::SendAnyMap; use crate::{ - geometry::{ - Area, - Size2D, - }, + geometry::Area, node::Node, prelude::{ AreaModel, @@ -24,9 +21,6 @@ pub struct LayoutNode { /// Area inside this Node pub inner_area: Area, - /// Ocuppied sizes from the inner children in this Node - pub inner_sizes: Size2D, - /// Outer margin pub margin: Gaps, @@ -38,7 +32,6 @@ impl PartialEq for LayoutNode { fn eq(&self, other: &Self) -> bool { self.area == other.area && self.inner_area == other.inner_area - && self.inner_sizes == other.inner_sizes && self.margin == other.margin } } diff --git a/crates/torin/src/measure.rs b/crates/torin/src/measure.rs index 476f05b1a..4ba77a479 100644 --- a/crates/torin/src/measure.rs +++ b/crates/torin/src/measure.rs @@ -231,16 +231,21 @@ where inner_sizes.width += node.padding.horizontal(); inner_sizes.height += node.padding.vertical(); - ( - must_cache_children, - LayoutNode { - area, - margin: node.margin, - inner_area, - inner_sizes, - data: node_data, - }, - ) + let layout_node = LayoutNode { + area, + margin: node.margin, + inner_area, + data: node_data, + }; + + // In case of any layout listener, notify it with the new areas. + if node.has_layout_references { + if let Some(measurer) = self.measurer { + measurer.notify_layout_references(node_id, layout_node.area, inner_sizes); + } + } + + (must_cache_children, layout_node) } else { let layout_node = self.layout.get(node_id).unwrap().clone(); @@ -268,6 +273,13 @@ where &mut inner_area, false, ); + + // In case of any layout listener, notify it with the new areas. + if node.has_layout_references { + if let Some(measurer) = self.measurer { + measurer.notify_layout_references(node_id, layout_node.area, inner_sizes); + } + } } (false, layout_node) @@ -490,13 +502,6 @@ where // Cache the child layout if it was mutated and children must be cached if child_revalidated && must_cache_children { - // In case of any layout listener, notify it with the new areas. - if child_data.has_layout_references { - if let Some(measurer) = self.measurer { - measurer.notify_layout_references(child_id, &child_areas); - } - } - // Finally cache this node areas into Torin self.layout.cache_node(child_id, child_areas); } diff --git a/crates/torin/src/torin.rs b/crates/torin/src/torin.rs index 5171bd563..abe4511bb 100644 --- a/crates/torin/src/torin.rs +++ b/crates/torin/src/torin.rs @@ -16,10 +16,7 @@ use crate::{ LayoutNode, NodeKey, }, - geometry::{ - Area, - Size2D, - }, + geometry::Area, measure::{ MeasureContext, Phase, @@ -250,7 +247,6 @@ impl Torin { .unwrap_or(LayoutNode { area: root_area, inner_area: root_area, - inner_sizes: Size2D::default(), margin: Gaps::default(), data: None, }); @@ -292,16 +288,11 @@ impl Torin { Phase::Final, ); - // Adjust the size of the area if needed - root_layout_node.area.adjust_size(&root); - // Cache the root Node results if it was modified if root_revalidated { - if let Some(measurer) = measurer { - if root.has_layout_references { - measurer.notify_layout_references(root_id, &root_layout_node); - } - } + // Adjust the size of the area if needed + root_layout_node.area.adjust_size(&root); + self.cache_node(root_id, root_layout_node); } From 2531a94f9fdab8af8ed6500e90d3e704c9adcf82 Mon Sep 17 00:00:00 2001 From: Marc Espin Date: Sat, 28 Sep 2024 15:18:25 +0200 Subject: [PATCH 33/39] fix: Use the real text height for layout (#932) * fix: Use the real text height for layout * clean up * chore: Clean up * fixes * fix tests --- crates/components/src/button.rs | 1 - crates/components/src/tooltip.rs | 11 +++--- crates/core/src/elements/paragraph.rs | 2 +- crates/core/src/node.rs | 3 +- crates/core/src/render/skia_measurer.rs | 52 ++++++++----------------- crates/devtools/src/tabs/style.rs | 9 +++++ crates/hooks/tests/use_editable.rs | 4 +- crates/state/src/font_style.rs | 21 +++++----- examples/input.rs | 10 +++-- 9 files changed, 51 insertions(+), 62 deletions(-) diff --git a/crates/components/src/button.rs b/crates/components/src/button.rs index c9a9149ce..130e9f566 100644 --- a/crates/components/src/button.rs +++ b/crates/components/src/button.rs @@ -190,7 +190,6 @@ pub fn Button( text_align: "center", main_align: "center", cross_align: "center", - line_height: "1.1", {&children} } ) diff --git a/crates/components/src/tooltip.rs b/crates/components/src/tooltip.rs index 45f9d6909..d43c83553 100644 --- a/crates/components/src/tooltip.rs +++ b/crates/components/src/tooltip.rs @@ -84,11 +84,11 @@ pub fn TooltipContainer( onmouseenter, onmouseleave, {children}, - if *is_hovering.read() { - rect { - height: "0", - width: "0", - layer: "-1500", + rect { + height: "0", + width: "0", + layer: "-1500", + if *is_hovering.read() { match position { TooltipPosition::Below => rsx!( rect { @@ -107,7 +107,6 @@ pub fn TooltipContainer( } ), } - } } } 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/node.rs b/crates/core/src/node.rs index 11509017d..0cdae8fac 100644 --- a/crates/core/src/node.rs +++ b/crates/core/src/node.rs @@ -125,7 +125,7 @@ impl NodeState { ), ( "line_height", - AttributeType::Measure(self.font_style.line_height), + AttributeType::OptionalMeasure(self.font_style.line_height), ), ( "text_align", @@ -160,6 +160,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/skia_measurer.rs b/crates/core/src/render/skia_measurer.rs index a6abf8e34..86ec72159 100644 --- a/crates/core/src/render/skia_measurer.rs +++ b/crates/core/src/render/skia_measurer.rs @@ -68,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, @@ -90,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, @@ -130,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(); @@ -142,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); @@ -160,16 +161,7 @@ pub fn create_label( 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 } /// Align the Y axis of the highlights and cursor of a paragraph @@ -227,7 +219,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(); @@ -241,13 +233,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() @@ -256,7 +245,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 { @@ -277,14 +266,5 @@ pub fn create_paragraph( 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 } 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/hooks/tests/use_editable.rs b/crates/hooks/tests/use_editable.rs index 5ffc80256..3e699bcb3 100644 --- a/crates/hooks/tests/use_editable.rs +++ b/crates/hooks/tests/use_editable.rs @@ -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; @@ -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; 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/examples/input.rs b/examples/input.rs index d1462554a..79bfb88b5 100644 --- a/examples/input.rs +++ b/examples/input.rs @@ -18,6 +18,7 @@ fn app() -> Element { padding: "7", width: "100%", height: "100%", + font_size: "10", label { color: "black", "Your name:" @@ -38,9 +39,12 @@ fn app() -> Element { values.write().1 = txt; } }, - label { - color: "black", - "You are {values.read().0} and you are {values.read().1} years old." + rect { + background: "red", + label { + color: "black", + "You are {values.read().0} and you are {values.read().1} years old." + } } } ) From 5abfc321d3bade9af928fa24a5c8e7ea96f5ea42 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Sat, 28 Sep 2024 15:20:16 +0200 Subject: [PATCH 34/39] chore: Fix clippy warnings in use_init_native_platforms tests --- crates/hooks/src/use_init_native_platform.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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(), From f9b5e7a40c48ee143284ba8e971b2a65ed26c898 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Sat, 28 Sep 2024 15:31:15 +0200 Subject: [PATCH 35/39] chore: Fix dropdown test --- crates/components/src/dropdown.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/components/src/dropdown.rs b/crates/components/src/dropdown.rs index dd2440f08..3b3795292 100644 --- a/crates/components/src/dropdown.rs +++ b/crates/components/src/dropdown.rs @@ -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; From 5a055ccf06f20d45fa08472dd47a04b7ffd144a3 Mon Sep 17 00:00:00 2001 From: Marc Espin Date: Sat, 28 Sep 2024 15:42:34 +0200 Subject: [PATCH 36/39] feat: `LaunchConfig::with_visible` (#935) * feat: `LaunchConfig::with_visible` * fix: Typo --- crates/renderer/src/config.rs | 9 +++++++++ crates/renderer/src/window_state.rs | 5 +++++ 2 files changed, 14 insertions(+) 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/window_state.rs b/crates/renderer/src/window_state.rs index 8e7a0c081..57a136264 100644 --- a/crates/renderer/src/window_state.rs +++ b/crates/renderer/src/window_state.rs @@ -75,6 +75,7 @@ impl<'a, State: Clone + 'a> WindowState<'a, State> { }; let mut window_attributes = Window::default_attributes() + .with_visible(false) .with_title(config.window_config.title) .with_decorations(config.window_config.decorations) .with_transparent(config.window_config.transparent) @@ -100,6 +101,10 @@ 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); From 05f07f83ed991dc93ccb3aae158f23a27d2eb171 Mon Sep 17 00:00:00 2001 From: Marc Espin Date: Sat, 28 Sep 2024 15:46:37 +0200 Subject: [PATCH 37/39] feat: Unified colors theming (#914) * feat: Unified colors theming * some improvements * improvements * clean up * fix example * chore: Fix example --- crates/components/src/input.rs | 1 + crates/hooks/src/theming/base.rs | 229 +++++++++++++++++++++++++++++ crates/hooks/src/theming/dark.rs | 212 -------------------------- crates/hooks/src/theming/light.rs | 212 -------------------------- crates/hooks/src/theming/mod.rs | 108 ++++++++------ crates/hooks/src/theming/themes.rs | 71 +++++++++ crates/hooks/src/use_theme.rs | 9 +- examples/switch_theme.rs | 138 +++++++++-------- examples/themer.rs | 54 +++++++ 9 files changed, 500 insertions(+), 534 deletions(-) create mode 100644 crates/hooks/src/theming/base.rs delete mode 100644 crates/hooks/src/theming/dark.rs delete mode 100644 crates/hooks/src/theming/light.rs create mode 100644 crates/hooks/src/theming/themes.rs create mode 100644 examples/themer.rs diff --git a/crates/components/src/input.rs b/crates/components/src/input.rs index 2287524a2..355f89ce6 100644 --- a/crates/components/src/input.rs +++ b/crates/components/src/input.rs @@ -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/hooks/src/theming/base.rs b/crates/hooks/src/theming/base.rs new file mode 100644 index 000000000..f017c4d43 --- /dev/null +++ b/crates/hooks/src/theming/base.rs @@ -0,0 +1,229 @@ +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!(""), + }, + 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!("rgb(100, 100, 100)"), + }, + 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!("rgb(40, 40, 40)"), + }, + 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!("black"), + 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!("rgb(43,106,208)"), + }, + 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!("black"), + }, + background: cow_borrowed!("key(background)"), + arrow_fill: cow_borrowed!("rgb(40, 40, 40)"), + 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)"), + }, + 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!("black"), + 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..121d25ea8 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); @@ -565,9 +527,57 @@ 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>, +} + +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(), + _ => 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..289bec04e --- /dev/null +++ b/crates/hooks/src/theming/themes.rs @@ -0,0 +1,71 @@ +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)"), + }, + ..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)"), + }, + ..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)"), + }, + ..BASE_THEME +}; 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/examples/switch_theme.rs b/examples/switch_theme.rs index 705b8df7b..32b466e74 100644 --- a/examples/switch_theme.rs +++ b/examples/switch_theme.rs @@ -14,6 +14,15 @@ fn ThemeChanger() -> Element { let mut theme = use_theme(); rsx!( + Tile { + onselect: move |_| theme.set(BANANA_THEME), + leading: rsx!( + Radio { + selected: theme.read().name == "banana", + }, + ), + label { "Banana" } + } TooltipContainer { position: TooltipPosition::Besides, tooltip: rsx!( @@ -58,69 +67,82 @@ fn app() -> Element { rsx!( Body { - rect { - width: "fill", - height: "fill", - main_align: "center", - cross_align: "center", - spacing: "20", - padding: "40", - Switch { - enabled: value() >= 50., - ontoggled: move |_| { - if value() >= 50. { - value.set(25.0); - } else { - value.set(75.0); + ScrollView { + rect { + spacing: "20", + padding: "40", + cross_align: "center", + Switch { + enabled: value() >= 50., + ontoggled: move |_| { + if value() >= 50. { + value.set(25.0); + } else { + value.set(75.0); + } } } - } - Slider { - size: "fill", - value: value(), - onmoved: move |e| value.set(e), - } - Slider { - size: "200", - value: value(), - direction: "vertical", - onmoved: move |e| value.set(e), - } - ProgressBar { - show_progress: true, - progress: value() as f32 - } - Tile { - onselect: move |_| { - if value() >= 50. { - value.set(25.0); - } else { - value.set(75.0); - } - }, - leading: rsx!( - Checkbox { - selected: value() >= 50., + Slider { + size: "fill", + value: value(), + onmoved: move |e| value.set(e), + } + ProgressBar { + show_progress: true, + progress: value() as f32 + } + Tile { + onselect: move |_| { + if value() >= 50. { + value.set(25.0); + } else { + value.set(75.0); + } }, - ), - label { "First choice" } - } - Tile { - onselect: move |_| { - if value() < 50. { - value.set(75.0); - } else { - value.set(25.0); - } - }, - leading: rsx!( - Checkbox { - selected: value() < 50., + leading: rsx!( + Checkbox { + selected: value() >= 50., + }, + ), + label { "First choice" } + } + Tile { + onselect: move |_| { + if value() < 50. { + value.set(75.0); + } else { + value.set(25.0); + } }, - ), - label { "Second choice" } + leading: rsx!( + Checkbox { + selected: value() < 50., + }, + ), + label { "Second choice" } + } + Input { + value: value().round().to_string(), + onchange: move |num: String| { + if let Ok(num) = num.parse() { + *value.write() = num; + } + } + } + Button { + onclick: move |_| value.set(35.), + label { + "Set to 35%" + } + } + ThemeChanger { } + } + } + SnackBar { + open: value() >= 50., + label { + "Hello!" } - ThemeChanger { } } } ) diff --git a/examples/themer.rs b/examples/themer.rs new file mode 100644 index 000000000..db915d9d7 --- /dev/null +++ b/examples/themer.rs @@ -0,0 +1,54 @@ +#![cfg_attr( + all(not(debug_assertions), target_os = "windows"), + windows_subsystem = "windows" +)] + +use freya::prelude::*; + +fn main() { + launch_with_props(app, "Themer", (400.0, 350.0)); +} + +fn app() -> Element { + use_init_default_theme(); + let mut theme = use_theme(); + let mut r = use_signal::(|| 103. / 255. * 100.); + let mut g = use_signal::(|| 80. / 255. * 100.); + let mut b = use_signal::(|| 164. / 255. * 100.); + + use_effect(move || { + let r = (255. / 100. * *r.read()).round() as u8; + let g = (255. / 100. * *g.read()).round() as u8; + let b = (255. / 100. * *b.read()).round() as u8; + theme.write().colors.primary = format!("rgb({r}, {g}, {b})").into(); + }); + + rsx!( + rect { + height: "fill", + width: "fill", + main_align: "center", + cross_align: "center", + padding: "10", + Switch { + enabled: true, + ontoggled: |_| {} + } + Slider { + size: "fill", + value: r(), + onmoved: move |e| r.set(e), + } + Slider { + size: "fill", + value: g(), + onmoved: move |e| g.set(e), + } + Slider { + size: "fill", + value: b(), + onmoved: move |e| b.set(e), + } + } + ) +} From 6e7681fefb345421b4056481f7edd12674f5f054 Mon Sep 17 00:00:00 2001 From: Marc Espin Date: Sat, 28 Sep 2024 15:50:30 +0200 Subject: [PATCH 38/39] feat: Keyboard navigation for `Checkbox` (#926) * feat: Unified colors theming * some improvements * improvements * clean up * feat: Keyboard navigation support for `Checkbox` * chore: Update switch_theme example --- crates/components/src/checkbox.rs | 44 ++++++++++++++++++++++--------- crates/hooks/src/theming/base.rs | 1 + crates/hooks/src/theming/mod.rs | 1 + examples/switch_theme.rs | 2 +- 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/crates/components/src/checkbox.rs b/crates/components/src/checkbox.rs index fb1aad579..b5e58b8f6 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,46 @@ 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 solid {}", 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, + border_align: "outer", 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 solid {outer_fill}", + background: "{inner_fill}", + onkeydown, + if selected { + TickIcon { + fill: selected_icon_fill + } } } } diff --git a/crates/hooks/src/theming/base.rs b/crates/hooks/src/theming/base.rs index f017c4d43..d8ab036ee 100644 --- a/crates/hooks/src/theming/base.rs +++ b/crates/hooks/src/theming/base.rs @@ -180,6 +180,7 @@ pub(crate) const BASE_THEME: Theme = Theme { 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)"), diff --git a/crates/hooks/src/theming/mod.rs b/crates/hooks/src/theming/mod.rs index 121d25ea8..ae3f32f7d 100644 --- a/crates/hooks/src/theming/mod.rs +++ b/crates/hooks/src/theming/mod.rs @@ -482,6 +482,7 @@ define_theme! { unselected_fill: str, selected_fill: str, selected_icon_fill: str, + border_fill: str, } } diff --git a/examples/switch_theme.rs b/examples/switch_theme.rs index 32b466e74..befb14012 100644 --- a/examples/switch_theme.rs +++ b/examples/switch_theme.rs @@ -130,7 +130,7 @@ fn app() -> Element { } } Button { - onclick: move |_| value.set(35.), + onpress: move |_| value.set(35.), label { "Set to 35%" } From edc1af2e87b90a594f243eb0d4bb2aba5eff6664 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Sat, 28 Sep 2024 15:55:01 +0200 Subject: [PATCH 39/39] fix: Update checkbox tests --- crates/components/src/checkbox.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/components/src/checkbox.rs b/crates/components/src/checkbox.rs index b5e58b8f6..3d3806c7f 100644 --- a/crates/components/src/checkbox.rs +++ b/crates/components/src/checkbox.rs @@ -192,29 +192,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()); } }