diff --git a/frontends/rioterm/src/grid_emit.rs b/frontends/rioterm/src/grid_emit.rs index 043d6a03..c537512b 100644 --- a/frontends/rioterm/src/grid_emit.rs +++ b/frontends/rioterm/src/grid_emit.rs @@ -24,13 +24,74 @@ use rio_backend::config::colors::term::TermColors; use rio_backend::crosswords::grid::row::Row; -use rio_backend::crosswords::pos::Column; +use rio_backend::crosswords::pos::{Column, Line}; use rio_backend::crosswords::square::{ContentTag, Square}; use rio_backend::crosswords::style::{StyleFlags, StyleSet}; +use rio_backend::selection::SelectionRange; use rustc_hash::FxHashMap; use crate::renderer::Renderer; +/// Per-row selection interval, in column indices. `None` = row is +/// outside the selection. Block selections reduce to the same +/// `[lo, hi]` on every row; linear selections expand middle rows to +/// the full width. +#[derive(Clone, Copy)] +pub struct RowSelection { + pub lo: u16, + pub hi: u16, +} + +/// Compute the selection interval (if any) for visible row `y`. +/// `display_offset` translates visible-row index → absolute `Line`. +pub fn row_selection_for( + sel: Option, + y: usize, + cols: usize, + display_offset: i32, +) -> Option { + let sel = sel?; + if cols == 0 { + return None; + } + let line = Line((y as i32) - display_offset); + if line < sel.start.row || line > sel.end.row { + return None; + } + let cols_max = cols.saturating_sub(1); + // Block selections: every row inside the band uses the same span. + if sel.is_block { + let lo = sel.start.col.0.min(cols_max); + let hi = sel.end.col.0.min(cols_max); + return Some(RowSelection { + lo: lo as u16, + hi: hi as u16, + }); + } + let lo = if line == sel.start.row { + sel.start.col.0 + } else { + 0 + }; + let hi = if line == sel.end.row { + sel.end.col.0 + } else { + cols_max + }; + Some(RowSelection { + lo: lo.min(cols_max) as u16, + hi: hi.min(cols_max) as u16, + }) +} + +#[inline] +fn cell_in_row_sel(row_sel: Option, col: u16) -> bool { + match row_sel { + Some(s) => col >= s.lo && col <= s.hi, + None => false, + } +} + use rio_backend::sugarloaf::font::FontLibrary; use rio_backend::sugarloaf::grid::{ AtlasSlot, CellBg, CellText, GlyphKey, GridRenderer, RasterizedGlyph, @@ -52,6 +113,27 @@ pub fn cell_fg( normalized_to_u8(color) } +/// Foreground for a selected cell. Mirrors Ghostty's selection-fg +/// rule (`generic.zig:2867`): use the configured `selection-foreground` +/// unless the user asked to keep the cell's own fg (Rio's +/// `ignore-selection-foreground-color`). Ghostty falls back to +/// `state.colors.background` when no color is configured; Rio always +/// has a default selection_foreground populated in its theme, so we +/// use it directly. +#[inline] +pub fn cell_fg_selected( + sq: Square, + style_set: &StyleSet, + renderer: &Renderer, + term_colors: &TermColors, +) -> [u8; 4] { + if renderer.ignore_selection_fg_color { + cell_fg(sq, style_set, renderer, term_colors) + } else { + normalized_to_u8(renderer.named_colors.selection_foreground) + } +} + // Decoration sprites (underlines, strikethrough) // // Ghostty pre-rasterizes underline/strikethrough sprites into the @@ -228,8 +310,7 @@ fn rasterize_decoration( // Top of strike sits at cell_h/2 + thickness/2 above cell // bottom (i.e., strike is centered at cell_h/2). let center_from_bottom = cell_h / 2; - let bearing_y = - center_from_bottom as i16 + (thickness as i16 + 1) / 2; + let bearing_y = center_from_bottom as i16 + (thickness as i16 + 1) / 2; (bytes, cell_w, thickness, bearing_y) } } @@ -343,12 +424,25 @@ pub fn build_row_bg( style_set: &StyleSet, renderer: &Renderer, term_colors: &TermColors, + row_sel: Option, bg_scratch: &mut Vec, ) { bg_scratch.clear(); + // Precompute once — selection_background stays constant across the row. + let sel_bg = if row_sel.is_some() { + Some(normalized_to_u8(renderer.named_colors.selection_background)) + } else { + None + }; for x in 0..cols { let sq = row[Column(x)]; - let rgba = cell_bg(sq, style_set, renderer, term_colors); + let rgba = if cell_in_row_sel(row_sel, x as u16) { + // Selection bg wins over the cell's own bg, matching Ghostty + // `generic.zig:2817` (`.selection` branch). + sel_bg.unwrap_or_else(|| cell_bg(sq, style_set, renderer, term_colors)) + } else { + cell_bg(sq, style_set, renderer, term_colors) + }; bg_scratch.push(CellBg { rgba }); } } @@ -458,7 +552,8 @@ impl GridGlyphRasterizer { #[cfg(not(target_os = "macos"))] let (id, emoji) = { let lib = font_library.inner.read(); - lib.find_best_font_match(ch, &span_style).unwrap_or((0, false)) + lib.find_best_font_match(ch, &span_style) + .unwrap_or((0, false)) }; (id as u32, emoji) }) @@ -672,6 +767,7 @@ pub fn build_row_fg( size_px: f32, cell_w: f32, cell_h: f32, + row_sel: Option, font_library: &FontLibrary, fg_scratch: &mut Vec, ) { @@ -697,6 +793,7 @@ pub fn build_row_fg( cell_w_u32, cell_h_u32, thickness, + row_sel, fg_scratch, ); @@ -766,13 +863,8 @@ pub fn build_row_fg( let shaped_opt = shape_run_ct(rasterizer, font_id, size_u16, size_bucket, font_library); #[cfg(not(target_os = "macos"))] - let shaped_opt = shape_run_swash( - rasterizer, - font_id, - size_u16, - size_bucket, - font_library, - ); + let shaped_opt = + shape_run_swash(rasterizer, font_id, size_u16, size_bucket, font_library); let Some((glyphs, ascent_px)) = shaped_opt else { x = end; continue; @@ -803,8 +895,7 @@ pub fn build_row_fg( &rasterizer.run_str_scratch, ) .expect("just inserted"); - let mut char_cursor = - rasterizer.run_str_scratch.char_indices().peekable(); + let mut char_cursor = rasterizer.run_str_scratch.char_indices().peekable(); let mut cell_idx_in_run: u16 = 0; let mut out = Vec::with_capacity(glyphs.len()); for g in glyphs { @@ -851,8 +942,16 @@ pub fn build_row_fg( let src_col = (run_start + cell_idx_in_run as usize).min(cols.saturating_sub(1)); let src_sq = row[Column(src_col)]; + let is_sel = cell_in_row_sel(row_sel, src_col as u16); let (atlas, color) = if is_color { + // Colour glyphs (emoji) don't take the selection-fg swap — + // matches Ghostty's behaviour for bitmap/COLR atlas entries. (CellText::ATLAS_COLOR, [255, 255, 255, 255]) + } else if is_sel { + ( + CellText::ATLAS_GRAYSCALE, + cell_fg_selected(src_sq, style_set, renderer, term_colors), + ) } else { ( CellText::ATLAS_GRAYSCALE, @@ -888,6 +987,7 @@ pub fn build_row_fg( cell_w_u32, cell_h_u32, thickness, + row_sel, fg_scratch, ); } @@ -904,6 +1004,7 @@ fn emit_underlines( cell_w: u32, cell_h: u32, thickness: u32, + row_sel: Option, fg_scratch: &mut Vec, ) { for x in 0..cols { @@ -919,7 +1020,15 @@ fn emit_underlines( if slot.w == 0 || slot.h == 0 { continue; } - let color = decoration_color(sq, &style, style_set, renderer, term_colors); + let color = if cell_in_row_sel(row_sel, x as u16) { + // Inside selection: underline follows the selection fg so + // it stays visible against the selection bg. SGR 58 is + // suppressed here — a theme's selection_foreground + // overrides per-cell decoration color. + cell_fg_selected(sq, style_set, renderer, term_colors) + } else { + decoration_color(sq, &style, style_set, renderer, term_colors) + }; fg_scratch.push(CellText { glyph_pos: [slot.x as u32, slot.y as u32], glyph_size: [slot.w as u32, slot.h as u32], @@ -945,6 +1054,7 @@ fn emit_strikethroughs( cell_w: u32, cell_h: u32, thickness: u32, + row_sel: Option, fg_scratch: &mut Vec, ) { for x in 0..cols { @@ -967,7 +1077,11 @@ fn emit_strikethroughs( } // Strikethrough always uses the cell fg (there's no SGR for // a separate strike color, matching Ghostty). - let color = cell_fg(sq, style_set, renderer, term_colors); + let color = if cell_in_row_sel(row_sel, x as u16) { + cell_fg_selected(sq, style_set, renderer, term_colors) + } else { + cell_fg(sq, style_set, renderer, term_colors) + }; fg_scratch.push(CellText { glyph_pos: [slot.x as u32, slot.y as u32], glyph_size: [slot.w as u32, slot.h as u32], diff --git a/frontends/rioterm/src/renderer/assistant.rs b/frontends/rioterm/src/renderer/assistant.rs index 103e76d6..8ad725b5 100644 --- a/frontends/rioterm/src/renderer/assistant.rs +++ b/frontends/rioterm/src/renderer/assistant.rs @@ -311,8 +311,9 @@ impl AssistantOverlay { color: color_u8(LINK_COLOR), ..DrawOpts::default() }; - let rendered_width = - sugarloaf.text_mut().draw(link_x, link_y, DOCS_URL, &link_opts); + let rendered_width = sugarloaf + .text_mut() + .draw(link_x, link_y, DOCS_URL, &link_opts); self.link_button_width = rendered_width; let (dbx, dby, dbw, dbh) = self.docs_button_rect(ox, oy); diff --git a/frontends/rioterm/src/renderer/mod.rs b/frontends/rioterm/src/renderer/mod.rs index 28b2be23..bd063a50 100644 --- a/frontends/rioterm/src/renderer/mod.rs +++ b/frontends/rioterm/src/renderer/mod.rs @@ -45,8 +45,7 @@ pub struct Renderer { last_active: Option, pub config_has_blinking_enabled: bool, pub config_blinking_interval: u64, - #[allow(dead_code)] // selection fg inverting is handled in cell_fg now - ignore_selection_fg_color: bool, + pub(crate) ignore_selection_fg_color: bool, pub search: search::SearchOverlay, pub assistant: assistant::AssistantOverlay, pub scrollbar: scrollbar::Scrollbar, @@ -62,7 +61,6 @@ pub struct Renderer { pub trail_cursor: trail_cursor::TrailCursor, } - impl Renderer { pub fn new(config: &Config) -> Renderer { let colors = List::from(&config.colors); @@ -209,7 +207,6 @@ impl Renderer { } } - #[inline] pub fn set_vi_mode(&mut self, is_vi_mode_enabled: bool) { self.is_vi_mode_enabled = is_vi_mode_enabled; @@ -889,4 +886,3 @@ impl Renderer { } } } - diff --git a/frontends/rioterm/src/screen/mod.rs b/frontends/rioterm/src/screen/mod.rs index 4e45872b..225b0ff1 100644 --- a/frontends/rioterm/src/screen/mod.rs +++ b/frontends/rioterm/src/screen/mod.rs @@ -3482,6 +3482,14 @@ impl Screen<'_> { cursor_visible: bool, is_active: bool, damage: rio_backend::event::TerminalDamage, + /// Selection is per-context (`renderable_content`), not + /// per-terminal. Grabbed alongside the grid snapshot so + /// `build_row_bg`/`build_row_fg` can tint selected cells. + selection: Option, + /// `i - display_offset = absolute Line` for the + /// per-row selection interval check. Snapshotted at + /// the same lock as `visible_rows` to stay consistent. + display_offset: i32, } let (active_key, scaled_margin) = { @@ -3519,14 +3527,16 @@ impl Screen<'_> { let s = self.sugarloaf.style(); s.font_size * s.scale_factor }); - let (visible_rows, style_set, term_colors) = { + let (visible_rows, style_set, term_colors, display_offset) = { let terminal = ctx.terminal.lock(); ( terminal.visible_rows(), terminal.grid.style_set.clone(), terminal.colors, + terminal.display_offset() as i32, ) }; + let selection = ctx.renderable_content.selection_range; let cursor = &ctx.renderable_content.cursor; // Take + reset so next frame sees fresh damage only // from this frame's `Renderer::run`. @@ -3550,6 +3560,8 @@ impl Screen<'_> { cursor_visible: cursor.state.is_visible(), is_active: *key == active_key, damage, + selection, + display_offset, }); } @@ -3635,12 +3647,19 @@ impl Screen<'_> { let Some(row) = p.visible_rows.get(y) else { return; }; + let row_sel = crate::grid_emit::row_selection_for( + p.selection, + y, + cols, + p.display_offset, + ); crate::grid_emit::build_row_bg( row, cols, &p.style_set, renderer_ref, &p.term_colors, + row_sel, &mut bg_scratch, ); crate::grid_emit::build_row_fg( @@ -3655,6 +3674,7 @@ impl Screen<'_> { p.font_px, p.cell_w, p.cell_h, + row_sel, &font_library, &mut fg_scratch, ); diff --git a/sugarloaf/src/font/constants.rs b/sugarloaf/src/font/constants.rs index 5f61e7c5..39a61bad 100644 --- a/sugarloaf/src/font/constants.rs +++ b/sugarloaf/src/font/constants.rs @@ -58,7 +58,7 @@ pub const FONT_CASCADIAMONO_SEMI_LIGHT_ITALIC: &[u8] = font!("./resources/CascadiaCode/CascadiaCode-SemiLightItalic.otf"); // pub const FONT_SYMBOLS_NERD_FONT_MONO: &[u8] = - // font!("./resources/SymbolsNerdFontMono/SymbolsNerdFontMono-Regular.ttf"); +// font!("./resources/SymbolsNerdFontMono/SymbolsNerdFontMono-Regular.ttf"); // macOS gets system Apple Color Emoji through the font fallback chain (see // `fallbacks::external_fallbacks`) and doesn't need the bundled Twemoji — diff --git a/sugarloaf/src/font/metrics.rs b/sugarloaf/src/font/metrics.rs index ef0f2257..9a8ac3b1 100644 --- a/sugarloaf/src/font/metrics.rs +++ b/sugarloaf/src/font/metrics.rs @@ -113,9 +113,7 @@ impl FaceMetrics { /// - It's present in virtually all CJK fonts (basic kanji/hanzi) /// - Its width is representative of typical CJK character width /// - It avoids edge cases like punctuation or rare characters - fn measure_cjk_character_width( - font_ref: &swash::FontRef, - ) -> Option { + fn measure_cjk_character_width(font_ref: &swash::FontRef) -> Option { const CJK_WATER_IDEOGRAPH: u32 = 0x6C34; // "水" // Get character map diff --git a/sugarloaf/src/font/mod.rs b/sugarloaf/src/font/mod.rs index 743bb089..b9f1341d 100644 --- a/sugarloaf/src/font/mod.rs +++ b/sugarloaf/src/font/mod.rs @@ -17,12 +17,6 @@ pub const FONT_ID_REGULAR: usize = 0; use crate::font::constants::*; use crate::font::fonts::{parse_unicode, SugarloafFontStyle, SugarloafFontWidth}; use crate::font::metrics::{FaceMetrics, Metrics}; -use swash::text::cluster::Parser; -use swash::text::cluster::Token; -use swash::text::cluster::{CharCluster, Status}; -use swash::text::Codepoint; -use swash::text::Script; -use swash::{tag_from_bytes, CacheKey, FontRef, Synthesis}; use crate::layout::SpanStyle; use crate::SugarloafErrors; use dashmap::DashMap; @@ -31,6 +25,12 @@ use rustc_hash::FxHashMap; use std::ops::Range; use std::path::PathBuf; use std::sync::{Arc, OnceLock}; +use swash::text::cluster::Parser; +use swash::text::cluster::Token; +use swash::text::cluster::{CharCluster, Status}; +use swash::text::Codepoint; +use swash::text::Script; +use swash::{tag_from_bytes, CacheKey, FontRef, Synthesis}; pub use swash::{Style, Weight}; diff --git a/sugarloaf/src/font_cache.rs b/sugarloaf/src/font_cache.rs index 68b45c57..d2644a96 100644 --- a/sugarloaf/src/font_cache.rs +++ b/sugarloaf/src/font_cache.rs @@ -3,10 +3,10 @@ // This source code is licensed under the MIT license found in the // LICENSE file in the root directory of this source tree. -use swash::Attributes; use crate::sugarloaf::primitives::is_private_user_area; use crate::SpanStyle; use rustc_hash::FxHashMap; +use swash::Attributes; use unicode_width::UnicodeWidthChar; /// Unscaled horizontal advance for a glyph + the font's units-per-em, diff --git a/sugarloaf/src/layout/content.rs b/sugarloaf/src/layout/content.rs index f5a8735d..80a19212 100644 --- a/sugarloaf/src/layout/content.rs +++ b/sugarloaf/src/layout/content.rs @@ -6,10 +6,6 @@ #![allow(clippy::uninlined_format_args)] use crate::font::FontLibrary; -use swash::shape::ShapeContext; -use swash::text::Script; -#[cfg(not(target_os = "macos"))] -use swash::FontRef; use crate::layout::content_data::{ContentData, ContentState}; use crate::layout::render_data::RenderData; use crate::layout::TextLayout; @@ -19,11 +15,15 @@ use smallvec::SmallVec; use std::collections::HashSet; use std::hash::{Hash, Hasher}; use std::num::NonZeroUsize; +use swash::shape::ShapeContext; +use swash::text::Script; +#[cfg(not(target_os = "macos"))] +use swash::FontRef; use tracing::debug; +use crate::{sugarloaf::primitives::SugarCursor, DrawableChar, Graphic}; use swash::Attributes; use swash::Setting; -use crate::{sugarloaf::primitives::SugarCursor, DrawableChar, Graphic}; /// Pre-packed shaping result ready to push directly as a RunData. /// Avoids re-packing OwnedGlyphClusters on every cache hit. @@ -509,10 +509,7 @@ impl Content { } #[inline] - pub fn set_font_features( - &mut self, - font_features: Vec>, - ) { + pub fn set_font_features(&mut self, font_features: Vec>) { self.font_features = font_features; } @@ -584,10 +581,9 @@ impl Content { if let Some((font_data, offset, _key)) = font_library_data.get_data(&font_id) { // Create swash FontRef directly from font data - if let Some(font_ref) = swash::FontRef::from_index( - &font_data, - offset as usize, - ) { + if let Some(font_ref) = + swash::FontRef::from_index(&font_data, offset as usize) + { // Get metrics using swash let font_metrics = font_ref.metrics(&[]); diff --git a/sugarloaf/src/layout/glyph.rs b/sugarloaf/src/layout/glyph.rs index 50427b30..ffe76206 100644 --- a/sugarloaf/src/layout/glyph.rs +++ b/sugarloaf/src/layout/glyph.rs @@ -6,9 +6,9 @@ // layout_data.rs was originally retired from dfrg/swash_demo licensed under MIT // https://github.com/dfrg/swash_demo/blob/master/LICENSE +use crate::layout::SpanStyle; use swash::shape::cluster::Glyph as ShapedGlyph; use swash::GlyphId; -use crate::layout::SpanStyle; pub const GLYPH_DETAILED: u32 = 0x80000000; diff --git a/sugarloaf/src/layout/render_data.rs b/sugarloaf/src/layout/render_data.rs index 848e5531..b8cebc09 100644 --- a/sugarloaf/src/layout/render_data.rs +++ b/sugarloaf/src/layout/render_data.rs @@ -11,14 +11,14 @@ //! RenderData. use super::glyph::*; -#[cfg(not(target_os = "macos"))] -use swash::shape::Shaper; -use swash::Metrics; use crate::layout::content::{CachedRun, ShapingCache, SpanStyleDecoration}; use crate::layout::SpanStyle; use crate::sugarloaf::primitives::SugarCursor; use crate::{Graphic, GraphicId}; use std::hash::Hasher; +#[cfg(not(target_os = "macos"))] +use swash::shape::Shaper; +use swash::Metrics; use wyhash::WyHash; /// Compute a cache key from glyph IDs, font_id and size. diff --git a/sugarloaf/src/sugarloaf.rs b/sugarloaf/src/sugarloaf.rs index f0026c69..7a2e109f 100644 --- a/sugarloaf/src/sugarloaf.rs +++ b/sugarloaf/src/sugarloaf.rs @@ -6,10 +6,10 @@ use crate::components::core::image::Handle; use crate::components::filters::{Filter, FiltersBrush}; use crate::font::{fonts::SugarloafFont, FontLibrary}; use crate::font_cache::{compute_advance, resolve_with, FontCache, ResolvedGlyph}; -use swash::Attributes; use crate::layout::{RootStyle, TextLayout}; use crate::renderer::Renderer; use crate::sugarloaf::graphics::{GraphicDataEntry, Graphics}; +use swash::Attributes; use crate::context::Context; use crate::Content; diff --git a/sugarloaf/src/sugarloaf/state.rs b/sugarloaf/src/sugarloaf/state.rs index fe4ad634..9ea1e726 100644 --- a/sugarloaf/src/sugarloaf/state.rs +++ b/sugarloaf/src/sugarloaf/state.rs @@ -38,8 +38,7 @@ impl SugarState { let mut found_font_features = vec![]; if let Some(features) = font_features { for feature in features { - let setting: swash::Setting = - (feature.as_str(), 1).into(); + let setting: swash::Setting = (feature.as_str(), 1).into(); found_font_features.push(setting); } } diff --git a/sugarloaf/src/text.rs b/sugarloaf/src/text.rs index 3ad3557a..f6082615 100644 --- a/sugarloaf/src/text.rs +++ b/sugarloaf/src/text.rs @@ -185,8 +185,7 @@ pub struct Text { /// Cached `(shared_data, offset, cache_key)` per font_id so the /// `FontLibraryData` read-lock isn't re-acquired per shape. #[cfg(not(target_os = "macos"))] - font_data_cache: - FxHashMap, + font_data_cache: FxHashMap, #[cfg(not(target_os = "macos"))] wgpu: Option, } @@ -277,32 +276,36 @@ impl Text { (if opts.bold { 1u8 } else { 0 }) | (if opts.italic { 2u8 } else { 0 }); let first_ch = text.chars().next()?; - let (font_id, _is_emoji) = - match self.font_resolve.entry((first_ch, style_flags)) { - std::collections::hash_map::Entry::Occupied(e) => *e.get(), - std::collections::hash_map::Entry::Vacant(e) => { - let mut ss = SpanStyle::default(); - let weight = if opts.bold { Weight::BOLD } else { Weight::NORMAL }; - let fstyle = if opts.italic { - FontStyle::Italic - } else { - FontStyle::Normal - }; - ss.font_attrs = Attributes::new(Stretch::NORMAL, weight, fstyle); - #[cfg(target_os = "macos")] - let resolved = - self.font_library.resolve_font_for_char(first_ch, &ss); - - #[cfg(not(target_os = "macos"))] - let resolved = { - let lib = self.font_library.inner.read(); - lib.find_best_font_match(first_ch, &ss).unwrap_or((0, false)) - }; - let v = (resolved.0 as u32, resolved.1); - e.insert(v); - v - } - }; + let (font_id, _is_emoji) = match self.font_resolve.entry((first_ch, style_flags)) + { + std::collections::hash_map::Entry::Occupied(e) => *e.get(), + std::collections::hash_map::Entry::Vacant(e) => { + let mut ss = SpanStyle::default(); + let weight = if opts.bold { + Weight::BOLD + } else { + Weight::NORMAL + }; + let fstyle = if opts.italic { + FontStyle::Italic + } else { + FontStyle::Normal + }; + ss.font_attrs = Attributes::new(Stretch::NORMAL, weight, fstyle); + #[cfg(target_os = "macos")] + let resolved = self.font_library.resolve_font_for_char(first_ch, &ss); + + #[cfg(not(target_os = "macos"))] + let resolved = { + let lib = self.font_library.inner.read(); + lib.find_best_font_match(first_ch, &ss) + .unwrap_or((0, false)) + }; + let v = (resolved.0 as u32, resolved.1); + e.insert(v); + v + } + }; let font_id = opts.font_id.map(|id| id as u32).unwrap_or(font_id); let hash = shape_hash(font_id, size_bucket, style_flags, text); @@ -310,22 +313,20 @@ impl Text { return Some(entry.clone()); } - let (synthetic_bold, synthetic_italic) = - match self.synthesis_cache.entry(font_id) { - std::collections::hash_map::Entry::Occupied(e) => *e.get(), - std::collections::hash_map::Entry::Vacant(e) => { - let lib = self.font_library.inner.read(); - let fd = lib.get(&(font_id as usize)); - *e.insert((fd.should_embolden, fd.should_italicize)) - } - }; + let (synthetic_bold, synthetic_italic) = match self.synthesis_cache.entry(font_id) + { + std::collections::hash_map::Entry::Occupied(e) => *e.get(), + std::collections::hash_map::Entry::Vacant(e) => { + let lib = self.font_library.inner.read(); + let fd = lib.get(&(font_id as usize)); + *e.insert((fd.should_embolden, fd.should_italicize)) + } + }; #[cfg(target_os = "macos")] let (glyphs, ascent_px) = { let handle = match self.handle_cache.entry(font_id) { - std::collections::hash_map::Entry::Occupied(e) => { - e.into_mut().clone() - } + std::collections::hash_map::Entry::Occupied(e) => e.into_mut().clone(), std::collections::hash_map::Entry::Vacant(e) => { let h = self.font_library.ct_font(font_id as usize)?; e.insert(h.clone()); @@ -360,15 +361,12 @@ impl Text { // Pull (or cache) the font bytes + offset + key once per // font_id to avoid the RwLock read-lock per shape. - let font_entry = self - .font_data_cache - .entry(font_id) - .or_insert_with(|| { - let lib = self.font_library.inner.read(); - lib.get_data(&(font_id as usize)).expect( - "font id resolved but get_data returned None — cache invariant", - ) - }); + let font_entry = self.font_data_cache.entry(font_id).or_insert_with(|| { + let lib = self.font_library.inner.read(); + lib.get_data(&(font_id as usize)).expect( + "font id resolved but get_data returned None — cache invariant", + ) + }); let font_ref = FontRef { data: font_entry.0.as_ref(), offset: font_entry.1, @@ -376,12 +374,13 @@ impl Text { }; // Ascent — via swash metrics scaled to device-px size. - let ascent_px = *self.ascent_cache.entry((font_id, size_bucket)).or_insert_with( - || { + let ascent_px = *self + .ascent_cache + .entry((font_id, size_bucket)) + .or_insert_with(|| { let m = font_ref.metrics(&[]).scale(size_u16 as f32); m.ascent.round().clamp(i16::MIN as f32, i16::MAX as f32) as i16 - }, - ); + }); // Shape with swash. Flatten clusters to a Vec // with UTF-8 byte offset as `cluster`. @@ -502,8 +501,7 @@ impl Text { height: raw.height.min(u16::MAX as u32) as u16, bearing_x: raw.left.clamp(i16::MIN as i32, i16::MAX as i32) as i16, bearing_y: { - let top_i16 = - raw.top.clamp(i16::MIN as i32, i16::MAX as i32) as i16; + let top_i16 = raw.top.clamp(i16::MIN as i32, i16::MAX as i32) as i16; run.ascent_px.saturating_sub(top_i16) }, bytes: &raw.bytes, @@ -534,7 +532,15 @@ impl Text { } } }; - Some((slot.x, slot.y, slot.w, slot.h, slot.bearing_x, slot.bearing_y, is_color)) + Some(( + slot.x, + slot.y, + slot.w, + slot.h, + slot.bearing_x, + slot.bearing_y, + is_color, + )) } // ---- non-macOS (swash → WgpuGlyphAtlas) ---- @@ -566,8 +572,7 @@ impl Text { height: raw.height.min(u16::MAX as u32) as u16, bearing_x: raw.left.clamp(i16::MIN as i32, i16::MAX as i32) as i16, bearing_y: { - let top_i16 = - raw.top.clamp(i16::MIN as i32, i16::MAX as i32) as i16; + let top_i16 = raw.top.clamp(i16::MIN as i32, i16::MAX as i32) as i16; run.ascent_px.saturating_sub(top_i16) }, bytes: &raw.bytes, @@ -577,7 +582,15 @@ impl Text { } else { state.atlas_grayscale.insert(key, raster)? }; - Some((slot.x, slot.y, slot.w, slot.h, slot.bearing_x, slot.bearing_y, is_color)) + Some(( + slot.x, + slot.y, + slot.w, + slot.h, + slot.bearing_x, + slot.bearing_y, + is_color, + )) } } @@ -628,11 +641,7 @@ impl Text { unsafe { let dst = state.instance_buffer.contents() as *mut TextInstance; - std::ptr::copy_nonoverlapping( - self.instances.as_ptr(), - dst, - instance_count, - ); + std::ptr::copy_nonoverlapping(self.instances.as_ptr(), dst, instance_count); } encoder.set_render_pipeline_state(&state.pipeline); @@ -709,7 +718,8 @@ impl Text { atlas_color.view(), ); - let pipeline = build_text_pipeline_wgpu(device, format, &[&uniform_bgl, &atlas_bgl]); + let pipeline = + build_text_pipeline_wgpu(device, format, &[&uniform_bgl, &atlas_bgl]); let instance_capacity: usize = 256; let instance_buffer = alloc_instance_buffer_wgpu(device, instance_capacity); @@ -830,11 +840,7 @@ fn rasterize_swash_glyph( key: font_entry.2, }; - let mut scaler = scale_ctx - .builder(font_ref) - .hint(hint) - .size(size_px) - .build(); + let mut scaler = scale_ctx.builder(font_ref).hint(hint).size(size_px).build(); let mut image = GlyphImage::new(); let sources: &[Source] = &[ @@ -954,10 +960,7 @@ fn build_text_pipeline_metal(device: &metal::Device) -> metal::RenderPipelineSta } #[cfg(target_os = "macos")] -fn alloc_instance_buffer_metal( - device: &metal::Device, - capacity: usize, -) -> metal::Buffer { +fn alloc_instance_buffer_metal(device: &metal::Device, capacity: usize) -> metal::Buffer { let size = (capacity.max(1) * std::mem::size_of::()) as u64; device.new_buffer(size, metal::MTLResourceOptions::StorageModeShared) } @@ -1035,9 +1038,7 @@ fn build_text_pipeline_wgpu( ) -> wgpu::RenderPipeline { let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("sugarloaf.text.wgsl"), - source: wgpu::ShaderSource::Wgsl( - include_str!("text_shader.wgsl").into(), - ), + source: wgpu::ShaderSource::Wgsl(include_str!("text_shader.wgsl").into()), }); let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {