From adf50e7216b9d99ee90a40360199e56453cf82ca Mon Sep 17 00:00:00 2001 From: "Austin M. Reppert" Date: Sat, 20 Dec 2025 20:59:32 -0500 Subject: [PATCH 001/143] Remove layout and add scale factor setter. --- crates/craft_retained/src/app.rs | 21 +++++----- .../craft_retained/src/elements/container.rs | 5 --- .../src/elements/core/element_internals.rs | 39 ++++++++++++++----- crates/craft_retained/src/elements/element.rs | 29 ++++++++++++++ crates/craft_retained/src/elements/image.rs | 7 ---- .../src/elements/slider/slider.rs | 9 ----- crates/craft_retained/src/elements/text.rs | 12 +++++- .../craft_retained/src/elements/text_input.rs | 28 +++++++++---- 8 files changed, 97 insertions(+), 53 deletions(-) diff --git a/crates/craft_retained/src/app.rs b/crates/craft_retained/src/app.rs index df175867..ff1b321e 100644 --- a/crates/craft_retained/src/app.rs +++ b/crates/craft_retained/src/app.rs @@ -125,6 +125,7 @@ impl App { pub fn on_scale_factor_changed(&mut self, scale_factor: f64) { self.window_context.scale_factor = scale_factor; self.on_resize(self.window.as_ref().unwrap().inner_size()); + self.root.borrow_mut().scale_factor(self.window_context.effective_scale_factor()); } pub fn on_process_user_events(&mut self) {} @@ -177,6 +178,7 @@ impl App { if let Some(renderer) = self.renderer.as_mut() { renderer.resize_surface(new_size.width.max(1) as f32, new_size.height.max(1) as f32); } + style_root_element(self.root.borrow_mut().deref_mut(), self.window_context.window_size()); // On macOS the window needs to be redrawn manually after resizing #[cfg(target_os = "macos")] { @@ -322,6 +324,7 @@ impl App { } else { self.window_context.zoom_in(); } + self.root.borrow_mut().scale_factor(self.window_context.effective_scale_factor()); self.request_redraw(RedrawFlags::new(true)); return; } @@ -488,7 +491,6 @@ impl App { mouse_position: Option, ) { let root_element = self.root.clone(); - style_root_element(root_element.borrow_mut().deref_mut(), viewport_size); let text_context = self.text_context.as_mut().unwrap(); { @@ -595,6 +597,7 @@ fn style_root_element(root: &mut dyn Element, root_size: LogicalSize) { } else { style.set_height(Unit::Px(root_size.height)); } + root.update_taffy_style(); } #[allow(clippy::too_many_arguments)] @@ -608,17 +611,11 @@ fn layout( scale_factor: f64, pointer: Option, ) -> NodeId { - let root_node = { - let span = span!(Level::INFO, "compute layout(internal)"); - let _enter = span.enter(); - root_element.borrow_mut().compute_layout(taffy_tree, scale_factor); - // There should usually be a layout node. If not, exiting early could also make sense. - root_element.borrow() - .element_data() - .layout_item - .taffy_node_id - .expect("A root element must have a layout node.") - }; + let root_node = root_element.borrow() + .element_data() + .layout_item + .taffy_node_id + .expect("A root element must have a layout node."); let available_space: taffy::Size = taffy::Size { width: AvailableSpace::Definite(window_size.width), diff --git a/crates/craft_retained/src/elements/container.rs b/crates/craft_retained/src/elements/container.rs index 93164d55..c008f58e 100644 --- a/crates/craft_retained/src/elements/container.rs +++ b/crates/craft_retained/src/elements/container.rs @@ -124,11 +124,6 @@ impl Element for Container { } impl ElementInternals for Container { - fn compute_layout(&mut self, taffy_tree: &mut TaffyTree, scale_factor: f64) { - self.compute_layout_children(taffy_tree, scale_factor); - - self.apply_style_to_layout_node_if_dirty(taffy_tree); - } fn apply_layout( &mut self, diff --git a/crates/craft_retained/src/elements/core/element_internals.rs b/crates/craft_retained/src/elements/core/element_internals.rs index a669cbd8..247896e4 100644 --- a/crates/craft_retained/src/elements/core/element_internals.rs +++ b/crates/craft_retained/src/elements/core/element_internals.rs @@ -18,18 +18,10 @@ use crate::elements::Element; #[cfg(feature = "accesskit")] use accesskit::{Action, Role}; use craft_primitives::geometry::borders::CssRoundedRect; +use crate::app::TAFFY_TREE; /// Internal element methods that should typically be ignored by users. Public for custom elements. -pub trait ElementInternals: ElementData { - /// Compute the element's layout. - fn compute_layout(&mut self, taffy_tree: &mut TaffyTree, scale_factor: f64); - - /// A helper to compute the layout for all children. - fn compute_layout_children(&mut self, taffy_tree: &mut TaffyTree, scale_factor: f64) { - for child in &self.element_data().children { - child.borrow_mut().compute_layout(taffy_tree, scale_factor); - } - } +pub trait ElementInternals: ElementData { /// A helper to apply the layout for all children. fn apply_layout_children( @@ -371,6 +363,33 @@ pub trait ElementInternals: ElementData { { Style::default() } + + /// Mark layout node dirty. + fn mark_dirty(&mut self) { + let id = self.element_data().layout_item.taffy_node_id; + if let Some(id) = id { + TAFFY_TREE.with_borrow_mut(|taffy_tree| { + taffy_tree.mark_dirty(id).expect("Failed to mark taffy node as dirty"); + }); + } + } + + /// Updates taffy's style to reflect craft's style struct. + fn update_taffy_style(&mut self) { + let id = self.element_data().layout_item.taffy_node_id; + if let Some(id) = id { + TAFFY_TREE.with_borrow_mut(|taffy_tree| { + taffy_tree.set_style(id, self.element_data().style.to_taffy_style()).expect("Failed to set style."); + }); + } + } + + /// Set's this element's scale factor. This should not be used to scale individual elements. + fn scale_factor(&mut self, scale_factor: f64) { + for child in &self.element_data().children { + child.borrow_mut().scale_factor(scale_factor); + } + } } pub(crate) fn resolve_clip_for_scrollable(element: &mut dyn Element, clip_bounds: Option) { diff --git a/crates/craft_retained/src/elements/element.rs b/crates/craft_retained/src/elements/element.rs index 972ca2a5..9c9b3b2a 100644 --- a/crates/craft_retained/src/elements/element.rs +++ b/crates/craft_retained/src/elements/element.rs @@ -287,6 +287,7 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { Self: Sized, { self.style_mut().set_display(display); + self.update_taffy_style(); self } @@ -295,6 +296,7 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { Self: Sized, { self.style_mut().set_box_sizing(box_sizing); + self.update_taffy_style(); self } @@ -303,6 +305,7 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { Self: Sized, { self.style_mut().set_position(position); + self.update_taffy_style(); self } @@ -311,6 +314,7 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { Self: Sized, { self.style_mut().set_margin(TrblRectangle::new(top, right, bottom, left)); + self.update_taffy_style(); self } @@ -319,6 +323,7 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { Self: Sized, { self.style_mut().set_padding(TrblRectangle::new(top, right, bottom, left)); + self.update_taffy_style(); self } @@ -327,6 +332,7 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { Self: Sized, { self.style_mut().set_gap([row_gap, column_gap]); + self.update_taffy_style(); self } @@ -335,6 +341,7 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { Self: Sized, { self.style_mut().set_inset(TrblRectangle::new(top, right, bottom, left)); + self.update_taffy_style(); self } @@ -343,6 +350,7 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { Self: Sized, { self.style_mut().set_min_width(min_width); + self.update_taffy_style(); self } @@ -351,6 +359,7 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { Self: Sized, { self.style_mut().set_min_height(min_height); + self.update_taffy_style(); self } @@ -359,6 +368,7 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { Self: Sized, { self.style_mut().set_width(width); + self.update_taffy_style(); self } @@ -367,6 +377,7 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { Self: Sized, { self.style_mut().set_height(height); + self.update_taffy_style(); self } @@ -375,6 +386,7 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { Self: Sized, { self.style_mut().set_max_width(max_width); + self.update_taffy_style(); self } @@ -383,6 +395,7 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { Self: Sized, { self.style_mut().set_max_height(max_height); + self.update_taffy_style(); self } @@ -391,6 +404,7 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { Self: Sized, { self.style_mut().set_wrap(wrap); + self.update_taffy_style(); self } @@ -399,6 +413,7 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { Self: Sized, { self.style_mut().set_align_items(align_items); + self.update_taffy_style(); self } @@ -407,6 +422,7 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { Self: Sized, { self.style_mut().set_justify_content(justify_content); + self.update_taffy_style(); self } @@ -415,6 +431,7 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { Self: Sized, { self.style_mut().set_flex_direction(flex_direction); + self.update_taffy_style(); self } @@ -423,6 +440,7 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { Self: Sized, { self.style_mut().set_flex_grow(flex_grow); + self.update_taffy_style(); self } @@ -431,6 +449,7 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { Self: Sized, { self.style_mut().set_flex_shrink(flex_shrink); + self.update_taffy_style(); self } @@ -439,6 +458,7 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { Self: Sized, { self.style_mut().set_flex_basis(flex_basis); + self.update_taffy_style(); self } @@ -447,6 +467,7 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { Self: Sized, { self.style_mut().set_font_family(font_family); + self.update_taffy_style(); self } @@ -455,6 +476,7 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { Self: Sized, { self.style_mut().set_color(color); + self.update_taffy_style(); self } @@ -471,6 +493,7 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { Self: Sized, { self.style_mut().set_font_size(font_size); + self.update_taffy_style(); self } @@ -479,6 +502,7 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { Self: Sized, { self.style_mut().set_line_height(line_height); + self.update_taffy_style(); self } @@ -487,6 +511,7 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { Self: Sized, { self.style_mut().set_font_weight(font_weight); + self.update_taffy_style(); self } @@ -495,6 +520,7 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { Self: Sized, { self.style_mut().set_font_style(font_style); + self.update_taffy_style(); self } @@ -503,6 +529,7 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { Self: Sized, { self.style_mut().set_underline(underline); + self.update_taffy_style(); self } @@ -511,6 +538,7 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { Self: Sized, { self.style_mut().set_overflow([overflow_x, overflow_y]); + self.update_taffy_style(); self } @@ -527,6 +555,7 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { Self: Sized, { self.style_mut().set_border_width(TrblRectangle::new(top, right, bottom, left)); + self.update_taffy_style(); self } diff --git a/crates/craft_retained/src/elements/image.rs b/crates/craft_retained/src/elements/image.rs index c75c2d1b..267fb516 100644 --- a/crates/craft_retained/src/elements/image.rs +++ b/crates/craft_retained/src/elements/image.rs @@ -96,13 +96,6 @@ impl Element for Image { } impl ElementInternals for Image { - fn compute_layout(&mut self, taffy_tree: &mut TaffyTree, _scale_factor: f64) { - if self.is_image_dirty { - taffy_tree.mark_dirty(self.element_data.layout_item.taffy_node_id.unwrap()).unwrap(); - } - - self.apply_style_to_layout_node_if_dirty(taffy_tree); - } fn apply_layout( &mut self, diff --git a/crates/craft_retained/src/elements/slider/slider.rs b/crates/craft_retained/src/elements/slider/slider.rs index 35b0a802..180f5768 100644 --- a/crates/craft_retained/src/elements/slider/slider.rs +++ b/crates/craft_retained/src/elements/slider/slider.rs @@ -28,7 +28,6 @@ pub enum SliderDirection { } pub struct Slider { - is_layout_dirty: bool, element_data: ElementData, me: Option>>, @@ -52,7 +51,6 @@ pub struct Slider { impl Slider { pub fn new(thumb_size: f32) -> Rc> { let me = Rc::new(RefCell::new(Self { - is_layout_dirty: false, element_data: ElementData::new(true), me: None, step: 1.0, @@ -241,13 +239,6 @@ impl Element for Slider { } impl ElementInternals for Slider { - fn compute_layout(&mut self, taffy_tree: &mut TaffyTree, _scale_factor: f64) { - if self.is_layout_dirty { - taffy_tree.mark_dirty(self.element_data.layout_item.taffy_node_id.unwrap()).unwrap(); - } - - self.apply_style_to_layout_node_if_dirty(taffy_tree); - } fn apply_layout( &mut self, diff --git a/crates/craft_retained/src/elements/text.rs b/crates/craft_retained/src/elements/text.rs index 62d23189..8e768946 100644 --- a/crates/craft_retained/src/elements/text.rs +++ b/crates/craft_retained/src/elements/text.rs @@ -152,6 +152,7 @@ impl Text { self.state.text = text; self.state.is_layout_dirty = true; self.state.is_render_dirty = true; + self.mark_dirty(); } pub(crate) fn measure( @@ -256,7 +257,7 @@ impl ElementInternals for Text { tree.nodes.push((current_node_id, current_node)); } - fn compute_layout( + /*fn compute_layout( &mut self, taffy_tree: &mut TaffyTree, scale_factor: f64, @@ -270,7 +271,7 @@ impl ElementInternals for Text { } self.apply_style_to_layout_node_if_dirty(taffy_tree); - } + }*/ fn apply_layout( &mut self, @@ -386,6 +387,13 @@ impl ElementInternals for Text { } } } + + fn scale_factor(&mut self, scale_factor: f64) { + self.state.is_layout_dirty = true; + self.state.is_render_dirty = true; + self.mark_dirty(); + self.state.scale_factor = scale_factor as f32; + } } impl TextState { diff --git a/crates/craft_retained/src/elements/text_input.rs b/crates/craft_retained/src/elements/text_input.rs index 5519fb64..b9394b09 100644 --- a/crates/craft_retained/src/elements/text_input.rs +++ b/crates/craft_retained/src/elements/text_input.rs @@ -11,7 +11,7 @@ use std::cell::RefCell; use std::collections::HashMap; use std::ops::{Deref, Range}; use std::rc::{Rc, Weak}; -use taffy::{AvailableSpace, TaffyTree}; +use taffy::{AvailableSpace, NodeId, TaffyTree}; use crate::app::TAFFY_TREE; use crate::elements::core::{resolve_clip_for_scrollable, ElementInternals}; @@ -69,6 +69,7 @@ pub enum TextInputMessage { #[derive(Clone, Default)] pub struct TextInputState { + taffy_node: Option, pub is_active: bool, #[allow(dead_code)] pub(crate) ime_state: ImeState, @@ -107,6 +108,7 @@ impl TextInput { default_style.add_styles_to_style_set(style_set); let text_input_state = TextInputState { + taffy_node: None, ime_state: ImeState::default(), is_active: false, editor, @@ -155,6 +157,7 @@ impl TextInput { .new_leaf_with_context(me.borrow().element_data.current_style().to_taffy_style(), context) .expect("TODO: panic message"); me.borrow_mut().element_data.layout_item.taffy_node_id = Some(node_id); + me.borrow_mut().state.taffy_node(Some(node_id)); }); ELEMENTS.with_borrow_mut(|elements| { @@ -176,13 +179,6 @@ impl crate::elements::core::ElementData for TextInput { } impl ElementInternals for TextInput { - fn compute_layout(&mut self, taffy_tree: &mut TaffyTree, _scale_factor: f64) { - if self.state.is_layout_dirty { - taffy_tree.mark_dirty(self.element_data.layout_item.taffy_node_id.unwrap()).unwrap(); - } - - self.apply_style_to_layout_node_if_dirty(taffy_tree); - } fn apply_layout( &mut self, @@ -747,6 +743,12 @@ impl ElementInternals for TextInput { style } + + fn scale_factor(&mut self, scale_factor: f64) { + self.state.editor.set_scale(scale_factor as f32); + self.state.scale_factor = scale_factor; + self.state.clear_cache(); + } } impl TextInput { @@ -809,6 +811,12 @@ impl TextInputState { self.current_render_key = None; self.text_render = None; self.content_widths = None; + + if let Some(id) = self.taffy_node { + TAFFY_TREE.with_borrow_mut(|taffy_tree| { + taffy_tree.mark_dirty(id).expect("Failed to mark node dirty"); + }) + } } pub fn layout( @@ -888,6 +896,10 @@ impl TextInputState { size } + fn taffy_node(&mut self, taffy_node: Option) { + self.taffy_node = taffy_node; + } + pub fn get_cursor_link(&self, cursor_pos: Point, element: &TextInput) -> Option { if let Some(ranged_styles) = &element.ranged_styles { let layout = self.editor.try_layout().unwrap(); From 2fd917c3971e5876bcd893b618ac05dcc0a56eaf Mon Sep 17 00:00:00 2001 From: "Austin M. Reppert" Date: Sun, 21 Dec 2025 02:01:05 -0500 Subject: [PATCH 002/143] Cull when adding. --- .../src/geometry/rectangle.rs | 19 +++++++-- crates/craft_renderer/src/renderer.rs | 39 +++++++++++++++++-- crates/craft_retained/src/app.rs | 4 +- .../craft_retained/src/layout/layout_item.rs | 8 ++-- 4 files changed, 57 insertions(+), 13 deletions(-) diff --git a/crates/craft_primitives/src/geometry/rectangle.rs b/crates/craft_primitives/src/geometry/rectangle.rs index a2b8904b..6af62827 100644 --- a/crates/craft_primitives/src/geometry/rectangle.rs +++ b/crates/craft_primitives/src/geometry/rectangle.rs @@ -25,6 +25,7 @@ impl Rectangle { /// # Returns /// /// `true` if the rectangle contains the point, `false` otherwise. + #[inline(always)] pub fn contains(&self, point: &Point) -> bool { point.x as f32 >= self.left() && point.x as f32 <= self.right() @@ -65,30 +66,31 @@ impl Rectangle { } /// Returns the position of the top-left corner of the rectangle. + #[inline(always)] pub fn position(&self) -> Point { Point::new(self.x as f64, self.y as f64) } /// Returns the y-coordinate of the top edge of the rectangle. - #[inline] + #[inline(always)] pub fn top(&self) -> f32 { self.y } /// Returns the x-coordinate of the right edge of the rectangle. - #[inline] + #[inline(always)] pub fn right(&self) -> f32 { self.x + self.width } /// Returns the y-coordinate of the bottom edge of the rectangle. - #[inline] + #[inline(always)] pub fn bottom(&self) -> f32 { self.y + self.height } /// Returns the x-coordinate of the left edge of the rectangle. - #[inline] + #[inline(always)] pub fn left(&self) -> f32 { self.x } @@ -106,6 +108,7 @@ impl Rectangle { } } + pub fn intersection(&self, other: &Rectangle) -> Option { let x0 = self.x.max(other.x); let y0 = self.y.max(other.y); @@ -118,6 +121,14 @@ impl Rectangle { None } } + + #[inline(always)] + pub fn intersects(&self, other: &Rectangle) -> bool { + self.x < other.right() + && self.right() > other.x + && self.y < other.bottom() + && self.bottom() > other.y + } } /*impl From> for Rectangle { diff --git a/crates/craft_renderer/src/renderer.rs b/crates/craft_renderer/src/renderer.rs index 7c7b5138..4dbbc7ee 100644 --- a/crates/craft_renderer/src/renderer.rs +++ b/crates/craft_renderer/src/renderer.rs @@ -96,6 +96,7 @@ pub struct RenderList { pub commands: Vec, /// Stores a sorted list of render command handles. This gets set in `Renderer::sort_render_list`. pub overlay: SortedCommands, + cull: Option, } impl Default for RenderList { @@ -110,6 +111,7 @@ impl RenderList { targets: Vec::new(), commands: Vec::new(), overlay: SortedCommands { children: vec![] }, + cull: None, } } @@ -119,19 +121,41 @@ impl RenderList { self.overlay.children.clear(); } + #[inline(always)] pub fn draw_rect(&mut self, rectangle: Rectangle, fill_color: Color) { + if let Some(cull) = &self.cull { + if !cull.intersects(&rectangle) { + return; + } + } self.commands.push(RenderCommand::DrawRect(rectangle, fill_color)); } + pub fn push_hit_testable(&mut self, id: u64, bounding_box: Rectangle) { + if let Some(cull) = &self.cull { + if !cull.intersects(&bounding_box) { + return; + } + } self.targets.push((id, bounding_box)); } pub fn draw_rect_outline(&mut self, rectangle: Rectangle, outline_color: Color, thickness: f64) { + if let Some(cull) = &self.cull { + if !cull.intersects(&rectangle) { + return; + } + } self.commands.push(RenderCommand::DrawRectOutline(rectangle, outline_color, thickness)); } pub fn fill_bez_path(&mut self, path: kurbo::BezPath, brush: Brush) { + if let Some(cull) = &self.cull { + if !cull.intersects(&Rectangle::from_kurbo(path.bounding_box())) { + return; + } + } self.commands.push(RenderCommand::FillBezPath(path, brush)); } @@ -142,8 +166,14 @@ impl RenderList { text_scroll: Option, show_cursor: bool, ) { + if let Some(cull) = &self.cull { + if !cull.intersects(&rectangle) { + return; + } + } self.commands.push(RenderCommand::DrawText(component, rectangle, text_scroll, show_cursor)); } + pub fn draw_image(&mut self, rectangle: Rectangle, resource_identifier: ResourceIdentifier) { self.commands.push(RenderCommand::DrawImage(rectangle, resource_identifier)); } @@ -172,6 +202,10 @@ impl RenderList { pub fn end_overlay(&mut self) { self.commands.push(RenderCommand::EndOverlay); } + + pub fn set_cull(&mut self, cull: Option) { + self.cull = cull; + } } pub trait Renderer: Any { @@ -185,6 +219,7 @@ pub trait Renderer: Any { fn as_any_mut(&mut self) -> &mut dyn Any; + #[inline(never)] fn sort_and_cull_render_list(&mut self, render_list: &mut RenderList) { fn should_cull(rectangle: &Rectangle, window_height: f32) -> bool { let cull_top = (rectangle.y + rectangle.height) < 0.0; @@ -207,10 +242,6 @@ pub trait Renderer: Any { let window_height = self.surface_height(); - render_list.targets.retain(|(_, bounding_box)| { - !should_cull(bounding_box, window_height) - }); - let mut current: *mut SortedCommands = &mut render_list.overlay; let mut stack: Vec<*mut SortedCommands> = vec![current]; diff --git a/crates/craft_retained/src/app.rs b/crates/craft_retained/src/app.rs index ff1b321e..173bfecf 100644 --- a/crates/craft_retained/src/app.rs +++ b/crates/craft_retained/src/app.rs @@ -178,6 +178,7 @@ impl App { if let Some(renderer) = self.renderer.as_mut() { renderer.resize_surface(new_size.width.max(1) as f32, new_size.height.max(1) as f32); } + self.render_list.set_cull(Some(Rectangle::new(0.0, 0.0, new_size.width as f32, new_size.height as f32))); style_root_element(self.root.borrow_mut().deref_mut(), self.window_context.window_size()); // On macOS the window needs to be redrawn manually after resizing #[cfg(target_os = "macos")] @@ -285,7 +286,7 @@ impl App { ); } - self.animate_tree(&delta_time, layout_origin, root_size); + //self.animate_tree(&delta_time, layout_origin, root_size); if self.renderer.is_some() { self.draw_reactive_tree(self.window_context.mouse_position); @@ -512,6 +513,7 @@ impl App { } #[allow(clippy::too_many_arguments)] + #[inline(never)] fn draw_reactive_tree(&mut self, mouse_position: Option) { let text_context = self.text_context.as_mut().unwrap(); { diff --git a/crates/craft_retained/src/layout/layout_item.rs b/crates/craft_retained/src/layout/layout_item.rs index d86fc728..8ad32091 100644 --- a/crates/craft_retained/src/layout/layout_item.rs +++ b/crates/craft_retained/src/layout/layout_item.rs @@ -191,9 +191,7 @@ impl LayoutItem { // OPTIMIZATION: Draw a normal rectangle if no border values have been modified. match &self.computed_border { - ComputedBorder::CssComputedBorder(computed_border) => { - draw_borders_generic(renderer, computed_border, current_style.border_color().to_array(), background_color); - } + ComputedBorder::None => {} ComputedBorder::Simple => { let padding_rect =self.computed_box_transformed.padding_rectangle().scale(scale_factor); let border_rect = self.computed_box_transformed.border_rectangle(); @@ -207,7 +205,9 @@ impl LayoutItem { renderer.draw_rect_outline(border_rect, border_color, thickness as f64); } } - ComputedBorder::None => {} + ComputedBorder::CssComputedBorder(computed_border) => { + draw_borders_generic(renderer, computed_border, current_style.border_color().to_array(), background_color); + } } } } From b20e529e4398f754638515500b87a5d18d2d7a19 Mon Sep 17 00:00:00 2001 From: "Austin M. Reppert" Date: Sun, 21 Dec 2025 14:13:38 -0500 Subject: [PATCH 003/143] Inlines --- crates/craft_primitives/src/geometry/size.rs | 1 + crates/craft_primitives/src/geometry/trblrectangle.rs | 1 + crates/craft_renderer/src/renderer.rs | 3 +-- crates/craft_retained/src/app.rs | 1 - crates/craft_retained/src/layout/layout_item.rs | 1 + 5 files changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/craft_primitives/src/geometry/size.rs b/crates/craft_primitives/src/geometry/size.rs index ac09e1fb..77122f1e 100644 --- a/crates/craft_primitives/src/geometry/size.rs +++ b/crates/craft_primitives/src/geometry/size.rs @@ -18,6 +18,7 @@ impl Size { /// # Returns /// /// A `Size` instance with the specified width and height. + #[inline(always)] pub fn new(width: T, height: T) -> Self { Self { width, height } } diff --git a/crates/craft_primitives/src/geometry/trblrectangle.rs b/crates/craft_primitives/src/geometry/trblrectangle.rs index 8cfac69b..484f4710 100644 --- a/crates/craft_primitives/src/geometry/trblrectangle.rs +++ b/crates/craft_primitives/src/geometry/trblrectangle.rs @@ -11,6 +11,7 @@ impl TrblRectangle where T: Copy + PartialEq, { + #[inline(always)] pub const fn new(top: T, right: T, bottom: T, left: T) -> Self { Self { top, diff --git a/crates/craft_renderer/src/renderer.rs b/crates/craft_renderer/src/renderer.rs index 4dbbc7ee..cc771d7b 100644 --- a/crates/craft_renderer/src/renderer.rs +++ b/crates/craft_renderer/src/renderer.rs @@ -218,8 +218,7 @@ pub trait Renderer: Any { fn surface_set_clear_color(&mut self, color: Color); fn as_any_mut(&mut self) -> &mut dyn Any; - - #[inline(never)] + fn sort_and_cull_render_list(&mut self, render_list: &mut RenderList) { fn should_cull(rectangle: &Rectangle, window_height: f32) -> bool { let cull_top = (rectangle.y + rectangle.height) < 0.0; diff --git a/crates/craft_retained/src/app.rs b/crates/craft_retained/src/app.rs index 173bfecf..6b6f2b5e 100644 --- a/crates/craft_retained/src/app.rs +++ b/crates/craft_retained/src/app.rs @@ -513,7 +513,6 @@ impl App { } #[allow(clippy::too_many_arguments)] - #[inline(never)] fn draw_reactive_tree(&mut self, mouse_position: Option) { let text_context = self.text_context.as_mut().unwrap(); { diff --git a/crates/craft_retained/src/layout/layout_item.rs b/crates/craft_retained/src/layout/layout_item.rs index 8ad32091..2a040e98 100644 --- a/crates/craft_retained/src/layout/layout_item.rs +++ b/crates/craft_retained/src/layout/layout_item.rs @@ -250,6 +250,7 @@ pub(crate) enum ComputedBorder { None, } +#[inline(always)] fn from_taffy_point(p: taffy::Point) -> Point { Point { x: p.x as f64, From f3c7409f0fe4dc63bc15a9fb2691b0f8e1e70d9a Mon Sep 17 00:00:00 2001 From: "Austin M. Reppert" Date: Mon, 22 Dec 2025 01:16:08 -0500 Subject: [PATCH 004/143] poc --- Cargo.lock | 2 - crates/craft_retained/Cargo.toml | 3 +- crates/craft_retained/src/app.rs | 11 ++-- .../craft_retained/src/craft_winit_state.rs | 12 ++--- .../craft_retained/src/elements/container.rs | 35 ++++++++++--- .../src/elements/core/element_internals.rs | 3 ++ crates/craft_retained/src/elements/image.rs | 1 + .../src/elements/scroll_state.rs | 12 +++++ .../src/elements/slider/slider.rs | 1 + crates/craft_retained/src/elements/text.rs | 50 +++++++++++++------ .../craft_retained/src/elements/text_input.rs | 1 + .../craft_retained/src/layout/layout_item.rs | 8 +++ crates/craft_retained/src/lib.rs | 3 -- 13 files changed, 100 insertions(+), 42 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cb8d8749..bcbd6929 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3904,8 +3904,6 @@ dependencies = [ [[package]] name = "taffy" version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41ba83ebaf2954d31d05d67340fd46cebe99da2b7133b0dd68d70c65473a437b" dependencies = [ "arrayvec", "grid", diff --git a/crates/craft_retained/Cargo.toml b/crates/craft_retained/Cargo.toml index 1c23e391..699ec0a8 100644 --- a/crates/craft_retained/Cargo.toml +++ b/crates/craft_retained/Cargo.toml @@ -87,7 +87,8 @@ optional = true workspace = true [dependencies.taffy] -version = "0.9.2" +#version = "0.9.2" +path = "../../../taffy" default-features = false features = ["std", "taffy_tree", "flexbox", "content_size", "block_layout"] diff --git a/crates/craft_retained/src/app.rs b/crates/craft_retained/src/app.rs index 6b6f2b5e..832fa39c 100644 --- a/crates/craft_retained/src/app.rs +++ b/crates/craft_retained/src/app.rs @@ -30,8 +30,6 @@ use { accesskit_winit::Adapter, }; -#[cfg(not(target_arch = "wasm32"))] -use std::time; #[cfg(target_arch = "wasm32")] use web_time as time; @@ -90,7 +88,6 @@ pub struct App { #[allow(dead_code)] pub(crate) runtime: CraftRuntimeHandle, pub(crate) modifiers: Modifiers, - pub(crate) last_frame_time: time::Instant, pub redraw_flags: RedrawFlags, pub(crate) render_list: RenderList, @@ -260,10 +257,6 @@ impl App { self.update_resources(); - let now = time::Instant::now(); - let delta_time = now - self.last_frame_time; - self.last_frame_time = now; - let surface_size = self.window_context.window_size(); self.update_view(); @@ -451,6 +444,7 @@ impl App { } /// "Animates" a tree by calling `on_animation_frame` and changing an element's styles. + #[allow(dead_code)] fn animate_tree(&mut self, delta_time: &Duration, layout_origin: Point, viewport_size: LogicalSize) { let span = span!(Level::INFO, "animate_tree"); let _enter = span.enter(); @@ -591,7 +585,7 @@ fn style_root_element(root: &mut dyn Element, root_size: LogicalSize) { style.set_width(Unit::Px(root_size.width)); style.set_wrap(Wrap::Wrap); - style.set_display(Display::Block); + style.set_display(Display::Flex); if is_user_root_height_auto { style.set_height(Unit::Auto); @@ -659,6 +653,7 @@ fn layout( text_context, None, scale_factor, + false, ); } diff --git a/crates/craft_retained/src/craft_winit_state.rs b/crates/craft_retained/src/craft_winit_state.rs index 1915bcc5..36226d6e 100644 --- a/crates/craft_retained/src/craft_winit_state.rs +++ b/crates/craft_retained/src/craft_winit_state.rs @@ -14,12 +14,10 @@ use craft_logging::info; use winit::application::ApplicationHandler; use winit::event::{StartCause, WindowEvent}; -use winit::event_loop::{ActiveEventLoop, ControlFlow}; +use winit::event_loop::{ActiveEventLoop}; use winit::window::WindowAttributes; use winit::window::{Window, WindowId}; -#[cfg(not(target_arch = "wasm32"))] -use std::time; #[cfg(target_arch = "wasm32")] use web_time as time; @@ -34,8 +32,6 @@ use ui_events_winit::{WindowEventReducer, WindowEventTranslation}; use winit::dpi::LogicalSize; use crate::document::Document; -const WAIT_TIME: time::Duration = time::Duration::from_millis(15); - /// Stores state related to Winit. /// /// Forwards most events to the main Craft Event Loop. @@ -253,9 +249,11 @@ impl ApplicationHandler for CraftWinitState { return; } + //self.craft_state.craft_app.window.clone().unwrap().request_redraw(); + // Switch to Poll mode if we are running animations. - let has_active_animation = self.craft_state.craft_app.previous_animation_flags.has_active_animation(); +/* let has_active_animation = self.craft_state.craft_app.previous_animation_flags.has_active_animation(); if has_active_animation { event_loop.set_control_flow(ControlFlow::Poll); @@ -263,7 +261,7 @@ impl ApplicationHandler for CraftWinitState { event_loop.set_control_flow(ControlFlow::WaitUntil(time::Instant::now() + WAIT_TIME)); } event_loop.set_control_flow(ControlFlow::Poll); - + */ } } diff --git a/crates/craft_retained/src/elements/container.rs b/crates/craft_retained/src/elements/container.rs index c008f58e..b2317a14 100644 --- a/crates/craft_retained/src/elements/container.rs +++ b/crates/craft_retained/src/elements/container.rs @@ -15,6 +15,7 @@ use std::cell::RefCell; use std::ops::Deref; use std::rc::{Rc, Weak}; use taffy::TaffyTree; +use crate::rgba; /// Stores one or more elements. /// @@ -135,18 +136,36 @@ impl ElementInternals for Container { text_context: &mut TextContext, clip_bounds: Option, scale_factor: f64, + dirty: bool, ) { - let layout = taffy_tree.layout(self.element_data.layout_item.taffy_node_id.unwrap()).unwrap(); - self.resolve_box(position, transform, layout, z_index); - self.apply_borders(scale_factor); + let node = self.element_data.layout_item.taffy_node_id.unwrap(); + let layout = taffy_tree.layout(node).unwrap(); + + let dirty = layout.has_new_layout || transform != self.element_data.layout_item.get_transform() ; + self.element_data.layout_item.has_new_layout = dirty; + if dirty { + self.resolve_box(position, transform, layout, z_index); + self.apply_borders(scale_factor); + // For scroll changes from taffy; + self.element_data.apply_scroll(layout); + self.apply_clip(clip_bounds); + self.element_data.scroll_state.as_mut().unwrap().mark_old(); + } + + // For manual scroll updates. + if !dirty && self.element_data.scroll_state.map(|scroll_state| scroll_state.is_new()).unwrap_or_default() { + self.element_data.apply_scroll(layout); + self.element_data.scroll_state.as_mut().unwrap().mark_old(); + } - self.element_data.apply_scroll(layout); - self.apply_clip(clip_bounds); + if layout.has_new_layout { + taffy_tree.mark_old(node); + } let scroll_y = self.element_data.scroll().map_or(0.0, |s| s.scroll_y() as f64); let child_transform = Affine::translate((0.0, -scroll_y)); - self.apply_layout_children(taffy_tree, z_index, transform * child_transform, pointer, text_context, scale_factor) + self.apply_layout_children(taffy_tree, z_index, transform * child_transform, pointer, text_context, scale_factor, false) } fn draw( @@ -164,6 +183,10 @@ impl ElementInternals for Container { // We draw the borders before we start any layers, so that we don't clip the borders. self.draw_borders(renderer, scale_factor); + if self.element_data.layout_item.has_new_layout { + renderer.draw_rect_outline(self.element_data.layout_item.computed_box_transformed.padding_rectangle(), rgba(255, 0, 0, 100), 1.0); + } + self.maybe_start_layer(renderer, scale_factor); self.draw_children(renderer, text_context, pointer, scale_factor); self.maybe_end_layer(renderer); diff --git a/crates/craft_retained/src/elements/core/element_internals.rs b/crates/craft_retained/src/elements/core/element_internals.rs index 247896e4..cfaf8a89 100644 --- a/crates/craft_retained/src/elements/core/element_internals.rs +++ b/crates/craft_retained/src/elements/core/element_internals.rs @@ -32,6 +32,7 @@ pub trait ElementInternals: ElementData { pointer: Option, text_context: &mut TextContext, scale_factor: f64, + dirty: bool, ) { for child in &self.element_data().children { child.borrow_mut().apply_layout( @@ -43,6 +44,7 @@ pub trait ElementInternals: ElementData { text_context, self.element_data().layout_item.clip_bounds, scale_factor, + dirty, ); } } @@ -109,6 +111,7 @@ pub trait ElementInternals: ElementData { text_context: &mut TextContext, clip_bounds: Option, scale_factor: f64, + dirty: bool, ); /// Draws the element and its visual contents. diff --git a/crates/craft_retained/src/elements/image.rs b/crates/craft_retained/src/elements/image.rs index 267fb516..6f767174 100644 --- a/crates/craft_retained/src/elements/image.rs +++ b/crates/craft_retained/src/elements/image.rs @@ -107,6 +107,7 @@ impl ElementInternals for Image { _text_context: &mut TextContext, clip_bounds: Option, scale_factor: f64, + dirty: bool, ) { let layout = taffy_tree.layout(self.element_data.layout_item.taffy_node_id.unwrap()).unwrap(); self.resolve_box(position, transform, layout, z_index); diff --git a/crates/craft_retained/src/elements/scroll_state.rs b/crates/craft_retained/src/elements/scroll_state.rs index b8ee9a5e..b0087704 100644 --- a/crates/craft_retained/src/elements/scroll_state.rs +++ b/crates/craft_retained/src/elements/scroll_state.rs @@ -8,6 +8,9 @@ pub struct ScrollState { /// Where the scrollbar was clicked. pub(crate) scroll_click: Option, + + // True if the scroll changes are new. + is_new: bool, } impl ScrollState { @@ -16,6 +19,14 @@ impl ScrollState { self.scroll_y } + pub fn mark_old(&mut self) { + self.is_new = false; + } + + pub fn is_new(&self) -> bool { + self.is_new + } + /// Sets the total amount of vertical scroll. /// /// # Panics @@ -25,6 +36,7 @@ impl ScrollState { if self.scroll_y < 0.0 { panic!("Scroll cannot be negative."); } + self.is_new = true; self.scroll_y = scroll_y; } } diff --git a/crates/craft_retained/src/elements/slider/slider.rs b/crates/craft_retained/src/elements/slider/slider.rs index 180f5768..4fe51aee 100644 --- a/crates/craft_retained/src/elements/slider/slider.rs +++ b/crates/craft_retained/src/elements/slider/slider.rs @@ -250,6 +250,7 @@ impl ElementInternals for Slider { _text_context: &mut TextContext, clip_bounds: Option, scale_factor: f64, + dirty: bool, ) { let layout = taffy_tree.layout(self.element_data.layout_item.taffy_node_id.unwrap()).unwrap(); self.resolve_box(position, transform, layout, z_index); diff --git a/crates/craft_retained/src/elements/text.rs b/crates/craft_retained/src/elements/text.rs index 8e768946..16ce4cb9 100644 --- a/crates/craft_retained/src/elements/text.rs +++ b/crates/craft_retained/src/elements/text.rs @@ -6,6 +6,7 @@ use crate::text::text_context::TextContext; use crate::text::text_render_data; use crate::text::text_render_data::TextRender; use craft_primitives::geometry::{Point, Rectangle}; +use craft_primitives::Color; use craft_renderer::renderer::RenderList; #[cfg(feature = "accesskit")] use parley::LayoutAccessibility; @@ -37,6 +38,7 @@ use craft_primitives::ColorBrush; use craft_renderer::text_renderer_data::TextData; use smol_str::{SmolStr, ToSmolStr}; use ui_events::pointer::{PointerButton, PointerId}; +use crate::rgba; // A stateful element that shows text. #[derive(Clone, Default)] @@ -203,6 +205,10 @@ impl ElementInternals for Text { self.draw_borders(renderer, scale_factor); + if self.element_data.layout_item.has_new_layout { + renderer.draw_rect_outline(self.element_data.layout_item.computed_box_transformed.padding_rectangle(), rgba(255, 0, 0, 100), 1.0); + } + renderer.draw_text(self.me.clone().unwrap(), content_rectangle.scale(scale_factor), None, false); } @@ -283,12 +289,23 @@ impl ElementInternals for Text { text_context: &mut TextContext, clip_bounds: Option, scale_factor: f64, + dirty: bool, ) { - let result = taffy_tree.layout(self.element_data.layout_item.taffy_node_id.unwrap()).unwrap(); - self.resolve_box(position, transform, result, z_index); - self.apply_clip(clip_bounds); + let node = self.element_data.layout_item.taffy_node_id.unwrap(); + let result = taffy_tree.layout(node).unwrap(); + + let dirty = dirty || result.has_new_layout || transform != self.element_data.layout_item.get_transform(); + self.element_data.layout_item.has_new_layout = dirty; + if dirty { + self.resolve_box(position, transform, result, z_index); + self.apply_clip(clip_bounds); + + self.apply_borders(scale_factor); + } - self.apply_borders(scale_factor); + if result.has_new_layout { + taffy_tree.mark_old(node); + } let state: &mut TextState = &mut self.state; if state.current_layout_key != state.last_requested_measure_key { @@ -299,16 +316,6 @@ impl ElementInternals for Text { } state.try_update_text_render(text_context); - - // This needs to be cached. - let layout = state.layout.as_ref().unwrap(); - let text_renderer = state.text_render.as_mut().unwrap(); - for line in text_renderer.lines.iter_mut() { - line.selections.clear(); - } - state.selection.geometry_with(layout, |rect, line| { - text_renderer.lines[line].selections.push((Rectangle::new(rect.x0 as f32, rect.y0 as f32, rect.width() as f32, rect.height() as f32), self.element_data.style.selection_color())); - }); } fn on_event( @@ -333,10 +340,10 @@ impl ElementInternals for Text { let text_position = self.computed_box_transformed().content_rectangle(); let state: &mut TextState = &mut self.state; - match message { CraftMessage::PointerButtonDown(pointer_button) => { if pointer_button.button.map(|button| button == PointerButton::Primary).unwrap_or_default() { + state.update_text_selection(self.element_data.style.selection_color()); state.pointer_down = true; state.cursor_reset(); let now = Instant::now(); @@ -365,6 +372,7 @@ impl ElementInternals for Text { } CraftMessage::PointerButtonUp(pointer_button) => { if pointer_button.button.map(|button| button == PointerButton::Primary).unwrap_or_default() { + state.update_text_selection(self.element_data.style.selection_color()); state.pointer_down = false; state.cursor_reset(); self.release_pointer_capture(PointerId::new(1).unwrap()); @@ -377,6 +385,7 @@ impl ElementInternals for Text { state.cursor_pos = pointer_moved.current.logical_point() - kurbo::Vec2::new(text_position.x as f64, text_position.y as f64); // macOS seems to generate a spurious move after selecting word? if state.pointer_down && prev_pos != state.cursor_pos { + state.update_text_selection(self.element_data.style.selection_color()); state.cursor_reset(); let cursor_pos = state.cursor_pos; state.extend_selection_to_point(cursor_pos); @@ -524,6 +533,17 @@ impl TextState { self.content_widths = None; self.is_layout_dirty = false; } + + fn update_text_selection(&mut self, selection_color: Color) { + let layout = self.layout.as_ref().unwrap(); + let text_renderer = self.text_render.as_mut().unwrap(); + for line in text_renderer.lines.iter_mut() { + line.selections.clear(); + } + self.selection.geometry_with(layout, |rect, line| { + text_renderer.lines[line].selections.push((Rectangle::new(rect.x0 as f32, rect.y0 as f32, rect.width() as f32, rect.height() as f32), selection_color)); + }); + } } impl TextData for Text { diff --git a/crates/craft_retained/src/elements/text_input.rs b/crates/craft_retained/src/elements/text_input.rs index b9394b09..3241bfd9 100644 --- a/crates/craft_retained/src/elements/text_input.rs +++ b/crates/craft_retained/src/elements/text_input.rs @@ -190,6 +190,7 @@ impl ElementInternals for TextInput { text_context: &mut TextContext, clip_bounds: Option, scale_factor: f64, + dirty: bool, ) { let result = taffy_tree.layout(self.element_data.layout_item.taffy_node_id.unwrap()).unwrap(); self.resolve_box(position, transform, result, z_index); diff --git a/crates/craft_retained/src/layout/layout_item.rs b/crates/craft_retained/src/layout/layout_item.rs index 2a040e98..d00bc59d 100644 --- a/crates/craft_retained/src/layout/layout_item.rs +++ b/crates/craft_retained/src/layout/layout_item.rs @@ -64,9 +64,16 @@ pub struct LayoutItem { //cache_border_spec: Option<(CssRoundedRect, f64)>, // f64 for scale factor cache_border_spec: Option, computed_border: ComputedBorder, + /// True if the layout is new. + pub has_new_layout: bool, + transform: Affine, } impl LayoutItem { + pub(crate) fn get_transform(&self) -> Affine { + self.transform + } + pub fn push_child(&mut self, child: &Option) { if let Some(taffy_node_id) = child.as_ref() { self.children_awaiting_add.push(*taffy_node_id); @@ -134,6 +141,7 @@ impl LayoutItem { size, }; self.computed_box_transformed = self.computed_box.transform(scroll_transform); + self.transform = scroll_transform; } pub fn apply_borders( diff --git a/crates/craft_retained/src/lib.rs b/crates/craft_retained/src/lib.rs index 3c56ba04..4ea52162 100644 --- a/crates/craft_retained/src/lib.rs +++ b/crates/craft_retained/src/lib.rs @@ -47,8 +47,6 @@ use std::future::Future; use std::pin::Pin; use std::rc::Rc; use std::sync::Arc; -#[cfg(not(target_arch = "wasm32"))] -use std::time; #[cfg(target_arch = "wasm32")] use web_time as time; #[cfg(target_arch = "wasm32")] @@ -208,7 +206,6 @@ pub fn setup_craft( runtime: runtime_copy, modifiers: Default::default(), - last_frame_time: time::Instant::now(), redraw_flags: RedrawFlags::new(true), render_list: RenderList::new(), target_scratch: Vec::new(), From 4e465e3d9176f2bf6548c37082e2a1af9a983396 Mon Sep 17 00:00:00 2001 From: "Austin M. Reppert" Date: Wed, 24 Dec 2025 13:54:16 -0500 Subject: [PATCH 005/143] poc --- Cargo.lock | 1 + crates/craft_renderer/src/renderer.rs | 8 + crates/craft_retained/Cargo.toml | 3 +- crates/craft_retained/src/app.rs | 57 +- .../craft_retained/src/craft_winit_state.rs | 12 +- .../craft_retained/src/elements/container.rs | 20 +- .../src/elements/core/element_internals.rs | 8 +- crates/craft_retained/src/elements/image.rs | 1 - .../craft_retained/src/elements/scrollable.rs | 5 + .../src/elements/slider/slider.rs | 21 +- crates/craft_retained/src/elements/text.rs | 42 +- .../craft_retained/src/elements/text_input.rs | 984 ------------------ .../src/elements/text_input/mod.rs | 431 ++++++++ .../elements/text_input/text_input_state.rs | 676 ++++++++++++ .../src/events/event_dispatch.rs | 4 +- .../craft_retained/src/layout/layout_item.rs | 4 +- crates/craft_retained/src/lib.rs | 2 + .../craft_retained/src/text/parley_editor.rs | 5 + examples/jsframeworkbench/main.rs | 45 +- 19 files changed, 1250 insertions(+), 1079 deletions(-) delete mode 100644 crates/craft_retained/src/elements/text_input.rs create mode 100644 crates/craft_retained/src/elements/text_input/mod.rs create mode 100644 crates/craft_retained/src/elements/text_input/text_input_state.rs diff --git a/Cargo.lock b/Cargo.lock index bcbd6929..a9f77194 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3904,6 +3904,7 @@ dependencies = [ [[package]] name = "taffy" version = "0.9.2" +source = "git+https://github.com/AustinMReppert/taffy?rev=c99a8e60bf43ea695abda49a4be2b07baf683934#c99a8e60bf43ea695abda49a4be2b07baf683934" dependencies = [ "arrayvec", "grid", diff --git a/crates/craft_renderer/src/renderer.rs b/crates/craft_renderer/src/renderer.rs index cc771d7b..2cb78d6d 100644 --- a/crates/craft_renderer/src/renderer.rs +++ b/crates/craft_renderer/src/renderer.rs @@ -132,6 +132,7 @@ impl RenderList { } + #[inline(always)] pub fn push_hit_testable(&mut self, id: u64, bounding_box: Rectangle) { if let Some(cull) = &self.cull { if !cull.intersects(&bounding_box) { @@ -141,6 +142,7 @@ impl RenderList { self.targets.push((id, bounding_box)); } + #[inline(always)] pub fn draw_rect_outline(&mut self, rectangle: Rectangle, outline_color: Color, thickness: f64) { if let Some(cull) = &self.cull { if !cull.intersects(&rectangle) { @@ -150,6 +152,7 @@ impl RenderList { self.commands.push(RenderCommand::DrawRectOutline(rectangle, outline_color, thickness)); } + #[inline(always)] pub fn fill_bez_path(&mut self, path: kurbo::BezPath, brush: Brush) { if let Some(cull) = &self.cull { if !cull.intersects(&Rectangle::from_kurbo(path.bounding_box())) { @@ -159,6 +162,7 @@ impl RenderList { self.commands.push(RenderCommand::FillBezPath(path, brush)); } + #[inline(always)] pub fn draw_text( &mut self, component: Weak>, @@ -174,10 +178,12 @@ impl RenderList { self.commands.push(RenderCommand::DrawText(component, rectangle, text_scroll, show_cursor)); } + #[inline(always)] pub fn draw_image(&mut self, rectangle: Rectangle, resource_identifier: ResourceIdentifier) { self.commands.push(RenderCommand::DrawImage(rectangle, resource_identifier)); } + #[inline(always)] pub fn draw_tiny_vg( &mut self, rectangle: Rectangle, @@ -187,10 +193,12 @@ impl RenderList { self.commands.push(RenderCommand::DrawTinyVg(rectangle, resource_identifier, override_color)); } + #[inline(always)] pub fn push_layer(&mut self, rect: Rectangle) { self.commands.push(RenderCommand::PushLayer(rect)); } + #[inline(always)] pub fn pop_layer(&mut self) { self.commands.push(RenderCommand::PopLayer); } diff --git a/crates/craft_retained/Cargo.toml b/crates/craft_retained/Cargo.toml index 699ec0a8..aff0a120 100644 --- a/crates/craft_retained/Cargo.toml +++ b/crates/craft_retained/Cargo.toml @@ -88,7 +88,8 @@ workspace = true [dependencies.taffy] #version = "0.9.2" -path = "../../../taffy" +git = "https://github.com/AustinMReppert/taffy" +rev = "c99a8e60bf43ea695abda49a4be2b07baf683934" default-features = false features = ["std", "taffy_tree", "flexbox", "content_size", "block_layout"] diff --git a/crates/craft_retained/src/app.rs b/crates/craft_retained/src/app.rs index 832fa39c..b228028c 100644 --- a/crates/craft_retained/src/app.rs +++ b/crates/craft_retained/src/app.rs @@ -62,6 +62,7 @@ thread_local! { pub(crate) static PENDING_RESOURCES: RefCell> = RefCell::new(VecDeque::new()); pub(crate) static IN_PROGRESS_RESOURCES: RefCell> = RefCell::new(VecDeque::new()); pub(crate) static FOCUS: RefCell>>> = RefCell::new(None); + pub(crate) static LAYOUT_DIRTY: RefCell = RefCell::new(true); } pub struct App { @@ -123,6 +124,7 @@ impl App { self.window_context.scale_factor = scale_factor; self.on_resize(self.window.as_ref().unwrap().inner_size()); self.root.borrow_mut().scale_factor(self.window_context.effective_scale_factor()); + style_root_element(self.root.borrow_mut().deref_mut(), self.window_context.window_size()); } pub fn on_process_user_events(&mut self) {} @@ -287,8 +289,8 @@ impl App { } { - let span = span!(Level::INFO, "renderer_submit"); - let _enter = span.enter(); + /*let span = span!(Level::INFO, "renderer_submit"); + let _enter = span.enter();*/ if self.renderer.is_some() { self.renderer.as_mut().unwrap().submit(self.resource_manager.clone()); @@ -319,6 +321,8 @@ impl App { self.window_context.zoom_in(); } self.root.borrow_mut().scale_factor(self.window_context.effective_scale_factor()); + request_layout(); + style_root_element(self.root.borrow_mut().deref_mut(), self.window_context.window_size()); self.request_redraw(RedrawFlags::new(true)); return; } @@ -446,8 +450,8 @@ impl App { /// "Animates" a tree by calling `on_animation_frame` and changing an element's styles. #[allow(dead_code)] fn animate_tree(&mut self, delta_time: &Duration, layout_origin: Point, viewport_size: LogicalSize) { - let span = span!(Level::INFO, "animate_tree"); - let _enter = span.enter(); + /*let span = span!(Level::INFO, "animate_tree"); + let _enter = span.enter();*/ let old_has_active_animation = self.previous_animation_flags.has_active_animation(); let root_element = self.root.clone(); @@ -489,8 +493,8 @@ impl App { let text_context = self.text_context.as_mut().unwrap(); { - let span = span!(Level::INFO, "layout"); - let _enter = span.enter(); + /*let span = span!(Level::INFO, "layout"); + let _enter = span.enter();*/ TAFFY_TREE.with_borrow_mut(|taffy_tree| { layout( taffy_tree, @@ -510,16 +514,16 @@ impl App { fn draw_reactive_tree(&mut self, mouse_position: Option) { let text_context = self.text_context.as_mut().unwrap(); { - let span = span!(Level::INFO, "render"); - let _enter = span.enter(); + /*let span = span!(Level::INFO, "render"); + let _enter = span.enter();*/ self.render_list.clear(); let scale_factor = self.window_context.effective_scale_factor(); let renderer = self.renderer.as_mut().unwrap(); { - let span = span!(Level::INFO, "render(element)"); - let _enter = span.enter(); + /*let span = span!(Level::INFO, "render(element)"); + let _enter = span.enter();*/ self.root.borrow_mut().draw(&mut self.render_list, text_context, mouse_position, scale_factor); } @@ -585,7 +589,7 @@ fn style_root_element(root: &mut dyn Element, root_size: LogicalSize) { style.set_width(Unit::Px(root_size.width)); style.set_wrap(Wrap::Wrap); - style.set_display(Display::Flex); + style.set_display(Display::Block); if is_user_root_height_auto { style.set_height(Unit::Auto); @@ -606,20 +610,30 @@ fn layout( scale_factor: f64, pointer: Option, ) -> NodeId { + let dirty = LAYOUT_DIRTY.with_borrow(|status| { + *status + }); + let root_node = root_element.borrow() .element_data() .layout_item .taffy_node_id .expect("A root element must have a layout node."); + if !dirty { + return root_node; + } + let available_space: taffy::Size = taffy::Size { width: AvailableSpace::Definite(window_size.width), height: AvailableSpace::Definite(window_size.height), }; { - let span = span!(Level::INFO, "layout(taffy)"); - let _enter = span.enter(); + /*let span = span!(Level::INFO, "layout(taffy)"); + let _enter = span.enter();*/ + //println!("before layout"); + //taffy_tree.print_tree(root_node); taffy_tree .compute_layout_with_measure( root_node, @@ -637,13 +651,15 @@ fn layout( ) .unwrap(); } + //println!("after layout"); + //taffy_tree.print_tree(root_node); let transform = Affine::IDENTITY; let mut layout_order: u32 = 0; { - let span = span!(Level::INFO, "layout(apply)"); - let _enter = span.enter(); + /*let span = span!(Level::INFO, "layout(apply)"); + let _enter = span.enter();*/ root_element.borrow_mut().apply_layout( taffy_tree, origin, @@ -653,9 +669,18 @@ fn layout( text_context, None, scale_factor, - false, ); } + LAYOUT_DIRTY.with_borrow_mut(|status| { + *status = false; + }); + root_node } + +pub fn request_layout() { + LAYOUT_DIRTY.with_borrow_mut(|status| { + *status = true; + }); +} \ No newline at end of file diff --git a/crates/craft_retained/src/craft_winit_state.rs b/crates/craft_retained/src/craft_winit_state.rs index 36226d6e..fbb58cfe 100644 --- a/crates/craft_retained/src/craft_winit_state.rs +++ b/crates/craft_retained/src/craft_winit_state.rs @@ -14,10 +14,12 @@ use craft_logging::info; use winit::application::ApplicationHandler; use winit::event::{StartCause, WindowEvent}; -use winit::event_loop::{ActiveEventLoop}; +use winit::event_loop::{ActiveEventLoop, ControlFlow}; use winit::window::WindowAttributes; use winit::window::{Window, WindowId}; +#[cfg(not(target_arch = "wasm32"))] +use std::time; #[cfg(target_arch = "wasm32")] use web_time as time; @@ -32,6 +34,8 @@ use ui_events_winit::{WindowEventReducer, WindowEventTranslation}; use winit::dpi::LogicalSize; use crate::document::Document; +const WAIT_TIME: time::Duration = time::Duration::from_millis(15); + /// Stores state related to Winit. /// /// Forwards most events to the main Craft Event Loop. @@ -249,19 +253,15 @@ impl ApplicationHandler for CraftWinitState { return; } - //self.craft_state.craft_app.window.clone().unwrap().request_redraw(); - // Switch to Poll mode if we are running animations. -/* let has_active_animation = self.craft_state.craft_app.previous_animation_flags.has_active_animation(); + let has_active_animation = self.craft_state.craft_app.previous_animation_flags.has_active_animation(); if has_active_animation { event_loop.set_control_flow(ControlFlow::Poll); } else { event_loop.set_control_flow(ControlFlow::WaitUntil(time::Instant::now() + WAIT_TIME)); } - event_loop.set_control_flow(ControlFlow::Poll); - */ } } diff --git a/crates/craft_retained/src/elements/container.rs b/crates/craft_retained/src/elements/container.rs index b2317a14..dd9a23fc 100644 --- a/crates/craft_retained/src/elements/container.rs +++ b/crates/craft_retained/src/elements/container.rs @@ -14,8 +14,7 @@ use std::any::Any; use std::cell::RefCell; use std::ops::Deref; use std::rc::{Rc, Weak}; -use taffy::TaffyTree; -use crate::rgba; +use taffy::{PrintTree, TaffyTree}; /// Stores one or more elements. /// @@ -136,13 +135,14 @@ impl ElementInternals for Container { text_context: &mut TextContext, clip_bounds: Option, scale_factor: f64, - dirty: bool, ) { let node = self.element_data.layout_item.taffy_node_id.unwrap(); let layout = taffy_tree.layout(node).unwrap(); + let has_new_layout = taffy_tree.get_has_new_layout(node); + + let dirty = has_new_layout || transform != self.element_data.layout_item.get_transform() || position != self.element_data.layout_item.position ; + self.element_data.layout_item.has_new_layout = has_new_layout; - let dirty = layout.has_new_layout || transform != self.element_data.layout_item.get_transform() ; - self.element_data.layout_item.has_new_layout = dirty; if dirty { self.resolve_box(position, transform, layout, z_index); self.apply_borders(scale_factor); @@ -158,8 +158,8 @@ impl ElementInternals for Container { self.element_data.scroll_state.as_mut().unwrap().mark_old(); } - if layout.has_new_layout { - taffy_tree.mark_old(node); + if has_new_layout { + taffy_tree.mark_seen(node); } let scroll_y = self.element_data.scroll().map_or(0.0, |s| s.scroll_y() as f64); @@ -183,9 +183,9 @@ impl ElementInternals for Container { // We draw the borders before we start any layers, so that we don't clip the borders. self.draw_borders(renderer, scale_factor); - if self.element_data.layout_item.has_new_layout { - renderer.draw_rect_outline(self.element_data.layout_item.computed_box_transformed.padding_rectangle(), rgba(255, 0, 0, 100), 1.0); - } + /*if self.element_data.layout_item.has_new_layout { + renderer.draw_rect_outline(self.element_data.layout_item.computed_box_transformed.padding_rectangle(), rgba(255, 0, 0, 100), 5.0); + }*/ self.maybe_start_layer(renderer, scale_factor); self.draw_children(renderer, text_context, pointer, scale_factor); diff --git a/crates/craft_retained/src/elements/core/element_internals.rs b/crates/craft_retained/src/elements/core/element_internals.rs index cfaf8a89..a35b8396 100644 --- a/crates/craft_retained/src/elements/core/element_internals.rs +++ b/crates/craft_retained/src/elements/core/element_internals.rs @@ -18,7 +18,8 @@ use crate::elements::Element; #[cfg(feature = "accesskit")] use accesskit::{Action, Role}; use craft_primitives::geometry::borders::CssRoundedRect; -use crate::app::TAFFY_TREE; +use crate::app::{LAYOUT_DIRTY, TAFFY_TREE}; +use crate::request_layout; /// Internal element methods that should typically be ignored by users. Public for custom elements. pub trait ElementInternals: ElementData { @@ -44,7 +45,6 @@ pub trait ElementInternals: ElementData { text_context, self.element_data().layout_item.clip_bounds, scale_factor, - dirty, ); } } @@ -111,7 +111,6 @@ pub trait ElementInternals: ElementData { text_context: &mut TextContext, clip_bounds: Option, scale_factor: f64, - dirty: bool, ); /// Draws the element and its visual contents. @@ -369,6 +368,7 @@ pub trait ElementInternals: ElementData { /// Mark layout node dirty. fn mark_dirty(&mut self) { + request_layout(); let id = self.element_data().layout_item.taffy_node_id; if let Some(id) = id { TAFFY_TREE.with_borrow_mut(|taffy_tree| { @@ -379,6 +379,8 @@ pub trait ElementInternals: ElementData { /// Updates taffy's style to reflect craft's style struct. fn update_taffy_style(&mut self) { + request_layout(); + let id = self.element_data().layout_item.taffy_node_id; if let Some(id) = id { TAFFY_TREE.with_borrow_mut(|taffy_tree| { diff --git a/crates/craft_retained/src/elements/image.rs b/crates/craft_retained/src/elements/image.rs index 6f767174..267fb516 100644 --- a/crates/craft_retained/src/elements/image.rs +++ b/crates/craft_retained/src/elements/image.rs @@ -107,7 +107,6 @@ impl ElementInternals for Image { _text_context: &mut TextContext, clip_bounds: Option, scale_factor: f64, - dirty: bool, ) { let layout = taffy_tree.layout(self.element_data.layout_item.taffy_node_id.unwrap()).unwrap(); self.resolve_box(position, transform, layout, z_index); diff --git a/crates/craft_retained/src/elements/scrollable.rs b/crates/craft_retained/src/elements/scrollable.rs index 97d911c4..3afbd9d1 100644 --- a/crates/craft_retained/src/elements/scrollable.rs +++ b/crates/craft_retained/src/elements/scrollable.rs @@ -3,6 +3,8 @@ use crate::events::{CraftMessage, Event}; use kurbo::Point; use ui_events::pointer::{PointerId, PointerType}; use ui_events::ScrollDelta; +use crate::app::LAYOUT_DIRTY; +use crate::request_layout; #[allow(clippy::too_many_arguments)] pub(crate) fn on_scroll_events(element: &mut dyn Element, message: &CraftMessage, event: &mut Event) { @@ -26,6 +28,7 @@ pub(crate) fn on_scroll_events(element: &mut dyn Element, message: &CraftMessage let current_scroll_y = state.scroll_y(); state.set_scroll_y((current_scroll_y + delta).clamp(0.0, max_scroll_y)); + request_layout(); event.prevent_propagate(); event.prevent_defaults(); @@ -76,6 +79,7 @@ pub(crate) fn on_scroll_events(element: &mut dyn Element, message: &CraftMessage let scroll_y = percent * element_data.layout_item.max_scroll_y; state.set_scroll_y(scroll_y.clamp(0.0, element_data.layout_item.max_scroll_y)); + request_layout(); event.prevent_propagate(); event.prevent_defaults(); @@ -112,6 +116,7 @@ pub(crate) fn on_scroll_events(element: &mut dyn Element, message: &CraftMessage let current_scroll_y = state.scroll_y(); state.set_scroll_y((current_scroll_y + delta).clamp(0.0, max_scroll_y)); + request_layout(); state.scroll_click = Some(Point::new(click.x, pointer_motion.current.position.y)); event.prevent_propagate(); event.prevent_defaults(); diff --git a/crates/craft_retained/src/elements/slider/slider.rs b/crates/craft_retained/src/elements/slider/slider.rs index 4fe51aee..da56b095 100644 --- a/crates/craft_retained/src/elements/slider/slider.rs +++ b/crates/craft_retained/src/elements/slider/slider.rs @@ -16,7 +16,7 @@ use std::any::Any; use std::cell::RefCell; use std::ops::Deref; use std::rc::{Rc, Weak}; -use taffy::TaffyTree; +use taffy::{PrintTree, TaffyTree}; use ui_events::keyboard::{Code, KeyState}; use ui_events::pointer::PointerId; @@ -250,13 +250,20 @@ impl ElementInternals for Slider { _text_context: &mut TextContext, clip_bounds: Option, scale_factor: f64, - dirty: bool, ) { - let layout = taffy_tree.layout(self.element_data.layout_item.taffy_node_id.unwrap()).unwrap(); - self.resolve_box(position, transform, layout, z_index); + let node = self.element_data.layout_item.taffy_node_id.unwrap(); + let layout = taffy_tree.layout(node).unwrap(); + let has_new_layout = taffy_tree.get_has_new_layout(node); - self.apply_borders(scale_factor); - self.apply_clip(clip_bounds); + let dirty = has_new_layout || transform != self.element_data.layout_item.get_transform() || position != self.element_data.layout_item.position ; + self.element_data.layout_item.has_new_layout = has_new_layout; + + if dirty { + self.resolve_box(position, transform, layout, z_index); + + self.apply_borders(scale_factor); + self.apply_clip(clip_bounds); + } } fn draw( @@ -328,7 +335,7 @@ impl ElementInternals for Slider { //event.result_message(CraftMessage::SliderValueChanged(value)); let new_event = Event::new(event.target.clone()); - dispatch_event(new_event, CraftMessage::SliderValueChanged(self.value)); + //dispatch_event(new_event, CraftMessage::SliderValueChanged(self.value)); } CraftMessage::PointerMovedEvent(pointer_update) => { if !self.dragging { diff --git a/crates/craft_retained/src/elements/text.rs b/crates/craft_retained/src/elements/text.rs index 16ce4cb9..dc43bf6d 100644 --- a/crates/craft_retained/src/elements/text.rs +++ b/crates/craft_retained/src/elements/text.rs @@ -24,7 +24,7 @@ use kurbo::Affine; use rustc_hash::FxHashMap; #[cfg(not(target_arch = "wasm32"))] use std::time; -use taffy::{AvailableSpace, Size, TaffyTree}; +use taffy::{AvailableSpace, PrintTree, Size, TaffyTree}; use time::{Duration, Instant}; use winit::dpi; #[cfg(target_arch = "wasm32")] @@ -38,7 +38,6 @@ use craft_primitives::ColorBrush; use craft_renderer::text_renderer_data::TextData; use smol_str::{SmolStr, ToSmolStr}; use ui_events::pointer::{PointerButton, PointerId}; -use crate::rgba; // A stateful element that shows text. #[derive(Clone, Default)] @@ -65,7 +64,11 @@ pub struct TextState { pub(crate) last_click_time: Option, pub(crate) click_count: u32, pub(crate) pointer_down: bool, - pub(crate) cursor_pos: Point, + /// The last known cursor position. + /// + /// The cursor is assumed to start at (0.0, 0.0). The cursor_pos may return points + /// outside the text input. + cursor_pos: Point, pub(crate) start_time: Option, pub(crate) blink_period: Duration, @@ -205,9 +208,9 @@ impl ElementInternals for Text { self.draw_borders(renderer, scale_factor); - if self.element_data.layout_item.has_new_layout { + /*if self.element_data.layout_item.has_new_layout { renderer.draw_rect_outline(self.element_data.layout_item.computed_box_transformed.padding_rectangle(), rgba(255, 0, 0, 100), 1.0); - } + }*/ renderer.draw_text(self.me.clone().unwrap(), content_rectangle.scale(scale_factor), None, false); } @@ -263,22 +266,6 @@ impl ElementInternals for Text { tree.nodes.push((current_node_id, current_node)); } - /*fn compute_layout( - &mut self, - taffy_tree: &mut TaffyTree, - scale_factor: f64, - ) { - if scale_factor as f32 != self.state.scale_factor { - self.state.is_layout_dirty = true; - self.state.scale_factor = scale_factor as f32; - } - if self.state.is_layout_dirty { - taffy_tree.mark_dirty(self.element_data.layout_item.taffy_node_id.unwrap()).unwrap(); - } - - self.apply_style_to_layout_node_if_dirty(taffy_tree); - }*/ - fn apply_layout( &mut self, taffy_tree: &mut TaffyTree, @@ -289,13 +276,13 @@ impl ElementInternals for Text { text_context: &mut TextContext, clip_bounds: Option, scale_factor: f64, - dirty: bool, ) { let node = self.element_data.layout_item.taffy_node_id.unwrap(); let result = taffy_tree.layout(node).unwrap(); + let has_new_layout = taffy_tree.get_has_new_layout(node); - let dirty = dirty || result.has_new_layout || transform != self.element_data.layout_item.get_transform(); - self.element_data.layout_item.has_new_layout = dirty; + let dirty = has_new_layout || transform != self.element_data.layout_item.get_transform() || position != self.element_data.layout_item.position; + self.element_data.layout_item.has_new_layout = has_new_layout; if dirty { self.resolve_box(position, transform, result, z_index); self.apply_clip(clip_bounds); @@ -303,8 +290,8 @@ impl ElementInternals for Text { self.apply_borders(scale_factor); } - if result.has_new_layout { - taffy_tree.mark_old(node); + if has_new_layout { + taffy_tree.mark_seen(node); } let state: &mut TextState = &mut self.state; @@ -325,9 +312,6 @@ impl ElementInternals for Text { event: &mut Event, _target: Option>>, ) { - //self.on_style_event(message, should_style, event); - //self.maybe_unset_focus(message, event, target); - if !self.selectable { return; } diff --git a/crates/craft_retained/src/elements/text_input.rs b/crates/craft_retained/src/elements/text_input.rs deleted file mode 100644 index 3241bfd9..00000000 --- a/crates/craft_retained/src/elements/text_input.rs +++ /dev/null @@ -1,984 +0,0 @@ -use crate::app::ELEMENTS; -use crate::elements::element::Element; -use crate::elements::element_data::ElementData; -use crate::layout::layout_context::{LayoutContext, TaffyTextInputContext}; -use crate::style::{Display, Style, TextStyleProperty, Unit}; -use craft_primitives::geometry::{Point, Rectangle, TrblRectangle}; -use craft_primitives::Color; -use craft_renderer::renderer::{RenderList, TextScroll}; -use std::any::Any; -use std::cell::RefCell; -use std::collections::HashMap; -use std::ops::{Deref, Range}; -use std::rc::{Rc, Weak}; -use taffy::{AvailableSpace, NodeId, TaffyTree}; - -use crate::app::TAFFY_TREE; -use crate::elements::core::{resolve_clip_for_scrollable, ElementInternals}; -#[cfg(feature = "accesskit")] -use crate::elements::element_id::create_unique_element_id; -use crate::elements::scrollable; -use crate::events::{CraftMessage, Event}; -use crate::layout::layout_context::TextHashKey; -use crate::text::parley_editor::{PlainEditor, PlainEditorDriver}; -use crate::text::text_context::TextContext; -use crate::text::text_render_data::TextRender; -use crate::text::{text_render_data, RangedStyles}; -use crate::utils::cloneable_any::CloneableAny; -use craft_renderer::text_renderer_data::TextData; -use kurbo::Affine; -use parley::{Affinity, BoundingBox, ContentWidths, Cursor, Selection}; -#[cfg(not(target_arch = "wasm32"))] -use std::time; -use time::{Duration, Instant}; -use ui_events::keyboard::{Key, Modifiers, NamedKey}; -use ui_events::pointer::PointerButton; -#[cfg(target_arch = "wasm32")] -use web_time as time; -use winit::dpi; -use winit::event::Ime; - -// A stateful element that shows text. -#[derive(Clone, Default)] -pub struct TextInput { - element_data: ElementData, - /// Whether the text input will update the editor every update with the user provided text. - /// NOTE: The editor will always use the user provided text on initialization. - use_text_value_on_update: bool, - pub text: Option, - pub ranged_styles: Option, - pub disabled: bool, - pub(crate) state: TextInputState, - me: Option>>, -} - -#[derive(Clone, Default, Debug, Copy)] -pub(crate) struct ImeState { - #[allow(dead_code)] - pub is_ime_active: bool, -} - -#[allow(dead_code)] -/// An external message that allows others to command the TextInput. -pub enum TextInputMessage { - Copy, - Paste, - Cut, - // TODO: Add more messages. -} - -#[derive(Clone, Default)] -pub struct TextInputState { - taffy_node: Option, - pub is_active: bool, - #[allow(dead_code)] - pub(crate) ime_state: ImeState, - pub(crate) editor: PlainEditor, - - cache: HashMap>, - - // The current key used for laying out the text input. - current_layout_key: Option, - - current_render_key: Option, - content_widths: Option, - - // The most recently requested key for laying out the text input. - last_requested_key: Option, - pub(crate) text_render: Option, - scale_factor: f64, - - last_click_time: Option, - click_count: u32, - pointer_down: bool, - cursor_pos: Point, - cursor_visible: bool, - modifiers: Option, - start_time: Option, - blink_period: Duration, - is_layout_dirty: bool, -} - -impl TextInput { - pub fn new(text: &str) -> Rc> { - let default_style = Self::get_default_style(); - let mut editor = PlainEditor::new(default_style.font_size()); - editor.set_scale(1.0f32); - let style_set = editor.edit_styles(); - default_style.add_styles_to_style_set(style_set); - - let text_input_state = TextInputState { - taffy_node: None, - ime_state: ImeState::default(), - is_active: false, - editor, - cache: Default::default(), - current_layout_key: None, - current_render_key: None, - content_widths: None, - last_requested_key: None, - text_render: None, - scale_factor: 1.0, - last_click_time: None, - click_count: 0, - pointer_down: false, - cursor_pos: Point::default(), - cursor_visible: false, - modifiers: None, - start_time: None, - blink_period: Default::default(), - is_layout_dirty: true, - }; - - let me = Rc::new(RefCell::new(Self { - text: Some(text.to_string()), - element_data: ElementData::new(true), - use_text_value_on_update: true, - ranged_styles: Some(RangedStyles::new(vec![])), - disabled: false, - state: text_input_state, - me: None, - })); - me.borrow_mut().element_data.style = default_style; - - let me2 = me.clone(); - me.borrow_mut().me = Some(Rc::downgrade(&me2)); - - let me_element: Rc> = me.clone(); - me.borrow_mut().element_data.me = Some(Rc::downgrade(&me_element)); - - me.borrow_mut().text(text); - - TAFFY_TREE.with_borrow_mut(|taffy_tree| { - let context = LayoutContext::TextInput(TaffyTextInputContext { - element: me.borrow().me.clone().unwrap(), - }); - let node_id = taffy_tree - .new_leaf_with_context(me.borrow().element_data.current_style().to_taffy_style(), context) - .expect("TODO: panic message"); - me.borrow_mut().element_data.layout_item.taffy_node_id = Some(node_id); - me.borrow_mut().state.taffy_node(Some(node_id)); - }); - - ELEMENTS.with_borrow_mut(|elements| { - elements.insert(me.borrow().deref()); - }); - - me - } -} - -impl crate::elements::core::ElementData for TextInput { - fn element_data(&self) -> &ElementData { - &self.element_data - } - - fn element_data_mut(&mut self) -> &mut ElementData { - &mut self.element_data - } -} - -impl ElementInternals for TextInput { - - fn apply_layout( - &mut self, - taffy_tree: &mut TaffyTree, - position: Point, - z_index: &mut u32, - transform: Affine, - _pointer: Option, - text_context: &mut TextContext, - clip_bounds: Option, - scale_factor: f64, - dirty: bool, - ) { - let result = taffy_tree.layout(self.element_data.layout_item.taffy_node_id.unwrap()).unwrap(); - self.resolve_box(position, transform, result, z_index); - self.apply_clip(clip_bounds); - - self.apply_borders(scale_factor); - - let focused = self.is_focused(); - - self.state.layout( - self.state.last_requested_key.unwrap().known_dimensions(), - self.state.last_requested_key.unwrap().available_space(), - text_context, - true, - ); - - let backgrounds: Vec<(Range, Color)> = self - .state - .editor - .ranged_styles - .styles - .iter() - .filter_map(|(range, style)| { - if let TextStyleProperty::BackgroundColor(color) = style { - Some((range.clone(), *color)) - } else { - None - } - }) - .collect(); - - let layout = self.state.editor.try_layout().unwrap(); - let backgrounds: Vec<(Selection, Color)> = backgrounds - .iter() - .map(|(range, color)| { - ( - Selection::new( - Cursor::from_byte_index(layout, range.start, Affinity::Downstream), - Cursor::from_byte_index(layout, range.end, Affinity::Downstream), - ), - *color, - ) - }) - .collect(); - - let text_renderer = self.state.text_render.as_mut().unwrap(); - for line in text_renderer.lines.iter_mut() { - line.backgrounds.clear(); - } - for (selection, color) in backgrounds.iter() { - selection.geometry_with(layout, |rect, line| { - text_renderer.lines[line].backgrounds.push(( - Rectangle::new(rect.x0 as f32, rect.y0 as f32, rect.width() as f32, rect.height() as f32), - *color, - )); - }); - } - - for line in text_renderer.lines.iter_mut() { - line.selections.clear(); - } - self.state.editor.selection_geometry_with(|rect, line| { - text_renderer.lines[line] - .selections - .push((parley_box_to_rect(rect), self.element_data.current_style().selection_color())); - }); - - if focused { - let color = - self.element_data.current_style().cursor_color().unwrap_or(self.element_data.current_style().color()); - text_renderer.cursor = self.state.editor.cursor_geometry(1.0).map(|r| (parley_box_to_rect(r), color)); - } else { - text_renderer.cursor = None; - } - - self.element_data.apply_scroll(result); - } - - fn draw( - &mut self, - renderer: &mut RenderList, - _text_context: &mut TextContext, - _pointer: Option, - scale_factor: f64, - ) { - if !self.is_visible() { - return; - } - - self.add_hit_testable(renderer, true, scale_factor); - - let computed_box_transformed = self.computed_box(); - let content_rectangle = computed_box_transformed.content_rectangle(); - - self.draw_borders(renderer, scale_factor); - - let is_scrollable = self.element_data.is_scrollable(); - - let element_data = &self.element_data; - let padding_rectangle = element_data.layout_item.computed_box_transformed.padding_rectangle(); - renderer.push_layer(padding_rectangle.scale(scale_factor)); - - let text_scroll = if is_scrollable { - Some(TextScroll::new( - self.element_data.scroll().map_or(0.0, |s| s.scroll_y()), - self.element_data.layout_item.computed_scroll_track.height, - )) - } else { - None - }; - - if self.state.text_render.as_ref().is_some() { - renderer.draw_text( - self.me.clone().unwrap(), - content_rectangle.scale(scale_factor), - text_scroll, - self.state.cursor_visible, - ); - } - - renderer.pop_layer(); - - self.draw_scrollbar(renderer, scale_factor); - } - - #[cfg(feature = "accesskit")] - fn compute_accessibility_tree( - &mut self, - tree: &mut accesskit::TreeUpdate, - parent_index: Option, - scale_factor: f64, - ) { - let state: &mut TextInputState = &mut self.state; - - if state.editor.try_layout().is_none() { - return; - } - - let editor = &mut state.editor; - - let current_node_id = accesskit::NodeId(self.element_data.internal_id); - - let mut current_node = accesskit::Node::new(accesskit::Role::TextInput); - let padding_box = - self.element_data.layout_item.computed_box_transformed.padding_rectangle().scale(scale_factor); - - current_node.set_bounds(accesskit::Rect { - x0: padding_box.left() as f64, - y0: padding_box.top() as f64, - x1: padding_box.right() as f64, - y1: padding_box.bottom() as f64, - }); - - editor.try_accessibility( - tree, - &mut current_node, - || accesskit::NodeId(create_unique_element_id()), - padding_box.x as f64, - padding_box.y as f64, - ); - - if let Some(parent_index) = parent_index { - let parent_node = tree.nodes.get_mut(parent_index).unwrap(); - parent_node.1.push_child(current_node_id); - } - - tree.nodes.push((current_node_id, current_node)); - } - - fn on_event( - &mut self, - message: &CraftMessage, - _text_context: &mut TextContext, - event: &mut Event, - _target: Option>>, - ) { - //self.on_style_event(message, element_state, should_style, event); - //self.maybe_unset_focus(message, event, target); - self.state.is_active = true; - - scrollable::on_scroll_events(self, message, event); - - if !event.propagate { - return; - } - - let scroll_y = self.element_data.scroll().map_or(0.0, |s| s.scroll_y() as f64); - - let scale_factor = self.state.scale_factor; - let text_position = self.computed_box().content_rectangle(); - let text_x = text_position.x; - let text_y = text_position.y; - let focused = self.is_focused(); - - #[cfg(all( - any(target_os = "windows", target_os = "macos", target_os = "linux"), - feature = "clipboard" - ))] - fn copy(drv: &mut PlainEditorDriver) { - use clipboard_rs::{Clipboard, ClipboardContext}; - if let Some(text) = drv.editor.selected_text() { - let cb = ClipboardContext::new().unwrap(); - cb.set_text(text.to_owned()).ok(); - } - } - - #[cfg(not(all( - any(target_os = "windows", target_os = "macos", target_os = "linux"), - feature = "clipboard" - )))] - fn copy(_drv: &mut PlainEditorDriver) {} - - #[cfg(all( - any(target_os = "windows", target_os = "macos", target_os = "linux"), - feature = "clipboard" - ))] - fn paste(drv: &mut PlainEditorDriver) { - use clipboard_rs::{Clipboard, ClipboardContext}; - let cb = ClipboardContext::new().unwrap(); - let text = cb.get_text().unwrap_or_default(); - drv.insert_or_replace_selection(&text); - } - - #[cfg(not(all( - any(target_os = "windows", target_os = "macos", target_os = "linux"), - feature = "clipboard" - )))] - fn paste(_drv: &mut PlainEditorDriver) {} - - #[cfg(all( - any(target_os = "windows", target_os = "macos", target_os = "linux"), - feature = "clipboard" - ))] - fn cut(drv: &mut PlainEditorDriver) { - use clipboard_rs::{Clipboard, ClipboardContext}; - if let Some(text) = drv.editor.selected_text() { - let cb = ClipboardContext::new().unwrap(); - cb.set_text(text.to_owned()).ok(); - drv.delete_selection(); - } - } - - #[cfg(not(all( - any(target_os = "windows", target_os = "macos", target_os = "linux"), - feature = "clipboard" - )))] - fn cut(_drv: &mut PlainEditorDriver) {} - - let mut generate_text_changed_event = |editor: &mut PlainEditor| { - // TODO: generate event. - let _new_text = editor.text().to_string(); - event.prevent_defaults(); - event.prevent_propagate(); - }; - - if let CraftMessage::ElementMessage(msg) = message - && let Some(msg) = msg.as_any().downcast_ref::() - { - let mut drv = self.state.driver(_text_context); - match msg { - TextInputMessage::Copy => { - copy(&mut drv); - } - TextInputMessage::Paste => { - if self.disabled { - return; - } - paste(&mut drv); - self.state.clear_cache(); - generate_text_changed_event(&mut self.state.editor); - } - TextInputMessage::Cut => { - if self.disabled { - return; - } - cut(&mut drv); - self.state.clear_cache(); - generate_text_changed_event(&mut self.state.editor); - } - } - } - - match message { - CraftMessage::KeyboardInputEvent(keyboard_input) if !self.state.editor.is_composing() => { - self.state.modifiers = Some(keyboard_input.modifiers); - - if self.disabled || !keyboard_input.state.is_down() || !focused { - return; - } - - self.state.cursor_reset(); - #[allow(unused)] - let (shift, action_mod) = self - .state - .modifiers - .map(|mods| (mods.shift(), if cfg!(target_os = "macos") { mods.meta() } else { mods.ctrl() })) - .unwrap_or_default(); - - let mut drv = self.state.driver(_text_context); - - match &keyboard_input.key { - Key::Character(c) if action_mod && matches!(c.as_str(), "c" | "x" | "v") => { - match c.to_lowercase().as_str() { - "c" => copy(&mut drv), - "x" => { - cut(&mut drv); - self.state.clear_cache(); - generate_text_changed_event(&mut self.state.editor); - } - "v" => { - paste(&mut drv); - self.state.clear_cache(); - generate_text_changed_event(&mut self.state.editor); - } - _ => (), - } - } - Key::Character(c) if action_mod && matches!(c.to_lowercase().as_str(), "a") => { - if shift { - drv.collapse_selection(); - } else { - drv.select_all(); - } - } - Key::Named(NamedKey::ArrowLeft) => { - if action_mod { - if shift { - drv.select_word_left(); - } else { - drv.move_word_left(); - } - } else if shift { - drv.select_left(); - } else { - drv.move_left(); - } - } - Key::Named(NamedKey::ArrowRight) => { - if action_mod { - if shift { - drv.select_word_right(); - } else { - drv.move_word_right(); - } - } else if shift { - drv.select_right(); - } else { - drv.move_right(); - } - } - Key::Named(NamedKey::ArrowUp) => { - if shift { - drv.select_up(); - } else { - drv.move_up(); - } - } - Key::Named(NamedKey::ArrowDown) => { - if shift { - drv.select_down(); - } else { - drv.move_down(); - } - } - Key::Named(NamedKey::Home) => { - if action_mod { - if shift { - drv.select_to_text_start(); - } else { - drv.move_to_text_start(); - } - } else if shift { - drv.select_to_line_start(); - } else { - drv.move_to_line_start(); - } - } - Key::Named(NamedKey::End) => { - let mut drv = self.state.driver(_text_context); - - if action_mod { - if shift { - drv.select_to_text_end(); - } else { - drv.move_to_text_end(); - } - } else if shift { - drv.select_to_line_end(); - } else { - drv.move_to_line_end(); - } - } - Key::Named(NamedKey::Delete) => { - if action_mod { - drv.delete_word(); - self.state.clear_cache(); - } else { - drv.delete(); - self.state.clear_cache(); - } - generate_text_changed_event(&mut self.state.editor); - } - Key::Named(NamedKey::Backspace) => { - if action_mod { - drv.backdelete_word(); - self.state.clear_cache(); - } else { - drv.backdelete(); - self.state.clear_cache(); - } - generate_text_changed_event(&mut self.state.editor); - } - Key::Named(NamedKey::Enter) => { - drv.insert_or_replace_selection("\n"); - self.state.clear_cache(); - generate_text_changed_event(&mut self.state.editor); - } - Key::Character(s) => { - drv.insert_or_replace_selection(s); - self.state.clear_cache(); - generate_text_changed_event(&mut self.state.editor); - } - _ => (), - } - } - // WindowEvent::Touch(Touch { - // phase, location, .. - // }) if !self.editor.is_composing() => { - // let mut drv = self.editor.driver(&mut self.font_cx, &mut self.layout_cx); - // use winit::event::TouchPhase::*; - // match phase { - // Started => { - // // TODO: start a timer to convert to a SelectWordAtPoint - // drv.move_to_point(location.x as f32, location.y as f32); - // } - // Cancelled => { - // drv.collapse_selection(); - // } - // Moved => { - // // TODO: cancel SelectWordAtPoint timer - // drv.extend_selection_to_point( - // location.x as f32, - // location.y as f32, - // ); - // } - // Ended => (), - // } - // } - CraftMessage::PointerButtonDown(pointer_button) => { - if pointer_button.button == Some(PointerButton::Primary) { - self.focus(); - self.state.pointer_down = true; - self.state.cursor_reset(); - if !self.state.editor.is_composing() { - let now = Instant::now(); - if let Some(last) = self.state.last_click_time.take() { - if now.duration_since(last).as_secs_f64() < 0.25 { - self.state.click_count = (self.state.click_count + 1) % 4; - } else { - self.state.click_count = 1; - } - } else { - self.state.click_count = 1; - } - self.state.last_click_time = Some(now); - let click_count = self.state.click_count; - let cursor_pos = self.state.cursor_pos; - let cursor_x = cursor_pos.x as f32; - let cursor_y = cursor_pos.y as f32; - - if click_count == 1 { - if let Some(_link) = self.state.get_cursor_link(cursor_pos, self) { - // TODO generate event - return; - } - } - - let mut drv = self.state.driver(_text_context); - - match click_count { - 2 => drv.select_word_at_point(cursor_x, cursor_y), - 3 => drv.select_line_at_point(cursor_x, cursor_y), - _ => drv.move_to_point(cursor_x, cursor_y), - } - } - } - } - CraftMessage::PointerButtonUp(pointer_button) => { - if pointer_button.button == Some(PointerButton::Primary) { - self.state.pointer_down = false; - self.state.cursor_reset(); - } - } - CraftMessage::PointerMovedEvent(pointer_moved) => { - let prev_pos = self.state.cursor_pos; - // NOTE: Cursor position should be relative to the top left of the text box. - let cursor_pos = pointer_moved.current.position; - let cursor_pos: Point = (cursor_pos.x as f32 - text_x, cursor_pos.y as f32 - text_y).into(); - let mut cursor_pos = Point::new(cursor_pos.x * scale_factor, cursor_pos.y * scale_factor); - cursor_pos.y += scroll_y as f64; - self.state.cursor_pos = cursor_pos; - // macOS seems to generate a spurious move after selecting word? - if self.state.pointer_down && prev_pos != self.state.cursor_pos && !self.state.editor.is_composing() { - self.state.cursor_reset(); - let cursor_pos = self.state.cursor_pos; - self.state - .driver(_text_context) - .extend_selection_to_point(cursor_pos.x as f32, cursor_pos.y as f32); - } - } - CraftMessage::ImeEvent(Ime::Disabled) => { - self.state.driver(_text_context).clear_compose(); - self.state.clear_cache(); - } - CraftMessage::ImeEvent(Ime::Commit(text)) => { - self.state.driver(_text_context).insert_or_replace_selection(text); - self.state.clear_cache(); - generate_text_changed_event(&mut self.state.editor); - } - CraftMessage::ImeEvent(Ime::Preedit(text, cursor)) => { - if text.is_empty() { - self.state.driver(_text_context).clear_compose(); - } else { - self.state.driver(_text_context).set_compose(text, *cursor); - } - self.state.clear_cache(); - } - _ => {} - } - } - - fn apply_clip(&mut self, clip_bounds: Option) { - resolve_clip_for_scrollable(self, clip_bounds); - } - - fn get_default_style() -> Style - where - Self: Sized, - { - let mut style = Style::default(); - - style.set_display(Display::Block); - - const BORDER_COLOR: Color = Color::from_rgb8(199, 199, 206); - style.set_border_color(TrblRectangle::new_all(BORDER_COLOR)); - style.set_border_width(TrblRectangle::new_all(Unit::Px(1.0))); - style.set_border_radius([(5.0, 5.0); 4]); - - let padding = Unit::Px(4.0); - style.set_padding(TrblRectangle::new_all(padding)); - - style - } - - fn scale_factor(&mut self, scale_factor: f64) { - self.state.editor.set_scale(scale_factor as f32); - self.state.scale_factor = scale_factor; - self.state.clear_cache(); - } -} - -impl TextInput { - /// Whether the text input will update the editor every update with the user provided text. - /// NOTE: The editor will always use the user provided text on initialization. - pub fn use_text_value_on_update(mut self, use_initial_text_value: bool) -> Self { - self.use_text_value_on_update = use_initial_text_value; - self - } - - pub fn disable(&mut self) -> &mut Self { - self.disabled = true; - self - } - - pub fn get_disabled(&mut self) -> bool { - self.disabled - } - - pub fn get_text(&self) -> &str { - &self.state.editor.raw_text() - } - - /// Set the text. - /// - /// Updates the text content immediately. Mark layout and render caches as dirty. Layout and - /// render caches will be computed in the next layout/render pass. - pub fn text(&mut self, text: &str) -> &mut Self { - self.state.editor.set_text(text); - self.state.clear_cache(); - self - } - - pub fn ranged_styles(&mut self, ranged_styles: RangedStyles) -> &mut Self { - self.state.editor.set_ranged_styles(ranged_styles); - self.state.clear_cache(); - self - } -} - -impl TextInputState { - pub fn measure( - &mut self, - known_dimensions: taffy::Size>, - available_space: taffy::Size, - text_context: &mut TextContext, - ) -> taffy::Size { - let key = TextHashKey::new(known_dimensions, available_space); - - self.last_requested_key = Some(key); - - self.layout(known_dimensions, available_space, text_context, false) - } - - pub fn clear_cache(&mut self) { - self.is_layout_dirty = true; - self.cache.clear(); - self.current_layout_key = None; - self.last_requested_key = None; - self.current_render_key = None; - self.text_render = None; - self.content_widths = None; - - if let Some(id) = self.taffy_node { - TAFFY_TREE.with_borrow_mut(|taffy_tree| { - taffy_tree.mark_dirty(id).expect("Failed to mark node dirty"); - }) - } - } - - pub fn layout( - &mut self, - known_dimensions: taffy::Size>, - available_space: taffy::Size, - text_context: &mut TextContext, - last_pass: bool, - ) -> taffy::Size { - let key = TextHashKey::new(known_dimensions, available_space); - - if let Some(value) = self.cache.get(&key) { - if last_pass { - if self.current_layout_key == Some(key) { - if self.current_render_key != self.current_layout_key { - self.current_render_key = self.current_layout_key; - - let layout = self.editor.try_layout().unwrap(); - self.text_render = Some(text_render_data::from_editor(layout)); - } - return *value; - } - } else { - return *value; - } - } - - if self.editor.try_layout().is_none() || self.is_layout_dirty || self.content_widths.is_none() { - self.editor.set_width(None); - self.editor.refresh_layout(&mut text_context.font_context, &mut text_context.layout_context); - self.content_widths = Some(self.editor.try_layout().unwrap().calculate_content_widths()); - } - - let content_widths = self.content_widths.unwrap(); - let width_constraint: Option = known_dimensions - .width - .or(match available_space.width { - AvailableSpace::MinContent => Some(content_widths.min), - AvailableSpace::MaxContent => Some(content_widths.max), - AvailableSpace::Definite(width) => Some(width), - }) - .map(|width| { - let width: f32 = dpi::PhysicalUnit::from_logical::(width, self.scale_factor).0; - // Taffy may give a min width > max_width. - // Min-width is preserved in this scenario to ensure text is readable. - width.clamp(content_widths.min, content_widths.max.max(content_widths.min)) - }); - - let _height_constraint: Option = known_dimensions - .height - .or(match available_space.height { - AvailableSpace::MinContent => None, - AvailableSpace::MaxContent => None, - AvailableSpace::Definite(height) => Some(height), - }) - .map(|height| dpi::PhysicalUnit::from_logical::(height, self.scale_factor).0); - - self.editor.set_width(width_constraint); - self.editor.refresh_layout(&mut text_context.font_context, &mut text_context.layout_context); - let layout = self.editor.try_layout().unwrap(); - - if last_pass { - self.current_render_key = self.current_layout_key; - self.text_render = Some(text_render_data::from_editor(layout)); - } - - let logical_width = dpi::LogicalUnit::from_physical::(layout.width(), self.scale_factor).0; - let logical_height = dpi::LogicalUnit::from_physical::(layout.height(), self.scale_factor).0; - - let size = taffy::Size { - width: logical_width, - height: logical_height, - }; - - self.cache.insert(key, size); - self.current_layout_key = Some(key); - size - } - - fn taffy_node(&mut self, taffy_node: Option) { - self.taffy_node = taffy_node; - } - - pub fn get_cursor_link(&self, cursor_pos: Point, element: &TextInput) -> Option { - if let Some(ranged_styles) = &element.ranged_styles { - let layout = self.editor.try_layout().unwrap(); - for (range, style) in ranged_styles.styles.iter() { - if let TextStyleProperty::Link(link) = style { - let anchor = Cursor::from_byte_index(layout, range.start, Affinity::Downstream); - let focus = Cursor::from_byte_index(layout, range.end, Affinity::Downstream); - let selection = Selection::new(anchor, focus); - let link_rects = selection.geometry(layout); - for link_rect in link_rects { - if parley_box_to_rect(link_rect.0).contains(&cursor_pos) { - return Some(link.clone()); - } - } - } - } - } - None - } - - pub fn cursor_reset(&mut self) { - self.start_time = Some(Instant::now()); - // TODO: for real world use, this should be reading from the system settings - self.blink_period = Duration::from_millis(500); - self.cursor_visible = true; - } - - #[allow(dead_code)] - pub fn disable_blink(&mut self) { - self.start_time = None; - } - - #[allow(dead_code)] - pub fn next_blink_time(&self) -> Option { - self.start_time.map(|start_time| { - let phase = Instant::now().duration_since(start_time); - - start_time - + Duration::from_nanos( - ((phase.as_nanos() / self.blink_period.as_nanos() + 1) * self.blink_period.as_nanos()) as u64, - ) - }) - } - - #[allow(dead_code)] - pub fn cursor_blink(&mut self) { - self.cursor_visible = self.start_time.is_some_and(|start_time| { - let elapsed = Instant::now().duration_since(start_time); - (elapsed.as_millis() / self.blink_period.as_millis()) % 2 == 0 - }); - } - - fn driver<'a>(&'a mut self, text_context: &'a mut TextContext) -> PlainEditorDriver<'a> { - self.editor.driver(&mut text_context.font_context, &mut text_context.layout_context) - } -} - -impl TextData for TextInput { - fn get_text_renderer(&self) -> Option<&TextRender> { - self.state.text_render.as_ref() - } -} - -fn parley_box_to_rect(bounding_box: BoundingBox) -> Rectangle { - Rectangle::new( - bounding_box.x0 as f32, - bounding_box.y0 as f32, - bounding_box.width() as f32, - bounding_box.height() as f32, - ) -} - -impl Element for TextInput { - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } -} diff --git a/crates/craft_retained/src/elements/text_input/mod.rs b/crates/craft_retained/src/elements/text_input/mod.rs new file mode 100644 index 00000000..a8938ae1 --- /dev/null +++ b/crates/craft_retained/src/elements/text_input/mod.rs @@ -0,0 +1,431 @@ +mod text_input_state; + +use crate::app::ELEMENTS; +use crate::elements::element::Element; +use crate::elements::element_data::ElementData; +use crate::layout::layout_context::{LayoutContext, TaffyTextInputContext}; +use crate::style::{Display, Style, Unit}; +use craft_primitives::geometry::{Point, Rectangle, TrblRectangle}; +use craft_primitives::Color; +use craft_renderer::renderer::{RenderList, TextScroll}; +use std::any::Any; +use std::cell::RefCell; +use std::ops::{Deref}; +use std::rc::{Rc, Weak}; +use taffy::{PrintTree, TaffyTree}; + +use crate::app::TAFFY_TREE; +use crate::elements::core::{resolve_clip_for_scrollable, ElementInternals}; +#[cfg(feature = "accesskit")] +use crate::elements::element_id::create_unique_element_id; +use crate::elements::scrollable; +use crate::elements::text_input::text_input_state::TextInputState; +use crate::events::{CraftMessage, Event}; +use crate::text::parley_editor::PlainEditor; +use crate::text::text_context::TextContext; +use crate::text::text_render_data::TextRender; +use crate::text::RangedStyles; +use crate::utils::cloneable_any::CloneableAny; +use craft_renderer::text_renderer_data::TextData; +use kurbo::Affine; +use parley::{BoundingBox}; +use ui_events::pointer::PointerButton; +use winit::event::Ime; + +// A stateful element that shows text. +#[derive(Clone, Default)] +pub struct TextInput { + element_data: ElementData, + /// Whether the text input will update the editor every update with the user provided text. + /// NOTE: The editor will always use the user provided text on initialization. + use_text_value_on_update: bool, + pub text: Option, + pub ranged_styles: Option, + pub disabled: bool, + pub(crate) state: TextInputState, + me: Option>>, +} + +#[allow(dead_code)] +/// An external message that allows others to command the TextInput. +pub enum TextInputMessage { + Copy, + Paste, + Cut, + // TODO: Add more messages. +} + +impl TextInput { + pub fn new(text: &str) -> Rc> { + let default_style = Self::get_default_style(); + + let text_input_state = TextInputState::default(); + + let me = Rc::new(RefCell::new(Self { + text: Some(text.to_string()), + element_data: ElementData::new(true), + use_text_value_on_update: true, + ranged_styles: Some(RangedStyles::new(vec![])), + disabled: false, + state: text_input_state, + me: None, + })); + me.borrow_mut().element_data.style = default_style; + + let me2 = me.clone(); + me.borrow_mut().me = Some(Rc::downgrade(&me2)); + + let me_element: Rc> = me.clone(); + me.borrow_mut().element_data.me = Some(Rc::downgrade(&me_element)); + + me.borrow_mut().text(text); + + TAFFY_TREE.with_borrow_mut(|taffy_tree| { + let context = LayoutContext::TextInput(TaffyTextInputContext { + element: me.borrow().me.clone().unwrap(), + }); + let node_id = taffy_tree + .new_leaf_with_context(me.borrow().element_data.current_style().to_taffy_style(), context) + .expect("TODO: panic message"); + me.borrow_mut().element_data.layout_item.taffy_node_id = Some(node_id); + me.borrow_mut().state.taffy_node(Some(node_id)); + }); + + ELEMENTS.with_borrow_mut(|elements| { + elements.insert(me.borrow().deref()); + }); + + me + } +} + +impl crate::elements::core::ElementData for TextInput { + fn element_data(&self) -> &ElementData { + &self.element_data + } + + fn element_data_mut(&mut self) -> &mut ElementData { + &mut self.element_data + } +} + +impl ElementInternals for TextInput { + fn apply_layout( + &mut self, + taffy_tree: &mut TaffyTree, + position: Point, + z_index: &mut u32, + transform: Affine, + _pointer: Option, + text_context: &mut TextContext, + clip_bounds: Option, + scale_factor: f64, + ) { + let node = self.element_data.layout_item.taffy_node_id.unwrap(); + let has_new_layout = taffy_tree.get_has_new_layout(node); + + let dirty = has_new_layout + || transform != self.element_data.layout_item.get_transform() + || position != self.element_data.layout_item.position; + self.element_data.layout_item.has_new_layout = has_new_layout; + + if dirty { + let result = taffy_tree.layout(node).unwrap(); + self.resolve_box(position, transform, result, z_index); + self.apply_clip(clip_bounds); + self.apply_borders(scale_factor); + + self.element_data.apply_scroll(result); + self.element_data.scroll_state.as_mut().unwrap().mark_old(); + + let text_position = self.computed_box().content_rectangle(); + self.state.set_origin(&text_position.position()); + + self.state.is_layout_dirty = false; + } + + // For manual scroll updates. + if !dirty && self.element_data.scroll_state.map(|scroll_state| scroll_state.is_new()).unwrap_or_default() { + let result = taffy_tree.layout(node).unwrap(); + self.element_data.apply_scroll(result); + self.element_data.scroll_state.as_mut().unwrap().mark_old(); + } + + if has_new_layout { + taffy_tree.mark_seen(node); + } + + self.state.layout( + self.state.last_requested_key.unwrap().known_dimensions(), + self.state.last_requested_key.unwrap().available_space(), + text_context, + true, + ); + + self.state.render_text(self.is_focused(), self.element_data.current_style()); + } + + fn draw( + &mut self, + renderer: &mut RenderList, + _text_context: &mut TextContext, + _pointer: Option, + scale_factor: f64, + ) { + if !self.is_visible() { + return; + } + + self.add_hit_testable(renderer, true, scale_factor); + + let computed_box_transformed = self.computed_box(); + let content_rectangle = computed_box_transformed.content_rectangle(); + + self.draw_borders(renderer, scale_factor); + + let is_scrollable = self.element_data.is_scrollable(); + + let element_data = &self.element_data; + let padding_rectangle = element_data.layout_item.computed_box_transformed.padding_rectangle(); + renderer.push_layer(padding_rectangle.scale(scale_factor)); + + let text_scroll = if is_scrollable { + Some(TextScroll::new( + self.element_data.scroll().map_or(0.0, |s| s.scroll_y()), + self.element_data.layout_item.computed_scroll_track.height, + )) + } else { + None + }; + + if self.state.text_render.as_ref().is_some() { + renderer.draw_text( + self.me.clone().unwrap(), + content_rectangle.scale(scale_factor), + text_scroll, + self.is_focused(), + ); + } + + renderer.pop_layer(); + + self.draw_scrollbar(renderer, scale_factor); + } + + #[cfg(feature = "accesskit")] + fn compute_accessibility_tree( + &mut self, + tree: &mut accesskit::TreeUpdate, + parent_index: Option, + scale_factor: f64, + ) { + let state: &mut TextInputState = &mut self.state; + + if state.editor().try_layout().is_none() { + return; + } + + let current_node_id = accesskit::NodeId(self.element_data.internal_id); + + let mut current_node = accesskit::Node::new(accesskit::Role::TextInput); + let padding_box = + self.element_data.layout_item.computed_box_transformed.padding_rectangle().scale(scale_factor); + + current_node.set_bounds(accesskit::Rect { + x0: padding_box.left() as f64, + y0: padding_box.top() as f64, + x1: padding_box.right() as f64, + y1: padding_box.bottom() as f64, + }); + + self.state.try_accessibility( + tree, + &mut current_node, + || accesskit::NodeId(create_unique_element_id()), + padding_box.x as f64, + padding_box.y as f64, + ); + + if let Some(parent_index) = parent_index { + let parent_node = tree.nodes.get_mut(parent_index).unwrap(); + parent_node.1.push_child(current_node_id); + } + + tree.nodes.push((current_node_id, current_node)); + } + + fn on_event( + &mut self, + message: &CraftMessage, + text_context: &mut TextContext, + event: &mut Event, + _target: Option>>, + ) { + self.state.is_active = true; + + scrollable::on_scroll_events(self, message, event); + + if !event.propagate { + return; + } + + let scroll_y = self.element_data.scroll().map_or(0.0, |s| s.scroll_y() as f64); + + let focused = self.is_focused(); + + if let CraftMessage::ElementMessage(msg) = message + && let Some(msg) = msg.as_any().downcast_ref::() + { + match msg { + TextInputMessage::Copy => { + self.state.copy(text_context); + } + TextInputMessage::Paste => { + if self.disabled { + return; + } + self.state.paste(text_context); + self.mark_dirty(); + //generate_text_changed_event(&mut self.state.editor); + } + TextInputMessage::Cut => { + if self.disabled { + return; + } + self.state.cut(text_context); + self.mark_dirty(); + //generate_text_changed_event(&mut self.state.editor); + } + } + } + + match message { + CraftMessage::KeyboardInputEvent(keyboard_event) if !self.state.editor().is_composing() => { + if self.disabled || !keyboard_event.state.is_down() || !focused { + return; + } + self.state.key_press(text_context, keyboard_event); + } + CraftMessage::PointerButtonDown(pointer_button) => { + if pointer_button.button == Some(PointerButton::Primary) { + self.focus(); + self.state.pointer_down(text_context); + } + } + CraftMessage::PointerButtonUp(pointer_button) => { + if pointer_button.button == Some(PointerButton::Primary) { + self.state.pointer_up(); + } + } + CraftMessage::PointerMovedEvent(pointer_moved) => { + self.state.move_pointer(text_context, pointer_moved, scroll_y); + } + CraftMessage::ImeEvent(Ime::Disabled) => { + self.state.disable_ime(text_context); + } + CraftMessage::ImeEvent(Ime::Commit(text)) => { + self.state.insert_or_replace_selection(text_context, text); + //generate_text_changed_event(&mut self.state.editor); + } + CraftMessage::ImeEvent(Ime::Preedit(text, cursor)) => { + self.state.ime_pre_edit(text_context, text, cursor); + } + _ => {} + } + + if self.state.is_layout_dirty { + self.mark_dirty(); + } + } + + fn apply_clip(&mut self, clip_bounds: Option) { + resolve_clip_for_scrollable(self, clip_bounds); + } + + fn get_default_style() -> Style + where + Self: Sized, + { + let mut style = Style::default(); + + style.set_display(Display::Block); + + const BORDER_COLOR: Color = Color::from_rgb8(199, 199, 206); + style.set_border_color(TrblRectangle::new_all(BORDER_COLOR)); + style.set_border_width(TrblRectangle::new_all(Unit::Px(1.0))); + style.set_border_radius([(5.0, 5.0); 4]); + + let padding = Unit::Px(4.0); + style.set_padding(TrblRectangle::new_all(padding)); + + style + } + + fn scale_factor(&mut self, scale_factor: f64) { + self.state.set_scale_factor(scale_factor); + self.mark_dirty(); + } +} + +impl TextInput { + /// Whether the text input will update the editor every update with the user provided text. + /// NOTE: The editor will always use the user provided text on initialization. + pub fn use_text_value_on_update(mut self, use_initial_text_value: bool) -> Self { + self.use_text_value_on_update = use_initial_text_value; + self + } + + pub fn disable(&mut self) -> &mut Self { + self.disabled = true; + self + } + + pub fn get_disabled(&mut self) -> bool { + self.disabled + } + + pub fn get_text(&self) -> &str { + &self.state.editor().raw_text() + } + + /// Set the text. + /// + /// Updates the text content immediately. Mark layout and render caches as dirty. Layout and + /// render caches will be computed in the next layout/render pass. + pub fn text(&mut self, text: &str) -> &mut Self { + self.state.set_text(text); + self.mark_dirty(); + self + } + + pub fn ranged_styles(&mut self, ranged_styles: RangedStyles) -> &mut Self { + self.state.set_ranged_styles(ranged_styles); + self.mark_dirty(); + self + } +} + +impl TextData for TextInput { + fn get_text_renderer(&self) -> Option<&TextRender> { + self.state.text_render.as_ref() + } +} + +fn parley_box_to_rect(bounding_box: BoundingBox) -> Rectangle { + Rectangle::new( + bounding_box.x0 as f32, + bounding_box.y0 as f32, + bounding_box.width() as f32, + bounding_box.height() as f32, + ) +} + +impl Element for TextInput { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} \ No newline at end of file diff --git a/crates/craft_retained/src/elements/text_input/text_input_state.rs b/crates/craft_retained/src/elements/text_input/text_input_state.rs new file mode 100644 index 00000000..229c0c16 --- /dev/null +++ b/crates/craft_retained/src/elements/text_input/text_input_state.rs @@ -0,0 +1,676 @@ +use crate::layout::layout_context::TextHashKey; +use craft_primitives::geometry::{Point, Rectangle}; +use craft_renderer::text_renderer_data::TextRender; +use std::collections::HashMap; +use std::ops::Range; +use taffy::{AvailableSpace, NodeId}; + +use crate::text::parley_editor::{PlainEditor, PlainEditorDriver}; + +use ui_events::keyboard::{Key, KeyboardEvent, Modifiers, NamedKey}; + +#[cfg(feature = "accesskit")] +use accesskit::{Node, TreeUpdate}; +#[cfg(not(target_arch = "wasm32"))] +use std::time::{Duration, Instant}; +#[cfg(target_arch = "wasm32")] +use web_time::{Duration, Instant}; + +use crate::app::{LAYOUT_DIRTY, TAFFY_TREE}; +use crate::elements::core::ElementInternals; +use crate::elements::text_input::parley_box_to_rect; +use crate::elements::TextInput; +use crate::style::{Style, TextStyleProperty}; +use crate::text::text_context::TextContext; +use crate::text::{text_render_data, RangedStyles}; +use parley::{Affinity, ContentWidths, Cursor, Selection}; +use peniko::Color; +use ui_events::pointer::PointerUpdate; +use winit::dpi; +use crate::request_layout; + +#[derive(Clone)] +pub struct TextInputState { + origin: Point, + + taffy_node: Option, + pub is_active: bool, + #[allow(dead_code)] + pub(crate) ime_state: ImeState, + editor: PlainEditor, + + cache: HashMap>, + + // The current key used for laying out the text input. + current_layout_key: Option, + + current_render_key: Option, + content_widths: Option, + + // The most recently requested key for laying out the text input. + pub(crate) last_requested_key: Option, + pub(crate) text_render: Option, + scale_factor: f64, + + last_click_time: Option, + click_count: u32, + pointer_down: bool, + cursor_pos: Point, + cursor_visible: bool, + modifiers: Option, + start_time: Option, + blink_period: Duration, + + /// True if the node needs laid-out. + pub is_layout_dirty: bool, +} + +impl Default for TextInputState { + fn default() -> Self { + let default_style = TextInput::get_default_style(); + let mut editor = PlainEditor::new(default_style.font_size()); + editor.set_scale(1.0f32); + let style_set = editor.edit_styles(); + default_style.add_styles_to_style_set(style_set); + Self { + origin: Default::default(), + taffy_node: None, + ime_state: ImeState::default(), + is_active: false, + editor, + cache: Default::default(), + current_layout_key: None, + current_render_key: None, + content_widths: None, + last_requested_key: None, + text_render: None, + scale_factor: 1.0, + last_click_time: None, + click_count: 0, + pointer_down: false, + cursor_pos: Point::default(), + cursor_visible: false, + modifiers: None, + start_time: None, + blink_period: Default::default(), + is_layout_dirty: true, + } + } +} + +#[derive(Clone, Default, Debug, Copy)] +pub(crate) struct ImeState { + #[allow(dead_code)] + pub is_ime_active: bool, +} + +impl TextInputState { + /// Returns the last know positon of the cursor relative to the origin. + /// + /// The cursor is assumed to start at (0.0, 0.0). The cursor_pos may return points + /// outside the text input. + pub fn cursor_pos(&self) -> Point { + self.cursor_pos + } + + /// Set the mouse positon. + /// + /// The point should be relative to the top left of the window. + pub fn move_pointer(&mut self, text_context: &mut TextContext, pointer_moved: &PointerUpdate, scroll_y: f64) { + let prev_pos = self.cursor_pos(); + // NOTE: Cursor position should be relative to the top left of the text box. + let cursor_pos = pointer_moved.current.logical_point(); + let cursor_pos: Point = (cursor_pos - self.origin).to_point(); + let mut cursor_pos = Point::new(cursor_pos.x * self.scale_factor, cursor_pos.y * self.scale_factor); + cursor_pos.y += scroll_y; + self.cursor_pos = cursor_pos; + // macOS seems to generate a spurious move after selecting word? + if self.is_pointer_down() && prev_pos != self.cursor_pos() && !self.editor.is_composing() { + self.reset_blink(); + let cursor_pos = self.cursor_pos(); + self.driver(text_context).extend_selection_to_point(cursor_pos.x as f32, cursor_pos.y as f32); + request_layout(); + } + } + + /// Set the origin of the text input state. + /// + /// The point should be relative to the top left of the window. + pub fn set_origin(&mut self, origin: &Point) { + let diff = *origin - self.origin; + self.cursor_pos += diff; + self.origin = *origin; + } + + pub fn measure( + &mut self, + known_dimensions: taffy::Size>, + available_space: taffy::Size, + text_context: &mut TextContext, + ) -> taffy::Size { + let key = TextHashKey::new(known_dimensions, available_space); + + self.last_requested_key = Some(key); + + self.layout(known_dimensions, available_space, text_context, false) + } + + pub fn clear_cache(&mut self) { + self.is_layout_dirty = true; + self.cache.clear(); + self.current_layout_key = None; + self.last_requested_key = None; + self.current_render_key = None; + self.text_render = None; + self.content_widths = None; + + if let Some(id) = self.taffy_node { + TAFFY_TREE.with_borrow_mut(|taffy_tree| { + taffy_tree.mark_dirty(id).expect("Failed to mark node dirty"); + }) + } + } + + pub fn layout( + &mut self, + known_dimensions: taffy::Size>, + available_space: taffy::Size, + text_context: &mut TextContext, + last_pass: bool, + ) -> taffy::Size { + let key = TextHashKey::new(known_dimensions, available_space); + + if let Some(value) = self.cache.get(&key) { + if last_pass { + if self.current_layout_key == Some(key) { + if self.current_render_key != self.current_layout_key { + self.current_render_key = self.current_layout_key; + + let layout = self.editor.try_layout().unwrap(); + self.text_render = Some(text_render_data::from_editor(layout)); + } + return *value; + } + } else { + return *value; + } + } + + if self.editor.try_layout().is_none() || self.is_layout_dirty || self.content_widths.is_none() { + self.editor.set_width(None); + self.editor.refresh_layout(&mut text_context.font_context, &mut text_context.layout_context); + self.content_widths = Some(self.editor.try_layout().unwrap().calculate_content_widths()); + } + + let content_widths = self.content_widths.unwrap(); + let width_constraint: Option = known_dimensions + .width + .or(match available_space.width { + AvailableSpace::MinContent => Some(content_widths.min), + AvailableSpace::MaxContent => Some(content_widths.max), + AvailableSpace::Definite(width) => Some(width), + }) + .map(|width| { + let width: f32 = dpi::PhysicalUnit::from_logical::(width, self.scale_factor).0; + // Taffy may give a min width > max_width. + // Min-width is preserved in this scenario to ensure text is readable. + width.clamp(content_widths.min, content_widths.max.max(content_widths.min)) + }); + + let _height_constraint: Option = known_dimensions + .height + .or(match available_space.height { + AvailableSpace::MinContent => None, + AvailableSpace::MaxContent => None, + AvailableSpace::Definite(height) => Some(height), + }) + .map(|height| dpi::PhysicalUnit::from_logical::(height, self.scale_factor).0); + + self.editor.set_width(width_constraint); + self.editor.refresh_layout(&mut text_context.font_context, &mut text_context.layout_context); + let layout = self.editor.try_layout().unwrap(); + + if last_pass { + self.current_render_key = self.current_layout_key; + self.text_render = Some(text_render_data::from_editor(layout)); + } + + let logical_width = dpi::LogicalUnit::from_physical::(layout.width(), self.scale_factor).0; + let logical_height = dpi::LogicalUnit::from_physical::(layout.height(), self.scale_factor).0; + + let size = taffy::Size { + width: logical_width, + height: logical_height, + }; + + self.cache.insert(key, size); + self.current_layout_key = Some(key); + size + } + + pub fn taffy_node(&mut self, taffy_node: Option) { + self.taffy_node = taffy_node; + } + + #[allow(dead_code)] + pub fn get_cursor_link(&self, cursor_pos: Point, element: &TextInput) -> Option { + if let Some(ranged_styles) = &element.ranged_styles { + let layout = self.editor.try_layout().unwrap(); + for (range, style) in ranged_styles.styles.iter() { + if let TextStyleProperty::Link(link) = style { + let anchor = Cursor::from_byte_index(layout, range.start, Affinity::Downstream); + let focus = Cursor::from_byte_index(layout, range.end, Affinity::Downstream); + let selection = Selection::new(anchor, focus); + let link_rects = selection.geometry(layout); + for link_rect in link_rects { + if parley_box_to_rect(link_rect.0).contains(&cursor_pos) { + return Some(link.clone()); + } + } + } + } + } + None + } + + /// Resets the cursor blink. + pub fn reset_blink(&mut self) { + self.start_time = Some(Instant::now()); + // TODO: for real world use, this should be reading from the system settings + self.blink_period = Duration::from_millis(500); + self.cursor_visible = true; + } + + #[allow(dead_code)] + pub fn disable_blink(&mut self) { + self.start_time = None; + } + + #[allow(dead_code)] + pub fn next_blink_time(&self) -> Option { + self.start_time.map(|start_time| { + let phase = Instant::now().duration_since(start_time); + + start_time + + Duration::from_nanos( + ((phase.as_nanos() / self.blink_period.as_nanos() + 1) * self.blink_period.as_nanos()) as u64, + ) + }) + } + + #[allow(dead_code)] + pub fn cursor_blink(&mut self) { + self.cursor_visible = self.start_time.is_some_and(|start_time| { + let elapsed = Instant::now().duration_since(start_time); + (elapsed.as_millis() / self.blink_period.as_millis()) % 2 == 0 + }); + } + + fn driver<'a>(&'a mut self, text_context: &'a mut TextContext) -> PlainEditorDriver<'a> { + self.editor.driver(&mut text_context.font_context, &mut text_context.layout_context) + } + + /// Set's the scale factor. + pub fn set_scale_factor(&mut self, scale_factor: f64) { + self.scale_factor = scale_factor; + self.editor.set_scale(scale_factor as f32); + self.clear_cache(); + } + + pub fn pointer_down(&mut self, text_context: &mut TextContext) { + self.cursor_visible = true; + self.pointer_down = true; + self.reset_blink(); + if !self.editor.is_composing() { + let now = Instant::now(); + if let Some(last) = self.last_click_time.take() { + if now.duration_since(last).as_secs_f64() < 0.25 { + self.click_count = (self.click_count + 1) % 4; + } else { + self.click_count = 1; + } + } else { + self.click_count = 1; + } + self.last_click_time = Some(now); + let click_count = self.click_count; + let cursor_pos = self.cursor_pos; + let cursor_x = cursor_pos.x as f32; + let cursor_y = cursor_pos.y as f32; + + if click_count == 1 { + /*if let Some(_link) = self.get_cursor_link(cursor_pos, element) { + // TODO generate event + return; + }*/ + } + + let mut drv = self.driver(text_context); + + match click_count { + 2 => drv.select_word_at_point(cursor_x, cursor_y), + 3 => drv.select_line_at_point(cursor_x, cursor_y), + _ => drv.move_to_point(cursor_x, cursor_y), + } + } + } + + pub fn pointer_up(&mut self) { + self.pointer_down = false; + self.reset_blink(); + } + + /// Insert at cursor, or replace selection. + /// + /// This requires a relayout. + pub fn insert_or_replace_selection(&mut self, text_context: &mut TextContext, text: &str) { + self.driver(text_context).insert_or_replace_selection(text); + self.clear_cache(); + } + + pub fn is_pointer_down(&self) -> bool { + self.pointer_down + } + + pub fn key_press(&mut self, text_context: &mut TextContext, keyboard_event: &KeyboardEvent) { + // TODO: self.reset_blink(); + + self.modifiers = Some(keyboard_event.modifiers); + + #[allow(unused)] + let (shift, action_mod) = self + .modifiers + .map(|mods| (mods.shift(), if cfg!(target_os = "macos") { mods.meta() } else { mods.ctrl() })) + .unwrap_or_default(); + + let mut driver = self.driver(text_context); + + match &keyboard_event.key { + Key::Character(c) if action_mod && matches!(c.as_str(), "c" | "x" | "v") => { + match c.to_lowercase().as_str() { + "c" => copy(&mut driver), + "x" => { + cut(&mut driver); + self.clear_cache(); + //generate_text_changed_event(&mut self.editor); + } + "v" => { + paste(&mut driver); + self.clear_cache(); + //generate_text_changed_event(&mut self.editor); + } + _ => (), + } + } + Key::Character(c) if action_mod && matches!(c.to_lowercase().as_str(), "a") => { + if shift { + driver.collapse_selection(); + } else { + driver.select_all(); + } + } + Key::Named(NamedKey::ArrowLeft) => { + if action_mod { + if shift { + driver.select_word_left(); + } else { + driver.move_word_left(); + } + } else if shift { + driver.select_left(); + } else { + driver.move_left(); + } + } + Key::Named(NamedKey::ArrowRight) => { + if action_mod { + if shift { + driver.select_word_right(); + } else { + driver.move_word_right(); + } + } else if shift { + driver.select_right(); + } else { + driver.move_right(); + } + } + Key::Named(NamedKey::ArrowUp) => { + if shift { + driver.select_up(); + } else { + driver.move_up(); + } + } + Key::Named(NamedKey::ArrowDown) => { + if shift { + driver.select_down(); + } else { + driver.move_down(); + } + } + Key::Named(NamedKey::Home) => { + if action_mod { + if shift { + driver.select_to_text_start(); + } else { + driver.move_to_text_start(); + } + } else if shift { + driver.select_to_line_start(); + } else { + driver.move_to_line_start(); + } + } + Key::Named(NamedKey::End) => { + let mut drv = self.driver(text_context); + + if action_mod { + if shift { + drv.select_to_text_end(); + } else { + drv.move_to_text_end(); + } + } else if shift { + drv.select_to_line_end(); + } else { + drv.move_to_line_end(); + } + } + Key::Named(NamedKey::Delete) => { + if action_mod { + driver.delete_word(); + self.clear_cache(); + } else { + driver.delete(); + self.clear_cache(); + } + //generate_text_changed_event(&mut self.state.editor); + } + Key::Named(NamedKey::Backspace) => { + if action_mod { + driver.backdelete_word(); + self.clear_cache(); + } else { + driver.backdelete(); + self.clear_cache(); + } + //generate_text_changed_event(&mut self.state.editor); + } + Key::Named(NamedKey::Enter) => { + driver.insert_or_replace_selection("\n"); + self.clear_cache(); + //generate_text_changed_event(&mut self.state.editor); + } + Key::Character(s) => { + driver.insert_or_replace_selection(s); + self.clear_cache(); + //generate_text_changed_event(&mut self.state.editor); + } + _ => (), + } + } + + pub fn copy(&mut self, text_context: &mut TextContext) { + copy(&mut self.driver(text_context)); + } + + pub fn paste(&mut self, text_context: &mut TextContext) { + paste(&mut self.driver(text_context)); + } + + pub fn cut(&mut self, text_context: &mut TextContext) { + cut(&mut self.driver(text_context)); + self.clear_cache(); + } + + pub fn ime_pre_edit(&mut self, text_context: &mut TextContext, text: &String, cursor: &Option<(usize, usize)>) { + if text.is_empty() { + self.driver(text_context).clear_compose(); + } else { + self.driver(text_context).set_compose(text, *cursor); + } + self.clear_cache(); + } + + pub fn disable_ime(&mut self, text_context: &mut TextContext) { + self.driver(text_context).clear_compose(); + self.clear_cache(); + } + + pub fn editor(&self) -> &PlainEditor { + &self.editor + } + + pub fn set_text(&mut self, text: &str) { + self.editor.set_text(text); + self.clear_cache(); + } + + pub fn set_ranged_styles(&mut self, ranged_styles: RangedStyles) { + self.editor.set_ranged_styles(ranged_styles); + self.clear_cache(); + } + + pub fn render_text(&mut self, focused: bool, style: &Style) { + let backgrounds: Vec<(Range, Color)> = self + .editor() + .ranged_styles + .styles + .iter() + .filter_map(|(range, style)| { + if let TextStyleProperty::BackgroundColor(color) = style { + Some((range.clone(), *color)) + } else { + None + } + }) + .collect(); + + let layout = self.editor.try_layout().unwrap(); + let backgrounds: Vec<(Selection, Color)> = backgrounds + .iter() + .map(|(range, color)| { + ( + Selection::new( + Cursor::from_byte_index(layout, range.start, Affinity::Downstream), + Cursor::from_byte_index(layout, range.end, Affinity::Downstream), + ), + *color, + ) + }) + .collect(); + let text_renderer = self.text_render.as_mut().unwrap(); + for line in text_renderer.lines.iter_mut() { + line.backgrounds.clear(); + } + for (selection, color) in backgrounds.iter() { + selection.geometry_with(layout, |rect, line| { + text_renderer.lines[line].backgrounds.push(( + Rectangle::new(rect.x0 as f32, rect.y0 as f32, rect.width() as f32, rect.height() as f32), + *color, + )); + }); + } + + for line in text_renderer.lines.iter_mut() { + line.selections.clear(); + } + self.editor.selection_geometry_with(|rect, line| { + text_renderer.lines[line].selections.push((parley_box_to_rect(rect), style.selection_color())); + }); + + if focused { + let color = style.cursor_color().unwrap_or(style.color()); + text_renderer.cursor = self.editor.cursor_geometry(1.0).map(|r| (parley_box_to_rect(r), color)); + } else { + text_renderer.cursor = None; + } + } + + #[cfg(feature = "accesskit")] + pub fn try_accessibility( + &mut self, + tree: &mut TreeUpdate, + current_node: &mut Node, + next_node_id: impl FnMut() -> accesskit::NodeId, + x_offset: f64, + y_offset: f64, + ) { + self.editor.try_accessibility(tree, current_node, next_node_id, x_offset, y_offset); + } +} + +#[cfg(all( + any(target_os = "windows", target_os = "macos", target_os = "linux"), + feature = "clipboard" +))] +fn copy(drv: &mut PlainEditorDriver) { + use clipboard_rs::{Clipboard, ClipboardContext}; + if let Some(text) = drv.editor.selected_text() { + let cb = ClipboardContext::new().unwrap(); + cb.set_text(text.to_owned()).ok(); + } +} + +#[cfg(not(all( + any(target_os = "windows", target_os = "macos", target_os = "linux"), + feature = "clipboard" +)))] +fn copy(_drv: &mut PlainEditorDriver) {} + +#[cfg(all( + any(target_os = "windows", target_os = "macos", target_os = "linux"), + feature = "clipboard" +))] +fn paste(drv: &mut PlainEditorDriver) { + use clipboard_rs::{Clipboard, ClipboardContext}; + let cb = ClipboardContext::new().unwrap(); + let text = cb.get_text().unwrap_or_default(); + drv.insert_or_replace_selection(&text); +} + +#[cfg(not(all( + any(target_os = "windows", target_os = "macos", target_os = "linux"), + feature = "clipboard" +)))] +fn paste(_drv: &mut PlainEditorDriver) {} + +#[cfg(all( + any(target_os = "windows", target_os = "macos", target_os = "linux"), + feature = "clipboard" +))] +fn cut(drv: &mut PlainEditorDriver) { + use clipboard_rs::{Clipboard, ClipboardContext}; + if let Some(text) = drv.editor.selected_text() { + let cb = ClipboardContext::new().unwrap(); + cb.set_text(text.to_owned()).ok(); + drv.delete_selection(); + } +} + +#[cfg(not(all( + any(target_os = "windows", target_os = "macos", target_os = "linux"), + feature = "clipboard" +)))] +fn cut(_drv: &mut PlainEditorDriver) {} diff --git a/crates/craft_retained/src/events/event_dispatch.rs b/crates/craft_retained/src/events/event_dispatch.rs index 9ffddcf0..8bee51c7 100644 --- a/crates/craft_retained/src/events/event_dispatch.rs +++ b/crates/craft_retained/src/events/event_dispatch.rs @@ -158,8 +158,8 @@ impl EventDispatcher { target_scratch: &mut Vec>>, ) { let mut _focus = FocusAction::None; - let span = span!(Level::INFO, "dispatch event"); - let _enter = span.enter(); + /*let span = span!(Level::INFO, "dispatch event"); + let _enter = span.enter();*/ /*for (node, _) in &render_list.targets { println!("target: {}", node); diff --git a/crates/craft_retained/src/layout/layout_item.rs b/crates/craft_retained/src/layout/layout_item.rs index d00bc59d..c5894b3a 100644 --- a/crates/craft_retained/src/layout/layout_item.rs +++ b/crates/craft_retained/src/layout/layout_item.rs @@ -5,7 +5,7 @@ use craft_primitives::geometry::{Border, ElementBox, Margin, Padding, Point, Rec use craft_renderer::{Brush, RenderList}; use kurbo::{Affine, BezPath, Shape, Vec2}; use peniko::Color; -use taffy::{NodeId, Position, TaffyTree}; +use taffy::{Layout, NodeId, Position, TaffyTree}; impl CssComputedBorder { pub(crate) fn scale(&mut self, scale_factor: f64) { @@ -67,6 +67,7 @@ pub struct LayoutItem { /// True if the layout is new. pub has_new_layout: bool, transform: Affine, + pub position: Point, } impl LayoutItem { @@ -142,6 +143,7 @@ impl LayoutItem { }; self.computed_box_transformed = self.computed_box.transform(scroll_transform); self.transform = scroll_transform; + self.position = relative_position; } pub fn apply_borders( diff --git a/crates/craft_retained/src/lib.rs b/crates/craft_retained/src/lib.rs index 4ea52162..59478ae6 100644 --- a/crates/craft_retained/src/lib.rs +++ b/crates/craft_retained/src/lib.rs @@ -11,6 +11,8 @@ mod tests; pub mod text; pub mod document; +pub use crate::app::request_layout; + mod app; pub use craft_primitives::geometry as geometry; pub mod layout; diff --git a/crates/craft_retained/src/text/parley_editor.rs b/crates/craft_retained/src/text/parley_editor.rs index c2c1b8d1..30270721 100644 --- a/crates/craft_retained/src/text/parley_editor.rs +++ b/crates/craft_retained/src/text/parley_editor.rs @@ -19,6 +19,7 @@ use parley::layout::LayoutAccessibility; use accesskit::{Node, NodeId, TreeUpdate}; use crate::text::RangedStyles; use craft_primitives::ColorBrush; +use crate::request_layout; /// Opaque representation of a generation. /// @@ -456,6 +457,7 @@ impl PlainEditorDriver<'_> self.refresh_layout(); self.editor .set_selection(Selection::from_point(&self.editor.layout, x, y)); + request_layout(); } /// Move the cursor to a byte index. @@ -661,6 +663,7 @@ impl PlainEditorDriver<'_> .selection .next_visual_word(&self.editor.layout, true), ); + request_layout(); } /// Select the word at the point. @@ -668,6 +671,7 @@ impl PlainEditorDriver<'_> self.refresh_layout(); self.editor .set_selection(Selection::word_from_point(&self.editor.layout, x, y)); + request_layout(); } /// Select the physical line at the point. @@ -675,6 +679,7 @@ impl PlainEditorDriver<'_> self.refresh_layout(); let line = Selection::line_from_point(&self.editor.layout, x, y); self.editor.set_selection(line); + request_layout(); } /// Move the selection focus point to the cluster boundary closest to point. diff --git a/examples/jsframeworkbench/main.rs b/examples/jsframeworkbench/main.rs index 1bbd0249..816c418e 100644 --- a/examples/jsframeworkbench/main.rs +++ b/examples/jsframeworkbench/main.rs @@ -9,7 +9,7 @@ use rand::rng; use rand::rngs::ThreadRng; use rand::seq::IndexedRandom; use std::cell::RefCell; -use std::rc::Rc; +use std::rc::{Rc, Weak}; use craft_retained::palette::css::WHITE; const ADJECTIVES: &[&str] = &[ @@ -81,6 +81,7 @@ impl State { self.remove_all_rows(); self.store.clear(); self.rows.clear(); + self.rows.shrink_to_fit(); self.store.run(); self.append_rows(); self.select(None); @@ -90,6 +91,7 @@ impl State { self.remove_all_rows(); self.store.clear(); self.rows.clear(); + self.rows.shrink_to_fit(); self.store.run_lots(); self.append_rows(); self.select(None); @@ -248,6 +250,7 @@ impl Store { pub fn clear(&mut self) { self.data.clear(); + self.data.shrink_to_fit(); self.selected = None; } @@ -279,25 +282,27 @@ impl Store { #[allow(unused)] #[cfg(not(target_os = "android"))] fn main() { - util::setup_logging(); - let root = build_root(); + //util::setup_logging(); + + let data_list = build_data_list(); + let state = Rc::new(RefCell::new(State::new(data_list.clone()))); + + let root = build_root(state.clone()); use craft_retained::CraftOptions; craft_retained::craft_main(root, CraftOptions::basic("jsframeworkbench")); } -fn build_root() -> Rc> { +fn build_root(state: Rc>) -> Rc> { let root = Container::new(); - let body = build_body(); + let body = build_body(state); root.borrow_mut().push(body); root } -fn build_body() -> Rc> { +fn build_body(state: Rc>) -> Rc> { let body = Container::new(); - let data_list = build_data_list(); - let state = Rc::new(RefCell::new(State::new(data_list.clone()))); - let buttons = build_buttons(state.clone()); + let buttons = build_buttons(Rc::downgrade(&state)); body.borrow_mut() .overflow(Overflow::Visible, Overflow::Scroll) @@ -331,18 +336,20 @@ fn build_body() -> Rc> { .width(Unit::Percentage(100.0)) .push(buttons); - body.borrow_mut().push(header).push(data_list); + body.borrow_mut().push(header).push(state.borrow().element.clone()); body } fn build_data_list() -> Rc> { let data_list = Container::new(); - data_list.borrow_mut().flex_direction(FlexDirection::Column); + data_list.borrow_mut() + .flex_direction(FlexDirection::Column) + .width(Unit::Percentage(100.0)); data_list } -fn build_buttons(state: Rc>) -> Rc> { +fn build_buttons(state: Weak>) -> Rc> { let buttons = Container::new(); buttons.borrow_mut() .flex_direction(FlexDirection::Column) @@ -354,26 +361,26 @@ fn build_buttons(state: Rc>) -> Rc> { let state1 = state.clone(); let btn_create_1k = build_button("Create 1,000 rows", move |_, _| { - state1.borrow_mut().run(); + state1.upgrade().unwrap().borrow_mut().run(); }); let state2 = state.clone(); let btn_create_10k = build_button("Create 10,000 rows", move |_, _| { - state2.borrow_mut().run_lots(); + state2.upgrade().unwrap().borrow_mut().run_lots(); }); let state3 = state.clone(); - let btn_append_1k = build_button("Append 1,000 rows", move |_, _| state3.borrow_mut().add()); + let btn_append_1k = build_button("Append 1,000 rows", move |_, _| state3.upgrade().unwrap().borrow_mut().add()); let state4 = state.clone(); - let btn_update_10th_row = build_button("Update every 10th row", move |_, _| state4.borrow_mut().update()); + let btn_update_10th_row = build_button("Update every 10th row", move |_, _| state4.upgrade().unwrap().borrow_mut().update()); let state5 = state.clone(); - let btn_clear = build_button("Clear", move |_, _| state5.borrow_mut().clear()); + let btn_clear = build_button("Clear", move |_, _| state5.upgrade().unwrap().borrow_mut().clear()); let state6 = state.clone(); - let btn_swap = build_button("Swap Rows", move |_, _| state6.borrow_mut().swap_rows()); - + let btn_swap = build_button("Swap Rows", move |_, _| state6.upgrade().unwrap().borrow_mut().swap_rows()); + buttons .borrow_mut() .push(btn_create_1k) From 85cbbfc4b1c51405bba7da2cdb33834efb14cd7e Mon Sep 17 00:00:00 2001 From: NoahR02 Date: Wed, 24 Dec 2025 18:31:09 -0500 Subject: [PATCH 006/143] Allow elements and users to queue an event Once it is queued and the current event ends, it will dequeue and run the event. --- crates/craft_retained/src/app.rs | 27 ++++++++++++++++--- .../src/elements/core/element_internals.rs | 2 +- crates/craft_retained/src/elements/element.rs | 10 ++++++- .../src/elements/element_data.rs | 4 ++- .../craft_retained/src/elements/scrollable.rs | 1 - .../src/elements/slider/slider.rs | 22 +++++++++------ .../src/elements/text_input/mod.rs | 1 - .../elements/text_input/text_input_state.rs | 2 +- .../src/events/event_dispatch.rs | 27 ++++++++++--------- crates/craft_retained/src/events/helpers.rs | 8 +++++- crates/craft_retained/src/events/mod.rs | 2 +- .../craft_retained/src/layout/layout_item.rs | 2 +- 12 files changed, 75 insertions(+), 33 deletions(-) diff --git a/crates/craft_retained/src/app.rs b/crates/craft_retained/src/app.rs index b228028c..ce9b1fd6 100644 --- a/crates/craft_retained/src/app.rs +++ b/crates/craft_retained/src/app.rs @@ -1,12 +1,12 @@ use crate::elements::ElementIdMap; -use crate::events::EventDispatcher; +use crate::events::{Event, EventDispatcher}; use crate::events::internal::InternalMessage; use crate::events::{CraftMessage}; use crate::layout::layout_context::measure_content; use crate::style::{Display, Unit, Wrap}; use crate::text::text_context::TextContext; use crate::{RendererBox, WindowContext}; -use craft_logging::{info, span, Level}; +use craft_logging::info; use craft_primitives::geometry::Rectangle; use craft_resource_manager::{ResourceIdentifier, ResourceManager}; use craft_runtime::CraftRuntimeHandle; @@ -63,11 +63,32 @@ thread_local! { pub(crate) static IN_PROGRESS_RESOURCES: RefCell> = RefCell::new(VecDeque::new()); pub(crate) static FOCUS: RefCell>>> = RefCell::new(None); pub(crate) static LAYOUT_DIRTY: RefCell = RefCell::new(true); + /// An event queue that users or elements can manipulate. Cleared at the start and end of every event dispatch. + static EVENT_DISPATCH_QUEUE: RefCell> = RefCell::new(VecDeque::with_capacity(10)); +} + +/// Enqueues an event at the back of the dispatch queue. +/// +/// This does **not** invoke any element `on_event` handlers. +/// Only user-registered event callbacks will be dispatched. +pub fn queue_event(event: Event, message: CraftMessage) { + EVENT_DISPATCH_QUEUE.with_borrow_mut(|event_queue| { + return event_queue.push_back((event, message)); + }); +} + +/// Pops from the front of the event dispatch queue and returns the result. +pub(crate) fn dequeue_event() -> Option<(Event, CraftMessage)> { + let event = EVENT_DISPATCH_QUEUE.with_borrow_mut(|event_queue| { + return event_queue.pop_front(); + }); + + event } pub struct App { pub(crate) event_dispatcher: EventDispatcher, - pub(crate) root: Rc>, + pub(crate) root: Rc>, /// A winit window. This is only valid between resume and pause. pub window: Option>, /// The text context is used to manage fonts and text rendering. It is only valid between resume and pause. diff --git a/crates/craft_retained/src/elements/core/element_internals.rs b/crates/craft_retained/src/elements/core/element_internals.rs index a35b8396..ab648a30 100644 --- a/crates/craft_retained/src/elements/core/element_internals.rs +++ b/crates/craft_retained/src/elements/core/element_internals.rs @@ -18,7 +18,7 @@ use crate::elements::Element; #[cfg(feature = "accesskit")] use accesskit::{Action, Role}; use craft_primitives::geometry::borders::CssRoundedRect; -use crate::app::{LAYOUT_DIRTY, TAFFY_TREE}; +use crate::app::TAFFY_TREE; use crate::request_layout; /// Internal element methods that should typically be ignored by users. Public for custom elements. diff --git a/crates/craft_retained/src/elements/element.rs b/crates/craft_retained/src/elements/element.rs index 9c9b3b2a..33e1d11c 100644 --- a/crates/craft_retained/src/elements/element.rs +++ b/crates/craft_retained/src/elements/element.rs @@ -1,6 +1,6 @@ use crate::app::{DOCUMENTS, FOCUS, TAFFY_TREE}; use crate::elements::core::ElementData; -use crate::events::{KeyboardInputHandler, PointerCaptureHandler, PointerEnterHandler, PointerEventHandler, PointerLeaveHandler, PointerUpdateHandler}; +use crate::events::{KeyboardInputHandler, PointerCaptureHandler, PointerEnterHandler, PointerEventHandler, PointerLeaveHandler, PointerUpdateHandler, SliderValueChangedHandler}; use crate::layout::layout_context::LayoutContext; use crate::style::{ AlignItems, Display, FlexDirection, FontFamily, FontStyle, JustifyContent, ScrollbarColor, Style, Underline, Unit, @@ -141,6 +141,14 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { { self.element_data_mut().on_pointer_enter.push(on_pointer_enter); self + } + + fn on_slider_value_changed(&mut self, on_slider_value_changed: SliderValueChangedHandler) -> &mut Self + where + Self: Sized, + { + self.element_data_mut().on_slider_value_changed.push(on_slider_value_changed); + self } fn on_pointer_leave(&mut self, on_pointer_leave: PointerLeaveHandler) -> &mut Self diff --git a/crates/craft_retained/src/elements/element_data.rs b/crates/craft_retained/src/elements/element_data.rs index 35cbd6fa..2f7852d0 100644 --- a/crates/craft_retained/src/elements/element_data.rs +++ b/crates/craft_retained/src/elements/element_data.rs @@ -3,7 +3,7 @@ use crate::elements::element_id::create_unique_element_id; use crate::elements::element_states::ElementState; use crate::elements::scroll_state::ScrollState; use crate::elements::Element; -use crate::events::{KeyboardInputHandler, PointerCaptureHandler, PointerEnterHandler, PointerEventHandler, PointerLeaveHandler, PointerUpdateHandler}; +use crate::events::{KeyboardInputHandler, PointerCaptureHandler, PointerEnterHandler, PointerEventHandler, PointerLeaveHandler, PointerUpdateHandler, SliderValueChangedHandler}; use crate::layout::layout_item::LayoutItem; use crate::style::Style; use craft_primitives::geometry::{Rectangle, Size}; @@ -45,6 +45,7 @@ pub struct ElementData { pub(crate) animations: Option>, pub(crate) parent: Option>>, + pub on_slider_value_changed: Vec, pub on_pointer_enter: Vec, pub on_pointer_leave: Vec, pub(crate) me: Option>>, @@ -146,6 +147,7 @@ impl Default for ElementData { internal_id: create_unique_element_id(), animations: None, parent: None, + on_slider_value_changed: vec![], on_pointer_enter: vec![], on_pointer_leave: vec![], me: None, diff --git a/crates/craft_retained/src/elements/scrollable.rs b/crates/craft_retained/src/elements/scrollable.rs index 3afbd9d1..ad468c27 100644 --- a/crates/craft_retained/src/elements/scrollable.rs +++ b/crates/craft_retained/src/elements/scrollable.rs @@ -3,7 +3,6 @@ use crate::events::{CraftMessage, Event}; use kurbo::Point; use ui_events::pointer::{PointerId, PointerType}; use ui_events::ScrollDelta; -use crate::app::LAYOUT_DIRTY; use crate::request_layout; #[allow(clippy::too_many_arguments)] diff --git a/crates/craft_retained/src/elements/slider/slider.rs b/crates/craft_retained/src/elements/slider/slider.rs index da56b095..9f63c0e9 100644 --- a/crates/craft_retained/src/elements/slider/slider.rs +++ b/crates/craft_retained/src/elements/slider/slider.rs @@ -1,9 +1,9 @@ -use crate::app::ELEMENTS; +use crate::app::{queue_event, ELEMENTS}; use crate::app::TAFFY_TREE; use crate::elements::core::ElementInternals; use crate::elements::element_data::ElementData; use crate::elements::Element; -use crate::events::{dispatch_event, CraftMessage, Event}; +use crate::events::{CraftMessage, Event}; use crate::layout::layout_context::LayoutContext; use crate::palette; use crate::style::Unit; @@ -291,7 +291,8 @@ impl ElementInternals for Slider { event: &mut Event, _target: Option>>, ) { - let focused = false; + // @HARDCODED + let focused = true; match message { CraftMessage::KeyboardInputEvent(key) => { @@ -313,7 +314,9 @@ impl ElementInternals for Slider { if let Some(new_value) = new_value { self.value = new_value; - //event.result_message(CraftMessage::SliderValueChanged(self.value)); + + let new_event = Event::new(event.target.clone()); + queue_event(new_event, CraftMessage::SliderValueChanged(self.value)); } } CraftMessage::PointerButtonUp(pointer_button_update) => { @@ -323,7 +326,9 @@ impl ElementInternals for Slider { let value = self.compute_slider_value(&Point::new(pointer_button_update.state.position.x, pointer_button_update.state.position.y)); self.value = value; - //event.result_message(CraftMessage::SliderValueChanged(value)); + + let new_event = Event::new(event.target.clone()); + queue_event(new_event, CraftMessage::SliderValueChanged(self.value)); } CraftMessage::PointerButtonDown(pointer_button_update) => { self.dragging = true; @@ -332,10 +337,9 @@ impl ElementInternals for Slider { let value = self.compute_slider_value(&Point::new(pointer_button_update.state.position.x, pointer_button_update.state.position.y)); self.value = value; - //event.result_message(CraftMessage::SliderValueChanged(value)); let new_event = Event::new(event.target.clone()); - //dispatch_event(new_event, CraftMessage::SliderValueChanged(self.value)); + queue_event(new_event, CraftMessage::SliderValueChanged(self.value)); } CraftMessage::PointerMovedEvent(pointer_update) => { if !self.dragging { @@ -344,7 +348,9 @@ impl ElementInternals for Slider { let value = self.compute_slider_value(&Point::new(pointer_update.current.position.x, pointer_update.current.position.y)); self.value = value; - //event.result_message(CraftMessage::SliderValueChanged(value)); + + let new_event = Event::new(event.target.clone()); + queue_event(new_event, CraftMessage::SliderValueChanged(self.value)); } _ => {} } diff --git a/crates/craft_retained/src/elements/text_input/mod.rs b/crates/craft_retained/src/elements/text_input/mod.rs index a8938ae1..37e682f0 100644 --- a/crates/craft_retained/src/elements/text_input/mod.rs +++ b/crates/craft_retained/src/elements/text_input/mod.rs @@ -21,7 +21,6 @@ use crate::elements::element_id::create_unique_element_id; use crate::elements::scrollable; use crate::elements::text_input::text_input_state::TextInputState; use crate::events::{CraftMessage, Event}; -use crate::text::parley_editor::PlainEditor; use crate::text::text_context::TextContext; use crate::text::text_render_data::TextRender; use crate::text::RangedStyles; diff --git a/crates/craft_retained/src/elements/text_input/text_input_state.rs b/crates/craft_retained/src/elements/text_input/text_input_state.rs index 229c0c16..a72c205e 100644 --- a/crates/craft_retained/src/elements/text_input/text_input_state.rs +++ b/crates/craft_retained/src/elements/text_input/text_input_state.rs @@ -16,7 +16,7 @@ use std::time::{Duration, Instant}; #[cfg(target_arch = "wasm32")] use web_time::{Duration, Instant}; -use crate::app::{LAYOUT_DIRTY, TAFFY_TREE}; +use crate::app::TAFFY_TREE; use crate::elements::core::ElementInternals; use crate::elements::text_input::parley_box_to_rect; use crate::elements::TextInput; diff --git a/crates/craft_retained/src/events/event_dispatch.rs b/crates/craft_retained/src/events/event_dispatch.rs index 8bee51c7..9e334c15 100644 --- a/crates/craft_retained/src/events/event_dispatch.rs +++ b/crates/craft_retained/src/events/event_dispatch.rs @@ -1,17 +1,17 @@ use crate::elements::Element; use crate::events::{CraftMessage, Event, FocusAction}; +use crate::app::dequeue_event; use crate::events::helpers::{ call_default_element_event_handler, call_user_event_handlers, find_target, freeze_target_list, }; -use crate::events::pointer_capture::{maybe_handle_implicit_pointer_capture_release}; +use crate::events::pointer_capture::maybe_handle_implicit_pointer_capture_release; use crate::text::text_context::TextContext; -use craft_logging::{span, Level}; use craft_primitives::geometry::Point; +use craft_renderer::RenderList; use std::cell::RefCell; use std::collections::VecDeque; use std::rc::{Rc, Weak}; -use craft_renderer::RenderList; pub(super) fn dispatch_capturing_event( _message: &CraftMessage, @@ -198,15 +198,16 @@ impl EventDispatcher { // - gotpointercapture(capture), gotpointercapture(bubble) maybe_handle_implicit_pointer_capture_release(message); - self.previous_targets = targets.iter().map(Rc::downgrade).collect(); - } -} + // Drain the event dispatch queue and invoke user callbacks. + while let Some((event, message)) = dequeue_event() { + let mut targets: VecDeque>> = freeze_target_list(event.target); + // Handle capturing + dispatch_capturing_event(&message, &mut targets); -pub fn dispatch_event(event: Event, craft_message: CraftMessage) { - let mut targets: VecDeque>> = freeze_target_list(event.target); - // Handle capturing - dispatch_capturing_event(&craft_message, &mut targets); + // Handle bubbling + let _ = dispatch_bubbling_event(&message, &mut targets); + } - // Handle bubbling - dispatch_bubbling_event(&craft_message, &mut targets); -} + self.previous_targets = targets.iter().map(Rc::downgrade).collect(); + } +} \ No newline at end of file diff --git a/crates/craft_retained/src/events/helpers.rs b/crates/craft_retained/src/events/helpers.rs index fe1b8f83..f0ac6e23 100644 --- a/crates/craft_retained/src/events/helpers.rs +++ b/crates/craft_retained/src/events/helpers.rs @@ -112,7 +112,13 @@ pub(super) fn call_user_event_handlers( CraftMessage::DropdownToggled(_) => {} CraftMessage::DropdownItemSelected(_) => {} CraftMessage::SwitchToggled(_) => {} - CraftMessage::SliderValueChanged(_) => {} + CraftMessage::SliderValueChanged(slider_value) => { + let element_data = current_target.borrow().element_data().clone(); + + for handler in &element_data.on_slider_value_changed { + (*handler)(event, *slider_value); + } + } CraftMessage::ElementMessage(_) => {} CraftMessage::GotPointerCapture() => { let element_data = current_target.borrow().element_data().clone(); diff --git a/crates/craft_retained/src/events/mod.rs b/crates/craft_retained/src/events/mod.rs index 54d41101..c88f8688 100644 --- a/crates/craft_retained/src/events/mod.rs +++ b/crates/craft_retained/src/events/mod.rs @@ -19,7 +19,6 @@ use ui_events::pointer::{PointerButtonEvent, PointerScrollEvent, PointerUpdate}; pub use winit::event::Ime; pub use winit::event::Modifiers; pub use winit::event::MouseButton; -pub use event_dispatch::dispatch_event; use crate::PinnedFutureAny; use crate::utils::cloneable_any::CloneableAny; @@ -28,6 +27,7 @@ use crate::elements::Element; pub type PointerEventHandler = Rc; pub type PointerCaptureHandler = Rc; +pub type SliderValueChangedHandler = Rc; pub type PointerEnterHandler = Rc; pub type PointerLeaveHandler = Rc; diff --git a/crates/craft_retained/src/layout/layout_item.rs b/crates/craft_retained/src/layout/layout_item.rs index c5894b3a..3c821af4 100644 --- a/crates/craft_retained/src/layout/layout_item.rs +++ b/crates/craft_retained/src/layout/layout_item.rs @@ -5,7 +5,7 @@ use craft_primitives::geometry::{Border, ElementBox, Margin, Padding, Point, Rec use craft_renderer::{Brush, RenderList}; use kurbo::{Affine, BezPath, Shape, Vec2}; use peniko::Color; -use taffy::{Layout, NodeId, Position, TaffyTree}; +use taffy::{NodeId, Position, TaffyTree}; impl CssComputedBorder { pub(crate) fn scale(&mut self, scale_factor: f64) { From 337f22a9b271fb51b9745558721e2b90401e891d Mon Sep 17 00:00:00 2001 From: "Austin M. Reppert" Date: Sat, 27 Dec 2025 01:48:28 -0500 Subject: [PATCH 007/143] Remove unused layout_item funcs --- crates/craft_retained/src/layout/layout_item.rs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/crates/craft_retained/src/layout/layout_item.rs b/crates/craft_retained/src/layout/layout_item.rs index 3c821af4..bf666c31 100644 --- a/crates/craft_retained/src/layout/layout_item.rs +++ b/crates/craft_retained/src/layout/layout_item.rs @@ -58,9 +58,6 @@ pub struct LayoutItem { pub layout_order: u32, pub clip_bounds: Option, - // --- - pub children_awaiting_add: Vec, - //cache_border_spec: Option<(CssRoundedRect, f64)>, // f64 for scale factor cache_border_spec: Option, computed_border: ComputedBorder, @@ -75,18 +72,6 @@ impl LayoutItem { self.transform } - pub fn push_child(&mut self, child: &Option) { - if let Some(taffy_node_id) = child.as_ref() { - self.children_awaiting_add.push(*taffy_node_id); - } - } - - pub fn build_tree(&mut self, taffy_tree: &mut TaffyTree, style: taffy::Style) -> Option { - self.taffy_node_id = Some(taffy_tree.new_with_children(style, &self.children_awaiting_add).unwrap()); - self.children_awaiting_add.clear(); - self.taffy_node_id - } - pub fn build_tree_with_context( &mut self, taffy_tree: &mut TaffyTree, From 0ab748f4cfa9ad1805c2a4cad9500680164ce8c7 Mon Sep 17 00:00:00 2001 From: "Austin M. Reppert" Date: Sat, 27 Dec 2025 01:49:55 -0500 Subject: [PATCH 008/143] Remove unused layout_item funcs --- crates/craft_retained/src/layout/layout_item.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/crates/craft_retained/src/layout/layout_item.rs b/crates/craft_retained/src/layout/layout_item.rs index bf666c31..3b0cc94c 100644 --- a/crates/craft_retained/src/layout/layout_item.rs +++ b/crates/craft_retained/src/layout/layout_item.rs @@ -72,16 +72,6 @@ impl LayoutItem { self.transform } - pub fn build_tree_with_context( - &mut self, - taffy_tree: &mut TaffyTree, - style: taffy::Style, - layout_context: LayoutContext, - ) -> Option { - self.taffy_node_id = Some(taffy_tree.new_leaf_with_context(style, layout_context).unwrap()); - self.taffy_node_id - } - pub fn resolve_box( &mut self, relative_position: Point, From b899e06fe99732d91e002cc1ec27692ec6e1ec2a Mon Sep 17 00:00:00 2001 From: "Austin M. Reppert" Date: Sat, 27 Dec 2025 11:22:22 -0500 Subject: [PATCH 009/143] cleanup on remove --- .../src/elements/core/element_internals.rs | 2 +- crates/craft_retained/src/elements/element.rs | 61 +++++++++++++++---- crates/craft_retained/src/events/helpers.rs | 4 +- .../craft_retained/src/layout/layout_item.rs | 3 +- .../craft_retained/src/text/parley_editor.rs | 1 + examples/jsframeworkbench/main.rs | 3 - 6 files changed, 55 insertions(+), 19 deletions(-) diff --git a/crates/craft_retained/src/elements/core/element_internals.rs b/crates/craft_retained/src/elements/core/element_internals.rs index ab648a30..6ae75bb6 100644 --- a/crates/craft_retained/src/elements/core/element_internals.rs +++ b/crates/craft_retained/src/elements/core/element_internals.rs @@ -33,7 +33,7 @@ pub trait ElementInternals: ElementData { pointer: Option, text_context: &mut TextContext, scale_factor: f64, - dirty: bool, + _dirty: bool, ) { for child in &self.element_data().children { child.borrow_mut().apply_layout( diff --git a/crates/craft_retained/src/elements/element.rs b/crates/craft_retained/src/elements/element.rs index 33e1d11c..0d18a7b3 100644 --- a/crates/craft_retained/src/elements/element.rs +++ b/crates/craft_retained/src/elements/element.rs @@ -1,6 +1,11 @@ -use crate::app::{DOCUMENTS, FOCUS, TAFFY_TREE}; +use crate::app::{DOCUMENTS, ELEMENTS, FOCUS, TAFFY_TREE}; +use crate::document::Document; use crate::elements::core::ElementData; -use crate::events::{KeyboardInputHandler, PointerCaptureHandler, PointerEnterHandler, PointerEventHandler, PointerLeaveHandler, PointerUpdateHandler, SliderValueChangedHandler}; +use crate::elements::ElementIdMap; +use crate::events::{ + KeyboardInputHandler, PointerCaptureHandler, PointerEnterHandler, PointerEventHandler, PointerLeaveHandler, + PointerUpdateHandler, SliderValueChangedHandler, +}; use crate::layout::layout_context::LayoutContext; use crate::style::{ AlignItems, Display, FlexDirection, FontFamily, FontStyle, JustifyContent, ScrollbarColor, Style, Underline, Unit, @@ -112,6 +117,32 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { taffy_tree.mark_dirty(parent_id.unwrap()).expect("Failed to mark taffy node dirty."); }); + // TODO: Move to document + fn remove_element_from_document( + node: Rc>, + document: &mut Document, + elements: &mut ElementIdMap, + ) { + elements.remove_id(node.borrow().element_data().internal_id); + document + .pointer_captures + .retain(|_, v| !Weak::ptr_eq(v, &node.borrow().element_data().me.as_ref().unwrap())); + document + .pending_pointer_captures + .retain(|_, v| !Weak::ptr_eq(v, &node.borrow().element_data().me.as_ref().unwrap())); + for child in node.borrow().children() { + remove_element_from_document(child.clone(), document, elements); + } + } + + DOCUMENTS.with_borrow_mut(|documents| { + ELEMENTS.with_borrow_mut(|elements| { + remove_element_from_document(child.clone(), documents.get_current_document(), elements); + }); + }); + + child.borrow_mut().unfocus_dyn(); + Ok(child) } @@ -141,8 +172,8 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { { self.element_data_mut().on_pointer_enter.push(on_pointer_enter); self - } - + } + fn on_slider_value_changed(&mut self, on_slider_value_changed: SliderValueChangedHandler) -> &mut Self where Self: Sized, @@ -282,7 +313,8 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { // https://w3c.github.io/pointerevents/#dom-element-haspointercapture DOCUMENTS.with_borrow_mut(|docs| { let current_doc = docs.get_current_document(); - current_doc.pending_pointer_captures.get(&pointer_id).cloned().map(|w| w.as_ptr()) == self.element_data().me.clone().map(|w|w.as_ptr()) + current_doc.pending_pointer_captures.get(&pointer_id).cloned().map(|w| w.as_ptr()) + == self.element_data().me.clone().map(|w| w.as_ptr()) }) } @@ -624,7 +656,8 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { /// Sets focus on the specified element, if it can be focused. /// /// The focused element is the element that will receive keyboard and similar events by default. - fn focus(&mut self) where + fn focus(&mut self) + where Self: Sized, { // Todo: check if the element is focusable. Should we return a result? @@ -635,9 +668,7 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { /// Returns true if the element has focus. fn is_focused(&self) -> bool { - let focus_element = FOCUS.with(|focus| { - focus.borrow().clone() - }); + let focus_element = FOCUS.with(|focus| focus.borrow().clone()); if focus_element.is_none() { return false; @@ -649,15 +680,21 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { } /// Removes focus if the element has focus. - fn unfocus(&mut self) -> &mut Self where + fn unfocus(&mut self) -> &mut Self + where Self: Sized, { + self.unfocus_dyn(); + + self + } + + /// Removes focus if the element has focus. + fn unfocus_dyn(&mut self) { if self.is_focused() { FOCUS.with(|focus| { *focus.borrow_mut() = None; }); } - - self } } diff --git a/crates/craft_retained/src/events/helpers.rs b/crates/craft_retained/src/events/helpers.rs index f0ac6e23..3ae89fc1 100644 --- a/crates/craft_retained/src/events/helpers.rs +++ b/crates/craft_retained/src/events/helpers.rs @@ -37,7 +37,9 @@ pub(super) fn find_target( ELEMENTS.with_borrow_mut(|elements| { target_scratch.extend(render_list.targets.iter().rev().filter_map(|(id, _)| { - elements.get(*id).unwrap().upgrade() + // When an element is removed from the dom, we do not remove it from targets. + // So we must handle it here. + elements.get(*id).and_then(|target| target.upgrade()) })); }); diff --git a/crates/craft_retained/src/layout/layout_item.rs b/crates/craft_retained/src/layout/layout_item.rs index 3b0cc94c..9cc3cd12 100644 --- a/crates/craft_retained/src/layout/layout_item.rs +++ b/crates/craft_retained/src/layout/layout_item.rs @@ -1,11 +1,10 @@ -use crate::layout::layout_context::LayoutContext; use crate::style::Style; use craft_primitives::geometry::borders::{CssRoundedRect, BOTTOM, LEFT, RIGHT, TOP}; use craft_primitives::geometry::{Border, ElementBox, Margin, Padding, Point, Rectangle, Size, TrblRectangle}; use craft_renderer::{Brush, RenderList}; use kurbo::{Affine, BezPath, Shape, Vec2}; use peniko::Color; -use taffy::{NodeId, Position, TaffyTree}; +use taffy::{NodeId, Position}; impl CssComputedBorder { pub(crate) fn scale(&mut self, scale_factor: f64) { diff --git a/crates/craft_retained/src/text/parley_editor.rs b/crates/craft_retained/src/text/parley_editor.rs index 30270721..62eb1ed2 100644 --- a/crates/craft_retained/src/text/parley_editor.rs +++ b/crates/craft_retained/src/text/parley_editor.rs @@ -903,6 +903,7 @@ impl PlainEditor /// /// The return value is a `SplitString` because it /// excludes the IME preedit region. + #[allow(dead_code)] pub fn text(&self) -> SplitString<'_> { if let Some(preedit_range) = &self.compose { SplitString([ diff --git a/examples/jsframeworkbench/main.rs b/examples/jsframeworkbench/main.rs index 816c418e..e75d5956 100644 --- a/examples/jsframeworkbench/main.rs +++ b/examples/jsframeworkbench/main.rs @@ -81,7 +81,6 @@ impl State { self.remove_all_rows(); self.store.clear(); self.rows.clear(); - self.rows.shrink_to_fit(); self.store.run(); self.append_rows(); self.select(None); @@ -91,7 +90,6 @@ impl State { self.remove_all_rows(); self.store.clear(); self.rows.clear(); - self.rows.shrink_to_fit(); self.store.run_lots(); self.append_rows(); self.select(None); @@ -250,7 +248,6 @@ impl Store { pub fn clear(&mut self) { self.data.clear(); - self.data.shrink_to_fit(); self.selected = None; } From ac8fcff072d0ab573f046ff2f8d287b8f09ba6b1 Mon Sep 17 00:00:00 2001 From: "Austin M. Reppert" Date: Sat, 27 Dec 2025 11:50:54 -0500 Subject: [PATCH 010/143] Request re-layout on swap --- crates/craft_retained/src/elements/element.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/craft_retained/src/elements/element.rs b/crates/craft_retained/src/elements/element.rs index 0d18a7b3..a85f86b1 100644 --- a/crates/craft_retained/src/elements/element.rs +++ b/crates/craft_retained/src/elements/element.rs @@ -11,7 +11,7 @@ use crate::style::{ AlignItems, Display, FlexDirection, FontFamily, FontStyle, JustifyContent, ScrollbarColor, Style, Underline, Unit, Weight, Wrap, }; -use crate::CraftError; +use crate::{request_layout, CraftError}; use craft_primitives::geometry::Point; use craft_primitives::geometry::{ElementBox, TrblRectangle}; use craft_primitives::Color; @@ -64,6 +64,7 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { taffy_tree.set_children(parent_id, &tchildren).expect("Failed set taffy children"); taffy_tree.mark_dirty(parent_id).expect("Failed to mark taffy node dirty."); + request_layout(); } }); From b8c5d12ceabdeb3e59221fbdac152f46f914ab24 Mon Sep 17 00:00:00 2001 From: "Austin M. Reppert" Date: Sun, 28 Dec 2025 19:53:35 -0500 Subject: [PATCH 011/143] sets --- crates/craft_retained/src/elements/element.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/craft_retained/src/elements/element.rs b/crates/craft_retained/src/elements/element.rs index a85f86b1..363316af 100644 --- a/crates/craft_retained/src/elements/element.rs +++ b/crates/craft_retained/src/elements/element.rs @@ -327,18 +327,26 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { where Self: Sized, { + self.set_display(display); + self + } + + fn set_display(&mut self, display: Display) { self.style_mut().set_display(display); self.update_taffy_style(); - self } fn box_sizing(&mut self, box_sizing: BoxSizing) -> &mut Self where Self: Sized, { + self.set_box_sizing(box_sizing); + self + } + + fn set_box_sizing(&mut self, box_sizing: BoxSizing) { self.style_mut().set_box_sizing(box_sizing); self.update_taffy_style(); - self } fn position(&mut self, position: Position) -> &mut Self From 3b0d0f0db034f8883c2dc130e42715d3cea6f141 Mon Sep 17 00:00:00 2001 From: "Austin M. Reppert" Date: Sun, 28 Dec 2025 20:51:30 -0500 Subject: [PATCH 012/143] Add position setter --- crates/craft_retained/src/elements/element.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/craft_retained/src/elements/element.rs b/crates/craft_retained/src/elements/element.rs index 363316af..d0b473c5 100644 --- a/crates/craft_retained/src/elements/element.rs +++ b/crates/craft_retained/src/elements/element.rs @@ -353,9 +353,13 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { where Self: Sized, { + self.set_position(position); + self + } + + fn set_position(&mut self, position: Position) { self.style_mut().set_position(position); self.update_taffy_style(); - self } fn margin(&mut self, top: Unit, right: Unit, bottom: Unit, left: Unit) -> &mut Self From 905db0afabee51f9b9c044e22f75dfda3c45a9b5 Mon Sep 17 00:00:00 2001 From: "Austin M. Reppert" Date: Sun, 28 Dec 2025 21:57:16 -0500 Subject: [PATCH 013/143] more setters --- crates/craft_retained/src/elements/element.rs | 60 +++++++++++++++---- 1 file changed, 50 insertions(+), 10 deletions(-) diff --git a/crates/craft_retained/src/elements/element.rs b/crates/craft_retained/src/elements/element.rs index d0b473c5..a0b058be 100644 --- a/crates/craft_retained/src/elements/element.rs +++ b/crates/craft_retained/src/elements/element.rs @@ -366,90 +366,130 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { where Self: Sized, { + self.set_margin(top, right, bottom, left); + self + } + + fn set_margin(&mut self, top: Unit, right: Unit, bottom: Unit, left: Unit) { self.style_mut().set_margin(TrblRectangle::new(top, right, bottom, left)); self.update_taffy_style(); - self } fn padding(&mut self, top: Unit, right: Unit, bottom: Unit, left: Unit) -> &mut Self where Self: Sized, { + self.set_padding(top, right, bottom, left); + self + } + + fn set_padding(&mut self, top: Unit, right: Unit, bottom: Unit, left: Unit) { self.style_mut().set_padding(TrblRectangle::new(top, right, bottom, left)); self.update_taffy_style(); - self } fn gap(&mut self, row_gap: Unit, column_gap: Unit) -> &mut Self where Self: Sized, { + self.set_gap(row_gap, column_gap); + self + } + + fn set_gap(&mut self, row_gap: Unit, column_gap: Unit) { self.style_mut().set_gap([row_gap, column_gap]); self.update_taffy_style(); - self } fn inset(&mut self, top: Unit, right: Unit, bottom: Unit, left: Unit) -> &mut Self where Self: Sized, { + self.set_inset(top, right, bottom, left); + self + } + + fn set_inset(&mut self, top: Unit, right: Unit, bottom: Unit, left: Unit) { self.style_mut().set_inset(TrblRectangle::new(top, right, bottom, left)); self.update_taffy_style(); - self } fn min_width(&mut self, min_width: Unit) -> &mut Self where Self: Sized, { + self.set_min_width(min_width); + self + } + + fn set_min_width(&mut self, min_width: Unit) { self.style_mut().set_min_width(min_width); self.update_taffy_style(); - self } fn min_height(&mut self, min_height: Unit) -> &mut Self where Self: Sized, { + self.set_min_height(min_height); + self + } + + fn set_min_height(&mut self, min_height: Unit) { self.style_mut().set_min_height(min_height); self.update_taffy_style(); - self } fn width(&mut self, width: Unit) -> &mut Self where Self: Sized, { + self.set_width(width); + self + } + + fn set_width(&mut self, width: Unit) { self.style_mut().set_width(width); self.update_taffy_style(); - self } fn height(&mut self, height: Unit) -> &mut Self where Self: Sized, { + self.set_height(height); + self + } + + fn set_height(&mut self, height: Unit) { self.style_mut().set_height(height); self.update_taffy_style(); - self } fn max_width(&mut self, max_width: Unit) -> &mut Self where Self: Sized, { + self.set_max_width(max_width); + self + } + + fn set_max_width(&mut self, max_width: Unit) { self.style_mut().set_max_width(max_width); self.update_taffy_style(); - self } fn max_height(&mut self, max_height: Unit) -> &mut Self where Self: Sized, { + self.set_max_height(max_height); + self + } + + fn set_max_height(&mut self, max_height: Unit) { self.style_mut().set_max_height(max_height); self.update_taffy_style(); - self } fn wrap(&mut self, wrap: Wrap) -> &mut Self From 65e4bee48ac565e4ca8b4a6fbb0ed75d793058ea Mon Sep 17 00:00:00 2001 From: "Austin M. Reppert" Date: Sun, 28 Dec 2025 22:47:05 -0500 Subject: [PATCH 014/143] more setters --- crates/craft_retained/src/elements/element.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/craft_retained/src/elements/element.rs b/crates/craft_retained/src/elements/element.rs index a0b058be..f95b7d11 100644 --- a/crates/craft_retained/src/elements/element.rs +++ b/crates/craft_retained/src/elements/element.rs @@ -496,18 +496,26 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { where Self: Sized, { + self.set_wrap(wrap); + self + } + + fn set_wrap(&mut self, wrap: Wrap) { self.style_mut().set_wrap(wrap); self.update_taffy_style(); - self } fn align_items(&mut self, align_items: Option) -> &mut Self where Self: Sized, { + self.set_align_items(align_items); + self + } + + fn set_align_items(&mut self, align_items: Option) { self.style_mut().set_align_items(align_items); self.update_taffy_style(); - self } fn justify_content(&mut self, justify_content: Option) -> &mut Self From 28751887470b291d9e2246de82cdb24ef119f347 Mon Sep 17 00:00:00 2001 From: "Austin M. Reppert" Date: Mon, 29 Dec 2025 07:52:15 -0500 Subject: [PATCH 015/143] more setters --- crates/craft_retained/src/elements/element.rs | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/crates/craft_retained/src/elements/element.rs b/crates/craft_retained/src/elements/element.rs index f95b7d11..544c29cf 100644 --- a/crates/craft_retained/src/elements/element.rs +++ b/crates/craft_retained/src/elements/element.rs @@ -522,45 +522,65 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { where Self: Sized, { + self.set_justify_content(justify_content); + self + } + + fn set_justify_content(&mut self, justify_content: Option) { self.style_mut().set_justify_content(justify_content); self.update_taffy_style(); - self } fn flex_direction(&mut self, flex_direction: FlexDirection) -> &mut Self where Self: Sized, { + self.set_flex_direction(flex_direction); + self + } + + fn set_flex_direction(&mut self, flex_direction: FlexDirection) { self.style_mut().set_flex_direction(flex_direction); self.update_taffy_style(); - self } fn flex_grow(&mut self, flex_grow: f32) -> &mut Self where Self: Sized, { + self.set_flex_grow(flex_grow); + self + } + + fn set_flex_grow(&mut self, flex_grow: f32) { self.style_mut().set_flex_grow(flex_grow); self.update_taffy_style(); - self } fn flex_shrink(&mut self, flex_shrink: f32) -> &mut Self where Self: Sized, { + self.set_flex_shrink(flex_shrink); + self + } + + fn set_flex_shrink(&mut self, flex_shrink: f32) { self.style_mut().set_flex_shrink(flex_shrink); self.update_taffy_style(); - self } fn flex_basis(&mut self, flex_basis: Unit) -> &mut Self where Self: Sized, { + self.set_flex_basis(flex_basis); + self + } + + fn set_flex_basis(&mut self, flex_basis: Unit) { self.style_mut().set_flex_basis(flex_basis); self.update_taffy_style(); - self } fn font_family(&mut self, font_family: FontFamily) -> &mut Self From 0a52d1e4d23c4cc6e36c6038a4b514e11bcfff27 Mon Sep 17 00:00:00 2001 From: "Austin M. Reppert" Date: Mon, 29 Dec 2025 10:16:45 -0500 Subject: [PATCH 016/143] more setters --- crates/craft_retained/src/elements/element.rs | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/crates/craft_retained/src/elements/element.rs b/crates/craft_retained/src/elements/element.rs index 544c29cf..100477f2 100644 --- a/crates/craft_retained/src/elements/element.rs +++ b/crates/craft_retained/src/elements/element.rs @@ -587,53 +587,77 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { where Self: Sized, { + self.set_font_family(font_family); + self + } + + fn set_font_family(&mut self, font_family: FontFamily) { self.style_mut().set_font_family(font_family); self.update_taffy_style(); - self } fn color(&mut self, color: Color) -> &mut Self where Self: Sized, { + self.set_color(color); + self + } + + fn set_color(&mut self, color: Color) { self.style_mut().set_color(color); self.update_taffy_style(); - self } fn background_color(&mut self, color: Color) -> &mut Self where Self: Sized, { - self.style_mut().set_background(color); + self.set_background_color(color); self } + fn set_background_color(&mut self, color: Color) { + self.style_mut().set_background(color); + } + fn font_size(&mut self, font_size: f32) -> &mut Self where Self: Sized, { + self.set_font_size(font_size); + self + } + + fn set_font_size(&mut self, font_size: f32) { self.style_mut().set_font_size(font_size); self.update_taffy_style(); - self } fn line_height(&mut self, line_height: f32) -> &mut Self where Self: Sized, { + self.set_line_height(line_height); + self + } + + fn set_line_height(&mut self, line_height: f32) { self.style_mut().set_line_height(line_height); self.update_taffy_style(); - self } fn font_weight(&mut self, font_weight: Weight) -> &mut Self where Self: Sized, { + self.set_font_weight(font_weight); + self + } + + fn set_font_weight(&mut self, font_weight: Weight) { self.style_mut().set_font_weight(font_weight); self.update_taffy_style(); - self } fn font_style(&mut self, font_style: FontStyle) -> &mut Self From 545b34219da38d5d3600dca66a66a0bae48b11e1 Mon Sep 17 00:00:00 2001 From: "Austin M. Reppert" Date: Mon, 29 Dec 2025 16:56:19 -0500 Subject: [PATCH 017/143] more setters --- crates/craft_retained/src/elements/element.rs | 66 +++++++++++++++---- 1 file changed, 55 insertions(+), 11 deletions(-) diff --git a/crates/craft_retained/src/elements/element.rs b/crates/craft_retained/src/elements/element.rs index 100477f2..36aead80 100644 --- a/crates/craft_retained/src/elements/element.rs +++ b/crates/craft_retained/src/elements/element.rs @@ -664,70 +664,102 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { where Self: Sized, { + self.set_font_style(font_style); + self + } + + fn set_font_style(&mut self, font_style: FontStyle) { self.style_mut().set_font_style(font_style); self.update_taffy_style(); - self } fn underline(&mut self, underline: Option) -> &mut Self where Self: Sized, { + self.set_underline(underline); + self + } + + fn set_underline(&mut self, underline: Option) { self.style_mut().set_underline(underline); self.update_taffy_style(); - self } fn overflow(&mut self, overflow_x: Overflow, overflow_y: Overflow) -> &mut Self where Self: Sized, { + self.set_overflow(overflow_x, overflow_y); + self + } + + fn set_overflow(&mut self, overflow_x: Overflow, overflow_y: Overflow) { self.style_mut().set_overflow([overflow_x, overflow_y]); self.update_taffy_style(); - self } fn border_color(&mut self, top: Color, right: Color, bottom: Color, left: Color) -> &mut Self where Self: Sized, { - self.style_mut().set_border_color(TrblRectangle::new(top, right, bottom, left)); + self.set_border_color(top, right, bottom, left); self } + fn set_border_color(&mut self, top: Color, right: Color, bottom: Color, left: Color) { + self.style_mut().set_border_color(TrblRectangle::new(top, right, bottom, left)); + } + fn border_width(&mut self, top: Unit, right: Unit, bottom: Unit, left: Unit) -> &mut Self where Self: Sized, { + self.set_border_width(top, right, bottom, left); + self + } + + fn set_border_width(&mut self, top: Unit, right: Unit, bottom: Unit, left: Unit) { self.style_mut().set_border_width(TrblRectangle::new(top, right, bottom, left)); self.update_taffy_style(); - self } fn border_radius(&mut self, top: (f32, f32), right: (f32, f32), bottom: (f32, f32), left: (f32, f32)) -> &mut Self where Self: Sized, { - self.style_mut().set_border_radius([top, right, bottom, left]); + self.set_border_radius(top, right, bottom, left); self } + fn set_border_radius(&mut self, top: (f32, f32), right: (f32, f32), bottom: (f32, f32), left: (f32, f32)) { + self.style_mut().set_border_radius([top, right, bottom, left]); + } + fn scrollbar_color(&mut self, scrollbar_color: ScrollbarColor) -> &mut Self where Self: Sized, { - self.style_mut().set_scrollbar_color(scrollbar_color); + self.set_scrollbar_color(scrollbar_color); self } + fn set_scrollbar_color(&mut self, scrollbar_color: ScrollbarColor) { + self.style_mut().set_scrollbar_color(scrollbar_color); + } + fn scrollbar_thumb_margin(&mut self, top: f32, right: f32, bottom: f32, left: f32) -> &mut Self where Self: Sized, { - self.style_mut().set_scrollbar_thumb_margin(TrblRectangle::new(top, right, bottom, left)); + self.set_scrollbar_thumb_margin(top, right, bottom, left); self } + fn set_scrollbar_thumb_margin(&mut self, top: f32, right: f32, bottom: f32, left: f32) { + self.style_mut().set_scrollbar_thumb_margin(TrblRectangle::new(top, right, bottom, left)); + } + fn scrollbar_thumb_radius( &mut self, top: (f32, f32), @@ -738,26 +770,38 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { where Self: Sized, { - self.style_mut().set_scrollbar_thumb_radius([top, right, bottom, left]); + self.set_scrollbar_thumb_radius(top, right, bottom, left); self } + fn set_scrollbar_thumb_radius(&mut self, top: (f32, f32), right: (f32, f32), bottom: (f32, f32), left: (f32, f32)) { + self.style_mut().set_scrollbar_thumb_radius([top, right, bottom, left]); + } + fn scrollbar_width(&mut self, scrollbar_width: f32) -> &mut Self where Self: Sized, { - self.style_mut().set_scrollbar_width(scrollbar_width); + self.set_scrollbar_width(scrollbar_width); self } + fn set_scrollbar_width(&mut self, scrollbar_width: f32) { + self.style_mut().set_scrollbar_width(scrollbar_width); + } + fn selection_color(&mut self, selection_color: Color) -> &mut Self where Self: Sized, { - self.style_mut().set_selection_color(selection_color); + self.set_selection_color(selection_color); self } + fn set_selection_color(&mut self, selection_color: Color) { + self.style_mut().set_selection_color(selection_color); + } + /// Sets focus on the specified element, if it can be focused. /// /// The focused element is the element that will receive keyboard and similar events by default. From cce291e248f8a39d2d20f14a5d86fe2db5903e93 Mon Sep 17 00:00:00 2001 From: "Austin M. Reppert" Date: Tue, 30 Dec 2025 09:01:29 -0500 Subject: [PATCH 018/143] Taffy Tree wrapper --- crates/craft_retained/src/app.rs | 61 ++++------ .../craft_retained/src/elements/container.rs | 24 ++-- .../src/elements/core/element_internals.rs | 16 +-- crates/craft_retained/src/elements/element.rs | 27 ++--- crates/craft_retained/src/elements/image.rs | 10 +- .../src/elements/slider/slider.rs | 9 +- crates/craft_retained/src/elements/text.rs | 9 +- .../src/elements/text_input/mod.rs | 11 +- .../elements/text_input/text_input_state.rs | 2 +- crates/craft_retained/src/layout/mod.rs | 3 + .../craft_retained/src/layout/taffy_tree.rs | 113 ++++++++++++++++++ 11 files changed, 186 insertions(+), 99 deletions(-) create mode 100644 crates/craft_retained/src/layout/taffy_tree.rs diff --git a/crates/craft_retained/src/app.rs b/crates/craft_retained/src/app.rs index ce9b1fd6..e97cbf54 100644 --- a/crates/craft_retained/src/app.rs +++ b/crates/craft_retained/src/app.rs @@ -1,8 +1,7 @@ use crate::elements::ElementIdMap; -use crate::events::{Event, EventDispatcher}; use crate::events::internal::InternalMessage; -use crate::events::{CraftMessage}; -use crate::layout::layout_context::measure_content; +use crate::events::CraftMessage; +use crate::events::{Event, EventDispatcher}; use crate::style::{Display, Unit, Wrap}; use crate::text::text_context::TextContext; use crate::{RendererBox, WindowContext}; @@ -12,9 +11,9 @@ use craft_resource_manager::{ResourceIdentifier, ResourceManager}; use craft_runtime::CraftRuntimeHandle; use kurbo::{Affine, Point}; use peniko::Color; -use std::cell::{Cell}; +use std::cell::Cell; use std::cell::RefCell; -use std::collections::{VecDeque}; +use std::collections::VecDeque; use std::ops::DerefMut; use std::rc::{Rc, Weak}; use std::sync::Arc; @@ -36,13 +35,13 @@ use web_time as time; use crate::animations::animation::AnimationFlags; use crate::document::DocumentManager; use crate::elements::Element; -use crate::layout::layout_context::LayoutContext; +use crate::layout::TaffyTree; use craft_renderer::RenderList; use craft_resource_manager::resource_event::ResourceEvent; use craft_resource_manager::resource_type::ResourceType; use craft_runtime::Sender; use std::time::Duration; -use taffy::{AvailableSpace, NodeId, TaffyTree}; +use taffy::{AvailableSpace, NodeId}; use ui_events::keyboard::{KeyboardEvent, Modifiers, NamedKey}; use ui_events::pointer::{PointerButtonEvent, PointerScrollEvent, PointerUpdate}; use ui_events::ScrollDelta; @@ -58,7 +57,7 @@ thread_local! { /// Records document-level state (focus, pointer captures, etc.) for internal use. pub static DOCUMENTS: RefCell = RefCell::new(DocumentManager::new()); pub(crate) static ELEMENTS: RefCell = RefCell::new(ElementIdMap::new()); - pub(crate) static TAFFY_TREE: RefCell> = RefCell::new(TaffyTree::new()); + pub(crate) static TAFFY_TREE: RefCell = RefCell::new(TaffyTree::new()); pub(crate) static PENDING_RESOURCES: RefCell> = RefCell::new(VecDeque::new()); pub(crate) static IN_PROGRESS_RESOURCES: RefCell> = RefCell::new(VecDeque::new()); pub(crate) static FOCUS: RefCell>>> = RefCell::new(None); @@ -408,7 +407,7 @@ impl App { self.root.clone(), &mut self.text_context, &mut self.render_list, - &mut self.target_scratch + &mut self.target_scratch, ); self.window.clone().unwrap().request_redraw(); } @@ -442,7 +441,7 @@ impl App { match resource_event { ResourceEvent::Loaded(resource_identifier, resource_type, resource) => { IN_PROGRESS_RESOURCES.with_borrow_mut(|in_progress| { - in_progress.retain_mut(|(resource, _resource_type)| *resource != resource_identifier); + in_progress.retain_mut(|(resource, _resource_type)| *resource != resource_identifier); }); if let Some(_text_context) = self.text_context.as_mut() && resource_type == ResourceType::Font @@ -548,7 +547,7 @@ impl App { self.root.borrow_mut().draw(&mut self.render_list, text_context, mouse_position, scale_factor); } - renderer.sort_and_cull_render_list(&mut self.render_list); + renderer.sort_and_cull_render_list(&mut self.render_list); let window = Rectangle { x: 0.0, @@ -589,10 +588,16 @@ impl App { PENDING_RESOURCES.with_borrow_mut(|pending_resources| { IN_PROGRESS_RESOURCES.with_borrow_mut(|in_progress| { for (resource, resource_type) in pending_resources.drain(..) { - if self.resource_manager.contains(&resource) || in_progress.contains(&(resource.clone(), resource_type)) { + if self.resource_manager.contains(&resource) + || in_progress.contains(&(resource.clone(), resource_type)) + { continue; } - self.resource_manager.async_download_resource_and_send_message_on_finish(self.app_sender.clone(), resource.clone(), resource_type); + self.resource_manager.async_download_resource_and_send_message_on_finish( + self.app_sender.clone(), + resource.clone(), + resource_type, + ); in_progress.push_back((resource, resource_type)); } }); @@ -622,7 +627,7 @@ fn style_root_element(root: &mut dyn Element, root_size: LogicalSize) { #[allow(clippy::too_many_arguments)] fn layout( - taffy_tree: &mut TaffyTree, + taffy_tree: &mut TaffyTree, root_element: Rc>, window_size: LogicalSize, text_context: &mut TextContext, @@ -631,11 +636,10 @@ fn layout( scale_factor: f64, pointer: Option, ) -> NodeId { - let dirty = LAYOUT_DIRTY.with_borrow(|status| { - *status - }); + let dirty = LAYOUT_DIRTY.with_borrow(|status| *status); - let root_node = root_element.borrow() + let root_node = root_element + .borrow() .element_data() .layout_item .taffy_node_id @@ -655,24 +659,9 @@ fn layout( let _enter = span.enter();*/ //println!("before layout"); //taffy_tree.print_tree(root_node); - taffy_tree - .compute_layout_with_measure( - root_node, - available_space, - |known_dimensions, available_space, _node_id, node_context, style| { - measure_content( - known_dimensions, - available_space, - node_context, - text_context, - resource_manager.clone(), - style, - ) - }, - ) - .unwrap(); + taffy_tree.compute_layout(root_node, available_space, text_context, resource_manager.clone()) } - //println!("after layout"); + //println!("after layout"); //taffy_tree.print_tree(root_node); let transform = Affine::IDENTITY; @@ -704,4 +693,4 @@ pub fn request_layout() { LAYOUT_DIRTY.with_borrow_mut(|status| { *status = true; }); -} \ No newline at end of file +} diff --git a/crates/craft_retained/src/elements/container.rs b/crates/craft_retained/src/elements/container.rs index dd9a23fc..14357476 100644 --- a/crates/craft_retained/src/elements/container.rs +++ b/crates/craft_retained/src/elements/container.rs @@ -5,7 +5,6 @@ use crate::elements::core::{resolve_clip_for_scrollable, ElementInternals}; use crate::elements::element_data::ElementData; use crate::elements::{scrollable, Element}; use crate::events::{CraftMessage, Event}; -use crate::layout::layout_context::LayoutContext; use crate::text::text_context::TextContext; use craft_primitives::geometry::Rectangle; use craft_renderer::RenderList; @@ -14,31 +13,28 @@ use std::any::Any; use std::cell::RefCell; use std::ops::Deref; use std::rc::{Rc, Weak}; -use taffy::{PrintTree, TaffyTree}; +use crate::layout::TaffyTree; /// Stores one or more elements. /// /// If overflow is set to scroll, it will become scrollable. pub struct Container { element_data: ElementData, - me: Option>>, } impl Container { pub fn new() -> Rc> { let me = Rc::new(RefCell::new(Self { element_data: ElementData::new(true), - me: None, })); TAFFY_TREE.with_borrow_mut(|taffy_tree| { - let node_id = taffy_tree.new_leaf(me.borrow().style().to_taffy_style()).expect("TODO: panic message"); + let node_id = taffy_tree.new_leaf(me.borrow().style().to_taffy_style()); me.borrow_mut().element_data.layout_item.taffy_node_id = Some(node_id); }); let me_element: Rc> = me.clone(); - me.borrow_mut().me = Some(Rc::downgrade(&me.clone())); me.borrow_mut().element_data.me = Some(Rc::downgrade(&me_element)); ELEMENTS.with_borrow_mut(|elements| { @@ -64,7 +60,7 @@ impl Element for Container { where Self: Sized, { - let me: Weak> = self.me.clone().unwrap() as Weak>; + let me: Weak> = self.element_data.me.clone().unwrap(); child.borrow_mut().element_data_mut().parent = Some(me); self.element_data.children.push(child.clone()); @@ -73,9 +69,9 @@ impl Element for Container { let parent_id = self.element_data.layout_item.taffy_node_id.unwrap(); let child_id = child.borrow().element_data().layout_item.taffy_node_id; if let Some(child_id) = child_id { - taffy_tree.add_child(parent_id, child_id).unwrap(); + taffy_tree.add_child(parent_id, child_id); - taffy_tree.mark_dirty(parent_id).expect("Failed to mark taffy node dirty"); + taffy_tree.mark_dirty(parent_id); } }); @@ -91,7 +87,7 @@ impl Element for Container { where Self: Sized, { - let me: Weak> = self.me.clone().unwrap() as Weak>; + let me: Weak> = self.element_data.me.clone().unwrap(); let children: Vec<_> = children.into_iter().collect(); for child in &children { @@ -105,10 +101,10 @@ impl Element for Container { let parent_id = self.element_data.layout_item.taffy_node_id.unwrap(); for child in &children { if let Some(child_id) = child.borrow().element_data().layout_item.taffy_node_id { - taffy_tree.add_child(parent_id, child_id).unwrap(); + taffy_tree.add_child(parent_id, child_id); } } - taffy_tree.mark_dirty(parent_id).expect("Failed to mark taffy node dirty"); + taffy_tree.mark_dirty(parent_id); }); self @@ -127,7 +123,7 @@ impl ElementInternals for Container { fn apply_layout( &mut self, - taffy_tree: &mut TaffyTree, + taffy_tree: &mut TaffyTree, position: Point, z_index: &mut u32, transform: Affine, @@ -137,7 +133,7 @@ impl ElementInternals for Container { scale_factor: f64, ) { let node = self.element_data.layout_item.taffy_node_id.unwrap(); - let layout = taffy_tree.layout(node).unwrap(); + let layout = taffy_tree.layout(node); let has_new_layout = taffy_tree.get_has_new_layout(node); let dirty = has_new_layout || transform != self.element_data.layout_item.get_transform() || position != self.element_data.layout_item.position ; diff --git a/crates/craft_retained/src/elements/core/element_internals.rs b/crates/craft_retained/src/elements/core/element_internals.rs index 6ae75bb6..32cc0060 100644 --- a/crates/craft_retained/src/elements/core/element_internals.rs +++ b/crates/craft_retained/src/elements/core/element_internals.rs @@ -1,6 +1,5 @@ use crate::animations::animation::{ActiveAnimation, AnimationFlags, AnimationStatus}; use crate::events::{CraftMessage, Event}; -use crate::layout::layout_context::LayoutContext; use crate::layout::layout_item::{draw_borders_generic, CssComputedBorder, LayoutItem}; use crate::style::{Display, Style}; use crate::text::text_context::TextContext; @@ -11,7 +10,7 @@ use rustc_hash::FxHashMap; use std::cell::RefCell; use std::rc::Rc; use std::time::Duration; -use taffy::{Overflow, TaffyTree}; +use taffy::{Overflow}; use crate::elements::core::element_data::ElementData; use crate::elements::Element; @@ -19,6 +18,7 @@ use crate::elements::Element; use accesskit::{Action, Role}; use craft_primitives::geometry::borders::CssRoundedRect; use crate::app::TAFFY_TREE; +use crate::layout::TaffyTree; use crate::request_layout; /// Internal element methods that should typically be ignored by users. Public for custom elements. @@ -27,7 +27,7 @@ pub trait ElementInternals: ElementData { /// A helper to apply the layout for all children. fn apply_layout_children( &mut self, - taffy_tree: &mut TaffyTree, + taffy_tree: &mut TaffyTree, z_index: &mut u32, transform: Affine, pointer: Option, @@ -69,12 +69,12 @@ pub trait ElementInternals: ElementData { } /// A helper to re-apply the style to the layout node when dirty. - fn apply_style_to_layout_node_if_dirty(&mut self, taffy_tree: &mut TaffyTree) { + fn apply_style_to_layout_node_if_dirty(&mut self, taffy_tree: &mut TaffyTree) { let element_data = self.element_data_mut(); if element_data.style.is_dirty { let node_id = element_data.layout_item.taffy_node_id.unwrap(); let style: taffy::Style = element_data.style.to_taffy_style(); - taffy_tree.set_style(node_id, style).expect("Failed to set style on node."); + taffy_tree.set_style(node_id, style); element_data.style.is_dirty = false; } } @@ -103,7 +103,7 @@ pub trait ElementInternals: ElementData { #[allow(clippy::too_many_arguments)] fn apply_layout( &mut self, - taffy_tree: &mut TaffyTree, + taffy_tree: &mut TaffyTree, position: Point, z_index: &mut u32, transform: Affine, @@ -372,7 +372,7 @@ pub trait ElementInternals: ElementData { let id = self.element_data().layout_item.taffy_node_id; if let Some(id) = id { TAFFY_TREE.with_borrow_mut(|taffy_tree| { - taffy_tree.mark_dirty(id).expect("Failed to mark taffy node as dirty"); + taffy_tree.mark_dirty(id); }); } } @@ -384,7 +384,7 @@ pub trait ElementInternals: ElementData { let id = self.element_data().layout_item.taffy_node_id; if let Some(id) = id { TAFFY_TREE.with_borrow_mut(|taffy_tree| { - taffy_tree.set_style(id, self.element_data().style.to_taffy_style()).expect("Failed to set style."); + taffy_tree.set_style(id, self.element_data().style.to_taffy_style()); }); } } diff --git a/crates/craft_retained/src/elements/element.rs b/crates/craft_retained/src/elements/element.rs index 36aead80..3b6b0b46 100644 --- a/crates/craft_retained/src/elements/element.rs +++ b/crates/craft_retained/src/elements/element.rs @@ -6,7 +6,7 @@ use crate::events::{ KeyboardInputHandler, PointerCaptureHandler, PointerEnterHandler, PointerEventHandler, PointerLeaveHandler, PointerUpdateHandler, SliderValueChangedHandler, }; -use crate::layout::layout_context::LayoutContext; + use crate::style::{ AlignItems, Display, FlexDirection, FontFamily, FontStyle, JustifyContent, ScrollbarColor, Style, Underline, Unit, Weight, Wrap, @@ -18,7 +18,7 @@ use craft_primitives::Color; use std::any::Any; use std::cell::RefCell; use std::rc::{Rc, Weak}; -use taffy::{BoxSizing, NodeId, Overflow, Position, TaffyResult, TaffyTree}; +use taffy::{BoxSizing, Overflow, Position}; use ui_events::pointer::PointerId; /// The element trait for end-users. @@ -47,7 +47,7 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { && let Some(child_2_id) = child_2_id { // There isn't a swap API in the taffy tree. Instead swap the children and call set_children. - let mut tchildren = taffy_tree.children(parent_id).expect("Failed to get taffy children").to_vec(); + let mut tchildren = taffy_tree.children(parent_id).to_vec(); let i1 = tchildren .iter() @@ -62,8 +62,8 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { tchildren.swap(i1, i2); - taffy_tree.set_children(parent_id, &tchildren).expect("Failed set taffy children"); - taffy_tree.mark_dirty(parent_id).expect("Failed to mark taffy node dirty."); + taffy_tree.set_children(parent_id, &tchildren); + taffy_tree.mark_dirty(parent_id); request_layout(); } }); @@ -94,28 +94,15 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { child.borrow_mut().element_data_mut().parent = None; - // Remove the entire layout subtree. - - fn remove_subtree(taffy: &mut TaffyTree, node: NodeId) -> TaffyResult<()> { - // Can we avoid this allocation? - let children = taffy.children(node)?; - - for child in children { - remove_subtree(taffy, child)?; - } - - taffy.remove(node).map(|_| ()) - } - TAFFY_TREE.with_borrow_mut(|taffy_tree| { let child_id = child.borrow().element_data().layout_item.taffy_node_id; if let Some(child_id) = child_id { - remove_subtree(taffy_tree, child_id).expect("Failed to remove taffy element."); + taffy_tree.remove_subtree(child_id); } let parent_id = self.element_data().layout_item.taffy_node_id; - taffy_tree.mark_dirty(parent_id.unwrap()).expect("Failed to mark taffy node dirty."); + taffy_tree.mark_dirty(parent_id.unwrap()); }); // TODO: Move to document diff --git a/crates/craft_retained/src/elements/image.rs b/crates/craft_retained/src/elements/image.rs index 267fb516..740e0505 100644 --- a/crates/craft_retained/src/elements/image.rs +++ b/crates/craft_retained/src/elements/image.rs @@ -16,7 +16,7 @@ use std::any::Any; use std::cell::RefCell; use std::ops::Deref; use std::rc::{Rc, Weak}; -use taffy::TaffyTree; +use crate::layout::TaffyTree; /// Displays an image. pub struct Image { @@ -38,7 +38,7 @@ impl Image { let resource_identifier_2= resource_identifier.clone(); TAFFY_TREE.with_borrow_mut(|taffy_tree| { let context = LayoutContext::Image(ImageContext::new(resource_identifier_2)); - let node_id = taffy_tree.new_leaf_with_context(me.borrow().style().to_taffy_style(), context).expect("TODO: panic message"); + let node_id = taffy_tree.new_leaf_with_context(me.borrow().style().to_taffy_style(), context); me.borrow_mut().element_data.layout_item.taffy_node_id = Some(node_id); }); @@ -62,7 +62,7 @@ impl Image { TAFFY_TREE.with_borrow_mut(|taffy_tree| { let context = LayoutContext::Image(ImageContext::new(resource_identifier)); let node = self.element_data.layout_item.taffy_node_id.expect("Failed to get Image node"); - taffy_tree.set_node_context(node, Some(context)).expect("Failed to set Image node context"); + taffy_tree.set_node_context(node, Some(context)); }); self @@ -99,7 +99,7 @@ impl ElementInternals for Image { fn apply_layout( &mut self, - taffy_tree: &mut TaffyTree, + taffy_tree: &mut TaffyTree, position: Point, z_index: &mut u32, transform: Affine, @@ -108,7 +108,7 @@ impl ElementInternals for Image { clip_bounds: Option, scale_factor: f64, ) { - let layout = taffy_tree.layout(self.element_data.layout_item.taffy_node_id.unwrap()).unwrap(); + let layout = taffy_tree.layout(self.element_data.layout_item.taffy_node_id.unwrap()); self.resolve_box(position, transform, layout, z_index); self.apply_borders(scale_factor); diff --git a/crates/craft_retained/src/elements/slider/slider.rs b/crates/craft_retained/src/elements/slider/slider.rs index 9f63c0e9..0d3ad68a 100644 --- a/crates/craft_retained/src/elements/slider/slider.rs +++ b/crates/craft_retained/src/elements/slider/slider.rs @@ -4,7 +4,6 @@ use crate::elements::core::ElementInternals; use crate::elements::element_data::ElementData; use crate::elements::Element; use crate::events::{CraftMessage, Event}; -use crate::layout::layout_context::LayoutContext; use crate::palette; use crate::style::Unit; use crate::text::text_context::TextContext; @@ -16,9 +15,9 @@ use std::any::Any; use std::cell::RefCell; use std::ops::Deref; use std::rc::{Rc, Weak}; -use taffy::{PrintTree, TaffyTree}; use ui_events::keyboard::{Code, KeyState}; use ui_events::pointer::PointerId; +use crate::layout::TaffyTree; #[derive(Clone, Copy, Default, Debug, PartialEq, Eq)] pub enum SliderDirection { @@ -84,7 +83,7 @@ impl Slider { TAFFY_TREE.with_borrow_mut(|taffy_tree| { - let node_id = taffy_tree.new_leaf(me.borrow().style().to_taffy_style()).expect("TODO: panic message"); + let node_id = taffy_tree.new_leaf(me.borrow().style().to_taffy_style()); me.borrow_mut().element_data.layout_item.taffy_node_id = Some(node_id); }); @@ -242,7 +241,7 @@ impl ElementInternals for Slider { fn apply_layout( &mut self, - taffy_tree: &mut TaffyTree, + taffy_tree: &mut TaffyTree, position: Point, z_index: &mut u32, transform: Affine, @@ -252,7 +251,7 @@ impl ElementInternals for Slider { scale_factor: f64, ) { let node = self.element_data.layout_item.taffy_node_id.unwrap(); - let layout = taffy_tree.layout(node).unwrap(); + let layout = taffy_tree.layout(node); let has_new_layout = taffy_tree.get_has_new_layout(node); let dirty = has_new_layout || transform != self.element_data.layout_item.get_transform() || position != self.element_data.layout_item.position ; diff --git a/crates/craft_retained/src/elements/text.rs b/crates/craft_retained/src/elements/text.rs index dc43bf6d..dc1f2ea9 100644 --- a/crates/craft_retained/src/elements/text.rs +++ b/crates/craft_retained/src/elements/text.rs @@ -24,7 +24,7 @@ use kurbo::Affine; use rustc_hash::FxHashMap; #[cfg(not(target_arch = "wasm32"))] use std::time; -use taffy::{AvailableSpace, PrintTree, Size, TaffyTree}; +use taffy::{AvailableSpace, Size}; use time::{Duration, Instant}; use winit::dpi; #[cfg(target_arch = "wasm32")] @@ -38,6 +38,7 @@ use craft_primitives::ColorBrush; use craft_renderer::text_renderer_data::TextData; use smol_str::{SmolStr, ToSmolStr}; use ui_events::pointer::{PointerButton, PointerId}; +use crate::layout::TaffyTree; // A stateful element that shows text. #[derive(Clone, Default)] @@ -116,7 +117,7 @@ impl Text { let context = LayoutContext::Text(TaffyTextContext{ element: me.borrow().me.clone().unwrap() }); - let node_id = taffy_tree.new_leaf_with_context(me.borrow().style().to_taffy_style(), context).expect("TODO: panic message"); + let node_id = taffy_tree.new_leaf_with_context(me.borrow().style().to_taffy_style(), context); me.borrow_mut().element_data.layout_item.taffy_node_id = Some(node_id); }); @@ -268,7 +269,7 @@ impl ElementInternals for Text { fn apply_layout( &mut self, - taffy_tree: &mut TaffyTree, + taffy_tree: &mut TaffyTree, position: Point, z_index: &mut u32, transform: Affine, @@ -278,7 +279,7 @@ impl ElementInternals for Text { scale_factor: f64, ) { let node = self.element_data.layout_item.taffy_node_id.unwrap(); - let result = taffy_tree.layout(node).unwrap(); + let result = taffy_tree.layout(node); let has_new_layout = taffy_tree.get_has_new_layout(node); let dirty = has_new_layout || transform != self.element_data.layout_item.get_transform() || position != self.element_data.layout_item.position; diff --git a/crates/craft_retained/src/elements/text_input/mod.rs b/crates/craft_retained/src/elements/text_input/mod.rs index 37e682f0..43055ccf 100644 --- a/crates/craft_retained/src/elements/text_input/mod.rs +++ b/crates/craft_retained/src/elements/text_input/mod.rs @@ -12,7 +12,6 @@ use std::any::Any; use std::cell::RefCell; use std::ops::{Deref}; use std::rc::{Rc, Weak}; -use taffy::{PrintTree, TaffyTree}; use crate::app::TAFFY_TREE; use crate::elements::core::{resolve_clip_for_scrollable, ElementInternals}; @@ -30,6 +29,7 @@ use kurbo::Affine; use parley::{BoundingBox}; use ui_events::pointer::PointerButton; use winit::event::Ime; +use crate::layout::TaffyTree; // A stateful element that shows text. #[derive(Clone, Default)] @@ -84,8 +84,7 @@ impl TextInput { element: me.borrow().me.clone().unwrap(), }); let node_id = taffy_tree - .new_leaf_with_context(me.borrow().element_data.current_style().to_taffy_style(), context) - .expect("TODO: panic message"); + .new_leaf_with_context(me.borrow().element_data.current_style().to_taffy_style(), context); me.borrow_mut().element_data.layout_item.taffy_node_id = Some(node_id); me.borrow_mut().state.taffy_node(Some(node_id)); }); @@ -111,7 +110,7 @@ impl crate::elements::core::ElementData for TextInput { impl ElementInternals for TextInput { fn apply_layout( &mut self, - taffy_tree: &mut TaffyTree, + taffy_tree: &mut TaffyTree, position: Point, z_index: &mut u32, transform: Affine, @@ -129,7 +128,7 @@ impl ElementInternals for TextInput { self.element_data.layout_item.has_new_layout = has_new_layout; if dirty { - let result = taffy_tree.layout(node).unwrap(); + let result = taffy_tree.layout(node); self.resolve_box(position, transform, result, z_index); self.apply_clip(clip_bounds); self.apply_borders(scale_factor); @@ -145,7 +144,7 @@ impl ElementInternals for TextInput { // For manual scroll updates. if !dirty && self.element_data.scroll_state.map(|scroll_state| scroll_state.is_new()).unwrap_or_default() { - let result = taffy_tree.layout(node).unwrap(); + let result = taffy_tree.layout(node); self.element_data.apply_scroll(result); self.element_data.scroll_state.as_mut().unwrap().mark_old(); } diff --git a/crates/craft_retained/src/elements/text_input/text_input_state.rs b/crates/craft_retained/src/elements/text_input/text_input_state.rs index a72c205e..e8373ba8 100644 --- a/crates/craft_retained/src/elements/text_input/text_input_state.rs +++ b/crates/craft_retained/src/elements/text_input/text_input_state.rs @@ -166,7 +166,7 @@ impl TextInputState { if let Some(id) = self.taffy_node { TAFFY_TREE.with_borrow_mut(|taffy_tree| { - taffy_tree.mark_dirty(id).expect("Failed to mark node dirty"); + taffy_tree.mark_dirty(id); }) } } diff --git a/crates/craft_retained/src/layout/mod.rs b/crates/craft_retained/src/layout/mod.rs index 54df2125..64ee0a6c 100644 --- a/crates/craft_retained/src/layout/mod.rs +++ b/crates/craft_retained/src/layout/mod.rs @@ -1,2 +1,5 @@ pub mod layout_context; pub mod layout_item; +mod taffy_tree; + +pub(crate) use taffy_tree::TaffyTree; \ No newline at end of file diff --git a/crates/craft_retained/src/layout/taffy_tree.rs b/crates/craft_retained/src/layout/taffy_tree.rs new file mode 100644 index 00000000..f89c5996 --- /dev/null +++ b/crates/craft_retained/src/layout/taffy_tree.rs @@ -0,0 +1,113 @@ +use crate::layout::layout_context::{measure_content, LayoutContext}; +use crate::text::text_context::TextContext; +use craft_resource_manager::ResourceManager; +use std::sync::Arc; +use taffy::{Layout, NodeId, PrintTree, Size, Style}; + +pub struct TaffyTree { + inner: taffy::TaffyTree, + /// True if at least one node is dirty. + dirty: bool, +} + +impl TaffyTree { + pub(crate) fn new() -> Self { + Self { + inner: taffy::TaffyTree::::new(), + dirty: true, + } + } + + pub fn new_leaf(&mut self, layout: Style) -> NodeId { + self.inner.new_leaf(layout).unwrap() + } + + pub fn add_child(&mut self, parent: NodeId, child: NodeId) { + self.inner.add_child(parent, child).unwrap(); + } + + pub fn mark_dirty(&mut self, node: NodeId) { + self.inner.mark_dirty(node).unwrap(); + } + + pub fn children(&self, parent: NodeId) -> Vec { + self.inner.children(parent).unwrap() + } + + pub fn set_children(&mut self, parent: NodeId, children: &[NodeId]) { + self.inner.set_children(parent, children).unwrap(); + } + + pub fn compute_layout( + &mut self, + node_id: NodeId, + available_space: Size, + text_context: &mut TextContext, + resource_manager: Arc, + ) { + self.inner + .compute_layout_with_measure( + node_id, + available_space, + |known_dimensions, available_space, _node_id, node_context, style| { + measure_content( + known_dimensions, + available_space, + node_context, + text_context, + resource_manager.clone(), + style, + ) + }, + ) + .unwrap(); + } + + // Remove the entire layout subtree. + + pub fn remove_subtree(&mut self, node: NodeId) { + // Can we avoid this allocation? + let children = self.inner.children(node).unwrap(); + + for child in children { + self.remove_subtree(child); + } + + self.inner.remove(node).map(|_| ()).unwrap(); + } + + #[inline] + pub fn set_style(&mut self, node: NodeId, style: Style) { + self.inner.set_style(node, style).unwrap(); + } + + /// Creates and adds a new unattached leaf node to the tree, and returns the [`NodeId`] of the new node + /// + /// Creates and adds a new leaf node with a supplied context + pub fn new_leaf_with_context(&mut self, style: Style, context: LayoutContext) -> NodeId { + self.inner.new_leaf_with_context(style, context).unwrap() + } + + /// Sets the context data associated with the node + #[inline] + pub fn set_node_context(&mut self, node: NodeId, measure: Option) { + self.inner.set_node_context(node, measure).unwrap(); + } + + /// Return this node layout relative to its parent + #[inline] + pub fn layout(&self, node: NodeId) -> &Layout { + self.inner.layout(node).unwrap() + } + + #[inline(always)] + pub fn get_has_new_layout(&self, node_id: NodeId) -> bool { + self.inner.get_has_new_layout(node_id) + } + + /// Marks the layout of this node as seen + #[inline] + pub fn mark_seen(&mut self, node: NodeId) { + self.inner.mark_seen(node); + } +} From 933108932bcd978fcc0b3b59ec60386140223960 Mon Sep 17 00:00:00 2001 From: "Austin M. Reppert" Date: Tue, 30 Dec 2025 10:10:56 -0500 Subject: [PATCH 019/143] Reduce tolerance --- crates/craft_primitives/src/geometry/borders.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/craft_primitives/src/geometry/borders.rs b/crates/craft_primitives/src/geometry/borders.rs index 9ee4feb9..e7aedb0d 100644 --- a/crates/craft_primitives/src/geometry/borders.rs +++ b/crates/craft_primitives/src/geometry/borders.rs @@ -216,7 +216,7 @@ impl CssRoundedRect { let mut current_arc = FIRST_ARC; if let Some(arc) = &self.corners_arcs[corner][current_arc] { - path.extend(&arc.to_path(0.01)); + path.extend(&arc.to_path(0.1)); } else { path.move_to(rect_corner); } @@ -224,7 +224,7 @@ impl CssRoundedRect { current_arc = NEXT_ARC[current_arc]; if let Some(arc) = &self.corners_arcs[next_corner][current_arc] { - extend_path_with_arc(&mut path, &arc.to_path(0.01)); + extend_path_with_arc(&mut path, &arc.to_path(0.1)); } else { path.line_to(next_rect_corner); } @@ -232,7 +232,7 @@ impl CssRoundedRect { current_arc = NEXT_ARC[current_arc]; if let Some(arc) = &self.corners_arcs[next_corner][current_arc] { - extend_path_with_arc(&mut path, &arc.to_path(0.01)); + extend_path_with_arc(&mut path, &arc.to_path(0.1)); } else { let offset = CORNER_RADIUS_SIGN[next_corner]; let horizontal_side = CORNER_HORIZONTAL_SIDE[next_corner]; @@ -249,7 +249,7 @@ impl CssRoundedRect { current_arc = NEXT_ARC[current_arc]; if let Some(arc) = &self.corners_arcs[corner][current_arc] { - extend_path_with_arc(&mut path, &arc.to_path(0.01)); + extend_path_with_arc(&mut path, &arc.to_path(0.1)); } else { let offset = CORNER_RADIUS_SIGN[corner]; let horizontal_side = CORNER_HORIZONTAL_SIDE[corner]; From 5779880c3e82aa72d4731e0292b9210700b1290e Mon Sep 17 00:00:00 2001 From: "Austin M. Reppert" Date: Tue, 30 Dec 2025 14:38:30 -0500 Subject: [PATCH 020/143] Add dirty flag for apply layout. --- crates/craft_retained/src/app.rs | 44 ++++++++----------- .../craft_retained/src/elements/scrollable.rs | 8 ++-- .../craft_retained/src/layout/taffy_tree.rs | 43 ++++++++++++++++-- 3 files changed, 62 insertions(+), 33 deletions(-) diff --git a/crates/craft_retained/src/app.rs b/crates/craft_retained/src/app.rs index e97cbf54..43cc955f 100644 --- a/crates/craft_retained/src/app.rs +++ b/crates/craft_retained/src/app.rs @@ -61,7 +61,6 @@ thread_local! { pub(crate) static PENDING_RESOURCES: RefCell> = RefCell::new(VecDeque::new()); pub(crate) static IN_PROGRESS_RESOURCES: RefCell> = RefCell::new(VecDeque::new()); pub(crate) static FOCUS: RefCell>>> = RefCell::new(None); - pub(crate) static LAYOUT_DIRTY: RefCell = RefCell::new(true); /// An event queue that users or elements can manipulate. Cleared at the start and end of every event dispatch. static EVENT_DISPATCH_QUEUE: RefCell> = RefCell::new(VecDeque::with_capacity(10)); } @@ -636,8 +635,6 @@ fn layout( scale_factor: f64, pointer: Option, ) -> NodeId { - let dirty = LAYOUT_DIRTY.with_borrow(|status| *status); - let root_node = root_element .borrow() .element_data() @@ -645,52 +642,49 @@ fn layout( .taffy_node_id .expect("A root element must have a layout node."); - if !dirty { - return root_node; - } - let available_space: taffy::Size = taffy::Size { width: AvailableSpace::Definite(window_size.width), height: AvailableSpace::Definite(window_size.height), }; - { + if taffy_tree.is_layout_dirty() { /*let span = span!(Level::INFO, "layout(taffy)"); let _enter = span.enter();*/ - //println!("before layout"); - //taffy_tree.print_tree(root_node); - taffy_tree.compute_layout(root_node, available_space, text_context, resource_manager.clone()) + taffy_tree.compute_layout(root_node, available_space, text_context, resource_manager.clone()); } - //println!("after layout"); - //taffy_tree.print_tree(root_node); - let transform = Affine::IDENTITY; - - let mut layout_order: u32 = 0; - { + if taffy_tree.is_apply_layout_dirty() { /*let span = span!(Level::INFO, "layout(apply)"); - let _enter = span.enter();*/ + let _enter = span.enter();*/ + + // TODO: move into taffy_tree + let mut layout_order: u32 = 0; root_element.borrow_mut().apply_layout( taffy_tree, origin, &mut layout_order, - transform, + Affine::IDENTITY, pointer, text_context, None, scale_factor, ); + taffy_tree.apply_layout(); } - LAYOUT_DIRTY.with_borrow_mut(|status| { - *status = false; - }); - root_node } +#[inline] pub fn request_layout() { - LAYOUT_DIRTY.with_borrow_mut(|status| { - *status = true; + TAFFY_TREE.with_borrow_mut(|taffy_tree| { + taffy_tree.request_layout(); + }); +} + +#[inline] +pub fn request_apply_layout() { + TAFFY_TREE.with_borrow_mut(|taffy_tree| { + taffy_tree.request_apply_layout(); }); } diff --git a/crates/craft_retained/src/elements/scrollable.rs b/crates/craft_retained/src/elements/scrollable.rs index ad468c27..d90ebb60 100644 --- a/crates/craft_retained/src/elements/scrollable.rs +++ b/crates/craft_retained/src/elements/scrollable.rs @@ -3,7 +3,7 @@ use crate::events::{CraftMessage, Event}; use kurbo::Point; use ui_events::pointer::{PointerId, PointerType}; use ui_events::ScrollDelta; -use crate::request_layout; +use crate::app::request_apply_layout; #[allow(clippy::too_many_arguments)] pub(crate) fn on_scroll_events(element: &mut dyn Element, message: &CraftMessage, event: &mut Event) { @@ -27,7 +27,7 @@ pub(crate) fn on_scroll_events(element: &mut dyn Element, message: &CraftMessage let current_scroll_y = state.scroll_y(); state.set_scroll_y((current_scroll_y + delta).clamp(0.0, max_scroll_y)); - request_layout(); + request_apply_layout(); event.prevent_propagate(); event.prevent_defaults(); @@ -78,7 +78,7 @@ pub(crate) fn on_scroll_events(element: &mut dyn Element, message: &CraftMessage let scroll_y = percent * element_data.layout_item.max_scroll_y; state.set_scroll_y(scroll_y.clamp(0.0, element_data.layout_item.max_scroll_y)); - request_layout(); + request_apply_layout(); event.prevent_propagate(); event.prevent_defaults(); @@ -115,7 +115,7 @@ pub(crate) fn on_scroll_events(element: &mut dyn Element, message: &CraftMessage let current_scroll_y = state.scroll_y(); state.set_scroll_y((current_scroll_y + delta).clamp(0.0, max_scroll_y)); - request_layout(); + request_apply_layout(); state.scroll_click = Some(Point::new(click.x, pointer_motion.current.position.y)); event.prevent_propagate(); event.prevent_defaults(); diff --git a/crates/craft_retained/src/layout/taffy_tree.rs b/crates/craft_retained/src/layout/taffy_tree.rs index f89c5996..9bb29b8e 100644 --- a/crates/craft_retained/src/layout/taffy_tree.rs +++ b/crates/craft_retained/src/layout/taffy_tree.rs @@ -7,14 +7,17 @@ use taffy::{Layout, NodeId, PrintTree, Size, Style}; pub struct TaffyTree { inner: taffy::TaffyTree, /// True if at least one node is dirty. - dirty: bool, + is_layout_dirty: bool, + /// True if the layout should be re-applied. + is_apply_layout_dirty: bool, } impl TaffyTree { pub(crate) fn new() -> Self { Self { inner: taffy::TaffyTree::::new(), - dirty: true, + is_layout_dirty: true, + is_apply_layout_dirty: true, } } @@ -24,10 +27,12 @@ impl TaffyTree { pub fn add_child(&mut self, parent: NodeId, child: NodeId) { self.inner.add_child(parent, child).unwrap(); + self.request_layout(); } pub fn mark_dirty(&mut self, node: NodeId) { self.inner.mark_dirty(node).unwrap(); + self.request_layout(); } pub fn children(&self, parent: NodeId) -> Vec { @@ -36,6 +41,7 @@ impl TaffyTree { pub fn set_children(&mut self, parent: NodeId, children: &[NodeId]) { self.inner.set_children(parent, children).unwrap(); + self.request_layout(); } pub fn compute_layout( @@ -61,10 +67,10 @@ impl TaffyTree { }, ) .unwrap(); + self.is_layout_dirty = false; } - // Remove the entire layout subtree. - + /// Remove the entire layout subtree. pub fn remove_subtree(&mut self, node: NodeId) { // Can we avoid this allocation? let children = self.inner.children(node).unwrap(); @@ -74,11 +80,13 @@ impl TaffyTree { } self.inner.remove(node).map(|_| ()).unwrap(); + self.request_layout(); } #[inline] pub fn set_style(&mut self, node: NodeId, style: Style) { self.inner.set_style(node, style).unwrap(); + self.request_layout(); } /// Creates and adds a new unattached leaf node to the tree, and returns the [`NodeId`] of the new node @@ -92,6 +100,7 @@ impl TaffyTree { #[inline] pub fn set_node_context(&mut self, node: NodeId, measure: Option) { self.inner.set_node_context(node, measure).unwrap(); + self.request_layout(); } /// Return this node layout relative to its parent @@ -110,4 +119,30 @@ impl TaffyTree { pub fn mark_seen(&mut self, node: NodeId) { self.inner.mark_seen(node); } + + #[inline(always)] + pub fn request_layout(&mut self) { + self.is_layout_dirty = true; + self.is_apply_layout_dirty = true; + } + + #[inline(always)] + pub fn request_apply_layout(&mut self) { + self.is_layout_dirty = true; + self.is_apply_layout_dirty = true; + } + + #[inline(always)] + pub fn is_layout_dirty(&self) -> bool { + self.is_layout_dirty + } + + #[inline(always)] + pub fn is_apply_layout_dirty(&self) -> bool { + self.is_apply_layout_dirty + } + + pub fn apply_layout(&mut self) { + self.is_apply_layout_dirty = false; + } } From cf16ce3877e8c721cfe31d6273ba8accaab23e08 Mon Sep 17 00:00:00 2001 From: "Austin M. Reppert" Date: Tue, 30 Dec 2025 21:34:11 -0500 Subject: [PATCH 021/143] create element setup helpers --- .../craft_retained/src/elements/container.rs | 17 +-- .../src/elements/element_data.rs | 25 ++++ .../src/elements/element_id_map.rs | 4 + crates/craft_retained/src/elements/text.rs | 132 +++++++++--------- .../craft_retained/src/layout/taffy_tree.rs | 2 +- 5 files changed, 100 insertions(+), 80 deletions(-) diff --git a/crates/craft_retained/src/elements/container.rs b/crates/craft_retained/src/elements/container.rs index 14357476..79b3d6d9 100644 --- a/crates/craft_retained/src/elements/container.rs +++ b/crates/craft_retained/src/elements/container.rs @@ -1,6 +1,6 @@ //! Stores one or more elements. -use crate::app::{ELEMENTS, TAFFY_TREE}; +use crate::app::{TAFFY_TREE}; use crate::elements::core::{resolve_clip_for_scrollable, ElementInternals}; use crate::elements::element_data::ElementData; use crate::elements::{scrollable, Element}; @@ -11,7 +11,6 @@ use craft_renderer::RenderList; use kurbo::{Affine, Point}; use std::any::Any; use std::cell::RefCell; -use std::ops::Deref; use std::rc::{Rc, Weak}; use crate::layout::TaffyTree; @@ -28,18 +27,8 @@ impl Container { element_data: ElementData::new(true), })); - TAFFY_TREE.with_borrow_mut(|taffy_tree| { - let node_id = taffy_tree.new_leaf(me.borrow().style().to_taffy_style()); - me.borrow_mut().element_data.layout_item.taffy_node_id = Some(node_id); - }); - - let me_element: Rc> = me.clone(); - - me.borrow_mut().element_data.me = Some(Rc::downgrade(&me_element)); - - ELEMENTS.with_borrow_mut(|elements| { - elements.insert(me.borrow().deref()); - }); + me.borrow_mut().element_data.crate_layout_node(None); + me.borrow_mut().element_data.set_element(me.clone()); me } diff --git a/crates/craft_retained/src/elements/element_data.rs b/crates/craft_retained/src/elements/element_data.rs index 2f7852d0..0c887363 100644 --- a/crates/craft_retained/src/elements/element_data.rs +++ b/crates/craft_retained/src/elements/element_data.rs @@ -12,6 +12,8 @@ use smol_str::SmolStr; use std::cell::RefCell; use std::rc::{Rc, Weak}; use taffy::{Layout, Overflow}; +use crate::app::{ELEMENTS, TAFFY_TREE}; +use crate::layout::layout_context::LayoutContext; /// Stores common data to most elements. #[derive(Clone)] @@ -60,9 +62,11 @@ pub struct ElementData { } impl ElementData { + pub fn new(scrollable: bool) -> Self { let mut default = Self::default(); + // Create scroll state if needed. if scrollable { default.scroll_state = Some(ScrollState::default()); } @@ -70,6 +74,27 @@ impl ElementData { default } + /// Creates a new taffy node for this element with optional layout context. + pub fn crate_layout_node(&mut self, layout_context: Option) { + TAFFY_TREE.with_borrow_mut(|taffy_tree| { + let style = self.style.to_taffy_style(); + let node_id = if let Some(layout_context) = layout_context { + taffy_tree.new_leaf_with_context(style, layout_context) + } else { + taffy_tree.new_leaf(style) + }; + self.layout_item.taffy_node_id = Some(node_id); + }); + } + + /// Sets element Weak + pub fn set_element(&mut self, element: Rc>) { + self.me = Some(Rc::downgrade(&element)); + ELEMENTS.with_borrow_mut(|elements| { + elements.insert_id(self.internal_id, self.me.clone().unwrap()); + }); + } + /// Computes the scrollbar's tack and thumb layout. pub(crate) fn apply_scroll(&mut self, layout: &Layout) { self.layout_item.scrollbar_size = Size::new(layout.scrollbar_size.width, layout.scrollbar_size.height); diff --git a/crates/craft_retained/src/elements/element_id_map.rs b/crates/craft_retained/src/elements/element_id_map.rs index 0aa1b251..66fbe8db 100644 --- a/crates/craft_retained/src/elements/element_id_map.rs +++ b/crates/craft_retained/src/elements/element_id_map.rs @@ -21,6 +21,10 @@ impl ElementIdMap { self.map.insert(element_data.internal_id, element_data.me.clone().unwrap()) } + pub fn insert_id(&mut self, id: u64, element: Weak>) -> Option>> { + self.map.insert(id, element) + } + pub fn remove(&mut self, element: &dyn Element) -> Option>> { self.map.remove(&element.element_data().internal_id) } diff --git a/crates/craft_retained/src/elements/text.rs b/crates/craft_retained/src/elements/text.rs index dc1f2ea9..62905421 100644 --- a/crates/craft_retained/src/elements/text.rs +++ b/crates/craft_retained/src/elements/text.rs @@ -13,32 +13,30 @@ use parley::LayoutAccessibility; use parley::{Alignment, AlignmentOptions, ContentWidths, Selection}; use std::any::Any; use std::cell::RefCell; -use std::ops::Deref; use std::rc::{Rc, Weak}; const MAX_CACHE_SIZE: usize = 16; +use crate::elements::core::ElementInternals; +#[cfg(feature = "accesskit")] +use crate::elements::element_id::create_unique_element_id; +use crate::elements::Element; +use crate::layout::TaffyTree; #[cfg(feature = "accesskit")] use accesskit::{Action, Role}; +use craft_primitives::ColorBrush; +use craft_renderer::text_renderer_data::TextData; use kurbo::Affine; use rustc_hash::FxHashMap; +use smol_str::{SmolStr, ToSmolStr}; #[cfg(not(target_arch = "wasm32"))] use std::time; use taffy::{AvailableSpace, Size}; use time::{Duration, Instant}; -use winit::dpi; +use ui_events::pointer::{PointerButton, PointerId}; #[cfg(target_arch = "wasm32")] use web_time as time; -use crate::app::{ELEMENTS, TAFFY_TREE}; -use crate::elements::core::ElementInternals; -#[cfg(feature = "accesskit")] -use crate::elements::element_id::create_unique_element_id; -use crate::elements::Element; -use craft_primitives::ColorBrush; -use craft_renderer::text_renderer_data::TextData; -use smol_str::{SmolStr, ToSmolStr}; -use ui_events::pointer::{PointerButton, PointerId}; -use crate::layout::TaffyTree; +use winit::dpi; // A stateful element that shows text. #[derive(Clone, Default)] @@ -49,7 +47,7 @@ pub struct Text { me: Option>>, } -#[derive(Clone, Default)] +#[derive(Clone)] pub struct TextState { text: SmolStr, scale_factor: f32, @@ -79,52 +77,22 @@ pub struct TextState { impl Text { pub fn new(text: &str) -> Rc> { - let text_state = TextState { - text: SmolStr::new(""), - scale_factor: 1.0, - selection: Selection::default(), - text_render: None, - layout: None, - cache: Default::default(), - current_layout_key: None, - last_requested_measure_key: None, - current_render_key: None, - content_widths: None, - last_click_time: None, - click_count: 0, - pointer_down: false, - cursor_pos: Point::new(0.0, 0.0), - start_time: None, - blink_period: Default::default(), - is_layout_dirty: false, - is_render_dirty: false, - }; let me = Rc::new(RefCell::new(Text { element_data: Default::default(), selectable: true, - state: text_state, + state: TextState::default(), me: None, })); - let me2 = me.clone(); - me.borrow_mut().me = Some(Rc::downgrade(&me2)); - let me_element: Rc> = me.clone(); - me.borrow_mut().element_data.me = Some(Rc::downgrade(&me_element)); + me.borrow_mut().me = Some(Rc::downgrade(&me.clone())); + let text_context = Some(LayoutContext::Text(TaffyTextContext { + element: me.borrow().me.clone().unwrap(), + })); + me.borrow_mut().element_data.crate_layout_node(text_context); + me.borrow_mut().element_data.set_element(me.clone()); me.borrow_mut().text(text); - TAFFY_TREE.with_borrow_mut(|taffy_tree| { - let context = LayoutContext::Text(TaffyTextContext{ - element: me.borrow().me.clone().unwrap() - }); - let node_id = taffy_tree.new_leaf_with_context(me.borrow().style().to_taffy_style(), context); - me.borrow_mut().element_data.layout_item.taffy_node_id = Some(node_id); - }); - - ELEMENTS.with_borrow_mut(|elements| { - elements.insert(me.borrow().deref()); - }); - me } @@ -223,7 +191,8 @@ impl ElementInternals for Text { parent_index: Option, scale_factor: f64, ) { - let padding_box = self.element_data.layout_item.computed_box_transformed.padding_rectangle().scale(scale_factor); + let padding_box = + self.element_data.layout_item.computed_box_transformed.padding_rectangle().scale(scale_factor); let state: &mut TextState = &mut self.state; if state.layout.is_none() { @@ -282,7 +251,9 @@ impl ElementInternals for Text { let result = taffy_tree.layout(node); let has_new_layout = taffy_tree.get_has_new_layout(node); - let dirty = has_new_layout || transform != self.element_data.layout_item.get_transform() || position != self.element_data.layout_item.position; + let dirty = has_new_layout + || transform != self.element_data.layout_item.get_transform() + || position != self.element_data.layout_item.position; self.element_data.layout_item.has_new_layout = has_new_layout; if dirty { self.resolve_box(position, transform, result, z_index); @@ -303,7 +274,7 @@ impl ElementInternals for Text { ); } - state.try_update_text_render(text_context); + state.try_update_text_render(text_context, self.element_data.style.selection_color()); } fn on_event( @@ -317,7 +288,6 @@ impl ElementInternals for Text { return; } - let _content_rect = self.computed_box().content_rectangle(); // Handle selection. @@ -367,7 +337,8 @@ impl ElementInternals for Text { CraftMessage::PointerMovedEvent(pointer_moved) => { let prev_pos = state.cursor_pos; // NOTE: Cursor position should be relative to the top left of the text box. - state.cursor_pos = pointer_moved.current.logical_point() - kurbo::Vec2::new(text_position.x as f64, text_position.y as f64); + state.cursor_pos = pointer_moved.current.logical_point() + - kurbo::Vec2::new(text_position.x as f64, text_position.y as f64); // macOS seems to generate a spurious move after selecting word? if state.pointer_down && prev_pos != state.cursor_pos { state.update_text_selection(self.element_data.style.selection_color()); @@ -431,7 +402,7 @@ impl TextState { AvailableSpace::Definite(width) => { let scaled_width: f32 = dpi::PhysicalUnit::from_logical::(width, self.scale_factor as f64).0; Some(scaled_width.clamp(content_widths.min, content_widths.max)) - }, + } }); let height_constraint = known_dimensions.height.or(match available_space.height { @@ -440,7 +411,7 @@ impl TextState { AvailableSpace::Definite(height) => { let scaled_height = dpi::PhysicalUnit::from_logical::(height, self.scale_factor as f64).0; Some(scaled_height) - }, + } }); let layout = self.layout.as_mut().unwrap(); @@ -469,7 +440,7 @@ impl TextState { size } - pub fn try_update_text_render(&mut self, _text_context: &mut TextContext) { + pub fn try_update_text_render(&mut self, _text_context: &mut TextContext, selection_color: Color) { if self.current_render_key == self.current_layout_key { return; } @@ -477,6 +448,8 @@ impl TextState { let layout = self.layout.as_ref().unwrap(); self.text_render = Some(text_render_data::from_editor(layout)); self.current_render_key = self.current_layout_key; + + self.update_text_selection(selection_color); } pub fn cursor_reset(&mut self) { @@ -520,14 +493,18 @@ impl TextState { } fn update_text_selection(&mut self, selection_color: Color) { - let layout = self.layout.as_ref().unwrap(); - let text_renderer = self.text_render.as_mut().unwrap(); - for line in text_renderer.lines.iter_mut() { - line.selections.clear(); + if let Some(layout) = self.layout.as_ref() { + let text_renderer = self.text_render.as_mut().unwrap(); + for line in text_renderer.lines.iter_mut() { + line.selections.clear(); + } + self.selection.geometry_with(layout, |rect, line| { + text_renderer.lines[line].selections.push(( + Rectangle::new(rect.x0 as f32, rect.y0 as f32, rect.width() as f32, rect.height() as f32), + selection_color, + )); + }); } - self.selection.geometry_with(layout, |rect, line| { - text_renderer.lines[line].selections.push((Rectangle::new(rect.x0 as f32, rect.y0 as f32, rect.width() as f32, rect.height() as f32), selection_color)); - }); } } @@ -535,4 +512,29 @@ impl TextData for Text { fn get_text_renderer(&self) -> Option<&TextRender> { self.state.text_render.as_ref() } +} + +impl Default for TextState { + fn default() -> Self { + Self { + text: SmolStr::new(""), + scale_factor: 1.0, + selection: Selection::default(), + text_render: None, + layout: None, + cache: Default::default(), + current_layout_key: None, + last_requested_measure_key: None, + current_render_key: None, + content_widths: None, + last_click_time: None, + click_count: 0, + pointer_down: false, + cursor_pos: Point::new(0.0, 0.0), + start_time: None, + blink_period: Default::default(), + is_layout_dirty: false, + is_render_dirty: false, + } + } } \ No newline at end of file diff --git a/crates/craft_retained/src/layout/taffy_tree.rs b/crates/craft_retained/src/layout/taffy_tree.rs index 9bb29b8e..49122dbb 100644 --- a/crates/craft_retained/src/layout/taffy_tree.rs +++ b/crates/craft_retained/src/layout/taffy_tree.rs @@ -145,4 +145,4 @@ impl TaffyTree { pub fn apply_layout(&mut self) { self.is_apply_layout_dirty = false; } -} +} \ No newline at end of file From 9dfea4acf0d82e9ee6fd11202cc0763ca0b799ba Mon Sep 17 00:00:00 2001 From: "Austin M. Reppert" Date: Tue, 30 Dec 2025 23:26:53 -0500 Subject: [PATCH 022/143] Fix swap --- .../craft_retained/src/elements/container.rs | 22 ++++++++++++------- crates/craft_retained/src/elements/element.rs | 4 ++-- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/crates/craft_retained/src/elements/container.rs b/crates/craft_retained/src/elements/container.rs index 79b3d6d9..f513c17a 100644 --- a/crates/craft_retained/src/elements/container.rs +++ b/crates/craft_retained/src/elements/container.rs @@ -1,10 +1,11 @@ //! Stores one or more elements. -use crate::app::{TAFFY_TREE}; +use crate::app::TAFFY_TREE; use crate::elements::core::{resolve_clip_for_scrollable, ElementInternals}; use crate::elements::element_data::ElementData; use crate::elements::{scrollable, Element}; use crate::events::{CraftMessage, Event}; +use crate::layout::TaffyTree; use crate::text::text_context::TextContext; use craft_primitives::geometry::Rectangle; use craft_renderer::RenderList; @@ -12,7 +13,6 @@ use kurbo::{Affine, Point}; use std::any::Any; use std::cell::RefCell; use std::rc::{Rc, Weak}; -use crate::layout::TaffyTree; /// Stores one or more elements. /// @@ -59,8 +59,6 @@ impl Element for Container { let child_id = child.borrow().element_data().layout_item.taffy_node_id; if let Some(child_id) = child_id { taffy_tree.add_child(parent_id, child_id); - - taffy_tree.mark_dirty(parent_id); } }); @@ -93,7 +91,6 @@ impl Element for Container { taffy_tree.add_child(parent_id, child_id); } } - taffy_tree.mark_dirty(parent_id); }); self @@ -109,7 +106,6 @@ impl Element for Container { } impl ElementInternals for Container { - fn apply_layout( &mut self, taffy_tree: &mut TaffyTree, @@ -125,7 +121,9 @@ impl ElementInternals for Container { let layout = taffy_tree.layout(node); let has_new_layout = taffy_tree.get_has_new_layout(node); - let dirty = has_new_layout || transform != self.element_data.layout_item.get_transform() || position != self.element_data.layout_item.position ; + let dirty = has_new_layout + || transform != self.element_data.layout_item.get_transform() + || position != self.element_data.layout_item.position; self.element_data.layout_item.has_new_layout = has_new_layout; if dirty { @@ -150,7 +148,15 @@ impl ElementInternals for Container { let scroll_y = self.element_data.scroll().map_or(0.0, |s| s.scroll_y() as f64); let child_transform = Affine::translate((0.0, -scroll_y)); - self.apply_layout_children(taffy_tree, z_index, transform * child_transform, pointer, text_context, scale_factor, false) + self.apply_layout_children( + taffy_tree, + z_index, + transform * child_transform, + pointer, + text_context, + scale_factor, + false, + ) } fn draw( diff --git a/crates/craft_retained/src/elements/element.rs b/crates/craft_retained/src/elements/element.rs index 3b6b0b46..67f8ca69 100644 --- a/crates/craft_retained/src/elements/element.rs +++ b/crates/craft_retained/src/elements/element.rs @@ -11,7 +11,7 @@ use crate::style::{ AlignItems, Display, FlexDirection, FontFamily, FontStyle, JustifyContent, ScrollbarColor, Style, Underline, Unit, Weight, Wrap, }; -use crate::{request_layout, CraftError}; +use crate::{CraftError}; use craft_primitives::geometry::Point; use craft_primitives::geometry::{ElementBox, TrblRectangle}; use craft_primitives::Color; @@ -64,7 +64,7 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { taffy_tree.set_children(parent_id, &tchildren); taffy_tree.mark_dirty(parent_id); - request_layout(); + taffy_tree.request_layout(); } }); From 8431c5219478e2771f7089c4bd003732c7f60ce3 Mon Sep 17 00:00:00 2001 From: NoahR02 Date: Wed, 31 Dec 2025 01:36:28 -0500 Subject: [PATCH 023/143] Fix Image --- crates/craft_retained/src/elements/image.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/craft_retained/src/elements/image.rs b/crates/craft_retained/src/elements/image.rs index 740e0505..75927316 100644 --- a/crates/craft_retained/src/elements/image.rs +++ b/crates/craft_retained/src/elements/image.rs @@ -23,7 +23,6 @@ pub struct Image { is_image_dirty: bool, resource_identifier: ResourceIdentifier, element_data: ElementData, - me: Option>>, } impl Image { @@ -32,7 +31,6 @@ impl Image { is_image_dirty: false, resource_identifier: resource_identifier.clone(), element_data: ElementData::new(true), - me: None, })); let resource_identifier_2= resource_identifier.clone(); @@ -46,7 +44,7 @@ impl Image { pending_resources.push_back((resource_identifier, ResourceType::Image)); }); - me.borrow_mut().me = Some(Rc::downgrade(&me.clone())); + me.borrow_mut().element_data.set_element(me.clone()); ELEMENTS.with_borrow_mut(|elements| { elements.insert(me.borrow().deref()); From 28402770ce9f19610155a298b0106ac08b845f8e Mon Sep 17 00:00:00 2001 From: "Austin M. Reppert" Date: Wed, 31 Dec 2025 07:55:45 -0500 Subject: [PATCH 024/143] Remove unused import --- crates/craft_retained/src/elements/image.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/craft_retained/src/elements/image.rs b/crates/craft_retained/src/elements/image.rs index 75927316..c31d976d 100644 --- a/crates/craft_retained/src/elements/image.rs +++ b/crates/craft_retained/src/elements/image.rs @@ -15,7 +15,7 @@ use kurbo::{Affine, Point}; use std::any::Any; use std::cell::RefCell; use std::ops::Deref; -use std::rc::{Rc, Weak}; +use std::rc::{Rc}; use crate::layout::TaffyTree; /// Displays an image. From 999e909fad0d9d241c9af32f452eb28c0a01513b Mon Sep 17 00:00:00 2001 From: "Austin M. Reppert" Date: Wed, 31 Dec 2025 16:02:42 -0500 Subject: [PATCH 025/143] Make me mandatory in element data. --- .../craft_retained/src/elements/container.rs | 13 ++-- crates/craft_retained/src/elements/element.rs | 12 ++-- .../src/elements/element_data.rs | 72 ++++++++----------- .../src/elements/element_id_map.rs | 2 +- crates/craft_retained/src/elements/image.rs | 29 +++----- .../src/elements/slider/slider.rs | 27 +++---- crates/craft_retained/src/elements/text.rs | 16 ++--- .../src/elements/text_input/mod.rs | 54 ++++++-------- .../elements/text_input/text_input_state.rs | 4 -- 9 files changed, 95 insertions(+), 134 deletions(-) diff --git a/crates/craft_retained/src/elements/container.rs b/crates/craft_retained/src/elements/container.rs index f513c17a..3dc464ef 100644 --- a/crates/craft_retained/src/elements/container.rs +++ b/crates/craft_retained/src/elements/container.rs @@ -23,12 +23,13 @@ pub struct Container { impl Container { pub fn new() -> Rc> { - let me = Rc::new(RefCell::new(Self { - element_data: ElementData::new(true), - })); + let me = Rc::new_cyclic(|me: &Weak>| { + RefCell::new(Self { + element_data: ElementData::new(me.clone(), true), + }) + }); me.borrow_mut().element_data.crate_layout_node(None); - me.borrow_mut().element_data.set_element(me.clone()); me } @@ -49,7 +50,7 @@ impl Element for Container { where Self: Sized, { - let me: Weak> = self.element_data.me.clone().unwrap(); + let me: Weak> = self.element_data.me.clone(); child.borrow_mut().element_data_mut().parent = Some(me); self.element_data.children.push(child.clone()); @@ -74,7 +75,7 @@ impl Element for Container { where Self: Sized, { - let me: Weak> = self.element_data.me.clone().unwrap(); + let me: Weak> = self.element_data.me.clone(); let children: Vec<_> = children.into_iter().collect(); for child in &children { diff --git a/crates/craft_retained/src/elements/element.rs b/crates/craft_retained/src/elements/element.rs index 67f8ca69..b073cc4b 100644 --- a/crates/craft_retained/src/elements/element.rs +++ b/crates/craft_retained/src/elements/element.rs @@ -114,10 +114,10 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { elements.remove_id(node.borrow().element_data().internal_id); document .pointer_captures - .retain(|_, v| !Weak::ptr_eq(v, &node.borrow().element_data().me.as_ref().unwrap())); + .retain(|_, v| !Weak::ptr_eq(v, &node.borrow().element_data().me)); document .pending_pointer_captures - .retain(|_, v| !Weak::ptr_eq(v, &node.borrow().element_data().me.as_ref().unwrap())); + .retain(|_, v| !Weak::ptr_eq(v, &node.borrow().element_data().me)); for child in node.borrow().children() { remove_element_from_document(child.clone(), document, elements); } @@ -275,7 +275,7 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { // 5. If the pointer is not in the active buttons state or the element's node document is not the active document of the pointer, then terminate these steps. // TODO (POINTER CAPTURE) // 6. For the specified pointerId, set the pending pointer capture target override to the Element on which this method was invoked. - current_doc.pending_pointer_captures.insert(pointer_id, self.element_data().me.clone().unwrap()); + current_doc.pending_pointer_captures.insert(pointer_id, self.element_data().me.clone()); }); } @@ -302,7 +302,7 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { DOCUMENTS.with_borrow_mut(|docs| { let current_doc = docs.get_current_document(); current_doc.pending_pointer_captures.get(&pointer_id).cloned().map(|w| w.as_ptr()) - == self.element_data().me.clone().map(|w| w.as_ptr()) + == Some(self.element_data().me.clone().as_ptr()) }) } @@ -798,7 +798,7 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { { // Todo: check if the element is focusable. Should we return a result? FOCUS.with_borrow_mut(|focus| { - *focus = self.element_data().me.clone(); + *focus = Some(self.element_data().me.clone()); }); } @@ -812,7 +812,7 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { let focus_element = focus_element.unwrap(); - Weak::ptr_eq(&focus_element, self.element_data().me.as_ref().unwrap()) + Weak::ptr_eq(&focus_element, &self.element_data().me) } /// Removes focus if the element has focus. diff --git a/crates/craft_retained/src/elements/element_data.rs b/crates/craft_retained/src/elements/element_data.rs index 0c887363..2ecedb47 100644 --- a/crates/craft_retained/src/elements/element_data.rs +++ b/crates/craft_retained/src/elements/element_data.rs @@ -50,7 +50,7 @@ pub struct ElementData { pub on_slider_value_changed: Vec, pub on_pointer_enter: Vec, pub on_pointer_leave: Vec, - pub(crate) me: Option>>, + pub(crate) me: Weak>, pub on_got_pointer_capture: Vec, pub on_lost_pointer_capture: Vec, pub on_pointer_button_down: Vec, @@ -63,14 +63,42 @@ pub struct ElementData { impl ElementData { - pub fn new(scrollable: bool) -> Self { - let mut default = Self::default(); + pub fn new(me: Weak>, scrollable: bool) -> Self { + let mut default = Self { + current_state: Default::default(), + style: Default::default(), + layout_item: Default::default(), + hover_style: None, + pressed_style: None, + disabled_style: None, + focused_style: None, + children: Default::default(), + id: None, + internal_id: create_unique_element_id(), + animations: None, + parent: None, + on_slider_value_changed: vec![], + on_pointer_enter: vec![], + on_pointer_leave: vec![], + me, + on_got_pointer_capture: vec![], + on_lost_pointer_capture: vec![], + on_pointer_button_down: vec![], + on_pointer_button_up: vec![], + on_pointer_moved: vec![], + on_keyboard_input: vec![], + scroll_state: None, + }; // Create scroll state if needed. if scrollable { default.scroll_state = Some(ScrollState::default()); } + ELEMENTS.with_borrow_mut(|elements| { + elements.insert_id(default.internal_id, default.me.clone()); + }); + default } @@ -87,14 +115,6 @@ impl ElementData { }); } - /// Sets element Weak - pub fn set_element(&mut self, element: Rc>) { - self.me = Some(Rc::downgrade(&element)); - ELEMENTS.with_borrow_mut(|elements| { - elements.insert_id(self.internal_id, self.me.clone().unwrap()); - }); - } - /// Computes the scrollbar's tack and thumb layout. pub(crate) fn apply_scroll(&mut self, layout: &Layout) { self.layout_item.scrollbar_size = Size::new(layout.scrollbar_size.width, layout.scrollbar_size.height); @@ -157,36 +177,6 @@ impl ElementData { } } -impl Default for ElementData { - fn default() -> Self { - Self { - current_state: Default::default(), - style: Default::default(), - layout_item: Default::default(), - hover_style: None, - pressed_style: None, - disabled_style: None, - focused_style: None, - children: Default::default(), - id: None, - internal_id: create_unique_element_id(), - animations: None, - parent: None, - on_slider_value_changed: vec![], - on_pointer_enter: vec![], - on_pointer_leave: vec![], - me: None, - on_got_pointer_capture: vec![], - on_lost_pointer_capture: vec![], - on_pointer_button_down: vec![], - on_pointer_button_up: vec![], - on_pointer_moved: vec![], - on_keyboard_input: vec![], - scroll_state: None, - } - } -} - impl ElementData { pub fn is_scrollable(&self) -> bool { self.style.overflow()[1] == taffy::Overflow::Scroll diff --git a/crates/craft_retained/src/elements/element_id_map.rs b/crates/craft_retained/src/elements/element_id_map.rs index 66fbe8db..40c579f8 100644 --- a/crates/craft_retained/src/elements/element_id_map.rs +++ b/crates/craft_retained/src/elements/element_id_map.rs @@ -18,7 +18,7 @@ impl ElementIdMap { pub fn insert(&mut self, element: &dyn Element) -> Option>> { let element_data = element.element_data(); - self.map.insert(element_data.internal_id, element_data.me.clone().unwrap()) + self.map.insert(element_data.internal_id, element_data.me.clone()) } pub fn insert_id(&mut self, id: u64, element: Weak>) -> Option>> { diff --git a/crates/craft_retained/src/elements/image.rs b/crates/craft_retained/src/elements/image.rs index c31d976d..33a60c2b 100644 --- a/crates/craft_retained/src/elements/image.rs +++ b/crates/craft_retained/src/elements/image.rs @@ -6,6 +6,7 @@ use crate::elements::core::ElementInternals; use crate::elements::element_data::ElementData; use crate::elements::Element; use crate::layout::layout_context::{ImageContext, LayoutContext}; +use crate::layout::TaffyTree; use crate::text::text_context::TextContext; use craft_primitives::geometry::Rectangle; use craft_renderer::RenderList; @@ -15,8 +16,7 @@ use kurbo::{Affine, Point}; use std::any::Any; use std::cell::RefCell; use std::ops::Deref; -use std::rc::{Rc}; -use crate::layout::TaffyTree; +use std::rc::{Rc, Weak}; /// Displays an image. pub struct Image { @@ -27,25 +27,21 @@ pub struct Image { impl Image { pub fn new(resource_identifier: ResourceIdentifier) -> Rc> { - let me = Rc::new(RefCell::new(Self { - is_image_dirty: false, - resource_identifier: resource_identifier.clone(), - element_data: ElementData::new(true), - })); - - let resource_identifier_2= resource_identifier.clone(); - TAFFY_TREE.with_borrow_mut(|taffy_tree| { - let context = LayoutContext::Image(ImageContext::new(resource_identifier_2)); - let node_id = taffy_tree.new_leaf_with_context(me.borrow().style().to_taffy_style(), context); - me.borrow_mut().element_data.layout_item.taffy_node_id = Some(node_id); + let me = Rc::new_cyclic(|me: &Weak>| { + RefCell::new(Self { + is_image_dirty: false, + resource_identifier: resource_identifier.clone(), + element_data: ElementData::new(me.clone(), false), + }) }); + let layout_context = Some(LayoutContext::Image(ImageContext::new(resource_identifier.clone()))); + me.borrow_mut().element_data.crate_layout_node(layout_context); + PENDING_RESOURCES.with_borrow_mut(|pending_resources| { pending_resources.push_back((resource_identifier, ResourceType::Image)); }); - me.borrow_mut().element_data.set_element(me.clone()); - ELEMENTS.with_borrow_mut(|elements| { elements.insert(me.borrow().deref()); }); @@ -69,7 +65,6 @@ impl Image { pub fn get_image(&self) -> &ResourceIdentifier { &self.resource_identifier } - } impl crate::elements::core::ElementData for Image { @@ -83,7 +78,6 @@ impl crate::elements::core::ElementData for Image { } impl Element for Image { - fn as_any(&self) -> &dyn Any { self } @@ -94,7 +88,6 @@ impl Element for Image { } impl ElementInternals for Image { - fn apply_layout( &mut self, taffy_tree: &mut TaffyTree, diff --git a/crates/craft_retained/src/elements/slider/slider.rs b/crates/craft_retained/src/elements/slider/slider.rs index 0d3ad68a..8cd3b1d3 100644 --- a/crates/craft_retained/src/elements/slider/slider.rs +++ b/crates/craft_retained/src/elements/slider/slider.rs @@ -28,7 +28,6 @@ pub enum SliderDirection { pub struct Slider { element_data: ElementData, - me: Option>>, step: f64, min: f64, @@ -49,9 +48,8 @@ pub struct Slider { impl Slider { pub fn new(thumb_size: f32) -> Rc> { - let me = Rc::new(RefCell::new(Self { - element_data: ElementData::new(true), - me: None, + let me = Rc::new_cyclic(|me: &Weak>| RefCell::new(Self { + element_data: ElementData::new(me.clone(), false), step: 1.0, min: 0.0, max: 100.0, @@ -87,10 +85,6 @@ impl Slider { me.borrow_mut().element_data.layout_item.taffy_node_id = Some(node_id); }); - let me_element: Rc> = me.clone(); - me.borrow_mut().me = Some(Rc::downgrade(&me.clone())); - me.borrow_mut().element_data.me = Some(Rc::downgrade(&me_element)); - ELEMENTS.with_borrow_mut(|elements| { elements.insert(me.borrow().deref()); }); @@ -205,15 +199,6 @@ impl crate::elements::core::ElementData for Slider { } impl Element for Slider { - - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - fn in_bounds(&self, point: Point) -> bool { let element_data = &self.element_data; let rect = element_data.layout_item.computed_box_transformed.border_rectangle(); @@ -235,6 +220,14 @@ impl Element for Slider { rect.contains(&point) } } + + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } } impl ElementInternals for Slider { diff --git a/crates/craft_retained/src/elements/text.rs b/crates/craft_retained/src/elements/text.rs index 62905421..e2b708fb 100644 --- a/crates/craft_retained/src/elements/text.rs +++ b/crates/craft_retained/src/elements/text.rs @@ -39,12 +39,12 @@ use web_time as time; use winit::dpi; // A stateful element that shows text. -#[derive(Clone, Default)] +#[derive(Clone)] pub struct Text { element_data: ElementData, selectable: bool, pub(crate) state: TextState, - me: Option>>, + me: Weak>, } #[derive(Clone)] @@ -77,19 +77,17 @@ pub struct TextState { impl Text { pub fn new(text: &str) -> Rc> { - let me = Rc::new(RefCell::new(Text { - element_data: Default::default(), + let me = Rc::new_cyclic(|me: &Weak>| RefCell::new(Text { + element_data: ElementData::new(me.clone(), false), selectable: true, state: TextState::default(), - me: None, + me: me.clone(), })); - me.borrow_mut().me = Some(Rc::downgrade(&me.clone())); let text_context = Some(LayoutContext::Text(TaffyTextContext { - element: me.borrow().me.clone().unwrap(), + element: me.borrow().me.clone(), })); me.borrow_mut().element_data.crate_layout_node(text_context); - me.borrow_mut().element_data.set_element(me.clone()); me.borrow_mut().text(text); @@ -181,7 +179,7 @@ impl ElementInternals for Text { renderer.draw_rect_outline(self.element_data.layout_item.computed_box_transformed.padding_rectangle(), rgba(255, 0, 0, 100), 1.0); }*/ - renderer.draw_text(self.me.clone().unwrap(), content_rectangle.scale(scale_factor), None, false); + renderer.draw_text(self.me.clone(), content_rectangle.scale(scale_factor), None, false); } #[cfg(feature = "accesskit")] diff --git a/crates/craft_retained/src/elements/text_input/mod.rs b/crates/craft_retained/src/elements/text_input/mod.rs index 43055ccf..e065349c 100644 --- a/crates/craft_retained/src/elements/text_input/mod.rs +++ b/crates/craft_retained/src/elements/text_input/mod.rs @@ -10,29 +10,28 @@ use craft_primitives::Color; use craft_renderer::renderer::{RenderList, TextScroll}; use std::any::Any; use std::cell::RefCell; -use std::ops::{Deref}; +use std::ops::Deref; use std::rc::{Rc, Weak}; -use crate::app::TAFFY_TREE; use crate::elements::core::{resolve_clip_for_scrollable, ElementInternals}; #[cfg(feature = "accesskit")] use crate::elements::element_id::create_unique_element_id; use crate::elements::scrollable; use crate::elements::text_input::text_input_state::TextInputState; use crate::events::{CraftMessage, Event}; +use crate::layout::TaffyTree; use crate::text::text_context::TextContext; use crate::text::text_render_data::TextRender; use crate::text::RangedStyles; use crate::utils::cloneable_any::CloneableAny; use craft_renderer::text_renderer_data::TextData; use kurbo::Affine; -use parley::{BoundingBox}; +use parley::BoundingBox; use ui_events::pointer::PointerButton; use winit::event::Ime; -use crate::layout::TaffyTree; // A stateful element that shows text. -#[derive(Clone, Default)] +#[derive(Clone)] pub struct TextInput { element_data: ElementData, /// Whether the text input will update the editor every update with the user provided text. @@ -42,7 +41,7 @@ pub struct TextInput { pub ranged_styles: Option, pub disabled: bool, pub(crate) state: TextInputState, - me: Option>>, + me: Weak>, } #[allow(dead_code)] @@ -60,34 +59,25 @@ impl TextInput { let text_input_state = TextInputState::default(); - let me = Rc::new(RefCell::new(Self { - text: Some(text.to_string()), - element_data: ElementData::new(true), - use_text_value_on_update: true, - ranged_styles: Some(RangedStyles::new(vec![])), - disabled: false, - state: text_input_state, - me: None, - })); + let me = Rc::new_cyclic(|me: &Weak>| { + RefCell::new(Self { + text: Some(text.to_string()), + element_data: ElementData::new(me.clone(), true), + use_text_value_on_update: true, + ranged_styles: Some(RangedStyles::new(vec![])), + disabled: false, + state: text_input_state, + me: me.clone(), + }) + }); me.borrow_mut().element_data.style = default_style; - let me2 = me.clone(); - me.borrow_mut().me = Some(Rc::downgrade(&me2)); - - let me_element: Rc> = me.clone(); - me.borrow_mut().element_data.me = Some(Rc::downgrade(&me_element)); - me.borrow_mut().text(text); - TAFFY_TREE.with_borrow_mut(|taffy_tree| { - let context = LayoutContext::TextInput(TaffyTextInputContext { - element: me.borrow().me.clone().unwrap(), - }); - let node_id = taffy_tree - .new_leaf_with_context(me.borrow().element_data.current_style().to_taffy_style(), context); - me.borrow_mut().element_data.layout_item.taffy_node_id = Some(node_id); - me.borrow_mut().state.taffy_node(Some(node_id)); - }); + let context = Some(LayoutContext::TextInput(TaffyTextInputContext { + element: me.borrow().me.clone(), + })); + me.borrow_mut().element_data.crate_layout_node(context); ELEMENTS.with_borrow_mut(|elements| { elements.insert(me.borrow().deref()); @@ -198,7 +188,7 @@ impl ElementInternals for TextInput { if self.state.text_render.as_ref().is_some() { renderer.draw_text( - self.me.clone().unwrap(), + self.me.clone(), content_rectangle.scale(scale_factor), text_scroll, self.is_focused(), @@ -426,4 +416,4 @@ impl Element for TextInput { fn as_any_mut(&mut self) -> &mut dyn Any { self } -} \ No newline at end of file +} diff --git a/crates/craft_retained/src/elements/text_input/text_input_state.rs b/crates/craft_retained/src/elements/text_input/text_input_state.rs index e8373ba8..0febbc98 100644 --- a/crates/craft_retained/src/elements/text_input/text_input_state.rs +++ b/crates/craft_retained/src/elements/text_input/text_input_state.rs @@ -248,10 +248,6 @@ impl TextInputState { size } - pub fn taffy_node(&mut self, taffy_node: Option) { - self.taffy_node = taffy_node; - } - #[allow(dead_code)] pub fn get_cursor_link(&self, cursor_pos: Point, element: &TextInput) -> Option { if let Some(ranged_styles) = &element.ranged_styles { From c3ae0a915eecbafec58f3264eeb090e91ea0aa42 Mon Sep 17 00:00:00 2001 From: "Austin M. Reppert" Date: Wed, 31 Dec 2025 16:47:49 -0500 Subject: [PATCH 026/143] start multi-window --- Cargo.lock | 9 + Cargo.toml | 1 + crates/craft_retained/src/app.rs | 3 + .../craft_retained/src/craft_winit_state.rs | 6 +- .../craft_retained/src/elements/container.rs | 5 +- crates/craft_retained/src/elements/mod.rs | 4 +- crates/craft_retained/src/elements/window.rs | 210 ++++++++++++++++++ crates/craft_retained/src/lib.rs | 1 + crates/craft_retained/src/window_manager.rs | 31 +++ examples/multiwindow/Cargo.toml | 25 +++ examples/multiwindow/main.rs | 104 +++++++++ 11 files changed, 394 insertions(+), 5 deletions(-) create mode 100644 crates/craft_retained/src/elements/window.rs create mode 100644 crates/craft_retained/src/window_manager.rs create mode 100644 examples/multiwindow/Cargo.toml create mode 100644 examples/multiwindow/main.rs diff --git a/Cargo.lock b/Cargo.lock index a9f77194..de424a40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2199,6 +2199,15 @@ dependencies = [ "pxfm", ] +[[package]] +name = "multiwindow" +version = "0.1.0" +dependencies = [ + "craft_renderer", + "craft_retained", + "util", +] + [[package]] name = "naga" version = "26.0.0" diff --git a/Cargo.toml b/Cargo.toml index 385e4f2a..e349c9b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "crates/craft_logger", "examples/util", "examples/counter_retained", + "examples/multiwindow", "examples/pointer_events", "examples/text", "examples/jsframeworkbench", diff --git a/crates/craft_retained/src/app.rs b/crates/craft_retained/src/app.rs index 43cc955f..e8d9c604 100644 --- a/crates/craft_retained/src/app.rs +++ b/crates/craft_retained/src/app.rs @@ -50,6 +50,7 @@ use winit::dpi::{LogicalSize, PhysicalSize}; use winit::event::Ime; use winit::event_loop::ActiveEventLoop; use winit::window::{Window, WindowId}; +use crate::window_manager::WindowManager; thread_local! { /// The most recently recorded window id. This is set every time a windows event occurs. @@ -63,6 +64,8 @@ thread_local! { pub(crate) static FOCUS: RefCell>>> = RefCell::new(None); /// An event queue that users or elements can manipulate. Cleared at the start and end of every event dispatch. static EVENT_DISPATCH_QUEUE: RefCell> = RefCell::new(VecDeque::with_capacity(10)); + + pub(crate) static WINDOW_MANAGER: RefCell = RefCell::new(WindowManager::new()); } /// Enqueues an event at the back of the dispatch queue. diff --git a/crates/craft_retained/src/craft_winit_state.rs b/crates/craft_retained/src/craft_winit_state.rs index fbb58cfe..9b22b048 100644 --- a/crates/craft_retained/src/craft_winit_state.rs +++ b/crates/craft_retained/src/craft_winit_state.rs @@ -27,7 +27,7 @@ use craft_runtime::Receiver; use craft_runtime::Sender; use craft_runtime::CraftRuntimeHandle; -use crate::app::{App, CURRENT_WINDOW_ID, DOCUMENTS}; +use crate::app::{App, CURRENT_WINDOW_ID, DOCUMENTS, WINDOW_MANAGER}; use std::sync::Arc; use ui_events::pointer::{PointerEvent}; use ui_events_winit::{WindowEventReducer, WindowEventTranslation}; @@ -80,6 +80,10 @@ impl ApplicationHandler for CraftWinitState { window_attributes = window_attributes.with_inner_size(LogicalSize::new(window_size.width, window_size.height)); } + + WINDOW_MANAGER.with_borrow_mut(|window_manager| { + window_manager.create_windows(event_loop); + }); #[cfg(target_arch = "wasm32")] let window_attributes = { diff --git a/crates/craft_retained/src/elements/container.rs b/crates/craft_retained/src/elements/container.rs index 3dc464ef..2d2a7393 100644 --- a/crates/craft_retained/src/elements/container.rs +++ b/crates/craft_retained/src/elements/container.rs @@ -23,14 +23,13 @@ pub struct Container { impl Container { pub fn new() -> Rc> { - let me = Rc::new_cyclic(|me: &Weak>| { - RefCell::new(Self { + let me = Rc::new_cyclic(|me: &Weak>| { + RefCell::new(Container { element_data: ElementData::new(me.clone(), true), }) }); me.borrow_mut().element_data.crate_layout_node(None); - me } } diff --git a/crates/craft_retained/src/elements/mod.rs b/crates/craft_retained/src/elements/mod.rs index 65f95f6f..37e8ca0c 100644 --- a/crates/craft_retained/src/elements/mod.rs +++ b/crates/craft_retained/src/elements/mod.rs @@ -12,6 +12,7 @@ mod image; mod text_input; mod element_id_map; mod slider; +mod window; pub use container::Container; pub use text::Text; @@ -19,4 +20,5 @@ pub use slider::*; pub use text_input::TextInput; pub use image::Image; pub use element::Element; -pub use element_id_map::ElementIdMap; \ No newline at end of file +pub use element_id_map::ElementIdMap; +pub use window::Window; \ No newline at end of file diff --git a/crates/craft_retained/src/elements/window.rs b/crates/craft_retained/src/elements/window.rs new file mode 100644 index 00000000..01cb8de5 --- /dev/null +++ b/crates/craft_retained/src/elements/window.rs @@ -0,0 +1,210 @@ +//! Stores one or more elements. + +use crate::app::{TAFFY_TREE, WINDOW_MANAGER}; +use crate::elements::core::{resolve_clip_for_scrollable, ElementInternals}; +use crate::elements::element_data::ElementData; +use crate::elements::{scrollable, Element}; +use crate::events::{CraftMessage, Event}; +use crate::layout::TaffyTree; +use crate::text::text_context::TextContext; +use craft_primitives::geometry::Rectangle; +use craft_renderer::RenderList; +use kurbo::{Affine, Point}; +use std::any::Any; +use std::cell::RefCell; +use std::rc::{Rc, Weak}; + +use winit::window::Window as WinitWindow; + +/// Stores one or more elements. +/// +/// If overflow is set to scroll, it will become scrollable. +pub struct Window { + element_data: ElementData, + winit_window: Option>>, +} + +impl Window { + pub fn new() -> Rc> { + let me = Rc::new_cyclic(|me: &Weak>| { + RefCell::new(Self { + element_data: ElementData::new(me.clone(), true), + winit_window: None, + }) + }); + + me.borrow_mut().element_data.crate_layout_node(None); + + WINDOW_MANAGER.with_borrow_mut(|window_manager| { + window_manager.add_window(me.clone()); + }); + + me + } +} + +impl crate::elements::core::ElementData for Window { + fn element_data(&self) -> &ElementData { + &self.element_data + } + + fn element_data_mut(&mut self) -> &mut ElementData { + &mut self.element_data + } +} + +impl Element for Window { + fn push(&mut self, child: Rc>) -> &mut Self + where + Self: Sized, + { + let me: Weak> = self.element_data.me.clone(); + child.borrow_mut().element_data_mut().parent = Some(me); + self.element_data.children.push(child.clone()); + + // Add the children's taffy node. + TAFFY_TREE.with_borrow_mut(|taffy_tree| { + let parent_id = self.element_data.layout_item.taffy_node_id.unwrap(); + let child_id = child.borrow().element_data().layout_item.taffy_node_id; + if let Some(child_id) = child_id { + taffy_tree.add_child(parent_id, child_id); + } + }); + + self + } + + fn push_dyn(&mut self, child: Rc>) { + self.push(child); + } + + /// Appends multiple typed children in one call + fn extend(&mut self, children: impl IntoIterator>>) -> &mut Self + where + Self: Sized, + { + let me: Weak> = self.element_data.me.clone(); + let children: Vec<_> = children.into_iter().collect(); + + for child in &children { + child.borrow_mut().element_data_mut().parent = Some(me.clone()); + } + + self.element_data.children.extend(children.iter().cloned()); + + // Add the children's taffy node. + TAFFY_TREE.with_borrow_mut(|taffy_tree| { + let parent_id = self.element_data.layout_item.taffy_node_id.unwrap(); + for child in &children { + if let Some(child_id) = child.borrow().element_data().layout_item.taffy_node_id { + taffy_tree.add_child(parent_id, child_id); + } + } + }); + + self + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} + +impl ElementInternals for Window { + fn apply_layout( + &mut self, + taffy_tree: &mut TaffyTree, + position: Point, + z_index: &mut u32, + transform: Affine, + pointer: Option, + text_context: &mut TextContext, + clip_bounds: Option, + scale_factor: f64, + ) { + let node = self.element_data.layout_item.taffy_node_id.unwrap(); + let layout = taffy_tree.layout(node); + let has_new_layout = taffy_tree.get_has_new_layout(node); + + let dirty = has_new_layout + || transform != self.element_data.layout_item.get_transform() + || position != self.element_data.layout_item.position; + self.element_data.layout_item.has_new_layout = has_new_layout; + + if dirty { + self.resolve_box(position, transform, layout, z_index); + self.apply_borders(scale_factor); + // For scroll changes from taffy; + self.element_data.apply_scroll(layout); + self.apply_clip(clip_bounds); + self.element_data.scroll_state.as_mut().unwrap().mark_old(); + } + + // For manual scroll updates. + if !dirty && self.element_data.scroll_state.map(|scroll_state| scroll_state.is_new()).unwrap_or_default() { + self.element_data.apply_scroll(layout); + self.element_data.scroll_state.as_mut().unwrap().mark_old(); + } + + if has_new_layout { + taffy_tree.mark_seen(node); + } + + let scroll_y = self.element_data.scroll().map_or(0.0, |s| s.scroll_y() as f64); + let child_transform = Affine::translate((0.0, -scroll_y)); + + self.apply_layout_children( + taffy_tree, + z_index, + transform * child_transform, + pointer, + text_context, + scale_factor, + false, + ) + } + + fn draw( + &mut self, + renderer: &mut RenderList, + text_context: &mut TextContext, + pointer: Option, + scale_factor: f64, + ) { + if !self.is_visible() { + return; + } + self.add_hit_testable(renderer, true, scale_factor); + + // We draw the borders before we start any layers, so that we don't clip the borders. + self.draw_borders(renderer, scale_factor); + + /*if self.element_data.layout_item.has_new_layout { + renderer.draw_rect_outline(self.element_data.layout_item.computed_box_transformed.padding_rectangle(), rgba(255, 0, 0, 100), 5.0); + }*/ + + self.maybe_start_layer(renderer, scale_factor); + self.draw_children(renderer, text_context, pointer, scale_factor); + self.maybe_end_layer(renderer); + + self.draw_scrollbar(renderer, scale_factor); + } + + fn on_event( + &mut self, + message: &CraftMessage, + _text_context: &mut TextContext, + event: &mut Event, + _target: Option>>, + ) { + scrollable::on_scroll_events(self, message, event); + } + + fn apply_clip(&mut self, clip_bounds: Option) { + resolve_clip_for_scrollable(self, clip_bounds); + } +} \ No newline at end of file diff --git a/crates/craft_retained/src/lib.rs b/crates/craft_retained/src/lib.rs index 59478ae6..f29f28fc 100644 --- a/crates/craft_retained/src/lib.rs +++ b/crates/craft_retained/src/lib.rs @@ -22,6 +22,7 @@ mod utils; #[cfg(target_arch = "wasm32")] pub mod wasm_queue; pub mod spatial; +mod window_manager; pub use options::CraftOptions; pub use craft_primitives::palette; diff --git a/crates/craft_retained/src/window_manager.rs b/crates/craft_retained/src/window_manager.rs new file mode 100644 index 00000000..438c23a4 --- /dev/null +++ b/crates/craft_retained/src/window_manager.rs @@ -0,0 +1,31 @@ +use std::cell::RefCell; +use std::rc::Rc; +use winit::event_loop::ActiveEventLoop; +use winit::window::WindowAttributes; +use crate::elements::Window; + +pub(crate) struct WindowManager { + windows: Vec>>, +} + +impl WindowManager { + + pub(crate) fn new() -> Self { + Self { + windows: Vec::new(), + } + } + + pub(crate) fn create_windows(&mut self, event_loop: &ActiveEventLoop) { + for window_element in &self.windows { + println!("Creating window"); + let winit_window = event_loop.create_window(WindowAttributes::default()).expect("Failed to create window"); + winit_window.set_visible(true); + } + } + + pub(crate) fn add_window(&mut self, window: Rc>) { + self.windows.push(window); + } + +} \ No newline at end of file diff --git a/examples/multiwindow/Cargo.toml b/examples/multiwindow/Cargo.toml new file mode 100644 index 00000000..02037243 --- /dev/null +++ b/examples/multiwindow/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "multiwindow" +version = "0.1.0" +edition = "2024" +default-run = "multiwindow" + +[[bin]] +name = "multiwindow" +path = "main.rs" + +[dependencies] +util = { path = "../util" } + +[dependencies.craft_retained] +path = "../../crates/craft_retained" +default-features = false +features = ["accesskit", "system_fonts"] + +[target.'cfg(not(target_os = "android"))'.dependencies.craft_renderer] +path = "../../crates/craft_renderer" +features = ["vello_hybrid_renderer"] + +[target.'cfg(target_os = "android")'.dependencies.craft_renderer] +path = "../../crates/craft_renderer" +features = ["vello_renderer"] \ No newline at end of file diff --git a/examples/multiwindow/main.rs b/examples/multiwindow/main.rs new file mode 100644 index 00000000..eda014b5 --- /dev/null +++ b/examples/multiwindow/main.rs @@ -0,0 +1,104 @@ +use craft_retained::elements::{Element, Window}; +use craft_retained::events::ui_events::pointer::PointerButton; +use craft_retained::style::{AlignItems, Display, FlexDirection, JustifyContent, Unit}; +use craft_retained::{ + elements::{Container, Text}, + rgb, Color, +}; +use std::cell::RefCell; +use std::rc::Rc; + +#[derive(Default, Clone, Copy)] +pub struct Counter { + count: i64, +} + +impl Counter { + fn change(&mut self, delta: i64) { + self.count += delta; + } + + fn count(&self) -> i64 { + self.count + } +} + +fn create_button( + label: &str, + base_color: Color, + delta: i64, + state: Rc>, + count_text: Rc>, +) -> Rc> { + let border_color = rgb(0, 0, 0); + let label = Text::new(label); + label.borrow_mut().font_size(24.0).color(Color::WHITE).selectable(false); + let container = Container::new(); + container + .borrow_mut() + .border_width(Unit::Px(1.0), Unit::Px(2.0), Unit::Px(3.0), Unit::Px(4.0)) + .border_color(border_color, border_color, border_color, border_color) + .border_radius((10.0, 10.0), (10.0, 10.0), (10.0, 10.0), (10.0, 10.0)) + .padding(Unit::Px(15.0), Unit::Px(30.0), Unit::Px(15.0), Unit::Px(30.0)) + .display(Display::Flex) + .justify_content(Some(JustifyContent::Center)) + .align_items(Some(AlignItems::Center)) + .background_color(base_color) + .on_pointer_button_up(Rc::new(move |event, pointer_button_event| { + if pointer_button_event.button == Some(PointerButton::Primary) { + state.borrow_mut().change(delta); + count_text.borrow_mut().text(&format!("Count: {}", state.borrow().count())); + event.prevent_propagate(); + } + })) + .push(label); + container +} + +pub fn counter() -> Rc> { + let count = Rc::new(RefCell::new(Counter::default())); + + let container = Container::new(); + + let count_text = Text::new(&format!("Count: {}", count.borrow().count())); + + let button = Container::new(); + button + .borrow_mut() + .display(Display::Flex) + .flex_direction(FlexDirection::Row) + .gap(Unit::Px(20.0), Unit::Px(20.0)) + .push(create_button("-", rgb(244, 67, 54), -1, count.clone(), count_text.clone())) + .push(create_button("+", rgb(76, 175, 80), 1, count.clone(), count_text.clone())); + + container + .borrow_mut() + .display(Display::Flex) + .flex_direction(FlexDirection::Column) + .justify_content(Some(JustifyContent::Center)) + .align_items(Some(AlignItems::Center)) + .width(Unit::Percentage(100.0)) + .height(Unit::Percentage(100.0)) + .gap(Unit::Px(20.0), Unit::Px(20.0)) + .push(count_text) + .font_size(72.0) + .color(rgb(50, 50, 50)) + .push(button); + + let root = Container::new(); + root.borrow_mut().push(container); + + root +} + +#[allow(unused)] +#[cfg(not(target_os = "android"))] +fn main() { + let counter = counter(); + + let window_2 = Window::new(); + + use craft_retained::CraftOptions; + util::setup_logging(); + craft_retained::craft_main(counter, CraftOptions::basic("Counter")); +} From 396fa1856bb174003374bc1daff8e1ded7f19f30 Mon Sep 17 00:00:00 2001 From: "Austin M. Reppert" Date: Thu, 1 Jan 2026 15:20:44 -0500 Subject: [PATCH 027/143] start multi-window --- crates/craft_renderer/src/vello_cpu/mod.rs | 4 +- crates/craft_retained/src/app.rs | 373 ++++-------------- .../craft_retained/src/craft_winit_state.rs | 93 ++--- .../craft_retained/src/elements/container.rs | 24 +- .../src/elements/core/element_internals.rs | 15 +- crates/craft_retained/src/elements/element.rs | 12 +- .../src/elements/element_data.rs | 127 +++--- .../src/elements/element_states.rs | 9 - crates/craft_retained/src/elements/image.rs | 20 +- crates/craft_retained/src/elements/mod.rs | 1 - .../craft_retained/src/elements/scrollable.rs | 19 +- .../src/elements/slider/slider.rs | 21 +- crates/craft_retained/src/elements/text.rs | 19 +- .../src/elements/text_input/mod.rs | 13 +- .../elements/text_input/text_input_state.rs | 25 +- crates/craft_retained/src/elements/window.rs | 185 ++++++++- crates/craft_retained/src/lib.rs | 18 +- .../craft_retained/src/text/parley_editor.rs | 19 +- crates/craft_retained/src/window_context.rs | 9 +- crates/craft_retained/src/window_manager.rs | 79 +++- examples/counter_retained/Cargo.toml | 2 +- examples/counter_retained/main.rs | 6 +- examples/jsframeworkbench/main.rs | 38 +- examples/multiwindow/Cargo.toml | 10 +- examples/multiwindow/main.rs | 32 +- examples/pointer_events/Cargo.toml | 2 +- examples/pointer_events/main.rs | 17 +- examples/text/main.rs | 6 +- 28 files changed, 622 insertions(+), 576 deletions(-) delete mode 100644 crates/craft_retained/src/elements/element_states.rs diff --git a/crates/craft_renderer/src/vello_cpu/mod.rs b/crates/craft_renderer/src/vello_cpu/mod.rs index 3befd9ef..e8849a36 100644 --- a/crates/craft_renderer/src/vello_cpu/mod.rs +++ b/crates/craft_renderer/src/vello_cpu/mod.rs @@ -152,8 +152,8 @@ impl Renderer for VelloCpuRenderer { self.render_context.set_paint(PaintType::Solid(*fill_color)); self.render_context.fill_rect(&rectangle.to_kurbo()); } - RenderCommand::DrawRectOutline(rectangle, outline_color) => { - self.render_context.set_stroke(Stroke::new(1.0)); + RenderCommand::DrawRectOutline(rectangle, outline_color, thickness) => { + self.render_context.set_stroke(Stroke::new(*thickness)); self.render_context.set_paint(PaintType::Solid(*outline_color)); self.render_context.stroke_rect(&rectangle.to_kurbo()); } diff --git a/crates/craft_retained/src/app.rs b/crates/craft_retained/src/app.rs index e8d9c604..3412fa71 100644 --- a/crates/craft_retained/src/app.rs +++ b/crates/craft_retained/src/app.rs @@ -1,16 +1,13 @@ -use crate::elements::ElementIdMap; +use crate::elements::{ElementIdMap, Window}; use crate::events::internal::InternalMessage; use crate::events::CraftMessage; use crate::events::{Event, EventDispatcher}; use crate::style::{Display, Unit, Wrap}; use crate::text::text_context::TextContext; -use crate::{RendererBox, WindowContext}; use craft_logging::info; -use craft_primitives::geometry::Rectangle; +use craft_primitives::geometry::{Size, Point}; use craft_resource_manager::{ResourceIdentifier, ResourceManager}; use craft_runtime::CraftRuntimeHandle; -use kurbo::{Affine, Point}; -use peniko::Color; use std::cell::Cell; use std::cell::RefCell; use std::collections::VecDeque; @@ -35,21 +32,18 @@ use web_time as time; use crate::animations::animation::AnimationFlags; use crate::document::DocumentManager; use crate::elements::Element; -use crate::layout::TaffyTree; -use craft_renderer::RenderList; use craft_resource_manager::resource_event::ResourceEvent; use craft_resource_manager::resource_type::ResourceType; use craft_runtime::Sender; use std::time::Duration; -use taffy::{AvailableSpace, NodeId}; use ui_events::keyboard::{KeyboardEvent, Modifiers, NamedKey}; use ui_events::pointer::{PointerButtonEvent, PointerScrollEvent, PointerUpdate}; use ui_events::ScrollDelta; use ui_events::ScrollDelta::PixelDelta; -use winit::dpi::{LogicalSize, PhysicalSize}; use winit::event::Ime; use winit::event_loop::ActiveEventLoop; -use winit::window::{Window, WindowId}; +use winit::window::{WindowId}; +use crate::elements::core::ElementInternals; use crate::window_manager::WindowManager; thread_local! { @@ -58,7 +52,6 @@ thread_local! { /// Records document-level state (focus, pointer captures, etc.) for internal use. pub static DOCUMENTS: RefCell = RefCell::new(DocumentManager::new()); pub(crate) static ELEMENTS: RefCell = RefCell::new(ElementIdMap::new()); - pub(crate) static TAFFY_TREE: RefCell = RefCell::new(TaffyTree::new()); pub(crate) static PENDING_RESOURCES: RefCell> = RefCell::new(VecDeque::new()); pub(crate) static IN_PROGRESS_RESOURCES: RefCell> = RefCell::new(VecDeque::new()); pub(crate) static FOCUS: RefCell>>> = RefCell::new(None); @@ -89,21 +82,13 @@ pub(crate) fn dequeue_event() -> Option<(Event, CraftMessage)> { pub struct App { pub(crate) event_dispatcher: EventDispatcher, - pub(crate) root: Rc>, - /// A winit window. This is only valid between resume and pause. - pub window: Option>, /// The text context is used to manage fonts and text rendering. It is only valid between resume and pause. pub(crate) text_context: Option, - /// The renderer is used to draw the view. It is only valid between resume and pause. - pub renderer: Option, pub(crate) reload_fonts: bool, /// The resource manager is used to manage resources such as images and fonts. /// /// The resource manager is responsible for loading, caching, and providing access to resources. pub(crate) resource_manager: Arc, - // The user's reactive tree. - /// Provides a way for the user to get and set common window properties during view and update. - pub window_context: WindowContext, pub(crate) app_sender: Sender, #[cfg(feature = "accesskit")] @@ -113,7 +98,6 @@ pub struct App { pub(crate) modifiers: Modifiers, pub redraw_flags: RedrawFlags, - pub(crate) render_list: RenderList, pub(super) target_scratch: Vec>>, pub(crate) previous_animation_flags: AnimationFlags, @@ -143,22 +127,17 @@ impl App { } pub fn on_scale_factor_changed(&mut self, scale_factor: f64) { - self.window_context.scale_factor = scale_factor; + /*self.window_context.scale_factor = scale_factor; self.on_resize(self.window.as_ref().unwrap().inner_size()); self.root.borrow_mut().scale_factor(self.window_context.effective_scale_factor()); - style_root_element(self.root.borrow_mut().deref_mut(), self.window_context.window_size()); + style_root_element(self.root.borrow_mut().deref_mut(), self.window_context.window_size());*/ } - pub fn on_process_user_events(&mut self) {} - #[allow(unused_variables)] - pub fn on_resume(&mut self, window: Arc, renderer: RendererBox, event_loop: &ActiveEventLoop) { - window.set_ime_allowed(true); + pub fn on_resume(&mut self, event_loop: &ActiveEventLoop) { + //window.set_ime_allowed(true); self.setup_text_context(); - self.renderer = Some(renderer); - - self.window = Some(window.clone()); #[cfg(all(feature = "accesskit", not(target_arch = "wasm32")))] let action_handler = CraftAccessHandler { @@ -168,44 +147,32 @@ impl App { #[cfg(all(feature = "accesskit", not(target_arch = "wasm32")))] let deactivation_handler = CraftDeactivationHandler::new(); - let scale_factor = window.scale_factor(); + /*let scale_factor = window.scale_factor(); self.window = Some(window.clone()); self.window_context.scale_factor = scale_factor; self.on_resize(window.inner_size()); - let tree_update = self.on_request_redraw(); - - #[cfg(all(feature = "accesskit", not(target_arch = "wasm32")))] - let craft_activation_handler = CraftActivationHandler::new(tree_update); - - #[cfg(all(feature = "accesskit", not(target_arch = "wasm32")))] - { - self.accesskit_adapter = Some(Adapter::with_direct_handlers( - event_loop, - &window, - craft_activation_handler, - #[cfg(feature = "accesskit")] - action_handler, - deactivation_handler, - )); - } - - window.set_visible(true); + let tree_update = self.on_request_redraw();*/ + + /*#[cfg(all(feature = "accesskit", not(target_arch = "wasm32")))] + let craft_activation_handler = CraftActivationHandler::new(tree_update);*/ + + //#[cfg(all(feature = "accesskit", not(target_arch = "wasm32")))] + //{ + // self.accesskit_adapter = Some(Adapter::with_direct_handlers( + // event_loop, + // &window, + // craft_activation_handler, + // #[cfg(feature = "accesskit")] + // action_handler, + // deactivation_handler, + // )); + //} } /// Handles the window resize event. - pub fn on_resize(&mut self, new_size: PhysicalSize) { - self.window_context.window_size = new_size; - if let Some(renderer) = self.renderer.as_mut() { - renderer.resize_surface(new_size.width.max(1) as f32, new_size.height.max(1) as f32); - } - self.render_list.set_cull(Some(Rectangle::new(0.0, 0.0, new_size.width as f32, new_size.height as f32))); - style_root_element(self.root.borrow_mut().deref_mut(), self.window_context.window_size()); - // On macOS the window needs to be redrawn manually after resizing - #[cfg(target_os = "macos")] - { - self.window.as_ref().unwrap().request_redraw(); - } + pub fn on_resize(&mut self, window: Rc>, new_size: Size) { + window.borrow_mut().on_resize(new_size); } /// Initialize any data needed to layout/render text. @@ -245,91 +212,38 @@ impl App { } } - /// Updates the view by applying the latest changes to the reactive tree. - pub(crate) fn update_view(&mut self) {} - /// Updates the reactive tree, layouts the elements, and draws the view. #[cfg(feature = "accesskit")] - pub fn on_request_redraw(&mut self) -> Option { - self.on_request_redraw_internal(); - self.window.as_ref()?; - let window = self.window.as_mut().unwrap().clone(); + pub fn on_request_redraw(&mut self, window: Rc>) -> Option { + self.on_request_redraw_internal(window.clone()); + let winit_window = window.borrow().winit_window().as_mut().unwrap().clone(); let tree_update = self.compute_accessibility_tree(); if let Some(accesskit_adapter) = &mut self.accesskit_adapter { accesskit_adapter.update_if_active(|| tree_update); - window.pre_present_notify(); + winit_window.pre_present_notify(); None } else { - window.pre_present_notify(); + winit_window.pre_present_notify(); Some(tree_update) } } /// Updates the reactive tree, layouts the elements, and draws the view. #[cfg(not(feature = "accesskit"))] - pub fn on_request_redraw(&mut self) { - self.on_request_redraw_internal(); + pub fn on_request_redraw(&mut self, window: Rc>) { + self.on_request_redraw_internal(window); } //#[cfg(not(feature = "accesskit"))] - fn on_request_redraw_internal(&mut self) { - if self.window.is_none() { - return; - } - + fn on_request_redraw_internal(&mut self, window: Rc>) { self.update_resources(); - let surface_size = self.window_context.window_size(); - - self.update_view(); - - let root_size = surface_size; - - if self.renderer.is_some() { - self.renderer.as_mut().unwrap().surface_set_clear_color(Color::WHITE); - } - - let layout_origin = Point::new(0.0, 0.0); - - { - if self.redraw_flags.should_rebuild_layout() { - self.layout_tree( - root_size, - layout_origin, - self.window_context.effective_scale_factor(), - self.window_context.mouse_position, - ); - } - - //self.animate_tree(&delta_time, layout_origin, root_size); - - if self.renderer.is_some() { - self.draw_reactive_tree(self.window_context.mouse_position); - } - } - - { - /*let span = span!(Level::INFO, "renderer_submit"); - let _enter = span.enter();*/ - - if self.renderer.is_some() { - self.renderer.as_mut().unwrap().submit(self.resource_manager.clone()); - } - } - - if let Some(window) = &self.window { - self.window_context.apply_requests(window); - self.window_context.reset(); - } - - self.on_process_user_events(); - - self.view_introspection(); + window.borrow_mut().on_redraw(self.text_context.as_mut().unwrap(), self.resource_manager.clone()); } - pub fn on_pointer_scroll(&mut self, pointer_scroll_update: PointerScrollEvent) { + pub fn on_pointer_scroll(&mut self, window: Rc>, pointer_scroll_update: PointerScrollEvent) { if self.modifiers.ctrl() && pointer_scroll_update.pointer.pointer_type == ui_events::pointer::PointerType::Mouse { let y: f32 = match pointer_scroll_update.delta { @@ -338,13 +252,14 @@ impl App { PixelDelta(physical) => physical.y as f32, }; if y < 0.0 { - self.window_context.zoom_out(); + window.borrow_mut().zoom_out(); } else { - self.window_context.zoom_in(); + window.borrow_mut().zoom_in(); } - self.root.borrow_mut().scale_factor(self.window_context.effective_scale_factor()); - request_layout(); - style_root_element(self.root.borrow_mut().deref_mut(), self.window_context.window_size()); + let scale_factor = window.borrow().effective_scale_factor(); + window.borrow_mut().scale_factor(scale_factor); + window.borrow_mut().mark_dirty(); + //style_root_element(window.borrow_mut().deref_mut(), window.borrow().window_size()); self.request_redraw(RedrawFlags::new(true)); return; } @@ -352,13 +267,13 @@ impl App { let event = CraftMessage::PointerScroll(pointer_scroll_update); let message = event; - self.dispatch_event(&message, false); + self.dispatch_event(window, &message, false); self.request_redraw(RedrawFlags::new(true)); } - pub fn on_pointer_button(&mut self, pointer_event: PointerButtonEvent, is_up: bool) { + pub fn on_pointer_button(&mut self, window: Rc>, pointer_event: PointerButtonEvent, is_up: bool) { let mut pointer_event = pointer_event; - let zoom = self.window_context.zoom_factor; + let zoom = window.borrow().zoom_scale_factor(); pointer_event.state.position.x /= zoom; pointer_event.state.position.y /= zoom; @@ -370,62 +285,63 @@ impl App { CraftMessage::PointerButtonDown(pointer_event) }; let message = event; - self.window_context.mouse_position = Some(Point::new(cursor_position.x, cursor_position.y)); + window.borrow_mut().set_mouse_position(Some(Point::new(cursor_position.x, cursor_position.y))); - self.dispatch_event(&message, true); + self.dispatch_event(window.clone(), &message, true); self.request_redraw(RedrawFlags::new(true)); } - pub fn on_pointer_moved(&mut self, mouse_moved: PointerUpdate) { + pub fn on_pointer_moved(&mut self, window: Rc>, mouse_moved: PointerUpdate) { let mut mouse_moved = mouse_moved; - let zoom = self.window_context.zoom_factor; + let zoom = window.borrow().zoom_scale_factor(); mouse_moved.current.position.x /= zoom; mouse_moved.current.position.y /= zoom; - self.window_context.mouse_position = Some(mouse_moved.current.logical_point()); + window.borrow_mut().set_mouse_position(Some(mouse_moved.current.logical_point())); let message = CraftMessage::PointerMovedEvent(mouse_moved); - self.dispatch_event(&message, true); + self.dispatch_event(window.clone(), &message, true); self.request_redraw(RedrawFlags::new(true)); } - pub fn on_ime(&mut self, ime: Ime) { + pub fn on_ime(&mut self, window: Rc>, ime: Ime) { let event = CraftMessage::ImeEvent(ime); let message = event; - self.dispatch_event(&message, false); + self.dispatch_event(window.clone(), &message, false); self.request_redraw(RedrawFlags::new(true)); } - /// Dispatch messages to the reactive tree. - fn dispatch_event(&mut self, message: &CraftMessage, _is_style: bool) { + fn dispatch_event(&mut self, window: Rc>, message: &CraftMessage, _is_style: bool) { + let mouse_pos = Some(window.borrow().mouse_position()); + let render_list = window.borrow().render_list.clone(); self.event_dispatcher.dispatch_event( message, - self.window_context.mouse_position, - self.root.clone(), + mouse_pos.unwrap_or_default(), + window.clone(), &mut self.text_context, - &mut self.render_list, + render_list.borrow_mut().deref_mut(), &mut self.target_scratch, ); - self.window.clone().unwrap().request_redraw(); + window.borrow().winit_window().unwrap().request_redraw(); } - pub fn on_keyboard_input(&mut self, keyboard_input: KeyboardEvent) { + pub fn on_keyboard_input(&mut self, window: Rc>, keyboard_input: KeyboardEvent) { self.modifiers = keyboard_input.modifiers; if keyboard_input.key == ui_events::keyboard::Key::Named(NamedKey::Control) && keyboard_input.state.is_up() { self.modifiers.set(Modifiers::CONTROL, false); } if keyboard_input.modifiers.ctrl() { if keyboard_input.key == ui_events::keyboard::Key::Character("=".to_string()) { - self.window_context.zoom_in(); + window.borrow_mut().zoom_in(); self.request_redraw(RedrawFlags::new(true)); return; } else if keyboard_input.key == ui_events::keyboard::Key::Character("-".to_string()) { - self.window_context.zoom_out(); + window.borrow_mut().zoom_out(); self.request_redraw(RedrawFlags::new(true)); return; } @@ -434,7 +350,7 @@ impl App { let keyboard_event = CraftMessage::KeyboardInputEvent(keyboard_input.clone()); let message = keyboard_event; - self.dispatch_event(&message, false); + self.dispatch_event(window.clone(), &message, false); self.request_redraw(RedrawFlags::new(true)); } @@ -460,17 +376,15 @@ impl App { } } - fn view_introspection(&mut self) {} - fn request_redraw(&mut self, redraw_flags: RedrawFlags) { self.redraw_flags = redraw_flags; - if let Some(window) = &self.window { + /*if let Some(window) = &self.window { window.request_redraw(); - } + }*/ } /// "Animates" a tree by calling `on_animation_frame` and changing an element's styles. - #[allow(dead_code)] + /*#[allow(dead_code)] fn animate_tree(&mut self, delta_time: &Duration, layout_origin: Point, viewport_size: LogicalSize) { /*let span = span!(Level::INFO, "animate_tree"); let _enter = span.enter();*/ @@ -501,65 +415,7 @@ impl App { // Winit does not guarantee when a redraw event will happen, but that should be fine, at worst we redraw an extra time. self.request_redraw(RedrawFlags::new(old_has_active_animation)); } - } - - #[allow(clippy::too_many_arguments)] - fn layout_tree( - &mut self, - viewport_size: LogicalSize, - origin: Point, - scale_factor: f64, - mouse_position: Option, - ) { - let root_element = self.root.clone(); - let text_context = self.text_context.as_mut().unwrap(); - - { - /*let span = span!(Level::INFO, "layout"); - let _enter = span.enter();*/ - TAFFY_TREE.with_borrow_mut(|taffy_tree| { - layout( - taffy_tree, - root_element, - viewport_size, - text_context, - origin, - self.resource_manager.clone(), - scale_factor, - mouse_position, - ) - }); - }; - } - - #[allow(clippy::too_many_arguments)] - fn draw_reactive_tree(&mut self, mouse_position: Option) { - let text_context = self.text_context.as_mut().unwrap(); - { - /*let span = span!(Level::INFO, "render"); - let _enter = span.enter();*/ - self.render_list.clear(); - let scale_factor = self.window_context.effective_scale_factor(); - - let renderer = self.renderer.as_mut().unwrap(); - - { - /*let span = span!(Level::INFO, "render(element)"); - let _enter = span.enter();*/ - self.root.borrow_mut().draw(&mut self.render_list, text_context, mouse_position, scale_factor); - } - - renderer.sort_and_cull_render_list(&mut self.render_list); - - let window = Rectangle { - x: 0.0, - y: 0.0, - width: renderer.surface_width(), - height: renderer.surface_height(), - }; - renderer.prepare_render_list(&mut self.render_list, self.resource_manager.clone(), window); - } - } + }*/ #[cfg(feature = "accesskit")] fn compute_accessibility_tree(&mut self) -> TreeUpdate { @@ -576,11 +432,11 @@ impl App { focus: accesskit::NodeId(focus_id), }; - self.root.borrow_mut().compute_accessibility_tree( + /*self.root.borrow_mut().compute_accessibility_tree( &mut tree_update, None, self.window_context.effective_scale_factor(), - ); + );*/ tree_update.nodes[0].1.set_role(Role::Window); tree_update @@ -605,89 +461,4 @@ impl App { }); }); } -} - -fn style_root_element(root: &mut dyn Element, root_size: LogicalSize) { - let is_user_root_height_auto = { - let root_children = root.children(); - root_children[0].borrow().style().height().is_auto() - }; - - let style = root.style_mut(); - - style.set_width(Unit::Px(root_size.width)); - style.set_wrap(Wrap::Wrap); - style.set_display(Display::Block); - - if is_user_root_height_auto { - style.set_height(Unit::Auto); - } else { - style.set_height(Unit::Px(root_size.height)); - } - root.update_taffy_style(); -} - -#[allow(clippy::too_many_arguments)] -fn layout( - taffy_tree: &mut TaffyTree, - root_element: Rc>, - window_size: LogicalSize, - text_context: &mut TextContext, - origin: Point, - resource_manager: Arc, - scale_factor: f64, - pointer: Option, -) -> NodeId { - let root_node = root_element - .borrow() - .element_data() - .layout_item - .taffy_node_id - .expect("A root element must have a layout node."); - - let available_space: taffy::Size = taffy::Size { - width: AvailableSpace::Definite(window_size.width), - height: AvailableSpace::Definite(window_size.height), - }; - - if taffy_tree.is_layout_dirty() { - /*let span = span!(Level::INFO, "layout(taffy)"); - let _enter = span.enter();*/ - taffy_tree.compute_layout(root_node, available_space, text_context, resource_manager.clone()); - } - - if taffy_tree.is_apply_layout_dirty() { - /*let span = span!(Level::INFO, "layout(apply)"); - let _enter = span.enter();*/ - - // TODO: move into taffy_tree - let mut layout_order: u32 = 0; - root_element.borrow_mut().apply_layout( - taffy_tree, - origin, - &mut layout_order, - Affine::IDENTITY, - pointer, - text_context, - None, - scale_factor, - ); - taffy_tree.apply_layout(); - } - - root_node -} - -#[inline] -pub fn request_layout() { - TAFFY_TREE.with_borrow_mut(|taffy_tree| { - taffy_tree.request_layout(); - }); -} - -#[inline] -pub fn request_apply_layout() { - TAFFY_TREE.with_borrow_mut(|taffy_tree| { - taffy_tree.request_apply_layout(); - }); -} +} \ No newline at end of file diff --git a/crates/craft_retained/src/craft_winit_state.rs b/crates/craft_retained/src/craft_winit_state.rs index 9b22b048..051bb460 100644 --- a/crates/craft_retained/src/craft_winit_state.rs +++ b/crates/craft_retained/src/craft_winit_state.rs @@ -1,3 +1,5 @@ +use std::cell::RefCell; +use std::rc::Rc; #[cfg(target_arch = "wasm32")] use { wasm_bindgen::JsCast, @@ -8,15 +10,13 @@ use { use {crate::wasm_queue::WasmQueue, crate::wasm_queue::WASM_QUEUE}; use crate::events::internal::InternalMessage; -use craft_renderer::renderer::Renderer; use crate::{CraftOptions}; use craft_logging::info; use winit::application::ApplicationHandler; use winit::event::{StartCause, WindowEvent}; use winit::event_loop::{ActiveEventLoop, ControlFlow}; -use winit::window::WindowAttributes; -use winit::window::{Window, WindowId}; +use winit::window::{WindowId}; #[cfg(not(target_arch = "wasm32"))] use std::time; @@ -28,10 +28,9 @@ use craft_runtime::Sender; use craft_runtime::CraftRuntimeHandle; use crate::app::{App, CURRENT_WINDOW_ID, DOCUMENTS, WINDOW_MANAGER}; -use std::sync::Arc; use ui_events::pointer::{PointerEvent}; use ui_events_winit::{WindowEventReducer, WindowEventTranslation}; -use winit::dpi::LogicalSize; +use craft_primitives::geometry::Size; use crate::document::Document; const WAIT_TIME: time::Duration = time::Duration::from_millis(15); @@ -73,19 +72,22 @@ impl ApplicationHandler for CraftWinitState { fn resumed(&mut self, event_loop: &ActiveEventLoop) { let craft_state = &mut self.craft_state; - let mut window_attributes = +/* let mut window_attributes = WindowAttributes::default().with_title(craft_state.craft_options.window_title.as_str()).with_visible(false); if let Some(window_size) = &craft_state.craft_options.window_size { window_attributes = window_attributes.with_inner_size(LogicalSize::new(window_size.width, window_size.height)); - } + }*/ WINDOW_MANAGER.with_borrow_mut(|window_manager| { - window_manager.create_windows(event_loop); + window_manager.on_resume(craft_state, event_loop); }); - #[cfg(target_arch = "wasm32")] + + craft_state.craft_app.on_resume(event_loop); + + /*#[cfg(target_arch = "wasm32")] let window_attributes = { let canvas = web_sys::window() .unwrap() @@ -97,39 +99,21 @@ impl ApplicationHandler for CraftWinitState { .unwrap(); window_attributes.with_canvas(Some(canvas)) - }; - - let window: Arc = - Arc::from(event_loop.create_window(window_attributes).expect("Failed to create window.")); - info!("Created window"); - + };*/ //craft_state.event_reducer.set_scale_factor(&window); - - let renderer_type = craft_state.craft_options.renderer; - let window_copy = window.clone(); - - cfg_if::cfg_if! { - if #[cfg(not(target_arch = "wasm32"))] { - let renderer = craft_state.runtime.borrow_tokio_runtime().block_on(async { - let renderer: Box = renderer_type.create(window_copy).await; - renderer - }); - craft_state.craft_app.on_resume(window, renderer, event_loop); - } else { - let app_sender = craft_state.app_sender.clone(); - let window_copy_2 = window_copy.clone(); - craft_state.runtime.spawn(async move { - let renderer: Box = renderer_type.create(window_copy).await; - app_sender - .send(InternalMessage::RendererCreated(window_copy_2, renderer)) - .await - .expect("Failed to send RendererCreated message"); - }); - } - } } fn window_event(&mut self, event_loop: &ActiveEventLoop, window_id: WindowId, event: WindowEvent) { + let window: Option>> = WINDOW_MANAGER.with_borrow_mut(|window_manager| { + window_manager.get_window_by_id(window_id) + }); + + let window = if let Some(window) = window { + window + } else { + return + }; + let craft_state = &mut self.craft_state; CURRENT_WINDOW_ID.set(Some(window_id)); @@ -140,10 +124,10 @@ impl ApplicationHandler for CraftWinitState { } }); - #[cfg(feature = "accesskit")] + /*#[cfg(feature = "accesskit")] if let Some(accesskit_adapter) = &mut craft_state.craft_app.accesskit_adapter { accesskit_adapter.process_event(craft_state.craft_app.window.as_ref().unwrap(), &event); - } + }*/ if !matches!( event, @@ -158,26 +142,26 @@ impl ApplicationHandler for CraftWinitState { if keyboard_event.state.is_down() && matches!(keyboard_event.key, Key::Named(NamedKey::Escape)) { event_loop.exit(); } else { - craft_state.craft_app.on_keyboard_input(keyboard_event); + craft_state.craft_app.on_keyboard_input(window, keyboard_event); } return; } Some(WindowEventTranslation::Pointer(pointer_event)) => { match pointer_event { PointerEvent::Down(pointer_button_update) => { - craft_state.craft_app.on_pointer_button(pointer_button_update, false); + craft_state.craft_app.on_pointer_button(window, pointer_button_update, false); } PointerEvent::Up(pointer_button_update) => { - craft_state.craft_app.on_pointer_button(pointer_button_update, true); + craft_state.craft_app.on_pointer_button(window, pointer_button_update, true); } PointerEvent::Move(pointer_update) => { - craft_state.craft_app.on_pointer_moved(pointer_update); + craft_state.craft_app.on_pointer_moved(window, pointer_update); } PointerEvent::Cancel(_) => {} PointerEvent::Enter(_) => {} PointerEvent::Leave(_) => {} PointerEvent::Scroll(pointer_scroll_update) => { - craft_state.craft_app.on_pointer_scroll(pointer_scroll_update); + craft_state.craft_app.on_pointer_scroll(window, pointer_scroll_update); }, PointerEvent::Gesture(_) => todo!() } @@ -189,20 +173,29 @@ impl ApplicationHandler for CraftWinitState { match event { WindowEvent::CloseRequested => { - craft_state.close_requested = true; - craft_state.craft_app.on_close_requested(); + WINDOW_MANAGER.with_borrow_mut(|window_manager| { + window_manager.close_window(&window); + if window_manager.is_empty() { + craft_state.close_requested = true; + craft_state.craft_app.on_close_requested(); + } + }); } WindowEvent::ScaleFactorChanged { scale_factor, .. } => { craft_state.craft_app.on_scale_factor_changed(scale_factor); } WindowEvent::Resized(new_size) => { - craft_state.craft_app.on_resize(new_size); + let new_size = Size:: { + width: new_size.width as f32, + height: new_size.height as f32, + }; + craft_state.craft_app.on_resize(window, new_size); } WindowEvent::Ime(ime) => { - craft_state.craft_app.on_ime(ime); + craft_state.craft_app.on_ime(window, ime); } WindowEvent::RedrawRequested => { - craft_state.craft_app.on_request_redraw(); + craft_state.craft_app.on_request_redraw(window); } _ => (), } diff --git a/crates/craft_retained/src/elements/container.rs b/crates/craft_retained/src/elements/container.rs index 2d2a7393..3f65c183 100644 --- a/crates/craft_retained/src/elements/container.rs +++ b/crates/craft_retained/src/elements/container.rs @@ -1,9 +1,8 @@ //! Stores one or more elements. -use crate::app::TAFFY_TREE; use crate::elements::core::{resolve_clip_for_scrollable, ElementInternals}; use crate::elements::element_data::ElementData; -use crate::elements::{scrollable, Element}; +use crate::elements::{scrollable, Element, Window}; use crate::events::{CraftMessage, Event}; use crate::layout::TaffyTree; use crate::text::text_context::TextContext; @@ -13,6 +12,7 @@ use kurbo::{Affine, Point}; use std::any::Any; use std::cell::RefCell; use std::rc::{Rc, Weak}; +use crate::window_manager::WindowManager; /// Stores one or more elements. /// @@ -22,16 +22,20 @@ pub struct Container { } impl Container { - pub fn new() -> Rc> { + pub fn new_mw(window: &Rc>) -> Rc> { let me = Rc::new_cyclic(|me: &Weak>| { RefCell::new(Container { - element_data: ElementData::new(me.clone(), true), + element_data: ElementData::new(Rc::downgrade(window), me.clone(), true), }) }); - me.borrow_mut().element_data.crate_layout_node(None); + me.borrow_mut().element_data.create_layout_node(None); me } + + pub fn new() -> Rc> { + Self::new_mw(&WindowManager::get_main_window()) + } } impl crate::elements::core::ElementData for Container { @@ -54,13 +58,14 @@ impl Element for Container { self.element_data.children.push(child.clone()); // Add the children's taffy node. - TAFFY_TREE.with_borrow_mut(|taffy_tree| { + { + let mut taffy_tree = self.element_data.taffy_tree.borrow_mut(); let parent_id = self.element_data.layout_item.taffy_node_id.unwrap(); let child_id = child.borrow().element_data().layout_item.taffy_node_id; if let Some(child_id) = child_id { taffy_tree.add_child(parent_id, child_id); } - }); + } self } @@ -84,14 +89,15 @@ impl Element for Container { self.element_data.children.extend(children.iter().cloned()); // Add the children's taffy node. - TAFFY_TREE.with_borrow_mut(|taffy_tree| { + { + let mut taffy_tree = self.element_data.taffy_tree.borrow_mut(); let parent_id = self.element_data.layout_item.taffy_node_id.unwrap(); for child in &children { if let Some(child_id) = child.borrow().element_data().layout_item.taffy_node_id { taffy_tree.add_child(parent_id, child_id); } } - }); + }; self } diff --git a/crates/craft_retained/src/elements/core/element_internals.rs b/crates/craft_retained/src/elements/core/element_internals.rs index 32cc0060..0a93fa02 100644 --- a/crates/craft_retained/src/elements/core/element_internals.rs +++ b/crates/craft_retained/src/elements/core/element_internals.rs @@ -17,9 +17,7 @@ use crate::elements::Element; #[cfg(feature = "accesskit")] use accesskit::{Action, Role}; use craft_primitives::geometry::borders::CssRoundedRect; -use crate::app::TAFFY_TREE; use crate::layout::TaffyTree; -use crate::request_layout; /// Internal element methods that should typically be ignored by users. Public for custom elements. pub trait ElementInternals: ElementData { @@ -368,24 +366,19 @@ pub trait ElementInternals: ElementData { /// Mark layout node dirty. fn mark_dirty(&mut self) { - request_layout(); let id = self.element_data().layout_item.taffy_node_id; if let Some(id) = id { - TAFFY_TREE.with_borrow_mut(|taffy_tree| { - taffy_tree.mark_dirty(id); - }); + let mut taffy_tree = self.element_data().taffy_tree.borrow_mut(); + taffy_tree.mark_dirty(id); } } /// Updates taffy's style to reflect craft's style struct. fn update_taffy_style(&mut self) { - request_layout(); - let id = self.element_data().layout_item.taffy_node_id; if let Some(id) = id { - TAFFY_TREE.with_borrow_mut(|taffy_tree| { - taffy_tree.set_style(id, self.element_data().style.to_taffy_style()); - }); + let mut taffy_tree = self.element_data().taffy_tree.borrow_mut(); + taffy_tree.set_style(id, self.element_data().style.to_taffy_style()); } } diff --git a/crates/craft_retained/src/elements/element.rs b/crates/craft_retained/src/elements/element.rs index b073cc4b..818c18a0 100644 --- a/crates/craft_retained/src/elements/element.rs +++ b/crates/craft_retained/src/elements/element.rs @@ -1,4 +1,4 @@ -use crate::app::{DOCUMENTS, ELEMENTS, FOCUS, TAFFY_TREE}; +use crate::app::{DOCUMENTS, ELEMENTS, FOCUS}; use crate::document::Document; use crate::elements::core::ElementData; use crate::elements::ElementIdMap; @@ -37,7 +37,8 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { self.element_data_mut().children.swap(position_1, position_2); // Swap the children's taffy nodes. - TAFFY_TREE.with_borrow_mut(|taffy_tree| { + { + let mut taffy_tree = self.element_data().taffy_tree.borrow_mut(); let parent_id = self.element_data().layout_item.taffy_node_id; let child_1_id = child_1.borrow().element_data().layout_item.taffy_node_id; let child_2_id = child_2.borrow().element_data().layout_item.taffy_node_id; @@ -66,7 +67,7 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { taffy_tree.mark_dirty(parent_id); taffy_tree.request_layout(); } - }); + } Ok(()) } @@ -94,7 +95,8 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { child.borrow_mut().element_data_mut().parent = None; - TAFFY_TREE.with_borrow_mut(|taffy_tree| { + { + let mut taffy_tree = self.element_data().taffy_tree.borrow_mut(); let child_id = child.borrow().element_data().layout_item.taffy_node_id; if let Some(child_id) = child_id { @@ -103,7 +105,7 @@ pub trait Element: ElementData + crate::elements::core::ElementInternals + Any { let parent_id = self.element_data().layout_item.taffy_node_id; taffy_tree.mark_dirty(parent_id.unwrap()); - }); + } // TODO: Move to document fn remove_element_from_document( diff --git a/crates/craft_retained/src/elements/element_data.rs b/crates/craft_retained/src/elements/element_data.rs index 2ecedb47..9d112178 100644 --- a/crates/craft_retained/src/elements/element_data.rs +++ b/crates/craft_retained/src/elements/element_data.rs @@ -1,8 +1,7 @@ use crate::animations::animation::ActiveAnimation; use crate::elements::element_id::create_unique_element_id; -use crate::elements::element_states::ElementState; use crate::elements::scroll_state::ScrollState; -use crate::elements::Element; +use crate::elements::{Element, Window}; use crate::events::{KeyboardInputHandler, PointerCaptureHandler, PointerEnterHandler, PointerEventHandler, PointerLeaveHandler, PointerUpdateHandler, SliderValueChangedHandler}; use crate::layout::layout_item::LayoutItem; use crate::style::Style; @@ -12,30 +11,30 @@ use smol_str::SmolStr; use std::cell::RefCell; use std::rc::{Rc, Weak}; use taffy::{Layout, Overflow}; -use crate::app::{ELEMENTS, TAFFY_TREE}; +use crate::app::{ELEMENTS}; use crate::layout::layout_context::LayoutContext; +use crate::layout::TaffyTree; /// Stores common data to most elements. #[derive(Clone)] pub struct ElementData { - pub current_state: ElementState, - - /// The style of the element. - pub style: Style, + /// A cyclic weak pointer to the element. + pub(crate) me: Weak>, - pub layout_item: LayoutItem, + /// The window which owns this element. + pub(crate) window: Option>>, - /// The style of the element when it is hovered. - pub hover_style: Option + + + + + + + + \ No newline at end of file diff --git a/website/src/docs.rs b/website/src/docs.rs new file mode 100644 index 00000000..d23db73f --- /dev/null +++ b/website/src/docs.rs @@ -0,0 +1,19 @@ +use craft_retained::elements::{Container, Element}; +use craft_retained::pct; +use craft_retained::style::{Display, FlexDirection, Overflow, Unit}; + +use crate::router::NavigateFn; + +pub(crate) fn docs(_navigate_fn: NavigateFn) -> Container { + Container::new() + .width(pct(100)) + .overflow(Overflow::Visible, Overflow::Scroll) + .push( + Container::new() + .display(Display::Flex) + .width(pct(100)) + .margin(Unit::Px(0.0), Unit::Auto, Unit::Px(0.0), Unit::Auto) + .flex_direction(FlexDirection::Column) + .flex_grow(1.0), + ) +} diff --git a/website/src/index.rs b/website/src/index.rs new file mode 100644 index 00000000..bc74c7d3 --- /dev/null +++ b/website/src/index.rs @@ -0,0 +1,177 @@ +use craft_retained::elements::{Container, Element, Text}; +use craft_retained::style::{AlignItems, Display, FlexDirection, FlexWrap, FontWeight, JustifyContent, Overflow, Unit}; +use craft_retained::{Color, ResourceIdentifier, WinitWindow, palette, pct, px, rgb}; +use std::cell::RefCell; +use std::rc::Rc; + +use crate::link::Link; +use crate::router::{NavigateFn, Router}; +use crate::theme::{WRAPPER_PADDING_LEFT, WRAPPER_PADDING_RIGHT, wrapper}; +use crate::web_link::WebLink; + +fn hero_intro(navigate_fn: NavigateFn) -> Container { + let bg_wrapper = Container::new().width(pct(100)).background_color(rgb(45, 48, 53)); + + let inner_wrapper = wrapper() + .display(Display::Flex) + .flex_direction(FlexDirection::Column) + .padding( + Unit::Px(100.0), + WRAPPER_PADDING_RIGHT, + Unit::Px(100.0), + WRAPPER_PADDING_LEFT, + ); + + let inner_wrapper = inner_wrapper + .push( + Text::new("A Reactive GUI Framework for Rust") + .color(Color::WHITE) + /*.font_size(if window_ctx.inner_size().width <= MOBILE_MEDIA_QUERY_WIDTH { + 36.0 + } else { + 56.0 + })*/ + .font_size(56.0) + .line_height(1.0) + .max_width(px(680)) + .font_weight(FontWeight::BOLD) + .margin(px(0), px(0), px(32), px(0)), + ) + .push( + Text::new("Build your UI with regular Rust code.") + .line_height(1.0) + .color(Color::WHITE) + .font_size(20.0), + ); + + let github_button = Text::new("GitHub") + .display(Display::Flex) + .align_items(Some(AlignItems::Center)) + .justify_content(Some(JustifyContent::Center)) + .font_size(22.0) + .min_width(px(100)) + .border_width(px(1), px(1), px(1), px(1)) + .border_radius((8.0, 8.0), (8.0, 8.0), (8.0, 8.0), (8.0, 8.0)) + .padding(px(8), px(20), px(8), px(20)) + .border_color( + palette::css::WHITE, + palette::css::WHITE, + palette::css::WHITE, + palette::css::WHITE, + ) + .color(palette::css::WHITE); + + let craft_button = Text::new("Learn Craft") + .id("xxx") + .display(Display::Flex) + .align_items(Some(AlignItems::Center)) + .justify_content(Some(JustifyContent::Center)) + .font_size(22.0) + .min_width(px(100)) + .border_radius((8.0, 8.0), (8.0, 8.0), (8.0, 8.0), (8.0, 8.0)) + .padding(px(8), px(20), px(8), px(20)) + .background_color(rgb(69, 117, 230)) + .color(palette::css::WHITE); + + let buttons = Container::new() + .display(Display::Flex) + .wrap(FlexWrap::Wrap) + .gap(px(17), px(17)) + .margin(px(40), px(0), px(0), px(0)) + .push(Link("/docs", move || navigate_fn("/docs")).push(craft_button)) + .push(WebLink("https://github.com/craft-gui/craft").push(github_button)); + + let inner_wrapper = inner_wrapper.push(buttons); + + bg_wrapper.push(inner_wrapper) +} + +fn hero_features() -> Container { + fn hero_item(title: &str, text: &str, icon: ResourceIdentifier) -> Container { + let sub_title_color = Color::from_rgb8(70, 70, 70); + + let icon_title = Container::new() + // .push(TinyVg::new(icon)) + .push( + Text::new(title) + .font_weight(FontWeight::MEDIUM) + .font_size(24.0) + .margin(px(0), px(0), px(0), px(10)), + ); + + Container::new() + .gap(px(10), px(10)) + .flex_grow(1.0) + .flex_shrink(1.0) + .min_width(px(320)) + .flex_basis(pct(50)) + .display(Display::Flex) + .flex_direction(FlexDirection::Column) + .push(icon_title) + .push(Text::new(text).font_size(18.0).color(sub_title_color)) + } + + Container::new() + .background_color(rgb(247, 247, 247)) + .width(pct(100)) + .push( + wrapper() + .padding(Unit::Px(100.0), WRAPPER_PADDING_LEFT, Unit::Px(100.0), WRAPPER_PADDING_RIGHT) + .display(Display::Flex) + .wrap(FlexWrap::Wrap) + .gap(px(0), px(50)) + .push(Text::new("Features").width(pct(100)).font_size(36.0).font_weight(FontWeight::SEMIBOLD)) + .push( + hero_item( + "Reactive", + "When your data changes, we automatically re-run your view function.", + ResourceIdentifier::Bytes(include_bytes!("../assets/electric_bolt_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg")) + ) + ) + .push( + hero_item( + "Components", + "Components are reusable blocks that manage their own state and define both how they are rendered and how they respond to updates.", + ResourceIdentifier::Bytes(include_bytes!("../assets/view_comfy_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg")) + ) + ) + .push( + hero_item( + "Pure Rust without macros", + "No macros.", + ResourceIdentifier::Bytes(include_bytes!("../assets/code_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg")) + ) + ) + .push( + hero_item( + "Web-like styling", + "We use Taffy, an implementation of the CSS flexbox, block, and grid layout algorithms, for simple and familiar styling.", + ResourceIdentifier::Bytes(include_bytes!("../assets/brush_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg")) + ) + ) + .push( + hero_item( + "Cross Platform", + "Currently we support Windows, macOS, Linux, Web, and Android.", + ResourceIdentifier::Bytes(include_bytes!("../assets/devices_24dp_000000_FILL0_wght400_GRAD0_opsz24.tvg")) + ) + ) + + ) +} + +pub(crate) fn index_page(navigate_fn: NavigateFn) -> Container { + Container::new() + .width(pct(100)) + .overflow(Overflow::Visible, Overflow::Scroll) + .push( + Container::new() + .display(Display::Flex) + .width(pct(100)) + .margin(Unit::Px(0.0), Unit::Auto, Unit::Px(0.0), Unit::Auto) + .flex_direction(FlexDirection::Column) + .flex_grow(1.0) + .push(hero_intro(navigate_fn)) + .push(hero_features()), + ) +} diff --git a/website/src/link.rs b/website/src/link.rs new file mode 100644 index 00000000..46a27bf2 --- /dev/null +++ b/website/src/link.rs @@ -0,0 +1,18 @@ +use std::rc::Rc; + +use craft_retained::elements::{Container, Element}; +use craft_retained::events::ui_events::pointer::PointerButton; + +#[allow(non_snake_case)] +pub fn Link(href: &str, on_click: F) -> Container +where + F: Fn() + 'static, +{ + let on_click = Rc::new(on_click); + + Container::new().on_pointer_button_up(Rc::new(move |_event, pointer_button_event| { + if pointer_button_event.button == Some(PointerButton::Primary) { + on_click(); + } + })) +} diff --git a/website/src/main.rs b/website/src/main.rs new file mode 100644 index 00000000..204acafd --- /dev/null +++ b/website/src/main.rs @@ -0,0 +1,100 @@ +use std::cell::RefCell; +use std::rc::Rc; + +use craft_retained::elements::{Container, Element, ElementInternals, Window}; +use craft_retained::{CraftOptions, craft_main, pct}; + +use crate::router::Router; + +mod docs; +mod index; +mod link; +mod navbar; +mod router; +mod theme; +mod web_link; + +pub(crate) struct WebsiteGlobalState { + /// The current route that we are viewing. + route: String, +} + +impl WebsiteGlobalState { + pub(crate) fn get_route(&self) -> String { + #[cfg(target_arch = "wasm32")] + let path: String; + #[cfg(target_arch = "wasm32")] + { + let window = web_sys::window().expect("No window available."); + path = window + .location() + .pathname() + .map(|s| { + let trimmed_path = s.trim_end_matches('/'); + if trimmed_path.is_empty() { + "/".to_string() + } else { + trimmed_path.to_string() + } + }) + .unwrap_or("/".to_string()); + } + #[cfg(not(target_arch = "wasm32"))] + let path = self.route.clone(); + path + } + + pub(crate) fn set_route(&mut self, route: &str) { + self.route = route.to_string(); + + #[cfg(target_arch = "wasm32")] + { + let window = web_sys::window().unwrap(); + let history = window.history().unwrap(); + + history + .push_state_with_url(&web_sys::wasm_bindgen::JsValue::NULL, "", Some(route)) + .unwrap(); + } + } + + pub fn load_route(&mut self) { + #[cfg(not(target_arch = "wasm32"))] + { + // NOTE: In Git Bash, use `cargo run -- //examples`. + let route = std::env::args().nth(1).unwrap_or_else(|| "/".to_string()); + self.set_route(route.as_str()); + } + } +} + +impl Default for WebsiteGlobalState { + fn default() -> Self { + WebsiteGlobalState { + route: "/".to_string(), + } + } +} + +fn main() { + let options = CraftOptions { + ..Default::default() + }; + + #[allow(unused_mut)] + let mut global_state = Rc::new(RefCell::new(WebsiteGlobalState::default())); + + util::setup_logging(); + + global_state.borrow_mut().load_route(); + + let page_wrapper = Router::new(global_state.clone()); + + /* let root = page_wrapper.borrow().root.clone(); + + root.inner.borrow().print_tree_ids(4); + */ + page_wrapper.borrow().navigate(); + + craft_main(options); +} diff --git a/website/src/navbar.rs b/website/src/navbar.rs new file mode 100644 index 00000000..6ce616e5 --- /dev/null +++ b/website/src/navbar.rs @@ -0,0 +1,69 @@ +use crate::link::Link; +use crate::router::NavigateFn; +use crate::theme::{NAVBAR_BACKGROUND_COLOR, NAVBAR_TEXT_COLOR, wrapper}; +use craft_retained::elements::{Container, Element, Text}; +use craft_retained::style::{AlignItems, Display, FontWeight, JustifyContent, Unit}; +use craft_retained::{pct, px, rgb}; + +pub const NAVBAR_HEIGHT: f32 = 60.0; + +fn create_link(navigate_fn: NavigateFn, label: &str, route: &str) -> Container { + let route_owned = route.to_string(); + let nav = navigate_fn.clone(); + Link(route, move || { + nav(&route_owned); + }) + .push( + Text::new(label) + .id(format!("route_{route}").as_str()) + .margin(px(0), px(12), px(0), px(0)) + .font_size(16.0) + .selectable(false) + .color(NAVBAR_TEXT_COLOR), + ) + /*.hovered() + .color(NAVBAR_TEXT_HOVERED_COLOR) + .underline(1.0, Color::BLACK, None) + .margin(px(0), "12px", px(0), px(0)) + .font_size(16.5) + .disable_selection() + .normal()*/ +} + +pub fn navbar(navigate_fn: NavigateFn) -> Container { + let border_color = rgb(240, 240, 240); + let container = Container::new() + .width(pct(100)) + .height(Unit::Px(NAVBAR_HEIGHT)) + .min_height(Unit::Px(NAVBAR_HEIGHT)) + .max_height(Unit::Px(NAVBAR_HEIGHT)) + .border_width(px(0), px(0), px(2), px(0)) + .border_color(border_color, border_color, border_color, border_color) + .background_color(NAVBAR_BACKGROUND_COLOR); + + let wrapper = wrapper() + .display(Display::Flex) + .justify_content(Some(JustifyContent::SpaceBetween)) + .align_items(Some(AlignItems::Center)) + // Left + .push( + Container::new() + .display(Display::Flex) + .justify_content(Some(JustifyContent::Center)) + .align_items(Some(AlignItems::Center)) + .push( + create_link(navigate_fn.clone(), "Craft", "/") + .font_size(32.0) + .font_weight(FontWeight::BOLD) + .margin(px(0), px(24), px(0), px(0)), /*.hovered() + .font_size(32.0) + .font_weight(Weight::BOLD) + .margin(px(0), "24px", px(0), px(0)),*/ + ) + .push(create_link(navigate_fn.clone(), "Home", "/").margin(px(0), px(12), px(0), px(0))) + .push(create_link(navigate_fn.clone(), "Docs", "/docs").margin(px(0), px(12), px(0), px(0))) + .push(create_link(navigate_fn.clone(), "Examples", "/examples").margin(px(0), px(12), px(0), px(0))), + ); + + container.push(wrapper) +} diff --git a/website/src/router.rs b/website/src/router.rs new file mode 100644 index 00000000..f0f3dc44 --- /dev/null +++ b/website/src/router.rs @@ -0,0 +1,76 @@ +use craft_retained::elements::{Container, Element, Window}; +use craft_retained::pct; +use craft_retained::style::{Display, FlexDirection}; +use std::cell::RefCell; +use std::rc::{Rc, Weak}; +use std::sync::Arc; + +use crate::docs::docs; +use crate::index::index_page; +use crate::navbar::navbar; +use crate::theme::BODY_BACKGROUND_COLOR; +use crate::{WebsiteGlobalState, docs, index}; + +#[derive(Clone)] +pub struct Router { + pub root: Window, + global_state: Rc>, + index: Container, + docs: Container, +} + +pub type NavigateFn = Rc; + +impl Router { + pub fn new(global_state: Rc>) -> Rc> { + let state = global_state.clone(); + Rc::new_cyclic(|me: &Weak>| { + let me = me.clone(); + + let navigate_logic: NavigateFn = Rc::new(move |route| { + state.borrow_mut().set_route(route); + if let Some(router) = me.upgrade() { + router.borrow().navigate(); + } + }); + + let window = Window::new("Craft Gui") + .display(Display::Flex) + .flex_direction(FlexDirection::Column) + .width(pct(100)) + .height(pct(100)) + .push(navbar(navigate_logic.clone())) + .background_color(BODY_BACKGROUND_COLOR); + + RefCell::new(Self { + root: window.clone(), + index: index_page(navigate_logic.clone()), + docs: docs(navigate_logic.clone()), + global_state: global_state.clone(), + }) + }) + } + + fn set_content(&self, container: Container) { + if let Some(current_content) = self.root.get_children().get(1) { + self.root + .remove_child(current_content.clone()) + .expect("Failed to remove child"); + } + self.root.clone().push(container); + } + + pub fn navigate(&self) { + let page = match self.global_state.borrow().route.as_str() { + "/" => self.index.clone(), + "/docs" => self.docs.clone(), + _ => self.index.clone(), + }; + + self.set_content(page); + } + + /*pub fn window(&self) -> Arc { + self.root.inner.borrow().winit_window().expect("No widow") + }*/ +} diff --git a/website/src/theme.rs b/website/src/theme.rs new file mode 100644 index 00000000..17456eb1 --- /dev/null +++ b/website/src/theme.rs @@ -0,0 +1,33 @@ +use craft_retained::elements::{Container, Element}; +use craft_retained::style::Unit; +use craft_retained::{Color, pct}; + +pub(crate) const BODY_BACKGROUND_COLOR: Color = Color::from_rgb8(255, 255, 255); + +pub(crate) const NAVBAR_BACKGROUND_COLOR: Color = Color::from_rgb8(255, 255, 255); +pub(crate) const NAVBAR_TEXT_COLOR: Color = Color::from_rgb8(50, 50, 50); +//pub(crate) const NAVBAR_TEXT_HOVERED_COLOR: Color = Color::from_rgb8(0, 0, 0); + +//pub(crate) const ACTIVE_LINK_COLOR: Color = Color::from_rgb8(42, 108, 200); +//pub(crate) const DEFAULT_LINK_COLOR: Color = Color::from_rgb8(102, 102, 102); + +pub(crate) const WRAPPER_MAX_WIDTH: Unit = Unit::Px(1300.0); +pub(crate) const WRAPPER_MARGIN_LEFT: Unit = Unit::Auto; +pub(crate) const WRAPPER_MARGIN_RIGHT: Unit = Unit::Auto; +pub(crate) const WRAPPER_PADDING_LEFT: Unit = Unit::Px(20.0); +pub(crate) const WRAPPER_PADDING_RIGHT: Unit = Unit::Px(20.0); + +//pub(crate) const MOBILE_MEDIA_QUERY_WIDTH: u32 = 850; + +pub(crate) fn wrapper() -> Container { + Container::new() + .margin(Unit::Px(0.0), WRAPPER_MARGIN_RIGHT, Unit::Px(0.0), WRAPPER_MARGIN_LEFT) + .padding( + Unit::Px(0.0), + WRAPPER_PADDING_RIGHT, + Unit::Px(0.0), + WRAPPER_PADDING_LEFT, + ) + .width(pct(100)) + .max_width(WRAPPER_MAX_WIDTH) +} diff --git a/website/src/web_link.rs b/website/src/web_link.rs new file mode 100644 index 00000000..564fbdc6 --- /dev/null +++ b/website/src/web_link.rs @@ -0,0 +1,26 @@ +use std::rc::Rc; + +use craft_retained::elements::{Container, Element}; +use craft_retained::events::ui_events::pointer::PointerButton; + +#[allow(non_snake_case)] +pub fn WebLink(href: &str) -> Container { + let href = href.to_string(); + + Container::new().on_pointer_button_up(Rc::new(move |_event, pointer_button_event| { + if pointer_button_event.button == Some(PointerButton::Primary) { + #[cfg(target_arch = "wasm32")] + { + if let Some(win) = web_sys::window() { + // Use the captured owned string + let _ = win.open_with_url(&href); + } + } + + #[cfg(not(target_arch = "wasm32"))] + { + open::that(&href).unwrap(); + } + } + })) +} diff --git a/website/wasm-build.sh b/website/wasm-build.sh new file mode 100644 index 00000000..07fb3318 --- /dev/null +++ b/website/wasm-build.sh @@ -0,0 +1,12 @@ +set -e + + +#rustup target add wasm32-unknown-unknown +#cargo install -f wasm-bindgen-cli +#cargo install simple-http-server + +cargo build --target wasm32-unknown-unknown --release + +wasm-bindgen ../target/wasm32-unknown-unknown/release/website.wasm --target web --no-typescript --out-dir dist --out-name website +cp index.html dist/index.html +simple-http-server dist -c wasm,html,js --try-file dist/index.html -i --coep --coop --ip 0.0.0.0 \ No newline at end of file From 1d6b8273ece6481c3ea6b5ec756894c45f84a7ae Mon Sep 17 00:00:00 2001 From: "Austin M. Reppert" Date: Sun, 29 Mar 2026 00:33:37 -0400 Subject: [PATCH 122/143] Maybe fix hit test with scrollable --- crates/craft_renderer/src/helpers.rs | 12 +++- crates/craft_renderer/src/lib.rs | 14 ++--- crates/craft_renderer/src/render_command.rs | 16 ++--- crates/craft_renderer/src/render_list.rs | 61 ++++++++----------- crates/craft_renderer/src/renderer.rs | 4 +- crates/craft_renderer/src/screenshot.rs | 2 +- crates/craft_renderer/src/sort_commands.rs | 4 +- crates/craft_renderer/src/target_item.rs | 2 +- crates/craft_renderer/src/vello/mod.rs | 23 ++++--- crates/craft_renderer/src/vello_cpu/mod.rs | 30 ++++----- crates/craft_renderer/src/vello_cpu/tinyvg.rs | 2 +- crates/craft_renderer/src/vello_hybrid/mod.rs | 18 +++--- .../craft_renderer/src/vello_hybrid/tinyvg.rs | 3 +- .../craft_retained/src/elements/container.rs | 8 ++- .../craft_retained/src/elements/dropdown.rs | 4 +- .../src/elements/internal_helpers.rs | 10 ++- .../src/elements/slider/slider_element.rs | 4 +- crates/craft_retained/src/elements/text.rs | 7 ++- .../src/elements/text_input/mod.rs | 6 +- .../src/elements/traits/element_internals.rs | 6 +- crates/craft_retained/src/elements/window.rs | 15 ++++- crates/craft_retained/src/events/helpers.rs | 3 +- crates/craft_retained/src/layout/layout.rs | 11 ++-- website/src/index.rs | 3 +- 24 files changed, 152 insertions(+), 116 deletions(-) diff --git a/crates/craft_renderer/src/helpers.rs b/crates/craft_renderer/src/helpers.rs index fd2d2f45..efa7a9bb 100644 --- a/crates/craft_renderer/src/helpers.rs +++ b/crates/craft_renderer/src/helpers.rs @@ -1,9 +1,17 @@ -#[cfg(any(feature = "vello_cpu_renderer", feature = "vello_hybrid_renderer", feature = "vello_hybrid_renderer_webgl"))] +#[cfg(any( + feature = "vello_cpu_renderer", + feature = "vello_hybrid_renderer", + feature = "vello_hybrid_renderer_webgl" +))] use vello_common::paint::PaintType; use crate::Brush; -#[cfg(any(feature = "vello_cpu_renderer", feature = "vello_hybrid_renderer", feature = "vello_hybrid_renderer_webgl"))] +#[cfg(any( + feature = "vello_cpu_renderer", + feature = "vello_hybrid_renderer", + feature = "vello_hybrid_renderer_webgl" +))] pub(crate) fn brush_to_paint(brush: &Brush) -> PaintType { match brush { Brush::Color(color) => PaintType::Solid(*color), diff --git a/crates/craft_renderer/src/lib.rs b/crates/craft_renderer/src/lib.rs index 150e807d..8220d85d 100644 --- a/crates/craft_renderer/src/lib.rs +++ b/crates/craft_renderer/src/lib.rs @@ -10,22 +10,22 @@ pub mod vello; pub mod vello_cpu; pub mod blank_renderer; +pub(crate) mod helpers; mod image_adapter; pub mod render_command; +mod render_list; mod renderer_type; +mod screenshot; +mod sort_commands; +mod target_item; pub mod text_renderer_data; pub(crate) mod tinyvg_helpers; #[cfg(feature = "vello_hybrid_renderer")] pub mod vello_hybrid; -pub(crate) mod helpers; -mod render_list; -mod sort_commands; -mod screenshot; -mod target_item; +pub use brush::Brush; pub use render_command::RenderCommand; pub use render_list::RenderList; -pub use brush::Brush; +pub use renderer_type::RendererType; pub use screenshot::Screenshot; pub use target_item::TargetItem; -pub use renderer_type::RendererType; diff --git a/crates/craft_renderer/src/render_command.rs b/crates/craft_renderer/src/render_command.rs index 9a07b383..c9296dec 100644 --- a/crates/craft_renderer/src/render_command.rs +++ b/crates/craft_renderer/src/render_command.rs @@ -28,27 +28,27 @@ pub enum RenderCommand { #[derive(Clone)] pub struct DrawRectCmd { pub rect: Rectangle, - pub color: Color + pub color: Color, } #[derive(Clone)] pub struct DrawRectOutlineCmd { pub rect: Rectangle, pub outline_color: Color, - pub thickness: f64 + pub thickness: f64, } #[derive(Clone)] pub struct DrawImageCmd { pub rect: Rectangle, - pub resource_id: ResourceIdentifier + pub resource_id: ResourceIdentifier, } #[derive(Clone)] pub struct DrawTinyVgCmd { pub rect: Rectangle, pub resource_id: ResourceIdentifier, - pub override_color: Option + pub override_color: Option, } #[derive(Clone)] @@ -56,19 +56,19 @@ pub struct DrawTextCmd { pub rect: Rectangle, pub data: Weak>, pub text_scroll: Option, - pub show_cursor: bool + pub show_cursor: bool, } #[derive(Clone)] pub enum PushLayerCmd { BezPath(BezPath), - Rect(Rectangle) + Rect(Rectangle), } #[derive(Clone)] pub struct FillBezPathCmd { pub path: BezPath, - pub brush: Brush + pub brush: Brush, } #[derive(Clone)] @@ -80,4 +80,4 @@ pub struct BoxShadowCmd { pub blur_radius: f64, pub color: Color, pub border_box: Rectangle, -} \ No newline at end of file +} diff --git a/crates/craft_renderer/src/render_list.rs b/crates/craft_renderer/src/render_list.rs index 205c2350..448622c3 100644 --- a/crates/craft_renderer/src/render_list.rs +++ b/crates/craft_renderer/src/render_list.rs @@ -6,12 +6,10 @@ use peniko::Color; use craft_primitives::geometry::{BezPath, Rectangle, Shape}; use craft_resource_manager::ResourceIdentifier; -use crate::Brush; use crate::render_command::{BoxShadowCmd, DrawImageCmd, DrawRectCmd, DrawRectOutlineCmd, DrawTextCmd, DrawTinyVgCmd, FillBezPathCmd, PushLayerCmd}; -use crate::{RenderCommand}; -use crate::{TargetItem}; use crate::sort_commands::SortedCommands; -use crate::text_renderer_data::{TextScroll, TextData}; +use crate::text_renderer_data::{TextData, TextScroll}; +use crate::{Brush, RenderCommand, TargetItem}; pub struct RenderList { current_overlay_depth: u64, @@ -52,9 +50,7 @@ impl RenderList { { return; } - self.commands.push( - RenderCommand::DrawRect(DrawRectCmd { rect, color }) - ); + self.commands.push(RenderCommand::DrawRect(DrawRectCmd { rect, color })); } #[inline(always)] @@ -75,9 +71,11 @@ impl RenderList { { return; } - self.commands.push( - RenderCommand::DrawRectOutline(DrawRectOutlineCmd { rect, outline_color, thickness }) - ); + self.commands.push(RenderCommand::DrawRectOutline(DrawRectOutlineCmd { + rect, + outline_color, + thickness, + })); } #[inline(always)] @@ -87,9 +85,8 @@ impl RenderList { { return; } - self.commands.push( - RenderCommand::FillBezPath(FillBezPathCmd { path, brush }) - ); + self.commands + .push(RenderCommand::FillBezPath(FillBezPathCmd { path, brush })); } #[inline(always)] @@ -105,9 +102,12 @@ impl RenderList { { return; } - self.commands.push( - RenderCommand::DrawText(DrawTextCmd{ rect, data, text_scroll, show_cursor}) - ); + self.commands.push(RenderCommand::DrawText(DrawTextCmd { + rect, + data, + text_scroll, + show_cursor, + })); } #[inline(always)] @@ -117,30 +117,22 @@ impl RenderList { { return; } - self.commands.push( - RenderCommand::DrawImage(DrawImageCmd { rect, resource_id }) - ); + self.commands + .push(RenderCommand::DrawImage(DrawImageCmd { rect, resource_id })); } #[inline(always)] - pub fn draw_tiny_vg( - &mut self, - rect: Rectangle, - resource_id: ResourceIdentifier, - override_color: Option, - ) { + pub fn draw_tiny_vg(&mut self, rect: Rectangle, resource_id: ResourceIdentifier, override_color: Option) { if let Some(cull) = &self.cull && !cull.intersects(&rect) { return; } - self.commands.push( - RenderCommand::DrawTinyVg(DrawTinyVgCmd { - rect, - resource_id, - override_color, - } - )); + self.commands.push(RenderCommand::DrawTinyVg(DrawTinyVgCmd { + rect, + resource_id, + override_color, + })); } #[inline(always)] @@ -149,7 +141,8 @@ impl RenderList { } pub fn push_layer_with_bez_path(&mut self, path: BezPath) { - self.commands.push(RenderCommand::PushLayer(PushLayerCmd::BezPath(path))); + self.commands + .push(RenderCommand::PushLayer(PushLayerCmd::BezPath(path))); } #[inline(always)] @@ -184,4 +177,4 @@ impl RenderList { pub fn set_cull(&mut self, cull: Option) { self.cull = cull; } -} \ No newline at end of file +} diff --git a/crates/craft_renderer/src/renderer.rs b/crates/craft_renderer/src/renderer.rs index b53e3fda..63f56e40 100644 --- a/crates/craft_renderer/src/renderer.rs +++ b/crates/craft_renderer/src/renderer.rs @@ -1,8 +1,8 @@ use std::any::Any; use std::sync::Arc; -use craft_primitives::geometry::Rectangle; use craft_primitives::Color; +use craft_primitives::geometry::Rectangle; use craft_resource_manager::ResourceManager; @@ -40,4 +40,4 @@ pub trait Renderer: Any { pixels: Vec::new(), } } -} \ No newline at end of file +} diff --git a/crates/craft_renderer/src/screenshot.rs b/crates/craft_renderer/src/screenshot.rs index 570cb866..20a58b2c 100644 --- a/crates/craft_renderer/src/screenshot.rs +++ b/crates/craft_renderer/src/screenshot.rs @@ -2,4 +2,4 @@ pub struct Screenshot { pub width: u16, pub height: u16, pub pixels: Vec, -} \ No newline at end of file +} diff --git a/crates/craft_renderer/src/sort_commands.rs b/crates/craft_renderer/src/sort_commands.rs index 3e927fa3..80fd57c4 100644 --- a/crates/craft_renderer/src/sort_commands.rs +++ b/crates/craft_renderer/src/sort_commands.rs @@ -1,5 +1,5 @@ -use craft_primitives::geometry::{Rectangle, Shape}; use crate::{RenderCommand, RenderList}; +use craft_primitives::geometry::{Rectangle, Shape}; #[derive(Debug)] pub enum SortedItem { @@ -116,4 +116,4 @@ pub(crate) fn sort_and_cull_render_list_internal(surface_height: f32, render_lis } } } -} \ No newline at end of file +} diff --git a/crates/craft_renderer/src/target_item.rs b/crates/craft_renderer/src/target_item.rs index 60d1df14..2431fbc1 100644 --- a/crates/craft_renderer/src/target_item.rs +++ b/crates/craft_renderer/src/target_item.rs @@ -20,4 +20,4 @@ impl TargetItem { pub fn sort_items_by_overlay_depth(targets: &mut [TargetItem]) { targets.sort_by_key(|t1| t1.overlay_depth); } -} \ No newline at end of file +} diff --git a/crates/craft_renderer/src/vello/mod.rs b/crates/craft_renderer/src/vello/mod.rs index 635cb2b2..386f705f 100644 --- a/crates/craft_renderer/src/vello/mod.rs +++ b/crates/craft_renderer/src/vello/mod.rs @@ -19,14 +19,14 @@ use wgpu::{Adapter, Device, Instance, Limits, MemoryHints, Queue, Surface, Surfa use winit::window::Window; +use crate::RenderCommand; use crate::image_adapter::ImageAdapter; use crate::render_command::PushLayerCmd; -use crate::RenderCommand; -use crate::renderer::{Renderer}; -use crate::text_renderer_data::{TextRenderLine, TextScroll}; -use crate::vello::tinyvg::draw_tiny_vg; use crate::render_list::RenderList; +use crate::renderer::Renderer; use crate::sort_commands::SortedCommands; +use crate::text_renderer_data::{TextRenderLine, TextScroll}; +use crate::vello::tinyvg::draw_tiny_vg; pub struct RenderSurface { pub surface: Surface<'static>, @@ -316,8 +316,7 @@ impl Renderer for VelloRenderer { let vello_image = vello::peniko::ImageBrush::new(vello_image); let mut transform = Affine::IDENTITY; - transform = - transform.with_translation(kurbo::Vec2::new(cmd.rect.x as f64, cmd.rect.y as f64)); + transform = transform.with_translation(kurbo::Vec2::new(cmd.rect.x as f64, cmd.rect.y as f64)); transform = transform.pre_scale_non_uniform( cmd.rect.width as f64 / image.width() as f64, cmd.rect.height as f64 / image.height() as f64, @@ -425,7 +424,9 @@ impl Renderer for VelloRenderer { } }); - if cmd.show_cursor && let Some((cursor, cursor_color)) = &text_render.cursor { + if cmd.show_cursor + && let Some((cursor, cursor_color)) = &text_render.cursor + { let cursor_rect = Rectangle { x: cursor.x + cmd.rect.x, y: -scroll + cursor.y + cmd.rect.y, @@ -450,7 +451,13 @@ impl Renderer for VelloRenderer { scene.push_layer(Fill::NonZero, BlendMode::default(), 1.0, Affine::IDENTITY, p); } PushLayerCmd::Rect(r) => { - scene.push_layer(Fill::NonZero, BlendMode::default(), 1.0, Affine::IDENTITY, &r.to_kurbo()); + scene.push_layer( + Fill::NonZero, + BlendMode::default(), + 1.0, + Affine::IDENTITY, + &r.to_kurbo(), + ); } }; } diff --git a/crates/craft_renderer/src/vello_cpu/mod.rs b/crates/craft_renderer/src/vello_cpu/mod.rs index 420441a2..cf3d5001 100644 --- a/crates/craft_renderer/src/vello_cpu/mod.rs +++ b/crates/craft_renderer/src/vello_cpu/mod.rs @@ -6,11 +6,11 @@ use std::ops::{Deref, DerefMut}; use std::sync::Arc; use craft_primitives::geometry::Rectangle; -use craft_resource_manager::resource::Resource; use craft_resource_manager::ResourceManager; +use craft_resource_manager::resource::Resource; use peniko::kurbo::{Affine, Shape}; -use peniko::{kurbo, BlendMode, Blob, Color, Compose, Fill, ImageAlphaType, Mix}; +use peniko::{BlendMode, Blob, Color, Compose, Fill, ImageAlphaType, Mix, kurbo}; use softbuffer::Buffer; @@ -22,16 +22,16 @@ use vello_cpu::{Pixmap, RenderContext}; use winit::window::Window; +use crate::RenderCommand; use crate::helpers::{brush_to_paint, rgba_to_encoded_u32}; use crate::image_adapter::ImageAdapter; use crate::render_command::{BoxShadowCmd, PushLayerCmd}; use crate::render_list::RenderList; -use crate::renderer::{Renderer}; +use crate::renderer::Renderer; +use crate::screenshot::Screenshot; +use crate::sort_commands::SortedCommands; use crate::text_renderer_data::{TextRenderLine, TextScroll}; use crate::vello_cpu::tinyvg::draw_tiny_vg; -use crate::RenderCommand; -use crate::sort_commands::SortedCommands; -use crate::screenshot::Screenshot; pub(crate) struct VelloCpuRenderer { render_context: RenderContext, @@ -187,8 +187,7 @@ impl Renderer for VelloCpuRenderer { }; let mut transform = Affine::IDENTITY; - transform = - transform.with_translation(kurbo::Vec2::new(cmd.rect.x as f64, cmd.rect.y as f64)); + transform = transform.with_translation(kurbo::Vec2::new(cmd.rect.x as f64, cmd.rect.y as f64)); transform = transform.pre_scale_non_uniform( cmd.rect.width as f64 / image.width() as f64, cmd.rect.height as f64 / image.height() as f64, @@ -214,8 +213,8 @@ impl Renderer for VelloCpuRenderer { } } RenderCommand::DrawText(cmd) => { - let text_transform = - kurbo::Affine::default().with_translation(kurbo::Vec2::new(cmd.rect.x as f64, cmd.rect.y as f64)); + let text_transform = kurbo::Affine::default() + .with_translation(kurbo::Vec2::new(cmd.rect.x as f64, cmd.rect.y as f64)); let scroll = cmd.text_scroll.unwrap_or(TextScroll::default()).scroll_y; let text_transform = text_transform.then_translate(kurbo::Vec2::new(0.0, -scroll as f64)); @@ -309,7 +308,9 @@ impl Renderer for VelloCpuRenderer { } }); - if cmd.show_cursor && let Some((cursor, cursor_color)) = &text_render.cursor { + if cmd.show_cursor + && let Some((cursor, cursor_color)) = &text_render.cursor + { let cursor_rect = Rectangle { x: cursor.x + cmd.rect.x, y: -scroll + cursor.y + cmd.rect.y, @@ -322,11 +323,10 @@ impl Renderer for VelloCpuRenderer { RenderCommand::PushLayer(cmd) => { let clip_path = match cmd { PushLayerCmd::BezPath(path) => path, - PushLayerCmd::Rect(rect) => &rect.to_kurbo().into_path(0.1) + PushLayerCmd::Rect(rect) => &rect.to_kurbo().into_path(0.1), }; - self.render_context - .push_layer(Some(clip_path), None, None, None, None); + self.render_context.push_layer(Some(clip_path), None, None, None, None); } RenderCommand::PopLayer => { self.render_context.pop_layer(); @@ -431,4 +431,4 @@ impl VelloCpuRenderer { self.render_context.pop_layer(); } } -} \ No newline at end of file +} diff --git a/crates/craft_renderer/src/vello_cpu/tinyvg.rs b/crates/craft_renderer/src/vello_cpu/tinyvg.rs index e22308e2..0af9a402 100644 --- a/crates/craft_renderer/src/vello_cpu/tinyvg.rs +++ b/crates/craft_renderer/src/vello_cpu/tinyvg.rs @@ -10,8 +10,8 @@ use tinyvg_rs::commands::{DrawCommand, Path, PathCommand, Segment, Style}; use tinyvg_rs::common::Unit; use vello_cpu::RenderContext; -use crate::{Brush, tinyvg_helpers}; use crate::helpers::brush_to_paint; +use crate::{Brush, tinyvg_helpers}; fn stroke_path(scene: &mut RenderContext, bez_path: &BezPath, affine: &Affine, line_width: f64, brush: &Brush) { scene.set_stroke(Stroke::new(line_width)); diff --git a/crates/craft_renderer/src/vello_hybrid/mod.rs b/crates/craft_renderer/src/vello_hybrid/mod.rs index 3b430b1d..853e0021 100644 --- a/crates/craft_renderer/src/vello_hybrid/mod.rs +++ b/crates/craft_renderer/src/vello_hybrid/mod.rs @@ -23,14 +23,14 @@ use vello_hybrid::{RenderSize, RenderTargetConfig, Renderer, Scene}; use wgpu::TextureFormat; use winit::window::Window; -use crate::{Brush, RenderCommand}; use crate::render_command::{BoxShadowCmd, PushLayerCmd}; use crate::render_list::RenderList; -use crate::renderer::{Renderer as CraftRenderer}; +use crate::renderer::Renderer as CraftRenderer; +use crate::sort_commands::SortedCommands; use crate::text_renderer_data::{TextRenderLine, TextScroll}; use crate::vello_hybrid::render_context::{RenderContext, RenderSurface}; use crate::vello_hybrid::tinyvg::draw_tiny_vg; -use crate::sort_commands::SortedCommands; +use crate::{Brush, RenderCommand}; pub struct ActiveRenderState { // The fields MUST be in this order, so that the surface is dropped before the window @@ -302,16 +302,14 @@ impl CraftRenderer for VelloHybridRenderer { &mut encoder, &pixmap, ); - self.images - .insert(cmd.resource_id.clone(), (image_id, expiration_time)); + self.images.insert(cmd.resource_id.clone(), (image_id, expiration_time)); image_id }; seen_images.insert(image_id); let mut transform = Affine::IDENTITY; - transform = - transform.with_translation(kurbo::Vec2::new(cmd.rect.x as f64, cmd.rect.y as f64)); + transform = transform.with_translation(kurbo::Vec2::new(cmd.rect.x as f64, cmd.rect.y as f64)); transform = transform.pre_scale_non_uniform( cmd.rect.width as f64 / image.width() as f64, cmd.rect.height as f64 / image.height() as f64, @@ -425,7 +423,9 @@ impl CraftRenderer for VelloHybridRenderer { } }); - if cmd.show_cursor && let Some((cursor, cursor_color)) = &text_render.cursor { + if cmd.show_cursor + && let Some((cursor, cursor_color)) = &text_render.cursor + { let cursor_rect = Rectangle { x: cursor.x + cmd.rect.x, y: -scroll + cursor.y + cmd.rect.y, @@ -447,7 +447,7 @@ impl CraftRenderer for VelloHybridRenderer { RenderCommand::PushLayer(cmd) => { let clip_path = match cmd { PushLayerCmd::BezPath(path) => path, - PushLayerCmd::Rect(rect) => &rect.to_kurbo().into_path(0.1) + PushLayerCmd::Rect(rect) => &rect.to_kurbo().into_path(0.1), }; scene.push_layer(Some(clip_path), None, None, None, None); diff --git a/crates/craft_renderer/src/vello_hybrid/tinyvg.rs b/crates/craft_renderer/src/vello_hybrid/tinyvg.rs index 341b7b43..f4ebd20a 100644 --- a/crates/craft_renderer/src/vello_hybrid/tinyvg.rs +++ b/crates/craft_renderer/src/vello_hybrid/tinyvg.rs @@ -11,9 +11,8 @@ use tinyvg_rs::commands::{DrawCommand, Path, PathCommand, Segment, Style}; use tinyvg_rs::common::Unit; use vello_hybrid::Scene; -use crate::Brush; -use crate::tinyvg_helpers; use crate::vello_hybrid::brush_to_paint; +use crate::{Brush, tinyvg_helpers}; fn stroke_path(scene: &mut Scene, bez_path: &BezPath, affine: &Affine, line_width: f64, brush: &Brush) { scene.set_stroke(Stroke::new(line_width)); diff --git a/crates/craft_retained/src/elements/container.rs b/crates/craft_retained/src/elements/container.rs index ac5e0967..ebf218c1 100644 --- a/crates/craft_retained/src/elements/container.rs +++ b/crates/craft_retained/src/elements/container.rs @@ -13,6 +13,7 @@ use crate::elements::traits::DeepClone; use crate::elements::{AsElement, Element, ElementInternals, resolve_clip_for_scrollable, scrollable}; use crate::events::{Event, EventKind}; use crate::layout::TaffyTree; +use crate::style::Overflow; use crate::text::text_context::TextContext; #[derive(Clone)] @@ -100,7 +101,12 @@ impl ElementInternals for ContainerInner { } fn apply_clip(&mut self, clip_bounds: Option) { - resolve_clip_for_scrollable(self, clip_bounds); + let overflow = self.style().get_overflow(); + if overflow[0] == Overflow::Scroll || overflow[1] == Overflow::Scroll { + resolve_clip_for_scrollable(self, clip_bounds); + } else { + self.element_data.layout.resolve_clip(clip_bounds); + } } fn push(&mut self, child: Rc>) { diff --git a/crates/craft_retained/src/elements/dropdown.rs b/crates/craft_retained/src/elements/dropdown.rs index 4aab5daf..ba938858 100644 --- a/crates/craft_retained/src/elements/dropdown.rs +++ b/crates/craft_retained/src/elements/dropdown.rs @@ -122,12 +122,14 @@ impl ElementInternals for DropdownInner { self.element_data.layout.has_new_layout = taffy_tree.has_new_layout(node); let dirty = self.element_data.layout.has_new_layout || transform != self.element_data.layout.get_transform() - || position != self.element_data.layout.position; + || position != self.element_data.layout.position + || clip_bounds != self.element_data.layout.parent_clip; if dirty { self.resolve_box(position, transform, layout, z_index); self.apply_borders(scale_factor); self.apply_clip(clip_bounds); + self.element_data.layout.parent_clip = clip_bounds; self.element_data.layout.scroll_state.mark_old(); } taffy_tree.mark_seen(node); diff --git a/crates/craft_retained/src/elements/internal_helpers.rs b/crates/craft_retained/src/elements/internal_helpers.rs index e1d349cf..54034286 100644 --- a/crates/craft_retained/src/elements/internal_helpers.rs +++ b/crates/craft_retained/src/elements/internal_helpers.rs @@ -46,7 +46,8 @@ pub fn apply_generic_container_layout( let dirty = has_new_layout || transform != element.element_data_mut().layout.get_transform() - || position != element.element_data_mut().layout.position; + || position != element.element_data_mut().layout.position + || clip_bounds != element.element_data().layout.parent_clip; element.element_data_mut().layout.has_new_layout = has_new_layout; if dirty { element.resolve_box(position, transform, layout, z_index); @@ -54,6 +55,7 @@ pub fn apply_generic_container_layout( // For scroll changes from taffy; element.element_data_mut().apply_scroll(layout); element.apply_clip(clip_bounds); + element.element_data_mut().layout.parent_clip = clip_bounds; element.element_data_mut().layout.scroll_state.mark_old(); } @@ -76,7 +78,7 @@ pub fn apply_generic_container_layout( transform * child_transform, text_context, scale_factor, - false, + element.element_data().layout.clip_bounds, ) } @@ -96,12 +98,14 @@ pub fn apply_generic_leaf_layout( let dirty = has_new_layout || transform != element.element_data_mut().layout.get_transform() - || position != element.element_data_mut().layout.position; + || position != element.element_data_mut().layout.position + || clip_bounds != element.element_data().layout.parent_clip; element.element_data_mut().layout.has_new_layout = has_new_layout; if dirty { element.resolve_box(position, transform, layout, z_index); element.apply_borders(scale_factor); element.apply_clip(clip_bounds); + element.element_data_mut().layout.parent_clip = clip_bounds; element.element_data_mut().layout.scroll_state.mark_old(); } diff --git a/crates/craft_retained/src/elements/slider/slider_element.rs b/crates/craft_retained/src/elements/slider/slider_element.rs index d06b1caa..410ffa1e 100644 --- a/crates/craft_retained/src/elements/slider/slider_element.rs +++ b/crates/craft_retained/src/elements/slider/slider_element.rs @@ -348,7 +348,8 @@ impl ElementInternals for SliderInner { let dirty = has_new_layout || transform != self.element_data.layout.get_transform() - || position != self.element_data.layout.position; + || position != self.element_data.layout.position + || clip_bounds != self.element_data.layout.parent_clip; self.element_data.layout.has_new_layout = has_new_layout; if dirty { @@ -356,6 +357,7 @@ impl ElementInternals for SliderInner { self.apply_borders(scale_factor); self.apply_clip(clip_bounds); + self.element_data.layout.parent_clip = clip_bounds; } } diff --git a/crates/craft_retained/src/elements/text.rs b/crates/craft_retained/src/elements/text.rs index 52fe90f3..441eacfd 100644 --- a/crates/craft_retained/src/elements/text.rs +++ b/crates/craft_retained/src/elements/text.rs @@ -7,8 +7,8 @@ use std::time; #[cfg(all(feature = "accesskit", not(target_arch = "wasm32")))] use accesskit::{Action, Role}; -use craft_renderer::text_renderer_data::TextData; use craft_renderer::RenderList; +use craft_renderer::text_renderer_data::TextData; use craft_primitives::geometry::{Affine, Point, Rectangle, Vec2}; use craft_primitives::{Color, ColorBrush}; @@ -163,12 +163,13 @@ impl ElementInternals for TextInner { let dirty = has_new_layout || transform != self.element_data.layout.get_transform() - || position != self.element_data.layout.position; + || position != self.element_data.layout.position + || clip_bounds != self.element_data.layout.parent_clip; self.element_data.layout.has_new_layout = has_new_layout; if dirty { self.resolve_box(position, transform, result, z_index); self.apply_clip(clip_bounds); - + self.element_data.layout.parent_clip = clip_bounds; self.apply_borders(scale_factor); } diff --git a/crates/craft_retained/src/elements/text_input/mod.rs b/crates/craft_retained/src/elements/text_input/mod.rs index f9e1a420..f55fe972 100644 --- a/crates/craft_retained/src/elements/text_input/mod.rs +++ b/crates/craft_retained/src/elements/text_input/mod.rs @@ -8,8 +8,8 @@ use std::rc::{Rc, Weak}; use craft_primitives::Color; use craft_primitives::geometry::{Affine, Point, Rectangle, TrblRectangle}; -use craft_renderer::text_renderer_data::{TextData, TextScroll}; use craft_renderer::RenderList; +use craft_renderer::text_renderer_data::{TextData, TextScroll}; use parley::BoundingBox; @@ -177,13 +177,15 @@ impl ElementInternals for TextInputInner { let dirty = has_new_layout || transform != self.element_data.layout.get_transform() - || position != self.element_data.layout.position; + || position != self.element_data.layout.position + || clip_bounds != self.element_data.layout.parent_clip; self.element_data.layout.has_new_layout = has_new_layout; if dirty { let result = taffy_tree.get_layout(node); self.resolve_box(position, transform, result, z_index); self.apply_clip(clip_bounds); + self.element_data.layout.parent_clip = clip_bounds; self.apply_borders(scale_factor); self.element_data.apply_scroll(result); diff --git a/crates/craft_retained/src/elements/traits/element_internals.rs b/crates/craft_retained/src/elements/traits/element_internals.rs index a6991e39..1649631c 100644 --- a/crates/craft_retained/src/elements/traits/element_internals.rs +++ b/crates/craft_retained/src/elements/traits/element_internals.rs @@ -56,7 +56,7 @@ pub trait ElementInternals: ElementData + Any + Drop { transform: Affine, text_context: &mut TextContext, scale_factor: f64, - _dirty: bool, + clip_bounds: Option, ) { for child in &self.element_data().children { child.borrow_mut().apply_layout( @@ -65,7 +65,7 @@ pub trait ElementInternals: ElementData + Any + Drop { z_index, transform, text_context, - self.element_data().layout.clip_bounds, + clip_bounds, scale_factor, ); } @@ -605,7 +605,7 @@ pub trait ElementInternals: ElementData + Any + Drop { None => false, } } else { - rect.contains(&point) + false } } diff --git a/crates/craft_retained/src/elements/window.rs b/crates/craft_retained/src/elements/window.rs index 42ffab8c..9bf4f818 100644 --- a/crates/craft_retained/src/elements/window.rs +++ b/crates/craft_retained/src/elements/window.rs @@ -47,6 +47,7 @@ use crate::events::internal::InternalMessage; use crate::events::pointer_capture::PointerCapture; use crate::events::{Event, EventKind}; use crate::layout::TaffyTree; +use crate::style::Overflow; use crate::text::text_context::TextContext; #[cfg(target_arch = "wasm32")] use crate::wasm_queue::WASM_QUEUE; @@ -214,7 +215,12 @@ impl ElementInternals for WindowInternal { } fn apply_clip(&mut self, clip_bounds: Option) { - resolve_clip_for_scrollable(self, clip_bounds); + let overflow = self.style().get_overflow(); + if overflow[0] == Overflow::Scroll || overflow[1] == Overflow::Scroll { + resolve_clip_for_scrollable(self, clip_bounds); + } else { + self.element_data.layout.resolve_clip(clip_bounds); + } } fn push(&mut self, child: Rc>) { @@ -678,7 +684,12 @@ impl WindowInternal { &mut layout_order, Affine::IDENTITY, text_context, - None, + Some(Rectangle::new( + 0.0, + 0.0, + self.window_size.width, + self.window_size.height, + )), sf, ); taffy_tree.apply_layout(root_node); diff --git a/crates/craft_retained/src/events/helpers.rs b/crates/craft_retained/src/events/helpers.rs index 7c2edbb0..0c247e82 100644 --- a/crates/craft_retained/src/events/helpers.rs +++ b/crates/craft_retained/src/events/helpers.rs @@ -4,8 +4,7 @@ use std::rc::Rc; use craft_primitives::geometry::Point; -use craft_renderer::RenderList; -use craft_renderer::TargetItem; +use craft_renderer::{RenderList, TargetItem}; use crate::app::ELEMENTS; use crate::elements::ElementInternals; diff --git a/crates/craft_retained/src/layout/layout.rs b/crates/craft_retained/src/layout/layout.rs index 428016c5..8efa0099 100644 --- a/crates/craft_retained/src/layout/layout.rs +++ b/crates/craft_retained/src/layout/layout.rs @@ -30,6 +30,7 @@ pub struct Layout { pub layout_order: u32, pub clip_bounds: Option, + pub parent_clip: Option, //cache_border_spec: Option<(CssRoundedRect, f64)>, // f64 for scale factor cache_border_spec: Option, @@ -419,13 +420,13 @@ impl Layout { } pub fn resolve_clip_for_scrollable(&mut self, clip_bounds: Option) { + if clip_bounds.is_none() { + self.clip_bounds = None; + return; + } if self.is_scrollable_layout() { let scroll_clip_bounds = self.computed_box_transformed.padding_rectangle(); - if let Some(clip_bounds) = clip_bounds { - self.clip_bounds = scroll_clip_bounds.intersection(&clip_bounds); - } else { - self.clip_bounds = Some(scroll_clip_bounds); - } + self.clip_bounds = scroll_clip_bounds.intersection(&clip_bounds.unwrap()); } else { self.clip_bounds = clip_bounds; } diff --git a/website/src/index.rs b/website/src/index.rs index bc74c7d3..37a5e30a 100644 --- a/website/src/index.rs +++ b/website/src/index.rs @@ -45,6 +45,7 @@ fn hero_intro(navigate_fn: NavigateFn) -> Container { ); let github_button = Text::new("GitHub") + .selectable(false) .display(Display::Flex) .align_items(Some(AlignItems::Center)) .justify_content(Some(JustifyContent::Center)) @@ -62,7 +63,7 @@ fn hero_intro(navigate_fn: NavigateFn) -> Container { .color(palette::css::WHITE); let craft_button = Text::new("Learn Craft") - .id("xxx") + .selectable(false) .display(Display::Flex) .align_items(Some(AlignItems::Center)) .justify_content(Some(JustifyContent::Center)) From cb5e6084cecc477dc3d1e5fba9b26586f12adbe7 Mon Sep 17 00:00:00 2001 From: "Austin M. Reppert" Date: Sun, 29 Mar 2026 00:47:06 -0400 Subject: [PATCH 123/143] Maybe fix hit test with scrollable --- crates/craft_retained/src/elements/text_input/mod.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/craft_retained/src/elements/text_input/mod.rs b/crates/craft_retained/src/elements/text_input/mod.rs index f55fe972..b366f672 100644 --- a/crates/craft_retained/src/elements/text_input/mod.rs +++ b/crates/craft_retained/src/elements/text_input/mod.rs @@ -27,7 +27,7 @@ use crate::elements::{AsElement, Element, ElementInternals, resolve_clip_for_scr use crate::events::{Event, EventKind}; use crate::layout::TaffyTree; use crate::layout::layout_context::{LayoutContext, TaffyTextInputContext}; -use crate::style::{Display, Style, Unit}; +use crate::style::{Display, Overflow, Style, Unit}; use crate::text::RangedStyles; use crate::text::text_context::TextContext; use crate::text::text_render_data::TextRender; @@ -391,7 +391,12 @@ impl ElementInternals for TextInputInner { } fn apply_clip(&mut self, clip_bounds: Option) { - resolve_clip_for_scrollable(self, clip_bounds); + let overflow = self.style().get_overflow(); + if overflow[0] == Overflow::Scroll || overflow[1] == Overflow::Scroll { + resolve_clip_for_scrollable(self, clip_bounds); + } else { + self.element_data.layout.resolve_clip(clip_bounds); + } } fn get_default_style() -> Box