@@ -323,6 +323,13 @@ enum DecorationStyle {
323323/// `font.sprite_index` idea.
324324const DECORATION_FONT_ID_BASE : u32 = 0xFFFF_FF00 ;
325325
326+ /// Sentinel font_id for Glyph Protocol registrations. Pulled directly
327+ /// in u32 form from `sugarloaf::font::glyph_registry`; lands above the
328+ /// cursor/decoration ranges and never collides with a real font index.
329+ /// The atlas `glyph_id` for a registered cell is
330+ /// `pack_atlas_glyph_id(codepoint, version)`.
331+ use rio_backend:: sugarloaf:: font:: glyph_registry:: CUSTOM_GLYPH_FONT_ID_U32 ;
332+
326333/// Sentinel font_id base for cursor sprites. Distinct from the
327334/// decoration range so the two never collide in the atlas
328335/// hash-key space.
@@ -1347,6 +1354,14 @@ pub fn build_row_fg(
13471354 let has_color_hints = row_hints. iter ( ) . any ( |rh| rh. tag != HintTag :: HyperlinkHover ) ;
13481355 let needs_per_cell_check = has_sel || has_color_hints;
13491356
1357+ // Glyph Protocol registry — cloned once per row so the per-cell
1358+ // custom-glyph helper avoids re-acquiring the FontLibrary read
1359+ // lock on every registered cell. Arc clone is cheap; `None` when
1360+ // no program in this session has used the protocol.
1361+ let glyph_registry: Option <
1362+ rio_backend:: sugarloaf:: font:: glyph_registry:: GlyphRegistry ,
1363+ > = font_library. inner . read ( ) . glyph_registry . clone ( ) ;
1364+
13501365 // Phase 1: underline pass. Emit before glyphs so grayscale quads
13511366 // draw under the characters.
13521367 emit_underlines (
@@ -1379,6 +1394,93 @@ pub fn build_row_fg(
13791394 ( style_set. get ( sq. style_id ( ) ) . flags . bits ( ) & SHAPING_FLAG_MASK ) as u8 ;
13801395 let ( font_id, is_emoji) =
13811396 rasterizer. resolve_font ( ch, run_style_flags, font_library) ;
1397+
1398+ // Glyph Protocol short-circuit: registered codepoints render
1399+ // directly from the registry without shaping, run-extension,
1400+ // or per-platform shaper plumbing. Each registered cell is
1401+ // its own one-cell run.
1402+ if font_id == CUSTOM_GLYPH_FONT_ID_U32 {
1403+ // The font cascade reported a custom glyph but the row
1404+ // already cloned `glyph_registry` as None — registry was
1405+ // detached between font resolution and this branch (rare,
1406+ // but harmless: render nothing).
1407+ let Some ( registry) = glyph_registry. as_ref ( ) else {
1408+ x += 1 ;
1409+ continue ;
1410+ } ;
1411+
1412+ // Borrow the primary font's ascent at this size if the
1413+ // run-shaper has populated it; otherwise approximate at
1414+ // 80% of the glyph size. The approximation only fires
1415+ // when no regular text has been laid out at this size yet
1416+ // — once the user types real text the cache fills and
1417+ // subsequent registered cells use the precise ascent.
1418+ let ascent_px = rasterizer
1419+ . ascent_cache
1420+ . get ( & (
1421+ rio_backend:: sugarloaf:: font:: FONT_ID_REGULAR as u32 ,
1422+ size_bucket,
1423+ ) )
1424+ . copied ( )
1425+ . unwrap_or_else ( || ( size_u16 as i16 ) . saturating_mul ( 4 ) / 5 ) ;
1426+
1427+ // fg colour, mirroring the regular emit loop's
1428+ // selection / hint precedence.
1429+ let color = if !needs_per_cell_check {
1430+ cell_fg ( sq, style_set, renderer, term_colors)
1431+ } else {
1432+ let is_sel = cell_in_row_sel ( row_sel, x as u16 ) ;
1433+ let hint_tag = if is_sel {
1434+ None
1435+ } else {
1436+ cell_in_row_hints ( row_hints, x as u16 )
1437+ } ;
1438+ if is_sel {
1439+ cell_fg_selected ( sq, style_set, renderer, term_colors)
1440+ } else if let Some ( tag) = hint_tag {
1441+ cell_fg_hinted ( tag, renderer)
1442+ } else {
1443+ cell_fg ( sq, style_set, renderer, term_colors)
1444+ }
1445+ } ;
1446+
1447+ if let Some ( ( _, slot, is_color) ) = ensure_custom_glyph_by_codepoint (
1448+ grid,
1449+ registry,
1450+ ch as u32 ,
1451+ size_bucket,
1452+ size_u16,
1453+ cell_h,
1454+ ascent_px,
1455+ color,
1456+ ) {
1457+ if slot. w != 0 && slot. h != 0 {
1458+ // Colour atlas entries are pre-painted (palette
1459+ // applied during COLR rasterisation), so the
1460+ // shader multiplies by white. Mono entries take
1461+ // the per-cell fg colour the run loop computed
1462+ // above.
1463+ let ( atlas, color) = if is_color {
1464+ ( CellText :: ATLAS_COLOR , [ 255 , 255 , 255 , 255 ] )
1465+ } else {
1466+ ( CellText :: ATLAS_GRAYSCALE , color)
1467+ } ;
1468+ fg_scratch. push ( CellText {
1469+ glyph_pos : [ slot. x as u32 , slot. y as u32 ] ,
1470+ glyph_size : [ slot. w as u32 , slot. h as u32 ] ,
1471+ bearings : [ slot. bearing_x , slot. bearing_y ] ,
1472+ grid_pos : [ x as u16 , y] ,
1473+ color,
1474+ atlas,
1475+ bools : 0 ,
1476+ _pad : [ 0 , 0 ] ,
1477+ } ) ;
1478+ }
1479+ }
1480+ x += 1 ;
1481+ continue ;
1482+ }
1483+
13821484 let run_start = x;
13831485
13841486 // Kitty Unicode placeholder shapes as a space — the cell
@@ -1840,6 +1942,87 @@ fn ensure_glyph_by_id(
18401942 Some ( ( key, slot, is_color) )
18411943}
18421944
1945+ /// Look up or rasterise a Glyph Protocol registration into the grid
1946+ /// atlas. The atlas key combines the codepoint with the registration's
1947+ /// `version` (bumped on every register/clear) so re-registering the
1948+ /// same codepoint never serves a stale rasterisation. Each unique
1949+ /// (codepoint × version × pixel size) combination owns one atlas slot;
1950+ /// previous-version slots become unreachable and the atlas LRU evicts
1951+ /// them in due course.
1952+ ///
1953+ /// `ascent_px` matches the primary font's ascent at the same size
1954+ /// bucket — Glyph Protocol payloads have no font-of-their-own, so we
1955+ /// align registered glyphs to the surrounding text baseline. A more
1956+ /// faithful rendering would walk the registered outline's bbox to
1957+ /// compute per-glyph bearings, but for icon-style PUA glyphs the
1958+ /// primary-font baseline produces the expected appearance.
1959+ ///
1960+ /// `registry` is the active terminal's glyph registry, cloned once
1961+ /// per row by `build_row_fg`. Passing it in (instead of going through
1962+ /// the `FontLibrary` write lock) keeps the per-cell hot loop allocation
1963+ /// and lock free.
1964+ ///
1965+ /// Returns `None` when the registration was cleared between font
1966+ /// resolution and render, or when rasterisation produces no pixels
1967+ /// (zero-area outline, malformed COLR, etc.).
1968+ #[ allow( clippy:: too_many_arguments) ]
1969+ fn ensure_custom_glyph_by_codepoint (
1970+ grid : & mut GridRenderer ,
1971+ registry : & rio_backend:: sugarloaf:: font:: glyph_registry:: GlyphRegistry ,
1972+ codepoint : u32 ,
1973+ size_bucket : u16 ,
1974+ size_u16 : u16 ,
1975+ cell_h : f32 ,
1976+ ascent_px : i16 ,
1977+ foreground_rgba : [ u8 ; 4 ] ,
1978+ ) -> Option < ( GlyphKey , AtlasSlot , bool ) > {
1979+ use rio_backend:: sugarloaf:: font:: glyph_registry:: pack_atlas_glyph_id;
1980+
1981+ // Fetch first so we know the registration's version. The lookup
1982+ // happens under the registry's RwLock read; the entry's payload is
1983+ // cloned out so the lock drops before we hit tiny-skia.
1984+ let entry = registry. get ( codepoint) ?;
1985+ let key = GlyphKey {
1986+ font_id : CUSTOM_GLYPH_FONT_ID_U32 ,
1987+ glyph_id : pack_atlas_glyph_id ( codepoint, entry. version ) ,
1988+ size_bucket,
1989+ } ;
1990+ if let Some ( slot) = grid. lookup_glyph ( key) {
1991+ return Some ( ( key, slot, false ) ) ;
1992+ }
1993+ if let Some ( slot) = grid. lookup_glyph_color ( key) {
1994+ return Some ( ( key, slot, true ) ) ;
1995+ }
1996+
1997+ let raster = rio_backend:: sugarloaf:: glyph_protocol:: rasterize_payload (
1998+ & entry. payload ,
1999+ entry. upm ,
2000+ size_u16,
2001+ foreground_rgba,
2002+ ) ?;
2003+
2004+ let bearing_y = {
2005+ let cell_h_i16 = cell_h. round ( ) . clamp ( 0.0 , i16:: MAX as f32 ) as i16 ;
2006+ cell_h_i16
2007+ . saturating_sub ( ascent_px)
2008+ . saturating_add ( raster. top . clamp ( i16:: MIN as i32 , i16:: MAX as i32 ) as i16 )
2009+ } ;
2010+ let raster_in = RasterizedGlyph {
2011+ width : raster. width ,
2012+ height : raster. height ,
2013+ bearing_x : raster. left . clamp ( i16:: MIN as i32 , i16:: MAX as i32 ) as i16 ,
2014+ bearing_y,
2015+ bytes : & raster. data ,
2016+ } ;
2017+
2018+ let slot = if raster. is_color {
2019+ grid. insert_glyph_color ( key, raster_in) ?
2020+ } else {
2021+ grid. insert_glyph ( key, raster_in) ?
2022+ } ;
2023+ Some ( ( key, slot, raster. is_color ) )
2024+ }
2025+
18432026/// Platform-agnostic raw-glyph struct. Both backends populate this
18442027/// shape and let the caller convert bearings to the grid's
18452028/// cell-bottom-relative convention.
0 commit comments