diff --git a/frontends/rioterm/src/renderer/mod.rs b/frontends/rioterm/src/renderer/mod.rs index 8fdf955f..28b2be23 100644 --- a/frontends/rioterm/src/renderer/mod.rs +++ b/frontends/rioterm/src/renderer/mod.rs @@ -373,15 +373,18 @@ impl Renderer { let has_virtual = !terminal_snapshot.kitty_virtual_placements.is_empty(); if has_overlays || has_virtual { let line_height = sugarloaf.style().line_height; - let content = sugarloaf.content(); - content.sel(context.rich_text_id); - content.clear_image_overlays(); let layout = context.dimension; let cell_width = layout.dimension.width; let cell_height = layout.dimension.height * line_height; let origin_x = panel_rect[0] + grid_scaled_margin.left; let origin_y = panel_rect[1] + grid_scaled_margin.top; + let overlays = sugarloaf + .image_overlays + .entry(context.rich_text_id) + .or_default(); + overlays.clear(); + if has_overlays { let history_size = terminal_snapshot.history_size as i64; let display_offset = terminal_snapshot.display_offset as i64; @@ -394,8 +397,7 @@ impl Renderer { if image_bottom_row <= 0 || screen_row >= screen_lines { continue; } - content - .push_image_overlay(rio_backend::sugarloaf::GraphicOverlay { + overlays.push(rio_backend::sugarloaf::GraphicOverlay { image_id: p.image_id, x: origin_x + p.dest_col as f32 * cell_width, y: origin_y + screen_row as f32 * cell_height, @@ -410,7 +412,7 @@ impl Renderer { if has_virtual { Self::push_virtual_placeholder_overlays( - content, + overlays, &terminal_snapshot, origin_x, origin_y, @@ -420,9 +422,7 @@ impl Renderer { } } else if terminal_snapshot.kitty_graphics_dirty { // Placements were removed — clear overlays - let content = sugarloaf.content(); - content.sel(context.rich_text_id); - content.clear_image_overlays(); + sugarloaf.clear_image_overlays_for(context.rich_text_id); } // Get hint matches from renderable content @@ -717,7 +717,7 @@ impl Renderer { /// cells that fall in the centering padding /// (`renderPlacement`, `graphics_unicode.zig:212-329`). fn push_virtual_placeholder_overlays( - content: &mut rio_backend::sugarloaf::Content, + overlays: &mut Vec, snapshot: &TerminalSnapshot, origin_x: f32, origin_y: f32, @@ -748,7 +748,7 @@ impl Renderer { if square.c() != PLACEHOLDER { if let Some((p, start_col)) = run.take() { flush_run( - content, + overlays, snapshot, p.complete(), line_idx, @@ -783,7 +783,7 @@ impl Renderer { _ => { if let Some((p, start_col)) = run.take() { flush_run( - content, + overlays, snapshot, p.complete(), line_idx, @@ -814,7 +814,7 @@ impl Renderer { if let Some((p, start_col)) = run { flush_run( - content, + overlays, snapshot, p.complete(), line_idx, @@ -836,7 +836,7 @@ impl Renderer { /// entirely in the aspect-fit centering padding. #[allow(clippy::too_many_arguments)] fn flush_run( - content: &mut rio_backend::sugarloaf::Content, + overlays: &mut Vec, snapshot: &TerminalSnapshot, run: PlaceholderRun, screen_line: usize, @@ -877,7 +877,7 @@ impl Renderer { None => return, }; - content.push_image_overlay(rio_backend::sugarloaf::GraphicOverlay { + overlays.push(rio_backend::sugarloaf::GraphicOverlay { image_id: run.image_id, x: geom.x, y: geom.y, diff --git a/sugarloaf/src/layout/content.rs b/sugarloaf/src/layout/content.rs index f0e78d5e..c2830741 100644 --- a/sugarloaf/src/layout/content.rs +++ b/sugarloaf/src/layout/content.rs @@ -458,27 +458,6 @@ impl Content { self } - /// Clear image overlays on the selected content state. - pub fn clear_image_overlays(&mut self) { - if let Some(id) = self.selector { - if let Some(state) = self.states.get_mut(&id) { - state.image_overlays.clear(); - } - } - } - - /// Push an image overlay onto the selected content state. - pub fn push_image_overlay( - &mut self, - overlay: crate::sugarloaf::graphics::GraphicOverlay, - ) { - if let Some(id) = self.selector { - if let Some(state) = self.states.get_mut(&id) { - state.image_overlays.push(overlay); - } - } - } - #[inline] pub fn font_library(&self) -> &FontLibrary { &self.fonts diff --git a/sugarloaf/src/layout/content_data.rs b/sugarloaf/src/layout/content_data.rs index 3a77885d..258dd23e 100644 --- a/sugarloaf/src/layout/content_data.rs +++ b/sugarloaf/src/layout/content_data.rs @@ -4,7 +4,6 @@ // LICENSE file in the root directory of this source tree. use crate::layout::content::BuilderState; -use crate::sugarloaf::graphics::GraphicOverlay; use smallvec::SmallVec; /// Unified content data for all shape types @@ -166,7 +165,6 @@ impl ContentRenderData { pub struct ContentState { pub data: ContentData, pub render_data: ContentRenderData, - pub image_overlays: SmallVec<[GraphicOverlay; 4]>, } impl ContentState { @@ -174,7 +172,6 @@ impl ContentState { Self { data, render_data: ContentRenderData::default(), - image_overlays: SmallVec::new(), } } diff --git a/sugarloaf/src/renderer/compositor.rs b/sugarloaf/src/renderer/compositor.rs index a1a509bd..63f2a725 100644 --- a/sugarloaf/src/renderer/compositor.rs +++ b/sugarloaf/src/renderer/compositor.rs @@ -5,11 +5,8 @@ // // Compositor with vertex capture for text run caching -use crate::layout::{SpanStyleDecoration, UnderlineShape}; -use crate::renderer::batch::{BatchManager, DrawCmd, QuadInstance, RunUnderline}; +use crate::renderer::batch::{BatchManager, DrawCmd, QuadInstance}; pub use crate::renderer::batch::{Rect, Vertex}; -use crate::renderer::image_cache::glyph::GlyphCacheSession; -use crate::renderer::text::*; pub struct Compositor { pub batches: BatchManager, @@ -22,88 +19,6 @@ impl Compositor { } } - /// Creates an underline decoration based on the style and rect - pub fn create_underline_from_decoration( - &self, - style: &TextRunStyle, - ) -> Option { - match style.decoration { - Some(SpanStyleDecoration::Underline(info)) => { - // Use font metrics for thickness when available, otherwise fall back to shape-based defaults - let underline_thickness = if style.underline_thickness > 0.0 { - style.underline_thickness - } else { - // Fallback thickness based on font size - // Use approximately 8% of font size for all underline types - let base_thickness = style.font_size * 0.08; - base_thickness.max(1.0) - }; - - // Use real font metrics for proper underline positioning - let underline_offset = - self.calculate_underline_offset(style, underline_thickness); - - Some(RunUnderline { - enabled: true, - offset: underline_offset, - size: underline_thickness, - color: style.decoration_color.unwrap_or(style.color), - is_doubled: info.is_doubled, - shape: info.shape, - }) - } - Some(SpanStyleDecoration::Strikethrough) => { - // Strikethrough should be positioned through the middle of the text - // Use font-provided strikeout_offset if available, otherwise x_height/2 - let strikethrough_offset = if style.strikeout_offset != 0.0 { - // Font provides strikeout_offset as distance from baseline - -style.strikeout_offset - } else if style.x_height > 0.0 { - // x_height is the height of lowercase letters, strike through middle - -(style.x_height / 2.0) - } else { - // Fallback: 25% of ascent above baseline - -(style.ascent * 0.25) - }; - - // Use font metrics for thickness when available - let strikethrough_thickness = if style.underline_thickness > 0.0 { - style.underline_thickness - } else { - 1.5 - }; - - Some(RunUnderline { - enabled: true, - offset: strikethrough_offset, - size: strikethrough_thickness, - color: style.decoration_color.unwrap_or(style.color), - is_doubled: false, - shape: UnderlineShape::Regular, - }) - } - _ => None, - } - } - - /// Calculate underline offset using font metrics, with fallback - fn calculate_underline_offset( - &self, - style: &TextRunStyle, - underline_thickness: f32, - ) -> f32 { - // Use font's built-in underline position when available - if style.underline_offset != 0.0 { - // Font provides underline_offset as distance from baseline to underline top - // Negative values mean below baseline, which is what we want - // But Rio's renderer expects positive offset for below baseline - -style.underline_offset - } else { - // Fallback: place underline 1 thickness below baseline - underline_thickness - } - } - #[inline] pub fn finish( &mut self, @@ -114,335 +29,6 @@ impl Compositor { self.batches.build_display_list(instances, vertices, cmds); self.batches.reset(); } - - /// Standard draw_run method (for compatibility) - #[inline] - pub fn draw_run( - &mut self, - session: &mut GlyphCacheSession, - rect: impl Into, - depth: f32, - style: &TextRunStyle, - glyphs: &[Glyph], - order: u8, - ) { - self.draw_run_internal(session, rect, depth, style, glyphs, order); - } - - /// Internal rendering implementation - #[inline] - fn draw_run_internal( - &mut self, - session: &mut GlyphCacheSession, - rect: impl Into, - depth: f32, - style: &TextRunStyle, - glyphs: &[Glyph], - order: u8, - ) { - let rect = rect.into(); - let underline = self.create_underline_from_decoration(style); - - let subpx_bias = (0.125, 0.); - let color = style.color; - - if let Some(builtin_character) = style.drawable_char { - if let Some(bg_color) = style.background_color { - let bg_rect = - Rect::new(rect.x, style.topline, rect.width, style.line_height); - self.batches.rect(&bg_rect, depth, &bg_color, 0); - } - - if let Some(cursor) = style.cursor { - // Calculate cursor dimensions based on font metrics, not line height - let font_height = style.ascent + style.descent; - let cursor_top = style.baseline - style.ascent; - - match cursor.kind { - crate::CursorKind::Block => { - let cursor_rect = - Rect::new(rect.x, cursor_top, rect.width, font_height); - self.batches.rect( - &cursor_rect, - depth, - &cursor.color, - cursor.order, - ); - } - crate::CursorKind::HollowBlock => { - let outer_rect = - Rect::new(rect.x, cursor_top, rect.width, font_height); - self.batches.rect( - &outer_rect, - depth, - &cursor.color, - cursor.order, - ); - - if let Some(bg_color) = style.background_color { - let inner_rect = Rect::new( - rect.x + 1.0, - cursor_top + 1.0, - rect.width - 2.0, - font_height - 2.0, - ); - self.batches.rect( - &inner_rect, - depth, - &bg_color, - cursor.order, - ); - } - } - crate::CursorKind::Caret => { - let caret_rect = Rect::new(rect.x, cursor_top, 2.0, font_height); - self.batches.rect( - &caret_rect, - depth, - &cursor.color, - cursor.order, - ); - } - crate::CursorKind::Underline => { - let caret_rect = - Rect::new(rect.x, style.baseline + 1.0, rect.width, 2.0); - self.batches.rect( - &caret_rect, - depth, - &cursor.color, - cursor.order, - ); - } - } - } - - if let Some(underline) = underline { - self.batches.draw_underline( - &underline, - rect.x, - rect.width, - style.baseline, - depth, - style.line_height, - ); - } - - self.batches.draw_drawable_character( - rect.x, - style.topline, - rect.width, - builtin_character, - color, - depth, - style.line_height, - 0, - ); - } else { - // Handle regular glyphs - for glyph in glyphs { - // Rasterize Nerd Font / PUA glyphs once at the nominal font - // size. An earlier "rasterize at cells × font_size" trick - // produced a 2× raster that the constraint math below then - // tried to re-scale *from*, compounding into a ~2× oversized - // glyph. With nominal rasterization, the constraint's - // width/height factors land where they should. - let entry = session.get(glyph.id); - if let Some(entry) = entry { - if let Some(img) = session.get_image(entry.image) { - let gx = (glyph.x + subpx_bias.0).floor() + entry.left as f32; - let gy = (glyph.y + subpx_bias.1).floor() - entry.top as f32; - - // Proportional fit in both directions. After the - // targeted rasterization above this is effectively a - // downscale-or-identity, which keeps output crisp. - // Matches ghostty's `.size = .fit` for PUA symbols. - let glyph_rect = if let Some((cell_w, cells)) = - style.scale_constraint - { - // If this codepoint has a per-glyph entry - // in the Nerd Fonts patcher table (ported - // from ghostty), defer to ghostty's - // `Constraint::constrain` for size and - // alignment. Otherwise fall back to Rio's - // generic cell-centered fit. - if let Some(constraint) = style.nerd_font_constraint { - compute_nerd_font_rect(NerdFontRectInput { - constraint: &constraint, - entry: &entry, - glyph, - cell_w, - cells, - line_height: style.line_height, - topline: style.topline, - baseline: style.baseline, - }) - } else { - // No per-codepoint attribute: no scaling, no - // slot-centering. Glyph renders at its - // natural pen position and natural raster - // size. - let _ = (cell_w, cells); - Rect::new(gx, gy, entry.width as f32, entry.height as f32) - } - } else if entry.is_bitmap { - // Color bitmap (emoji) glyphs fall here when the - // shaper didn't attach an explicit constraint. - // - // `.cover` sizing with center alignment and - // 2.5 % horizontal padding. Cover scales the - // bitmap so it fills the advance × cell-height - // slot on at least one axis, rather than fit - // which leaves gaps. - // - // Vertical centering uses the font's *natural* - // cell (ascent + descent) rather than - // `line_height` — the latter picks up user - // line-height modifiers that shouldn't shift the - // emoji inside its cell. - const PAD_EACH: f32 = 0.025; - let orig_w = entry.width as f32; - let orig_h = entry.height as f32; - if orig_w > 0.0 && orig_h > 0.0 { - let cell_top = style.baseline - style.ascent; - let cell_h = style.ascent + style.descent; - let available_w = glyph.advance * (1.0 - 2.0 * PAD_EACH); - // Cover: pick the larger scale factor so the - // emoji fills the slot on at least one axis. - let scale = (available_w / orig_w).max(cell_h / orig_h); - let sw = orig_w * scale; - let sh = orig_h * scale; - let cx = (glyph.x + subpx_bias.0).floor() - + (glyph.advance - sw) / 2.0; - let cy = cell_top + (cell_h - sh) / 2.0; - // Snap both edges to the pixel grid. Bitmap - // emoji (sbix — Apple Color Emoji) sampled - // at fractional offsets looks blurry; - // rounding cx/cy/sw/sh to whole pixels lets - // the sampler hit source texels cleanly. - // No-op for COLR glyphs whose scale already - // snapped. - let x0 = cx.round(); - let x1 = (cx + sw).round(); - let y0 = cy.round(); - let y1 = (cy + sh).round(); - Rect::new(x0, y0, x1 - x0, y1 - y0) - } else { - Rect::new(gx, gy, orig_w, orig_h) - } - } else { - Rect::new(gx, gy, entry.width as f32, entry.height as f32) - }; - let coords = [img.min.0, img.min.1, img.max.0, img.max.1]; - - if entry.is_bitmap { - let bitmap_color = [1.0, 1.0, 1.0, 1.0]; - // Get atlas index for this image (0-based), add 1 for layer (0 = no texture) - let atlas_layer = session - .get_atlas_index(entry.image) - .map(|idx| (idx + 1) as i32) - .unwrap_or(1); - self.batches.add_image_rect( - &glyph_rect, - depth, - &bitmap_color, - &coords, - atlas_layer, - ); - } else { - self.batches.add_mask_rect_with_order( - &glyph_rect, - depth, - &color, - &coords, - true, - order, - ); - } - } - } - } - - if let Some(bg_color) = style.background_color { - let bg_rect = - Rect::new(rect.x, style.topline, rect.width, style.line_height); - self.batches.rect(&bg_rect, depth, &bg_color, 0); - } - - if let Some(cursor) = style.cursor { - // Calculate cursor dimensions based on font metrics, not line height - let font_height = style.ascent + style.descent; - let cursor_top = style.baseline - style.ascent; - - match cursor.kind { - crate::CursorKind::Block => { - let cursor_rect = - Rect::new(rect.x, cursor_top, rect.width, font_height); - self.batches.rect( - &cursor_rect, - depth, - &cursor.color, - cursor.order, - ); - } - crate::CursorKind::HollowBlock => { - let outer_rect = - Rect::new(rect.x, cursor_top, rect.width, font_height); - self.batches.rect( - &outer_rect, - depth, - &cursor.color, - cursor.order, - ); - - if let Some(bg_color) = style.background_color { - let inner_rect = Rect::new( - rect.x + 1.0, - cursor_top + 1.0, - rect.width - 2.0, - font_height - 2.0, - ); - self.batches.rect( - &inner_rect, - depth, - &bg_color, - cursor.order, - ); - } - } - crate::CursorKind::Caret => { - let caret_rect = Rect::new(rect.x, cursor_top, 2.0, font_height); - self.batches.rect( - &caret_rect, - depth, - &cursor.color, - cursor.order, - ); - } - crate::CursorKind::Underline => { - let caret_rect = - Rect::new(rect.x, style.baseline + 1.0, rect.width, 2.0); - self.batches.rect( - &caret_rect, - depth, - &cursor.color, - cursor.order, - ); - } - } - } - - if let Some(underline) = underline { - self.batches.draw_underline( - &underline, - rect.x, - rect.width, - style.baseline, - depth, - style.line_height, - ); - } - } - } } impl Default for Compositor { @@ -450,90 +36,3 @@ impl Default for Compositor { Self::new() } } - -/// Inputs for `compute_nerd_font_rect`, bundled so the function signature -/// stays within the clippy arg-count lint while keeping call sites -/// readable. -struct NerdFontRectInput<'a> { - constraint: &'a crate::font::nerd_font_attributes::Constraint, - entry: &'a crate::renderer::image_cache::glyph::GlyphEntry, - glyph: &'a Glyph, - cell_w: f32, - cells: u8, - line_height: f32, - topline: f32, - baseline: f32, -} - -/// Apply ghostty's per-glyph `Constraint` to a rasterized glyph entry and -/// return the target draw rectangle in screen space. -/// -/// Ghostty's constraint model operates on a `GlyphSize { x, y, width, -/// height }` in cell-relative y-up coordinates: -/// `x` is the left bearing from cell left, `y` is the *bottom* of the -/// glyph bounding box measured from the cell *bottom* (y-up), and the -/// text baseline sits `cell_baseline` above the cell bottom. After -/// `constrain()` returns, ghostty's shader flips `y` via -/// `offset_y_final = cell_height − offset_y` to land in y-down screen -/// space. We do the same translation here so glyph drawing stays in Rio's -/// y-down frame. -/// -/// Input mapping (swash → ghostty cell-bottom y-up): -/// `bitmap bottom = cell_baseline + entry.top − entry.height`, -/// `bitmap left = entry.left`. -/// -/// Output mapping (ghostty y-up → screen y-down): -/// `screen top = topline + (cell_height − out.y − out.height)`, -/// `screen left = glyph.x + out.x`. -fn compute_nerd_font_rect(input: NerdFontRectInput<'_>) -> Rect { - let NerdFontRectInput { - constraint, - entry, - glyph, - cell_w, - cells, - line_height, - topline, - baseline, - } = input; - use crate::font::nerd_font_attributes::{GlyphSize, Metrics}; - - // Ghostty's `cell_baseline`: distance from cell bottom *up to* baseline. - // Rio keeps `baseline` in y-down screen coords (same frame as `topline`), - // so we flip via `(topline + line_height) − baseline`. - let cell_baseline = ((topline + line_height) - baseline) as f64; - let face_width = cell_w as f64; - let face_height = line_height as f64; - let metrics = Metrics { - face_width, - face_height, - // `face_y`: offset from cell bottom up to the bottom of the face box. - // Rio doesn't separately track a face box, so the face sits at the - // cell bottom — good enough for the aligned-y math. - face_y: 0.0, - cell_width: cell_w.max(1.0) as u32, - cell_height: line_height.max(1.0) as u32, - // `Height::Icon` entries scale against the icon height from grid - // metrics; ghostty exposes this via `adjust-icon-height`. Until Rio - // has the knob, use 75% of face_height (ghostty's single-cell - // default post-rounding). - icon_height_single: 0.75 * face_height, - icon_height: 0.75 * face_height, - }; - - // Swash's (`entry.left`, baseline-up `entry.top`, width, height) → - // ghostty's cell-bottom y-up bounding box. - let glyph_size = GlyphSize { - width: entry.width as f64, - height: entry.height as f64, - x: entry.left as f64, - y: cell_baseline + entry.top as f64 - entry.height as f64, - }; - - let out = constraint.constrain(glyph_size, metrics, cells.clamp(1, 2)); - - // y-up cell-bottom → y-down screen top: `cell_height − (y + height)`. - let cx = glyph.x + out.x as f32; - let cy = topline + (line_height - (out.y as f32 + out.height as f32)); - Rect::new(cx, cy, out.width as f32, out.height as f32) -} diff --git a/sugarloaf/src/renderer/image_cache/glyph.rs b/sugarloaf/src/renderer/image_cache/glyph.rs deleted file mode 100644 index f6160a26..00000000 --- a/sugarloaf/src/renderer/image_cache/glyph.rs +++ /dev/null @@ -1,444 +0,0 @@ -use super::cache::ImageCache; -use super::{AddImage, ImageData, ImageId, ImageLocation}; -use crate::font::FontLibrary; -use swash::scale::{ - image::{Content, Image as GlyphImage}, - *, -}; -#[cfg(not(target_os = "macos"))] -use swash::zeno::Format; -#[cfg(not(target_os = "macos"))] -use swash::FontRef; -use core::borrow::Borrow; -use core::hash::{Hash, Hasher}; -use rustc_hash::FxHashMap; -use tracing::debug; -#[cfg(not(target_os = "macos"))] -use zeno::{Angle, Transform}; - -#[cfg(not(target_os = "macos"))] -const SOURCES: &[Source] = &[ - Source::ColorOutline(0), - Source::ColorBitmap(StrikeWith::BestFit), - // Source::Bitmap(StrikeWith::ExactSize), - Source::Outline, -]; - -/// macOS rasterization path: populates `scaled` with CoreText/CoreGraphics output -/// shaped to match what zeno fills in on other platforms. -/// -/// `is_emoji` drives the bitmap format (RGBA for color, R8 alpha for mono) — -/// it's taken from `FontData::is_emoji` so the caller doesn't have to probe the -/// font per glyph. -#[cfg(target_os = "macos")] -#[allow(clippy::too_many_arguments)] -fn rasterize_macos( - scaled: &mut GlyphImage, - handle: &crate::font::macos::FontHandle, - glyph_id: u16, - size: u16, - is_emoji: bool, - synthetic_italic: bool, - synthetic_bold: bool, -) -> bool { - match crate::font::macos::rasterize_glyph( - handle, - glyph_id, - size as f32, - is_emoji, - synthetic_italic, - synthetic_bold, - ) { - Some(g) => { - scaled.placement = zeno::Placement { - left: g.left, - top: g.top, - width: g.width, - height: g.height, - }; - scaled.content = if g.is_color { - Content::Color - } else { - Content::Mask - }; - scaled.data.clear(); - scaled.data.extend_from_slice(&g.bytes); - true - } - None => false, - } -} - -pub struct GlyphCache { - scx: ScaleContext, - fonts: FxHashMap, - img: GlyphImage, - max_height: u16, -} - -impl GlyphCache { - pub fn new() -> Self { - GlyphCache { - scx: ScaleContext::new(), - fonts: FxHashMap::default(), - img: GlyphImage::new(), - max_height: 0, - } - } - - #[inline] - pub fn session<'a>( - &'a mut self, - images: &'a mut ImageCache, - font: usize, - font_library: &'a FontLibrary, - coords: &[i16], - size: f32, - ) -> GlyphCacheSession<'a> { - // let quant_size = (size * 32.) as u16; - let quant_size = size as u16; - let entry = get_entry(&mut self.fonts, font, coords); - GlyphCacheSession { - font, - entry, - images, - font_library, - max_height: &self.max_height, - scaled_image: &mut self.img, - quant_size, - scale_context: &mut self.scx, - } - } - - // pub fn prune(&mut self, images: &mut ImageCache) { - // self.fonts.retain(|_, entry| { - // for glyph in &entry.glyphs { - // images.deallocate(glyph.1.image); - // } - // false - // }); - // } -} - -fn get_entry<'a>( - fonts: &'a mut FxHashMap, - id: usize, - coords: &[i16], -) -> &'a mut FontEntry { - let key = (id, Coords::Ref(coords)); - if let Some(entry) = fonts.get_mut(&key) { - // Remove this unsafe when Rust learns that early returns should not - // hold a borrow until the end of the function, or HashMap gets an - // entry API that accepts borrowed keys (in which case, the double - // lookup here can be removed altogether) - return unsafe { core::mem::transmute::<&mut FontEntry, &mut FontEntry>(entry) }; - } - let key = FontKey { - key: (id, Coords::new(coords)), - }; - fonts.entry(key).or_default() -} - -pub struct GlyphCacheSession<'a> { - entry: &'a mut FontEntry, - images: &'a mut ImageCache, - scaled_image: &'a mut GlyphImage, - font: usize, - font_library: &'a FontLibrary, - #[cfg_attr(target_os = "macos", allow(dead_code))] - scale_context: &'a mut ScaleContext, - quant_size: u16, - #[allow(unused)] - max_height: &'a u16, -} - -impl GlyphCacheSession<'_> { - pub fn get_image(&mut self, image: ImageId) -> Option { - self.images.get(&image) - } - - pub fn get_atlas_index(&self, image: ImageId) -> Option { - self.images.get_atlas_index(image) - } - - #[inline] - pub fn get(&mut self, id: u16) -> Option { - self.get_at_size(id, self.quant_size) - } - - /// Look up a glyph rasterized at a specific pixel size instead of the - /// session's nominal font size. Used to pre-rasterize PUA glyphs at - /// `cells × font_size` so the compositor's constraint-fit pass only - /// ever downscales — upscaling a bitmap atlas entry is blurry, so a - /// 1-cell-wide Nerd Font glyph stretched into a 2-cell slot needs a - /// fresh larger rasterization, not a stretched one. - /// Results are cached under `(id, size)`, so repeated lookups at the - /// same constraint size don't re-rasterize. - #[inline] - pub fn get_at_size(&mut self, id: u16, size: u16) -> Option { - let key = GlyphKey { id, size }; - if let Some(entry) = self.entry.glyphs.get(&key) { - if self.images.is_valid(entry.image) { - return Some(*entry); - } - } - - // Log cache miss for debugging - debug!( - "GlyphCache miss for glyph_id={} size={} font={}", - id, size, self.font - ); - - self.scaled_image.data.clear(); - - // Pull per-font metadata under a brief read lock, then release it so - // the macOS `ct_font` call (which may take its own read lock on cache - // miss) doesn't nest acquisitions. - let should_embolden; - let should_italicize; - #[cfg(target_os = "macos")] - let is_emoji; - #[cfg(not(target_os = "macos"))] - let enable_hint; - #[cfg(not(target_os = "macos"))] - let font_bytes_opt; - { - let font_library_data = self.font_library.inner.read(); - let font_data = font_library_data.get(&self.font); - should_embolden = font_data.should_embolden; - should_italicize = font_data.should_italicize; - #[cfg(target_os = "macos")] - { - is_emoji = font_data.is_emoji; - } - #[cfg(not(target_os = "macos"))] - { - enable_hint = font_library_data.hinting; - font_bytes_opt = font_library_data.get_data(&self.font); - } - } - - #[cfg(target_os = "macos")] - let did_render = match self.font_library.ct_font(self.font) { - Some(handle) => rasterize_macos( - self.scaled_image, - &handle, - id, - size, - is_emoji, - should_italicize, - should_embolden, - ), - None => false, - }; - - #[cfg(not(target_os = "macos"))] - let did_render = if let Some((shared_data, offset, cache_key)) = font_bytes_opt { - let font_ref = FontRef { - data: shared_data.as_ref(), - offset, - key: cache_key, - }; - let mut scaler = self - .scale_context - .builder(font_ref) - // With the advent of high-DPI displays (displays with >300 pixels per inch), - // font hinting has become less relevant, as aliasing effects become - // un-noticeable to the human eye. - // As a result Apple's Quartz text renderer, which is targeted for Retina displays, - // now ignores font hint information completely. - .hint(enable_hint) - .size(size.into()) - // .normalized_coords(coords) - .build(); - - Render::new(SOURCES) - .format(Format::Alpha) - // .offset(Vector::new(subpx[0].to_f32(), subpx[1].to_f32())) - .embolden(if should_embolden { 0.5 } else { 0.0 }) - .transform(if should_italicize { - Some(Transform::skew( - Angle::from_degrees(14.0), - Angle::from_degrees(0.0), - )) - } else { - None - }) - .render_into(&mut scaler, id, self.scaled_image) - } else { - false - }; - - if did_render { - let p = self.scaled_image.placement; - let w = p.width as u16; - let h = p.height as u16; - - // Handle zero-sized glyphs (spaces, zero-width characters) efficiently - if w == 0 || h == 0 { - let entry = GlyphEntry { - left: p.left, - top: p.top, - width: w, - height: h, - image: ImageId::empty(), // Use a special empty image ID - is_bitmap: false, - }; - self.entry.glyphs.insert(key, entry); - return Some(entry); - } - - // Use the appropriate content type and data format - let (image_data, content_type) = match self.scaled_image.content { - Content::Mask => { - // Alpha format: use data directly for R8 texture - ( - ImageData::Borrowed(&self.scaled_image.data), - super::ContentType::Mask, - ) - } - Content::Color => { - // Already RGBA format - ( - ImageData::Borrowed(&self.scaled_image.data), - super::ContentType::Color, - ) - } - Content::SubpixelMask => { - // Subpixel format (should not happen with Format::Alpha) - ( - ImageData::Borrowed(&self.scaled_image.data), - super::ContentType::Color, - ) - } - }; - - let req = AddImage { - width: w, - height: h, - has_alpha: true, - data: image_data, - content_type, - }; - let image = self.images.allocate(req)?; - - // let mut top = p.top; - // let mut height = h; - - // If dimension is None it means that we are running - // for the first time and in this case, we will obtain - // what the next glyph entries should respect in terms of - // top and height values - // - // e.g: Placement { left: 11, top: 42, width: 8, height: 50 } - // - // The calculation is made based on max_height - // If the rect max height is 50 and the glyph height is 68 - // and 48 top, then (68 - 50 = 18) height as difference and - // apply it to the top (bigger the top == up ^). - // if self.max_height > &0 && &h > self.max_height { - // let difference = h - self.max_height; - - // top -= difference as i32; - // height = *self.max_height; - // } - - let entry = GlyphEntry { - left: p.left, - top: p.top, - width: w, - height: h, - image, - is_bitmap: self.scaled_image.content == Content::Color, - }; - - self.entry.glyphs.insert(key, entry); - return Some(entry); - } - - None - } -} - -#[derive(Clone, PartialEq, Eq, Hash, Debug)] -struct FontKey { - key: (usize, Coords<'static>), -} - -impl<'a> Borrow<(usize, Coords<'a>)> for FontKey { - fn borrow(&self) -> &(usize, Coords<'a>) { - &self.key - } -} - -#[derive(Default)] -struct FontEntry { - glyphs: FxHashMap, -} - -#[derive(Clone, Debug)] -#[repr(u8)] -enum Coords<'a> { - None, - Inline(u8, [i16; 8]), - Heap(Vec), - Ref(&'a [i16]), -} - -impl Coords<'_> { - fn new(coords: &[i16]) -> Self { - let len = coords.len(); - if len == 0 { - Self::None - } else if len <= 8 { - let mut arr = [0i16; 8]; - arr[..len].copy_from_slice(coords); - Self::Inline(len as u8, arr) - } else { - Self::Heap(coords.into()) - } - } -} - -impl Coords<'_> { - fn as_ref(&self) -> &[i16] { - match self { - Self::None => &[], - Self::Inline(len, arr) => &arr[..*len as usize], - Self::Heap(vec) => vec, - Self::Ref(slice) => slice, - } - } -} - -impl PartialEq for Coords<'_> { - fn eq(&self, other: &Self) -> bool { - self.as_ref() == other.as_ref() - } -} - -impl Eq for Coords<'_> {} - -impl Hash for Coords<'_> { - fn hash(&self, state: &mut H) { - self.as_ref().hash(state); - } -} - -#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] -struct GlyphKey { - id: u16, - // subpx: [SubpixelOffset; 2], - size: u16, -} - -#[derive(Copy, Clone, Debug)] -pub struct GlyphEntry { - pub left: i32, - pub top: i32, - pub width: u16, - pub height: u16, - pub image: ImageId, - pub is_bitmap: bool, - // pub desc: DescenderRegion, -} diff --git a/sugarloaf/src/renderer/image_cache/mod.rs b/sugarloaf/src/renderer/image_cache/mod.rs index 7c4612da..1701fad7 100644 --- a/sugarloaf/src/renderer/image_cache/mod.rs +++ b/sugarloaf/src/renderer/image_cache/mod.rs @@ -1,11 +1,9 @@ pub(crate) mod atlas; mod cache; -pub mod glyph; use std::sync::Arc; pub use cache::ImageCache; -pub use glyph::GlyphCache; /// Identifier for an image in a cache. #[derive(Copy, Clone, PartialEq, Eq, Debug)] diff --git a/sugarloaf/src/renderer/mod.rs b/sugarloaf/src/renderer/mod.rs index 6bd152ab..a5ce9554 100644 --- a/sugarloaf/src/renderer/mod.rs +++ b/sugarloaf/src/renderer/mod.rs @@ -2,32 +2,23 @@ mod batch; mod compositor; pub mod cpu; pub(crate) mod image_cache; -#[cfg(test)] -mod positioning_tests; -pub mod text; -mod text_run_manager; use crate::components::core::orthographic_projection; use crate::context::webgpu::WgpuContext; use crate::context::{Context, ContextType}; use crate::font::FontLibrary; -use swash::GlyphId; -use crate::layout::{TextDimensions, TextLayout}; -use crate::renderer::image_cache::{GlyphCache, ImageCache}; -use crate::renderer::text_run_manager::{CacheResult, TextRunManager}; +use crate::layout::TextDimensions; +use crate::renderer::image_cache::ImageCache; use crate::sugarloaf::graphics::GraphicId; use crate::Graphics; -use crate::RichTextLinesRange; use compositor::{Compositor, Rect, Vertex}; use rustc_hash::FxHashMap; -use std::collections::HashSet; #[cfg(target_os = "macos")] use std::sync::Arc; use std::{borrow::Cow, mem}; #[cfg(target_os = "macos")] use parking_lot::Mutex; -use text::{Glyph, TextRunStyle}; use wgpu::util::DeviceExt; #[cfg(target_os = "macos")] @@ -776,8 +767,6 @@ pub struct Renderer { vertices: Vec, draw_cmds: Vec, images: ImageCache, - glyphs: GlyphCache, - text_run_manager: TextRunManager, graphic_cache: FxHashMap, current_frame: u64, /// Per-image GPU textures (one map, any backend). @@ -914,8 +903,6 @@ impl Renderer { vertices: vec![], draw_cmds: vec![], images: ImageCache::new(context), - glyphs: GlyphCache::new(), - text_run_manager: TextRunManager::new(), graphic_cache: FxHashMap::default(), current_frame: 0, image_textures: FxHashMap::default(), @@ -942,18 +929,25 @@ impl Renderer { &mut self, context: &mut crate::context::Context, state: &crate::sugarloaf::state::SugarState, - graphics: &mut Graphics, + _graphics: &mut Graphics, image_data: &mut rustc_hash::FxHashMap< u32, crate::sugarloaf::graphics::GraphicDataEntry, >, + image_overlays: &rustc_hash::FxHashMap< + usize, + Vec, + >, ) { self.instances.clear(); self.vertices.clear(); self.draw_cmds.clear(); - let library = state.content.font_library(); - // Iterate over all content states and render visible ones + // 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 @@ -966,30 +960,10 @@ impl Renderer { content_state.render_data.bounds.unwrap_or([0.0; 4]); match &content_state.data { - crate::layout::ContentData::Text(builder_state) => { - // Skip if there are no lines to render - if builder_state.lines.is_empty() { - continue; - } - - let pos = ( - content_state.render_data.position[0], - content_state.render_data.position[1], - ); - let depth = content_state.render_data.depth; - let order = content_state.render_data.order; - - self.draw_layout( - &builder_state.lines, - &None, - Some(pos), - depth, - library, - Some(&builder_state.layout), - graphics, - content_state.render_data.use_grid_cell_size, - order, - ); + 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, @@ -1103,64 +1077,32 @@ impl Renderer { } } - // Process transient texts (rendered once then cleared) - for content_state in state.content.transient_texts.iter() { - // Skip if hidden - if 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]); - - if let crate::layout::ContentData::Text(builder_state) = &content_state.data { - // Skip if there are no lines to render - if builder_state.lines.is_empty() { - continue; - } - - let pos = ( - content_state.render_data.position[0], - content_state.render_data.position[1], - ); - let depth = content_state.render_data.depth; - let order = content_state.render_data.order; - - // Use index + large offset to avoid collision with cached text IDs - self.draw_layout( - &builder_state.lines, - &None, - Some(pos), - depth, - library, - Some(&builder_state.layout), - graphics, - content_state.render_data.use_grid_cell_size, - order, - ); - } - } + // 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]; - // Render image overlays from visible content states only. - // Hidden states (e.g. inactive tabs) must be excluded so - // their images don't bleed through. - let has_overlays = state - .content - .states - .values() - .any(|cs| !cs.render_data.hidden && !cs.image_overlays.is_empty()); - if has_overlays { - let overlays: Vec<_> = state - .content - .states - .values() - .filter(|cs| !cs.render_data.hidden) - .flat_map(|cs| cs.image_overlays.iter()) - .collect(); + // 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(); + if !overlays.is_empty() { self.render_graphic_overlays(context, image_data, &overlays); } else { // No overlays visible — clear draw commands so stale images @@ -1269,538 +1211,6 @@ impl Renderer { #[inline] #[allow(clippy::too_many_arguments)] - fn draw_layout( - &mut self, - lines: &Vec, - selected_lines: &Option, - pos: Option<(f32, f32)>, - depth: f32, - font_library: &FontLibrary, - rte_layout: Option<&TextLayout>, - graphics: &mut Graphics, - use_grid_cell_size: bool, - order: u8, - ) { - if lines.is_empty() { - return; - } - - // For dimensions mode, we only process the first line - let lines_to_process = lines.as_slice(); - - // Extract font metrics before borrowing self.comp - let font_metrics = - self.extract_normalized_metrics(lines_to_process, font_library); - - // let start = std::time::Instant::now(); - // Get initial position - let (x, y) = pos.unwrap_or((0.0, 0.0)); - - // Increment frame counter for LRU tracking - self.current_frame += 1; - - // Pre-process: Upload graphics to atlas and cache their data - // This must happen BEFORE we borrow comp to avoid borrow checker issues - for line in lines_to_process { - for run in &line.render_data.runs { - if let Some(graphic) = run.span.media { - // Check if already cached - if let Some(cached) = self.graphic_cache.get_mut(&graphic.id) { - // Update last used frame - cached.last_used_frame = self.current_frame; - continue; - } - - // Not cached - need to upload to atlas - if let Some(entry) = graphics.get(&graphic.id) { - if let crate::components::core::image::Data::Rgba { - width, - height, - ref pixels, - } = entry.handle.data - { - let add_image = image_cache::AddImage { - width: width as u16, - height: height as u16, - has_alpha: true, - data: image_cache::ImageData::Borrowed(pixels.as_ref()), - content_type: image_cache::ContentType::Color, - }; - - // Try to allocate, with eviction retry if needed - let mut image_id = self.images.allocate(add_image.clone()); - - if image_id.is_none() { - // Atlas full - try evicting oldest graphics - tracing::warn!( - "Atlas full, attempting to evict oldest graphics" - ); - let mut evicted_count = 0; - - // Try evicting up to 5 graphics - while evicted_count < 5 { - if let Some(oldest_id) = self.find_oldest_graphic() { - self.evict_graphic(oldest_id); - evicted_count += 1; - - // Retry allocation - image_id = - self.images.allocate(add_image.clone()); - if image_id.is_some() { - tracing::info!("Successfully allocated after evicting {} graphics", evicted_count); - break; - } - } else { - break; // No more graphics to evict - } - } - - if image_id.is_none() { - tracing::error!("Failed to allocate graphic {:?} even after evicting {} graphics", graphic.id, evicted_count); - } - } - - if let Some(id) = image_id { - if let Some(location) = self.images.get(&id) { - // Get atlas layer for this image - let atlas_layer = self - .images - .get_atlas_index(id) - .map(|idx| (idx + 1) as i32) - .unwrap_or(1); - - // Cache coords + dimensions + frame + atlas layer - self.graphic_cache.insert( - graphic.id, - CachedGraphic { - location, - image_id: id, - width: entry.width, - height: entry.height, - last_used_frame: self.current_frame, - atlas_layer, - }, - ); - } - } - } - } - } - } - } - - // Now set up rendering - borrow comp and caches - let comp = &mut self.comp; - let caches = (&mut self.images, &mut self.glyphs); - let (image_cache, glyphs_cache) = caches; - let font_coords: &[i16] = &[0, 0, 0, 0]; - - // Set up caches based on mode - let mut glyphs = Vec::new(); - let mut last_rendered_graphic = HashSet::new(); - let mut line_y = y; - if let Some(( - ascent, - descent, - leading, - current_font_from_valid_run, - current_font_size_from_valid_run, - )) = font_metrics - { - // Initialize from first run if available - let mut current_font = current_font_from_valid_run; - let mut current_font_size = current_font_size_from_valid_run; - - let mut session = glyphs_cache.session( - image_cache, - current_font, - font_library, - font_coords, - current_font_size, - ); - - // Calculate line height with modifier if available - let line_height_without_mod = ascent + descent + leading; - let line_height_mod = rte_layout.map_or(1.0, |layout| layout.line_height); - let line_height = line_height_without_mod * line_height_mod; - - let skip_count = selected_lines.map_or(0, |range| range.start); - let take_count = selected_lines - .map_or(lines_to_process.len(), |range| range.end - range.start); - - for (_line_idx, line) in lines_to_process - .iter() - .enumerate() - .skip(skip_count) - .take(take_count) - { - if line.render_data.runs.is_empty() { - continue; - } - - let mut px = x; - - // Calculate baseline using proper typographic positioning - let padding_top = (line_height - ascent - descent) / 2.0; - let baseline = line_y + padding_top + ascent; - - // Keep line_y as the top of the line for proper line spacing - // Don't modify line_y here - it should remain at the top of the line - - // Calculate padding - let padding_y = if line_height_mod > 1.0 { - (line_height - line_height_without_mod) / 2.0 - } else { - 0.0 - }; - - let py = line_y; - - let cell_width = rte_layout.unwrap().dimensions.width; - - for run in &line.render_data.runs { - let char_width = run.span.width; - - // Fast path: empty run (blanks/spaces) — just advance - // and optionally paint background/cursor/decoration. - // Decoration must be checked too: an underline cursor - // on a blank cell carries its line through `decoration` - // (not `cursor`) and the cell's `background_color` may - // have been stripped to `None` when the window has a - // background image / opacity < 1, so without the - // decoration check the cursor would silently vanish. - if run.glyphs.is_empty() { - let advance = cell_width * char_width; - let run_x = px; - px += advance; - - if run.span.background_color.is_some() - || run.span.cursor.is_some() - || run.span.decoration.is_some() - { - let style = TextRunStyle { - font_coords, - font_size: run.size, - color: run.span.color, - cursor: run.span.cursor, - drawable_char: run.span.drawable_char, - background_color: run.span.background_color, - baseline, - topline: py, - line_height, - padding_y, - line_height_without_mod, - advance, - decoration: run.span.decoration, - decoration_color: run.span.decoration_color, - underline_offset: run.underline_offset, - strikeout_offset: run.strikeout_offset, - underline_thickness: run.strikeout_size, - x_height: run.x_height, - ascent: run.ascent, - descent: run.descent, - scale_constraint: None, - nerd_font_constraint: None, - }; - comp.draw_run( - &mut session, - Rect::new(run_x, py, advance, 1.), - depth, - &style, - &[], - order, - ); - } - - continue; - } - - let font = run.span.font_id; - let run_x = px; - - // Use pre-computed cache key — no String allocation needed - let cached_result = if run.cache_key != 0 { - self.text_run_manager.get_cached_data_by_key(run.cache_key) - } else { - CacheResult::Miss - }; - - match cached_result { - CacheResult::Hit { - glyphs: cached_glyphs, - .. - } => { - // Use cached glyph data but need to render - glyphs.clear(); - // Cell centering for East-Asian-Wide codepoints: - // when a glyph's shaped slot is wider than one - // primary cell (char_width > 1), shift it by - // half the extra space so 겔 / 水 / 한 sit - // visually centered across their two cells - // instead of hugging the left cell. Formula: - // `dx = (cell_width - face_width) / 2`. No-op - // for char_width == 1 (Latin, box-drawing), - // so glyphs that rely on tiling at their - // natural pen advance stay aligned. - let cell_shift = if use_grid_cell_size && char_width > 1.0 { - cell_width * (char_width - 1.0) / 2.0 - } else { - 0.0 - }; - for shaped_glyph in cached_glyphs.iter() { - let x = px + cell_shift; - let y = baseline; - // Effective per-glyph pen advance — on the - // grid-cell-size path this is `cell_width * - // char_width` (e.g. 2 cells for East Asian - // Wide emoji), not the shaper's advance. - // Pass this through to the compositor so - // emoji bitmaps center inside the actual - // cell slot, not a 1-cell shaper advance. - let advance = if use_grid_cell_size { - cell_width * char_width - } else { - shaped_glyph.x_advance - }; - px += advance; - - glyphs.push(Glyph { - id: shaped_glyph.glyph_id as GlyphId, - x, - y, - advance, - }); - } - - // Render using cached glyph data - let style = TextRunStyle { - font_coords, - font_size: run.size, - color: run.span.color, - cursor: run.span.cursor, - drawable_char: run.span.drawable_char, - background_color: run.span.background_color, - baseline, - topline: py, // Use py (line top) for cursor positioning - line_height, - padding_y, - line_height_without_mod, - advance: cached_glyphs.iter().map(|g| g.x_advance).sum(), - decoration: run.span.decoration, - decoration_color: run.span.decoration_color, - underline_offset: run.underline_offset, - strikeout_offset: run.strikeout_offset, - underline_thickness: run.strikeout_size, - x_height: run.x_height, - ascent: run.ascent, - descent: run.descent, - scale_constraint: run.span.pua_constraint.and_then(|c| { - rte_layout.map(|l| (l.dimensions.width, c as u8)) - }), - nerd_font_constraint: run.span.nerd_font_constraint, - }; - - // Update font session if needed - if font != current_font - || style.font_size != current_font_size - { - current_font = font; - current_font_size = style.font_size; - - session = glyphs_cache.session( - image_cache, - current_font, - font_library, - font_coords, - style.font_size, - ); - } - - comp.draw_run( - &mut session, - Rect::new(run_x, py, px - run_x, 1.), - depth, - &style, - &glyphs, - order, - ); - } - CacheResult::Miss => { - // No cached data - need to shape and render from scratch - glyphs.clear(); - let mut shaped_glyphs = Vec::new(); - - // Same cell centering as above. - let cell_shift = if use_grid_cell_size && char_width > 1.0 { - cell_width * (char_width - 1.0) / 2.0 - } else { - 0.0 - }; - - for glyph in &run.glyphs { - let x = px + cell_shift; - let y = baseline; - let shaper_advance = glyph.simple_data().1; - // See the cached-path comment above — on the - // grid-cell-size path the effective advance - // is cell_width × char_width, not the shaper's. - let advance = if use_grid_cell_size { - cell_width * char_width - } else { - shaper_advance - }; - px += advance; - - let glyph_id = glyph.simple_data().0; - - glyphs.push(Glyph { - id: glyph_id, - x, - y, - advance, - }); - - // Cache the raw shaper advance; `use_grid_cell_size` - // is a property of the font/config pipeline and - // its grid-adjusted advance is recomputed on - // cache hits, so only the shaper value belongs - // in the persistent cache. - shaped_glyphs.push( - crate::font::text_run_cache::ShapedGlyph { - glyph_id: glyph_id as u32, - x_advance: shaper_advance, - y_advance: 0.0, - x_offset: 0.0, - y_offset: 0.0, - cluster: 0, - }, - ); - } - - // Cache the shaped glyphs for future use - if run.cache_key != 0 { - self.text_run_manager.cache_shaping_data_by_key( - run.cache_key, - font, - run.size, - shaped_glyphs, - false, - ); - } - - // Create style for rendering - let style = TextRunStyle { - font_coords, - font_size: run.size, - color: run.span.color, - cursor: run.span.cursor, - drawable_char: run.span.drawable_char, - background_color: run.span.background_color, - baseline, - topline: py, // Use py (line top) for cursor positioning - line_height, - padding_y, - line_height_without_mod, - advance: px - run_x, - decoration: run.span.decoration, - decoration_color: run.span.decoration_color, - underline_offset: run.underline_offset, - strikeout_offset: run.strikeout_offset, - underline_thickness: run.strikeout_size, - x_height: run.x_height, - ascent: run.ascent, - descent: run.descent, - scale_constraint: run.span.pua_constraint.and_then(|c| { - rte_layout.map(|l| (l.dimensions.width, c as u8)) - }), - nerd_font_constraint: run.span.nerd_font_constraint, - }; - - // Update font session if needed - if font != current_font - || style.font_size != current_font_size - { - current_font = font; - current_font_size = style.font_size; - - session = glyphs_cache.session( - image_cache, - current_font, - font_library, - font_coords, - style.font_size, - ); - } - - comp.draw_run( - &mut session, - Rect::new(run_x, py, px - run_x, 1.), - depth, - &style, - &glyphs, - order, - ); - } - } - - // Handle graphics - render directly using add_image_rect - if let Some(graphic) = run.span.media { - // Each cell stores which part of the graphic it shows via offset_x/offset_y - // We render once per graphic per frame, using the first cell we encounter - // We calculate the graphic's position by subtracting the cell's offset - // This ensures the graphic renders even when the origin cell is scrolled off-screen - if !last_rendered_graphic.contains(&graphic.id) { - // Get cached graphic data - if let Some(cached) = self.graphic_cache.get(&graphic.id) { - // Calculate graphic position: current cell position minus this cell's offset - // This positions the full graphic correctly regardless of which cell we encounter - let gx = run_x - graphic.offset_x as f32; - let gy = py - graphic.offset_y as f32; - - tracing::info!( - "Drawing graphic at ({}, {}), size={}x{}, atlas_layer={}", - gx, - gy, - cached.width, - cached.height, - cached.atlas_layer - ); - - // Clip display size to cell grid boundaries - // so the image never overflows into the next line. - let cw = rte_layout.unwrap().dimensions.width; - let render_w = (cached.width / cw).floor() * cw; - let render_h = - (cached.height / line_height).floor() * line_height; - comp.batches.add_image_rect( - &Rect::new(gx, gy, render_w, render_h), - depth, - &[1.0, 1.0, 1.0, 1.0], - &[ - cached.location.min.0, - cached.location.min.1, - cached.location.max.0, - cached.location.max.1, - ], - cached.atlas_layer, - ); - } else { - tracing::warn!( - "Graphic {} not in cache!", - graphic.id.get() - ); - } - - last_rendered_graphic.insert(graphic.id); - } - } - } - - // Advance line_y for the next line - line_y += line_height; - } - } - } /// Render image overlays using per-image GPU textures. fn render_graphic_overlays( @@ -2192,8 +1602,6 @@ impl Renderer { #[inline] pub fn reset(&mut self) { - self.glyphs = GlyphCache::new(); - self.text_run_manager.clear_all(); self.graphic_cache.clear(); self.image_textures.clear(); self.image_draws.clear(); @@ -2202,14 +1610,10 @@ impl Renderer { #[inline] pub fn clear_atlas(&mut self) { self.images.clear_atlas(); - self.glyphs = GlyphCache::new(); - self.text_run_manager.clear_all(); self.graphic_cache.clear(); self.image_textures.clear(); self.image_draws.clear(); - tracing::info!( - "Renderer atlas, glyph cache, text run cache, and graphic cache cleared" - ); + tracing::info!("Renderer atlas + graphic cache cleared"); } #[inline] diff --git a/sugarloaf/src/renderer/positioning_tests.rs b/sugarloaf/src/renderer/positioning_tests.rs deleted file mode 100644 index bc231979..00000000 --- a/sugarloaf/src/renderer/positioning_tests.rs +++ /dev/null @@ -1,1091 +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)] -// Positioning tests for cached vs non-cached text rendering - -use super::text_run_manager::{CacheResult, TextRunManager}; -use crate::font::text_run_cache::ShapedGlyph; -use crate::sugarloaf::primitives::{CursorKind, DrawableChar, SugarCursor}; - -/// Captured positioning data for comparison -#[derive(Debug, Clone, PartialEq)] -pub struct PositioningData { - pub baseline: f32, - pub topline: f32, - pub py: f32, - pub padding_y: f32, - pub line_height: f32, - pub glyph_positions: Vec<(f32, f32)>, // (x, y) for each glyph - pub cursor: Option, - pub drawable_char: Option, - pub advance: f32, -} - -/// Test helper to simulate text rendering and capture positioning data -pub struct PositioningTestHelper { - text_run_manager: TextRunManager, -} - -impl PositioningTestHelper { - pub fn new() -> Self { - Self { - text_run_manager: TextRunManager::new(), - } - } - - /// Simulate the positioning calculations from the rich text renderer - #[allow(clippy::too_many_arguments)] - pub fn calculate_positioning( - &mut self, - text: &str, - font_id: usize, - font_size: f32, - ascent: f32, - descent: f32, - line_height: f32, - cursor: Option, - drawable_char: Option, - use_cache: bool, - ) -> PositioningData { - let char_width = 1.0f32; - - // Simulate the line positioning calculations from mod.rs - let line_y = 0.0f32; // Starting position - let padding_top = (line_height - ascent - descent) / 2.0; - let baseline = line_y + padding_top + ascent; - let py = line_y; // py is now just the line_y, not modified - - // Calculate padding (from line height modifier logic) - let line_height_without_mod = ascent + descent; - let line_height_mod = line_height / line_height_without_mod; - let padding_y = if line_height_mod > 1.0 { - (line_height - line_height_without_mod) / 2.0 - } else { - 0.0 - }; - - let mut glyph_positions = Vec::new(); - let mut px = 0.0f32; - let advance; - - if use_cache { - // Try to get cached data - let key = crate::font::text_run_cache::create_text_run_key( - text, font_id, font_size, - ); - let cached_result = self.text_run_manager.get_cached_data_by_key(key); - - match cached_result { - CacheResult::Hit { - glyphs: cached_glyphs, - .. - } => { - // Use cached glyph data - for shaped_glyph in cached_glyphs.iter() { - let x = px; - let y = py + padding_y; - glyph_positions.push((x, y)); - px += shaped_glyph.x_advance * char_width; - } - advance = cached_glyphs.iter().map(|g| g.x_advance).sum(); - } - CacheResult::Miss => { - // Cache miss - simulate fresh shaping - advance = self.simulate_fresh_shaping( - text, - &mut px, - py, - padding_y, - char_width, - &mut glyph_positions, - ); - } - } - } else { - // Simulate non-cached path - advance = self.simulate_fresh_shaping( - text, - &mut px, - py, - padding_y, - char_width, - &mut glyph_positions, - ); - } - - PositioningData { - baseline, // Use the calculated baseline position - topline: py, // Use py (line top) for cursor positioning - py, - padding_y, - line_height, - glyph_positions, - cursor, - drawable_char, - advance, - } - } - - fn simulate_fresh_shaping( - &mut self, - text: &str, - px: &mut f32, - py: f32, - padding_y: f32, - char_width: f32, - glyph_positions: &mut Vec<(f32, f32)>, - ) -> f32 { - let mut shaped_glyphs = Vec::new(); - let run_start_x = *px; - - // Simulate shaping each character - for (i, _ch) in text.chars().enumerate() { - let x = *px; - let y = py + padding_y; - let advance = 10.0f32; // Mock advance per character - - glyph_positions.push((x, y)); - *px += advance * char_width; - - // Create shaped glyph for caching - shaped_glyphs.push(ShapedGlyph { - glyph_id: (65 + i) as u32, // Mock glyph IDs (A, B, C, ...) - x_advance: advance, - y_advance: 0.0, - x_offset: 0.0, - y_offset: 0.0, - cluster: i as u32, - }); - } - - // Cache the shaped data for future use - let key = crate::font::text_run_cache::create_text_run_key(text, 0, 12.0); - self.text_run_manager.cache_shaping_data_by_key( - key, - 0, // font_id - 12.0, // font_size - shaped_glyphs, - false, // has_emoji - ); - - *px - run_start_x - } - - /// Clear the cache to test non-cached behavior - pub fn clear_cache(&mut self) { - self.text_run_manager.clear_all(); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_positioning_consistency_without_cursor() { - let mut helper = PositioningTestHelper::new(); - - // Test basic text positioning - let text = "Hello"; - let font_id = 0; - let font_size = 12.0; - let ascent = 10.0; - let descent = 3.0; - let line_height = 15.0; - - // First run - no cache (fresh shaping) - let non_cached = helper.calculate_positioning( - text, - font_id, - font_size, - ascent, - descent, - line_height, - None, - None, - false, - ); - - // Second run - should use cache - let cached = helper.calculate_positioning( - text, - font_id, - font_size, - ascent, - descent, - line_height, - None, - None, - true, - ); - - // Compare positioning data - assert_eq!( - non_cached.baseline, cached.baseline, - "Baseline should be identical" - ); - assert_eq!( - non_cached.topline, cached.topline, - "Topline should be identical" - ); - assert_eq!(non_cached.py, cached.py, "py should be identical"); - assert_eq!( - non_cached.padding_y, cached.padding_y, - "padding_y should be identical" - ); - assert_eq!( - non_cached.glyph_positions.len(), - cached.glyph_positions.len(), - "Should have same number of glyphs" - ); - - // Check each glyph position - for (i, (non_cached_pos, cached_pos)) in non_cached - .glyph_positions - .iter() - .zip(cached.glyph_positions.iter()) - .enumerate() - { - assert_eq!( - non_cached_pos.0, cached_pos.0, - "Glyph {} x position should match", - i - ); - assert_eq!( - non_cached_pos.1, cached_pos.1, - "Glyph {} y position should match", - i - ); - } - - // println!("Non-cached positioning: {:?}", non_cached); - // println!("Cached positioning: {:?}", cached); - } - - #[test] - fn test_cursor_positioning_consistency() { - let mut helper = PositioningTestHelper::new(); - - let text = "Test"; - let font_id = 0; - let font_size = 12.0; - let ascent = 10.0; - let descent = 3.0; - let line_height = 15.0; - let _cursor = Some(SugarCursor { - kind: CursorKind::Block, - color: [1.0, 0.0, 0.0, 1.0], - order: 0, - }); - - // Test with different cursor types - let cursor_types = vec![ - SugarCursor { - kind: CursorKind::Block, - color: [1.0, 0.0, 0.0, 1.0], - order: 0, - }, - SugarCursor { - kind: CursorKind::Underline, - color: [0.0, 1.0, 0.0, 1.0], - order: 0, - }, - SugarCursor { - kind: CursorKind::Caret, - color: [0.0, 0.0, 1.0, 1.0], - order: 0, - }, - ]; - - for cursor_type in cursor_types { - helper.clear_cache(); - - // Non-cached run - let non_cached = helper.calculate_positioning( - text, - font_id, - font_size, - ascent, - descent, - line_height, - Some(cursor_type), - None, - false, - ); - - // Cached run - let cached = helper.calculate_positioning( - text, - font_id, - font_size, - ascent, - descent, - line_height, - Some(cursor_type), - None, - true, - ); - - // Cursor positioning should be identical - assert_eq!( - non_cached.baseline, cached.baseline, - "Cursor baseline should match for {:?}", - cursor_type - ); - assert_eq!( - non_cached.topline, cached.topline, - "Cursor topline should match for {:?}", - cursor_type - ); - - // println!( - // "Cursor {:?} - Non-cached: baseline={}, topline={}", - // cursor_type, non_cached.baseline, non_cached.topline - // ); - // println!( - // "Cursor {:?} - Cached: baseline={}, topline={}", - // cursor_type, cached.baseline, cached.topline - // ); - } - } - - #[test] - fn test_drawable_character_cached_vs_non_cached() { - let mut helper = PositioningTestHelper::new(); - - let text = "X"; - let font_id = 0; - let font_size = 12.0; - let ascent = 10.0; - let descent = 3.0; - let line_height = 15.0; - let drawable_char = Some(DrawableChar::Cross); - - helper.clear_cache(); - - // Non-cached run - let non_cached = helper.calculate_positioning( - text, - font_id, - font_size, - ascent, - descent, - line_height, - None, - drawable_char, - false, - ); - - // Cached run - let cached = helper.calculate_positioning( - text, - font_id, - font_size, - ascent, - descent, - line_height, - None, - drawable_char, - true, - ); - - // Drawable character positioning should be identical - assert_eq!( - non_cached.baseline, cached.baseline, - "Drawable char baseline should match" - ); - assert_eq!( - non_cached.topline, cached.topline, - "Drawable char topline should match" - ); - assert_eq!(non_cached.py, cached.py, "Drawable char py should match"); - } - - #[test] - fn test_padding_y_effects() { - let mut helper = PositioningTestHelper::new(); - - let text = "Test"; - let font_id = 0; - let font_size = 12.0; - let ascent = 10.0; - let descent = 3.0; - - // Test with different line heights to trigger different padding_y values - let line_heights = vec![13.0, 15.0, 20.0]; // Normal, slightly increased, significantly increased - - for line_height in line_heights { - helper.clear_cache(); - - let non_cached = helper.calculate_positioning( - text, - font_id, - font_size, - ascent, - descent, - line_height, - None, - None, - false, - ); - - let cached = helper.calculate_positioning( - text, - font_id, - font_size, - ascent, - descent, - line_height, - None, - None, - true, - ); - - // println!( - // "Line height {}: padding_y={}, baseline={}, topline={}, py={}", - // line_height, - // non_cached.padding_y, - // non_cached.baseline, - // non_cached.topline, - // non_cached.py - // ); - - assert_eq!( - non_cached.padding_y, cached.padding_y, - "padding_y should match for line_height {}", - line_height - ); - assert_eq!( - non_cached.baseline, cached.baseline, - "baseline should match for line_height {}", - line_height - ); - assert_eq!( - non_cached.topline, cached.topline, - "topline should match for line_height {}", - line_height - ); - } - } - - #[test] - fn test_glyph_vs_cursor_positioning_relationship() { - let mut helper = PositioningTestHelper::new(); - - let text = "A"; - let font_id = 0; - let font_size = 12.0; - let ascent = 10.0; - let descent = 3.0; - let line_height = 15.0; - - helper.clear_cache(); - - // Test text with cursor - let with_cursor = helper.calculate_positioning( - text, - font_id, - font_size, - ascent, - descent, - line_height, - Some(SugarCursor { - kind: CursorKind::Block, - color: [1.0, 0.0, 0.0, 1.0], - order: 0, - }), - None, - true, - ); - - // The relationship between glyph y position and cursor positioning elements: - // - Glyph y = py + padding_y - // - Cursor block uses topline = baseline - ascent - // - Cursor underline uses baseline + 1.0 - - if let Some((_glyph_x, glyph_y)) = with_cursor.glyph_positions.first() { - // println!("Glyph position: ({}, {})", glyph_x, glyph_y); - // println!( - // "Expected glyph y: py + padding_y = {} + {} = {}", - // with_cursor.py, - // with_cursor.padding_y, - // with_cursor.py + with_cursor.padding_y - // ); - // println!("Cursor topline (block): {}", with_cursor.topline); - // println!("Cursor baseline (underline): {}", with_cursor.baseline); - - // Verify the glyph y calculation - assert_eq!( - *glyph_y, - with_cursor.py + with_cursor.padding_y, - "Glyph y should equal py + padding_y" - ); - - // The cursor should be positioned relative to the same baseline as the text - // Now that baseline = py - descent, the relationships are: - // - glyph_y = py + padding_y (glyphs positioned relative to py) - // - cursor baseline = py - descent (actual text baseline) - let expected_glyph_baseline = glyph_y - with_cursor.padding_y; // This should be py - let cursor_baseline_in_line_coords = with_cursor.baseline; // This should be py - descent - - // println!( - // "Expected glyph baseline (line coords): {}", - // expected_glyph_baseline - // ); - // println!( - // "Cursor baseline (line coords): {}", - // cursor_baseline_in_line_coords - // ); - - // With the new coordinate system: cursor_baseline = glyph_baseline + padding_top + ascent - let padding_top = (line_height - ascent - descent) / 2.0; - assert!((cursor_baseline_in_line_coords - (expected_glyph_baseline + padding_top + ascent)).abs() < 0.1, - "Cursor baseline should equal glyph baseline + padding_top + ascent. Cursor: {}, Glyph: {}, Padding: {}, Ascent: {}", - cursor_baseline_in_line_coords, expected_glyph_baseline, padding_top, ascent); - } - } - - #[test] - fn test_drawable_character_positioning() { - let mut helper = PositioningTestHelper::new(); - let font_id = 0; - let font_size = 16.0; - let ascent = 12.0; - let descent = 3.0; - let line_height = 20.0; - let drawable_char = Some(crate::DrawableChar::Horizontal); - - let result = helper.calculate_positioning( - "─", - font_id, - font_size, - ascent, - descent, - line_height, - None, - drawable_char, - false, - ); - - // Verify baseline relationships are correct with new coordinate system - let padding_top = (line_height - ascent - descent) / 2.0; - let expected_baseline = result.py + padding_top + ascent; - assert_eq!( - result.baseline, expected_baseline, - "baseline should equal py + padding_top + ascent" - ); - assert_eq!( - result.topline, result.py, - "topline should be py (line top) for cursor positioning" - ); - - // The drawable character should be positioned at topline - // This means center_y = topline + (line_height / 2.0) - let expected_center_y = result.topline + (line_height / 2.0); - - // For horizontal lines, center should be reasonably close to text baseline - let text_baseline = result.baseline; // Use the actual baseline - let diff = (expected_center_y - text_baseline).abs(); - - // Should be within reasonable range (half the line height) - assert!(diff < line_height / 2.0, - "Drawable character center ({}) should be reasonably close to text baseline ({}). Diff: {}", - expected_center_y, text_baseline, diff); - } - - #[test] - fn test_cursor_positioning_relationships() { - let mut helper = PositioningTestHelper::new(); - let font_id = 0; - let font_size = 16.0; - let ascent = 12.0; - let descent = 3.0; - let line_height = 20.0; - - for cursor_type in [ - Some(SugarCursor { - kind: CursorKind::Block, - color: [1.0, 1.0, 1.0, 1.0], - order: 0, - }), - Some(SugarCursor { - kind: CursorKind::Caret, - color: [1.0, 1.0, 1.0, 1.0], - order: 0, - }), - Some(SugarCursor { - kind: CursorKind::Underline, - color: [1.0, 1.0, 1.0, 1.0], - order: 0, - }), - ] { - let result = helper.calculate_positioning( - "A", - font_id, - font_size, - ascent, - descent, - line_height, - cursor_type, - None, - false, - ); - - // Core relationships that must always hold with new coordinate system - let padding_top = (line_height - ascent - descent) / 2.0; - assert_eq!( - result.baseline, - result.py + padding_top + ascent, - "baseline should equal py + padding_top + ascent for cursor {:?}", - cursor_type - ); - assert_eq!( - result.topline, result.py, - "topline should be py (line top) for cursor {:?}", - cursor_type - ); - - // Block and Caret cursors use topline - // Underline cursor uses baseline + 1.0 - match cursor_type.map(|c| c.kind) { - Some(CursorKind::Block) | Some(CursorKind::Caret) => { - // These should span from topline to topline + line_height - let cursor_top = result.topline; - let cursor_bottom = result.topline + line_height; - - // Verify cursor encompasses the text area - let text_top = result.baseline - ascent; - let text_bottom = result.baseline + descent; - assert!( - cursor_top <= text_top, - "Block/Caret cursor top should be at or above text top" - ); - assert!( - cursor_bottom >= text_bottom, - "Block/Caret cursor bottom should be at or below text bottom" - ); - } - Some(CursorKind::Underline) => { - // Underline should be positioned at baseline + 1.0 - let underline_y = result.baseline + 1.0; - - // Should be close to the text baseline - let text_baseline = result.baseline; // In new system, baseline is the actual text baseline - assert!( - (underline_y - text_baseline).abs() < descent + 2.0, - "Underline cursor should be close to text baseline" - ); - } - _ => {} - } - } - } - - #[test] - fn test_glyph_and_cursor_baseline_consistency() { - let mut helper = PositioningTestHelper::new(); - let font_id = 0; - let font_size = 16.0; - let ascent = 12.0; - let descent = 3.0; - let cursor = Some(SugarCursor { - kind: CursorKind::Block, - color: [1.0, 1.0, 1.0, 1.0], - order: 0, - }); - - // Test with different line heights to ensure consistency - for test_line_height in [16.0, 20.0, 24.0, 30.0] { - let result = helper.calculate_positioning( - "Ag", - font_id, - font_size, - ascent, - descent, - test_line_height, - cursor, - None, - false, - ); - - // Get glyph positioning - let glyph_y = result.py + result.padding_y; - let glyph_baseline_in_line_coords = glyph_y - result.padding_y; // Should be py - - // With the new coordinate system: - // - glyph_baseline_in_line_coords is py (top of line) - // - cursor baseline is py + padding_top + ascent - // So the relationship is: cursor_baseline = glyph_baseline + padding_top + ascent - let padding_top = (test_line_height - ascent - descent) / 2.0; - assert_eq!(result.baseline, glyph_baseline_in_line_coords + padding_top + ascent, - "Cursor baseline ({}) should equal glyph baseline ({}) + padding_top ({}) + ascent ({}) for line_height {}", - result.baseline, glyph_baseline_in_line_coords, padding_top, ascent, test_line_height); - - // Verify padding_y calculation is correct - // The actual calculation is: if line_height_mod > 1.0 then (line_height - line_height_without_mod) / 2.0 else 0.0 - // where line_height = line_height_without_mod * line_height_mod - // and line_height_without_mod = ascent + descent + leading (leading is usually 0) - let line_height_without_mod = ascent + descent; // assuming leading = 0 - let line_height_mod = test_line_height / line_height_without_mod; - let expected_padding_y = if line_height_mod > 1.0 { - (test_line_height - line_height_without_mod) / 2.0 - } else { - 0.0 - }; - assert!((result.padding_y - expected_padding_y).abs() < 0.1, - "padding_y should be calculated correctly for line_height {}. Expected: {}, Got: {}, line_height_mod: {}", - test_line_height, expected_padding_y, result.padding_y, line_height_mod); - } - } - - #[test] - fn test_cached_vs_non_cached_positioning_consistency() { - let mut helper = PositioningTestHelper::new(); - let font_id = 0; - let font_size = 16.0; - let ascent = 12.0; - let descent = 3.0; - let line_height = 20.0; - - let test_cases = [ - ("Hello", None, None), - ( - "World", - Some(SugarCursor { - kind: CursorKind::Block, - color: [1.0, 1.0, 1.0, 1.0], - order: 0, - }), - None, - ), - ("─", None, Some(crate::DrawableChar::Horizontal)), - ( - "│", - Some(SugarCursor { - kind: CursorKind::Underline, - color: [1.0, 1.0, 1.0, 1.0], - order: 0, - }), - Some(crate::DrawableChar::Vertical), - ), - ]; - - for (text, cursor, drawable_char) in test_cases { - let non_cached = helper.calculate_positioning( - text, - font_id, - font_size, - ascent, - descent, - line_height, - cursor, - drawable_char, - false, - ); - - let cached = helper.calculate_positioning( - text, - font_id, - font_size, - ascent, - descent, - line_height, - cursor, - drawable_char, - true, - ); - - // All positioning values must be identical between cached and non-cached - assert_eq!( - non_cached.baseline, cached.baseline, - "Baseline mismatch for '{}': non-cached={}, cached={}", - text, non_cached.baseline, cached.baseline - ); - assert_eq!( - non_cached.topline, cached.topline, - "Topline mismatch for '{}': non-cached={}, cached={}", - text, non_cached.topline, cached.topline - ); - assert_eq!( - non_cached.py, cached.py, - "py mismatch for '{}': non-cached={}, cached={}", - text, non_cached.py, cached.py - ); - assert_eq!( - non_cached.padding_y, cached.padding_y, - "padding_y mismatch for '{}': non-cached={}, cached={}", - text, non_cached.padding_y, cached.padding_y - ); - } - } - - #[test] - fn test_positioning_invariants() { - let mut helper = PositioningTestHelper::new(); - let font_id = 0; - let font_size = 16.0; - let ascent = 12.0; - let descent = 3.0; - - // Test various line heights - for line_height in [16.0, 18.0, 20.0, 24.0, 32.0] { - let result = helper.calculate_positioning( - "Test", - font_id, - font_size, - ascent, - descent, - line_height, - None, - None, - false, - ); - - // Core invariants that must always hold with new coordinate system - let padding_top = (line_height - ascent - descent) / 2.0; - assert_eq!( - result.baseline, - result.py + padding_top + ascent, - "INVARIANT: baseline must equal py + padding_top + ascent (line_height={})", - line_height - ); - - assert_eq!( - result.topline, result.py, - "INVARIANT: topline must equal py (line top) (line_height={})", - line_height - ); - - // Padding calculation invariant - let line_height_without_mod = ascent + descent; // assuming leading = 0 - let line_height_mod = line_height / line_height_without_mod; - let expected_padding = if line_height_mod > 1.0 { - (line_height - line_height_without_mod) / 2.0 - } else { - 0.0 - }; - assert!( - (result.padding_y - expected_padding).abs() < 0.1, - "INVARIANT: padding_y calculation (line_height={}, mod={})", - line_height, - line_height_mod - ); - - // Glyph positioning invariant - let glyph_y = result.py + result.padding_y; - assert!( - glyph_y >= result.py, - "INVARIANT: glyph_y must be >= py (line_height={})", - line_height - ); - - // Text should fit within the line - let text_top = result.py - ascent; - let text_bottom = result.py + descent; - let line_span = text_bottom - text_top; - assert!( - line_span <= line_height + 1.0, // +1 for rounding tolerance - "INVARIANT: text should fit within line_height (line_height={})", - line_height - ); - } - } - - #[test] - fn test_strikethrough_positioning() { - use crate::layout::SpanStyleDecoration; - use crate::renderer::compositor::{Compositor, Rect}; - use crate::renderer::text::TextRunStyle; - - let compositor = Compositor::new(); - let font_size = 16.0; - let line_height = 20.0; - let x_height = 8.0; // Typical x-height is about half the font size - let strikeout_offset = 4.0; // Font-provided strikeout offset - let ascent = 12.0; - let descent = 4.0; - - // Test with font-provided strikeout offset - let style_with_offset = TextRunStyle { - font_coords: &[], - font_size, - color: [1.0, 1.0, 1.0, 1.0], - background_color: None, - baseline: 16.0, - topline: 0.0, - line_height, - padding_y: 0.0, - line_height_without_mod: line_height, - advance: 100.0, - decoration: Some(SpanStyleDecoration::Strikethrough), - decoration_color: None, - cursor: None, - drawable_char: None, - underline_offset: 2.0, - strikeout_offset, - underline_thickness: 1.0, - x_height, - ascent, - descent, - scale_constraint: None, - nerd_font_constraint: None, - }; - - let _rect = Rect::new(0.0, 0.0, 100.0, line_height); - let underline = compositor.create_underline_from_decoration(&style_with_offset); - - if let Some(underline) = underline { - // Should use font's strikeout offset (negated) - assert_eq!(underline.offset, -strikeout_offset); - } else { - panic!("Expected strikethrough underline"); - } - - // Test with x-height fallback (no font strikeout offset) - let style_with_x_height = TextRunStyle { - strikeout_offset: 0.0, // No font-provided offset - x_height, - ..style_with_offset - }; - - let underline = compositor.create_underline_from_decoration(&style_with_x_height); - - if let Some(underline) = underline { - // Should use half of x-height above baseline - assert_eq!(underline.offset, -(x_height / 2.0)); - } else { - panic!("Expected strikethrough underline"); - } - - // Test with final fallback (no font offset or x-height) - let style_fallback = TextRunStyle { - strikeout_offset: 0.0, - x_height: 0.0, - ..style_with_offset - }; - - let underline = compositor.create_underline_from_decoration(&style_fallback); - - if let Some(underline) = underline { - // Should use 25% of ascent above baseline as fallback - assert_eq!(underline.offset, -(ascent * 0.25)); - } else { - panic!("Expected strikethrough underline"); - } - } - - #[test] - fn test_cursor_font_based_sizing() { - // Test that cursor size is based on font metrics, not line height - let _font_size = 16.0; - let ascent = 12.0; - let descent = 4.0; - let font_height = ascent + descent; // 16.0 - - // Test with different line heights to ensure cursor size is independent - let line_heights = [16.0, 20.0, 24.0, 32.0]; - - for line_height in line_heights { - let baseline = 16.0; - let cursor_top = baseline - ascent; // 16 - 12 = 4.0 - - // Verify cursor dimensions are based on font metrics - assert_eq!( - font_height, - ascent + descent, - "Font height should equal ascent + descent" - ); - assert_eq!( - cursor_top, - baseline - ascent, - "Cursor top should be baseline - ascent" - ); - - // Cursor should be smaller than line height for increased line spacing - if line_height > font_height { - assert!(font_height < line_height, "Cursor should be smaller than line height when line spacing is increased"); - } - } - } - - #[test] - fn test_cursor_positioning_with_different_font_metrics() { - let _font_size = 16.0; - let _line_height = 24.0; // Larger than font size - let baseline = 20.0; - - // Test different font metric combinations - let font_metrics = [ - ("Normal font", 12.0, 4.0), // ascent=12, descent=4, total=16 - ("Tall font", 14.0, 6.0), // ascent=14, descent=6, total=20 - ("Short font", 10.0, 2.0), // ascent=10, descent=2, total=12 - ("Deep descenders", 11.0, 8.0), // ascent=11, descent=8, total=19 - ]; - - for (font_name, ascent, descent) in font_metrics { - let font_height = ascent + descent; - let cursor_top = baseline - ascent; - - // Verify cursor positioning - assert_eq!( - font_height, - ascent + descent, - "{}: Font height should equal ascent + descent", - font_name - ); - assert_eq!( - cursor_top, - baseline - ascent, - "{}: Cursor top should be baseline - ascent", - font_name - ); - - // Cursor should start above baseline for fonts with ascent - if ascent > 0.0 { - assert!( - cursor_top < baseline, - "{}: Cursor should start above baseline", - font_name - ); - } - - // Cursor should end below baseline for fonts with descent - if descent > 0.0 { - assert!( - cursor_top + font_height > baseline, - "{}: Cursor should extend below baseline", - font_name - ); - } - } - } - - #[test] - fn test_underline_cursor_positioning() { - let _font_size = 16.0; - let _line_height = 24.0; - let baseline = 20.0; - let _ascent = 12.0; - let _descent = 4.0; - - // Underline cursor should be positioned at baseline + 1.0 with height 2.0 - let expected_underline_y = baseline + 1.0; - let expected_underline_height = 2.0; - - // Underline cursor positioning should be independent of font metrics - // It should always be just below the baseline - assert_eq!( - expected_underline_y, - baseline + 1.0, - "Underline cursor should be 1px below baseline" - ); - assert_eq!( - expected_underline_height, 2.0, - "Underline cursor should be 2px tall" - ); - } -} diff --git a/sugarloaf/src/renderer/text.rs b/sugarloaf/src/renderer/text.rs deleted file mode 100644 index c51993b0..00000000 --- a/sugarloaf/src/renderer/text.rs +++ /dev/null @@ -1,77 +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. -// -// text.rs was originally retired from dfrg/swash_demo licensed under MIT -// https://github.com/dfrg/swash_demo/blob/master/LICENSE -// -// Eventually the file had updates to support other features like background-color, -// text color, underline color and etc. - -use swash::{GlyphId, NormalizedCoord}; -use crate::layout::SpanStyleDecoration; -use crate::sugarloaf::primitives::{DrawableChar, SugarCursor}; - -/// Properties for a text run. -#[derive(Copy, Clone)] -pub struct TextRunStyle<'a> { - /// Normalized variation coordinates for the font. - pub font_coords: &'a [NormalizedCoord], - /// Font size. - pub font_size: f32, - /// Color of the text. - pub color: [f32; 4], - /// Background of the text. - pub background_color: Option<[f32; 4]>, - /// Baseline of the run. - pub baseline: f32, - /// Topline of the run (basically y axis). - pub topline: f32, - /// Absolute line height of the run. - pub line_height: f32, - /// Padding y - pub padding_y: f32, - /// Absolute line height of the run without mod. - pub line_height_without_mod: f32, - /// Total advance of the run. - pub advance: f32, - /// Underline style. - pub decoration: Option, - /// Underline style. - pub decoration_color: Option<[f32; 4]>, - /// Cursor style. - pub cursor: Option, - pub drawable_char: Option, - /// Font metrics for proper underline/strikethrough positioning - pub underline_offset: f32, - pub strikeout_offset: f32, - pub underline_thickness: f32, - pub x_height: f32, - /// Font ascent and descent for cursor positioning - pub ascent: f32, - pub descent: f32, - /// Scale constraint: (cell_width_px, constraint_cells). - /// None for normal glyphs. When set, the glyph is scaled to fit - /// constraint_cells * cell_width_px without affecting positioning. - pub scale_constraint: Option<(f32, u8)>, - /// Per-glyph Nerd Font constraint from ghostty's patcher table. - /// When set, overrides the compositor's cell-centered fit with - /// ghostty's constrain() math — size + alignment + padding. Applies - /// only to PUA glyphs that have a hand-tuned entry in the table. - pub nerd_font_constraint: Option, -} - -/// Positioned glyph in a text run. -#[derive(Copy, Clone)] -pub struct Glyph { - /// Glyph identifier. - pub id: GlyphId, - /// X offset of the glyph. - pub x: f32, - /// Y offset of the glyph. - pub y: f32, - /// Horizontal advance. Used by the compositor to fit bitmap glyphs - /// (emoji) into their per-glyph cell slot. - pub advance: f32, -} diff --git a/sugarloaf/src/renderer/text_run_manager.rs b/sugarloaf/src/renderer/text_run_manager.rs deleted file mode 100644 index f81f2729..00000000 --- a/sugarloaf/src/renderer/text_run_manager.rs +++ /dev/null @@ -1,261 +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. -// -// Unified text run manager - replaces separate line cache and shaping cache - -use crate::font::text_run_cache::{create_cached_text_run, ShapedGlyph, TextRunCache}; -use std::sync::Arc; -use tracing::debug; - -/// Unified text run manager that handles shaping, glyph, and vertex caching -pub struct TextRunManager { - /// Unified cache for text runs (shaping + glyphs + vertices) - unified_cache: TextRunCache, -} - -impl TextRunManager { - pub fn new() -> Self { - Self { - unified_cache: TextRunCache::new(), - } - } - - /// Get cached shaping data for a text run using a pre-computed key - pub fn get_cached_data_by_key(&mut self, key: u64) -> CacheResult { - match self.unified_cache.get(&key) { - Some(cached_run) => CacheResult::Hit { - glyphs: cached_run.glyphs.clone(), - advance_width: cached_run.advance_width, - has_emoji: cached_run.has_emoji, - font_id: cached_run.font_id, - }, - None => CacheResult::Miss, - } - } - - /// Cache shaping data using a pre-computed key - pub fn cache_shaping_data_by_key( - &mut self, - key: u64, - font_id: usize, - font_size: f32, - glyphs: Vec, - has_emoji: bool, - ) { - let cached_run = create_cached_text_run(glyphs, font_id, font_size, has_emoji); - self.unified_cache.insert(key, cached_run); - } - - /// Apply cached vertices to output, adjusting for new position - #[cfg(test)] - pub fn apply_cached_vertices( - vertices_data: &[u8], - base_position: (f32, f32), - new_position: (f32, f32), - output_vertices: &mut Vec, - ) { - let dx = new_position.0 - base_position.0; - let dy = new_position.1 - base_position.1; - - // If there's no position change, just copy the data - if dx == 0.0 && dy == 0.0 { - output_vertices.extend_from_slice(vertices_data); - return; - } - - use super::batch::Vertex; - const VERTEX_SIZE: usize = Vertex::SIZE; - - if !vertices_data.len().is_multiple_of(VERTEX_SIZE) { - debug!("Invalid vertex data size: {}", vertices_data.len()); - output_vertices.extend_from_slice(vertices_data); - return; - } - - // Reserve space for the adjusted vertices - output_vertices.reserve(vertices_data.len()); - - // Process vertices in chunks of 44 bytes - for chunk in vertices_data.chunks_exact(VERTEX_SIZE) { - // Deserialize position (first 12 bytes - 3 f32s) - let x_bytes = [chunk[0], chunk[1], chunk[2], chunk[3]]; - let y_bytes = [chunk[4], chunk[5], chunk[6], chunk[7]]; - let z_bytes = [chunk[8], chunk[9], chunk[10], chunk[11]]; - - let mut x = f32::from_le_bytes(x_bytes); - let mut y = f32::from_le_bytes(y_bytes); - let z = f32::from_le_bytes(z_bytes); - - // Apply position offset (only to x and y, leave z unchanged) - x += dx; - y += dy; - - // Write adjusted position - output_vertices.extend_from_slice(&x.to_le_bytes()); - output_vertices.extend_from_slice(&y.to_le_bytes()); - output_vertices.extend_from_slice(&z.to_le_bytes()); - - // Copy the rest of the vertex data unchanged (color + uv + layers) - // color: bytes 12-27 (16 bytes) - // uv: bytes 28-35 (8 bytes) - // layers: bytes 36-43 (8 bytes) - output_vertices.extend_from_slice(&chunk[12..]); - } - - debug!("Applied cached vertices with offset ({}, {})", dx, dy); - } - - /// Clear all caches (called when fonts change) - pub fn clear_all(&mut self) { - self.unified_cache.clear(); - debug!("TextRunManager: Cleared unified cache due to font change"); - } -} - -/// Result of a cache lookup - indicates what level of cached data is available -#[derive(Debug)] -#[allow(unused)] -/// Simplified cache result - just shaping data or miss -pub enum CacheResult { - /// Shaping data available (glyph IDs + positions) - Hit { - glyphs: Arc>, - advance_width: f32, - has_emoji: bool, - font_id: usize, - }, - /// No cached data available - Miss, -} - -#[cfg(test)] -mod tests { - use super::super::batch::Vertex; - use super::*; - - #[test] - fn test_vertex_positioning() { - // Create mock vertex data for one complete vertex (88 bytes) - // Vertex structure: pos[3] + color[4] + uv[2] + layers[2] + corner_radii[4] + rect_size[2] + underline_style + clip_rect[4] - let mut vertices = Vec::new(); - - // Position: (10.0, 20.0, 0.0) - 12 bytes - vertices.extend_from_slice(&10.0f32.to_le_bytes()); // x - vertices.extend_from_slice(&20.0f32.to_le_bytes()); // y - vertices.extend_from_slice(&0.0f32.to_le_bytes()); // z - - // Color: (1.0, 0.5, 0.0, 1.0) - 16 bytes - vertices.extend_from_slice(&1.0f32.to_le_bytes()); - vertices.extend_from_slice(&0.5f32.to_le_bytes()); - vertices.extend_from_slice(&0.0f32.to_le_bytes()); - vertices.extend_from_slice(&1.0f32.to_le_bytes()); - - // UV: (0.5, 0.7) - 8 bytes - vertices.extend_from_slice(&0.5f32.to_le_bytes()); - vertices.extend_from_slice(&0.7f32.to_le_bytes()); - - // Layers: (1, 2) - 8 bytes - vertices.extend_from_slice(&1i32.to_le_bytes()); - vertices.extend_from_slice(&2i32.to_le_bytes()); - - // Corner radii: (0.0, 0.0, 0.0, 0.0) - 16 bytes - vertices.extend_from_slice(&0.0f32.to_le_bytes()); - vertices.extend_from_slice(&0.0f32.to_le_bytes()); - vertices.extend_from_slice(&0.0f32.to_le_bytes()); - vertices.extend_from_slice(&0.0f32.to_le_bytes()); - - // Rect size: (100.0, 50.0) - 8 bytes - vertices.extend_from_slice(&100.0f32.to_le_bytes()); - vertices.extend_from_slice(&50.0f32.to_le_bytes()); - - // Underline style: 0 - 4 bytes - vertices.extend_from_slice(&0i32.to_le_bytes()); - - // Clip rect: (0.0, 0.0, 0.0, 0.0) - 16 bytes - vertices.extend_from_slice(&0.0f32.to_le_bytes()); - vertices.extend_from_slice(&0.0f32.to_le_bytes()); - vertices.extend_from_slice(&0.0f32.to_le_bytes()); - vertices.extend_from_slice(&0.0f32.to_le_bytes()); - - assert_eq!(vertices.len(), Vertex::SIZE); - assert_eq!(vertices.len(), 88); - - let mut output_vertices = Vec::new(); - - TextRunManager::apply_cached_vertices( - &vertices, - (100.0, 200.0), // base position - (150.0, 250.0), // new position (offset by +50, +50) - &mut output_vertices, - ); - - // Expected: only position should be offset by (+50, +50) - // So (10, 20, 0) becomes (60, 70, 0) - assert_eq!(output_vertices.len(), Vertex::SIZE); - - // Check adjusted position - let x = f32::from_le_bytes([ - output_vertices[0], - output_vertices[1], - output_vertices[2], - output_vertices[3], - ]); - let y = f32::from_le_bytes([ - output_vertices[4], - output_vertices[5], - output_vertices[6], - output_vertices[7], - ]); - let z = f32::from_le_bytes([ - output_vertices[8], - output_vertices[9], - output_vertices[10], - output_vertices[11], - ]); - - assert_eq!(x, 60.0); - assert_eq!(y, 70.0); - assert_eq!(z, 0.0); - - // Check that color, uv, and layers are unchanged - let color_r = f32::from_le_bytes([ - output_vertices[12], - output_vertices[13], - output_vertices[14], - output_vertices[15], - ]); - let uv_u = f32::from_le_bytes([ - output_vertices[28], - output_vertices[29], - output_vertices[30], - output_vertices[31], - ]); - let layer_0 = i32::from_le_bytes([ - output_vertices[36], - output_vertices[37], - output_vertices[38], - output_vertices[39], - ]); - - assert_eq!(color_r, 1.0); - assert_eq!(uv_u, 0.5); - assert_eq!(layer_0, 1); - } - - #[test] - fn test_vertex_positioning_no_offset() { - let vertices = vec![0u8; Vertex::SIZE]; // Mock vertex data - let mut output_vertices = Vec::new(); - - TextRunManager::apply_cached_vertices( - &vertices, - (100.0, 200.0), - (100.0, 200.0), // Same position - no offset - &mut output_vertices, - ); - - assert_eq!(output_vertices, vertices); - } -} diff --git a/sugarloaf/src/sugarloaf.rs b/sugarloaf/src/sugarloaf.rs index 01437fa5..ab96cbdc 100644 --- a/sugarloaf/src/sugarloaf.rs +++ b/sugarloaf/src/sugarloaf.rs @@ -46,6 +46,12 @@ pub struct Sugarloaf<'a> { /// `memory/project_sugarloaf_content_drop.md`. Phase 1a: scaffold /// only — holds no atlases or GPU state yet. text: crate::text::Text, + /// Per-panel (rich_text_id) image overlays. Driven by the kitty + /// graphics frontend path; read by the renderer's image pass. + /// Replaces the old `BuilderState.image_overlays` storage — keeps + /// panel overlays alive while the Content system is stripped. + pub image_overlays: + rustc_hash::FxHashMap>, } #[derive(Debug)] @@ -189,6 +195,7 @@ impl Sugarloaf<'_> { cpu_cache: crate::renderer::cpu::CpuCache::new(), font_cache, text, + image_overlays: rustc_hash::FxHashMap::default(), }; Ok(instance) @@ -542,83 +549,6 @@ impl Sugarloaf<'_> { } } - /// Get mutable reference to text content by id (for cached text) - #[inline] - pub fn get_text_mut( - &mut self, - id: usize, - ) -> Option<&mut crate::layout::BuilderState> { - self.state.content.get_text_by_id_mut(id) - } - - /// Get mutable reference to transient text by index - #[inline] - pub fn get_transient_text_mut( - &mut self, - index: usize, - ) -> Option<&mut crate::layout::BuilderState> { - self.state.content.get_transient_text_mut(index) - } - - /// Set font size for transient text - #[inline] - pub fn set_transient_text_font_size(&mut self, index: usize, font_size: f32) { - if let Some(content_state) = self.state.content.get_transient_state_mut(index) { - 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.state.style.scale_factor; - } - content_state.render_data.needs_repaint = true; - } - } - - /// Set position for transient text - #[inline] - pub fn set_transient_position(&mut self, index: usize, x: f32, y: f32) { - if let Some(content_state) = self.state.content.get_transient_state_mut(index) { - content_state.render_data.set_position( - x * self.state.style.scale_factor, - y * self.state.style.scale_factor, - ); - } - } - - /// Set visibility for transient text - #[inline] - pub fn set_transient_visibility(&mut self, index: usize, visible: bool) { - if let Some(content_state) = self.state.content.get_transient_state_mut(index) { - content_state.render_data.set_hidden(!visible); - } - } - - /// Set whether to use grid cell size for glyph positioning (cached text) - /// - true: monospace grid alignment (default, for terminal) - /// - false: proportional text using actual glyph advances (for rich text) - #[inline] - pub fn set_use_grid_cell_size(&mut self, id: usize, use_grid: bool) { - if let Some(content_state) = self.state.content.states.get_mut(&id) { - content_state.render_data.use_grid_cell_size = use_grid; - } - } - - /// Set the render order for a transient text element. - #[inline] - pub fn set_transient_order(&mut self, index: usize, order: u8) { - if let Some(content_state) = self.state.content.get_transient_state_mut(index) { - content_state.render_data.order = order; - } - } - - /// Set whether to use grid cell size for glyph positioning (transient text) - /// - true: monospace grid alignment (default, for terminal) - /// - false: proportional text using actual glyph advances (for rich text) - #[inline] - pub fn set_transient_use_grid_cell_size(&mut self, index: usize, use_grid: bool) { - if let Some(content_state) = self.state.content.get_transient_state_mut(index) { - content_state.render_data.use_grid_cell_size = use_grid; - } - } - /// Get the next available ID for cached content. /// Returns the highest key + 1 (wrapping on overflow). /// Useful for dynamically allocating IDs without hardcoded constants. @@ -954,6 +884,30 @@ impl Sugarloaf<'_> { &mut self.text } + /// Register an image overlay anchored to `panel_id` (a + /// `rich_text_id`). Driven by the kitty graphics frontend; read + /// by the renderer's image pass. + #[inline] + pub fn push_image_overlay( + &mut self, + panel_id: usize, + overlay: crate::sugarloaf::graphics::GraphicOverlay, + ) { + self.image_overlays + .entry(panel_id) + .or_default() + .push(overlay); + } + + /// Drop all overlays for `panel_id`. Called by the frontend when + /// placements are removed or the kitty graphics cache clears. + #[inline] + pub fn clear_image_overlays_for(&mut self, panel_id: usize) { + if let Some(v) = self.image_overlays.get_mut(&panel_id) { + v.clear(); + } + } + /// 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. @@ -1046,6 +1000,7 @@ impl Sugarloaf<'_> { &mut self.ctx, &mut self.graphics, &mut self.image_data, + &self.image_overlays, ); match self.ctx.inner { diff --git a/sugarloaf/src/sugarloaf/state.rs b/sugarloaf/src/sugarloaf/state.rs index 97b84926..fe4ad634 100644 --- a/sugarloaf/src/sugarloaf/state.rs +++ b/sugarloaf/src/sugarloaf/state.rs @@ -166,10 +166,14 @@ impl SugarState { context: &mut super::Context, graphics: &mut Graphics, image_data: &mut rustc_hash::FxHashMap, + image_overlays: &rustc_hash::FxHashMap< + usize, + Vec, + >, ) { // Shape transient texts before rendering self.content.build_transient_texts(); - advance_brush.prepare(context, self, graphics, image_data); + advance_brush.prepare(context, self, graphics, image_data, image_overlays); } #[inline]