diff --git a/frontends/rioterm/src/context/grid.rs b/frontends/rioterm/src/context/grid.rs index d7cbd49bae..0099f34a31 100644 --- a/frontends/rioterm/src/context/grid.rs +++ b/frontends/rioterm/src/context/grid.rs @@ -3,7 +3,7 @@ use crate::mouse::Mouse; use rio_backend::crosswords::grid::Dimensions; use rio_backend::event::EventListener; use rio_backend::sugarloaf::{ - layout::SugarDimensions, Object, Quad, RichText, Sugarloaf, + layout::SugarDimensions, Object, Rect, RichText, Sugarloaf, }; use std::collections::HashMap; @@ -19,55 +19,74 @@ fn compute( line_height: f32, margin: Delta, ) -> (usize, usize) { + println!("=== COMPUTE CALLED ==="); + println!(" width: {}, height: {}", width, height); + println!(" dimensions: scale={}, width={}, height={}", dimensions.scale, dimensions.width, dimensions.height); + println!(" line_height: {}", line_height); + println!(" margin: x={}, top_y={}, bottom_y={}", margin.x, margin.top_y, margin.bottom_y); + // Ensure we have positive dimensions if width <= 0.0 || height <= 0.0 || dimensions.scale <= 0.0 || line_height <= 0.0 { + println!(" -> Returning MIN (invalid inputs)"); return (MIN_COLS, MIN_LINES); } - let margin_x = (margin.x * dimensions.scale).round(); - let margin_spaces = margin.top_y + margin.bottom_y; + // Convert margin to scaled/physical pixels + let margin_x_physical = margin.x * dimensions.scale; + let margin_top_physical = margin.top_y * dimensions.scale; + let margin_bottom_physical = margin.bottom_y * dimensions.scale; - // Calculate available space for content - let available_width = (width / dimensions.scale) - margin_x; - let available_height = (height / dimensions.scale) - margin_spaces; + // Calculate available space in physical pixels + let available_width_physical = width - margin_x_physical; + let available_height_physical = height - margin_top_physical - margin_bottom_physical; + + println!(" margin in physical pixels: x={}, top={}, bottom={}", + margin_x_physical, margin_top_physical, margin_bottom_physical); + println!(" available (physical): width={}, height={}", + available_width_physical, available_height_physical); // Ensure we have positive available space - if available_width <= 0.0 || available_height <= 0.0 { + if available_width_physical <= 0.0 || available_height_physical <= 0.0 { + println!(" -> Returning MIN (no available space)"); return (MIN_COLS, MIN_LINES); } - // Calculate columns - let char_width = dimensions.width / dimensions.scale; - if char_width <= 0.0 { + // Character dimensions from the brush are already in physical pixels + // (measured at the actual scaled font size) + // So we use them directly without scaling + let char_width_physical = dimensions.width; + let char_height_physical = dimensions.height * line_height; + + println!(" char (physical, from brush): width={}, height={}", char_width_physical, char_height_physical); + + if char_width_physical <= 0.0 { + eprintln!( + "WARNING: char_width_physical is {}, using fallback. dimensions: {:?}", + char_width_physical, dimensions + ); + println!(" -> Returning MIN (zero char_width)"); return (MIN_COLS, MIN_LINES); } - let visible_columns = - std::cmp::max((available_width / char_width) as usize, MIN_COLS); - - // Calculate lines - let char_height = (dimensions.height / dimensions.scale) * line_height; - if char_height <= 0.0 { + + if char_height_physical <= 0.0 { + println!(" -> Returning (cols, MIN) (zero char_height)"); + let visible_columns = std::cmp::max((available_width_physical / char_width_physical) as usize, MIN_COLS); return (visible_columns, MIN_LINES); } - let lines = (available_height / char_height) - 1.0; + + let visible_columns = std::cmp::max((available_width_physical / char_width_physical) as usize, MIN_COLS); + let lines = (available_height_physical / char_height_physical) - 1.0; let visible_lines = std::cmp::max(lines.round() as usize, MIN_LINES); + println!(" -> Result: {} cols x {} rows", visible_columns, visible_lines); + println!("=== END COMPUTE ===\n"); + (visible_columns, visible_lines) } #[inline] fn create_border(color: [f32; 4], position: [f32; 2], size: [f32; 2]) -> Object { - Object::Quad(Quad { - color, - position, - shadow_blur_radius: 0.0, - shadow_offset: [0.0, 0.0], - shadow_color: [0.0, 0.0, 0.0, 0.0], - border_color: [0.0, 0.0, 0.0, 0.0], - border_width: 0.0, - border_radius: [0.0, 0.0, 0.0, 0.0], - size, - }) + Object::Rect(Rect::new(position[0], position[1], size[0], size[1], color)) } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] @@ -100,8 +119,13 @@ impl ContextGridItem { pub fn new(context: Context) -> Self { let rich_text_object = Object::RichText(RichText { id: context.rich_text_id, - position: [0.0, 0.0], lines: None, + render_data: rio_backend::sugarloaf::RichTextRenderData { + position: [0.0, 0.0], + should_repaint: false, + should_remove: false, + hidden: false, + }, }); Self { @@ -129,7 +153,7 @@ impl ContextGridItem { #[inline] pub fn position(&self) -> [f32; 2] { if let Object::RichText(ref rich_text) = self.rich_text_object { - rich_text.position + rich_text.render_data.position } else { [0.0, 0.0] } @@ -138,7 +162,7 @@ impl ContextGridItem { /// Update the position in the rich text object fn set_position(&mut self, position: [f32; 2]) { if let Object::RichText(ref mut rich_text) = self.rich_text_object { - rich_text.position = position; + rich_text.render_data.position = position; } } } @@ -427,8 +451,10 @@ impl ContextGrid { for obj in objects { if let Object::RichText(rich_text_obj) = obj { if rich_text_obj.id == rich_text_id { - margin.x = rich_text_obj.position[0] + self.scaled_padding; - margin.top_y = rich_text_obj.position[1] + self.scaled_padding; + margin.x = + rich_text_obj.render_data.position[0] + self.scaled_padding; + margin.top_y = + rich_text_obj.render_data.position[1] + self.scaled_padding; break; } } @@ -459,10 +485,10 @@ impl ContextGrid { if let Object::RichText(rich_text_obj) = obj { if let Some(key) = self.find_by_rich_text_id(rich_text_obj.id) { if let Some(item) = self.inner.get(&key) { - let scaled_position_x = - rich_text_obj.position[0] * (self.scaled_padding / PADDING); - let scaled_position_y = - rich_text_obj.position[1] * (self.scaled_padding / PADDING); + let scaled_position_x = rich_text_obj.render_data.position[0] + * (self.scaled_padding / PADDING); + let scaled_position_y = rich_text_obj.render_data.position[1] + * (self.scaled_padding / PADDING); if mouse.x >= scaled_position_x as usize && mouse.y >= scaled_position_y as usize && mouse.x @@ -500,13 +526,27 @@ impl ContextGrid { pub fn grid_dimension(&self) -> ContextDimension { if let Some(current_item) = self.inner.get(&self.current) { let current_context_dimension = current_item.val.dimension; - ContextDimension::build( + println!("\n>>> grid_dimension called for current context"); + println!(" Context dimension: {}x{} (scale={})", + current_context_dimension.width, + current_context_dimension.height, + current_context_dimension.dimension.scale + ); + println!(" Grid size: {}x{}", self.width, self.height); + println!(" Margin: x={}, top_y={}, bottom_y={}", + self.margin.x, self.margin.top_y, self.margin.bottom_y); + + let result = ContextDimension::build( self.width, self.height, current_context_dimension.dimension, current_context_dimension.line_height, self.margin, - ) + ); + + println!(" Resulting dimension: {} cols x {} rows", result.columns, result.lines); + println!("<<< grid_dimension done\n"); + result } else { tracing::error!("Current key {:?} not found in grid", self.current); ContextDimension::default() @@ -587,9 +627,43 @@ impl ContextGrid { } } - pub fn update_dimensions(&mut self, sugarloaf: &Sugarloaf) { + pub fn update_dimensions(&mut self, sugarloaf: &mut Sugarloaf) { + println!("\n╔═══════════════════════════════════════════════════════════════╗"); + println!("║ Grid::update_dimensions called ║"); + println!("╚═══════════════════════════════════════════════════════════════╝"); + + // First, check if any context has invalid dimensions and force recomputation + for context in self.inner.values() { + let layout = sugarloaf.rich_text_layout(&context.val.rich_text_id); + println!(" Checking rich_text_id {}: dimensions {}x{} (scale={})", + context.val.rich_text_id, + layout.dimensions.width, + layout.dimensions.height, + layout.dimensions.scale + ); + + // If dimensions are zero or scale is wrong (equals font_size), force recomputation + if layout.dimensions.width == 0.0 + || layout.dimensions.height == 0.0 + || layout.dimensions.scale == layout.font_size { + println!(" -> Detected invalid dimensions, forcing recomputation"); + sugarloaf.force_update_dimensions(&context.val.rich_text_id); + } else { + println!(" -> Dimensions look valid, calling get_rich_text_dimensions"); + sugarloaf.get_rich_text_dimensions(&context.val.rich_text_id); + } + } + + println!("\n Getting final layouts for all contexts:"); + // Now get the updated layouts for context in self.inner.values_mut() { let layout = sugarloaf.rich_text_layout(&context.val.rich_text_id); + println!(" rich_text_id {}: dimensions {}x{} (scale={})", + context.val.rich_text_id, + layout.dimensions.width, + layout.dimensions.height, + layout.dimensions.scale + ); context.val.dimension.update_dimensions(layout.dimensions); } // Update scaled_padding from the first context (they should all have the same scale) @@ -597,9 +671,11 @@ impl ContextGrid { if let Some(first_context) = self.inner.get(&root) { self.scaled_padding = PADDING * first_context.val.dimension.dimension.scale; + println!(" Updated scaled_padding to {}", self.scaled_padding); } } self.calculate_positions(); + println!("╚═══════════════════════════════════════════════════════════════╝\n"); } pub fn resize(&mut self, new_width: f32, new_height: f32) { @@ -703,10 +779,15 @@ impl ContextGrid { /// Calculate and update positions for all grid items pub fn calculate_positions(&mut self) { + println!("DEBUG: calculate_positions called"); if self.inner.is_empty() { return; } if let Some(root) = self.root { + println!( + "DEBUG: calculate_positions with root {} and margin [{}, {}]", + root, self.margin.x, self.margin.top_y + ); self.calculate_positions_recursive(root, self.margin); } } @@ -774,6 +855,10 @@ impl ContextGrid { fn calculate_positions_recursive(&mut self, key: usize, margin: Delta) { if let Some(item) = self.inner.get_mut(&key) { // Set position for current item in the rich text object + println!( + "DEBUG: calculate_positions_recursive for key {} with margin [{}, {}]", + key, margin.x, margin.top_y + ); item.set_position([margin.x, margin.top_y]); // Calculate margin for down item @@ -7078,3 +7163,7 @@ pub mod test { ); } } + +#[cfg(test)] +#[path = "grid_compute_tests.rs"] +mod grid_compute_tests; diff --git a/frontends/rioterm/src/context/mod.rs b/frontends/rioterm/src/context/mod.rs index 083f4c5c24..f5b972a1c2 100644 --- a/frontends/rioterm/src/context/mod.rs +++ b/frontends/rioterm/src/context/mod.rs @@ -668,7 +668,6 @@ impl ContextManager { self.current_route = self.current().route_id; } - #[inline] pub fn extend_with_grid_objects(&self, target: &mut Vec) { self.contexts[self.current_index].extend_with_objects(target); } diff --git a/frontends/rioterm/src/renderer/mod.rs b/frontends/rioterm/src/renderer/mod.rs index 760821cc58..3c0811c4f2 100644 --- a/frontends/rioterm/src/renderer/mod.rs +++ b/frontends/rioterm/src/renderer/mod.rs @@ -25,7 +25,7 @@ use rio_backend::config::colors::{ use rio_backend::config::Config; use rio_backend::event::EventProxy; use rio_backend::sugarloaf::{ - drawable_character, Content, FragmentStyle, FragmentStyleDecoration, Graphic, Quad, + drawable_character, Content, FragmentStyle, FragmentStyleDecoration, Graphic, Stretch, Style, SugarCursor, Sugarloaf, UnderlineInfo, UnderlineShape, Weight, }; use std::collections::{BTreeSet, HashMap}; @@ -969,6 +969,7 @@ impl Renderer { } let rich_text_id = context.rich_text_id; + println!("{:?}", rich_text_id); let mut is_cursor_visible = context.renderable_content.cursor.state.is_visible(); @@ -1086,52 +1087,16 @@ impl Renderer { let window_size = sugarloaf.window_size(); let scale_factor = sugarloaf.scale_factor(); - let mut objects = Vec::with_capacity(15); - self.navigation.build_objects( - sugarloaf, - (window_size.width, window_size.height, scale_factor), - &self.named_colors, - context_manager, - self.search.active_search.is_some(), - &mut objects, - ); - - if has_search { - if let Some(rich_text_id) = self.search.rich_text_id { - search::draw_search_bar( - &mut objects, - rich_text_id, - &self.named_colors, - (window_size.width, window_size.height, scale_factor), - ); - } - - self.search.active_search = None; - self.search.rich_text_id = None; - } - - // let _duration = start.elapsed(); - context_manager.extend_with_grid_objects(&mut objects); - // let _duration = start.elapsed(); - - // Update visual bell state and set overlay if needed - // let visual_bell_active = self.update_visual_bell(); - - // Set visual bell overlay that renders on top of everything - // let bell_overlay = if visual_bell_active { - // Some(Quad { - // position: [0.0, 0.0], - // size: [window_size.width, window_size.height], - // color: self.named_colors.foreground, - // ..Quad::default() - // }) - // } else { - // None - // }; - // sugarloaf.set_visual_bell_overlay(bell_overlay); - - sugarloaf.set_objects(objects); + // Render navigation rectangles directly + // self.navigation.render_directly( + // sugarloaf, + // (window_size.width, window_size.height, scale_factor), + // &self.named_colors, + // context_manager, + // ); + // Search bar disabled for now - TODO: Implement direct rendering + // if has_search { ... } sugarloaf.render(); // let _duration = start.elapsed(); diff --git a/frontends/rioterm/src/renderer/navigation.rs b/frontends/rioterm/src/renderer/navigation.rs index d8bd2fe82e..7db8365c81 100644 --- a/frontends/rioterm/src/renderer/navigation.rs +++ b/frontends/rioterm/src/renderer/navigation.rs @@ -2,7 +2,7 @@ use crate::constants::*; use crate::context::title::ContextTitle; use rio_backend::config::colors::Colors; use rio_backend::config::navigation::{Navigation, NavigationMode}; -use rio_backend::sugarloaf::{FragmentStyle, Object, Quad, RichText, Sugarloaf}; +use rio_backend::sugarloaf::{FragmentStyle, Object, Rect, RichText, Sugarloaf}; use rustc_hash::FxHashMap; use std::collections::HashMap; @@ -91,6 +91,48 @@ impl ScreenNavigation { } } + /// Direct rendering version (replaces build_objects) + #[inline] + #[allow(clippy::too_many_arguments)] + pub fn render_directly( + &mut self, + sugarloaf: &mut Sugarloaf, + dimensions: (f32, f32, f32), + colors: &Colors, + context_manager: &crate::context::ContextManager, + ) { + let current = context_manager.current_index(); + let len = context_manager.len(); + let titles = &context_manager.titles.titles; + + match self.navigation.mode { + #[cfg(target_os = "macos")] + NavigationMode::NativeTab => {} + NavigationMode::BottomTab => { + let (_, window_height, scale_factor) = dimensions; + let position_y = window_height - (PADDING_Y_BOTTOM_TABS * scale_factor); + + self.bottom_tab_directly( + sugarloaf, + titles, + colors, + len, + current, + position_y, + self.navigation.hide_if_single, + dimensions, + ); + } + NavigationMode::TopTab => { + // TopTab mode - simplified placeholder + } + NavigationMode::Bookmark => { + // Bookmark mode - simplified placeholder + } + NavigationMode::Plain => {} + } + } + #[inline] #[allow(clippy::too_many_arguments)] pub fn bookmark( @@ -133,14 +175,9 @@ impl ScreenNavigation { } } - let renderable = Quad { - position: [initial_position, 0.0], - color, - size: [15.0, size], - ..Quad::default() - }; + let renderable = Rect::new(initial_position, 0.0, 15.0, size, color); initial_position -= position_modifier; - objects.push(Object::Quad(renderable)); + objects.push(Object::Rect(renderable)); } } @@ -166,14 +203,15 @@ impl ScreenNavigation { let mut initial_position_x = 0.; - let renderable = Quad { - position: [initial_position_x, position_y], - color: colors.bar, - size: [width, PADDING_Y_BOTTOM_TABS], - ..Quad::default() - }; + let renderable = Rect::new( + initial_position_x, + position_y, + width, + PADDING_Y_BOTTOM_TABS, + colors.bar, + ); - objects.push(Object::Quad(renderable)); + objects.push(Object::Rect(renderable)); let iter = 0..len; let mut tabs = Vec::from_iter(iter); @@ -217,12 +255,13 @@ impl ScreenNavigation { name = name[0..14].to_string(); } - objects.push(Object::Quad(Quad { - position: [initial_position_x, position_y], - color: background_color, - size: [125., PADDING_Y_BOTTOM_TABS], - ..Quad::default() - })); + objects.push(Object::Rect(Rect::new( + initial_position_x, + position_y, + 125., + PADDING_Y_BOTTOM_TABS, + background_color, + ))); if is_current { // TopBar case should render on bottom @@ -232,12 +271,13 @@ impl ScreenNavigation { position_y }; - objects.push(Object::Quad(Quad { - position: [initial_position_x, position], - color: colors.tabs_active_highlight, - size: [125., PADDING_Y_BOTTOM_TABS / 10.], - ..Quad::default() - })); + objects.push(Object::Rect(Rect::new( + initial_position_x, + position, + 125., + PADDING_Y_BOTTOM_TABS / 10., + colors.tabs_active_highlight, + ))); } let text = if is_current { @@ -265,13 +305,93 @@ impl ScreenNavigation { objects.push(Object::RichText(RichText { id: tab, - position: [initial_position_x + 4., position_y], lines: None, + render_data: rio_backend::sugarloaf::RichTextRenderData { + position: [initial_position_x + 4., position_y], + should_repaint: false, + should_remove: false, + hidden: false, + }, })); initial_position_x += name_modifier + 40.; } } + + /// Direct rendering for bottom tabs + #[inline] + #[allow(clippy::too_many_arguments)] + fn bottom_tab_directly( + &mut self, + sugarloaf: &mut Sugarloaf, + titles: &FxHashMap, + colors: &Colors, + len: usize, + current: usize, + position_y: f32, + hide_if_single: bool, + dimensions: (f32, f32, f32), + ) { + if hide_if_single && len <= 1 { + return; + } + + let (width, _, _scale) = dimensions; + let initial_position_x = 0.0; + + // Render main bar + sugarloaf.add_rect( + initial_position_x, + position_y, + width, + PADDING_Y_BOTTOM_TABS, + colors.bar, + ); + + // Render tab indicators + let mut current_x = initial_position_x; + let tab_width = 125.0; + + for i in 0..len { + if i == current { + // Active tab highlight + sugarloaf.add_rect( + current_x, + position_y, + tab_width, + PADDING_Y_BOTTOM_TABS / 10.0, + colors.tabs_active_highlight, + ); + } + current_x += tab_width; + } + } + + /// Direct rendering for collapsed tabs (simplified) + fn collapsed_tab_directly( + &mut self, + sugarloaf: &mut Sugarloaf, + colors: &Colors, + dimensions: (f32, f32, f32), + ) { + let (width, _, scale) = dimensions; + let size = 15.0; + let x = (width / scale) - PADDING_X_COLLAPSED_TABS; + let y = 0.0; + + sugarloaf.add_rect(x, y, size, size, colors.tabs); + } + + /// Direct rendering for breadcrumb (simplified line) + fn breadcrumb_directly( + &mut self, + sugarloaf: &mut Sugarloaf, + colors: &Colors, + dimensions: (f32, f32, f32), + ) { + let (width, _, _scale) = dimensions; + sugarloaf.add_rect(0.0, 30.0, width, 1.0, colors.tabs); + } } #[inline] diff --git a/frontends/rioterm/src/renderer/search.rs b/frontends/rioterm/src/renderer/search.rs index 6d071512ef..f420a76e76 100644 --- a/frontends/rioterm/src/renderer/search.rs +++ b/frontends/rioterm/src/renderer/search.rs @@ -1,6 +1,6 @@ use crate::constants::*; use rio_backend::config::colors::Colors; -use rio_backend::sugarloaf::{Object, Quad, RichText}; +use rio_backend::sugarloaf::{Object, Rect, RichText}; #[inline] pub fn draw_search_bar( @@ -12,16 +12,22 @@ pub fn draw_search_bar( let (width, height, scale) = dimensions; let position_y = (height / scale) - PADDING_Y_BOTTOM_TABS; - objects.push(Object::Quad(Quad { - position: [0.0, position_y], - color: colors.bar, - size: [width, PADDING_Y_BOTTOM_TABS], - ..Quad::default() - })); + objects.push(Object::Rect(Rect::new( + 0.0, + position_y, + width, + PADDING_Y_BOTTOM_TABS, + colors.bar, + ))); objects.push(Object::RichText(RichText { id: rich_text_id, - position: [4., position_y], lines: None, + render_data: rio_backend::sugarloaf::RichTextRenderData { + position: [4., position_y], + should_repaint: false, + should_remove: false, + hidden: false, + }, })); } diff --git a/frontends/rioterm/src/router/routes/assistant.rs b/frontends/rioterm/src/router/routes/assistant.rs index 61ed486458..16868fb642 100644 --- a/frontends/rioterm/src/router/routes/assistant.rs +++ b/frontends/rioterm/src/router/routes/assistant.rs @@ -1,6 +1,6 @@ use crate::context::grid::ContextDimension; use rio_backend::error::{RioError, RioErrorLevel}; -use rio_backend::sugarloaf::{FragmentStyle, Object, Quad, RichText, Sugarloaf}; +use rio_backend::sugarloaf::{FragmentStyle, Sugarloaf}; pub struct Assistant { pub inner: Option, @@ -46,35 +46,29 @@ pub fn screen( let layout = sugarloaf.window_size(); - let mut objects = Vec::with_capacity(7); - - objects.push(Object::Quad(Quad { - position: [0., 0.0], - color: black, - size: [ - layout.width / context_dimension.dimension.scale, - layout.height, - ], - ..Quad::default() - })); - objects.push(Object::Quad(Quad { - position: [0., 30.0], - color: blue, - size: [15., layout.height], - ..Quad::default() - })); - objects.push(Object::Quad(Quad { - position: [15., context_dimension.margin.top_y + 60.], - color: yellow, - size: [15., layout.height], - ..Quad::default() - })); - objects.push(Object::Quad(Quad { - position: [30., context_dimension.margin.top_y + 120.], - color: red, - size: [15., layout.height], - ..Quad::default() - })); + // Render rectangles directly + sugarloaf.add_rect( + 0.0, + 0.0, + layout.width / context_dimension.dimension.scale, + layout.height, + black, + ); + sugarloaf.add_rect(0.0, 30.0, 15.0, layout.height, blue); + sugarloaf.add_rect( + 15.0, + context_dimension.margin.top_y + 60.0, + 15.0, + layout.height, + yellow, + ); + sugarloaf.add_rect( + 30.0, + context_dimension.margin.top_y + 120.0, + 15.0, + layout.height, + red, + ); let heading = sugarloaf.create_temp_rich_text(); let paragraph_action = sugarloaf.create_temp_rich_text(); @@ -111,25 +105,16 @@ pub fn screen( } paragraph_line.build(); - - objects.push(Object::RichText(RichText { - id: paragraph, - position: [70., context_dimension.margin.top_y + 140.], - lines: None, - })); } - objects.push(Object::RichText(RichText { - id: heading, - position: [70., context_dimension.margin.top_y + 30.], - lines: None, - })); - - objects.push(Object::RichText(RichText { - id: paragraph_action, - position: [70., context_dimension.margin.top_y + 70.], - lines: None, - })); - - sugarloaf.set_objects(objects); + // Show rich texts at specific positions + sugarloaf.show_rich_text(heading, 70.0, context_dimension.margin.top_y + 30.0); + sugarloaf.show_rich_text( + paragraph_action, + 70.0, + context_dimension.margin.top_y + 70.0, + ); + if assistant.inner.is_some() { + sugarloaf.show_rich_text(paragraph, 70.0, context_dimension.margin.top_y + 140.0); + } } diff --git a/frontends/rioterm/src/router/routes/dialog.rs b/frontends/rioterm/src/router/routes/dialog.rs index bdfe254061..7582fcd8ed 100644 --- a/frontends/rioterm/src/router/routes/dialog.rs +++ b/frontends/rioterm/src/router/routes/dialog.rs @@ -1,5 +1,5 @@ use crate::context::grid::ContextDimension; -use rio_backend::sugarloaf::{FragmentStyle, Object, Quad, RichText, Sugarloaf}; +use rio_backend::sugarloaf::{FragmentStyle, Sugarloaf}; #[inline] pub fn screen( @@ -9,99 +9,50 @@ pub fn screen( confirm_content: &str, quit_content: &str, ) { - let blue = [0.1764706, 0.6039216, 1.0, 1.0]; - let yellow = [0.9882353, 0.7294118, 0.15686275, 1.0]; - let red = [1.0, 0.07058824, 0.38039216, 1.0]; - let black = [0.0, 0.0, 0.0, 1.0]; - let layout = sugarloaf.window_size(); - let mut objects = Vec::with_capacity(7); - - objects.push(Object::Quad(Quad { - position: [0., 0.0], - color: black, - size: [layout.width, layout.height], - ..Quad::default() - })); - objects.push(Object::Quad(Quad { - position: [0., 30.0], - color: blue, - size: [30., layout.height], - ..Quad::default() - })); - objects.push(Object::Quad(Quad { - position: [15., context_dimension.margin.top_y + 60.], - color: yellow, - size: [30., layout.height], - ..Quad::default() - })); - objects.push(Object::Quad(Quad { - position: [30., context_dimension.margin.top_y + 120.], - color: red, - size: [30., layout.height], - ..Quad::default() - })); + // Render rectangles directly + sugarloaf.add_rect( + 0.0, + 0.0, + layout.width / context_dimension.dimension.scale, + layout.height, + [0.0, 0.0, 0.0, 0.5], + ); + sugarloaf.add_rect(128.0, 256.0, 350.0, 150.0, [0.0, 0.0, 0.0, 1.0]); + sugarloaf.add_rect(128.0, 320.0, 106.0, 36.0, [0.133, 0.141, 0.176, 1.0]); + sugarloaf.add_rect(240.0, 320.0, 106.0, 36.0, [0.133, 0.141, 0.176, 1.0]); let heading = sugarloaf.create_temp_rich_text(); let confirm = sugarloaf.create_temp_rich_text(); let quit = sugarloaf.create_temp_rich_text(); - sugarloaf.set_rich_text_font_size(&heading, 28.0); - sugarloaf.set_rich_text_font_size(&confirm, 18.0); - sugarloaf.set_rich_text_font_size(&quit, 18.0); + sugarloaf.set_rich_text_font_size(&heading, 32.0); + sugarloaf.set_rich_text_font_size(&confirm, 20.0); + sugarloaf.set_rich_text_font_size(&quit, 20.0); let content = sugarloaf.content(); - let heading_line = content.sel(heading).clear(); - for line in heading_content.to_string().lines() { - heading_line.add_text(line, FragmentStyle::default()); - } - heading_line.build(); - - objects.push(Object::RichText(RichText { - id: heading, - position: [70., context_dimension.margin.top_y + 30.], - lines: None, - })); - - let confirm_line = content.sel(confirm); - confirm_line + content + .sel(heading) .clear() - .add_text( - &format!(" {confirm_content} "), - FragmentStyle { - color: [0., 0., 0., 1.], - background_color: Some(yellow), - ..FragmentStyle::default() - }, - ) + .add_text(heading_content, FragmentStyle::default()) .build(); - objects.push(Object::RichText(RichText { - id: confirm, - position: [70., context_dimension.margin.top_y + 100.], - lines: None, - })); - - let quit_line = content.sel(quit); - quit_line + content + .sel(confirm) .clear() - .add_text( - &format!(" {quit_content} "), - FragmentStyle { - color: [0., 0., 0., 1.], - background_color: Some(red), - ..FragmentStyle::default() - }, - ) + .add_text(confirm_content, FragmentStyle::default()) .build(); - objects.push(Object::RichText(RichText { - id: quit, - position: [70., context_dimension.margin.top_y + 140.], - lines: None, - })); + content + .sel(quit) + .clear() + .add_text(quit_content, FragmentStyle::default()) + .build(); - sugarloaf.set_objects(objects); + // Show rich texts at specific positions + sugarloaf.show_rich_text(heading, 150.0, 270.0); + sugarloaf.show_rich_text(confirm, 150.0, 330.0); + sugarloaf.show_rich_text(quit, 268.0, 330.0); } diff --git a/frontends/rioterm/src/router/routes/welcome.rs b/frontends/rioterm/src/router/routes/welcome.rs index 3115f472ee..c5a599a0b4 100644 --- a/frontends/rioterm/src/router/routes/welcome.rs +++ b/frontends/rioterm/src/router/routes/welcome.rs @@ -1,44 +1,33 @@ use crate::context::grid::ContextDimension; -use rio_backend::sugarloaf::{FragmentStyle, Object, Quad, RichText, Sugarloaf}; +use rio_backend::sugarloaf::{FragmentStyle, Sugarloaf}; #[inline] pub fn screen(sugarloaf: &mut Sugarloaf, context_dimension: &ContextDimension) { - let blue = [0.1764706, 0.6039216, 1.0, 1.0]; - let yellow = [0.9882353, 0.7294118, 0.15686275, 1.0]; - let red = [1.0, 0.07058824, 0.38039216, 1.0]; - let black = [0.0, 0.0, 0.0, 1.0]; - let layout = sugarloaf.window_size(); - let mut objects = Vec::with_capacity(7); - - objects.push(Object::Quad(Quad { - position: [0., 0.0], - color: black, - size: [ - layout.width / context_dimension.dimension.scale, - layout.height, - ], - ..Quad::default() - })); - objects.push(Object::Quad(Quad { - position: [0., 30.0], - color: blue, - size: [15., layout.height], - ..Quad::default() - })); - objects.push(Object::Quad(Quad { - position: [15., context_dimension.margin.top_y + 60.], - color: yellow, - size: [15., layout.height], - ..Quad::default() - })); - objects.push(Object::Quad(Quad { - position: [30., context_dimension.margin.top_y + 120.], - color: red, - size: [15., layout.height], - ..Quad::default() - })); + // Render rectangles directly + sugarloaf.add_rect( + 0.0, + 0.0, + layout.width / context_dimension.dimension.scale, + layout.height, + [0.0, 0.0, 0.0, 1.0], + ); + sugarloaf.add_rect(0.0, 30.0, 15.0, layout.height, [0.0, 0.0, 1.0, 1.0]); + sugarloaf.add_rect( + 15.0, + context_dimension.margin.top_y + 60.0, + 15.0, + layout.height, + [1.0, 1.0, 0.0, 1.0], + ); + sugarloaf.add_rect( + 30.0, + context_dimension.margin.top_y + 120.0, + 15.0, + layout.height, + [1.0, 0.0, 0.0, 1.0], + ); let heading = sugarloaf.create_temp_rich_text(); let paragraph_action = sugarloaf.create_temp_rich_text(); @@ -46,87 +35,99 @@ pub fn screen(sugarloaf: &mut Sugarloaf, context_dimension: &ContextDimension) { sugarloaf.set_rich_text_font_size(&heading, 28.0); sugarloaf.set_rich_text_font_size(¶graph_action, 18.0); - sugarloaf.set_rich_text_font_size(¶graph, 16.0); + sugarloaf.set_rich_text_font_size(¶graph, 14.0); let content = sugarloaf.content(); let heading_line = content.sel(heading); heading_line .clear() - .add_text("Welcome to Rio Terminal", FragmentStyle::default()) + .add_text( + "Welcome to Rio", + FragmentStyle { + color: [0.9019608, 0.494118, 0.13333334, 1.0], + ..FragmentStyle::default() + }, + ) + .add_text( + " terminal", + FragmentStyle { + color: [1.0, 1.0, 1.0, 1.0], + ..FragmentStyle::default() + }, + ) .build(); let paragraph_action_line = content.sel(paragraph_action); paragraph_action_line .clear() .add_text( - "> press enter to continue", + "> Hint: Use ", + FragmentStyle { + color: [0.7019608, 0.7019608, 0.7019608, 1.0], + ..FragmentStyle::default() + }, + ) + .add_text( + "config edit", FragmentStyle { - color: yellow, + color: [0.7019608, 0.7019608, 0.7019608, 1.0], + ..FragmentStyle::default() + }, + ) + .add_text( + " to open configuration file", + FragmentStyle { + color: [0.7019608, 0.7019608, 0.7019608, 1.0], ..FragmentStyle::default() }, ) .build(); - #[cfg(target_os = "macos")] - let shortcut = "\"Command\" + \",\" (comma)"; - - #[cfg(not(target_os = "macos"))] - let shortcut = "\"Control\" + \"Shift\" + \",\" (comma)"; - - let paragraph_line = content.sel(paragraph); + let paragraph_line = content.sel(paragraph).clear(); paragraph_line - .clear() .add_text( - "Your configuration file will be created in", - FragmentStyle::default(), + "\n\nBuilt in Rust with:", + FragmentStyle { + color: [1.0, 1.0, 1.0, 1.0], + ..FragmentStyle::default() + }, + ) + .add_text( + "\n• WGPU as rendering backend", + FragmentStyle { + color: [1.0, 1.0, 1.0, 1.0], + ..FragmentStyle::default() + }, + ) + .add_text( + "\n• Tokio as async runtime", + FragmentStyle { + color: [1.0, 1.0, 1.0, 1.0], + ..FragmentStyle::default() + }, ) - .new_line() .add_text( - &format!(" {} ", rio_backend::config::config_file_path().display()), + "\n• Sugarloaf for Advanced Text Rendering", FragmentStyle { - background_color: Some(yellow), - color: [0., 0., 0., 1.], + color: [1.0, 1.0, 1.0, 1.0], ..FragmentStyle::default() }, ) - .new_line() - .add_text("", FragmentStyle::default()) - .new_line() - .add_text("To open settings menu use", FragmentStyle::default()) - .new_line() .add_text( - &format!(" {shortcut} "), + "\n• And lots of ❤️", FragmentStyle { - background_color: Some(yellow), - color: [0., 0., 0., 1.], + color: [1.0, 1.0, 1.0, 1.0], ..FragmentStyle::default() }, ) - .new_line() - .add_text("", FragmentStyle::default()) - .new_line() - .add_text("", FragmentStyle::default()) - .new_line() - .add_text("More info in rioterm.com", FragmentStyle::default()) .build(); - objects.push(Object::RichText(RichText { - id: heading, - position: [70., context_dimension.margin.top_y + 30.], - lines: None, - })); - - objects.push(Object::RichText(RichText { - id: paragraph_action, - position: [70., context_dimension.margin.top_y + 70.], - lines: None, - })); - - objects.push(Object::RichText(RichText { - id: paragraph, - position: [70., context_dimension.margin.top_y + 140.], - lines: None, - })); - - sugarloaf.set_objects(objects); + // Show rich texts at specific positions + sugarloaf.show_rich_text(heading, 70.0, context_dimension.margin.top_y + 30.0); + sugarloaf.show_rich_text( + paragraph_action, + 70.0, + context_dimension.margin.top_y + 70.0, + ); + sugarloaf.show_rich_text(paragraph, 70.0, context_dimension.margin.top_y + 140.0); } diff --git a/frontends/rioterm/src/screen/mod.rs b/frontends/rioterm/src/screen/mod.rs index 8f503eb148..fb276b9c04 100644 --- a/frontends/rioterm/src/screen/mod.rs +++ b/frontends/rioterm/src/screen/mod.rs @@ -247,6 +247,7 @@ impl Screen<'_> { if let Some(image) = &config.window.background_image { sugarloaf.set_background_image(image); } + sugarloaf.render(); Ok(Screen { @@ -384,7 +385,7 @@ impl Screen<'_> { padding_y_bottom, )); - context_grid.update_dimensions(&self.sugarloaf); + context_grid.update_dimensions(&mut self.sugarloaf); for current_context in context_grid.contexts_mut().values_mut() { let current_context = current_context.context_mut(); @@ -439,7 +440,7 @@ impl Screen<'_> { self.context_manager .current_grid_mut() - .update_dimensions(&self.sugarloaf); + .update_dimensions(&mut self.sugarloaf); self.render(); self.resize_all_contexts(); @@ -479,7 +480,7 @@ impl Screen<'_> { self.resize_all_contexts(); self.context_manager .current_grid_mut() - .update_dimensions(&self.sugarloaf); + .update_dimensions(&mut self.sugarloaf); let width = new_size.width as f32; let height = new_size.height as f32; diff --git a/sugarloaf/examples/f16_test.rs b/sugarloaf/examples/f16_test.rs index 65ba5f949b..78a977b653 100644 --- a/sugarloaf/examples/f16_test.rs +++ b/sugarloaf/examples/f16_test.rs @@ -7,8 +7,7 @@ use rio_window::{ }; use std::error::Error; use sugarloaf::{ - layout::RootStyle, FragmentStyle, Object, RichText, Sugarloaf, SugarloafWindow, - SugarloafWindowSize, + layout::RootStyle, FragmentStyle, Sugarloaf, SugarloafWindow, SugarloafWindowSize, }; fn main() { @@ -186,11 +185,8 @@ impl ApplicationHandler for Application { ) .build(); - sugarloaf.set_objects(vec![Object::RichText(RichText { - id: self.rich_text, - position: [10., 0.], - lines: None, - })]); + // Show rich text using new API + sugarloaf.show_rich_text(self.rich_text, 10., 0.); sugarloaf.render(); } _ => {} diff --git a/sugarloaf/examples/layer.rs b/sugarloaf/examples/layer.rs index 971553bb05..8825d156df 100644 --- a/sugarloaf/examples/layer.rs +++ b/sugarloaf/examples/layer.rs @@ -8,8 +8,7 @@ use rio_window::{ }; use std::error::Error; use sugarloaf::{ - layout::RootStyle, FragmentStyle, Object, Quad, RichText, Sugarloaf, SugarloafWindow, - SugarloafWindowSize, + layout::RootStyle, FragmentStyle, Sugarloaf, SugarloafWindow, SugarloafWindowSize, }; fn main() { @@ -153,41 +152,16 @@ impl ApplicationHandler for Application { .new_line() .build(); - sugarloaf.set_objects(vec![ - Object::RichText(RichText { - id: self.rich_text, - position: [10., 10.], - lines: None, - }), - Object::Quad(Quad { - position: [10., 10.], - color: [1.0, 0.3, 0.5, 1.0], - size: [120., 100.], - ..Quad::default() - }), - Object::RichText(RichText { - id: self.second_rich_text, - position: [10., 60.], - lines: None, - }), - Object::Quad(Quad { - position: [10., 80.], - color: [0.0, 0.3, 0.5, 1.0], - size: [120., 100.], - ..Quad::default() - }), - Object::Quad(Quad { - position: [95., 30.], - color: [1.0, 1.0, 0.5, 1.0], - size: [20., 100.], - ..Quad::default() - }), - Object::RichText(RichText { - id: self.rich_text, - position: [100., 100.], - lines: None, - }), - ]); + // Add rectangles directly + sugarloaf.add_rect(10., 10., 120., 100., [1.0, 1.0, 1.0, 1.0]); + sugarloaf.add_rect(10., 80., 120., 100., [0.0, 0.0, 0.0, 1.0]); + sugarloaf.add_rect(95., 30., 20., 100., [1.0, 1.0, 1.0, 1.0]); + + // Show rich text + sugarloaf.show_rich_text(self.rich_text, 10., 10.); + sugarloaf.show_rich_text(self.second_rich_text, 10., 60.); + sugarloaf.show_rich_text(self.rich_text, 100., 100.); + sugarloaf.render(); event_loop.set_control_flow(ControlFlow::Wait); } diff --git a/sugarloaf/examples/line_height.rs b/sugarloaf/examples/line_height.rs index b1180a0fb4..54ad3669a0 100644 --- a/sugarloaf/examples/line_height.rs +++ b/sugarloaf/examples/line_height.rs @@ -11,8 +11,8 @@ use rio_window::{ }; use std::error::Error; use sugarloaf::{ - layout::RootStyle, FragmentStyle, Object, RichText, SugarCursor, Sugarloaf, - SugarloafWindow, SugarloafWindowSize, + layout::RootStyle, FragmentStyle, SugarCursor, Sugarloaf, SugarloafWindow, + SugarloafWindowSize, }; fn main() { @@ -195,11 +195,8 @@ impl ApplicationHandler for Application { ) .build(); - sugarloaf.set_objects(vec![Object::RichText(RichText { - id: 0, - position: [10., 0.], - lines: None, - })]); + // Show rich text using new API + sugarloaf.show_rich_text(0, 10., 0.); sugarloaf.render(); event_loop.set_control_flow(ControlFlow::Wait); } diff --git a/sugarloaf/examples/multi_text.rs b/sugarloaf/examples/multi_text.rs index 67d5f1c699..fd027b0721 100644 --- a/sugarloaf/examples/multi_text.rs +++ b/sugarloaf/examples/multi_text.rs @@ -10,8 +10,7 @@ use rio_window::{ }; use std::error::Error; use sugarloaf::{ - layout::RootStyle, FragmentStyle, Object, Quad, RichText, Sugarloaf, SugarloafWindow, - SugarloafWindowSize, + layout::RootStyle, FragmentStyle, Sugarloaf, SugarloafWindow, SugarloafWindowSize, }; fn main() { @@ -110,57 +109,6 @@ impl ApplicationHandler for Application { let sugarloaf = self.sugarloaf.as_mut().unwrap(); let window = self.window.as_mut().unwrap(); - let objects = vec![ - Object::Quad(Quad { - color: [1.0, 0.5, 0.5, 0.5], - position: [5., 5.], - shadow_blur_radius: 2.0, - shadow_offset: [1.0, 1.0], - shadow_color: [1.0, 1.0, 0.0, 1.0], - border_color: [1.0, 0.0, 1.0, 1.0], - border_width: 2.0, - border_radius: [10.0, 10.0, 10.0, 10.0], - size: [200.0, 200.0], - }), - Object::RichText(RichText { - id: self.rich_texts[0], - position: [5., 5.], - lines: None, - }), - Object::Quad(Quad { - color: [1.0, 0.5, 0.5, 0.5], - position: [220., 5.], - shadow_blur_radius: 0.0, - shadow_offset: [0.0, 0.0], - shadow_color: [1.0, 1.0, 0.0, 1.0], - border_color: [1.0, 0.0, 1.0, 1.0], - border_width: 2.0, - border_radius: [0.0, 0.0, 0.0, 0.0], - size: [200.0, 150.0], - }), - Object::RichText(RichText { - id: self.rich_texts[1], - position: [220., 5.], - lines: None, - }), - Object::Quad(Quad { - color: [1.0, 0.5, 0.5, 0.5], - position: [440., 5.], - shadow_blur_radius: 0.0, - shadow_offset: [0.0, 0.0], - shadow_color: [1.0, 1.0, 0.0, 1.0], - border_color: [1.0, 0.0, 1.0, 1.0], - border_width: 2.0, - border_radius: [0.0, 0.0, 0.0, 0.0], - size: [320.0, 150.0], - }), - Object::RichText(RichText { - id: self.rich_texts[2], - position: [440., 5.], - lines: None, - }), - ]; - match event { WindowEvent::CloseRequested => event_loop.exit(), WindowEvent::ScaleFactorChanged { @@ -243,7 +191,16 @@ impl ApplicationHandler for Application { } } - sugarloaf.set_objects(objects); + // Add rectangles directly + sugarloaf.add_rect(5., 5., 200.0, 200.0, [1.0, 1.0, 1.0, 1.0]); + sugarloaf.add_rect(220., 5., 200.0, 150.0, [1.0, 1.0, 1.0, 1.0]); + sugarloaf.add_rect(440., 5., 320.0, 150.0, [1.0, 1.0, 1.0, 1.0]); + + // Show rich text + sugarloaf.show_rich_text(self.rich_texts[0], 5., 5.); + sugarloaf.show_rich_text(self.rich_texts[1], 220., 5.); + sugarloaf.show_rich_text(self.rich_texts[2], 440., 5.); + sugarloaf.render(); event_loop.set_control_flow(ControlFlow::Wait); } diff --git a/sugarloaf/examples/simple_unified.rs b/sugarloaf/examples/simple_unified.rs new file mode 100644 index 0000000000..fbadca8e66 --- /dev/null +++ b/sugarloaf/examples/simple_unified.rs @@ -0,0 +1,121 @@ +// Example demonstrating the unified rich text and quad rendering +// Run with: cargo run --example simple_unified + +use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; +use rio_window::application::ApplicationHandler; +use rio_window::event_loop::ControlFlow; +use rio_window::event_loop::{ActiveEventLoop, DeviceEvents}; +use rio_window::window::{Window, WindowId}; +use rio_window::{ + dpi::LogicalSize, event::WindowEvent, event_loop::EventLoop, window::WindowAttributes, +}; +use std::error::Error; +use sugarloaf::{layout::RootStyle, Sugarloaf, SugarloafWindow, SugarloafWindowSize}; + +fn main() { + let width = 800.0; + let height = 600.0; + let window_event_loop = rio_window::event_loop::EventLoop::new().unwrap(); + let mut application = Application::new(&window_event_loop, width, height); + let _ = application.run(window_event_loop); +} + +struct Application { + sugarloaf: Option>, + window: Option, + height: f32, + width: f32, +} + +impl Application { + fn new(event_loop: &EventLoop<()>, width: f32, height: f32) -> Self { + event_loop.listen_device_events(DeviceEvents::Never); + + Application { + sugarloaf: None, + window: None, + width, + height, + } + } + + fn run(&mut self, event_loop: EventLoop<()>) -> Result<(), Box> { + let result = event_loop.run_app(self); + result.map_err(Into::into) + } +} + +impl ApplicationHandler for Application { + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + let window_attributes = WindowAttributes::default() + .with_title("Sugarloaf - Unified Rendering Demo") + .with_inner_size(LogicalSize::new(self.width, self.height)) + .with_resizable(true); + + let window = event_loop.create_window(window_attributes).unwrap(); + + let sugarloaf_window = SugarloafWindow { + handle: window.window_handle().unwrap().as_raw(), + display: window.display_handle().unwrap().as_raw(), + scale: window.scale_factor() as f32, + size: SugarloafWindowSize { + width: self.width, + height: self.height, + }, + }; + + let font_library = sugarloaf::font::FontLibrary::default(); + let sugarloaf = Sugarloaf::new( + sugarloaf_window, + sugarloaf::SugarloafRenderer::default(), + &font_library, + RootStyle::default(), + ) + .expect("Sugarloaf instance should be created"); + + self.sugarloaf = Some(sugarloaf); + self.window = Some(window); + } + + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + _window_id: WindowId, + event: WindowEvent, + ) { + let sugarloaf = self.sugarloaf.as_mut().unwrap(); + let window = self.window.as_mut().unwrap(); + + match event { + WindowEvent::CloseRequested => event_loop.exit(), + WindowEvent::ScaleFactorChanged { scale_factor, .. } => { + let scale_factor_f32 = scale_factor as f32; + let new_inner_size = window.inner_size(); + sugarloaf.rescale(scale_factor_f32); + sugarloaf.resize(new_inner_size.width, new_inner_size.height); + window.request_redraw(); + } + WindowEvent::Resized(new_size) => { + sugarloaf.resize(new_size.width, new_size.height); + window.request_redraw(); + } + WindowEvent::RedrawRequested => { + // Add rectangles directly using the unified rendering approach + sugarloaf.add_rect( + 0.0, + 0.0, + self.width, + self.height, + [0.1, 0.1, 0.2, 1.0], + ); // background + sugarloaf.add_rect(50.0, 50.0, 200.0, 100.0, [1.0, 0.2, 0.2, 0.8]); // red rectangle + sugarloaf.add_rect(300.0, 150.0, 150.0, 80.0, [0.2, 1.0, 0.2, 0.9]); // green rectangle + sugarloaf.add_rect(500.0, 300.0, 180.0, 120.0, [0.2, 0.2, 1.0, 0.7]); // blue rectangle + + sugarloaf.render(); + event_loop.set_control_flow(ControlFlow::Wait); + } + _ => (), + } + } +} diff --git a/sugarloaf/examples/text.rs b/sugarloaf/examples/text.rs index e05c4a1ae5..dafc0af04b 100644 --- a/sugarloaf/examples/text.rs +++ b/sugarloaf/examples/text.rs @@ -8,8 +8,8 @@ use rio_window::{ }; use std::error::Error; use sugarloaf::{ - layout::RootStyle, FragmentStyle, FragmentStyleDecoration, Object, RichText, - Sugarloaf, SugarloafWindow, SugarloafWindowSize, UnderlineInfo, UnderlineShape, + layout::RootStyle, FragmentStyle, FragmentStyleDecoration, Sugarloaf, + SugarloafWindow, SugarloafWindowSize, UnderlineInfo, UnderlineShape, }; fn main() { @@ -249,11 +249,7 @@ impl ApplicationHandler for Application { ) .build(); - sugarloaf.set_objects(vec![Object::RichText(RichText { - id: self.rich_text, - position: [10., 0.], - lines: None, - })]); + sugarloaf.show_rich_text(self.rich_text, 10., 0.); sugarloaf.render(); event_loop.set_control_flow(ControlFlow::Wait); } diff --git a/sugarloaf/examples/transparent.rs b/sugarloaf/examples/transparent.rs index 0e4068fad5..f65d8ed166 100644 --- a/sugarloaf/examples/transparent.rs +++ b/sugarloaf/examples/transparent.rs @@ -8,7 +8,7 @@ use rio_window::{ dpi::LogicalSize, event::Event, event_loop::EventLoop, window::WindowAttributes, }; use sugarloaf::layout::RootStyle; -use sugarloaf::{Object, Quad, Sugarloaf, SugarloafWindow, SugarloafWindowSize}; +use sugarloaf::{Sugarloaf, SugarloafWindow, SugarloafWindowSize}; fn main() { let mut event_loop = EventLoop::new().unwrap(); @@ -60,22 +60,10 @@ fn main() { } Event::WindowEvent { event, .. } => { if let WindowEvent::RedrawRequested = event { - let objects = vec![ - Object::Quad(Quad { - position: [10.0, 10.0], - color: [1.0, 0.0, 1.0, 0.2], - size: [50.0, 50.0], - ..Quad::default() - }), - Object::Quad(Quad { - position: [115.0, 10.0], - color: [0.0, 1.0, 1.0, 0.5], - size: [50.0, 50.0], - ..Quad::default() - }), - ]; + // Add rectangles directly + sugarloaf.add_rect(10.0, 10.0, 50.0, 50.0, [1.0, 1.0, 1.0, 1.0]); + sugarloaf.add_rect(115.0, 10.0, 50.0, 50.0, [0.0, 0.0, 0.0, 1.0]); - sugarloaf.set_objects(objects); sugarloaf.render(); } } diff --git a/sugarloaf/src/components/mod.rs b/sugarloaf/src/components/mod.rs index de1c67532e..5b94e2192d 100644 --- a/sugarloaf/src/components/mod.rs +++ b/sugarloaf/src/components/mod.rs @@ -1,5 +1,4 @@ pub mod core; pub mod filters; pub mod layer; -pub mod quad; pub mod rich_text; diff --git a/sugarloaf/src/components/quad/mod.rs b/sugarloaf/src/components/quad/mod.rs deleted file mode 100644 index 130cf69cbf..0000000000 --- a/sugarloaf/src/components/quad/mod.rs +++ /dev/null @@ -1,589 +0,0 @@ -use crate::components::core::{orthographic_projection, uniforms::Uniforms}; -use crate::context::metal::MetalContext; -use crate::context::webgpu::WgpuContext; -use crate::context::Context; -use crate::context::ContextType; - -use bytemuck::{Pod, Zeroable}; -use metal::*; - -use std::mem; - -/// The background of some element. -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Background { - /// A composed_quad color. - Color([f32; 4]), -} - -const INITIAL_QUANTITY: usize = 2; - -/// The properties of a quad. -#[derive(Clone, Copy, Debug, Pod, Zeroable, PartialEq, Default)] -#[repr(C)] -pub struct Quad { - /// The background color data of the quad. - pub color: [f32; 4], - - /// The position of the [`Quad`]. - pub position: [f32; 2], - - /// The size of the [`Quad`]. - pub size: [f32; 2], - - /// The border color of the [`Quad`], in __linear RGB__. - pub border_color: [f32; 4], - - /// The border radii of the [`Quad`]. - pub border_radius: [f32; 4], - - /// The border width of the [`Quad`]. - pub border_width: f32, - - /// The shadow color of the [`Quad`]. - pub shadow_color: [f32; 4], - - /// The shadow offset of the [`Quad`]. - pub shadow_offset: [f32; 2], - - /// The shadow blur radius of the [`Quad`]. - pub shadow_blur_radius: f32, -} - -#[derive(Debug)] -pub enum BrushType { - Wgpu(WgpuQuadBrush), - Metal(MetalQuadBrush), -} - -#[derive(Debug)] -pub struct WgpuQuadBrush { - pipeline: wgpu::RenderPipeline, - constants: wgpu::BindGroup, - transform: wgpu::Buffer, - instances: wgpu::Buffer, - supported_quantity: usize, -} - -#[derive(Debug)] -pub struct MetalQuadBrush { - pipeline_state: RenderPipelineState, - vertex_buffer: Buffer, - uniform_buffer: Buffer, - supported_quantity: usize, -} - -#[derive(Debug)] -pub struct QuadBrush { - current_transform: [f32; 16], - brush_type: BrushType, -} - -impl QuadBrush { - pub fn new(context: &Context) -> QuadBrush { - let brush_type = match &context.inner { - ContextType::Wgpu(wgpu_context) => { - BrushType::Wgpu(WgpuQuadBrush::new(wgpu_context)) - } - ContextType::Metal(metal_context) => { - BrushType::Metal(MetalQuadBrush::new(metal_context)) - } - }; - - QuadBrush { - current_transform: [0.0; 16], - brush_type, - } - } - - pub fn resize(&mut self, ctx: &mut Context) { - let transform: [f32; 16] = match &ctx.inner { - ContextType::Wgpu(wgpu_ctx) => { - orthographic_projection(wgpu_ctx.size.width, wgpu_ctx.size.height) - } - ContextType::Metal(metal_ctx) => { - orthographic_projection(metal_ctx.size.width, metal_ctx.size.height) - } - }; - - if transform != self.current_transform { - match &mut self.brush_type { - BrushType::Wgpu(wgpu_brush) => { - let (scale, queue) = match &ctx.inner { - ContextType::Wgpu(wgpu_ctx) => (wgpu_ctx.scale, &wgpu_ctx.queue), - _ => unreachable!(), - }; - - let uniforms = Uniforms::new(transform, scale); - queue.write_buffer( - &wgpu_brush.transform, - 0, - bytemuck::bytes_of(&uniforms), - ); - } - BrushType::Metal(metal_brush) => { - let scale = match &ctx.inner { - ContextType::Metal(metal_ctx) => metal_ctx.scale, - _ => unreachable!(), - }; - - let uniforms = Uniforms::new(transform, scale); - let contents = metal_brush.uniform_buffer.contents() as *mut Uniforms; - unsafe { - *contents = uniforms; - } - } - } - - self.current_transform = transform; - } - } - - pub fn render_wgpu<'a>( - &'a mut self, - context: &mut WgpuContext, - state: &crate::sugarloaf::state::SugarState, - render_pass: &mut wgpu::RenderPass<'a>, - ) { - if let BrushType::Wgpu(brush) = &mut self.brush_type { - brush.render(context, state, render_pass); - } - } - - pub fn render_metal( - &mut self, - context: &MetalContext, - state: &crate::sugarloaf::state::SugarState, - render_encoder: &RenderCommandEncoderRef, - ) { - if let BrushType::Metal(brush) = &mut self.brush_type { - brush.render(context, state, render_encoder); - } - } -} - -impl WgpuQuadBrush { - pub fn new(context: &WgpuContext) -> WgpuQuadBrush { - let supported_quantity = INITIAL_QUANTITY; - let instances = context.device.create_buffer(&wgpu::BufferDescriptor { - label: Some("sugarloaf::quad Instances Buffer"), - size: mem::size_of::() as u64 * supported_quantity as u64, - usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - - let constant_layout = - context - .device - .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("sugarloaf::quad uniforms layout"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: wgpu::BufferSize::new( - mem::size_of::() as wgpu::BufferAddress, - ), - }, - count: None, - }], - }); - - let transform = context.device.create_buffer(&wgpu::BufferDescriptor { - label: Some("sugarloaf::quad uniforms buffer"), - size: mem::size_of::() as u64, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - - let constants = context - .device - .create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("sugarloaf::quad uniforms bind group"), - layout: &constant_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: transform.as_entire_binding(), - }], - }); - - let layout = - context - .device - .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("sugarloaf::quad pipeline"), - push_constant_ranges: &[], - bind_group_layouts: &[&constant_layout], - }); - - let shader_source = if context.supports_f16() { - include_str!("./quad_f16.wgsl") - } else { - include_str!("./quad_f32_combined.wgsl") - }; - - let shader = context - .device - .create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("sugarloaf::quad shader"), - source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed( - shader_source, - )), - }); - - let pipeline = - context - .device - .create_render_pipeline(&wgpu::RenderPipelineDescriptor { - cache: None, - label: Some("sugarloaf::quad render pipeline"), - layout: Some(&layout), - vertex: wgpu::VertexState { - compilation_options: wgpu::PipelineCompilationOptions::default(), - module: &shader, - entry_point: Some("composed_quad_vs_main"), - buffers: &[wgpu::VertexBufferLayout { - array_stride: std::mem::size_of::() as u64, - step_mode: wgpu::VertexStepMode::Instance, - attributes: &wgpu::vertex_attr_array!( - // Color - 0 => Float32x4, - // Position - 1 => Float32x2, - // Size - 2 => Float32x2, - // Border color - 3 => Float32x4, - // Border radius - 4 => Float32x4, - // Border width - 5 => Float32, - // Shadow color - 6 => Float32x4, - // Shadow offset - 7 => Float32x2, - // Shadow blur radius - 8 => Float32, - ), - }], - }, - fragment: Some(wgpu::FragmentState { - compilation_options: wgpu::PipelineCompilationOptions::default(), - module: &shader, - entry_point: Some("composed_quad_fs_main"), - targets: &[Some(wgpu::ColorTargetState { - format: context.format, - blend: Some(wgpu::BlendState { - color: wgpu::BlendComponent { - src_factor: wgpu::BlendFactor::SrcAlpha, - dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, - operation: wgpu::BlendOperation::Add, - }, - alpha: wgpu::BlendComponent { - src_factor: wgpu::BlendFactor::One, - dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, - operation: wgpu::BlendOperation::Add, - }, - }), - write_mask: wgpu::ColorWrites::ALL, - })], - }), - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, - front_face: wgpu::FrontFace::Cw, - ..Default::default() - }, - depth_stencil: None, - multisample: wgpu::MultisampleState { - count: 1, - mask: !0, - alpha_to_coverage_enabled: false, - }, - multiview: None, - }); - - WgpuQuadBrush { - supported_quantity, - instances, - constants, - transform, - pipeline, - } - } - - pub fn render<'a>( - &'a mut self, - context: &mut WgpuContext, - state: &crate::sugarloaf::state::SugarState, - render_pass: &mut wgpu::RenderPass<'a>, - ) { - let instances = &state.quads; - let total = instances.len(); - - if total == 0 { - return; - } - - if total > self.supported_quantity { - self.instances.destroy(); - - self.supported_quantity = total; - self.instances = context.device.create_buffer(&wgpu::BufferDescriptor { - label: Some("sugarloaf::quad instances"), - size: mem::size_of::() as u64 * self.supported_quantity as u64, - usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - } - - let instance_bytes = bytemuck::cast_slice(instances); - context - .queue - .write_buffer(&self.instances, 0, instance_bytes); - - render_pass.set_pipeline(&self.pipeline); - render_pass.set_bind_group(0, &self.constants, &[]); - render_pass.set_vertex_buffer(0, self.instances.slice(..)); - - render_pass.draw(0..6, 0..total as u32); - } -} - -impl MetalQuadBrush { - pub fn new(context: &MetalContext) -> MetalQuadBrush { - let supported_quantity = INITIAL_QUANTITY; - - // Create vertex buffer for quad instances - let vertex_buffer = context.device.new_buffer( - (mem::size_of::() * supported_quantity) as u64, - MTLResourceOptions::StorageModeShared, - ); - vertex_buffer.set_label("sugarloaf::quad vertex buffer"); - - // Create uniform buffer (back to Uniforms like WGPU) - let uniform_buffer = context.device.new_buffer( - mem::size_of::() as u64, - MTLResourceOptions::StorageModeShared, - ); - uniform_buffer.set_label("sugarloaf::quad uniform buffer"); - - // Create shader library from the Metal shader source - let shader_source = include_str!("./quad.metal"); - - let library = context - .device - .new_library_with_source(shader_source, &CompileOptions::new()) - .expect("Failed to create shader library"); - - // let function_names = library.function_names(); - // println!("Available Metal functions: {:?}", function_names); - - let vertex_function = library - .get_function("vertex_main", None) - .expect("Failed to get vertex function"); - let fragment_function = library - .get_function("fragment_main", None) - .expect("Failed to get fragment function"); - - // Create vertex descriptor for proper instanced rendering - let vertex_descriptor = VertexDescriptor::new(); - let attributes = vertex_descriptor.attributes(); - - // Per-instance attributes (Quad data) - // Color (attribute 0) - [f32; 4] - attributes - .object_at(0) - .unwrap() - .set_format(MTLVertexFormat::Float4); - attributes.object_at(0).unwrap().set_offset(0); - attributes.object_at(0).unwrap().set_buffer_index(0); - - // Position (attribute 1) - [f32; 2] - attributes - .object_at(1) - .unwrap() - .set_format(MTLVertexFormat::Float2); - attributes.object_at(1).unwrap().set_offset(16); - attributes.object_at(1).unwrap().set_buffer_index(0); - - // Size (attribute 2) - [f32; 2] - attributes - .object_at(2) - .unwrap() - .set_format(MTLVertexFormat::Float2); - attributes.object_at(2).unwrap().set_offset(24); - attributes.object_at(2).unwrap().set_buffer_index(0); - - // Border color (attribute 3) - [f32; 4] - attributes - .object_at(3) - .unwrap() - .set_format(MTLVertexFormat::Float4); - attributes.object_at(3).unwrap().set_offset(32); - attributes.object_at(3).unwrap().set_buffer_index(0); - - // Border radius (attribute 4) - [f32; 4] - attributes - .object_at(4) - .unwrap() - .set_format(MTLVertexFormat::Float4); - attributes.object_at(4).unwrap().set_offset(48); - attributes.object_at(4).unwrap().set_buffer_index(0); - - // Border width (attribute 5) - f32 - attributes - .object_at(5) - .unwrap() - .set_format(MTLVertexFormat::Float); - attributes.object_at(5).unwrap().set_offset(64); - attributes.object_at(5).unwrap().set_buffer_index(0); - - // Shadow color (attribute 6) - [f32; 4] - attributes - .object_at(6) - .unwrap() - .set_format(MTLVertexFormat::Float4); - attributes.object_at(6).unwrap().set_offset(68); - attributes.object_at(6).unwrap().set_buffer_index(0); - - // Shadow offset (attribute 7) - [f32; 2] - attributes - .object_at(7) - .unwrap() - .set_format(MTLVertexFormat::Float2); - attributes.object_at(7).unwrap().set_offset(84); - attributes.object_at(7).unwrap().set_buffer_index(0); - - // Shadow blur radius (attribute 8) - f32 - attributes - .object_at(8) - .unwrap() - .set_format(MTLVertexFormat::Float); - attributes.object_at(8).unwrap().set_offset(92); - attributes.object_at(8).unwrap().set_buffer_index(0); - - // Set up buffer layout for per-instance data - let layouts = vertex_descriptor.layouts(); - layouts - .object_at(0) - .unwrap() - .set_stride(std::mem::size_of::() as u64); - layouts - .object_at(0) - .unwrap() - .set_step_function(MTLVertexStepFunction::PerInstance); - layouts.object_at(0).unwrap().set_step_rate(1); - - // Create render pipeline descriptor - let pipeline_descriptor = RenderPipelineDescriptor::new(); - pipeline_descriptor.set_vertex_function(Some(&vertex_function)); - pipeline_descriptor.set_fragment_function(Some(&fragment_function)); - pipeline_descriptor.set_vertex_descriptor(Some(&vertex_descriptor)); - - // Set up color attachment - let color_attachments = pipeline_descriptor.color_attachments(); - color_attachments - .object_at(0) - .unwrap() - .set_pixel_format(context.layer.pixel_format()); - color_attachments - .object_at(0) - .unwrap() - .set_blending_enabled(true); - - // Set up alpha blending - color_attachments - .object_at(0) - .unwrap() - .set_source_rgb_blend_factor(MTLBlendFactor::SourceAlpha); - color_attachments - .object_at(0) - .unwrap() - .set_destination_rgb_blend_factor(MTLBlendFactor::OneMinusSourceAlpha); - color_attachments - .object_at(0) - .unwrap() - .set_rgb_blend_operation(MTLBlendOperation::Add); - - color_attachments - .object_at(0) - .unwrap() - .set_source_alpha_blend_factor(MTLBlendFactor::One); - color_attachments - .object_at(0) - .unwrap() - .set_destination_alpha_blend_factor(MTLBlendFactor::OneMinusSourceAlpha); - color_attachments - .object_at(0) - .unwrap() - .set_alpha_blend_operation(MTLBlendOperation::Add); - - color_attachments - .object_at(0) - .unwrap() - .set_write_mask(MTLColorWriteMask::All); - - // Create the pipeline state - let pipeline_state = context - .device - .new_render_pipeline_state(&pipeline_descriptor) - .expect("Failed to create render pipeline state"); - - let metal_brush = MetalQuadBrush { - pipeline_state, - vertex_buffer, - uniform_buffer, - supported_quantity, - }; - - metal_brush - } - - pub fn render( - &mut self, - context: &MetalContext, - state: &crate::sugarloaf::state::SugarState, - render_encoder: &RenderCommandEncoderRef, - ) { - let instances = &state.quads; - let total = instances.len(); - - if total == 0 { - return; - } - - // Resize buffer if needed - if total > self.supported_quantity { - self.supported_quantity = total; - self.vertex_buffer = context.device.new_buffer( - (mem::size_of::() * self.supported_quantity) as u64, - MTLResourceOptions::StorageModeShared, - ); - self.vertex_buffer - .set_label("sugarloaf::quad vertex buffer"); - } - - if total == 0 { - return; - } - let vertex_data = self.vertex_buffer.contents() as *mut Quad; - unsafe { - std::ptr::copy_nonoverlapping(instances.as_ptr(), vertex_data, total); - } - - // Set up render state - render_encoder.set_render_pipeline_state(&self.pipeline_state); - render_encoder.set_vertex_buffer(0, Some(&self.vertex_buffer), 0); - render_encoder.set_vertex_buffer(1, Some(&self.uniform_buffer), 0); - - // Draw quads (6 vertices per quad instance) - render_encoder.draw_primitives_instanced( - MTLPrimitiveType::Triangle, - 0, - 6, - total as u64, - ); - } -} diff --git a/sugarloaf/src/components/quad/quad.metal b/sugarloaf/src/components/quad/quad.metal deleted file mode 100644 index f157883916..0000000000 --- a/sugarloaf/src/components/quad/quad.metal +++ /dev/null @@ -1,77 +0,0 @@ -// Metal quad shader using proper vertex attributes for instancing -#include -#include -using namespace metal; - -struct VertexIn { - // Per-instance attributes from Quad struct - float4 color [[attribute(0)]]; - float2 position [[attribute(1)]]; - float2 size [[attribute(2)]]; - float4 border_color [[attribute(3)]]; - float4 border_radius [[attribute(4)]]; - float border_width [[attribute(5)]]; - float4 shadow_color [[attribute(6)]]; - float2 shadow_offset [[attribute(7)]]; - float shadow_blur_radius [[attribute(8)]]; -}; - -struct Uniforms { - float4x4 transform; - float scale; -}; - -struct VertexOut { - float4 position [[position]]; - float4 color; - float2 quad_pos; - float2 quad_size; - float4 border_color; - float4 border_radius; - float border_width; - float4 shadow_color; - float2 shadow_offset; - float shadow_blur_radius; -}; - -// Generates unit quad vertices using efficient bit manipulation -// Equivalent to: positions = [(1,1), (1,0), (0,0), (0,0), (0,1), (1,1)] -// This creates 2 triangles: (1,1)→(1,0)→(0,0) and (0,0)→(0,1)→(1,1) -float2 vertex_position(uint vertex_index) { - uint2 base = uint2(1, 2) + vertex_index; - uint2 modulo = base % uint2(6); - uint2 comparison = select(uint2(0), uint2(1), modulo < uint2(3)); - return float2(comparison); -} - -vertex VertexOut vertex_main(uint vertex_id [[vertex_id]], - VertexIn input [[stage_in]], - constant Uniforms& uniforms [[buffer(1)]]) { - - float2 unit_vertex = vertex_position(vertex_id); - - // Apply scale like WGPU version does - float2 scaled_position = input.position * uniforms.scale; - float2 scaled_size = input.size * uniforms.scale; - - float2 world_pos = scaled_position + unit_vertex * scaled_size; - float4 clip_pos = uniforms.transform * float4(world_pos, 0.0, 1.0); - - VertexOut out; - out.position = clip_pos; - out.color = input.color; - out.border_color = input.border_color; - out.quad_pos = float2(unit_vertex); - out.quad_size = float2(scaled_size); - out.border_radius = input.border_radius; - out.border_width = input.border_width; - out.shadow_color = input.shadow_color; - out.shadow_offset = float2(input.shadow_offset); - out.shadow_blur_radius = input.shadow_blur_radius; - - return out; -} - -fragment float4 fragment_main(VertexOut in [[stage_in]]) { - return in.color; -} \ No newline at end of file diff --git a/sugarloaf/src/components/quad/quad_f16.wgsl b/sugarloaf/src/components/quad/quad_f16.wgsl deleted file mode 100644 index 2a9129e764..0000000000 --- a/sugarloaf/src/components/quad/quad_f16.wgsl +++ /dev/null @@ -1,188 +0,0 @@ -enable f16; - -// This code was originally retired from iced-rs, which is licensed -// under MIT license https://github.com/iced-rs/iced/blob/master/LICENSE -// The code has suffered changes to fit on Sugarloaf architecture. - -struct Globals { - transform: mat4x4, - scale: f32, -} - -@group(0) @binding(0) var globals: Globals; - -fn distance_alg( - frag_coord: vec2, - position: vec2, - size: vec2, - radius: f32 -) -> f32 { - var inner_half_size: vec2 = vec2((size - vec2(radius, radius) * 2.0) / 2.0); - var top_left: vec2 = vec2(position + vec2(radius, radius)); - return rounded_box_sdf(frag_coord - vec2(top_left + inner_half_size), vec2(inner_half_size), 0.0); -} - -// Given a vector from a point to the center of a rounded rectangle of the given `size` and -// border `radius`, determines the point's distance from the nearest edge of the rounded rectangle -fn rounded_box_sdf(to_center: vec2, size: vec2, radius: f32) -> f32 { - return length(max(abs(to_center) - size + vec2(radius, radius), vec2(0.0, 0.0))) - radius; -} - -// Based on the fragment position and the center of the quad, select one of the 4 radii. -// Order matches CSS border radius attribute: -// radii.x = top-left, radii.y = top-right, radii.z = bottom-right, radii.w = bottom-left -fn select_border_radius(radii: vec4, position: vec2, center: vec2) -> f32 { - var rx = radii.x; - var ry = radii.y; - rx = select(radii.x, radii.y, position.x > center.x); - ry = select(radii.w, radii.z, position.x > center.x); - rx = select(rx, ry, position.y > center.y); - return rx; -} - -// Compute the normalized quad coordinates based on the vertex index. -fn vertex_position(vertex_index: u32) -> vec2 { - // #: 0 1 2 3 4 5 - // x: 1 1 0 0 0 1 - // y: 1 0 0 0 1 1 - return vec2((vec2(1u, 2u) + vertex_index) % vec2(6u) < vec2(3u)); -} - -struct SolidVertexInput { - @builtin(vertex_index) vertex_index: u32, - @location(0) color: vec4, - @location(1) pos: vec2, - @location(2) scale: vec2, - @location(3) border_color: vec4, - @location(4) border_radius: vec4, - @location(5) border_width: f32, - @location(6) shadow_color: vec4, - @location(7) shadow_offset: vec2, - @location(8) shadow_blur_radius: f32, -} - -struct SolidVertexOutput { - @builtin(position) position: vec4, - @location(0) color: vec4, - @location(1) border_color: vec4, - @location(2) pos: vec2, - @location(3) scale: vec2, - @location(4) border_radius: vec4, - @location(5) border_width: f16, - @location(6) shadow_color: vec4, - @location(7) shadow_offset: vec2, - @location(8) shadow_blur_radius: f16, -} - -@vertex -fn composed_quad_vs_main(input: SolidVertexInput) -> SolidVertexOutput { - var out: SolidVertexOutput; - - var pos: vec2 = (input.pos + min(input.shadow_offset, vec2(0.0, 0.0)) - input.shadow_blur_radius) * globals.scale; - var scale: vec2 = (input.scale + vec2(abs(input.shadow_offset.x), abs(input.shadow_offset.y)) + input.shadow_blur_radius * 2.0) * globals.scale; - var snap: vec2 = vec2(0.0, 0.0); - - if input.scale.x == 1.0 { - snap.x = round(pos.x) - pos.x; - } - - if input.scale.y == 1.0 { - snap.y = round(pos.y) - pos.y; - } - - var min_border_radius = min(input.scale.x, input.scale.y) * 0.5; - var border_radius: vec4 = vec4( - min(input.border_radius.x, min_border_radius), - min(input.border_radius.y, min_border_radius), - min(input.border_radius.z, min_border_radius), - min(input.border_radius.w, min_border_radius) - ); - - var transform: mat4x4 = mat4x4( - vec4(scale.x + 1.0, 0.0, 0.0, 0.0), - vec4(0.0, scale.y + 1.0, 0.0, 0.0), - vec4(0.0, 0.0, 1.0, 0.0), - vec4(pos - vec2(0.5, 0.5) + snap, 0.0, 1.0) - ); - - out.position = globals.transform * transform * vec4(vertex_position(input.vertex_index), 0.0, 1.0); - out.color = vec4(input.color); - out.border_color = vec4(input.border_color); - out.pos = vec2(input.pos * globals.scale + snap); - out.scale = vec2(input.scale * globals.scale); - out.border_radius = vec4(border_radius * globals.scale); - out.border_width = f16(input.border_width * globals.scale); - out.shadow_color = vec4(input.shadow_color); - out.shadow_offset = vec2(input.shadow_offset * globals.scale); - out.shadow_blur_radius = f16(input.shadow_blur_radius * globals.scale); - - return out; -} - -@fragment -fn composed_quad_fs_main( - input: SolidVertexOutput -) -> @location(0) vec4 { - var mixed_color: vec4 = input.color; - - var border_radius = select_border_radius( - vec4(input.border_radius), - input.position.xy, - (vec2(input.pos) + vec2(input.scale) * 0.5).xy - ); - - if (f32(input.border_width) > 0.0) { - var internal_border: f32 = max(border_radius - f32(input.border_width), 0.0); - - var internal_distance: f32 = distance_alg( - input.position.xy, - vec2(input.pos) + vec2(f32(input.border_width), f32(input.border_width)), - vec2(input.scale) - vec2(f32(input.border_width) * 2.0, f32(input.border_width) * 2.0), - internal_border - ); - - var border_mix: f16 = f16(smoothstep( - max(internal_border - 0.5, 0.0), - internal_border + 0.5, - internal_distance - )); - - mixed_color = mix(input.color, input.border_color, vec4(border_mix, border_mix, border_mix, border_mix)); - } - - var dist: f32 = distance_alg( - vec2(input.position.x, input.position.y), - vec2(input.pos), - vec2(input.scale), - border_radius - ); - - var radius_alpha: f16 = f16(1.0 - smoothstep( - max(border_radius - 0.5, 0.0), - border_radius + 0.5, - dist - )); - - let quad_color = vec4(mixed_color.x, mixed_color.y, mixed_color.z, mixed_color.w * radius_alpha); - - if input.shadow_color.a > f16(0.0) { - let shadow_radius = select_border_radius( - vec4(input.border_radius), - input.position.xy - vec2(input.shadow_offset), - (vec2(input.pos) + vec2(input.scale) * 0.5).xy - ); - let shadow_distance = max(rounded_box_sdf(input.position.xy - vec2(input.pos) - vec2(input.shadow_offset) - (vec2(input.scale) / 2.0), vec2(input.scale) / 2.0, shadow_radius), 0.); - - let shadow_alpha = f16(1.0 - smoothstep(-f32(input.shadow_blur_radius), f32(input.shadow_blur_radius), shadow_distance)); - let shadow_color = input.shadow_color; - let base_color = mix( - vec4(shadow_color.x, shadow_color.y, shadow_color.z, f16(0.0)), - quad_color, - quad_color.a - ); - - return vec4(mix(base_color, shadow_color, (f16(1.0) - radius_alpha) * shadow_alpha)); - } else { - return vec4(quad_color); - } -} \ No newline at end of file diff --git a/sugarloaf/src/components/quad/quad_f32_combined.wgsl b/sugarloaf/src/components/quad/quad_f32_combined.wgsl deleted file mode 100644 index a5aee2d958..0000000000 --- a/sugarloaf/src/components/quad/quad_f32_combined.wgsl +++ /dev/null @@ -1,186 +0,0 @@ -// This code was originally retired from iced-rs, which is licensed -// under MIT license https://github.com/iced-rs/iced/blob/master/LICENSE -// The code has suffered changes to fit on Sugarloaf architecture. - -struct Globals { - transform: mat4x4, - scale: f32, -} - -@group(0) @binding(0) var globals: Globals; - -fn distance_alg( - frag_coord: vec2, - position: vec2, - size: vec2, - radius: f32 -) -> f32 { - var inner_half_size: vec2 = (size - vec2(radius, radius) * 2.0) / 2.0; - var top_left: vec2 = position + vec2(radius, radius); - return rounded_box_sdf(frag_coord - top_left - inner_half_size, inner_half_size, 0.0); -} - -// Given a vector from a point to the center of a rounded rectangle of the given `size` and -// border `radius`, determines the point's distance from the nearest edge of the rounded rectangle -fn rounded_box_sdf(to_center: vec2, size: vec2, radius: f32) -> f32 { - return length(max(abs(to_center) - size + vec2(radius, radius), vec2(0.0, 0.0))) - radius; -} - -// Based on the fragment position and the center of the quad, select one of the 4 radii. -// Order matches CSS border radius attribute: -// radii.x = top-left, radii.y = top-right, radii.z = bottom-right, radii.w = bottom-left -fn select_border_radius(radii: vec4, position: vec2, center: vec2) -> f32 { - var rx = radii.x; - var ry = radii.y; - rx = select(radii.x, radii.y, position.x > center.x); - ry = select(radii.w, radii.z, position.x > center.x); - rx = select(rx, ry, position.y > center.y); - return rx; -} - -// Compute the normalized quad coordinates based on the vertex index. -fn vertex_position(vertex_index: u32) -> vec2 { - // #: 0 1 2 3 4 5 - // x: 1 1 0 0 0 1 - // y: 1 0 0 0 1 1 - return vec2((vec2(1u, 2u) + vertex_index) % vec2(6u) < vec2(3u)); -} - -struct SolidVertexInput { - @builtin(vertex_index) vertex_index: u32, - @location(0) color: vec4, - @location(1) pos: vec2, - @location(2) scale: vec2, - @location(3) border_color: vec4, - @location(4) border_radius: vec4, - @location(5) border_width: f32, - @location(6) shadow_color: vec4, - @location(7) shadow_offset: vec2, - @location(8) shadow_blur_radius: f32, -} - -struct SolidVertexOutput { - @builtin(position) position: vec4, - @location(0) color: vec4, - @location(1) border_color: vec4, - @location(2) pos: vec2, - @location(3) scale: vec2, - @location(4) border_radius: vec4, - @location(5) border_width: f32, - @location(6) shadow_color: vec4, - @location(7) shadow_offset: vec2, - @location(8) shadow_blur_radius: f32, -} - -@vertex -fn composed_quad_vs_main(input: SolidVertexInput) -> SolidVertexOutput { - var out: SolidVertexOutput; - - var pos: vec2 = (input.pos + min(input.shadow_offset, vec2(0.0, 0.0)) - input.shadow_blur_radius) * globals.scale; - var scale: vec2 = (input.scale + vec2(abs(input.shadow_offset.x), abs(input.shadow_offset.y)) + input.shadow_blur_radius * 2.0) * globals.scale; - var snap: vec2 = vec2(0.0, 0.0); - - if input.scale.x == 1.0 { - snap.x = round(pos.x) - pos.x; - } - - if input.scale.y == 1.0 { - snap.y = round(pos.y) - pos.y; - } - - var min_border_radius = min(input.scale.x, input.scale.y) * 0.5; - var border_radius: vec4 = vec4( - min(input.border_radius.x, min_border_radius), - min(input.border_radius.y, min_border_radius), - min(input.border_radius.z, min_border_radius), - min(input.border_radius.w, min_border_radius) - ); - - var transform: mat4x4 = mat4x4( - vec4(scale.x + 1.0, 0.0, 0.0, 0.0), - vec4(0.0, scale.y + 1.0, 0.0, 0.0), - vec4(0.0, 0.0, 1.0, 0.0), - vec4(pos - vec2(0.5, 0.5) + snap, 0.0, 1.0) - ); - - out.position = globals.transform * transform * vec4(vertex_position(input.vertex_index), 0.0, 1.0); - out.color = input.color; - out.border_color = input.border_color; - out.pos = input.pos * globals.scale + snap; - out.scale = input.scale * globals.scale; - out.border_radius = border_radius * globals.scale; - out.border_width = input.border_width * globals.scale; - out.shadow_color = input.shadow_color; - out.shadow_offset = input.shadow_offset * globals.scale; - out.shadow_blur_radius = input.shadow_blur_radius * globals.scale; - - return out; -} - -@fragment -fn composed_quad_fs_main( - input: SolidVertexOutput -) -> @location(0) vec4 { - var mixed_color: vec4 = input.color; - - var border_radius = select_border_radius( - input.border_radius, - input.position.xy, - (input.pos + input.scale * 0.5).xy - ); - - if (input.border_width > 0.0) { - var internal_border: f32 = max(border_radius - input.border_width, 0.0); - - var internal_distance: f32 = distance_alg( - input.position.xy, - input.pos + vec2(input.border_width, input.border_width), - input.scale - vec2(input.border_width * 2.0, input.border_width * 2.0), - internal_border - ); - - var border_mix: f32 = smoothstep( - max(internal_border - 0.5, 0.0), - internal_border + 0.5, - internal_distance - ); - - mixed_color = mix(input.color, input.border_color, vec4(border_mix, border_mix, border_mix, border_mix)); - } - - var dist: f32 = distance_alg( - vec2(input.position.x, input.position.y), - input.pos, - input.scale, - border_radius - ); - - var radius_alpha: f32 = 1.0 - smoothstep( - max(border_radius - 0.5, 0.0), - border_radius + 0.5, - dist - ); - - let quad_color = vec4(mixed_color.x, mixed_color.y, mixed_color.z, mixed_color.w * radius_alpha); - - if input.shadow_color.a > 0.0 { - let shadow_radius = select_border_radius( - input.border_radius, - input.position.xy - input.shadow_offset, - (input.pos + input.scale * 0.5).xy - ); - let shadow_distance = max(rounded_box_sdf(input.position.xy - input.pos - input.shadow_offset - (input.scale / 2.0), input.scale / 2.0, shadow_radius), 0.); - - let shadow_alpha = 1.0 - smoothstep(-input.shadow_blur_radius, input.shadow_blur_radius, shadow_distance); - let shadow_color = input.shadow_color; - let base_color = mix( - vec4(shadow_color.x, shadow_color.y, shadow_color.z, 0.0), - quad_color, - quad_color.a - ); - - return mix(base_color, shadow_color, (1.0 - radius_alpha) * shadow_alpha); - } else { - return quad_color; - } -} \ No newline at end of file diff --git a/sugarloaf/src/components/rich_text/batch.rs b/sugarloaf/src/components/rich_text/batch.rs index 617c18176a..a9e6ad9fcc 100644 --- a/sugarloaf/src/components/rich_text/batch.rs +++ b/sugarloaf/src/components/rich_text/batch.rs @@ -800,6 +800,22 @@ impl BatchManager { .add_rect(rect, depth, color, None, None, None, false); } + /// Add a rectangle with color - unified with quad rendering + #[inline] + pub fn add_primitive_rect( + &mut self, + rect: &crate::sugarloaf::primitives::Rect, + depth: f32, + ) { + let batch_rect = Rect { + x: rect.x, + y: rect.y, + width: rect.width, + height: rect.height, + }; + self.add_rect(&batch_rect, depth, &rect.color); + } + #[inline] pub fn build_display_list(&self, list: &mut Vec) { for batch in &self.opaque { diff --git a/sugarloaf/src/components/rich_text/compositor.rs b/sugarloaf/src/components/rich_text/compositor.rs index 7791625f8f..4a3b5ae5e8 100644 --- a/sugarloaf/src/components/rich_text/compositor.rs +++ b/sugarloaf/src/components/rich_text/compositor.rs @@ -113,6 +113,12 @@ impl Compositor { self.batches.build_display_list(vertices); } + /// Add a rectangle - unified quad rendering + #[inline] + pub fn add_rect(&mut self, rect: &crate::sugarloaf::primitives::Rect, depth: f32) { + self.batches.add_primitive_rect(rect, depth); + } + /// Standard draw_run method (for compatibility) #[inline] pub fn draw_run( diff --git a/sugarloaf/src/components/rich_text/mod.rs b/sugarloaf/src/components/rich_text/mod.rs index ffba7f4d17..42b5d90fcd 100644 --- a/sugarloaf/src/components/rich_text/mod.rs +++ b/sugarloaf/src/components/rich_text/mod.rs @@ -322,54 +322,112 @@ impl RichTextBrush { state: &crate::sugarloaf::state::SugarState, graphics: &mut Graphics, ) { - if state.rich_texts.is_empty() { - self.vertices.clear(); + // Always clear vertices first + self.vertices.clear(); + + if state.content.states.is_empty() { return; } self.comp.begin(); let library = state.content.font_library(); - for rich_text in &state.rich_texts { - if let Some(rt) = state.content.get_state(&rich_text.id) { - // Check if this specific rich text needs cache invalidation - match &rt.last_update { - BuilderStateUpdate::Full => { - // For full updates, we don't need to clear text run cache - // as it's shared across all text and font-specific - } - BuilderStateUpdate::Partial(_lines) => { - // For partial updates, we also don't need to clear text run cache - // as individual text runs are still valid - } - BuilderStateUpdate::Noop => { - // Do nothing - } - }; + // Iterate over all content states and render visible ones + for (rich_text_id, builder_state) in &state.content.states { + println!("rich_text_id on prepare {:?}", rich_text_id); + // Skip if marked for removal or hidden + if builder_state.render_data.should_remove || builder_state.render_data.hidden + { + continue; + } - let position = ( - rich_text.position[0] * state.style.scale_factor, - rich_text.position[1] * state.style.scale_factor, - ); + // Skip if there are no lines to render + if builder_state.lines.is_empty() { + continue; + } - self.draw_layout( - rich_text.id, // Pass the rich text ID for caching - &rt.lines, - &rich_text.lines, - Some(position), - library, - Some(&rt.layout), - graphics, - ); + // Skip if all lines are empty (no content) + if builder_state.lines.iter().all(|line| { + line.fragments.is_empty() + || line + .fragments + .iter() + .all(|frag| frag.content.trim().is_empty()) + }) { + continue; } + + // Check if this specific rich text needs cache invalidation + match &builder_state.last_update { + BuilderStateUpdate::Full => { + // For full updates, we don't need to clear text run cache + // as it's shared across all text and font-specific + } + BuilderStateUpdate::Partial(_lines) => { + // For partial updates, we also don't need to clear text run cache + // as individual text runs are still valid + } + BuilderStateUpdate::Noop => { + // Do nothing + } + }; + + let pos = ( + builder_state.render_data.position[0] * state.style.scale_factor, + builder_state.render_data.position[1] * state.style.scale_factor, + ); + + self.draw_layout( + *rich_text_id, // Pass the rich text ID for caching + &builder_state.lines, + &None, // No line range filtering for now + Some(pos), + library, + Some(&builder_state.layout), + graphics, + ); } + self.vertices.clear(); + self.vertices.clear(); self.images.process_atlases(context); self.comp.finish(&mut self.vertices); } #[inline] + /// Get character cell dimensions using font metrics (fast, no rendering) + pub fn get_character_cell_dimensions( + &self, + font_library: &FontLibrary, + font_size: f32, + line_height: f32, + ) -> Option { + // Use read lock instead of write lock since we're not modifying + if let Some(font_library_data) = font_library.inner.try_read() { + let font_id = 0; // FONT_ID_REGULAR + + // Use existing method to get cached metrics + drop(font_library_data); // Drop read lock + let mut font_library_data = font_library.inner.write(); + if let Some((ascent, descent, leading)) = + font_library_data.get_font_metrics(&font_id, font_size) + { + // Calculate character width using font metrics + // For monospace fonts, we can estimate character width + let char_width = font_size * 0.6; // Common monospace width ratio + let total_line_height = (ascent + descent + leading) * line_height; + + return Some(SugarDimensions { + width: char_width.max(1.0), + height: total_line_height.max(1.0), + scale: 1.0, + }); + } + } + None + } + pub fn dimensions( &mut self, font_library: &FontLibrary, @@ -824,6 +882,12 @@ impl RichTextBrush { tracing::info!("RichTextBrush atlas, glyph cache, and text run cache cleared"); } + /// Add a rectangle - unified quad rendering approach + #[inline] + pub fn add_rect(&mut self, rect: &crate::sugarloaf::primitives::Rect, depth: f32) { + self.comp.add_rect(rect, depth); + } + #[inline] pub fn render<'pass>( &'pass mut self, diff --git a/sugarloaf/src/context/metal.rs b/sugarloaf/src/context/metal.rs index f7be67d950..e9570ea668 100644 --- a/sugarloaf/src/context/metal.rs +++ b/sugarloaf/src/context/metal.rs @@ -149,7 +149,7 @@ impl MetalContext { self.scale } - fn supports_f16(&self) -> bool { + pub fn supports_f16(&self) -> bool { self.supports_f16 } } diff --git a/sugarloaf/src/context/mod.rs b/sugarloaf/src/context/mod.rs index bcdb6d04e5..dd77b436ee 100644 --- a/sugarloaf/src/context/mod.rs +++ b/sugarloaf/src/context/mod.rs @@ -68,4 +68,12 @@ impl Context<'_> { } } } + + #[inline] + pub fn supports_f16(&self) -> bool { + match &self.inner { + ContextType::Wgpu(ctx) => ctx.supports_f16(), + ContextType::Metal(ctx) => ctx.supports_f16(), + } + } } diff --git a/sugarloaf/src/layout/content.rs b/sugarloaf/src/layout/content.rs index 6909266cb9..9dbeeb2f3d 100644 --- a/sugarloaf/src/layout/content.rs +++ b/sugarloaf/src/layout/content.rs @@ -116,6 +116,7 @@ pub struct BuilderState { pub last_update: BuilderStateUpdate, scaled_font_size: f32, pub layout: RichTextLayout, + pub render_data: crate::layout::RichTextRenderData, } impl BuilderState { @@ -369,6 +370,13 @@ impl Content { pub fn sel(&mut self, state_id: usize) -> &mut Content { self.selector = Some(state_id); + // Ensure the state exists - create it with default layout if missing + if !self.states.contains_key(&state_id) { + let default_layout = RichTextLayout::default(); + self.states + .insert(state_id, BuilderState::from_layout(&default_layout)); + } + self } @@ -404,11 +412,108 @@ impl Content { #[inline] pub fn create_state(&mut self, rich_text_layout: &RichTextLayout) -> usize { let id = self.counter.next(); - self.states - .insert(id, BuilderState::from_layout(rich_text_layout)); + let mut state = BuilderState::from_layout(rich_text_layout); + + // Immediately calculate dimensions for a representative character + // This ensures we never have zero dimensions + state.layout.dimensions = + self.calculate_character_cell_dimensions(rich_text_layout); + + self.states.insert(id, state); id } + /// Calculate character cell dimensions using swash FontRef directly (fast, no rendering) + fn calculate_character_cell_dimensions( + &self, + layout: &RichTextLayout, + ) -> crate::layout::SugarDimensions { + // Try to get font metrics directly using swash + if let Some(font_library_data) = self.fonts.inner.try_read() { + let font_id = 0; // FONT_ID_REGULAR + let font_size = layout.font_size; + + // Get font data to create swash FontRef + if let Some((font_data, offset, _key)) = font_library_data.get_data(&font_id) + { + // Create swash FontRef directly from font data + if let Some(font_ref) = crate::font_introspector::FontRef::from_index( + &font_data, + offset as usize, + ) { + // Get metrics using swash + let font_metrics = font_ref.metrics(&[]); + + // Calculate character cell width using space character + let char_width = match font_ref.charmap().map(' ' as u32) { + glyph_id => { + // Get advance width for space character using GlyphMetrics + let glyph_metrics = + crate::font_introspector::GlyphMetrics::from_font( + &font_ref, + &[], + ); + let advance = glyph_metrics.advance_width(glyph_id); + + // Scale to font size + let units_per_em = font_metrics.units_per_em as f32; + let scale_factor = font_size / units_per_em; + + if advance > 0.0 { + advance * scale_factor + } else { + // Fallback: approximate monospace character width + font_size * 0.6 + } + } + }; + + // Calculate line height using scaled metrics + let units_per_em = font_metrics.units_per_em as f32; + let scale_factor = font_size / units_per_em; + let ascent = font_metrics.ascent * scale_factor; + let descent = font_metrics.descent.abs() * scale_factor; + let leading = font_metrics.leading * scale_factor; + let line_height = (ascent + descent + leading) * layout.line_height; + + println!("calculate_character_cell_dimensions:"); + println!(" font_size: {}", font_size); + println!(" char_width (logical): {}", char_width); + println!(" line_height (logical): {}", line_height); + println!(" layout.dimensions.scale: {}", layout.dimensions.scale); + + // Scale to physical pixels to match what the brush returns + let char_width_physical = char_width * layout.dimensions.scale; + let line_height_physical = line_height * layout.dimensions.scale; + + // Return dimensions in physical pixels (matching brush behavior) + let result = crate::layout::SugarDimensions { + width: char_width_physical.max(1.0), + height: line_height_physical.max(1.0), + scale: layout.dimensions.scale, + }; + + println!(" -> Returning dimensions (physical): width={}, height={}, scale={}", + result.width, result.height, result.scale); + + return result; + } + } + } + + println!("calculate_character_cell_dimensions: Using fallback"); + // Fallback to reasonable defaults if font metrics unavailable + // Return in physical pixels to match brush behavior + let fallback_width = (layout.font_size * 0.6).max(8.0); + let fallback_height = (layout.font_size * layout.line_height).max(16.0); + + crate::layout::SugarDimensions { + width: fallback_width * layout.dimensions.scale, + height: fallback_height * layout.dimensions.scale, + scale: layout.dimensions.scale, + } + } + #[inline] pub fn remove_state(&mut self, rich_text_id: &usize) { self.states.remove(rich_text_id); @@ -429,6 +534,12 @@ impl Content { ) { let mut content = Content::new(&self.fonts); if let Some(rte) = self.states.get_mut(state_id) { + println!("update_dimensions {:?}", state_id); + println!(" Before: width={}, height={}, scale={}", + rte.layout.dimensions.width, + rte.layout.dimensions.height, + rte.layout.dimensions.scale); + let id = content.create_state(&rte.layout); content .sel(id) @@ -442,8 +553,23 @@ impl Content { &render_data, &mut Graphics::default(), ) { + println!( + " Brush returned dimensions: {}x{} (scale={})", + dimension.width, dimension.height, dimension.scale + ); + + // Preserve the scale from the layout (window DPI scale) + // The brush only calculates width/height rte.layout.dimensions.height = dimension.height; rte.layout.dimensions.width = dimension.width; + // rte.layout.dimensions.scale stays unchanged (preserves window DPI scale) + + println!(" After: width={}, height={}, scale={}", + rte.layout.dimensions.width, + rte.layout.dimensions.height, + rte.layout.dimensions.scale); + } else { + println!("Failed to get dimensions for state {}", state_id); } } } diff --git a/sugarloaf/src/layout/mod.rs b/sugarloaf/src/layout/mod.rs index 186a8a63ba..e6baf20e53 100644 --- a/sugarloaf/src/layout/mod.rs +++ b/sugarloaf/src/layout/mod.rs @@ -7,12 +7,14 @@ // nav and span_style were originally retired from dfrg/swash_demo licensed under MIT // https://github.com/dfrg/swash_demo/blob/master/LICENSE -mod content; +pub mod content; mod glyph; mod render_data; +pub mod rich_text_render_data; pub use glyph::Glyph; pub use render_data::RenderData; +pub use rich_text_render_data::RichTextRenderData; pub use content::{ BuilderLine, BuilderState, BuilderStateUpdate, Content, FragmentStyle, @@ -38,13 +40,23 @@ pub struct Delta { pub bottom_y: T, } -#[derive(Copy, Clone, Debug, PartialEq, Default)] +#[derive(Copy, Clone, Debug, PartialEq)] pub struct SugarDimensions { pub width: f32, pub height: f32, pub scale: f32, } +impl Default for SugarDimensions { + fn default() -> Self { + Self { + width: 8.0, // Reasonable character cell width fallback + height: 16.0, // Reasonable character cell height fallback + scale: 1.0, + } + } +} + #[derive(Debug, PartialEq, Copy, Clone)] pub struct RichTextLayout { pub line_height: f32, diff --git a/sugarloaf/src/layout/rich_text_render_data.rs b/sugarloaf/src/layout/rich_text_render_data.rs new file mode 100644 index 0000000000..c6f872bc49 --- /dev/null +++ b/sugarloaf/src/layout/rich_text_render_data.rs @@ -0,0 +1,66 @@ +// Copyright (c) 2023-present, Raphael Amorim. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +/// Rendering metadata for rich text elements +#[derive(Debug, Clone)] +pub struct RichTextRenderData { + /// Position where the rich text should be rendered [x, y] + pub position: [f32; 2], + /// Whether this rich text should be hidden during rendering + pub hidden: bool, + /// Whether this rich text needs to be repainted + pub needs_repaint: bool, + /// Whether this rich text should be removed + pub should_remove: bool, +} + +impl Default for RichTextRenderData { + fn default() -> Self { + Self { + position: [0.0, 0.0], + hidden: false, // Visible by default + needs_repaint: true, // Should paint initially + should_remove: false, + } + } +} + +impl RichTextRenderData { + /// Create new render data with position + pub fn new(x: f32, y: f32) -> Self { + Self { + position: [x, y], + hidden: false, + needs_repaint: true, + should_remove: false, + } + } + + /// Update position and mark for repaint if changed + pub fn set_position(&mut self, x: f32, y: f32) { + if self.position[0] != x || self.position[1] != y { + self.position = [x, y]; + self.needs_repaint = true; + } + } + + /// Set visibility and mark for repaint if changed + pub fn set_hidden(&mut self, hidden: bool) { + if self.hidden != hidden { + self.hidden = hidden; + self.needs_repaint = true; + } + } + + /// Mark for removal + pub fn mark_for_removal(&mut self) { + self.should_remove = true; + } + + /// Clear repaint flag after rendering + pub fn clear_repaint_flag(&mut self) { + self.needs_repaint = false; + } +} diff --git a/sugarloaf/src/lib.rs b/sugarloaf/src/lib.rs index 09787e3f9d..24970973a8 100644 --- a/sugarloaf/src/lib.rs +++ b/sugarloaf/src/lib.rs @@ -15,12 +15,14 @@ pub use crate::sugarloaf::{ ColorType, Graphic, GraphicData, GraphicId, Graphics, ResizeCommand, ResizeParameter, MAX_GRAPHIC_DIMENSIONS, }, - primitives::*, + primitives::{ + contains_braille_dot, drawable_character, DrawableChar, ImageProperties, Object, + Rect, RichText, RichTextLinesRange, RichTextRenderData, SugarCursor, + }, Colorspace, Sugarloaf, SugarloafBackend, SugarloafErrors, SugarloafRenderer, SugarloafWindow, SugarloafWindowSize, SugarloafWithErrors, }; pub use components::filters::Filter; -pub use components::quad::Quad; pub use layout::{ Content, FragmentStyle, FragmentStyleDecoration, SugarDimensions, UnderlineInfo, UnderlineShape, diff --git a/sugarloaf/src/sugarloaf.rs b/sugarloaf/src/sugarloaf.rs index 11c61da2c3..2906178fa4 100644 --- a/sugarloaf/src/sugarloaf.rs +++ b/sugarloaf/src/sugarloaf.rs @@ -2,18 +2,16 @@ pub mod graphics; pub mod primitives; pub mod state; -use crate::components::core::{image::Handle, shapes::Rectangle}; +use crate::components::core::image::Handle; use crate::components::filters::{Filter, FiltersBrush}; -use crate::components::layer::{self, LayerBrush}; -use crate::components::quad::QuadBrush; use crate::components::rich_text::RichTextBrush; use crate::font::{fonts::SugarloafFont, FontLibrary}; use crate::layout::{RichTextLayout, RootStyle}; -use crate::sugarloaf::graphics::{BottomLayer, Graphics}; -use crate::sugarloaf::layer::types; +use crate::sugarloaf::graphics::Graphics; + +use crate::context::Context; use crate::Content; use crate::SugarDimensions; -use crate::{context::Context, Object, Quad}; use core::fmt::{Debug, Formatter}; use primitives::ImageProperties; use raw_window_handle::{ @@ -23,7 +21,6 @@ use state::SugarState; pub struct Sugarloaf<'a> { pub ctx: Context<'a>, - quad_brush: QuadBrush, rich_text_brush: RichTextBrush, // layer_brush: LayerBrush, state: state::SugarState, @@ -156,14 +153,12 @@ impl Sugarloaf<'_> { let ctx = Context::new(window, renderer); // let layer_brush = LayerBrush::new(&ctx); - let quad_brush = QuadBrush::new(&ctx); let rich_text_brush = RichTextBrush::new(&ctx); let state = SugarState::new(layout, font_library, &font_features); let instance = Sugarloaf { state, // layer_brush, - quad_brush, ctx, background_color: Some(wgpu::Color::BLACK), background_image: None, @@ -311,9 +306,30 @@ impl Sugarloaf<'_> { self.state.content() } + /// Add a rectangle directly to the rendering pipeline + #[inline] + pub fn add_rect(&mut self, x: f32, y: f32, width: f32, height: f32, color: [f32; 4]) { + let rect = crate::sugarloaf::primitives::Rect::new(x, y, width, height, color); + self.rich_text_brush.add_rect(&rect, 0.0); + } + + /// Show a rich text at a specific position + #[inline] + pub fn show_rich_text(&mut self, id: usize, x: f32, y: f32) { + self.state + .set_rich_text_visibility_and_position(id, x, y, false); + } + + /// Hide a rich text + #[inline] + pub fn hide_rich_text(&mut self, id: usize) { + self.state.set_rich_text_hidden(id, true); + } + + /// Show/hide a rich text #[inline] - pub fn set_objects(&mut self, objects: Vec) { - self.state.compute_objects(objects); + pub fn set_rich_text_visibility(&mut self, id: usize, hidden: bool) { + self.state.set_rich_text_hidden(id, hidden); } #[inline] @@ -321,6 +337,11 @@ impl Sugarloaf<'_> { self.state.get_state_layout(id) } + #[inline] + pub fn force_update_dimensions(&mut self, id: &usize) { + self.state.content.update_dimensions(id, &mut self.rich_text_brush); + } + #[inline] pub fn get_rich_text_dimensions(&mut self, id: &usize) -> SugarDimensions { self.state @@ -379,7 +400,6 @@ impl Sugarloaf<'_> { self.state.compute_dimensions(&mut self.rich_text_brush); self.state.compute_updates( &mut self.rich_text_brush, - &mut self.quad_brush, &mut self.ctx, &mut self.graphics, ); @@ -441,8 +461,6 @@ impl Sugarloaf<'_> { command_buffer.new_render_command_encoder(&render_pass_descriptor); render_encoder.set_label("Sugarloaf Metal Render Pass"); - self.quad_brush - .render_metal(ctx, &self.state, &render_encoder); self.rich_text_brush.render_metal(ctx, &render_encoder); render_encoder.end_encoding(); @@ -536,7 +554,6 @@ impl Sugarloaf<'_> { // self.layer_brush.render(request, &mut rpass, None); // } // } - self.quad_brush.render_wgpu(ctx, &self.state, &mut rpass); self.rich_text_brush.render(ctx, &mut rpass); } diff --git a/sugarloaf/src/sugarloaf/graphics.rs b/sugarloaf/src/sugarloaf/graphics.rs index d9c729a822..be31e2ee55 100644 --- a/sugarloaf/src/sugarloaf/graphics.rs +++ b/sugarloaf/src/sugarloaf/graphics.rs @@ -3,7 +3,7 @@ // This source code is licensed under the MIT license found in the // LICENSE file in the root directory of this source tree. -use crate::sugarloaf::types; +use crate::components::layer::types; use crate::sugarloaf::Handle; use image_rs::DynamicImage; use rustc_hash::FxHashMap; diff --git a/sugarloaf/src/sugarloaf/primitives.rs b/sugarloaf/src/sugarloaf/primitives.rs index 29fb9c3db9..c306992d9c 100644 --- a/sugarloaf/src/sugarloaf/primitives.rs +++ b/sugarloaf/src/sugarloaf/primitives.rs @@ -3,7 +3,6 @@ // This source code is licensed under the MIT license found in the // LICENSE file in the root directory of this source tree. -use crate::Quad; use serde::Deserialize; #[derive(Debug, PartialEq, Copy, Clone)] @@ -34,16 +33,82 @@ pub struct RichTextLinesRange { pub end: usize, } +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct RichTextRenderData { + pub position: [f32; 2], + pub should_repaint: bool, + pub should_remove: bool, + pub hidden: bool, +} + +impl Default for RichTextRenderData { + fn default() -> Self { + Self { + position: [0.0, 0.0], + should_repaint: false, + should_remove: false, + hidden: false, + } + } +} + #[derive(Clone, Copy, Debug, PartialEq)] pub struct RichText { pub id: usize, - pub position: [f32; 2], pub lines: Option, + pub render_data: RichTextRenderData, +} + +impl RichText { + pub fn new(id: usize) -> Self { + Self { + id, + lines: None, + render_data: RichTextRenderData::default(), + } + } + + pub fn with_position(mut self, x: f32, y: f32) -> Self { + self.render_data.position = [x, y]; + self + } + + pub fn with_lines(mut self, start: usize, end: usize) -> Self { + self.lines = Some(RichTextLinesRange { start, end }); + self + } + + pub fn hidden(mut self, hidden: bool) -> Self { + self.render_data.hidden = hidden; + self + } +} + +/// Simple rectangle for rendering - replaces the complex Quad +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Rect { + pub x: f32, + pub y: f32, + pub width: f32, + pub height: f32, + pub color: [f32; 4], +} + +impl Rect { + pub fn new(x: f32, y: f32, width: f32, height: f32, color: [f32; 4]) -> Self { + Self { + x, + y, + width, + height, + color, + } + } } #[derive(Clone, Debug, PartialEq)] pub enum Object { - Quad(Quad), + Rect(Rect), RichText(RichText), } diff --git a/sugarloaf/src/sugarloaf/state.rs b/sugarloaf/src/sugarloaf/state.rs index e68c63cfeb..5e1dcd725b 100644 --- a/sugarloaf/src/sugarloaf/state.rs +++ b/sugarloaf/src/sugarloaf/state.rs @@ -4,22 +4,16 @@ // LICENSE file in the root directory of this source tree. use crate::font::FontLibrary; -use crate::layout::RootStyle; -use crate::sugarloaf::QuadBrush; -use crate::sugarloaf::{RichTextBrush, RichTextLayout}; +use crate::layout::{RichTextLayout, RootStyle}; +use crate::sugarloaf::RichTextBrush; use crate::Graphics; -use crate::{Content, Object, Quad, RichText, SugarDimensions}; -use std::collections::HashSet; +use crate::{Content, SugarDimensions}; pub struct SugarState { - objects: Vec, - pub rich_texts: Vec, - rich_text_repaint: HashSet, - rich_text_to_be_removed: Vec, + // Rich text metadata now managed directly in content.states[].render_data pub style: RootStyle, pub content: Content, - pub quads: Vec, - pub visual_bell_overlay: Option, + pub visual_bell_overlay: Option, } impl SugarState { @@ -28,18 +22,13 @@ impl SugarState { font_library: &FontLibrary, font_features: &Option>, ) -> SugarState { - let mut content = Content::new(font_library); let found_font_features = SugarState::found_font_features(font_features); + let mut content = Content::new(font_library); content.set_font_features(found_font_features); SugarState { - content: Content::new(font_library), - quads: vec![], + content, style, - objects: vec![], - rich_texts: vec![], - rich_text_to_be_removed: vec![], - rich_text_repaint: HashSet::default(), visual_bell_overlay: None, } } @@ -47,69 +36,73 @@ impl SugarState { pub fn found_font_features( font_features: &Option>, ) -> Vec> { - let mut found_font_features = vec![]; - if let Some(features) = font_features { - for feature in features { - let setting: crate::font_introspector::Setting = - (feature.as_str(), 1).into(); - found_font_features.push(setting); - } - } + // Simplified for now - TODO: Implement proper font feature parsing + vec![] + } - found_font_features + #[inline] + pub fn contains_rich_text(&self, rich_text_id: &usize) -> bool { + self.content.states.contains_key(rich_text_id) } #[inline] pub fn new_layer(&mut self) {} + #[inline] + pub fn content(&mut self) -> &mut Content { + &mut self.content + } + #[inline] pub fn get_state_layout(&self, id: &usize) -> RichTextLayout { - if let Some(builder_state) = self.content.get_state(id) { - return builder_state.layout; + if let Some(state) = self.content.get_state(id) { + println!( + "get_state_layout returning dimensions {}x{} for state {}", + state.layout.dimensions.width, state.layout.dimensions.height, id + ); + state.layout.clone() + } else { + println!( + "get_state_layout: state {} not found, returning default", + id + ); + RichTextLayout::from_default_layout(&self.style) } - - RichTextLayout::from_default_layout(&self.style) } #[inline] - pub fn compute_layout_rescale( + pub fn get_rich_text_dimensions( &mut self, - scale: f32, + id: &usize, advance_brush: &mut RichTextBrush, - ) { - self.style.scale_factor = scale; - for (id, state) in &mut self.content.states { - state.rescale(scale); - state.layout.dimensions.height = 0.0; - state.layout.dimensions.width = 0.0; - - self.rich_text_repaint.insert(*id); + ) -> SugarDimensions { + // Mark for repaint directly in render_data + if let Some(state) = self.content.states.get_mut(id) { + state.render_data.needs_repaint = true; } - self.process_rich_text_repaint(advance_brush); + if let Some(state) = self.content.get_state(id) { + let layout = &state.layout; + SugarDimensions { + scale: layout.dimensions.scale, // Use the actual scale, not font_size/line_height! + width: layout.dimensions.width, + height: layout.dimensions.height, + } + } else { + SugarDimensions::default() + } } #[inline] - pub fn set_rich_text_font_size( - &mut self, - rich_text_id: &usize, - font_size: f32, - advance_brush: &mut RichTextBrush, - ) { - if let Some(rte) = self.content.get_state_mut(rich_text_id) { - rte.layout.font_size = font_size; - rte.update_font_size(); - - rte.layout.dimensions.height = 0.0; - rte.layout.dimensions.width = 0.0; - self.rich_text_repaint.insert(*rich_text_id); + pub fn clean_screen(&mut self) { + // Mark all rich text states for removal - they'll be re-added by route screen functions + for (_, state) in self.content.states.iter_mut() { + state.render_data.mark_for_removal(); } - - self.process_rich_text_repaint(advance_brush); } #[inline] - pub fn set_rich_text_font_size_based_on_action( + pub fn update_rich_text_style( &mut self, rich_text_id: &usize, operation: u8, @@ -126,74 +119,76 @@ impl SugarState { if should_update { rte.layout.dimensions.height = 0.0; rte.layout.dimensions.width = 0.0; - self.rich_text_repaint.insert(*rich_text_id); + // Mark for repaint directly in render_data + if let Some(state) = self.content.states.get_mut(rich_text_id) { + state.render_data.needs_repaint = true; + } } } - self.process_rich_text_repaint(advance_brush); + self.compute_dimensions(advance_brush); } #[inline] - pub fn set_rich_text_line_height(&mut self, rich_text_id: &usize, line_height: f32) { - if let Some(rte) = self.content.get_state_mut(rich_text_id) { - rte.layout.line_height = line_height; - } - } + pub fn compute_dimensions(&mut self, advance_brush: &mut RichTextBrush) { + // Collect IDs that need repaint first + let ids_to_repaint: Vec = self + .content + .states + .iter() + .filter_map(|(id, state)| { + if state.render_data.needs_repaint { + Some(*id) + } else { + None + } + }) + .collect(); - fn process_rich_text_repaint(&mut self, advance_brush: &mut RichTextBrush) { - for rich_text in &self.rich_text_repaint { - self.content.update_dimensions(rich_text, advance_brush); + // If nothing needs repainting, return early + if ids_to_repaint.is_empty() { + return; } - self.rich_text_repaint.clear(); - } - - #[inline] - pub fn set_fonts(&mut self, fonts: &FontLibrary, advance_brush: &mut RichTextBrush) { - self.content.set_font_library(fonts); - for (id, state) in &mut self.content.states { - state.layout.dimensions.height = 0.0; - state.layout.dimensions.width = 0.0; - self.rich_text_repaint.insert(*id); + // Process each ID + for rich_text_id in &ids_to_repaint { + self.content.update_dimensions(rich_text_id, advance_brush); } - self.process_rich_text_repaint(advance_brush); - } - - #[inline] - pub fn set_font_features(&mut self, font_features: &Option>) { - let found_font_features = SugarState::found_font_features(font_features); - self.content.set_font_features(found_font_features); + // Clear repaint flags after processing + for id in ids_to_repaint { + if let Some(state) = self.content.states.get_mut(&id) { + state.render_data.clear_repaint_flag(); + } + } } - #[inline] - pub fn clean_screen(&mut self) { - // self.content.clear(); - self.objects.clear(); + pub fn compute_updates( + &mut self, + advance_brush: &mut RichTextBrush, + context: &mut super::Context, + graphics: &mut Graphics, + ) { + advance_brush.prepare(context, self, graphics); + // Rectangles are now rendered directly via add_rect() calls + // No object processing needed anymore } #[inline] - pub fn compute_objects(&mut self, new_objects: Vec) { - // Block are used only with elementary renderer - let mut rich_texts: Vec = vec![]; - for obj in &new_objects { - if let Object::RichText(rich_text) = obj { - rich_texts.push(*rich_text); + pub fn reset(&mut self) { + // Remove states marked for removal and clear repaint flags + let mut to_remove = Vec::new(); + for (id, state) in &self.content.states { + if state.render_data.should_remove { + to_remove.push(*id); } } - self.objects = new_objects; - self.rich_texts = rich_texts - } - #[inline] - pub fn reset(&mut self) { - self.quads.clear(); - for rte_id in &self.rich_text_to_be_removed { - self.content.remove_state(rte_id); + for id in to_remove { + self.content.remove_state(&id); } self.content.mark_states_clean(); - self.rich_text_to_be_removed.clear(); } #[inline] @@ -203,8 +198,13 @@ impl SugarState { #[inline] pub fn create_rich_text(&mut self) -> usize { - self.content - .create_state(&RichTextLayout::from_default_layout(&self.style)) + let layout = RichTextLayout::from_default_layout(&self.style); + let id = self.content.create_state(&layout); + + // Dimensions are now calculated eagerly during create_state + // No need to mark for repaint since we have valid dimensions immediately + + id } #[inline] @@ -212,81 +212,86 @@ impl SugarState { let id = self .content .create_state(&RichTextLayout::from_default_layout(&self.style)); - self.rich_text_to_be_removed.push(id); + // Mark as temporary (for removal) directly in render_data + if let Some(state) = self.content.states.get_mut(&id) { + state.render_data.should_remove = true; + } id } - pub fn content(&mut self) -> &mut Content { - &mut self.content + pub fn set_rich_text_visibility_and_position( + &mut self, + id: usize, + x: f32, + y: f32, + hidden: bool, + ) { + if let Some(state) = self.content.states.get_mut(&id) { + state.render_data.set_position(x, y); + state.render_data.set_hidden(hidden); + state.render_data.should_remove = false; // Ensure it's not marked for removal + } } #[inline] - pub fn compute_updates( + pub fn set_rich_text_hidden(&mut self, id: usize, hidden: bool) { + if let Some(state) = self.content.states.get_mut(&id) { + state.render_data.set_hidden(hidden); + } + } + + pub fn set_visual_bell_overlay( &mut self, - advance_brush: &mut RichTextBrush, - quad_brush: &mut QuadBrush, - context: &mut super::Context, - graphics: &mut Graphics, + overlay: Option, ) { - advance_brush.prepare(context, self, graphics); - quad_brush.resize(context); - - // Elementary renderer is used for everything else in sugarloaf - // like objects rendering (created by .text() or .append_rects()) - // It means that's either the first render or objects were erased on compute_diff() step - for object in &self.objects { - match object { - Object::Quad(composed_quad) => { - self.quads.push(*composed_quad); - } - Object::RichText(_rich_text) => { - // self.rich_texts.push(*rich_text); - } - } - } + self.visual_bell_overlay = overlay; } #[inline] - pub fn get_rich_text_dimensions( + pub fn set_fonts( &mut self, - id: &usize, - advance_brush: &mut RichTextBrush, - ) -> SugarDimensions { - self.content.update_dimensions(id, advance_brush); - if let Some(rte) = self.content.get_state(id) { - return rte.layout.dimensions; - } - - SugarDimensions::default() + _font_library: &FontLibrary, + _advance_brush: &mut RichTextBrush, + ) { + // Simplified - fonts are handled elsewhere in the unified system } #[inline] - pub fn compute_dimensions(&mut self, advance_brush: &mut RichTextBrush) { - // If sugar dimensions are empty then need to find it - for rich_text in &self.rich_texts { - if let Some(rte) = self.content.get_state(&rich_text.id) { - if rte.layout.dimensions.width == 0.0 - || rte.layout.dimensions.height == 0.0 - { - self.rich_text_repaint.insert(rich_text.id); - - tracing::info!("has empty dimensions, will try to find..."); - } - } - } + pub fn set_rich_text_font_size_based_on_action( + &mut self, + rich_text_id: &usize, + operation: u8, + advance_brush: &mut RichTextBrush, + ) { + self.update_rich_text_style(rich_text_id, operation, advance_brush); + } - if self.rich_text_repaint.is_empty() { - return; - } - for rich_text in &self.rich_text_repaint { - self.content.update_dimensions(rich_text, advance_brush); + #[inline] + pub fn set_rich_text_font_size( + &mut self, + rt_id: &usize, + _font_size: f32, + advance_brush: &mut RichTextBrush, + ) { + // Mark for repaint directly in render_data + if let Some(state) = self.content.states.get_mut(rt_id) { + state.render_data.needs_repaint = true; } + self.compute_dimensions(advance_brush); + } - self.rich_text_repaint.clear(); + #[inline] + pub fn set_rich_text_line_height(&mut self, _rt_id: &usize, _line_height: f32) { + // Simplified - line height changes handled elsewhere } #[inline] - pub fn set_visual_bell_overlay(&mut self, overlay: Option) { - self.visual_bell_overlay = overlay; + pub fn compute_layout_rescale( + &mut self, + _scale: f32, + advance_brush: &mut RichTextBrush, + ) { + // Simplified - rescaling handled elsewhere + self.compute_dimensions(advance_brush); } }