use rio_backend::config::colors::term::TermColors;
use rio_backend::crosswords::grid::row::Row;
use rio_backend::crosswords::pos::{Column, Line, Pos};
use rio_backend::crosswords::search::Match;
use rio_backend::crosswords::square::{ContentTag, Square};
use rio_backend::crosswords::style::{StyleFlags, StyleSet};
use rio_backend::selection::SelectionRange;
use rustc_hash::FxHashMap;
use smallvec::SmallVec;
use crate::renderer::Renderer;
#[derive(Clone, Copy)]
pub struct RowSelection {
pub lo: u16,
pub hi: u16,
}
pub fn row_selection_for(
sel: Option<SelectionRange>,
y: usize,
cols: usize,
display_offset: i32,
) -> Option<RowSelection> {
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);
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<RowSelection>, col: u16) -> bool {
match row_sel {
Some(s) => col >= s.lo && col <= s.hi,
None => false,
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum HintTag {
Match,
Focused,
HyperlinkHover,
}
#[derive(Clone, Copy, Debug)]
pub struct RowHint {
pub lo: u16,
pub hi: u16,
pub tag: HintTag,
}
pub fn row_hints_for(
hint_matches: Option<&[Match]>,
focused_match: Option<&Match>,
hover_hyperlink: Option<(Pos, Pos)>,
y: usize,
cols: usize,
display_offset: i32,
out: &mut Vec<RowHint>,
) {
out.clear();
if cols == 0 {
return;
}
let line = Line((y as i32) - display_offset);
let cols_max = cols.saturating_sub(1) as u16;
let pos_pair_to_row_hint = |start: Pos, end: Pos, tag: HintTag| -> Option<RowHint> {
if line < start.row || line > end.row {
return None;
}
let lo = if line == start.row {
start.col.0 as u16
} else {
0
};
let hi = if line == end.row {
end.col.0 as u16
} else {
cols_max
};
Some(RowHint {
lo: lo.min(cols_max),
hi: hi.min(cols_max),
tag,
})
};
let to_row_hint =
|m: &Match, tag: HintTag| pos_pair_to_row_hint(*m.start(), *m.end(), tag);
let is_same_match = |a: &Match, b: &Match| -> bool {
let (a_start, a_end) = (*a.start(), *a.end());
let (b_start, b_end) = (*b.start(), *b.end());
pos_eq(a_start, b_start) && pos_eq(a_end, b_end)
};
if let Some((start, end)) = hover_hyperlink {
if let Some(rh) = pos_pair_to_row_hint(start, end, HintTag::HyperlinkHover) {
out.push(rh);
}
}
let Some(matches) = hint_matches else {
return;
};
if let Some(fm) = focused_match {
if let Some(rh) = to_row_hint(fm, HintTag::Focused) {
out.push(rh);
}
}
for m in matches {
if let Some(fm) = focused_match {
if is_same_match(m, fm) {
continue;
}
}
if let Some(rh) = to_row_hint(m, HintTag::Match) {
out.push(rh);
}
}
}
#[inline]
fn pos_eq(a: Pos, b: Pos) -> bool {
a.row == b.row && a.col == b.col
}
#[inline]
fn cell_in_row_hints(row_hints: &[RowHint], col: u16) -> Option<HintTag> {
for rh in row_hints {
if rh.tag == HintTag::HyperlinkHover {
continue;
}
if col >= rh.lo && col <= rh.hi {
return Some(rh.tag);
}
}
None
}
#[inline]
fn cell_in_hover_underline(row_hints: &[RowHint], col: u16) -> bool {
row_hints
.iter()
.any(|rh| rh.tag == HintTag::HyperlinkHover && col >= rh.lo && col <= rh.hi)
}
#[inline]
fn cell_fg_hinted(tag: HintTag, renderer: &Renderer) -> [u8; 4] {
match tag {
HintTag::Focused => {
normalized_to_u8(renderer.named_colors.search_focused_match_foreground)
}
HintTag::Match => normalized_to_u8(renderer.named_colors.search_match_foreground),
HintTag::HyperlinkHover => [0, 0, 0, 0],
}
}
use rio_backend::sugarloaf::font::FontLibrary;
use rio_backend::sugarloaf::grid::{
AtlasSlot, CellBg, CellText, GlyphKey, GridRenderer, RasterizedGlyph,
};
pub fn cell_fg(
sq: Square,
style_set: &StyleSet,
renderer: &Renderer,
term_colors: &TermColors,
) -> [u8; 4] {
if sq.is_bg_only() {
return normalized_to_u8(renderer.named_colors.foreground);
}
let mut style = style_set.get(sq.style_id());
if style.flags.contains(StyleFlags::INVERSE) {
std::mem::swap(&mut style.fg, &mut style.bg);
}
let color = renderer.compute_color(&style.fg, style.flags, term_colors);
normalized_to_u8(color)
}
#[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)
}
}
#[derive(Clone, Copy, Debug)]
#[repr(u32)]
enum DecorationStyle {
Underline = 0,
DoubleUnderline = 1,
DottedUnderline = 2,
DashedUnderline = 3,
CurlyUnderline = 4,
Strikethrough = 5,
}
const DECORATION_FONT_ID_BASE: u32 = 0xFFFF_FF00;
const CURSOR_FONT_ID_BASE: u32 = 0xFFFF_FE00;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u32)]
enum CursorSpriteStyle {
Block = 0,
Hollow = 1,
Bar = 2,
Underline = 3,
}
impl CursorSpriteStyle {
#[inline]
fn is_block_slot(self) -> bool {
matches!(self, CursorSpriteStyle::Block)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CursorRenderStyle {
Block,
BlockHollow,
Bar,
Underline,
}
pub struct CursorRenderInputs {
pub visible: bool,
pub focused: bool,
pub blink_visible: bool,
pub blinking: bool,
pub preedit: bool,
pub shape: rio_backend::ansi::CursorShape,
}
pub fn cursor_render_style(opts: CursorRenderInputs) -> Option<CursorRenderStyle> {
use rio_backend::ansi::CursorShape;
if opts.preedit {
return Some(CursorRenderStyle::Block);
}
if !opts.visible || opts.shape == CursorShape::Hidden {
return None;
}
if !opts.focused {
return Some(CursorRenderStyle::BlockHollow);
}
if opts.blinking && !opts.blink_visible {
return None;
}
Some(match opts.shape {
CursorShape::Block => CursorRenderStyle::Block,
CursorShape::Underline => CursorRenderStyle::Underline,
CursorShape::Beam => CursorRenderStyle::Bar,
CursorShape::Hidden => unreachable!("hidden shape is filtered above"),
})
}
impl CursorRenderStyle {
#[inline]
fn sprite(self) -> CursorSpriteStyle {
match self {
CursorRenderStyle::Block => CursorSpriteStyle::Block,
CursorRenderStyle::BlockHollow => CursorSpriteStyle::Hollow,
CursorRenderStyle::Bar => CursorSpriteStyle::Bar,
CursorRenderStyle::Underline => CursorSpriteStyle::Underline,
}
}
}
#[inline]
fn cursor_thickness(cell_h: u32) -> u32 {
(cell_h / 16).clamp(1, 2)
}
fn rasterize_cursor(
style: CursorSpriteStyle,
cell_w: u32,
cell_h: u32,
thickness: u32,
) -> (Vec<u8>, u16, u16, i16, i16) {
let t = thickness.max(1);
match style {
CursorSpriteStyle::Block => {
let bytes = vec![0xFFu8; (cell_w * cell_h) as usize];
(
bytes,
cell_w.min(u16::MAX as u32) as u16,
cell_h.min(u16::MAX as u32) as u16,
0,
cell_h.min(i16::MAX as u32) as i16,
)
}
CursorSpriteStyle::Hollow => {
let row_w = cell_w as usize;
let h = cell_h as usize;
let mut bytes = vec![0u8; row_w * h];
let ti = (t as usize).max(1);
for row in 0..ti.min(h) {
let s = row * row_w;
bytes[s..s + row_w].fill(0xFF);
}
for row in h.saturating_sub(ti)..h {
let s = row * row_w;
bytes[s..s + row_w].fill(0xFF);
}
for row in ti..h.saturating_sub(ti) {
let s = row * row_w;
for col in 0..ti.min(row_w) {
bytes[s + col] = 0xFF;
}
for col in row_w.saturating_sub(ti)..row_w {
bytes[s + col] = 0xFF;
}
}
(
bytes,
cell_w.min(u16::MAX as u32) as u16,
cell_h.min(u16::MAX as u32) as u16,
0,
cell_h.min(i16::MAX as u32) as i16,
)
}
CursorSpriteStyle::Bar => {
let bytes = vec![0xFFu8; (t * cell_h) as usize];
let bearing_x = -((t as i16 + 1) / 2);
(
bytes,
t.min(u16::MAX as u32) as u16,
cell_h.min(u16::MAX as u32) as u16,
bearing_x,
cell_h.min(i16::MAX as u32) as i16,
)
}
CursorSpriteStyle::Underline => {
let bytes = vec![0xFFu8; (cell_w * t) as usize];
let bearing_y = (t + underline_gap_below(cell_h)) as i16;
(
bytes,
cell_w.min(u16::MAX as u32) as u16,
t.min(u16::MAX as u32) as u16,
0,
bearing_y,
)
}
}
}
fn ensure_cursor_sprite_slot(
grid: &mut GridRenderer,
style: CursorSpriteStyle,
cell_w: u32,
cell_h: u32,
thickness: u32,
) -> Option<AtlasSlot> {
let key = GlyphKey {
font_id: CURSOR_FONT_ID_BASE + style as u32,
glyph_id: cell_w,
size_bucket: ((thickness as u16 & 0xF) << 12) | (cell_h.min(0xFFF) as u16),
};
if let Some(slot) = grid.lookup_glyph(key) {
return Some(slot);
}
let (bytes, w, h, bearing_x, bearing_y) =
rasterize_cursor(style, cell_w, cell_h, thickness);
grid.insert_glyph(
key,
RasterizedGlyph {
width: w,
height: h,
bearing_x,
bearing_y,
bytes: &bytes,
},
)
}
pub fn emit_cursor_sprite(
grid: &mut GridRenderer,
style: CursorRenderStyle,
col: u16,
row: u16,
color: [u8; 4],
cell_w: u32,
cell_h: u32,
) {
let sprite = style.sprite();
let thickness = cursor_thickness(cell_h);
let Some(slot) = ensure_cursor_sprite_slot(grid, sprite, cell_w, cell_h, thickness)
else {
return;
};
if slot.w == 0 || slot.h == 0 {
return;
}
let cursor_cell = CellText {
glyph_pos: [slot.x as u32, slot.y as u32],
glyph_size: [slot.w as u32, slot.h as u32],
bearings: [slot.bearing_x, slot.bearing_y],
grid_pos: [col, row],
color,
atlas: CellText::ATLAS_GRAYSCALE,
bools: CellText::BOOL_IS_CURSOR_GLYPH,
_pad: [0, 0],
};
if sprite.is_block_slot() {
grid.set_block_cursor(&[cursor_cell]);
} else {
grid.set_non_block_cursor(&[cursor_cell]);
}
}
#[inline]
fn decoration_thickness(size_px: f32) -> u32 {
(size_px * 0.075).round().max(1.0) as u32
}
#[inline]
fn underline_gap_below(cell_h: u32) -> u32 {
(cell_h / 20).max(1)
}
fn rasterize_decoration(
style: DecorationStyle,
cell_w: u32,
cell_h: u32,
thickness: u32,
) -> (Vec<u8>, u32, u32, i16) {
match style {
DecorationStyle::Underline => {
let bytes = vec![0xFFu8; (cell_w * thickness) as usize];
let bearing_y = (thickness + underline_gap_below(cell_h)) as i16;
(bytes, cell_w, thickness, bearing_y)
}
DecorationStyle::DoubleUnderline => {
let gap = thickness;
let h = thickness * 2 + gap;
let mut bytes = vec![0u8; (cell_w * h) as usize];
let row_w = cell_w as usize;
for row in 0..thickness as usize {
let start = row * row_w;
bytes[start..start + row_w].fill(0xFF);
}
for row in (thickness + gap) as usize..h as usize {
let start = row * row_w;
bytes[start..start + row_w].fill(0xFF);
}
let bearing_y = (h + underline_gap_below(cell_h)) as i16;
(bytes, cell_w, h, bearing_y)
}
DecorationStyle::DottedUnderline => {
let h = thickness;
let diameter = thickness.max(1);
let period = diameter * 2;
let mut bytes = vec![0u8; (cell_w * h) as usize];
let row_w = cell_w as usize;
let mut x = 0u32;
while x < cell_w {
let end = (x + diameter).min(cell_w);
for row in 0..h as usize {
let start = row * row_w + x as usize;
bytes[start..start + (end - x) as usize].fill(0xFF);
}
x += period;
}
let bearing_y = (h + underline_gap_below(cell_h)) as i16;
(bytes, cell_w, h, bearing_y)
}
DecorationStyle::DashedUnderline => {
let h = thickness;
let b1 = cell_w / 4;
let b2 = cell_w / 2;
let b3 = (cell_w * 3) / 4;
let mut bytes = vec![0u8; (cell_w * h) as usize];
let row_w = cell_w as usize;
for (x_lo, x_hi) in [(0u32, b1), (b2, b3)] {
if x_hi <= x_lo {
continue;
}
for row in 0..h as usize {
let start = row * row_w + x_lo as usize;
let end = row * row_w + x_hi as usize;
bytes[start..end].fill(0xFF);
}
}
let bearing_y = (h + underline_gap_below(cell_h)) as i16;
(bytes, cell_w, h, bearing_y)
}
DecorationStyle::CurlyUnderline => {
use core::f32::consts::PI;
let amp = (cell_w as f32 / PI).max(thickness as f32);
let amp_i = amp.ceil() as u32;
let h = amp_i + thickness + 1;
let mut bytes = vec![0u8; (cell_w * h) as usize];
let row_w = cell_w as usize;
let half_t = thickness as f32 * 0.5;
let baseline = h as f32 - half_t - 0.5;
for col in 0..cell_w {
let x_norm = (col as f32 + 0.5) / cell_w as f32;
let s = 0.5 * (1.0 - (x_norm * 2.0 * PI).cos());
let y_center = baseline - s * amp;
let y_lo = (y_center - half_t).floor().max(0.0) as u32;
let y_hi = ((y_center + half_t).ceil() as u32).min(h);
for row in y_lo..y_hi {
bytes[row as usize * row_w + col as usize] = 0xFF;
}
}
let bearing_y = (h + underline_gap_below(cell_h)) as i16;
(bytes, cell_w, h, bearing_y)
}
DecorationStyle::Strikethrough => {
let bytes = vec![0xFFu8; (cell_w * thickness) as usize];
let center_from_bottom = cell_h / 2;
let bearing_y = center_from_bottom as i16 + (thickness as i16 + 1) / 2;
(bytes, cell_w, thickness, bearing_y)
}
}
}
fn ensure_decoration_slot(
grid: &mut GridRenderer,
style: DecorationStyle,
cell_w: u32,
cell_h: u32,
thickness: u32,
) -> Option<AtlasSlot> {
let key = GlyphKey {
font_id: DECORATION_FONT_ID_BASE + style as u32,
glyph_id: cell_w,
size_bucket: thickness as u16,
};
if let Some(slot) = grid.lookup_glyph(key) {
return Some(slot);
}
let (bytes, w, h, bearing_y) = rasterize_decoration(style, cell_w, cell_h, thickness);
grid.insert_glyph(
key,
RasterizedGlyph {
width: w.min(u16::MAX as u32) as u16,
height: h.min(u16::MAX as u32) as u16,
bearing_x: 0,
bearing_y,
bytes: &bytes,
},
)
}
#[inline]
fn underline_style_from_flags(flags: StyleFlags) -> Option<DecorationStyle> {
if flags.contains(StyleFlags::UNDERLINE) {
Some(DecorationStyle::Underline)
} else if flags.contains(StyleFlags::DOUBLE_UNDERLINE) {
Some(DecorationStyle::DoubleUnderline)
} else if flags.contains(StyleFlags::UNDERCURL) {
Some(DecorationStyle::CurlyUnderline)
} else if flags.contains(StyleFlags::DOTTED_UNDERLINE) {
Some(DecorationStyle::DottedUnderline)
} else if flags.contains(StyleFlags::DASHED_UNDERLINE) {
Some(DecorationStyle::DashedUnderline)
} else {
None
}
}
#[inline]
fn decoration_color(
sq: Square,
style: &rio_backend::crosswords::style::Style,
style_set: &StyleSet,
renderer: &Renderer,
term_colors: &TermColors,
) -> [u8; 4] {
if let Some(uc) = style.underline_color {
normalized_to_u8(renderer.compute_color(&uc, style.flags, term_colors))
} else {
cell_fg(sq, style_set, renderer, term_colors)
}
}
pub fn cell_bg(
sq: Square,
style_set: &StyleSet,
renderer: &Renderer,
term_colors: &TermColors,
) -> [u8; 4] {
let color = match sq.content_tag() {
ContentTag::BgRgb => {
let (r, g, b) = sq.bg_rgb();
return [r, g, b, 255];
}
ContentTag::BgPalette => {
let idx = sq.bg_palette_index() as usize;
renderer.color(idx, term_colors)
}
ContentTag::Codepoint => {
let mut style = style_set.get(sq.style_id());
if style.flags.contains(StyleFlags::INVERSE) {
std::mem::swap(&mut style.fg, &mut style.bg);
}
renderer.compute_bg_color(&style, term_colors)
}
};
normalized_to_u8(color)
}
#[inline]
fn normalized_to_u8(c: [f32; 4]) -> [u8; 4] {
[
(c[0].clamp(0.0, 1.0) * 255.0) as u8,
(c[1].clamp(0.0, 1.0) * 255.0) as u8,
(c[2].clamp(0.0, 1.0) * 255.0) as u8,
(c[3].clamp(0.0, 1.0) * 255.0) as u8,
]
}
#[allow(clippy::too_many_arguments)]
pub fn build_row_bg(
row: &Row<Square>,
cols: usize,
style_set: &StyleSet,
renderer: &Renderer,
term_colors: &TermColors,
row_sel: Option<RowSelection>,
row_hints: &[RowHint],
bg_scratch: &mut Vec<CellBg>,
) {
bg_scratch.clear();
let has_sel = row_sel.is_some();
let has_color_hints = row_hints.iter().any(|rh| rh.tag != HintTag::HyperlinkHover);
if !has_sel && !has_color_hints {
bg_scratch.reserve(cols);
for x in 0..cols {
let sq = row[Column(x)];
bg_scratch.push(CellBg {
rgba: cell_bg(sq, style_set, renderer, term_colors),
});
}
return;
}
let sel_bg = if has_sel {
Some(normalized_to_u8(renderer.named_colors.selection_background))
} else {
None
};
let (match_bg, focused_bg) = if has_color_hints {
(
Some(normalized_to_u8(
renderer.named_colors.search_match_background,
)),
Some(normalized_to_u8(
renderer.named_colors.search_focused_match_background,
)),
)
} else {
(None, None)
};
for x in 0..cols {
let sq = row[Column(x)];
let col = x as u16;
let rgba = if cell_in_row_sel(row_sel, col) {
sel_bg.unwrap_or_else(|| cell_bg(sq, style_set, renderer, term_colors))
} else if let Some(tag) = cell_in_row_hints(row_hints, col) {
match tag {
HintTag::Focused => focused_bg
.unwrap_or_else(|| cell_bg(sq, style_set, renderer, term_colors)),
HintTag::Match => match_bg
.unwrap_or_else(|| cell_bg(sq, style_set, renderer, term_colors)),
HintTag::HyperlinkHover => cell_bg(sq, style_set, renderer, term_colors),
}
} else {
cell_bg(sq, style_set, renderer, term_colors)
};
bg_scratch.push(CellBg { rgba });
}
}
const SHAPING_FLAG_MASK: u16 = StyleFlags::BOLD.bits() | StyleFlags::ITALIC.bits();
const RUN_BUCKET_COUNT: usize = 256;
const RUN_BUCKET_SIZE: usize = 8;
#[derive(Clone, Copy, Debug)]
#[allow(dead_code)] struct ShapedGlyph {
id: u16,
x: f32,
y: f32,
advance: f32,
cluster: u32,
}
struct RunCacheEntry {
hash: u64,
glyphs: Vec<ShapedGlyph>,
}
pub struct GridGlyphRasterizer {
font_resolve: FxHashMap<(char, u8), (u32, bool)>,
ascent_cache: FxHashMap<(u32, u16), i16>,
synthesis_cache: FxHashMap<u32, (bool, bool)>,
run_cache: Vec<Vec<RunCacheEntry>>,
#[cfg(target_os = "macos")]
run_utf16_scratch: Vec<u16>,
#[cfg(target_os = "macos")]
run_cell_starts: Vec<u32>,
#[cfg(target_os = "macos")]
handle_cache: FxHashMap<u32, rio_backend::sugarloaf::font::macos::FontHandle>,
#[cfg(not(target_os = "macos"))]
run_str_scratch: String,
#[cfg(not(target_os = "macos"))]
shape_ctx: rio_backend::sugarloaf::swash::shape::ShapeContext,
#[cfg(not(target_os = "macos"))]
scale_ctx: rio_backend::sugarloaf::swash::scale::ScaleContext,
#[cfg(not(target_os = "macos"))]
font_data_cache: FxHashMap<
u32,
(
rio_backend::sugarloaf::font::SharedData,
u32,
rio_backend::sugarloaf::swash::CacheKey,
),
>,
}
impl Default for GridGlyphRasterizer {
fn default() -> Self {
Self::new()
}
}
impl GridGlyphRasterizer {
pub fn new() -> Self {
Self {
font_resolve: FxHashMap::default(),
ascent_cache: FxHashMap::default(),
synthesis_cache: FxHashMap::default(),
run_cache: (0..RUN_BUCKET_COUNT)
.map(|_| Vec::with_capacity(RUN_BUCKET_SIZE))
.collect(),
#[cfg(target_os = "macos")]
run_utf16_scratch: Vec::new(),
#[cfg(target_os = "macos")]
run_cell_starts: Vec::new(),
#[cfg(not(target_os = "macos"))]
run_str_scratch: String::new(),
#[cfg(target_os = "macos")]
handle_cache: FxHashMap::default(),
#[cfg(not(target_os = "macos"))]
shape_ctx: rio_backend::sugarloaf::swash::shape::ShapeContext::new(),
#[cfg(not(target_os = "macos"))]
scale_ctx: rio_backend::sugarloaf::swash::scale::ScaleContext::new(),
#[cfg(not(target_os = "macos"))]
font_data_cache: FxHashMap::default(),
}
}
#[inline]
fn resolve_font(
&mut self,
ch: char,
style_flags: u8,
font_library: &FontLibrary,
) -> (u32, bool) {
if ch == rio_backend::ansi::kitty_virtual::PLACEHOLDER {
return (rio_backend::sugarloaf::font::FONT_ID_REGULAR as u32, false);
}
if style_flags == 0 && (' '..='~').contains(&ch) {
return (rio_backend::sugarloaf::font::FONT_ID_REGULAR as u32, false);
}
*self
.font_resolve
.entry((ch, style_flags))
.or_insert_with(|| {
let span_style = span_style_for_flags(style_flags);
#[cfg(target_os = "macos")]
let (id, emoji) = font_library.resolve_font_for_char(ch, &span_style);
#[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))
};
(id as u32, emoji)
})
}
#[inline]
fn get_synthesis(
&mut self,
font_id: u32,
font_library: &FontLibrary,
) -> (bool, bool) {
*self.synthesis_cache.entry(font_id).or_insert_with(|| {
let lib = font_library.inner.read();
let fd = lib.get(&(font_id as usize));
(fd.should_embolden, fd.should_italicize)
})
}
}
#[inline]
fn span_style_for_flags(style_flags: u8) -> rio_backend::sugarloaf::SpanStyle {
use rio_backend::sugarloaf::{Attributes, Stretch, Style as FontStyle, Weight};
let mut s = rio_backend::sugarloaf::SpanStyle::default();
let bold = (style_flags & StyleFlags::BOLD.bits() as u8) != 0;
let italic = (style_flags & StyleFlags::ITALIC.bits() as u8) != 0;
let weight = if bold { Weight::BOLD } else { Weight::NORMAL };
let fstyle = if italic {
FontStyle::Italic
} else {
FontStyle::Normal
};
s.font_attrs = Attributes::new(Stretch::NORMAL, weight, fstyle);
s
}
#[inline]
fn run_hash(font_id: u32, size_bucket: u16, style_flags: u8, run_bytes: &[u8]) -> u64 {
use core::hash::Hasher;
let mut h = rapidhash::fast::RapidHasher::default();
h.write_u32(font_id);
h.write_u16(size_bucket);
h.write_u8(style_flags);
h.write(run_bytes);
h.finish()
}
#[inline(always)]
fn is_run_breaker(sq: Square) -> bool {
if sq.is_bg_only() {
return true;
}
let ch = sq.c();
ch == '\0' || ch == ' '
}
fn run_cache_get(
buckets: &mut [Vec<RunCacheEntry>],
hash: u64,
) -> Option<&[ShapedGlyph]> {
let idx = (hash as usize) & (RUN_BUCKET_COUNT - 1);
let bucket = &mut buckets[idx];
let last = bucket.len().checked_sub(1)?;
for i in (0..bucket.len()).rev() {
if bucket[i].hash == hash {
if i != last {
bucket[i..=last].rotate_left(1);
}
return Some(&bucket[last].glyphs);
}
}
None
}
fn run_cache_put(buckets: &mut [Vec<RunCacheEntry>], entry: RunCacheEntry) {
let idx = (entry.hash as usize) & (RUN_BUCKET_COUNT - 1);
let bucket = &mut buckets[idx];
if bucket.len() >= RUN_BUCKET_SIZE {
bucket.remove(0);
}
bucket.push(entry);
}
#[cfg(target_os = "macos")]
fn shape_run_ct(
rasterizer: &mut GridGlyphRasterizer,
font_id: u32,
size_u16: u16,
size_bucket: u16,
font_library: &FontLibrary,
) -> Option<(Vec<ShapedGlyph>, i16)> {
let handle = match rasterizer.handle_cache.entry(font_id) {
std::collections::hash_map::Entry::Occupied(e) => e.into_mut().clone(),
std::collections::hash_map::Entry::Vacant(e) => {
let h = font_library.ct_font(font_id as usize)?;
e.insert(h.clone());
h
}
};
let ascent_px = *rasterizer
.ascent_cache
.entry((font_id, size_bucket))
.or_insert_with(|| {
let m = rio_backend::sugarloaf::font::macos::font_metrics(
&handle,
size_u16 as f32,
);
m.ascent.round().clamp(i16::MIN as f32, i16::MAX as f32) as i16
});
let ct_glyphs = rio_backend::sugarloaf::font::macos::shape_text_utf16(
&handle,
&rasterizer.run_utf16_scratch,
size_u16 as f32,
);
let glyphs: Vec<ShapedGlyph> = ct_glyphs
.iter()
.map(|g| ShapedGlyph {
id: g.id,
x: g.x,
y: g.y,
advance: g.advance,
cluster: g.cluster,
})
.collect();
Some((glyphs, ascent_px))
}
#[cfg(not(target_os = "macos"))]
fn shape_run_swash(
rasterizer: &mut GridGlyphRasterizer,
font_id: u32,
size_u16: u16,
size_bucket: u16,
font_library: &FontLibrary,
) -> Option<(Vec<ShapedGlyph>, i16)> {
use rio_backend::sugarloaf::swash::FontRef;
let font_entry = rasterizer
.font_data_cache
.entry(font_id)
.or_insert_with(|| {
let lib = font_library.inner.read();
lib.get_data(&(font_id as usize))
.expect("font id resolved but get_data returned None")
});
let font_ref = FontRef {
data: font_entry.0.as_ref(),
offset: font_entry.1,
key: font_entry.2,
};
let ascent_px = *rasterizer
.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
});
let mut shaper = rasterizer
.shape_ctx
.builder(font_ref)
.size(size_u16 as f32)
.build();
shaper.add_str(&rasterizer.run_str_scratch);
let mut glyphs: Vec<ShapedGlyph> = Vec::new();
shaper.shape_with(|cluster| {
let byte_offset = cluster.source.start;
for g in cluster.glyphs {
glyphs.push(ShapedGlyph {
id: g.id,
x: g.x,
y: g.y,
advance: g.advance,
cluster: byte_offset,
});
}
});
Some((glyphs, ascent_px))
}
#[allow(clippy::too_many_arguments)]
pub fn build_row_fg(
row: &Row<Square>,
cols: usize,
y: u16,
style_set: &StyleSet,
renderer: &Renderer,
term_colors: &TermColors,
rasterizer: &mut GridGlyphRasterizer,
grid: &mut GridRenderer,
size_px: f32,
cell_w: f32,
cell_h: f32,
row_sel: Option<RowSelection>,
row_hints: &[RowHint],
font_library: &FontLibrary,
fg_scratch: &mut Vec<CellText>,
) {
fg_scratch.clear();
let size_bucket = (size_px * 4.0).round().clamp(0.0, u16::MAX as f32) as u16;
let size_u16 = size_px.round().clamp(1.0, u16::MAX as f32) as u16;
let cell_w_u32 = cell_w.round().clamp(1.0, u32::MAX as f32) as u32;
let cell_h_u32 = cell_h.round().clamp(1.0, u32::MAX as f32) as u32;
let thickness = decoration_thickness(size_px);
let has_sel = row_sel.is_some();
let has_color_hints = row_hints.iter().any(|rh| rh.tag != HintTag::HyperlinkHover);
let needs_per_cell_check = has_sel || has_color_hints;
emit_underlines(
row,
cols,
y,
style_set,
renderer,
term_colors,
grid,
cell_w_u32,
cell_h_u32,
thickness,
row_sel,
row_hints,
fg_scratch,
);
let mut x: usize = 0;
while x < cols {
let sq = row[Column(x)];
if is_run_breaker(sq) {
x += 1;
continue;
}
let ch = sq.c();
let run_start_style_id = sq.style_id();
let run_style_flags =
(style_set.get(run_start_style_id).flags.bits() & SHAPING_FLAG_MASK) as u8;
let (font_id, is_emoji) =
rasterizer.resolve_font(ch, run_style_flags, font_library);
let run_start = x;
let mut prev_style_id = run_start_style_id;
let shape_ch = if ch == rio_backend::ansi::kitty_virtual::PLACEHOLDER {
' '
} else {
ch
};
#[cfg(target_os = "macos")]
{
rasterizer.run_utf16_scratch.clear();
rasterizer.run_cell_starts.clear();
rasterizer
.run_cell_starts
.push(rasterizer.run_utf16_scratch.len() as u32);
let mut buf = [0u16; 2];
rasterizer
.run_utf16_scratch
.extend_from_slice(shape_ch.encode_utf16(&mut buf));
}
#[cfg(not(target_os = "macos"))]
{
rasterizer.run_str_scratch.clear();
rasterizer.run_str_scratch.push(shape_ch);
}
let mut end = x + 1;
while end < cols {
let sq2 = row[Column(end)];
if is_run_breaker(sq2) {
break;
}
let style2_id = sq2.style_id();
if style2_id != prev_style_id {
let f = (style_set.get(style2_id).flags.bits() & SHAPING_FLAG_MASK) as u8;
if f != run_style_flags {
break;
}
prev_style_id = style2_id;
}
let ch2 = sq2.c();
let (font_id2, _) =
rasterizer.resolve_font(ch2, run_style_flags, font_library);
if font_id2 != font_id {
break;
}
let shape_ch2 = if ch2 == rio_backend::ansi::kitty_virtual::PLACEHOLDER {
' '
} else {
ch2
};
#[cfg(target_os = "macos")]
{
rasterizer
.run_cell_starts
.push(rasterizer.run_utf16_scratch.len() as u32);
let mut buf = [0u16; 2];
rasterizer
.run_utf16_scratch
.extend_from_slice(shape_ch2.encode_utf16(&mut buf));
}
#[cfg(not(target_os = "macos"))]
{
rasterizer.run_str_scratch.push(shape_ch2);
}
end += 1;
}
#[cfg(target_os = "macos")]
let run_bytes: &[u8] = {
let s = &rasterizer.run_utf16_scratch;
unsafe {
core::slice::from_raw_parts(
s.as_ptr() as *const u8,
s.len() * core::mem::size_of::<u16>(),
)
}
};
#[cfg(not(target_os = "macos"))]
let run_bytes: &[u8] = rasterizer.run_str_scratch.as_bytes();
let hash = run_hash(font_id, size_bucket, run_style_flags, run_bytes);
let ascent_px = if run_cache_get(&mut rasterizer.run_cache, hash).is_some() {
rasterizer
.ascent_cache
.get(&(font_id, size_bucket))
.copied()
.unwrap_or(0)
} else {
#[cfg(target_os = "macos")]
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 Some((glyphs, ascent_px)) = shaped_opt else {
x = end;
continue;
};
run_cache_put(&mut rasterizer.run_cache, RunCacheEntry { hash, glyphs });
ascent_px
};
let (synthetic_bold, synthetic_italic) =
rasterizer.get_synthesis(font_id, font_library);
let mut glyph_emits: SmallVec<[(u16, u16); 64]> = SmallVec::new();
{
let glyphs =
run_cache_get(&mut rasterizer.run_cache, hash).expect("just inserted");
let mut cell_idx_in_run: u16 = 0;
#[cfg(target_os = "macos")]
{
let cell_starts = &rasterizer.run_cell_starts;
for g in glyphs {
while (cell_idx_in_run as usize + 1) < cell_starts.len()
&& cell_starts[cell_idx_in_run as usize + 1] <= g.cluster
{
cell_idx_in_run = cell_idx_in_run.saturating_add(1);
}
glyph_emits.push((g.id, cell_idx_in_run));
}
}
#[cfg(not(target_os = "macos"))]
{
let mut char_cursor =
rasterizer.run_str_scratch.char_indices().peekable();
for g in glyphs {
while let Some(&(byte_offset, _)) = char_cursor.peek() {
if (byte_offset as u32) >= g.cluster {
break;
}
char_cursor.next();
cell_idx_in_run = cell_idx_in_run.saturating_add(1);
}
glyph_emits.push((g.id, cell_idx_in_run));
}
}
}
for &(glyph_id, cell_idx_in_run) in &glyph_emits {
let grid_col = (run_start as u16).saturating_add(cell_idx_in_run);
if (grid_col as usize) >= cols {
continue;
}
let Some((_, slot, is_color)) = ensure_glyph_by_id(
rasterizer,
grid,
font_id,
glyph_id,
size_bucket,
size_u16,
cell_h,
ascent_px,
is_emoji,
synthetic_italic,
synthetic_bold,
) else {
continue;
};
if slot.w == 0 || slot.h == 0 {
continue;
}
let src_col =
(run_start + cell_idx_in_run as usize).min(cols.saturating_sub(1));
let src_sq = row[Column(src_col)];
let (atlas, color) = if is_color {
(CellText::ATLAS_COLOR, [255, 255, 255, 255])
} else if !needs_per_cell_check {
(
CellText::ATLAS_GRAYSCALE,
cell_fg(src_sq, style_set, renderer, term_colors),
)
} else {
let is_sel = cell_in_row_sel(row_sel, src_col as u16);
let hint_tag = if is_sel {
None
} else {
cell_in_row_hints(row_hints, src_col as u16)
};
if is_sel {
(
CellText::ATLAS_GRAYSCALE,
cell_fg_selected(src_sq, style_set, renderer, term_colors),
)
} else if let Some(tag) = hint_tag {
(CellText::ATLAS_GRAYSCALE, cell_fg_hinted(tag, renderer))
} else {
(
CellText::ATLAS_GRAYSCALE,
cell_fg(src_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],
bearings: [slot.bearing_x, slot.bearing_y],
grid_pos: [grid_col, y],
color,
atlas,
bools: 0,
_pad: [0, 0],
});
}
x = end;
}
emit_strikethroughs(
row,
cols,
y,
style_set,
renderer,
term_colors,
grid,
cell_w_u32,
cell_h_u32,
thickness,
row_sel,
row_hints,
fg_scratch,
);
}
#[allow(clippy::too_many_arguments)]
fn emit_underlines(
row: &Row<Square>,
cols: usize,
y: u16,
style_set: &StyleSet,
renderer: &Renderer,
term_colors: &TermColors,
grid: &mut GridRenderer,
cell_w: u32,
cell_h: u32,
thickness: u32,
row_sel: Option<RowSelection>,
row_hints: &[RowHint],
fg_scratch: &mut Vec<CellText>,
) {
for x in 0..cols {
let sq = row[Column(x)];
let style = style_set.get(sq.style_id());
let col = x as u16;
let (deco, hover_force) = match underline_style_from_flags(style.flags) {
Some(d) => (d, false),
None if cell_in_hover_underline(row_hints, col) => {
(DecorationStyle::Underline, true)
}
None => continue,
};
let Some(slot) = ensure_decoration_slot(grid, deco, cell_w, cell_h, thickness)
else {
continue;
};
if slot.w == 0 || slot.h == 0 {
continue;
}
let color = if cell_in_row_sel(row_sel, col) {
cell_fg_selected(sq, style_set, renderer, term_colors)
} else if let Some(tag) = cell_in_row_hints(row_hints, col) {
cell_fg_hinted(tag, renderer)
} else if hover_force {
cell_fg(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],
bearings: [slot.bearing_x, slot.bearing_y],
grid_pos: [x as u16, y],
color,
atlas: CellText::ATLAS_GRAYSCALE,
bools: 0,
_pad: [0, 0],
});
}
}
#[allow(clippy::too_many_arguments)]
fn emit_strikethroughs(
row: &Row<Square>,
cols: usize,
y: u16,
style_set: &StyleSet,
renderer: &Renderer,
term_colors: &TermColors,
grid: &mut GridRenderer,
cell_w: u32,
cell_h: u32,
thickness: u32,
row_sel: Option<RowSelection>,
row_hints: &[RowHint],
fg_scratch: &mut Vec<CellText>,
) {
for x in 0..cols {
let sq = row[Column(x)];
let style = style_set.get(sq.style_id());
if !style.flags.contains(StyleFlags::STRIKEOUT) {
continue;
}
let Some(slot) = ensure_decoration_slot(
grid,
DecorationStyle::Strikethrough,
cell_w,
cell_h,
thickness,
) else {
continue;
};
if slot.w == 0 || slot.h == 0 {
continue;
}
let col = x as u16;
let color = if cell_in_row_sel(row_sel, col) {
cell_fg_selected(sq, style_set, renderer, term_colors)
} else if let Some(tag) = cell_in_row_hints(row_hints, col) {
cell_fg_hinted(tag, renderer)
} 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],
bearings: [slot.bearing_x, slot.bearing_y],
grid_pos: [x as u16, y],
color,
atlas: CellText::ATLAS_GRAYSCALE,
bools: 0,
_pad: [0, 0],
});
}
}
#[allow(clippy::too_many_arguments)]
fn ensure_glyph_by_id(
rasterizer: &mut GridGlyphRasterizer,
grid: &mut GridRenderer,
font_id: u32,
glyph_id: u16,
size_bucket: u16,
size_u16: u16,
cell_h: f32,
ascent_px: i16,
is_emoji: bool,
synthetic_italic: bool,
synthetic_bold: bool,
) -> Option<(GlyphKey, AtlasSlot, bool)> {
let key = GlyphKey {
font_id,
glyph_id: glyph_id as u32,
size_bucket,
};
if let Some(slot) = grid.lookup_glyph(key) {
return Some((key, slot, false));
}
if let Some(slot) = grid.lookup_glyph_color(key) {
return Some((key, slot, true));
}
let raw = rasterize_glyph_native(
rasterizer,
font_id,
glyph_id,
size_u16,
is_emoji,
synthetic_bold,
synthetic_italic,
)?;
let is_color = raw.is_color;
let bearing_y = {
let top_i16 = raw.top.clamp(i16::MIN as i32, i16::MAX as i32) as i16;
let cell_h_i16 = cell_h.round().clamp(0.0, i16::MAX as f32) as i16;
cell_h_i16.saturating_sub(ascent_px).saturating_add(top_i16)
};
let raster = RasterizedGlyph {
width: raw.width.min(u16::MAX as u32) as u16,
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,
bytes: &raw.bytes,
};
let slot = if is_color {
grid.insert_glyph_color(key, raster)?
} else {
grid.insert_glyph(key, raster)?
};
Some((key, slot, is_color))
}
struct RawGlyph {
width: u32,
height: u32,
left: i32,
top: i32,
is_color: bool,
bytes: Vec<u8>,
}
#[cfg(target_os = "macos")]
fn rasterize_glyph_native(
rasterizer: &mut GridGlyphRasterizer,
font_id: u32,
glyph_id: u16,
size_u16: u16,
is_emoji: bool,
synthetic_bold: bool,
synthetic_italic: bool,
) -> Option<RawGlyph> {
let handle = rasterizer.handle_cache.get(&font_id)?.clone();
let raw = rio_backend::sugarloaf::font::macos::rasterize_glyph(
&handle,
glyph_id,
size_u16 as f32,
is_emoji,
synthetic_italic,
synthetic_bold,
)?;
Some(RawGlyph {
width: raw.width,
height: raw.height,
left: raw.left,
top: raw.top,
is_color: raw.is_color,
bytes: raw.bytes,
})
}
#[cfg(not(target_os = "macos"))]
fn rasterize_glyph_native(
rasterizer: &mut GridGlyphRasterizer,
font_id: u32,
glyph_id: u16,
size_u16: u16,
_is_emoji: bool,
synthetic_bold: bool,
synthetic_italic: bool,
) -> Option<RawGlyph> {
use rio_backend::sugarloaf::swash::{
scale::{
image::{Content, Image as GlyphImage},
Render, Source, StrikeWith,
},
zeno::{Angle, Format, Transform},
FontRef,
};
let font_entry = rasterizer.font_data_cache.get(&font_id)?.clone();
let font_ref = FontRef {
data: font_entry.0.as_ref(),
offset: font_entry.1,
key: font_entry.2,
};
let hinting = font_library_hinting(rasterizer);
let mut scaler = rasterizer
.scale_ctx
.builder(font_ref)
.hint(hinting)
.size(size_u16 as f32)
.build();
let sources: &[Source] = &[
Source::ColorOutline(0),
Source::ColorBitmap(StrikeWith::BestFit),
Source::Outline,
];
let mut image = GlyphImage::new();
let embolden_amount = if synthetic_bold {
(size_u16 as f32 / 14.0).max(1.0)
} else {
0.0
};
let ok = Render::new(sources)
.format(Format::Alpha)
.embolden(embolden_amount)
.transform(if synthetic_italic {
Some(Transform::skew(
Angle::from_degrees(14.0),
Angle::from_degrees(0.0),
))
} else {
None
})
.render_into(&mut scaler, glyph_id, &mut image);
if !ok {
return None;
}
let is_color = image.content == Content::Color;
Some(RawGlyph {
width: image.placement.width,
height: image.placement.height,
left: image.placement.left,
top: image.placement.top,
is_color,
bytes: image.data,
})
}
#[cfg(not(target_os = "macos"))]
#[inline]
fn font_library_hinting(_r: &GridGlyphRasterizer) -> bool {
true
}