diff --git a/frontends/rioterm/src/application.rs b/frontends/rioterm/src/application.rs index 6cfaf25e..7219034d 100644 --- a/frontends/rioterm/src/application.rs +++ b/frontends/rioterm/src/application.rs @@ -1510,12 +1510,11 @@ impl ApplicationHandler for Application<'_> { match delta { MouseScrollDelta::LineDelta(columns, lines) => { - let current_id = route.window.screen.ctx().current().rich_text_id; - if let Some(layout) = - route.window.screen.sugarloaf.get_text_layout(¤t_id) - { - let new_scroll_px_x = columns * layout.font_size; - let new_scroll_px_y = lines * layout.font_size; + let font_size = + route.window.screen.ctx().current().dimension.font_size; + if font_size > 0.0 { + let new_scroll_px_x = columns * font_size; + let new_scroll_px_y = lines * font_size; route .window .screen diff --git a/frontends/rioterm/src/context/mod.rs b/frontends/rioterm/src/context/mod.rs index cc6f7695..728ca7fd 100644 --- a/frontends/rioterm/src/context/mod.rs +++ b/frontends/rioterm/src/context/mod.rs @@ -22,7 +22,7 @@ use rio_backend::error::{RioError, RioErrorLevel, RioErrorType}; use rio_backend::event::EventListener; use rio_backend::event::WindowId; use rio_backend::selection::SelectionRange; -use rio_backend::sugarloaf::{font::SugarloafFont, Object, Sugarloaf, SugarloafErrors}; +use rio_backend::sugarloaf::{font::SugarloafFont, Rect, Sugarloaf, SugarloafErrors}; use std::borrow::Cow; use std::error::Error; use std::sync::atomic::{AtomicUsize, Ordering}; @@ -790,7 +790,7 @@ impl ContextManager { /// Get panel borders for the current grid (returns empty vec if single panel) #[inline] - pub fn get_panel_borders(&self) -> Vec { + pub fn get_panel_borders(&self) -> Vec { self.contexts[self.current_index].get_panel_borders() } diff --git a/frontends/rioterm/src/context/title.rs b/frontends/rioterm/src/context/title.rs index 8d62d143..78ea2ead 100644 --- a/frontends/rioterm/src/context/title.rs +++ b/frontends/rioterm/src/context/title.rs @@ -296,6 +296,7 @@ pub mod test { face_y: 0.0, }, 1.0, + 14.0, Margin::default(), ); @@ -355,6 +356,7 @@ pub mod test { face_y: 0.0, }, 1.0, + 14.0, Margin::default(), ); diff --git a/frontends/rioterm/src/layout/compute_tests.rs b/frontends/rioterm/src/layout/compute_tests.rs index 6ceaf272..266737bf 100644 --- a/frontends/rioterm/src/layout/compute_tests.rs +++ b/frontends/rioterm/src/layout/compute_tests.rs @@ -274,6 +274,7 @@ fn test_context_dimension_build() { dims, cell_for(dims), 1.0, + 14.0, Margin::all(0.0), ); assert_eq!(cd.columns, 103); @@ -293,6 +294,7 @@ fn test_context_dimension_update_width() { dims, cell_for(dims), 1.0, + 14.0, Margin::all(0.0), ); assert_eq!(cd.columns, 100); @@ -315,6 +317,7 @@ fn test_context_dimension_update_height() { dims, cell_for(dims), 1.0, + 14.0, Margin::all(0.0), ); assert_eq!(cd.lines, 25); @@ -337,6 +340,7 @@ fn test_context_dimension_update_dimensions() { dims, cell_for(dims), 1.0, + 14.0, Margin::all(0.0), ); assert_eq!(cd.lines, 25); diff --git a/frontends/rioterm/src/layout/mod.rs b/frontends/rioterm/src/layout/mod.rs index c50e91e0..0e36e287 100644 --- a/frontends/rioterm/src/layout/mod.rs +++ b/frontends/rioterm/src/layout/mod.rs @@ -6,7 +6,7 @@ use crate::mouse::Mouse; use rio_backend::config::layout::Margin; use rio_backend::crosswords::grid::Dimensions; use rio_backend::event::EventListener; -use rio_backend::sugarloaf::{layout::TextDimensions, Object, Rect, Sugarloaf}; +use rio_backend::sugarloaf::{layout::TextDimensions, Rect, Sugarloaf}; use rustc_hash::FxHashMap; use taffy::{ @@ -85,8 +85,8 @@ fn compute( } #[inline] -fn create_border(color: [f32; 4], position: [f32; 2], size: [f32; 2]) -> Object { - Object::Rect(Rect::new(position[0], position[1], size[0], size[1], color)) +fn create_border(color: [f32; 4], position: [f32; 2], size: [f32; 2]) -> Rect { + Rect::new(position[0], position[1], size[0], size[1], color) } /// Separator configuration for split panels @@ -415,7 +415,7 @@ impl ContextGrid { } /// Get separator lines between adjacent panels for rendering. - pub fn get_panel_borders(&self) -> Vec { + pub fn get_panel_borders(&self) -> Vec { if !self.should_draw_borders() { return vec![]; } @@ -916,20 +916,11 @@ impl ContextGrid { crate::renderer::utils::terminal_dimensions(&item.val.dimension); let _ = item.val.messenger.send_resize(winsize); - // Update position via sugarloaf (handles scaling) - sugarloaf.set_position(item.val.rich_text_id, x, y); - - // Set clipping bounds for multi-panel text overflow prevention - if is_multi_panel { - let bounds_x = abs_x + self.scaled_margin.left; - let bounds_y = abs_y + self.scaled_margin.top; - sugarloaf.set_bounds( - item.val.rich_text_id, - Some([bounds_x, bounds_y, width, height]), - ); - } else { - sugarloaf.set_bounds(item.val.rich_text_id, None); - } + // Panel position / clipping bounds are tracked rio-side + // now; the grid pass reads `panel_rect` from the renderer's + // own per-panel iteration. Sugarloaf no longer carries + // panel metadata. + let _ = (x, y, abs_x, abs_y, width, height, is_multi_panel); } true } @@ -1167,6 +1158,7 @@ impl ContextGrid { current_context_dimension.dimension, current_context_dimension.cell, current_context_dimension.line_height, + current_context_dimension.font_size, unscaled_margin, ) } else { @@ -1186,13 +1178,22 @@ impl ContextGrid { } pub fn update_dimensions(&mut self, sugarloaf: &mut Sugarloaf) { + // Per-panel cell metrics are recomputed locally now — sugarloaf + // is consulted only for the font library it owns. Each panel's + // `dimension.font_size` / `line_height` / `dimension.scale` + // drive the result, so panels with per-panel zoom keep + // independent cell strides. for context in self.inner.values_mut() { - if let Some(layout) = sugarloaf.get_text_layout(&context.val.rich_text_id) { - context - .val - .dimension - .update_dimensions(layout.dimensions, layout.cell); + let dim = &mut context.val.dimension; + if dim.font_size <= 0.0 { + continue; } + let (text_dims, cell) = sugarloaf.compute_cell_metrics( + dim.font_size, + dim.line_height, + dim.dimension.scale, + ); + dim.update_dimensions(text_dims, cell); } // Always apply Taffy layout for consistent positioning @@ -1283,9 +1284,10 @@ impl ContextGrid { // Remove from inner map self.inner.remove(&to_remove); - // Cleanup rich text from sugarloaf + // Drop image overlays for the removed panel — sugarloaf has + // no other panel state to clean up post-Content removal. if let Some(id) = rich_text_id { - sugarloaf.remove_content(id); + sugarloaf.clear_image_overlays_for(id); } // Update root if necessary @@ -1544,17 +1546,29 @@ impl ContextGrid { false } + /// Hide the panel set by clearing per-panel image overlays. The + /// `visible=true` case is a no-op — the next `Renderer::run` will + /// repopulate overlays naturally for whichever tab/group is + /// active. (Naming preserved for callers; the function used to + /// drive sugarloaf's content visibility flag, which is gone.) #[inline] - pub fn set_all_rich_text_visibility(&self, sugarloaf: &mut Sugarloaf, hidden: bool) { + pub fn set_all_rich_text_visibility(&self, sugarloaf: &mut Sugarloaf, visible: bool) { + if visible { + return; + } for item in self.inner.values() { - sugarloaf.set_visibility(item.val.rich_text_id, hidden); + sugarloaf.clear_image_overlays_for(item.val.rich_text_id); } } + /// Drop image overlays for every panel in the grid. Used on tab + /// teardown — the panels themselves go away with the + /// `ContextManager`; only the kitty graphics state needs an + /// explicit cleanup signal. #[inline] pub fn remove_all_rich_text(&self, sugarloaf: &mut Sugarloaf) { for item in self.inner.values() { - sugarloaf.remove_content(item.val.rich_text_id); + sugarloaf.clear_image_overlays_for(item.val.rich_text_id); } } } @@ -1568,6 +1582,17 @@ pub struct ContextDimension { pub dimension: TextDimensions, pub margin: Margin, pub line_height: f32, + /// Logical-point font size for this panel. Per-panel zoom updates + /// here; the global `RootStyle.font_size` is the default that new + /// panels inherit at create time. + pub font_size: f32, + /// Font size at panel creation (or last `update_config`). Drives + /// the "reset font size" action so zoom in/out always returns to + /// the user's configured size. + pub original_font_size: f32, + /// `font_size * scale_factor` — physical-pixel size used by the + /// grid emit path to drive glyph rasterization. + pub scaled_font_size: f32, /// Canonical cell metrics — single source of truth shared by the /// GPU grid uniform, col/row count math, and mouse hit testing. /// Rounded `u32` cell width / height / baseline plus unrounded @@ -1584,6 +1609,9 @@ impl Default for ContextDimension { columns: MIN_COLS, lines: MIN_LINES, line_height: 1., + font_size: 0., + original_font_size: 0., + scaled_font_size: 0., dimension: TextDimensions::default(), margin: Margin::default(), cell: rio_backend::sugarloaf::layout::CellMetrics::default(), @@ -1598,6 +1626,7 @@ impl ContextDimension { dimension: TextDimensions, cell: rio_backend::sugarloaf::layout::CellMetrics, line_height: f32, + font_size: f32, margin: Margin, ) -> Self { let (columns, lines) = compute(width, height, cell, margin, dimension.scale); @@ -1609,6 +1638,9 @@ impl ContextDimension { dimension, margin, line_height, + font_size, + original_font_size: font_size, + scaled_font_size: font_size * dimension.scale, cell, } } @@ -1631,6 +1663,61 @@ impl ContextDimension { self.update(); } + #[inline] + pub fn update_font_size(&mut self, font_size: f32) { + self.font_size = font_size; + self.scaled_font_size = font_size * self.dimension.scale; + // Caller is responsible for re-running `compute_cell_metrics` + // and feeding the result back via `update_dimensions` — + // `font_size` alone doesn't change the cell stride, the + // recomputed metrics do. + } + + /// Re-baseline the font size — both current and "original". + /// Called from `update_config` so a config edit becomes the new + /// reset target. Per-panel zoom (`change_font_size`) uses + /// `update_font_size` instead so the original stays put. + #[inline] + pub fn rebaseline_font_size(&mut self, font_size: f32) { + self.font_size = font_size; + self.original_font_size = font_size; + self.scaled_font_size = font_size * self.dimension.scale; + } + + /// Increment the panel's font size by 1 logical point. Returns + /// `true` if the size changed (i.e. wasn't already at the upper + /// clamp). Caller must follow with `compute_cell_metrics` + + /// `update_dimensions` to refresh the cell stride. + #[inline] + pub fn increase_font_size(&mut self) -> bool { + if self.font_size < 100.0 { + self.update_font_size(self.font_size + 1.0); + true + } else { + false + } + } + + #[inline] + pub fn decrease_font_size(&mut self) -> bool { + if self.font_size > 6.0 { + self.update_font_size(self.font_size - 1.0); + true + } else { + false + } + } + + #[inline] + pub fn reset_font_size(&mut self) -> bool { + if (self.font_size - self.original_font_size).abs() > f32::EPSILON { + self.update_font_size(self.original_font_size); + true + } else { + false + } + } + #[inline] pub fn update_dimensions( &mut self, @@ -1639,6 +1726,7 @@ impl ContextDimension { ) { self.dimension = dimensions; self.cell = cell; + self.scaled_font_size = self.font_size * dimensions.scale; self.update(); } diff --git a/frontends/rioterm/src/renderer/mod.rs b/frontends/rioterm/src/renderer/mod.rs index 891b7c87..ba48acd3 100644 --- a/frontends/rioterm/src/renderer/mod.rs +++ b/frontends/rioterm/src/renderer/mod.rs @@ -630,48 +630,17 @@ impl Renderer { ); } - // Render panel borders (on top of terminal content) + // Render panel borders (on top of terminal content). Borders + // are flat rects today — the previous `Object` enum + // (Rect / Quad / RichText) was only ever populated with the + // Rect variant, so the dispatch is direct now. let grid_scaled_margin = context_manager.get_current_grid_scaled_margin(); - for border_object in context_manager.get_panel_borders() { - match border_object { - rio_backend::sugarloaf::Object::Quad(quad) => { - // Convert from physical pixels to logical coordinates - let x = (quad.x + grid_scaled_margin.left) / scale_factor; - let y = (quad.y + grid_scaled_margin.top) / scale_factor; - let width = quad.width / scale_factor; - let height = quad.height / scale_factor; - - let corner_radii = [ - quad.corner_radii.top_left / scale_factor, - quad.corner_radii.top_right / scale_factor, - quad.corner_radii.bottom_right / scale_factor, - quad.corner_radii.bottom_left / scale_factor, - ]; - - // Render quad with rounded corners - sugarloaf.quad( - None, - x, - y, - width, - height, - quad.background_color, - corner_radii, - 0.0, - 1, // Higher order renders on top - ); - } - rio_backend::sugarloaf::Object::Rect(rect) => { - // Simple rectangle (no rounded corners or borders) - let x = (rect.x + grid_scaled_margin.left) / scale_factor; - let y = (rect.y + grid_scaled_margin.top) / scale_factor; - let width = rect.width / scale_factor; - let height = rect.height / scale_factor; - - sugarloaf.rect(None, x, y, width, height, rect.color, 0.0, 1); - } - _ => {} - } + for rect in context_manager.get_panel_borders() { + let x = (rect.x + grid_scaled_margin.left) / scale_factor; + let y = (rect.y + grid_scaled_margin.top) / scale_factor; + let width = rect.width / scale_factor; + let height = rect.height / scale_factor; + sugarloaf.rect(None, x, y, width, height, rect.color, 0.0, 1); } // Derive the window bg color from the currently-active panel's diff --git a/frontends/rioterm/src/screen/mod.rs b/frontends/rioterm/src/screen/mod.rs index ac65a073..dd946765 100644 --- a/frontends/rioterm/src/screen/mod.rs +++ b/frontends/rioterm/src/screen/mod.rs @@ -218,10 +218,11 @@ impl Screen<'_> { scrollback_history_limit: config.scrollback_history_limit, }; - // Create rich text with initial position accounting for island + // Allocate a rich_text_id for the new panel. Sugarloaf no + // longer tracks per-id panel metadata — position/bounds live + // on `ContextDimension` via rio's layout system; the id is + // still useful as a key for image_overlays + grid renderers. let rich_text_id = next_rich_text_id(); - let _ = sugarloaf.text(Some(rich_text_id)); - sugarloaf.set_position(rich_text_id, config.margin.left, padding_y_top); // Create unscaled margin for ContextDimension (compute() will scale it) let margin = Margin::new( @@ -237,16 +238,18 @@ impl Screen<'_> { padding_y_bottom * scale as f32, config.margin.left * scale as f32, ); + let (text_dimensions, cell_metrics) = sugarloaf.compute_cell_metrics( + config.fonts.size, + config.line_height, + scale as f32, + ); let context_dimension = ContextDimension::build( size.width as f32, size.height as f32, - sugarloaf - .get_text_dimensions(&rich_text_id) - .unwrap_or_default(), - sugarloaf - .get_text_cell_metrics(&rich_text_id) - .unwrap_or_default(), + text_dimensions, + cell_metrics, config.line_height, + config.fonts.size, margin, ); @@ -474,15 +477,17 @@ impl Screen<'_> { config.margin.left * scale, )); - // Update font size and line height BEFORE update_dimensions + // Update per-panel font size and line height BEFORE + // update_dimensions — the recompute reads from these + // fields. `rebaseline_font_size` also re-anchors the + // "reset" target so the next change_font_size(Reset) + // returns to the new config size. for current_context in context_grid.contexts_mut().values_mut() { let current_context = current_context.context_mut(); - self.sugarloaf - .set_text_font_size(¤t_context.rich_text_id, config.fonts.size); - self.sugarloaf.set_text_line_height( - ¤t_context.rich_text_id, - current_context.dimension.line_height, - ); + current_context + .dimension + .rebaseline_font_size(config.fonts.size); + current_context.dimension.line_height = config.line_height; } context_grid.update_dimensions(&mut self.sugarloaf); @@ -525,16 +530,15 @@ impl Screen<'_> { #[inline] pub fn change_font_size(&mut self, action: FontSizeAction) { - let action: u8 = match action { - FontSizeAction::Increase => 2, - FontSizeAction::Decrease => 1, - FontSizeAction::Reset => 0, + let dim = &mut self.context_manager.current_mut().dimension; + let changed = match action { + FontSizeAction::Increase => dim.increase_font_size(), + FontSizeAction::Decrease => dim.decrease_font_size(), + FontSizeAction::Reset => dim.reset_font_size(), }; - - self.sugarloaf.set_text_font_size_action( - &self.context_manager.current().rich_text_id, - action, - ); + if !changed { + return; + } self.context_manager .current_grid_mut() @@ -1383,13 +1387,12 @@ impl Screen<'_> { } pub fn split_right_with_config(&mut self, config: rio_backend::config::Config) { - // Create rich text with initial position accounting for island - let padding_y_top = self.renderer.margin.top + // Allocate panel id; position lands on `ContextDimension` + // through the Taffy layout pass (`apply_taffy_layout`). + let _ = self.renderer.margin.top + self.renderer.island.as_ref().map_or(0.0, |i| i.height()); + let _ = config.margin.left; let rich_text_id = next_rich_text_id(); - let _ = self.sugarloaf.text(Some(rich_text_id)); - self.sugarloaf - .set_position(rich_text_id, config.margin.left, padding_y_top); self.context_manager.split_from_config( rich_text_id, false, @@ -1401,16 +1404,7 @@ impl Screen<'_> { } pub fn split_right(&mut self) { - // Create rich text with initial position accounting for island - let current_grid = self.context_manager.current_grid(); - let (_context, margin) = current_grid.current_context_with_computed_dimension(); - let padding_x = margin.left; - let padding_y_top = self.renderer.margin.top - + self.renderer.island.as_ref().map_or(0.0, |i| i.height()); let rich_text_id = next_rich_text_id(); - let _ = self.sugarloaf.text(Some(rich_text_id)); - self.sugarloaf - .set_position(rich_text_id, padding_x, padding_y_top); self.context_manager .split(rich_text_id, false, &mut self.sugarloaf); @@ -1418,16 +1412,7 @@ impl Screen<'_> { } pub fn split_down(&mut self) { - // Create rich text with initial position accounting for island - let current_grid = self.context_manager.current_grid(); - let (_context, margin) = current_grid.current_context_with_computed_dimension(); - let padding_x = margin.left; - let padding_y_top = self.renderer.margin.top - + self.renderer.island.as_ref().map_or(0.0, |i| i.height()); let rich_text_id = next_rich_text_id(); - let _ = self.sugarloaf.text(Some(rich_text_id)); - self.sugarloaf - .set_position(rich_text_id, padding_x, padding_y_top); self.context_manager .split(rich_text_id, true, &mut self.sugarloaf); @@ -1489,15 +1474,12 @@ impl Screen<'_> { self.context_manager.contexts_mut()[old_index] .update_dimensions(&mut self.sugarloaf); - // Use the base scaled_margin for the new tab position, not the - // split-panel-aware margin, because the new tab is full-window. - let padding_x = self.context_manager.current_grid().scaled_margin.left; - let padding_y_top = self.renderer.margin.top + // Allocate panel id; the layout pass handles positioning via + // `ContextDimension` once the new tab's grid is built. + let _ = self.context_manager.current_grid().scaled_margin.left; + let _ = self.renderer.margin.top + self.renderer.island.as_ref().map_or(0.0, |i| i.height()); let rich_text_id = next_rich_text_id(); - let _ = self.sugarloaf.text(Some(rich_text_id)); - self.sugarloaf - .set_position(rich_text_id, padding_x, padding_y_top); self.context_manager.add_context(redirect, rich_text_id); let new_index = self.context_manager.current_index(); self.context_manager.switch_context_visibility( @@ -1560,13 +1542,11 @@ impl Screen<'_> { if previous_margin.top != padding_y_top || previous_margin.bottom != padding_y_bottom { - if let Some(layout) = self - .sugarloaf - .get_text_layout(&self.context_manager.current().rich_text_id) - { + let current_dim = self.context_manager.current().dimension; + if current_dim.font_size > 0.0 { let s = self.sugarloaf.style_mut(); - s.font_size = layout.font_size; - s.line_height = layout.line_height; + s.font_size = current_dim.font_size; + s.line_height = current_dim.line_height; let scale = self.sugarloaf.scale_factor(); let d = self.context_manager.current_grid_mut(); @@ -3106,15 +3086,9 @@ impl Screen<'_> { #[inline] pub fn scroll(&mut self, new_scroll_x_px: f64, new_scroll_y_px: f64) { - let layout = match self - .sugarloaf - .get_text_layout(&self.context_manager.current().rich_text_id) - { - Some(l) => l, - None => return, - }; - let width = layout.dimensions.width as f64; - let height = layout.dimensions.height as f64; + let dim = self.context_manager.current().dimension.dimension; + let width = dim.width as f64; + let height = dim.height as f64; let mode = self.get_mode(); const MOUSE_WHEEL_UP: u8 = 64; @@ -3159,9 +3133,7 @@ impl Screen<'_> { let line_cmd = if new_scroll_y_px > 0. { b'A' } else { b'B' }; let column_cmd = if new_scroll_x_px > 0. { b'D' } else { b'C' }; - let lines = (self.mouse.accumulated_scroll.y - / (layout.dimensions.height) as f64) - .abs() as usize; + let lines = (self.mouse.accumulated_scroll.y / height).abs() as usize; let columns = (self.mouse.accumulated_scroll.x / width).abs() as usize; @@ -3185,8 +3157,7 @@ impl Screen<'_> { } else { self.mouse.accumulated_scroll.y += (new_scroll_y_px * self.mouse.multiplier) / self.mouse.divider; - let lines = (self.mouse.accumulated_scroll.y - / layout.dimensions.height as f64) as i32; + let lines = (self.mouse.accumulated_scroll.y / height) as i32; if lines != 0 { let current = self.context_manager.current_mut(); @@ -3604,17 +3575,16 @@ impl Screen<'_> { // show up. let cell_w = dim.cell.cell_width as f32; let cell_h = dim.cell.cell_height as f32; - // Per-panel font size (zoom is per-rich-text, not root). - // Falls back to root × scale if the text id can't be - // found — shouldn't happen post-init but keeps the emit - // loop from dividing by zero. - let font_px = self - .sugarloaf - .text_scaled_font_size(&ctx.rich_text_id) - .unwrap_or_else(|| { - let s = self.sugarloaf.style(); - s.font_size * s.scale_factor - }); + // Per-panel font size lives on `ContextDimension` since + // the panel-state migration; sugarloaf is no longer + // consulted. Per-panel zoom mutates + // `dim.scaled_font_size` directly. + let font_px = if dim.scaled_font_size > 0.0 { + dim.scaled_font_size + } else { + let s = self.sugarloaf.style(); + s.font_size * s.scale_factor + }; let (visible_rows, style_set, term_colors, display_offset) = { let terminal = ctx.terminal.lock(); ( diff --git a/sugarloaf/examples/layer.rs b/sugarloaf/examples/layer.rs deleted file mode 100644 index e74933cd..00000000 --- a/sugarloaf/examples/layer.rs +++ /dev/null @@ -1,179 +0,0 @@ -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, Color, SpanStyle, Sugarloaf, SugarloafWindow, SugarloafWindowSize, -}; - -fn main() { - let width = 400.0; - let height = 300.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, active_event_loop: &ActiveEventLoop) { - let window_attribute = WindowAttributes::default() - .with_title("Layer example") - .with_inner_size(LogicalSize::new(self.width, self.height)) - .with_resizable(true); - let window = active_event_loop.create_window(window_attribute).unwrap(); - - let scale_factor = window.scale_factor(); - let font_size = 24.; - - let sugarloaf_layout = RootStyle::new(scale_factor as f32, font_size, 1.0); - - let size = window.inner_size(); - let sugarloaf_window = SugarloafWindow { - handle: window.window_handle().unwrap().into(), - display: window.display_handle().unwrap().into(), - scale: scale_factor as f32, - size: SugarloafWindowSize { - width: size.width as f32, - height: size.height as f32, - }, - }; - - let mut sugarloaf = Sugarloaf::new( - sugarloaf_window, - sugarloaf::SugarloafRenderer::default(), - &sugarloaf::font::FontLibrary::default(), - sugarloaf_layout, - ) - .expect("Sugarloaf instance should be created"); - - sugarloaf.set_background_color(Some(Color { - r: 1.0, - g: 0.0, - b: 0.0, - a: 1.0, - })); - window.request_redraw(); - - // we will add three layers - sugarloaf.add_layers(3); - - self.sugarloaf = Some(sugarloaf); - self.window = Some(window); - } - - fn window_event( - &mut self, - event_loop: &ActiveEventLoop, - _window_id: WindowId, - event: WindowEvent, - ) { - if self.sugarloaf.is_none() || self.window.is_none() { - return; - } - - let sugarloaf = self.sugarloaf.as_mut().unwrap(); - let window = self.window.as_mut().unwrap(); - - match event { - WindowEvent::CloseRequested => event_loop.exit(), - WindowEvent::ScaleFactorChanged { - // mut inner_size_writer, - 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 => { - const TEXT_ID_0: usize = 0; - const TEXT_ID_1: usize = 1; - - sugarloaf.text(Some(TEXT_ID_0)); - sugarloaf - .content() - .sel(TEXT_ID_0) - .clear() - .new_line() - .add_span( - "First Layer", - SpanStyle { - color: [1.0, 1.0, 1.0, 1.0], - background_color: Some([0.0, 0.0, 0.0, 1.0]), - ..SpanStyle::default() - }, - ) - .new_line() - .build(); - - sugarloaf.text(Some(TEXT_ID_1)); - sugarloaf - .content() - .sel(TEXT_ID_1) - .clear() - .new_line() - .add_span( - "Second Layer", - SpanStyle { - color: [1.0, 1.0, 1.0, 1.0], - background_color: Some([0.0, 0.0, 0.0, 1.0]), - ..SpanStyle::default() - }, - ) - .new_line() - .build(); - - // Add rectangles directly - sugarloaf.rect(None, 10., 10., 120., 100., [1.0, 1.0, 1.0, 1.0], 0.0, 0); - sugarloaf.rect(None, 10., 80., 120., 100., [0.0, 0.0, 0.0, 1.0], 0.0, 0); - sugarloaf.rect(None, 95., 30., 20., 100., [1.0, 1.0, 1.0, 1.0], 0.0, 0); - - // Show rich text - sugarloaf.set_position(TEXT_ID_0, 10., 10.); - sugarloaf.set_visibility(TEXT_ID_0, true); - sugarloaf.set_position(TEXT_ID_1, 10., 60.); - sugarloaf.set_visibility(TEXT_ID_1, true); - - sugarloaf.render(); - event_loop.set_control_flow(ControlFlow::Wait); - } - _ => (), - } - } -} diff --git a/sugarloaf/examples/line_height.rs b/sugarloaf/examples/line_height.rs deleted file mode 100644 index 656eb47c..00000000 --- a/sugarloaf/examples/line_height.rs +++ /dev/null @@ -1,219 +0,0 @@ -use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; -use rio_window::application::ApplicationHandler; -use rio_window::event_loop::{ActiveEventLoop, ControlFlow, DeviceEvents}; -use rio_window::keyboard::{Key, NamedKey}; -use rio_window::window::{Window, WindowId}; -use rio_window::{ - dpi::LogicalSize, - event::{ElementState, WindowEvent}, - event_loop::EventLoop, - window::WindowAttributes, -}; -use std::error::Error; -use sugarloaf::{ - layout::RootStyle, Color, CursorKind, SpanStyle, SugarCursor, Sugarloaf, - SugarloafWindow, SugarloafWindowSize, -}; - -fn main() { - let width = 600.0; - let height = 400.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, - line_height: 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, - line_height: 2.0, - } - } - - 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, active_event_loop: &ActiveEventLoop) { - let window_attribute = WindowAttributes::default() - .with_title("Line height example") - .with_inner_size(LogicalSize::new(self.width, self.height)) - .with_resizable(true); - let window = active_event_loop.create_window(window_attribute).unwrap(); - - let scale_factor = window.scale_factor(); - let font_size = 24.; - - let sugarloaf_layout = - RootStyle::new(scale_factor as f32, font_size, self.line_height); - - let size = window.inner_size(); - let sugarloaf_window = SugarloafWindow { - handle: window.window_handle().unwrap().into(), - display: window.display_handle().unwrap().into(), - scale: scale_factor as f32, - size: SugarloafWindowSize { - width: size.width as f32, - height: size.height as f32, - }, - }; - - let mut sugarloaf = Sugarloaf::new( - sugarloaf_window, - sugarloaf::SugarloafRenderer::default(), - &sugarloaf::font::FontLibrary::default(), - sugarloaf_layout, - ) - .expect("Sugarloaf instance should be created"); - - sugarloaf.set_background_color(Some(Color { - r: 0.0, - g: 0.0, - b: 1.0, - a: 1.0, - })); - window.request_redraw(); - - self.sugarloaf = Some(sugarloaf); - self.window = Some(window); - } - - fn window_event( - &mut self, - event_loop: &ActiveEventLoop, - _window_id: WindowId, - event: WindowEvent, - ) { - if self.sugarloaf.is_none() || self.window.is_none() { - return; - } - - let sugarloaf = self.sugarloaf.as_mut().unwrap(); - let window = self.window.as_mut().unwrap(); - - match event { - WindowEvent::CloseRequested => event_loop.exit(), - WindowEvent::ScaleFactorChanged { - // mut inner_size_writer, - 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::KeyboardInput { - is_synthetic: false, - event: key_event, - .. - } => { - if key_event.state == ElementState::Pressed { - match key_event.logical_key.as_ref() { - Key::Named(NamedKey::ArrowUp) => { - self.line_height += 0.1; - sugarloaf.set_text_line_height(&0, self.line_height); - window.request_redraw(); - } - Key::Named(NamedKey::ArrowDown) => { - if self.line_height > 1.0 { - self.line_height -= 0.1; - sugarloaf.set_text_line_height(&0, self.line_height); - window.request_redraw(); - } - } - _ => {} - } - } - } - WindowEvent::RedrawRequested => { - const TEXT_ID: usize = 0; - - sugarloaf.text(Some(TEXT_ID)); - sugarloaf - .content() - .sel(TEXT_ID) - .clear() - .new_line() - .add_span( - &format!("current line_height: {:?}", self.line_height), - SpanStyle { - color: [0.0, 0.0, 0.0, 1.0], - background_color: Some([1.0, 1.0, 1.0, 1.0]), - ..SpanStyle::default() - }, - ) - .new_line() - .add_span( - "press arrow up to increase", - SpanStyle { - color: [1.0, 1.0, 1.0, 1.0], - background_color: Some([0.0, 0.0, 0.0, 1.0]), - ..SpanStyle::default() - }, - ) - .new_line() - .add_span( - "press arrow down to decrease", - SpanStyle { - color: [0.0, 0.0, 0.0, 1.0], - background_color: Some([1.0, 1.0, 1.0, 1.0]), - ..SpanStyle::default() - }, - ) - .new_line() - .add_span( - "│ \u{E0B6}Hello There!\u{e0b4}", - SpanStyle { - color: [1.0, 1.0, 1.0, 1.0], - background_color: Some([1.0, 0.5, 1.0, 1.0]), - ..SpanStyle::default() - }, - ) - .add_span( - "?", - SpanStyle { - color: [0.5, 0.5, 1.0, 1.0], - background_color: Some([1.0, 0.5, 1.0, 1.0]), - cursor: Some(SugarCursor { - kind: CursorKind::Block, - color: [1.0, 1.0, 1.0, 1.0], - order: 0, - }), - ..SpanStyle::default() - }, - ) - .build(); - - // Show rich text using new API - sugarloaf.set_position(TEXT_ID, 10., 0.); - sugarloaf.set_visibility(TEXT_ID, true); - sugarloaf.render(); - event_loop.set_control_flow(ControlFlow::Wait); - } - _ => (), - } - } -} diff --git a/sugarloaf/examples/multi_text.rs b/sugarloaf/examples/multi_text.rs deleted file mode 100644 index 459b539a..00000000 --- a/sugarloaf/examples/multi_text.rs +++ /dev/null @@ -1,278 +0,0 @@ -#![allow(clippy::uninlined_format_args)] - -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, SpanStyle, Sugarloaf, SugarloafWindow, SugarloafWindowSize, -}; - -fn main() { - let width = 800.0; - let height = 400.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); -} - -// User-defined content IDs -const TEXT_ID_0: usize = 0; -const TEXT_ID_1: usize = 1; -const TEXT_ID_2: usize = 2; -const RECT_ID_0: usize = 10; -const RECT_ID_1: usize = 11; -const RECT_ID_2: usize = 12; - -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, active_event_loop: &ActiveEventLoop) { - let window_attribute = WindowAttributes::default() - .with_title("Multi text example") - .with_inner_size(LogicalSize::new(self.width, self.height)) - .with_resizable(true); - let window = active_event_loop.create_window(window_attribute).unwrap(); - - let scale_factor = window.scale_factor(); - let font_size = 20.; - let line_height = 1.0; - - let sugarloaf_layout = - RootStyle::new(scale_factor as f32, font_size, line_height); - - let size = window.inner_size(); - let sugarloaf_window = SugarloafWindow { - handle: window.window_handle().unwrap().into(), - display: window.display_handle().unwrap().into(), - scale: scale_factor as f32, - size: SugarloafWindowSize { - width: size.width as f32, - height: size.height as f32, - }, - }; - - let mut sugarloaf = Sugarloaf::new( - sugarloaf_window, - sugarloaf::SugarloafRenderer::default(), - &sugarloaf::font::FontLibrary::default(), - sugarloaf_layout, - ) - .expect("Sugarloaf instance should be created"); - - // Initialize text areas with different font sizes - sugarloaf.text(Some(TEXT_ID_0)); // Default font size - sugarloaf.text(Some(TEXT_ID_1)); // Will set font size below - sugarloaf.text(Some(TEXT_ID_2)); // Will set font size below - - sugarloaf.set_text_font_size(&TEXT_ID_1, 24.0); - sugarloaf.set_text_font_size(&TEXT_ID_2, 12.0); - - sugarloaf.set_background_color(None); - window.request_redraw(); - - self.sugarloaf = Some(sugarloaf); - self.window = Some(window); - } - - fn window_event( - &mut self, - event_loop: &ActiveEventLoop, - _window_id: WindowId, - event: WindowEvent, - ) { - if self.sugarloaf.is_none() || self.window.is_none() { - return; - } - - 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 => { - let time = std::time::Instant::now(); - - // First text area - sugarloaf.text(Some(TEXT_ID_0)); - sugarloaf - .content() - .sel(TEXT_ID_0) - .clear() - .new_line() - .add_span( - &format!("Text area {:?}", TEXT_ID_0), - SpanStyle { - color: [1.0, 1.0, 1.0, 1.0], - background_color: Some([0.0, 0.0, 0.0, 1.0]), - ..SpanStyle::default() - }, - ) - .new_line() - .add_span( - &format!("{:?}", time.elapsed()), - SpanStyle { - color: [1.0, 1.0, 1.0, 1.0], - ..SpanStyle::default() - }, - ) - .build(); - - // Second text area - sugarloaf.text(Some(TEXT_ID_1)); - sugarloaf - .content() - .sel(TEXT_ID_1) - .clear() - .new_line() - .add_span( - &format!("Text area {:?}", TEXT_ID_1), - SpanStyle { - color: [1.0, 1.0, 1.0, 1.0], - background_color: Some([0.0, 0.0, 0.0, 1.0]), - ..SpanStyle::default() - }, - ) - .new_line() - .add_span( - &format!("{:?}", time.elapsed()), - SpanStyle { - color: [1.0, 1.0, 1.0, 1.0], - ..SpanStyle::default() - }, - ) - .build(); - - // Third text area - demonstrates partial updates - let needs_init = sugarloaf - .get_text_by_id(TEXT_ID_2) - .is_none_or(|state| state.lines.is_empty()); - - if needs_init { - // Initial setup - sugarloaf.text(Some(TEXT_ID_2)); - sugarloaf - .content() - .sel(TEXT_ID_2) - .new_line() - .add_span( - &format!("Should not update {:?}", time.elapsed()), - SpanStyle { - color: [1.0, 1.0, 1.0, 1.0], - ..SpanStyle::default() - }, - ) - .new_line() - .add_span( - &format!("Should update {:?}", time.elapsed()), - SpanStyle { - color: [1.0, 1.0, 1.0, 1.0], - ..SpanStyle::default() - }, - ) - .build(); - } else { - // Partial update - only update line 1 - sugarloaf - .content() - .sel(TEXT_ID_2) - .clear_line(1) - .add_span_on_line( - 1, - &format!("Updated {:?}", time.elapsed()), - SpanStyle { - color: [1.0, 1.0, 1.0, 1.0], - ..SpanStyle::default() - }, - ) - .build_line(1); - } - - // Add background rectangles (cached) - sugarloaf.rect( - Some(RECT_ID_0), - 5., - 5., - 200.0, - 200.0, - [1.0, 0.5, 0.5, 0.5], - 0.0, - 0, - ); - sugarloaf.rect( - Some(RECT_ID_1), - 220., - 5., - 200.0, - 150.0, - [1.0, 0.5, 0.5, 0.5], - 0.0, - 0, - ); - sugarloaf.rect( - Some(RECT_ID_2), - 440., - 5., - 320.0, - 150.0, - [1.0, 0.5, 0.5, 0.5], - 0.0, - 0, - ); - - // Position and show text - sugarloaf.set_position(TEXT_ID_0, 5., 5.); - sugarloaf.set_visibility(TEXT_ID_0, true); - - sugarloaf.set_position(TEXT_ID_1, 220., 5.); - sugarloaf.set_visibility(TEXT_ID_1, true); - - sugarloaf.set_position(TEXT_ID_2, 440., 5.); - sugarloaf.set_visibility(TEXT_ID_2, true); - - sugarloaf.render(); - event_loop.set_control_flow(ControlFlow::Wait); - } - _ => (), - } - } -} diff --git a/sugarloaf/examples/text.rs b/sugarloaf/examples/text.rs deleted file mode 100644 index 9f2cf38f..00000000 --- a/sugarloaf/examples/text.rs +++ /dev/null @@ -1,265 +0,0 @@ -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, Color, SpanStyle, SpanStyleDecoration, Sugarloaf, SugarloafWindow, - SugarloafWindowSize, UnderlineInfo, UnderlineShape, -}; - -fn main() { - let width = 1200.0; - let height = 500.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, active_event_loop: &ActiveEventLoop) { - let window_attribute = WindowAttributes::default() - .with_title("Text example") - .with_inner_size(LogicalSize::new(self.width, self.height)) - .with_resizable(true); - let window = active_event_loop.create_window(window_attribute).unwrap(); - - let scale_factor = window.scale_factor(); - let font_size = 24.; - - let sugarloaf_layout = RootStyle::new(scale_factor as f32, font_size, 1.0); - - let size = window.inner_size(); - let sugarloaf_window = SugarloafWindow { - handle: window.window_handle().unwrap().into(), - display: window.display_handle().unwrap().into(), - scale: scale_factor as f32, - size: SugarloafWindowSize { - width: size.width as f32, - height: size.height as f32, - }, - }; - - let mut sugarloaf = Sugarloaf::new( - sugarloaf_window, - sugarloaf::SugarloafRenderer::default(), - &sugarloaf::font::FontLibrary::default(), - sugarloaf_layout, - ) - .expect("Sugarloaf instance should be created"); - - sugarloaf.set_background_color(Some(Color { - r: 1.0, - g: 0.0, - b: 0.0, - a: 1.0, - })); - window.request_redraw(); - - self.sugarloaf = Some(sugarloaf); - self.window = Some(window); - } - - fn window_event( - &mut self, - event_loop: &ActiveEventLoop, - _window_id: WindowId, - event: WindowEvent, - ) { - if self.sugarloaf.is_none() || self.window.is_none() { - return; - } - - let sugarloaf = self.sugarloaf.as_mut().unwrap(); - let window = self.window.as_mut().unwrap(); - - match event { - WindowEvent::CloseRequested => event_loop.exit(), - WindowEvent::ScaleFactorChanged { - // mut inner_size_writer, - 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 => { - const TEXT_ID: usize = 1; - - sugarloaf.text(Some(TEXT_ID)); - sugarloaf - .content() - .sel(TEXT_ID) - .clear() - .add_span( - "Sugarloaf", - SpanStyle { - color: [1.0, 1.0, 1.0, 1.0], - background_color: Some([0.0, 0.0, 0.0, 1.0]), - ..SpanStyle::default() - }, - ) - .new_line() - .add_span( - "│㏑¼", - SpanStyle { - color: [0.0, 0.0, 0.0, 1.0], - background_color: Some([1.0, 1.0, 1.0, 1.0]), - width: 2.0, - ..SpanStyle::default() - }, - ) - .add_span( - "🥶", - SpanStyle { - color: [1.0, 0.0, 1.0, 1.0], - background_color: Some([0.3, 0.5, 1.0, 1.0]), - width: 2.0, - ..SpanStyle::default() - }, - ) - .new_line() - .add_span( - "│regular -> ", - SpanStyle { - decoration: Some(SpanStyleDecoration::Underline( - UnderlineInfo { - is_doubled: false, - shape: UnderlineShape::Regular, - }, - )), - color: [1.0, 1.0, 1.0, 1.0], - background_color: Some([0.0, 0.0, 0.0, 1.0]), - ..SpanStyle::default() - }, - ) - .add_span( - " ", - SpanStyle { - decoration: None, - color: [1.0, 1.0, 1.0, 1.0], - background_color: Some([0.0, 0.0, 0.0, 1.0]), - ..SpanStyle::default() - }, - ) - .add_span( - "|still|", - SpanStyle { - decoration: Some(SpanStyleDecoration::Underline( - UnderlineInfo { - is_doubled: false, - shape: UnderlineShape::Regular, - }, - )), - color: [1.0, 1.0, 1.0, 1.0], - background_color: Some([0.0, 0.0, 0.0, 1.0]), - ..SpanStyle::default() - }, - ) - .add_span( - " ", - SpanStyle { - decoration: None, - color: [1.0, 1.0, 1.0, 1.0], - background_color: Some([0.0, 0.0, 0.0, 1.0]), - ..SpanStyle::default() - }, - ) - .add_span( - "│curly", - SpanStyle { - decoration: Some(SpanStyleDecoration::Underline( - UnderlineInfo { - is_doubled: false, - shape: UnderlineShape::Curly, - }, - )), - color: [1.0, 1.0, 1.0, 1.0], - background_color: Some([0.0, 0.0, 0.0, 1.0]), - ..SpanStyle::default() - }, - ) - .new_line() - .add_span( - "│dashed", - SpanStyle { - decoration: Some(SpanStyleDecoration::Underline( - UnderlineInfo { - is_doubled: false, - shape: UnderlineShape::Dashed, - }, - )), - color: [1.0, 1.0, 1.0, 1.0], - background_color: Some([0.0, 0.0, 0.0, 1.0]), - ..SpanStyle::default() - }, - ) - .add_span( - " ", - SpanStyle { - decoration: None, - color: [1.0, 1.0, 1.0, 1.0], - background_color: Some([0.0, 0.0, 0.0, 1.0]), - ..SpanStyle::default() - }, - ) - .add_span( - "dotted", - SpanStyle { - decoration: Some(SpanStyleDecoration::Underline( - UnderlineInfo { - is_doubled: false, - shape: UnderlineShape::Dotted, - }, - )), - color: [1.0, 1.0, 1.0, 1.0], - background_color: Some([0.0, 0.0, 0.0, 1.0]), - ..SpanStyle::default() - }, - ) - .build(); - sugarloaf.set_position(TEXT_ID, 10., 10.); - sugarloaf.set_visibility(TEXT_ID, true); - sugarloaf.render(); - event_loop.set_control_flow(ControlFlow::Wait); - } - _ => (), - } - } -} diff --git a/sugarloaf/src/layout/content.rs b/sugarloaf/src/layout/content.rs deleted file mode 100644 index fa93eeb5..00000000 --- a/sugarloaf/src/layout/content.rs +++ /dev/null @@ -1,1977 +0,0 @@ -// 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. - -#![allow(clippy::uninlined_format_args)] - -use crate::font::FontLibrary; -use crate::layout::content_data::{ContentData, ContentState}; -use crate::layout::render_data::RenderData; -use crate::layout::TextLayout; -use lru::LruCache; -use rustc_hash::FxHashMap; -use smallvec::SmallVec; -use std::collections::HashSet; -use std::hash::{Hash, Hasher}; -use std::num::NonZeroUsize; -use swash::shape::ShapeContext; -use swash::text::Script; -#[cfg(not(target_os = "macos"))] -use swash::FontRef; -use tracing::debug; - -use crate::{sugarloaf::primitives::SugarCursor, DrawableChar, Graphic}; -use swash::Attributes; -use swash::Setting; - -/// Pre-packed shaping result ready to push directly as a RunData. -/// Avoids re-packing OwnedGlyphClusters on every cache hit. -#[derive(Clone, Debug)] -pub struct CachedRun { - pub glyphs: Vec, - pub detailed_glyphs: Vec, - pub advance: f32, - pub cache_key: u64, -} - -#[derive(Debug, Clone)] -pub struct FragmentData { - /// Range `(start, end)` into the owning `BuilderLine::text_buffer`. - /// `None` means advance position only (no shaping). - pub content: Option<(u32, u32)>, - pub style: SpanStyle, -} - -#[derive(Default, Clone, Debug)] -pub struct BuilderLine { - pub fragments: Vec, - /// Shared text buffer for all fragments on this line. Each fragment - /// stores a `(start, end)` range into this buffer instead of owning - /// its own `String`, eliminating per-span heap allocations. - pub text_buffer: String, - pub render_data: RenderData, -} - -impl BuilderLine { - /// Get the text slice for a fragment. - #[inline] - pub fn fragment_text(&self, frag: &FragmentData) -> Option<&str> { - let (start, end) = frag.content?; - self.text_buffer.get(start as usize..end as usize) - } - - /// Push text into the shared buffer and return the range. - #[inline] - pub fn push_text(&mut self, text: &str) -> (u32, u32) { - let start = self.text_buffer.len() as u32; - self.text_buffer.push_str(text); - let end = self.text_buffer.len() as u32; - (start, end) - } -} - -#[derive(Default, Clone, Debug, PartialEq)] -#[repr(C)] -pub enum BuilderStateUpdate { - #[default] - Full, - Partial(HashSet), - Noop, -} - -#[derive(Default, Clone, Debug)] -pub struct BuilderState { - pub lines: Vec, - pub vars: FontSettingCache, - pub last_update: BuilderStateUpdate, - pub scaled_font_size: f32, - pub layout: TextLayout, -} - -impl BuilderState { - #[inline] - pub fn new_line_at(&mut self, pos: usize) { - self.lines.insert(pos, BuilderLine::default()); - } - #[inline] - pub fn remove_line_at(&mut self, pos: usize) { - self.lines.remove(pos); - } - #[inline] - pub fn from_layout(layout: &TextLayout) -> Self { - Self { - layout: *layout, - scaled_font_size: layout.font_size * layout.dimensions.scale, - ..BuilderState::default() - } - } - #[inline] - pub fn current_line(&self) -> usize { - self.lines.len().wrapping_sub(1) - } - #[inline] - pub fn mark_clean(&mut self) { - self.last_update = BuilderStateUpdate::Noop; - } - #[inline] - pub fn mark_dirty(&mut self) { - self.last_update = BuilderStateUpdate::Full; - } - #[inline] - pub fn mark_line_dirty(&mut self, line: usize) { - match &mut self.last_update { - BuilderStateUpdate::Full => { - // No operation - } - BuilderStateUpdate::Noop => { - self.last_update = BuilderStateUpdate::Partial(HashSet::from([line])); - } - BuilderStateUpdate::Partial(set) => { - set.insert(line); - } - }; - } - #[inline] - pub fn clear(&mut self) -> &mut Self { - self.lines.clear(); - self.vars.clear(); - self.last_update = BuilderStateUpdate::Full; - self.lines.push(BuilderLine::default()); - self - } - - /// Add a text span with the given style - #[inline] - pub fn add_span(&mut self, text: &str, style: SpanStyle) -> &mut Self { - if self.lines.is_empty() { - self.lines.push(BuilderLine::default()); - } - let current_line = self.current_line(); - if let Some(line) = self.lines.get_mut(current_line) { - let range = line.push_text(text); - line.fragments.push(FragmentData { - content: Some(range), - style, - }); - } - self - } - - /// Add a new line - #[inline] - pub fn new_line(&mut self) -> &mut Self { - self.lines.push(BuilderLine::default()); - self - } - - /// Clear a specific line's fragments - #[inline] - pub fn clear_line(&mut self, line_number: usize) -> &mut Self { - if let Some(line) = self.lines.get_mut(line_number) { - line.fragments.clear(); - line.text_buffer.clear(); - line.render_data.glyphs.clear(); - line.render_data.runs.clear(); - self.mark_line_dirty(line_number); - } - self - } - - /// Add text to a specific line - #[inline] - pub fn add_span_on_line( - &mut self, - line_number: usize, - text: &str, - style: SpanStyle, - ) -> &mut Self { - if let Some(line) = self.lines.get_mut(line_number) { - let range = line.push_text(text); - line.fragments.push(FragmentData { - content: Some(range), - style, - }); - } - self - } - - /// Add an empty span to a specific line that only advances position - /// (renders background rect if set, but no text shaping). - #[inline] - pub fn add_span_as_rect_on_line( - &mut self, - line_number: usize, - style: SpanStyle, - ) -> &mut Self { - if let Some(line) = self.lines.get_mut(line_number) { - line.fragments.push(FragmentData { - content: None, - style, - }); - } - self - } - - /// Add an empty span that only advances position - /// (renders background rect if set, but no text shaping). - #[inline] - pub fn add_span_as_rect(&mut self, style: SpanStyle) -> &mut Self { - if self.lines.is_empty() { - self.lines.push(BuilderLine::default()); - } - let current_line = self.current_line(); - if let Some(line) = self.lines.get_mut(current_line) { - line.fragments.push(FragmentData { - content: None, - style, - }); - } - self - } - - /// Finalize the text building (placeholder for compatibility) - #[inline] - pub fn build(&mut self) -> &mut Self { - self.last_update = BuilderStateUpdate::Full; - self - } - - #[inline] - pub fn rescale(&mut self, scale_factor: f32) { - self.scaled_font_size = self.layout.font_size * scale_factor; - self.layout.rescale(scale_factor); - } - #[inline] - pub fn begin(&mut self) { - self.lines.push(BuilderLine::default()); - } - #[inline] - pub fn update_font_size(&mut self) { - let font_size = self.layout.font_size; - let scale = self.layout.dimensions.scale; - self.scaled_font_size = font_size * scale; - - self.last_update = BuilderStateUpdate::Full; - } - - pub fn increase_font_size(&mut self) -> bool { - if self.layout.font_size < 100.0 { - self.layout.font_size += 1.0; - self.update_font_size(); - return true; - } - false - } - - pub fn decrease_font_size(&mut self) -> bool { - if self.layout.font_size > 6.0 { - self.layout.font_size -= 1.0; - self.update_font_size(); - return true; - } - false - } - - pub fn reset_font_size(&mut self) -> bool { - if self.layout.font_size != self.layout.original_font_size { - self.layout.font_size = self.layout.original_font_size; - self.update_font_size(); - return true; - } - false - } -} - -/// Index into a font setting cache. -pub type FontSettingKey = u32; - -/// Cache of tag/value pairs for font settings. -#[derive(Default, Clone, Debug)] -pub struct FontSettingCache { - settings: Vec>, - lists: Vec, - tmp: Vec>, -} - -impl FontSettingCache { - pub fn get(&self, key: u32) -> &[Setting] { - if key == !0 { - &[] - } else { - self.lists - .get(key as usize) - .map(|list| list.get(&self.settings)) - .unwrap_or(&[]) - } - } - - pub fn clear(&mut self) { - self.settings.clear(); - self.lists.clear(); - self.tmp.clear(); - } -} - -/// Sentinel for an empty set of font settings. -pub const EMPTY_FONT_SETTINGS: FontSettingKey = !0; - -/// Range within a font setting cache. -#[derive(Copy, Clone, Debug)] -struct FontSettingList { - pub start: u32, - pub end: u32, -} - -impl FontSettingList { - pub fn get(self, elements: &[T]) -> &[T] { - elements - .get(self.start as usize..self.end as usize) - .unwrap_or(&[]) - } -} - -#[repr(u8)] -#[derive(Copy, Clone, PartialEq, Debug, Default)] -pub enum UnderlineShape { - #[default] - Regular = 0, - Dotted = 1, - Dashed = 2, - Curly = 3, -} - -#[derive(Copy, Clone, PartialEq, Debug)] -pub struct UnderlineInfo { - pub is_doubled: bool, - pub shape: UnderlineShape, -} - -#[derive(Copy, Clone, PartialEq, Debug)] -pub enum SpanStyleDecoration { - // offset, size - Underline(UnderlineInfo), - Strikethrough, -} - -#[derive(Copy, Clone, PartialEq, Debug)] -pub struct SpanStyle { - pub font_id: usize, - // Unicode width - pub width: f32, - /// Font attributes. - pub font_attrs: Attributes, - /// Font color. - pub color: [f32; 4], - /// Background color. - pub background_color: Option<[f32; 4]>, - /// Font variations. - pub font_vars: FontSettingKey, - /// Additional spacing between letters (clusters) of text. - // pub letter_spacing: f32, - /// Additional spacing between words of text. - // pub word_spacing: f32, - /// Multiplicative line spacing factor. - // pub line_spacing: f32, - /// Enable underline decoration. - pub decoration: Option, - /// Decoration color. - pub decoration_color: Option<[f32; 4]>, - /// Cursor style. - pub cursor: Option, - /// Media - pub media: Option, - /// Drawable character - pub drawable_char: Option, - /// PUA constraint width: how many cells the glyph should visually fill. - /// None for normal glyphs, Some(1.0) or Some(2.0) for PUA glyphs. - /// Does NOT affect positioning/advance — only compositor scaling. - pub pua_constraint: Option, - /// Optional per-glyph Nerd Font constraint (size / alignment / - /// padding) sourced from the Nerd Fonts patcher table. When set, - /// the compositor lays the glyph out using the constraint math - /// in `nerd_font_attributes` instead of the generic cell-centered - /// fit. Only populated by the renderer for codepoints with a - /// table entry (`get_constraint`). - pub nerd_font_constraint: Option, -} - -impl Default for SpanStyle { - fn default() -> Self { - Self { - font_id: 0, - width: 1.0, - font_attrs: Attributes::default(), - font_vars: EMPTY_FONT_SETTINGS, - // letter_spacing: 0., - // word_spacing: 0., - // line_spacing: 1., - color: [1.0, 1.0, 1.0, 1.0], - background_color: None, - cursor: None, - decoration: None, - decoration_color: None, - media: None, - drawable_char: None, - pua_constraint: None, - nerd_font_constraint: None, - } - } -} - -/// Context for paragraph layout. -pub struct Content { - fonts: FontLibrary, - font_features: Vec>, - scx: ShapeContext, - pub states: FxHashMap, - /// Transient text content that gets cleared after each render - pub transient_texts: Vec, - shaping_cache: ShapingCache, - selector: Option, -} - -/// Compute the canonical [`CellMetrics`] from face metrics already -/// scaled to physical pixels. Pure function so the formula is -/// testable without standing up a `Content`. -/// -/// Inputs: -/// - `face_width / face_height`: unrounded cell dims in physical px -/// (`face_height` already has the user's `line_height` multiplier -/// applied). -/// - `descent_phys / leading_phys`: descent and line gap in physical -/// px, also multiplied by `line_height`. Descent is a *positive* -/// magnitude (swash convention). -/// -/// Centering invariant: if `face_height` is `33.4` and rounds to -/// `33`, the baseline shifts up by `0.2` so the glyph stays -/// centered in the rounded cell — matches the half-rounding-delta -/// adjustment used elsewhere for vertical pixel-snapping. -#[inline] -pub(crate) fn canonical_cell_metrics( - face_width: f64, - face_height: f64, - descent_phys: f64, - leading_phys: f64, -) -> crate::layout::CellMetrics { - let cell_width = face_width.round().max(1.0) as u32; - let cell_height = face_height.round().max(1.0) as u32; - // Unrounded baseline: line_gap split evenly above and below the - // glyph, descent below. Sign-flipped from Zig's negative-descent - // convention since swash reports descent as positive. - let face_baseline = leading_phys * 0.5 + descent_phys; - let baseline_centered = face_baseline - (cell_height as f64 - face_height) * 0.5; - let cell_baseline = baseline_centered.round().max(0.0) as u32; - let face_y = cell_baseline as f64 - face_baseline; - crate::layout::CellMetrics { - cell_width, - cell_height, - cell_baseline, - face_width, - face_height, - face_y, - } -} - -impl Content { - /// Creates a new layout context with the specified font library. - pub fn new(font_library: &FontLibrary) -> Self { - Self { - fonts: font_library.clone(), - scx: ShapeContext::new(), - states: FxHashMap::default(), - transient_texts: Vec::new(), - shaping_cache: ShapingCache::new(), - font_features: vec![], - selector: None, - } - } - - #[inline] - pub fn sel(&mut self, state_id: usize) -> &mut Content { - self.selector = Some(state_id); - - // Ensure the state exists - create it with default text layout if missing - self.states.entry(state_id).or_insert_with(|| { - let default_layout = TextLayout::default(); - let builder_state = BuilderState::from_layout(&default_layout); - ContentState::new(ContentData::Text(builder_state)) - }); - - self - } - - #[inline] - pub fn font_library(&self) -> &FontLibrary { - &self.fonts - } - - #[inline] - pub fn set_font_library(&mut self, font_library: &FontLibrary) { - self.fonts = font_library.clone(); - self.shaping_cache = ShapingCache::new(); - } - - /// Get text state by ID (returns None if ID doesn't exist or is not text) - #[inline] - pub fn get_state(&self, state_id: &usize) -> Option<&BuilderState> { - self.states.get(state_id)?.as_text() - } - - /// Get mutable text state by ID (returns None if ID doesn't exist or is not text) - #[inline] - pub fn get_state_mut(&mut self, state_id: &usize) -> Option<&mut BuilderState> { - self.states.get_mut(state_id)?.as_text_mut() - } - - /// Get text by ID - returns the lines API if text, None otherwise - #[inline] - pub fn get_text_by_id(&self, id: usize) -> Option<&BuilderState> { - self.states.get(&id)?.as_text() - } - - /// Get mutable text by ID - #[inline] - pub fn get_text_by_id_mut(&mut self, id: usize) -> Option<&mut BuilderState> { - self.states.get_mut(&id)?.as_text_mut() - } - - /// Get content state by ID (any type) - #[inline] - pub fn get_content_state(&self, state_id: &usize) -> Option<&ContentState> { - self.states.get(state_id) - } - - /// Get mutable content state by ID (any type) - #[inline] - pub fn get_content_state_mut( - &mut self, - state_id: &usize, - ) -> Option<&mut ContentState> { - self.states.get_mut(state_id) - } - - #[inline] - pub fn set_font_features(&mut self, font_features: Vec>) { - self.font_features = font_features; - } - - /// Create text content at the given ID (overwrites existing content) - #[inline] - pub fn set_text(&mut self, id: usize, rich_text_layout: &TextLayout) { - let mut builder_state = BuilderState::from_layout(rich_text_layout); - - // Immediately calculate dimensions for a representative character - let (dims, cell) = self.calculate_character_cell_dimensions(rich_text_layout); - builder_state.layout.dimensions = dims; - builder_state.layout.cell = cell; - - if let Some(content_state) = self.states.get_mut(&id) { - content_state.data = ContentData::Text(builder_state); - content_state.render_data.needs_repaint = true; - content_state.render_data.should_remove = false; - } else { - self.states - .insert(id, ContentState::new(ContentData::Text(builder_state))); - } - } - - /// Calculate character cell dimensions and the canonical - /// [`CellMetrics`] used by the renderer + mouse + layout. - /// - /// Integer cell width / height / baseline are produced by - /// `.round()`-ing the unrounded `face_*` values once at this - /// layer. All downstream consumers read those integers — - /// there's no second rounding stage anywhere in the pipeline, - /// so the renderer's painted stride and the mouse hit-test - /// divide by the same value. - /// - /// `line_height` (the user's config multiplier) is applied to - /// `face_height` here, so callers must NOT re-apply it. - fn calculate_character_cell_dimensions( - &self, - layout: &TextLayout, - ) -> (crate::layout::TextDimensions, crate::layout::CellMetrics) { - let font_size = layout.font_size; - let scale_f64 = layout.dimensions.scale as f64; - let line_height_mod = layout.line_height as f64; - - // (char_width, ascent, descent, leading) in pixels at the - // *logical* font size (i.e. before `scale` is applied). - let raw: Option<(f64, f64, f64, f64)> = { - #[cfg(target_os = "macos")] - { - self.fonts.ct_font(0).map(|handle| { - let m = crate::font::macos::font_metrics(&handle, font_size); - // Cell width = max advance across printable ASCII at - // the real render size. Same progressive fallback as - // before — final fallback is `font_size` (the em). - let cw = crate::font::macos::max_ascii_advance_px(&handle, font_size) - .or_else(|| { - crate::font::macos::advance_units_for_char(&handle, ' ') - .map(|(units, upem)| units * font_size / upem as f32) - }) - .unwrap_or(font_size); - ( - cw as f64, - m.ascent as f64, - m.descent as f64, - m.leading as f64, - ) - }) - } - #[cfg(not(target_os = "macos"))] - { - self.fonts.inner.try_read().and_then(|lib| { - let id = 0; - let (data, offset, _key) = lib.get_data(&id)?; - let font_ref = swash::FontRef::from_index(&data, offset as usize)?; - let m = font_ref.metrics(&[]); - let upem = m.units_per_em as f32; - let s = font_size / upem; - let glyph = font_ref.charmap().map(' ' as u32); - let advance = font_ref.glyph_metrics(&[]).advance_width(glyph); - let cw = if advance > 0.0 { - advance * s - } else { - font_size - }; - Some(( - cw as f64, - (m.ascent * s) as f64, - (m.descent.abs() * s) as f64, - (m.leading * s) as f64, - )) - }) - } - }; - - let (face_width, face_height, descent_phys, leading_phys) = - if let Some((cw, ascent, descent, leading)) = raw { - let face_width = cw * scale_f64; - let face_height = - (ascent + descent + leading) * line_height_mod * scale_f64; - ( - face_width, - face_height, - descent * line_height_mod * scale_f64, - leading * line_height_mod * scale_f64, - ) - } else { - // Last-resort fallback: square em cell at the configured - // line_height. No baseline information, so cell_baseline - // ends up 0. - let fw = font_size as f64 * scale_f64; - let fh = font_size as f64 * line_height_mod * scale_f64; - (fw, fh, 0.0, 0.0) - }; - - let cell = - canonical_cell_metrics(face_width, face_height, descent_phys, leading_phys); - - let dims = crate::layout::TextDimensions { - // Legacy fields kept for back-compat with sugarloaf-side - // consumers that still read TextDimensions. Now snapped - // to the same integer cell stride (was: `.ceil()` on - // height, raw on width — produced drift at high column - // indexes when paired with the mouse path's unrounded - // divide). - width: cell.cell_width as f32, - height: cell.cell_height as f32, - scale: layout.dimensions.scale, - }; - (dims, cell) - } - - #[inline] - pub fn remove_state(&mut self, rich_text_id: &usize) { - self.states.remove(rich_text_id); - } - - #[inline] - pub fn mark_states_clean(&mut self) { - for content_state in self.states.values_mut() { - if let Some(text_state) = content_state.as_text_mut() { - text_state.mark_clean(); - } - } - } - - /// Add a transient text content that will be cleared after rendering. - /// Returns the index into transient_texts vec. - #[inline] - pub fn add_transient_text(&mut self, layout: &TextLayout) -> usize { - let mut builder_state = BuilderState::from_layout(layout); - let (dims, cell) = self.calculate_character_cell_dimensions(layout); - builder_state.layout.dimensions = dims; - builder_state.layout.cell = cell; - - let mut content_state = ContentState::new(ContentData::Text(builder_state)); - content_state.render_data.transient = true; - - let index = self.transient_texts.len(); - self.transient_texts.push(content_state); - index - } - - /// Get mutable reference to transient text by index - #[inline] - pub fn get_transient_text_mut(&mut self, index: usize) -> Option<&mut BuilderState> { - self.transient_texts.get_mut(index)?.as_text_mut() - } - - /// Get mutable reference to transient content state by index - #[inline] - pub fn get_transient_state_mut(&mut self, index: usize) -> Option<&mut ContentState> { - self.transient_texts.get_mut(index) - } - - /// Clear all transient texts (called after rendering) - #[inline] - pub fn clear_transient_texts(&mut self) { - self.transient_texts.clear(); - } - - /// Build/shape all transient texts - #[inline] - pub fn build_transient_texts(&mut self) { - let script = Script::Latin; - - for transient_idx in 0..self.transient_texts.len() { - let (scaled_font_size, num_lines) = { - let content_state = &self.transient_texts[transient_idx]; - let text_state = match content_state.as_text() { - Some(state) => state, - None => continue, - }; - (text_state.scaled_font_size, text_state.lines.len()) - }; - - // Process each line - for line_number in 0..num_lines { - let content_state = &mut self.transient_texts[transient_idx]; - let text_state = match content_state.as_text_mut() { - Some(state) => state, - None => continue, - }; - - Self::process_text_line( - text_state, - line_number, - scaled_font_size, - script, - &self.font_features, - &self.fonts, - &mut self.scx, - &mut self.shaping_cache, - ); - } - } - } - - #[inline] - pub fn update_dimensions(&mut self, state_id: &usize) { - let layout = if let Some(text_state) = self.get_state(state_id) { - text_state.layout - } else { - return; - }; - - let (new_dimension, new_cell) = self.calculate_character_cell_dimensions(&layout); - - if let Some(text_state) = self.get_state_mut(state_id) { - text_state.layout.dimensions = new_dimension; - text_state.layout.cell = new_cell; - } - } - - #[inline] - pub fn clear_state(&mut self, id: &usize) { - if let Some(text_state) = self.get_state_mut(id) { - text_state.clear(); - } - } - - #[inline] - pub fn new_line_with_id(&mut self, id: &usize) -> &mut Content { - if let Some(text_state) = self.get_state_mut(id) { - text_state.new_line(); - } - - self - } - - #[inline] - pub fn new_line(&mut self) -> &mut Content { - if let Some(selector) = self.selector { - return self.new_line_with_id(&selector); - } - - self - } - - #[inline] - pub fn new_line_at(&mut self, pos: usize) -> &mut Content { - if let Some(selector) = self.selector { - if let Some(text_state) = self.get_state_mut(&selector) { - text_state.new_line_at(pos); - } - } - - self - } - - #[inline] - pub fn remove_line_at(&mut self, pos: usize) -> &mut Content { - if let Some(selector) = self.selector { - if let Some(text_state) = self.get_state_mut(&selector) { - text_state.remove_line_at(pos); - } - } - - self - } - - #[inline] - pub fn clear_line(&mut self, line_to_clear: usize) -> &mut Content { - if let Some(selector) = self.selector { - if let Some(text_state) = self.get_state_mut(&selector) { - if let Some(line) = text_state.lines.get_mut(line_to_clear) { - line.fragments.clear(); - line.text_buffer.clear(); - line.render_data.clear(); - } - } - } - - self - } - - #[inline] - pub fn clear_with_id(&mut self, id: &usize) -> &mut Content { - if let Some(text_state) = self.get_state_mut(id) { - text_state.clear(); - } - - self - } - - #[inline] - pub fn clear_all(&mut self) -> &mut Content { - for content_state in self.states.values_mut() { - if let Some(text_state) = content_state.as_text_mut() { - text_state.clear(); - } - } - - self - } - - #[inline] - pub fn clear(&mut self) -> &mut Content { - if let Some(selector) = self.selector { - return self.clear_with_id(&selector); - } - - self - } - - #[inline] - pub fn add_span(&mut self, text: &str, style: SpanStyle) -> &mut Content { - if let Some(selector) = self.selector { - return self.add_span_with_id(&selector, text, style); - } - - self - } - - /// Add an empty span that only advances position (no shaping). - #[inline] - pub fn add_span_as_rect(&mut self, style: SpanStyle) -> &mut Content { - if let Some(selector) = self.selector { - if let Some(text_state) = self.get_state_mut(&selector) { - let current_line = text_state.current_line(); - if let Some(line) = text_state.lines.get_mut(current_line) { - line.fragments.push(FragmentData { - content: None, - style, - }); - } - } - } - self - } - - #[inline] - pub fn add_span_on_line( - &mut self, - line_idx: usize, - text: &str, - style: SpanStyle, - ) -> &mut Content { - if let Some(selector) = self.selector { - if let Some(text_state) = self.get_state_mut(&selector) { - text_state.mark_line_dirty(line_idx); - if let Some(line) = text_state.lines.get_mut(line_idx) { - let range = line.push_text(text); - line.fragments.push(FragmentData { - content: Some(range), - style, - }); - } - } - } - - self - } - - /// Add an empty span to advance position without shaping. - #[inline] - pub fn add_span_as_rect_on_line( - &mut self, - line_idx: usize, - style: SpanStyle, - ) -> &mut Content { - if let Some(selector) = self.selector { - if let Some(text_state) = self.get_state_mut(&selector) { - text_state.mark_line_dirty(line_idx); - if let Some(line) = text_state.lines.get_mut(line_idx) { - line.fragments.push(FragmentData { - content: None, - style, - }); - } - } - } - self - } - - /// Adds a text fragment to the paragraph. - pub fn add_span_with_id( - &mut self, - id: &usize, - text: &str, - style: SpanStyle, - ) -> &mut Content { - if let Some(text_state) = self.get_state_mut(id) { - let current_line = text_state.current_line(); - if let Some(line) = &mut text_state.lines.get_mut(current_line) { - let range = line.push_text(text); - line.fragments.push(FragmentData { - content: Some(range), - style, - }); - } - } - - self - } - - // Helper function to process a single line that avoids borrow issues - fn process_line(&mut self, state_id: usize, line_number: usize) { - // Get all needed data while borrowing parts of self separately - let script = Script::Latin; - - // First check if state exists and is text type, get immutable data - let (scaled_font_size, num_lines) = { - let content_state = match self.states.get(&state_id) { - Some(state) => state, - None => return, - }; - let text_state = match content_state.as_text() { - Some(state) => state, - None => return, - }; - (text_state.scaled_font_size, text_state.lines.len()) - }; - - let features = &self.font_features; - - // Check if the line exists - if line_number >= num_lines { - return; - } - - // Now get mutable borrow for the actual processing - let content_state = match self.states.get_mut(&state_id) { - Some(state) => state, - None => return, - }; - - let text_state = match content_state.as_text_mut() { - Some(state) => state, - None => return, - }; - - Self::process_text_line( - text_state, - line_number, - scaled_font_size, - script, - features, - &self.fonts, - &mut self.scx, - &mut self.shaping_cache, - ); - } - - #[allow(clippy::too_many_arguments)] - #[cfg_attr(target_os = "macos", allow(unused_variables))] - fn process_text_line( - text_state: &mut BuilderState, - line_number: usize, - scaled_font_size: f32, - script: Script, - features: &[swash::Setting], - fonts: &FontLibrary, - scx: &mut ShapeContext, - shaping_cache: &mut ShapingCache, - ) { - // Cache primary font metrics at line level to avoid repeated lock acquisition - let metrics_result = fonts.inner.write().get_font_metrics(&0, scaled_font_size); - - let line = &mut text_state.lines[line_number]; - - for fragment_idx in 0..line.fragments.len() { - let font_id = line.fragments[fragment_idx].style.font_id; - let font_vars = line.fragments[fragment_idx].style.font_vars; - let style = line.fragments[fragment_idx].style; - - // Resolve text range to &str from the shared buffer. - let content_range = line.fragments[fragment_idx].content; - - // None content = advance-only fragment (no shaping) - let content = match content_range { - Some((start, end)) => &line.text_buffer[start as usize..end as usize], - None => { - if let Some((ascent, descent, leading)) = if font_id == 0 { - metrics_result - } else { - fonts - .inner - .write() - .get_font_metrics(&font_id, scaled_font_size) - } { - let metrics = swash::Metrics { - ascent, - descent, - leading, - ..Default::default() - }; - line.render_data.push_empty_run( - style, - scaled_font_size, - line_number as u32, - &metrics, - ); - } - continue; - } - }; - - // Check run cache — pre-packed so no re-packing needed - if let Some(cached_run) = shaping_cache.get(&font_id, content) { - if let Some((ascent, descent, leading)) = if font_id == 0 { - metrics_result - } else { - fonts - .inner - .write() - .get_font_metrics(&font_id, scaled_font_size) - } { - line.render_data.push_cached_run( - style, - scaled_font_size, - line_number as u32, - cached_run, - ascent, - descent, - leading, - ); - continue; - } else { - debug!("Font metrics not available for font_id={}", font_id); - } - } - - // Cache miss: shape the full run and store result. - shaping_cache.set_content(font_id, content); - - #[cfg(target_os = "macos")] - { - if let Some(handle) = fonts.ct_font(font_id) { - let shaped = crate::font::macos::shape_text( - &handle, - content, - scaled_font_size, - ); - let macos_metrics = - crate::font::macos::font_metrics(&handle, scaled_font_size); - line.render_data.push_run_macos( - style, - scaled_font_size, - line_number as u32, - &shaped, - &macos_metrics, - shaping_cache, - ); - } - } - - #[cfg(not(target_os = "macos"))] - { - // Only allocate vars on the miss path - let vars: Vec<_> = text_state.vars.get(font_vars).to_vec(); - - let font_library = &fonts.inner.read(); - if let Some((shared_data, offset, key)) = font_library.get_data(&font_id) - { - let font_ref = FontRef { - data: shared_data.as_ref(), - offset, - key, - }; - let mut shaper = scx - .builder(font_ref) - .script(script) - .size(scaled_font_size) - .features(features.iter().copied()) - .variations(vars.iter().copied()) - .build(); - - shaper.add_str(content); - - line.render_data.push_run( - style, - scaled_font_size, - line_number as u32, - shaper, - shaping_cache, - ); - } - } - } - } - - #[inline] - pub fn build(&mut self) { - if let Some(selector) = self.selector { - let state_id = selector; - - let num_lines = { - if let Some(text_state) = self.get_state_mut(&state_id) { - text_state.mark_dirty(); - text_state.lines.len() - } else { - 0 - } - }; - - for line_number in 0..num_lines { - self.process_line(state_id, line_number); - } - } - } - - #[inline] - pub fn build_line(&mut self, line_number: usize) { - if let Some(selector) = self.selector { - // Process just the specified line - self.process_line(selector, line_number); - } - } - - /// Set rectangle at ID (overwrites existing content) - #[inline] - #[allow(clippy::too_many_arguments)] - pub fn set_rect( - &mut self, - id: usize, - x: f32, - y: f32, - width: f32, - height: f32, - color: [f32; 4], - depth: f32, - ) { - if let Some(content_state) = self.states.get_mut(&id) { - content_state.data = ContentData::Rect { - x, - y, - width, - height, - color, - depth, - }; - content_state.render_data.needs_repaint = true; - content_state.render_data.should_remove = false; - } else { - self.states.insert( - id, - ContentState::new(ContentData::Rect { - x, - y, - width, - height, - color, - depth, - }), - ); - } - } - - /// Set rounded rectangle at ID - #[inline] - #[allow(clippy::too_many_arguments)] - pub fn set_rounded_rect( - &mut self, - id: usize, - x: f32, - y: f32, - width: f32, - height: f32, - color: [f32; 4], - depth: f32, - border_radius: f32, - ) { - if let Some(content_state) = self.states.get_mut(&id) { - content_state.data = ContentData::RoundedRect { - x, - y, - width, - height, - color, - depth, - border_radius, - }; - content_state.render_data.needs_repaint = true; - content_state.render_data.should_remove = false; - } else { - self.states.insert( - id, - ContentState::new(ContentData::RoundedRect { - x, - y, - width, - height, - color, - depth, - border_radius, - }), - ); - } - } - - /// Set line at ID - #[inline] - #[allow(clippy::too_many_arguments)] - pub fn set_line( - &mut self, - id: usize, - x1: f32, - y1: f32, - x2: f32, - y2: f32, - width: f32, - color: [f32; 4], - depth: f32, - ) { - if let Some(content_state) = self.states.get_mut(&id) { - content_state.data = ContentData::Line { - x1, - y1, - x2, - y2, - width, - color, - depth, - }; - content_state.render_data.needs_repaint = true; - content_state.render_data.should_remove = false; - } else { - self.states.insert( - id, - ContentState::new(ContentData::Line { - x1, - y1, - x2, - y2, - width, - color, - depth, - }), - ); - } - } - - /// Set triangle at ID - #[inline] - pub fn set_triangle( - &mut self, - id: usize, - points: [(f32, f32); 3], - color: [f32; 4], - depth: f32, - ) { - if let Some(content_state) = self.states.get_mut(&id) { - content_state.data = ContentData::Triangle { - points, - color, - depth, - }; - content_state.render_data.needs_repaint = true; - content_state.render_data.should_remove = false; - } else { - self.states.insert( - id, - ContentState::new(ContentData::Triangle { - points, - color, - depth, - }), - ); - } - } - - /// Set polygon at ID - #[inline] - pub fn set_polygon( - &mut self, - id: usize, - points: &[(f32, f32)], - color: [f32; 4], - depth: f32, - ) { - let points_smallvec: SmallVec<[(f32, f32); 8]> = points.iter().copied().collect(); - if let Some(content_state) = self.states.get_mut(&id) { - content_state.data = ContentData::Polygon { - points: points_smallvec, - color, - depth, - }; - content_state.render_data.needs_repaint = true; - content_state.render_data.should_remove = false; - } else { - self.states.insert( - id, - ContentState::new(ContentData::Polygon { - points: points_smallvec, - color, - depth, - }), - ); - } - } - - /// Set arc at ID - #[inline] - #[allow(clippy::too_many_arguments)] - pub fn set_arc( - &mut self, - id: usize, - center_x: f32, - center_y: f32, - radius: f32, - start_angle: f32, - end_angle: f32, - stroke_width: f32, - color: [f32; 4], - depth: f32, - ) { - if let Some(content_state) = self.states.get_mut(&id) { - content_state.data = ContentData::Arc { - center_x, - center_y, - radius, - start_angle, - end_angle, - stroke_width, - color, - depth, - }; - content_state.render_data.needs_repaint = true; - content_state.render_data.should_remove = false; - } else { - self.states.insert( - id, - ContentState::new(ContentData::Arc { - center_x, - center_y, - radius, - start_angle, - end_angle, - stroke_width, - color, - depth, - }), - ); - } - } - - /// Set image rectangle at ID - #[inline] - #[allow(clippy::too_many_arguments)] - pub fn set_image( - &mut self, - id: usize, - x: f32, - y: f32, - width: f32, - height: f32, - color: [f32; 4], - coords: [f32; 4], - depth: f32, - atlas_layer: i32, - ) { - if let Some(content_state) = self.states.get_mut(&id) { - content_state.data = ContentData::Image { - x, - y, - width, - height, - color, - coords, - depth, - atlas_layer, - }; - content_state.render_data.needs_repaint = true; - content_state.render_data.should_remove = false; - } else { - self.states.insert( - id, - ContentState::new(ContentData::Image { - x, - y, - width, - height, - color, - coords, - depth, - atlas_layer, - }), - ); - } - } -} - -/// Run-level shaping cache (like ShaperCache). -/// -/// Caches pre-packed shaped runs per text run, keyed by (content + font_id). -/// The shaper always sees the full run so ligatures are handled naturally. -/// Stores packed GlyphData directly so cache hits avoid re-packing. -pub struct ShapingCache { - /// LRU cache per font_id: hash(content + font_id) → pre-packed run - inner: FxHashMap>, - /// Current shaping context - font_id: usize, - content_hash: u64, -} - -impl Default for ShapingCache { - fn default() -> Self { - Self::new() - } -} - -impl ShapingCache { - pub fn new() -> Self { - ShapingCache { - inner: FxHashMap::default(), - font_id: 0, - content_hash: 0, - } - } - - /// Look up a pre-packed cached run. - #[inline] - pub fn get(&mut self, font_id: &usize, content: &str) -> Option<&CachedRun> { - let key = Self::cache_key(content, *font_id); - if let Some(cache) = self.inner.get_mut(font_id) { - return cache.get(&key); - } - None - } - - /// Record which content is about to be shaped (called before shaping). - #[inline] - pub fn set_content(&mut self, font_id: usize, content: &str) { - self.font_id = font_id; - self.content_hash = Self::cache_key(content, font_id); - } - - /// Store a pre-packed run in the cache after shaping. - #[inline] - pub fn finish_with_run(&mut self, cached_run: CachedRun) { - if self.content_hash != 0 { - if let Some(cache) = self.inner.get_mut(&self.font_id) { - cache.put(self.content_hash, cached_run); - } else { - let size = if self.font_id == 0 { 512 } else { 256 }; - let mut cache = LruCache::new(NonZeroUsize::new(size).unwrap()); - cache.put(self.content_hash, cached_run); - self.inner.insert(self.font_id, cache); - } - } - self.font_id = 0; - self.content_hash = 0; - } - - /// Clear all caches (called when fonts change). - pub fn clear(&mut self) { - self.inner.clear(); - self.font_id = 0; - self.content_hash = 0; - debug!("ShapingCache cleared"); - } - - /// Compute a position-independent cache key from content and font_id. - #[inline] - pub fn cache_key(content: &str, font_id: usize) -> u64 { - let mut hasher = rustc_hash::FxHasher::default(); - content.hash(&mut hasher); - font_id.hash(&mut hasher); - hasher.finish() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use swash::shape::cluster::Glyph; - - /// Pixel-perfect when face dimensions are already integer: - /// rounding is a no-op so cell == face and the centering - /// adjustment vanishes. - #[test] - fn canonical_cell_metrics_no_rounding_delta() { - let cell = canonical_cell_metrics(16.0, 33.0, 4.0, 2.0); - assert_eq!(cell.cell_width, 16); - assert_eq!(cell.cell_height, 33); - // baseline = leading*0.5 + descent = 1 + 4 = 5 - assert_eq!(cell.cell_baseline, 5); - assert_eq!(cell.face_y, 0.0); - } - - /// Centering: face_height = 32.4 rounds DOWN to 32, so the - /// baseline must shift by half the rounding delta (≈ +0.2) - /// so the glyph stays vertically centered in the rounded cell. - #[test] - fn canonical_cell_metrics_centers_after_round_down() { - let cell = canonical_cell_metrics(16.0, 32.4, 4.0, 2.0); - assert_eq!(cell.cell_height, 32); - // face_baseline = 1 + 4 = 5 - // baseline_centered = 5 - (32 - 32.4) / 2 = 5 + 0.2 = 5.2 → round 5 - assert_eq!(cell.cell_baseline, 5); - // face_y = 5 - 5 = 0 (cell_baseline rounds back to face_baseline here) - assert!((cell.face_y - 0.0).abs() < 1e-9); - } - - /// Centering: face_height = 32.6 rounds UP to 33, baseline - /// shifts by ~ -0.3 (so the glyph drops slightly to stay - /// centered in the now-taller rounded cell). - #[test] - fn canonical_cell_metrics_centers_after_round_up() { - let cell = canonical_cell_metrics(16.0, 32.6, 4.0, 2.0); - assert_eq!(cell.cell_height, 33); - // face_baseline = 5 - // baseline_centered = 5 - (33 - 32.6) / 2 = 5 - 0.2 = 4.8 → round 5 - assert_eq!(cell.cell_baseline, 5); - } - - /// Invariants: 0 ≤ cell_baseline ≤ cell_height. - /// Holds across a sweep of face dimensions, including small - /// fonts where descent + half_gap might be larger than the - /// rounding delta. - #[test] - fn canonical_cell_metrics_baseline_within_cell() { - for face_h in [12.0, 13.4, 18.7, 24.0, 33.0, 49.5, 66.0] { - let cell = canonical_cell_metrics(8.0, face_h, 3.0, 1.0); - assert!( - cell.cell_baseline <= cell.cell_height, - "baseline {} > cell_height {} at face_h={}", - cell.cell_baseline, - cell.cell_height, - face_h - ); - } - } - - /// `face_width.round().max(1.0)` clamps degenerate sub-pixel - /// widths so the divide path in `compute()` doesn't get a - /// zero stride. - #[test] - fn canonical_cell_metrics_clamps_width_floor() { - let cell = canonical_cell_metrics(0.3, 16.0, 4.0, 2.0); - assert_eq!(cell.cell_width, 1); - } - - fn create_test_glyph(id: u16, x: f32, y: f32, advance: f32) -> Glyph { - Glyph { - id, - info: Default::default(), - x, - y, - advance, - data: Default::default(), - } - } - - fn make_cached_run(glyphs: &[(u16, f32)]) -> CachedRun { - use crate::layout::glyph::GlyphData; - let glyph_data: Vec = glyphs - .iter() - .map(|&(id, advance)| GlyphData::simple(id, advance, 0)) - .collect(); - let advance = glyphs.iter().map(|g| g.1).sum(); - let cache_key = 42; // dummy - CachedRun { - glyphs: glyph_data, - detailed_glyphs: vec![], - advance, - cache_key, - } - } - - #[test] - fn test_shaping_cache_hit_and_miss() { - let mut cache = ShapingCache::new(); - let font_id = 0; - - // Empty cache: miss - assert!(cache.get(&font_id, "hello").is_none()); - - // Store a pre-packed run for "hello" - cache.set_content(font_id, "hello"); - cache.finish_with_run(make_cached_run(&[ - (104, 8.0), - (101, 8.0), - (108, 8.0), - (108, 8.0), - (111, 8.0), - ])); - - // Same run: hit - assert!(cache.get(&font_id, "hello").is_some()); - assert_eq!(cache.get(&font_id, "hello").unwrap().glyphs.len(), 5); - - // Different run: miss - assert!(cache.get(&font_id, "world").is_none()); - - // Different font: miss - assert!(cache.get(&1, "hello").is_none()); - } - - #[test] - fn test_shaping_cache_ligature_preserved() { - let mut cache = ShapingCache::new(); - let font_id = 0; - - // Store "=>" as a single ligature glyph - cache.set_content(font_id, "=>"); - cache.finish_with_run(make_cached_run(&[(999, 16.0)])); - - // Should hit and preserve the ligature (1 glyph, not 2) - let cached = cache.get(&font_id, "=>").unwrap(); - assert_eq!(cached.glyphs.len(), 1); - } - - #[test] - fn test_shaping_cache_clear() { - let mut cache = ShapingCache::new(); - let font_id = 0; - - cache.set_content(font_id, "test"); - cache.finish_with_run(make_cached_run(&[(1, 8.0)])); - - assert!(cache.get(&font_id, "test").is_some()); - cache.clear(); - assert!(cache.get(&font_id, "test").is_none()); - } - - #[test] - fn test_shaping_cache_key_no_collision() { - let along_key = ShapingCache::cache_key("along", 1); - let clone_key = ShapingCache::cache_key("clone", 1); - assert_ne!(along_key, clone_key); - - // Same content, different font - assert_ne!( - ShapingCache::cache_key("test", 0), - ShapingCache::cache_key("test", 1), - ); - - // Deterministic - assert_eq!( - ShapingCache::cache_key("test", 0), - ShapingCache::cache_key("test", 0), - ); - } - - #[test] - fn test_empty_span_creates_fragment_with_none_content() { - // Simulates '\0' cells: None content means advance-only - let mut line = BuilderLine::default(); - - let range_a = line.push_text("A"); - line.fragments.push(FragmentData { - content: Some(range_a), - style: SpanStyle::default(), - }); - line.fragments.push(FragmentData { - content: None, // empty span (like '\0' cell) - style: SpanStyle::default(), - }); - let range_b = line.push_text("B"); - line.fragments.push(FragmentData { - content: Some(range_b), - style: SpanStyle::default(), - }); - - assert_eq!(line.fragments.len(), 3); - assert!(line.fragments[0].content.is_some()); - assert!(line.fragments[1].content.is_none()); - assert!(line.fragments[2].content.is_some()); - } - - #[test] - fn test_empty_run_has_no_glyphs() { - // Verify push_empty_run creates a run with empty glyphs - let mut render_data = RenderData::new(); - let metrics = swash::Metrics { - ascent: 12.0, - descent: 4.0, - leading: 0.0, - ..Default::default() - }; - - render_data.push_empty_run(SpanStyle::default(), 16.0, 0, &metrics); - - assert_eq!(render_data.runs.len(), 1); - assert!(render_data.runs[0].glyphs.is_empty()); - assert_eq!(render_data.runs[0].span.width, 1.0); - } - - #[test] - fn test_mixed_text_and_empty_runs_ordering() { - // Simulates a line like: "ABC" + [empty] + [empty] + "DEF" - // All runs should be in order and empty runs between text runs - let mut render_data = RenderData::new(); - let metrics = swash::Metrics { - ascent: 12.0, - descent: 4.0, - leading: 0.0, - ..Default::default() - }; - - // Simulate text run "ABC" with 3 glyphs - let glyphs_abc = [ - create_test_glyph(65, 0.0, 0.0, 8.0), - create_test_glyph(66, 0.0, 0.0, 8.0), - create_test_glyph(67, 0.0, 0.0, 8.0), - ]; - render_data.push_run_without_shaper( - SpanStyle::default(), - 16.0, - 0, - &glyphs_abc, - &metrics, - ); - - // Two empty runs (simulating '\0' cells) - render_data.push_empty_run(SpanStyle::default(), 16.0, 0, &metrics); - render_data.push_empty_run(SpanStyle::default(), 16.0, 0, &metrics); - - // Another text run "DEF" - let glyphs_def = [ - create_test_glyph(68, 0.0, 0.0, 8.0), - create_test_glyph(69, 0.0, 0.0, 8.0), - create_test_glyph(70, 0.0, 0.0, 8.0), - ]; - render_data.push_run_without_shaper( - SpanStyle::default(), - 16.0, - 0, - &glyphs_def, - &metrics, - ); - - // Should have 4 runs total in order - assert_eq!(render_data.runs.len(), 4); - // First run: 3 glyphs (ABC) - assert_eq!(render_data.runs[0].glyphs.len(), 3); - // Second run: empty - assert!(render_data.runs[1].glyphs.is_empty()); - // Third run: empty - assert!(render_data.runs[2].glyphs.is_empty()); - // Fourth run: 3 glyphs (DEF) - assert_eq!(render_data.runs[3].glyphs.len(), 3); - } - - #[test] - fn test_empty_run_preserves_background_color() { - // '\0' cells with colored background (like from \033[K) should preserve bg - let mut render_data = RenderData::new(); - let metrics = swash::Metrics { - ascent: 12.0, - descent: 4.0, - leading: 0.0, - ..Default::default() - }; - - let style_with_bg = SpanStyle { - background_color: Some([1.0, 0.0, 0.0, 1.0]), // red - ..SpanStyle::default() - }; - - render_data.push_empty_run(style_with_bg, 16.0, 0, &metrics); - - assert_eq!(render_data.runs.len(), 1); - assert!(render_data.runs[0].glyphs.is_empty()); - assert_eq!( - render_data.runs[0].span.background_color, - Some([1.0, 0.0, 0.0, 1.0]) - ); - } - - #[test] - fn test_empty_runs_survive_rebuild() { - // Simulates: printf '\033[41m\033[K\n\033[0m' - // Line with only empty fragments (None content) with colored bg. - // First build should create runs. Second build (simulating next frame - // where line is undamaged) should preserve them. - - let mut line = BuilderLine::default(); - - // Add 3 empty fragments with red bg (like \033[K erase with color) - let style_red_bg = SpanStyle { - background_color: Some([1.0, 0.0, 0.0, 1.0]), - ..SpanStyle::default() - }; - for _ in 0..3 { - line.fragments.push(FragmentData { - content: None, - style: style_red_bg, - }); - } - - assert_eq!(line.fragments.len(), 3); - assert!( - line.render_data.runs.is_empty(), - "runs should be empty before build" - ); - - // Simulate what process_text_line does for None fragments - let metrics = swash::Metrics { - ascent: 12.0, - descent: 4.0, - leading: 0.0, - ..Default::default() - }; - for frag in &line.fragments { - if frag.content.is_none() { - line.render_data - .push_empty_run(frag.style, 16.0, 0, &metrics); - } - } - - assert_eq!( - line.render_data.runs.len(), - 3, - "should have 3 empty runs after build" - ); - assert!(line.render_data.runs[0].glyphs.is_empty()); - assert_eq!( - line.render_data.runs[0].span.background_color, - Some([1.0, 0.0, 0.0, 1.0]) - ); - - // Now simulate "next frame" — line is NOT damaged, so fragments are - // cleared and re-added, but render_data should persist until rebuild. - // This is what happens in the partial update path: - // 1. clear_line clears fragments + render_data - // 2. create_line re-adds fragments - // 3. build_line re-processes - - // Simulate clear_line - line.fragments.clear(); - line.render_data.clear(); - - assert!( - line.render_data.runs.is_empty(), - "runs cleared after clear_line" - ); - - // Simulate re-adding same fragments - for _ in 0..3 { - line.fragments.push(FragmentData { - content: None, - style: style_red_bg, - }); - } - - // Simulate process_text_line again - for frag in &line.fragments { - if frag.content.is_none() { - line.render_data - .push_empty_run(frag.style, 16.0, 0, &metrics); - } - } - - assert_eq!( - line.render_data.runs.len(), - 3, - "runs should be restored after rebuild" - ); - assert_eq!( - line.render_data.runs[0].span.background_color, - Some([1.0, 0.0, 0.0, 1.0]) - ); - } - - #[test] - fn test_empty_runs_not_duplicated_on_full_rebuild() { - // Simulates the full rebuild path (content.build()) being called - // multiple times without clearing. Runs should NOT accumulate. - - let mut line = BuilderLine::default(); - - let style = SpanStyle { - background_color: Some([0.0, 1.0, 0.0, 1.0]), - ..SpanStyle::default() - }; - line.fragments.push(FragmentData { - content: None, - style, - }); - - let metrics = swash::Metrics { - ascent: 12.0, - descent: 4.0, - leading: 0.0, - ..Default::default() - }; - - // First build - for frag in &line.fragments { - if frag.content.is_none() { - line.render_data - .push_empty_run(frag.style, 16.0, 0, &metrics); - } - } - assert_eq!(line.render_data.runs.len(), 1); - - // Second build WITHOUT clearing — this simulates calling build() twice - for frag in &line.fragments { - if frag.content.is_none() { - line.render_data - .push_empty_run(frag.style, 16.0, 0, &metrics); - } - } - // BUG: runs accumulate! This is the issue. - assert_eq!( - line.render_data.runs.len(), - 2, - "runs duplicated without clear — this is the bug" - ); - } -} diff --git a/sugarloaf/src/layout/content_data.rs b/sugarloaf/src/layout/content_data.rs deleted file mode 100644 index 258dd23e..00000000 --- a/sugarloaf/src/layout/content_data.rs +++ /dev/null @@ -1,198 +0,0 @@ -// 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. - -use crate::layout::content::BuilderState; -use smallvec::SmallVec; - -/// Unified content data for all shape types -#[derive(Debug, Clone)] -pub enum ContentData { - /// Rich text content with full text rendering capabilities - Text(BuilderState), - - /// Simple rectangle - Rect { - x: f32, - y: f32, - width: f32, - height: f32, - color: [f32; 4], - depth: f32, - }, - - /// Rectangle with rounded corners - RoundedRect { - x: f32, - y: f32, - width: f32, - height: f32, - color: [f32; 4], - depth: f32, - border_radius: f32, - }, - - /// Line segment - Line { - x1: f32, - y1: f32, - x2: f32, - y2: f32, - width: f32, - color: [f32; 4], - depth: f32, - }, - - /// Triangle (3 points) - Triangle { - points: [(f32, f32); 3], - color: [f32; 4], - depth: f32, - }, - - /// Polygon with arbitrary number of points (inline up to 8) - Polygon { - points: SmallVec<[(f32, f32); 8]>, - color: [f32; 4], - depth: f32, - }, - - /// Arc segment - Arc { - center_x: f32, - center_y: f32, - radius: f32, - start_angle: f32, - end_angle: f32, - stroke_width: f32, - color: [f32; 4], - depth: f32, - }, - - /// Image rectangle - Image { - x: f32, - y: f32, - width: f32, - height: f32, - color: [f32; 4], - coords: [f32; 4], - depth: f32, - atlas_layer: i32, - }, -} - -/// Render metadata for content -#[derive(Debug, Clone)] -pub struct ContentRenderData { - /// Position where the content should be rendered [x, y] - pub position: [f32; 2], - /// Depth value for z-ordering - pub depth: f32, - /// Draw order for batch sorting (higher = drawn later = on top) - pub order: u8, - /// Whether this content should be hidden during rendering - pub hidden: bool, - /// Whether this content needs to be repainted - pub needs_repaint: bool, - /// Whether this content should be removed - pub should_remove: bool, - /// Whether this content is transient (render once then cleanup) - pub transient: bool, - /// Whether to use monospace grid dimensions for glyph positioning (terminal text) - /// When false, uses actual glyph advances (rich text) - pub use_grid_cell_size: bool, - /// Clipping bounds [x, y, width, height] in physical pixels. - /// When set, glyphs outside these bounds are skipped during rendering. - pub bounds: Option<[f32; 4]>, -} - -impl Default for ContentRenderData { - fn default() -> Self { - Self { - position: [0.0, 0.0], - depth: 0.0, - order: 0, - hidden: false, - needs_repaint: true, // Should paint initially - should_remove: false, - transient: false, - use_grid_cell_size: true, // Default to monospace grid for terminal - bounds: None, - } - } -} - -impl ContentRenderData { - /// 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; - } - } - - /// Set clipping bounds and mark for repaint if changed - pub fn set_bounds(&mut self, bounds: Option<[f32; 4]>) { - if self.bounds != bounds { - self.bounds = bounds; - 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; - } -} - -/// Unified content state that wraps data and render metadata -#[derive(Debug, Clone)] -pub struct ContentState { - pub data: ContentData, - pub render_data: ContentRenderData, -} - -impl ContentState { - pub fn new(data: ContentData) -> Self { - Self { - data, - render_data: ContentRenderData::default(), - } - } - - /// Check if this content is text - pub fn is_text(&self) -> bool { - matches!(self.data, ContentData::Text(_)) - } - - /// Get text data if this is a text content - pub fn as_text(&self) -> Option<&BuilderState> { - match &self.data { - ContentData::Text(state) => Some(state), - _ => None, - } - } - - /// Get mutable text data if this is a text content - pub fn as_text_mut(&mut self) -> Option<&mut BuilderState> { - match &mut self.data { - ContentData::Text(state) => Some(state), - _ => None, - } - } -} diff --git a/sugarloaf/src/layout/glyph.rs b/sugarloaf/src/layout/glyph.rs deleted file mode 100644 index ffe76206..00000000 --- a/sugarloaf/src/layout/glyph.rs +++ /dev/null @@ -1,97 +0,0 @@ -// 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. -// -// layout_data.rs was originally retired from dfrg/swash_demo licensed under MIT -// https://github.com/dfrg/swash_demo/blob/master/LICENSE - -use crate::layout::SpanStyle; -use swash::shape::cluster::Glyph as ShapedGlyph; -use swash::GlyphId; - -pub const GLYPH_DETAILED: u32 = 0x80000000; - -#[derive(Copy, Debug, Clone)] -pub struct GlyphData { - pub data: u32, - pub size: u32, -} - -impl GlyphData { - pub fn simple(id: u16, advance: f32, size: u32) -> Self { - let advance = (advance * 64.).max(0.) as u32; - Self { - data: id as u32 | ((advance & 0x7FFF) << 16), - size, - } - } - - pub fn is_simple(self) -> bool { - self.data & GLYPH_DETAILED == 0 - } - - pub fn simple_data(self) -> (u16, f32) { - ((self.data & 0xFFFF) as u16, (self.data >> 16) as f32 / 64.) - } - - pub fn detail_index(self) -> usize { - (self.data & !GLYPH_DETAILED) as usize - } - - pub fn add_spacing(&mut self, spacing: f32) { - let (id, advance) = self.simple_data(); - *self = Self::simple(id, (advance + spacing).max(0.), self.size); - } - - pub fn clear_advance(&mut self) { - let (id, _advance) = self.simple_data(); - *self = Self::simple(id, 0., self.size); - } -} - -#[derive(Debug, Clone)] -pub struct RunData { - pub span: SpanStyle, - pub line: u32, - pub size: f32, - pub glyphs: Vec, - pub detailed_glyphs: Vec, - pub ascent: f32, - pub descent: f32, - pub leading: f32, - pub underline_offset: f32, - pub strikeout_offset: f32, - pub strikeout_size: f32, - pub x_height: f32, - pub advance: f32, - /// Pre-computed cache key for text run lookup (0 = empty run, skip cache) - pub cache_key: u64, -} - -/// Shaped glyph in a paragraph. -#[derive(Copy, Debug, Clone)] -pub struct Glyph { - /// Glyph identifier. - pub id: GlyphId, - /// Horizontal offset. - pub x: f32, - /// Vertical offset. - pub y: f32, - /// Advance width or height. - pub advance: f32, - /// Span that generated the glyph. - pub span: usize, -} - -impl Glyph { - pub fn new(g: &ShapedGlyph) -> Self { - Self { - id: g.id, - x: g.x, - y: g.y, - advance: g.advance, - span: g.data as usize, - } - } -} diff --git a/sugarloaf/src/layout/mod.rs b/sugarloaf/src/layout/mod.rs index 2288427c..170bed90 100644 --- a/sugarloaf/src/layout/mod.rs +++ b/sugarloaf/src/layout/mod.rs @@ -7,22 +7,12 @@ // nav and span_style were originally retired from dfrg/swash_demo licensed under MIT // https://github.com/dfrg/swash_demo/blob/master/LICENSE -pub mod content; -pub mod content_data; -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, FragmentData, ShapingCache, - SpanStyle, SpanStyleDecoration, UnderlineInfo, UnderlineShape, +pub mod span; + +pub use span::{ + FontSettingCache, FontSettingKey, SpanStyle, SpanStyleDecoration, UnderlineInfo, + UnderlineShape, EMPTY_FONT_SETTINGS, }; -pub use content_data::{ContentData, ContentRenderData, ContentState}; -pub use render_data::Run; /// Index of a span in sequential order of submission to a paragraph content. #[derive(Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Hash, Default, Debug)] @@ -214,3 +204,128 @@ impl RootStyle { } } } + +/// Pure helper: compute canonical [`CellMetrics`] from already-scaled +/// face dimensions. Used by [`compute_cell_metrics`] and is the same +/// formula the previous `Content::set_text` ran inline. +/// +/// Centering invariant: if `face_height` is `33.4` and rounds to +/// `33`, the baseline shifts up by `0.2` so the glyph stays centered +/// in the rounded cell — matches the half-rounding-delta adjustment +/// used elsewhere for vertical pixel-snapping. +#[inline] +pub fn canonical_cell_metrics( + face_width: f64, + face_height: f64, + descent_phys: f64, + leading_phys: f64, +) -> CellMetrics { + let cell_width = face_width.round().max(1.0) as u32; + let cell_height = face_height.round().max(1.0) as u32; + let face_baseline = leading_phys * 0.5 + descent_phys; + let baseline_centered = face_baseline - (cell_height as f64 - face_height) * 0.5; + let cell_baseline = baseline_centered.round().max(0.0) as u32; + let face_y = cell_baseline as f64 - face_baseline; + CellMetrics { + cell_width, + cell_height, + cell_baseline, + face_width, + face_height, + face_y, + } +} + +/// Compute the canonical [`CellMetrics`] for a `(font_size, +/// line_height, scale_factor)` triple using `font_library`'s primary +/// font. Pure function — no per-id state, no caching, no +/// side effects. Callers (rioterm's `ContextDimension`, future panel +/// owners) recompute on font / size / scale change and store the +/// result themselves. +/// +/// `line_height` is the user's config multiplier; it's applied to +/// `face_height` here, so callers MUST NOT re-apply it. +/// +/// Mirrors `Content::calculate_character_cell_dimensions`'s formula +/// exactly so dimensions stay byte-identical across the migration. +/// Once `Content` is deleted that helper goes with it. +#[inline] +pub fn compute_cell_metrics( + font_library: &crate::font::FontLibrary, + font_size: f32, + line_height: f32, + scale_factor: f32, +) -> (TextDimensions, CellMetrics) { + let scale_f64 = scale_factor as f64; + let line_height_mod = line_height as f64; + + let raw: Option<(f64, f64, f64, f64)> = { + #[cfg(target_os = "macos")] + { + font_library.ct_font(0).map(|handle| { + let m = crate::font::macos::font_metrics(&handle, font_size); + let cw = crate::font::macos::max_ascii_advance_px(&handle, font_size) + .or_else(|| { + crate::font::macos::advance_units_for_char(&handle, ' ') + .map(|(units, upem)| units * font_size / upem as f32) + }) + .unwrap_or(font_size); + ( + cw as f64, + m.ascent as f64, + m.descent as f64, + m.leading as f64, + ) + }) + } + #[cfg(not(target_os = "macos"))] + { + font_library.inner.try_read().and_then(|lib| { + let id = 0; + let (data, offset, _key) = lib.get_data(&id)?; + let font_ref = swash::FontRef::from_index(&data, offset as usize)?; + let m = font_ref.metrics(&[]); + let upem = m.units_per_em as f32; + let s = font_size / upem; + let glyph = font_ref.charmap().map(' ' as u32); + let advance = font_ref.glyph_metrics(&[]).advance_width(glyph); + let cw = if advance > 0.0 { + advance * s + } else { + font_size + }; + Some(( + cw as f64, + (m.ascent * s) as f64, + (m.descent.abs() * s) as f64, + (m.leading * s) as f64, + )) + }) + } + }; + + let (face_width, face_height, descent_phys, leading_phys) = + if let Some((cw, ascent, descent, leading)) = raw { + let face_width = cw * scale_f64; + let face_height = (ascent + descent + leading) * line_height_mod * scale_f64; + ( + face_width, + face_height, + descent * line_height_mod * scale_f64, + leading * line_height_mod * scale_f64, + ) + } else { + let fw = font_size as f64 * scale_f64; + let fh = font_size as f64 * line_height_mod * scale_f64; + (fw, fh, 0.0, 0.0) + }; + + let cell = + canonical_cell_metrics(face_width, face_height, descent_phys, leading_phys); + let dims = TextDimensions { + width: cell.cell_width as f32, + height: cell.cell_height as f32, + scale: scale_factor, + }; + (dims, cell) +} diff --git a/sugarloaf/src/layout/render_data.rs b/sugarloaf/src/layout/render_data.rs deleted file mode 100644 index b8cebc09..00000000 --- a/sugarloaf/src/layout/render_data.rs +++ /dev/null @@ -1,442 +0,0 @@ -// 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. -// -// layout.rs was originally retired from dfrg/swash_demo licensed under MIT -// https://github.com/dfrg/swash_demo/blob/master/LICENSE -// -// This file however suffered updates made by Raphael Amorim to support -// underline_color, background_color, text color and other functionalities - -//! RenderData. -use super::glyph::*; -use crate::layout::content::{CachedRun, ShapingCache, SpanStyleDecoration}; -use crate::layout::SpanStyle; -use crate::sugarloaf::primitives::SugarCursor; -use crate::{Graphic, GraphicId}; -use std::hash::Hasher; -#[cfg(not(target_os = "macos"))] -use swash::shape::Shaper; -use swash::Metrics; -use wyhash::WyHash; - -/// Compute a cache key from glyph IDs, font_id and size. -/// Position-independent: same glyphs at different screen positions produce the same key. -#[inline] -fn compute_cache_key(glyphs: &[GlyphData], font_id: usize, size: f32) -> u64 { - let mut hasher = WyHash::with_seed(0); - for (i, g) in glyphs.iter().enumerate() { - hasher.write_u16(g.simple_data().0); - hasher.write_usize(i); - } - hasher.write_usize(glyphs.len()); - hasher.write_usize(font_id); - hasher.write_u32((size * 100.0) as u32); - hasher.finish() -} - -/// Collection of text, organized into lines, runs and clusters. -#[derive(Clone, Debug, Default)] -pub struct RenderData { - pub runs: Vec, - pub glyphs: Vec, - pub detailed_glyphs: Vec, - pub graphics: std::collections::HashSet, -} - -impl RenderData { - #[inline] - pub fn is_empty(&self) -> bool { - self.runs.is_empty() - } - - /// Creates a new empty paragraph. - pub fn new() -> Self { - Self::default() - } - - #[inline] - pub fn reserve(&mut self, capacity: usize) { - self.runs.reserve(capacity); - self.glyphs.reserve(capacity); - self.detailed_glyphs.reserve(capacity); - self.graphics.reserve(capacity); - } - - #[inline] - pub fn with_capacity(capacity: usize) -> Self { - Self { - runs: Vec::with_capacity(capacity), - glyphs: Vec::with_capacity(capacity), - detailed_glyphs: Vec::with_capacity(capacity), - graphics: std::collections::HashSet::with_capacity(capacity), - } - } - - /// Clears the paragraph. - #[inline] - pub fn clear(&mut self) { - self.runs.clear(); - self.glyphs.clear(); - self.detailed_glyphs.clear(); - self.graphics.clear(); - } -} - -impl RenderData { - #[cfg(not(target_os = "macos"))] - #[allow(clippy::too_many_arguments)] - pub(super) fn push_run( - &mut self, - style: SpanStyle, - size: f32, - line: u32, - shaper: Shaper<'_>, - shaping_cache: &mut ShapingCache, - ) { - let metrics = shaper.metrics(); - - let mut glyphs = vec![]; - let mut detailed_glyphs = vec![]; - let mut advance = 0.; - - shaper.shape_with(|c| { - let mut cluster_advance = 0.; - for glyph in c.glyphs { - cluster_advance += glyph.advance; - const MAX_SIMPLE_ADVANCE: u32 = 0x7FFF; - if glyph.x == 0. && glyph.y == 0. { - let packed_advance = (glyph.advance * 64.) as u32; - if packed_advance <= MAX_SIMPLE_ADVANCE { - glyphs.push(GlyphData { - data: glyph.id as u32 | (packed_advance << 16), - size: glyph.data, - }); - continue; - } - } - let detail_index = detailed_glyphs.len() as u32; - detailed_glyphs.push(Glyph::new(glyph)); - glyphs.push(GlyphData { - data: GLYPH_DETAILED | detail_index, - size: glyph.data, - }); - } - advance += cluster_advance; - }); - - if let Some(graphic) = style.media { - self.graphics.insert(graphic.id); - } - - let cache_key = compute_cache_key(&glyphs, style.font_id, size); - - // Store pre-packed run in shaping cache - shaping_cache.finish_with_run(CachedRun { - glyphs: glyphs.clone(), - detailed_glyphs: detailed_glyphs.clone(), - advance, - cache_key, - }); - - let run_data = RunData { - span: style, - line, - size, - detailed_glyphs, - glyphs, - ascent: metrics.ascent, - descent: metrics.descent, - leading: metrics.leading, - underline_offset: metrics.underline_offset, - strikeout_offset: metrics.strikeout_offset, - strikeout_size: metrics.stroke_size, - x_height: metrics.x_height, - advance, - cache_key, - }; - self.runs.push(run_data); - } - - /// macOS equivalent of `push_run`: consumes a pre-shaped slice from - /// CoreText instead of running the swash `Shaper` callback. - /// - /// Packing / cache-fill / `RunData` layout are byte-identical to the - /// swash path, so the cache-hit path (`push_cached_run`) and downstream - /// composition don't care which shaper produced the glyphs. - /// - /// `metrics` comes from [`crate::font::macos::font_metrics`] — CoreText - /// native ascent/descent/leading/underline, plus strikeout derived from - /// x-height (CT has no strikeout API). - #[cfg(target_os = "macos")] - #[allow(clippy::too_many_arguments)] - pub(super) fn push_run_macos( - &mut self, - style: SpanStyle, - size: f32, - line: u32, - shaped: &[crate::font::macos::ShapedGlyph], - metrics: &crate::font::macos::FontMetrics, - shaping_cache: &mut ShapingCache, - ) { - let mut glyphs = Vec::with_capacity(shaped.len()); - let mut detailed_glyphs = Vec::new(); - let mut advance = 0.0f32; - - for g in shaped { - advance += g.advance; - const MAX_SIMPLE_ADVANCE: u32 = 0x7FFF; - if g.x == 0.0 && g.y == 0.0 { - let packed_advance = (g.advance * 64.0) as u32; - if packed_advance <= MAX_SIMPLE_ADVANCE { - glyphs.push(GlyphData { - data: g.id as u32 | (packed_advance << 16), - size: g.cluster, - }); - continue; - } - } - let detail_index = detailed_glyphs.len() as u32; - detailed_glyphs.push(Glyph { - id: g.id, - x: g.x, - y: g.y, - advance: g.advance, - span: g.cluster as usize, - }); - glyphs.push(GlyphData { - data: GLYPH_DETAILED | detail_index, - size: g.cluster, - }); - } - - if let Some(graphic) = style.media { - self.graphics.insert(graphic.id); - } - - let cache_key = compute_cache_key(&glyphs, style.font_id, size); - - shaping_cache.finish_with_run(CachedRun { - glyphs: glyphs.clone(), - detailed_glyphs: detailed_glyphs.clone(), - advance, - cache_key, - }); - - let run_data = RunData { - span: style, - line, - size, - detailed_glyphs, - glyphs, - ascent: metrics.ascent, - descent: metrics.descent, - leading: metrics.leading, - underline_offset: metrics.underline_offset, - strikeout_offset: metrics.strikeout_offset, - strikeout_size: metrics.strikeout_thickness, - x_height: metrics.x_height, - advance, - cache_key, - }; - self.runs.push(run_data); - } - - /// Push a pre-packed cached run — no repacking, no hashing. - #[allow(clippy::too_many_arguments)] - pub(super) fn push_cached_run( - &mut self, - style: SpanStyle, - size: f32, - line: u32, - cached: &CachedRun, - ascent: f32, - descent: f32, - leading: f32, - ) { - if let Some(graphic) = style.media { - self.graphics.insert(graphic.id); - } - let run_data = RunData { - span: style, - line, - size, - glyphs: cached.glyphs.clone(), - detailed_glyphs: cached.detailed_glyphs.clone(), - ascent, - descent, - leading, - underline_offset: 0., - strikeout_offset: 0., - strikeout_size: 0., - x_height: 0., - advance: cached.advance, - cache_key: cached.cache_key, - }; - self.runs.push(run_data); - } - - /// Test helper: pack a flat list of shaped glyphs into a `RunData` - /// as if `push_run` had been called, without going through swash's - /// `Shaper`. `glyphs_in` is cluster-flattened — cluster boundaries - /// don't affect the pack math (advance just sums across all glyphs) - /// so the signature takes a single slice. - #[cfg(test)] - pub(super) fn push_run_without_shaper( - &mut self, - style: SpanStyle, - size: f32, - line: u32, - glyphs_in: &[swash::shape::cluster::Glyph], - metrics: &Metrics, - ) -> bool { - let mut advance = 0.; - let mut glyphs = vec![]; - let mut detailed_glyphs = vec![]; - - for glyph in glyphs_in { - advance += glyph.advance; - const MAX_SIMPLE_ADVANCE: u32 = 0x7FFF; - if glyph.x == 0. && glyph.y == 0. { - let packed_advance = (glyph.advance * 64.) as u32; - if packed_advance <= MAX_SIMPLE_ADVANCE { - // Simple glyph - glyphs.push(GlyphData { - data: glyph.id as u32 | (packed_advance << 16), - size: glyph.data, - }); - continue; - } - } - // Complex glyph - let detail_index = detailed_glyphs.len() as u32; - detailed_glyphs.push(Glyph::new(glyph)); - glyphs.push(GlyphData { - data: GLYPH_DETAILED | detail_index, - size: glyph.data, - }); - } - if let Some(graphic) = style.media { - self.graphics.insert(graphic.id); - } - let cache_key = compute_cache_key(&glyphs, style.font_id, size); - let run_data = RunData { - span: style, - line, - size, - detailed_glyphs, - glyphs, - ascent: metrics.ascent, - descent: metrics.descent, - leading: metrics.leading, - underline_offset: metrics.underline_offset, - strikeout_offset: metrics.strikeout_offset, - strikeout_size: metrics.stroke_size, - x_height: metrics.x_height, - advance, - cache_key, - }; - self.runs.push(run_data); - true - } - - /// Push an empty run that advances position without any glyphs. - /// Used for unwritten cells ('\0') that need to occupy space. - pub(super) fn push_empty_run( - &mut self, - style: SpanStyle, - size: f32, - line: u32, - metrics: &Metrics, - ) { - let run_data = RunData { - span: style, - line, - size, - detailed_glyphs: vec![], - glyphs: vec![], - ascent: metrics.ascent, - descent: metrics.descent, - leading: metrics.leading, - underline_offset: metrics.underline_offset, - strikeout_offset: metrics.strikeout_offset, - strikeout_size: metrics.stroke_size, - x_height: metrics.x_height, - advance: 0., - cache_key: 0, - }; - self.runs.push(run_data); - } -} - -/// Sequence of clusters sharing the same font, size and span. -#[derive(Copy, Clone)] -pub struct Run<'a> { - pub(super) run: &'a RunData, -} - -impl Run<'_> { - /// Returns the span that contains the run. - #[inline] - pub fn span(&self) -> SpanStyle { - self.run.span - } - - #[inline] - pub fn media(&self) -> Option { - self.run.span.media - } - - /// Returns the font for the run. - #[inline] - pub fn font(&self) -> &usize { - &self.run.span.font_id - } - - /// Returns the font size for the run. - #[inline] - pub fn font_size(&self) -> f32 { - self.run.size - } - - /// Returns the color for the run. - #[inline] - pub fn color(&self) -> [f32; 4] { - self.run.span.color - } - - #[inline] - pub fn char_width(&self) -> f32 { - self.run.span.width - } - - /// Returns the cursor - #[inline] - pub fn cursor(&self) -> Option { - self.run.span.cursor - } - - /// Returns the advance of the run. - #[inline] - pub fn advance(&self) -> f32 { - self.run.advance - } - - /// Returns true if the run has an background color - #[inline] - pub fn background_color(&self) -> Option<[f32; 4]> { - self.run.span.background_color - } - - /// Returns true if the run has an underline decoration. - #[inline] - pub fn decoration(&self) -> Option { - self.run.span.decoration - } - - #[inline] - pub fn decoration_color(&self) -> Option<[f32; 4]> { - self.run.span.decoration_color - } -} diff --git a/sugarloaf/src/layout/rich_text_render_data.rs b/sugarloaf/src/layout/rich_text_render_data.rs deleted file mode 100644 index 4429dded..00000000 --- a/sugarloaf/src/layout/rich_text_render_data.rs +++ /dev/null @@ -1,70 +0,0 @@ -// 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], - /// Depth value for z-ordering (more negative = closer to camera/in front) - pub depth: f32, - /// 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], - depth: 0.0, // Default depth - 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], - depth: 0.0, - 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/layout/span.rs b/sugarloaf/src/layout/span.rs new file mode 100644 index 00000000..6e61f499 --- /dev/null +++ b/sugarloaf/src/layout/span.rs @@ -0,0 +1,142 @@ +// 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. + +//! Span-level styling: the type set that survived the rich-text +//! emission deletion. `SpanStyle` is shared across grid emit (via +//! `font_cache::FontCache::find_best_font_match`), UI text shaping +//! (`crate::text`), and rio's frontend (font_cache + grid_emit). The +//! decoration enums are read by the underline / strikethrough quad +//! batches in the renderer. + +use crate::sugarloaf::primitives::SugarCursor; +use crate::{DrawableChar, Graphic}; +use swash::{Attributes, Setting}; + +/// Index into a font setting cache. +pub type FontSettingKey = u32; + +/// Cache of tag/value pairs for font settings. +#[derive(Default, Clone, Debug)] +pub struct FontSettingCache { + settings: Vec>, + lists: Vec, + tmp: Vec>, +} + +impl FontSettingCache { + pub fn get(&self, key: u32) -> &[Setting] { + if key == !0 { + &[] + } else { + self.lists + .get(key as usize) + .map(|list| list.get(&self.settings)) + .unwrap_or(&[]) + } + } + + pub fn clear(&mut self) { + self.settings.clear(); + self.lists.clear(); + self.tmp.clear(); + } +} + +/// Sentinel for an empty set of font settings. +pub const EMPTY_FONT_SETTINGS: FontSettingKey = !0; + +/// Range within a font setting cache. +#[derive(Copy, Clone, Debug)] +struct FontSettingList { + pub start: u32, + pub end: u32, +} + +impl FontSettingList { + pub fn get(self, elements: &[T]) -> &[T] { + elements + .get(self.start as usize..self.end as usize) + .unwrap_or(&[]) + } +} + +#[repr(u8)] +#[derive(Copy, Clone, PartialEq, Debug, Default)] +pub enum UnderlineShape { + #[default] + Regular = 0, + Dotted = 1, + Dashed = 2, + Curly = 3, +} + +#[derive(Copy, Clone, PartialEq, Debug)] +pub struct UnderlineInfo { + pub is_doubled: bool, + pub shape: UnderlineShape, +} + +#[derive(Copy, Clone, PartialEq, Debug)] +pub enum SpanStyleDecoration { + Underline(UnderlineInfo), + Strikethrough, +} + +#[derive(Copy, Clone, PartialEq, Debug)] +pub struct SpanStyle { + pub font_id: usize, + /// Unicode width. + pub width: f32, + /// Font attributes. + pub font_attrs: Attributes, + /// Font color. + pub color: [f32; 4], + /// Background color. + pub background_color: Option<[f32; 4]>, + /// Font variations. + pub font_vars: FontSettingKey, + /// Enable underline / strikethrough decoration. + pub decoration: Option, + /// Decoration color. + pub decoration_color: Option<[f32; 4]>, + /// Cursor style. + pub cursor: Option, + /// Media (kitty-protocol image). + pub media: Option, + /// Drawable character (Unicode box-drawing / Powerline / sextants). + pub drawable_char: Option, + /// PUA constraint width: how many cells the glyph should visually + /// fill. None for normal glyphs, Some(1.0) or Some(2.0) for PUA + /// glyphs. Does NOT affect positioning/advance — only compositor + /// scaling. + pub pua_constraint: Option, + /// Optional per-glyph Nerd Font constraint (size / alignment / + /// padding) sourced from the Nerd Fonts patcher table. When set, + /// the compositor lays the glyph out using the constraint math + /// in `nerd_font_attributes` instead of the generic cell-centered + /// fit. Only populated by the renderer for codepoints with a + /// table entry (`get_constraint`). + pub nerd_font_constraint: Option, +} + +impl Default for SpanStyle { + fn default() -> Self { + Self { + font_id: 0, + width: 1.0, + font_attrs: Attributes::default(), + font_vars: EMPTY_FONT_SETTINGS, + color: [1.0, 1.0, 1.0, 1.0], + background_color: None, + cursor: None, + decoration: None, + decoration_color: None, + media: None, + drawable_char: None, + pua_constraint: None, + nerd_font_constraint: None, + } + } +} diff --git a/sugarloaf/src/lib.rs b/sugarloaf/src/lib.rs index 07c4c4a4..fd074cfa 100644 --- a/sugarloaf/src/lib.rs +++ b/sugarloaf/src/lib.rs @@ -27,8 +27,7 @@ pub use crate::sugarloaf::{ }, primitives::{ contains_braille_dot, drawable_character, is_private_user_area, Corners, - CursorKind, DrawableChar, ImageProperties, Object, Quad, Rect, RichText, - RichTextLinesRange, RichTextRenderData, SugarCursor, + CursorKind, DrawableChar, ImageProperties, Quad, Rect, SugarCursor, }, Color, Colorspace, Sugarloaf, SugarloafBackend, SugarloafErrors, SugarloafRenderer, SugarloafWindow, SugarloafWindowSize, SugarloafWithErrors, @@ -37,6 +36,5 @@ pub use crate::sugarloaf::{ #[cfg(feature = "wgpu")] pub use components::filters::Filter; pub use layout::{ - Content, RichTextConfig, SpanStyle, SpanStyleDecoration, TextDimensions, - UnderlineInfo, UnderlineShape, + SpanStyle, SpanStyleDecoration, TextDimensions, UnderlineInfo, UnderlineShape, }; diff --git a/sugarloaf/src/renderer/mod.rs b/sugarloaf/src/renderer/mod.rs index ea07e19e..05ffbf00 100644 --- a/sugarloaf/src/renderer/mod.rs +++ b/sugarloaf/src/renderer/mod.rs @@ -1012,7 +1012,7 @@ impl Renderer { pub fn prepare( &mut self, context: &mut crate::context::Context, - state: &crate::sugarloaf::state::SugarState, + _state: &crate::sugarloaf::state::SugarState, _graphics: &mut Graphics, image_data: &mut rustc_hash::FxHashMap< u32, @@ -1027,165 +1027,20 @@ impl Renderer { self.vertices.clear(); self.draw_cmds.clear(); - // Iterate over all content states and render visible ones. - // The Text arm is gone — rich-text emission replaced by - // `sugarloaf::text` (UI) and `grid_emit::build_row_fg` - // (terminal). The remaining arms are shape primitives the - // frontend still drives through `sugarloaf.rect()` etc. - for content_state in state.content.states.values() { - // Skip if marked for removal or hidden - if content_state.render_data.should_remove || content_state.render_data.hidden - { - continue; - } - - // Set clip_rect for this content element's bounds - self.comp.batches.clip_rect = - content_state.render_data.bounds.unwrap_or([0.0; 4]); - - match &content_state.data { - crate::layout::ContentData::Text(_) => { - // Rich-text Text content is inert — the builder - // state is kept for panel font-size / dimensions - // bookkeeping but no glyphs are emitted from it. - } - crate::layout::ContentData::Rect { - x, - y, - width, - height, - color, - depth, - } => { - self.comp.batches.rect( - &Rect::new(*x, *y, *width, *height), - *depth, - color, - 0, - ); - } - crate::layout::ContentData::RoundedRect { - x, - y, - width, - height, - color, - depth, - border_radius, - } => { - self.comp.batches.rounded_rect( - &Rect::new(*x, *y, *width, *height), - *depth, - color, - *border_radius, - 0, - ); - } - crate::layout::ContentData::Line { - x1, - y1, - x2, - y2, - width, - color, - depth, - } => { - self.comp - .batches - .add_line(*x1, *y1, *x2, *y2, *width, *depth, *color); - } - crate::layout::ContentData::Triangle { - points, - color, - depth, - } => { - self.comp.batches.add_triangle( - points[0].0, - points[0].1, - points[1].0, - points[1].1, - points[2].0, - points[2].1, - *depth, - *color, - ); - } - crate::layout::ContentData::Polygon { - points, - color, - depth, - } => { - self.comp - .batches - .add_polygon(points.as_slice(), *depth, *color); - } - crate::layout::ContentData::Arc { - center_x, - center_y, - radius, - start_angle, - end_angle, - stroke_width, - color, - depth, - } => { - self.comp.batches.add_arc( - *center_x, - *center_y, - *radius, - *start_angle, - *end_angle, - *stroke_width, - *depth, - color, - ); - } - crate::layout::ContentData::Image { - x, - y, - width, - height, - color, - coords, - depth, - atlas_layer, - } => { - self.comp.batches.add_image_rect( - &Rect::new(*x, *y, *width, *height), - *depth, - color, - coords, - *atlas_layer, - ); - } - } - } - - // Transient texts gone — previously rendered one-shot rich - // text overlays (welcome screen / dialog); migrated to - // `sugarloaf::text` immediate-mode primitive. - - // Reset clip_rect after rendering all content - self.comp.batches.clip_rect = [0.0; 4]; - - // Image overlays come from the per-panel `image_overlays` map - // on `Sugarloaf`. Visibility filter: skip hidden panels so - // inactive-tab overlays don't bleed through. We still consult - // `state.content.states[id].render_data.hidden` for that — - // panel visibility bookkeeping lives in Content for now while - // the rest of the rich-text pipeline is being torn down. - let overlays: Vec<_> = image_overlays - .iter() - .filter(|(id, _)| { - state - .content - .states - .get(id) - .map(|cs| !cs.render_data.hidden) - .unwrap_or(true) - }) - .flat_map(|(_, v)| v.iter()) - .collect(); + // The per-id `Content.states` walk is gone — non-Text content + // arms (Rect/RoundedRect/Line/Triangle/Polygon/Arc/Image) had + // no rio caller passing `Some(id)`, so the Content registry + // never accumulated them. Immediate-mode primitives flow + // through `Renderer::rect/quad/...` straight into + // `comp.batches`; rich-text emission is handled by the grid + // pass and `sugarloaf::text`. + + // Image overlays: rio is responsible for not leaving stale + // overlays for hidden panels (callers `clear_image_overlays_for` + // on hide / panel removal). The renderer just drains whatever + // `image_overlays` currently holds. + let overlays: Vec<_> = + image_overlays.iter().flat_map(|(_, v)| v.iter()).collect(); if !overlays.is_empty() { self.render_graphic_overlays(context, image_data, &overlays); } else { diff --git a/sugarloaf/src/sugarloaf.rs b/sugarloaf/src/sugarloaf.rs index 1b6886da..8e8a7a6d 100644 --- a/sugarloaf/src/sugarloaf.rs +++ b/sugarloaf/src/sugarloaf.rs @@ -7,14 +7,12 @@ use crate::components::core::image::Handle; use crate::components::filters::{Filter, FiltersBrush}; use crate::font::{fonts::SugarloafFont, FontLibrary}; use crate::font_cache::{compute_advance, resolve_with, FontCache, ResolvedGlyph}; -use crate::layout::{RootStyle, TextLayout}; +use crate::layout::RootStyle; use crate::renderer::Renderer; use crate::sugarloaf::graphics::{GraphicDataEntry, Graphics}; use swash::Attributes; use crate::context::Context; -use crate::Content; -use crate::TextDimensions; use core::fmt::{Debug, Formatter}; use primitives::ImageProperties; use raw_window_handle::{ @@ -354,7 +352,7 @@ impl Sugarloaf<'_> { if let Some(cached) = self.font_cache.get(&(ch, attrs)) { return *cached; } - let font_lib = self.state.content.font_library().clone(); + let font_lib = self.state.fonts.clone(); resolve_with(&mut self.font_cache, &font_lib, ch, attrs) } @@ -391,7 +389,7 @@ impl Sugarloaf<'_> { } let computed = { - let font_ctx = self.state.content.font_library().inner.read(); + let font_ctx = self.state.fonts.inner.read(); compute_advance(&font_ctx, resolved.font_id, ch) }; // Cache both hits AND misses — misses become a zero-advance @@ -411,7 +409,7 @@ impl Sugarloaf<'_> { /// set changes rarely, and the one-off cost of walking the library /// is fine for a human-triggered lookup. pub fn font_family_names(&self) -> Vec { - self.state.content.font_library().family_names() + self.state.fonts.family_names() } /// Borrow the font library. Used by the grid emission path to @@ -419,7 +417,28 @@ impl Sugarloaf<'_> { /// own atlas. #[inline] pub fn font_library(&self) -> &crate::font::FontLibrary { - self.state.content.font_library() + &self.state.fonts + } + + /// Stateless cell-metrics computation. Callers (rioterm's + /// `ContextDimension`) recompute on font / size / scale change + /// and store the result themselves — sugarloaf no longer keeps + /// per-panel dimensions. Returns `(TextDimensions, CellMetrics)` + /// for `font_size` (in logical points) at `line_height` and + /// `scale_factor`. + #[inline] + pub fn compute_cell_metrics( + &self, + font_size: f32, + line_height: f32, + scale_factor: f32, + ) -> (crate::layout::TextDimensions, crate::layout::CellMetrics) { + crate::layout::compute_cell_metrics( + &self.state.fonts, + font_size, + line_height, + scale_factor, + ) } /// Resolve a batch of glyph queries with a single FontLibrary @@ -434,7 +453,7 @@ impl Sugarloaf<'_> { if queries.is_empty() { return Vec::new(); } - let font_lib = self.state.content.font_library().clone(); + let font_lib = self.state.fonts.clone(); let mut out = Vec::with_capacity(queries.len()); for &(ch, attrs) in queries { out.push(resolve_with(&mut self.font_cache, &font_lib, ch, attrs)); @@ -462,40 +481,6 @@ impl Sugarloaf<'_> { &mut self.state.style } - /// Update text font size based on action (0=reset, 1=decrease, 2=increase) - /// Returns true if the operation was applied, false if id is not text - #[inline] - pub fn set_text_font_size_action(&mut self, id: &usize, operation: u8) -> bool { - if self.state.content.get_text_by_id(*id).is_some() { - self.state.update_text_style(id, operation); - true - } else { - false - } - } - - /// Set font size for text content. Returns true if applied, false if id is not text - #[inline] - pub fn set_text_font_size(&mut self, id: &usize, font_size: f32) -> bool { - if self.state.content.get_text_by_id(*id).is_some() { - self.state.set_text_font_size(id, font_size); - true - } else { - false - } - } - - /// Set line height for text content. Returns true if applied, false if id is not text - #[inline] - pub fn set_text_line_height(&mut self, id: &usize, line_height: f32) -> bool { - if self.state.content.get_text_by_id(*id).is_some() { - self.state.set_text_line_height(id, line_height); - true - } else { - false - } - } - #[inline] /// Install librashader CRT/scanline filters. Only available with /// the `wgpu` feature — librashader's runtime is wgpu-only @@ -592,101 +577,6 @@ impl Sugarloaf<'_> { self.background_image = None; } - /// Remove content by ID (any type) - #[inline] - pub fn remove_content(&mut self, id: usize) { - self.state.content.remove_state(&id); - } - - /// Clear text content (resets to empty). Returns true if applied, false if id is not text - #[inline] - pub fn clear_text(&mut self, id: &usize) -> bool { - if self.state.content.get_text_by_id(*id).is_some() { - self.state.clear_text(id); - true - } else { - false - } - } - - pub fn content(&mut self) -> &mut Content { - self.state.content() - } - - #[inline] - pub fn get_text_by_id_mut( - &mut self, - id: usize, - ) -> Option<&mut crate::layout::BuilderState> { - self.state.content.get_text_by_id_mut(id) - } - - #[inline] - pub fn get_text_by_id(&mut self, id: usize) -> Option<&crate::layout::BuilderState> { - self.state.content.get_text_by_id(id) - } - - /// Device-pixel font size for the given rich-text id. This is - /// `layout.font_size * scale_factor` — the size glyphs should be - /// rasterized at. Mirrors per-text zoom (set via - /// `set_text_font_size_action`) so each panel can carry its own - /// size. Returns None for non-text ids or missing ids. - #[inline] - pub fn text_scaled_font_size(&self, id: &usize) -> Option { - self.state - .content - .get_text_by_id(*id) - .map(|s| s.scaled_font_size) - } - - #[inline] - pub fn build_text_by_id(&mut self, id: usize) { - self.state.content().sel(id).build(); - } - - #[inline] - pub fn build_text_by_id_line_number(&mut self, text_id: usize, line_number: usize) { - self.state.content().sel(text_id).build_line(line_number); - } - - /// Create or get text content. - /// - `id: Some(n)` - cached with id n, persistent across renders - /// - `id: None` - transient text, cleared after rendering. Returns index into transient vec. - #[inline] - pub fn text(&mut self, id: Option) -> usize { - match id { - Some(text_id) => { - // Check if text already exists - if self.state.content.get_text_by_id(text_id).is_none() { - // Create new text with default layout - let default_layout = - TextLayout::from_default_layout(&self.state.style); - self.state.content.set_text(text_id, &default_layout); - } - text_id - } - None => { - // Create transient text - let default_layout = TextLayout::from_default_layout(&self.state.style); - self.state.content.add_transient_text(&default_layout) - } - } - } - - /// Get the next available ID for cached content. - /// Returns the highest key + 1 (wrapping on overflow). - /// Useful for dynamically allocating IDs without hardcoded constants. - #[inline] - pub fn get_next_id(&self) -> usize { - self.state - .content - .states - .keys() - .max() - .map(|max_id| max_id.wrapping_add(1)) - .unwrap_or(0) - } - /// Add a rectangle to content system /// - `id: None` - not cached, rendered immediately /// - `id: Some(n)` - cached with id n, overwrites existing content @@ -695,7 +585,7 @@ impl Sugarloaf<'_> { #[allow(clippy::too_many_arguments)] pub fn rect( &mut self, - id: Option, + _id: Option, x: f32, y: f32, width: f32, @@ -708,39 +598,26 @@ impl Sugarloaf<'_> { let scaled_y = y * self.state.style.scale_factor; let scaled_width = width * self.state.style.scale_factor; let scaled_height = height * self.state.style.scale_factor; - - if let Some(content_id) = id { - self.state.content.set_rect( - content_id, - scaled_x, - scaled_y, - scaled_width, - scaled_height, - color, - depth, - ); - } else { - self.renderer.rect( - scaled_x, - scaled_y, - scaled_width, - scaled_height, - color, - depth, - order, - ); - } + // The `Some(id)` cached arm has no rio caller — every + // ephemeral rect goes through `Renderer.batches`. Argument + // kept for API stability with the other primitive helpers. + self.renderer.rect( + scaled_x, + scaled_y, + scaled_width, + scaled_height, + color, + depth, + order, + ); } - /// Add a rounded rectangle to content system - /// - `id: None` - not cached, rendered immediately - /// - `id: Some(n)` - cached with id n, overwrites existing content - /// - `order` - draw order (higher values render on top) + /// Add a rounded rectangle. Always immediate-mode now — see `rect`. #[inline] #[allow(clippy::too_many_arguments)] pub fn rounded_rect( &mut self, - id: Option, + _id: Option, x: f32, y: f32, width: f32, @@ -755,30 +632,16 @@ impl Sugarloaf<'_> { let scaled_width = width * self.state.style.scale_factor; let scaled_height = height * self.state.style.scale_factor; let scaled_border_radius = border_radius * self.state.style.scale_factor; - - if let Some(content_id) = id { - self.state.content.set_rounded_rect( - content_id, - scaled_x, - scaled_y, - scaled_width, - scaled_height, - color, - depth, - scaled_border_radius, - ); - } else { - self.renderer.rounded_rect( - scaled_x, - scaled_y, - scaled_width, - scaled_height, - color, - depth, - scaled_border_radius, - order, - ); - } + self.renderer.rounded_rect( + scaled_x, + scaled_y, + scaled_width, + scaled_height, + color, + depth, + scaled_border_radius, + order, + ); } /// Add a quad with per-corner radii and per-edge border widths @@ -823,14 +686,12 @@ impl Sugarloaf<'_> { ); } - /// Add an image rectangle to content system - /// - `id: None` - not cached, rendered immediately - /// - `id: Some(n)` - cached with id n, overwrites existing content + /// Add an image rectangle. Always immediate-mode now — see `rect`. #[inline] #[allow(clippy::too_many_arguments)] pub fn image_rect( &mut self, - id: Option, + _id: Option, x: f32, y: f32, width: f32, @@ -845,30 +706,16 @@ impl Sugarloaf<'_> { let scaled_width = width * self.state.style.scale_factor; let scaled_height = height * self.state.style.scale_factor; - if let Some(content_id) = id { - self.state.content.set_image( - content_id, - scaled_x, - scaled_y, - scaled_width, - scaled_height, - color, - coords, - depth, - atlas_layer, - ); - } else { - self.renderer.add_image_rect( - scaled_x, - scaled_y, - scaled_width, - scaled_height, - color, - coords, - depth, - atlas_layer, - ); - } + self.renderer.add_image_rect( + scaled_x, + scaled_y, + scaled_width, + scaled_height, + color, + coords, + depth, + atlas_layer, + ); } /// Draw an anti-aliased polygon from a list of points. @@ -956,49 +803,6 @@ impl Sugarloaf<'_> { ); } - /// Show content at a specific position (any type) - #[inline] - pub fn set_position(&mut self, id: usize, x: f32, y: f32) { - self.state.set_content_position(id, x, y); - } - - /// Set clipping bounds for content (physical pixels: [x, y, width, height]) - #[inline] - pub fn set_bounds(&mut self, id: usize, bounds: Option<[f32; 4]>) { - self.state.set_content_bounds(id, bounds); - } - - /// Set content visibility (any type) - #[inline] - pub fn set_visibility(&mut self, id: usize, visible: bool) { - self.state.set_content_hidden(id, !visible); - } - - /// Set content depth for z-ordering - #[inline] - pub fn set_depth(&mut self, id: usize, depth: f32) { - self.state.set_content_depth(id, depth); - } - - /// Set content draw order (higher = drawn later = on top) - #[inline] - pub fn set_order(&mut self, id: usize, order: u8) { - self.state.set_content_order(id, order); - } - - /// Get text layout. Returns None if id is not text - #[inline] - pub fn get_text_layout(&self, id: &usize) -> Option { - self.state.content.get_text_by_id(*id)?; - Some(self.state.get_state_layout(id)) - } - - /// Force update dimensions for text content - #[inline] - pub fn force_update_dimensions(&mut self, id: &usize) { - self.state.content.update_dimensions(id); - } - /// Immediate-mode text recorder for UI overlays. The per-sugarloaf /// `Text` instance. Overlays call `draw` / `measure` via this /// handle; sugarloaf flushes the recorded instances at render @@ -1032,54 +836,6 @@ impl Sugarloaf<'_> { } } - /// Get text dimensions. Returns None if id is not text - /// Get the total rendered width of text content by summing glyph advances. - /// Returns the width in logical (unscaled) pixels. - #[inline] - pub fn get_text_rendered_width(&self, id: &usize) -> f32 { - if let Some(builder_state) = self.state.content.get_text_by_id(*id) { - let scale = self.state.style.scale_factor; - let mut total: f32 = 0.0; - for line in &builder_state.lines { - for run in &line.render_data.runs { - total += run.advance; - } - } - total / scale - } else { - 0.0 - } - } - - #[inline] - pub fn get_text_dimensions(&mut self, id: &usize) -> Option { - if self.state.content.get_text_by_id(*id).is_some() { - Some(self.state.get_text_dimensions(id)) - } else { - None - } - } - - /// Canonical [`CellMetrics`] for `id`, mirroring - /// [`get_text_dimensions`]'s mark-for-repaint side effect. - /// Use alongside `get_text_dimensions` when constructing a - /// `ContextDimension` so the layout / GPU / mouse pipeline all - /// see the same `u32` cell stride. - #[inline] - pub fn get_text_cell_metrics( - &mut self, - id: &usize, - ) -> Option { - if self.state.content.get_text_by_id(*id).is_some() { - if let Some(content_state) = self.state.content.states.get_mut(id) { - content_state.render_data.needs_repaint = true; - } - Some(self.state.get_state_layout(id).cell) - } else { - None - } - } - #[inline] pub fn clear(&mut self) { self.state.clean_screen(); diff --git a/sugarloaf/src/sugarloaf/primitives.rs b/sugarloaf/src/sugarloaf/primitives.rs index a2582ba1..ba6a045b 100644 --- a/sugarloaf/src/sugarloaf/primitives.rs +++ b/sugarloaf/src/sugarloaf/primitives.rs @@ -113,63 +113,6 @@ impl Default for ImageProperties { } } -#[derive(Clone, Copy, Debug, PartialEq)] -pub struct RichTextLinesRange { - pub start: usize, - 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 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 - } -} - #[derive(Clone, Copy, Debug, PartialEq)] pub struct Rect { pub x: f32, @@ -222,13 +165,6 @@ impl Quad { } } -#[derive(Clone, Debug, PartialEq)] -pub enum Object { - Rect(Rect), - Quad(Quad), - RichText(RichText), -} - #[inline] pub fn is_private_user_area(character: &char) -> bool { matches!( diff --git a/sugarloaf/src/sugarloaf/state.rs b/sugarloaf/src/sugarloaf/state.rs index 9ea1e726..f0fca6ca 100644 --- a/sugarloaf/src/sugarloaf/state.rs +++ b/sugarloaf/src/sugarloaf/state.rs @@ -3,15 +3,23 @@ // This source code is licensed under the MIT license found in the // LICENSE file in the root directory of this source tree. +//! Sugarloaf-side global state — font handle, root style, and a +//! single visual-bell overlay slot. The previous version owned the +//! `Content` registry that tracked per-panel layout; that bookkeeping +//! moved to rio's `ContextDimension` (see +//! `memory/project_sugarloaf_content_drop.md`). This module is now +//! mostly a thin holder. + use crate::font::FontLibrary; -use crate::layout::{RootStyle, TextLayout}; +use crate::layout::RootStyle; use crate::renderer::Renderer; use crate::Graphics; -use crate::{Content, TextDimensions}; pub struct SugarState { pub style: RootStyle, - pub content: Content, + /// Live font handle. Cloned (Arc-shallow) into per-frame contexts. + /// Replaces the previous indirection through `Content`. + pub fonts: FontLibrary, pub visual_bell_overlay: Option, } @@ -19,145 +27,34 @@ impl SugarState { pub fn new( style: RootStyle, font_library: &FontLibrary, - font_features: &Option>, + _font_features: &Option>, ) -> SugarState { - let found_font_features = SugarState::found_font_features(font_features); - let mut content = Content::new(font_library); - content.set_font_features(found_font_features); - + // Font features used to be threaded through the rich-text + // shaper; with that pipeline gone they're no longer applied + // here. Grid-side shaping (`grid_emit`) and UI text shaping + // (`sugarloaf::text`) handle features inline at shape time. SugarState { - content, + fonts: font_library.clone(), style, visual_bell_overlay: None, } } + /// Compatibility shim used by the per-frame `compute_updates` — + /// the old shaper kept font-feature settings here. Kept as a + /// helper that returns an empty list so call sites don't need a + /// rewrite while the rest of the pipeline is being torn down. pub fn found_font_features( - font_features: &Option>, + _font_features: &Option>, ) -> Vec> { - let mut found_font_features = vec![]; - if let Some(features) = font_features { - for feature in features { - let setting: swash::Setting = (feature.as_str(), 1).into(); - found_font_features.push(setting); - } - } - - found_font_features - } - - #[inline] - pub fn contains_rich_text(&self, text_id: &usize) -> bool { - self.content.get_text_by_id(*text_id).is_some() - } - - #[inline] - pub fn contains_id(&self, id: &usize) -> bool { - self.content.states.contains_key(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) -> TextLayout { - if let Some(state) = self.content.get_state(id) { - state.layout - } else { - TextLayout::from_default_layout(&self.style) - } - } - - #[inline] - pub fn get_text_dimensions(&mut self, id: &usize) -> TextDimensions { - // Mark for repaint - if let Some(content_state) = self.content.states.get_mut(id) { - content_state.render_data.needs_repaint = true; - } - - if let Some(text_state) = self.content.get_state(id) { - let layout = &text_state.layout; - TextDimensions { - scale: layout.dimensions.scale, - width: layout.dimensions.width, - height: layout.dimensions.height, - } - } else { - TextDimensions::default() - } - } - - #[inline] - pub fn clean_screen(&mut self) { - // Mark all states for removal - they'll be re-added by route screen functions - for (_, content_state) in self.content.states.iter_mut() { - content_state.render_data.mark_for_removal(); - } - } - - #[inline] - pub fn update_text_style(&mut self, text_id: &usize, operation: u8) { - if let Some(text_state) = self.content.get_state_mut(text_id) { - let should_update = match operation { - 0 => text_state.reset_font_size(), - 2 => text_state.increase_font_size(), - 1 => text_state.decrease_font_size(), - _ => false, - }; - - if should_update { - text_state.layout.dimensions.height = 0.0; - text_state.layout.dimensions.width = 0.0; - } - } - - // Mark for repaint - if let Some(content_state) = self.content.states.get_mut(text_id) { - content_state.render_data.needs_repaint = true; - } - - self.compute_dimensions(); - } - - #[inline] - pub fn compute_dimensions(&mut self) { - // Collect text IDs that need repaint - let ids_to_repaint: Vec = self - .content - .states - .iter() - .filter_map(|(id, content_state)| { - // Only process text content that needs repaint - if content_state.render_data.needs_repaint && content_state.is_text() { - Some(*id) - } else { - None - } - }) - .collect(); - - if ids_to_repaint.is_empty() { - return; - } - - // Process each text ID - for text_id in &ids_to_repaint { - self.content.update_dimensions(text_id); - } - - // Clear repaint flags after processing - for id in ids_to_repaint { - if let Some(content_state) = self.content.states.get_mut(&id) { - content_state.render_data.clear_repaint_flag(); - } - } + Vec::new() } + /// Drive the per-frame `Renderer::prepare` step. The legacy + /// transient-text shaping pass is gone; `advance_brush.prepare` + /// remains because it still emits per-frame quads / clears the + /// instance buffers that the immediate-mode `rect/quad/...` + /// helpers fill. #[inline] pub fn compute_updates( &mut self, @@ -170,90 +67,39 @@ impl SugarState { Vec, >, ) { - // Shape transient texts before rendering - self.content.build_transient_texts(); advance_brush.prepare(context, self, graphics, image_data, image_overlays); } + /// `compute_dimensions` used to walk per-id Content states and + /// recompute cell metrics on the `needs_repaint` flag. Per-panel + /// dimensions live on rio's `ContextDimension` now and rio drives + /// the recompute through `Sugarloaf::compute_cell_metrics`. This + /// is preserved as a no-op stub so existing render-loop call + /// sites stay shape-compatible until they're audited. #[inline] - pub fn reset(&mut self) { - // Remove states marked for removal - let mut to_remove = Vec::new(); - for (id, content_state) in &self.content.states { - if content_state.render_data.should_remove { - to_remove.push(*id); - } - } - - for id in to_remove { - self.content.remove_state(&id); - } - - // Clear all transient texts (they get recreated each frame) - self.content.clear_transient_texts(); - - self.content.mark_states_clean(); - } - - #[inline] - pub fn clear_text(&mut self, id: &usize) { - self.content.clear_state(id); - } - - #[inline] - pub fn set_content_position(&mut self, id: usize, x: f32, y: f32) { - if let Some(content_state) = self.content.states.get_mut(&id) { - content_state - .render_data - .set_position(x * self.style.scale_factor, y * self.style.scale_factor); - } - } - - #[inline] - pub fn set_content_visibility_and_position( - &mut self, - id: usize, - x: f32, - y: f32, - hidden: bool, - ) { - if let Some(content_state) = self.content.states.get_mut(&id) { - content_state - .render_data - .set_position(x * self.style.scale_factor, y * self.style.scale_factor); - content_state.render_data.set_hidden(hidden); - content_state.render_data.should_remove = false; - } - } + pub fn compute_dimensions(&mut self) {} + /// Pre-frame cleanup hook. Was responsible for purging Content + /// states marked for removal. With Content gone, sugarloaf has no + /// per-id state of its own to GC; the only remaining sweep + /// (per-frame text instances) lives on `crate::text::Text`. #[inline] - pub fn set_content_bounds(&mut self, id: usize, bounds: Option<[f32; 4]>) { - if let Some(content_state) = self.content.states.get_mut(&id) { - content_state.render_data.set_bounds(bounds); - } - } + pub fn reset(&mut self) {} + /// Was responsible for marking every panel-text state for + /// removal so the next frame's `reset` would drop them. Without + /// Content there's nothing to mark — kept as a no-op so the + /// `Sugarloaf::clear` path doesn't change shape. #[inline] - pub fn set_content_hidden(&mut self, id: usize, hidden: bool) { - if let Some(content_state) = self.content.states.get_mut(&id) { - content_state.render_data.set_hidden(hidden); - } - } + pub fn clean_screen(&mut self) {} + /// Refresh `RootStyle.scale_factor`. Per-panel `dimension` / + /// `scaled_font_size` updates happen on rio's `ContextDimension` + /// — this only touches sugarloaf's global default that new panels + /// inherit from. #[inline] - pub fn set_content_depth(&mut self, id: usize, depth: f32) { - if let Some(content_state) = self.content.states.get_mut(&id) { - content_state.render_data.depth = depth; - content_state.render_data.needs_repaint = true; - } - } - - #[inline] - pub fn set_content_order(&mut self, id: usize, order: u8) { - if let Some(content_state) = self.content.states.get_mut(&id) { - content_state.render_data.order = order; - content_state.render_data.needs_repaint = true; - } + pub fn compute_layout_rescale(&mut self, scale: f32) { + self.style.scale_factor = scale; } pub fn set_visual_bell_overlay( @@ -263,51 +109,15 @@ impl SugarState { self.visual_bell_overlay = overlay; } + /// Was a no-op even before Content was removed; kept for the + /// `Sugarloaf::update_font` call site until that path is + /// simplified to skip routing through state. #[inline] pub fn set_fonts( &mut self, - _font_library: &FontLibrary, + font_library: &FontLibrary, _advance_brush: &mut Renderer, ) { - // Simplified - fonts are handled elsewhere in the unified system - } - - #[inline] - pub fn set_text_font_size_based_on_action(&mut self, text_id: &usize, operation: u8) { - self.update_text_style(text_id, operation); - } - - #[inline] - pub fn set_text_font_size(&mut self, rt_id: &usize, font_size: f32) { - if let Some(content_state) = self.content.states.get_mut(rt_id) { - if let Some(text_state) = content_state.as_text_mut() { - text_state.layout.font_size = font_size; - text_state.scaled_font_size = font_size * self.style.scale_factor; - } - content_state.render_data.needs_repaint = true; - } - self.compute_dimensions(); - } - - #[inline] - pub fn set_text_line_height(&mut self, _rt_id: &usize, _line_height: f32) { - // Simplified - line height changes handled elsewhere - } - - #[inline] - pub fn compute_layout_rescale(&mut self, scale: f32) { - self.style.scale_factor = scale; - - // Re-scale all text content with the new scale factor - for content_state in self.content.states.values_mut() { - if let Some(text_state) = content_state.as_text_mut() { - text_state.rescale(scale); - text_state.layout.dimensions.height = 0.0; - text_state.layout.dimensions.width = 0.0; - } - content_state.render_data.needs_repaint = true; - } - - self.compute_dimensions(); + self.fonts = font_library.clone(); } }