Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/docs/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@ These changes are particularly beneficial for:

- Fix: In some cases, the first typed character doesn't display until after a delay, or until another key is hit [#1098](https://github.com/raphamorim/rio/issues/1098).
- Fix: Anomalous behavior occurs with the Bookmark tab style in the new versions 0.14 and 0.13. [#1094](https://github.com/raphamorim/rio/issues/1094).
- Support private user area (NF) auto decrease of width in case of next cell is occupied.
- Replace Cascadia Code Regular with Cascadia Code NF Regular.
- Remove Symbols Nerd Font Mono font in favor of Cascadia Code NF Regular.

## 0.2.14

Expand Down
42 changes: 34 additions & 8 deletions frontends/rioterm/src/renderer/font_cache.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use lru::LruCache;
use rio_backend::sugarloaf::font::ops::FontOps;
use rio_backend::sugarloaf::font_introspector::Attributes;
use rio_backend::sugarloaf::is_private_user_area;
use std::collections::HashMap;
use std::num::NonZeroUsize;
use std::sync::Arc;
Expand All @@ -11,13 +12,21 @@ use unicode_width::UnicodeWidthChar;
/// Increased for better performance with complex terminal content
const MAX_FONT_CACHE_SIZE: usize = 8192;

/// Font cache data including PUA information
#[derive(Debug, Clone, Copy)]
pub struct FontCacheData {
pub font_id: usize,
pub width: f32,
pub is_pua: bool,
}

/// LRU cache for font metrics to prevent unbounded memory growth
/// Uses a two-tier caching strategy for better performance
pub struct FontCache {
// Hot cache for most frequently used characters (ASCII)
hot_cache: HashMap<(char, Attributes), (usize, f32)>,
hot_cache: HashMap<(char, Attributes), FontCacheData>,
// LRU cache for less frequent characters
cache: LruCache<(char, Attributes), (usize, f32)>,
cache: LruCache<(char, Attributes), FontCacheData>,
}

impl FontCache {
Expand All @@ -32,7 +41,7 @@ impl FontCache {
}

/// Get font metrics from cache with hot path optimization
pub fn get(&mut self, key: &(char, Attributes)) -> Option<&(usize, f32)> {
pub fn get(&mut self, key: &(char, Attributes)) -> Option<&FontCacheData> {
// Check hot cache first for ASCII characters
if key.0.is_ascii() {
if let Some(value) = self.hot_cache.get(key) {
Expand All @@ -52,7 +61,7 @@ impl FontCache {
}

/// Insert font metrics into cache with hot path optimization
pub fn insert(&mut self, key: (char, Attributes), value: (usize, f32)) {
pub fn insert(&mut self, key: (char, Attributes), value: FontCacheData) {
// Store ASCII characters in hot cache for faster access
if key.0.is_ascii() && self.hot_cache.len() < 128 {
self.hot_cache.insert(key, value);
Expand Down Expand Up @@ -149,7 +158,12 @@ impl FontCache {
if is_emoji {
width = 2.0;
}
self.insert(key, (font_id, width));
let is_pua = is_private_user_area(&ch);
self.insert(key, FontCacheData {
font_id,
width,
is_pua,
});
}
}
}
Expand Down Expand Up @@ -180,7 +194,11 @@ mod tests {
// Test insertion and retrieval
let attrs = Attributes::new(Stretch::NORMAL, Weight::NORMAL, Style::Normal);
let key = ('a', attrs);
let value = (1, 1.0);
let value = FontCacheData {
font_id: 1,
width: 1.0,
is_pua: false,
};

cache.insert(key, value);
assert!(!cache.is_empty());
Expand All @@ -200,7 +218,11 @@ mod tests {
for i in 0..=test_size {
let attrs = Attributes::new(Stretch::NORMAL, Weight::NORMAL, Style::Normal);
let key = (char::from_u32(i as u32 + 65).unwrap_or('A'), attrs);
let value = (i, i as f32);
let value = FontCacheData {
font_id: i,
width: i as f32,
is_pua: false,
};
cache.insert(key, value);
}

Expand All @@ -217,7 +239,11 @@ mod tests {
for i in 0..10 {
let attrs = Attributes::new(Stretch::NORMAL, Weight::NORMAL, Style::Normal);
let key = (char::from_u32(i as u32 + 65).unwrap_or('A'), attrs);
let value = (i, i as f32);
let value = FontCacheData {
font_id: i,
width: i as f32,
is_pua: false,
};
cache.insert(key, value);
}

Expand Down
104 changes: 91 additions & 13 deletions frontends/rioterm/src/renderer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ pub mod navigation;
mod search;
pub mod utils;

use font_cache::FontCache;
use font_cache::{FontCache, FontCacheData};

use crate::ansi::CursorShape;
use crate::context::renderable::{Cursor, RenderableContent};
Expand All @@ -23,8 +23,9 @@ use rio_backend::config::Config;
use rio_backend::crosswords::TermDamage;
use rio_backend::event::EventProxy;
use rio_backend::sugarloaf::{
drawable_character, Content, FragmentStyle, FragmentStyleDecoration, Graphic,
Stretch, Style, SugarCursor, Sugarloaf, UnderlineInfo, UnderlineShape, Weight,
drawable_character, is_private_user_area, Content, FragmentStyle,
FragmentStyleDecoration, Graphic, Stretch, Style, SugarCursor, Sugarloaf,
UnderlineInfo, UnderlineShape, Weight,
};
use std::collections::HashMap;
use std::ops::RangeInclusive;
Expand Down Expand Up @@ -262,19 +263,27 @@ impl Renderer {
let mut content = String::with_capacity(columns);
let mut last_char_was_space = false;
let mut last_style = FragmentStyle::default();
let mut skip_next_column = false;

// Collect all characters that need font lookups to batch them
let mut font_lookups = Vec::new();
let mut styles_and_chars = Vec::with_capacity(columns);

// First pass: collect all styles and identify font cache misses
for column in 0..columns {
// Skip this column if the previous PUA character expanded to width 2
if skip_next_column {
skip_next_column = false;
continue;
}

let square = &row.inner[column];

if square.flags.contains(Flags::WIDE_CHAR_SPACER) {
continue;
}

let is_last = column == (columns - 1);
let (mut style, square_content) =
if has_cursor && column == cursor.state.pos.col {
self.create_cursor_style(square, cursor, is_active, term_colors)
Expand Down Expand Up @@ -351,18 +360,56 @@ impl Renderer {

let has_drawable_char = style.drawable_char.is_some();
if !has_drawable_char {
if let Some((font_id, width)) =
let is_pua = if let Some(cached_data) =
self.font_cache.get(&(square_content, style.font_attrs))
{
style.font_id = *font_id;
style.width = *width;
style.font_id = cached_data.font_id;
style.width = cached_data.width;
cached_data.is_pua
} else {
// Mark this character for font lookup
font_lookups.push((
styles_and_chars.len(),
square_content,
style.font_attrs,
));
false // Default value, will be updated in batch lookup
};

// If we are a codepoint in the private use area and
// we are at the end or the next cell
// is not empty, we need to constrain rendering.
//
// We do this specifically so that Nerd Fonts can render their
// icons without overlapping with subsequent characters. But if
// the subsequent character is empty, then we allow it to use
// the full glyph size.
if is_pua {
let should_expand_width = if is_last {
// At the end of line, allow expansion
true
} else {
let next = &row.inner[column + 1];
let next_content =
if next.c == '\t' || next.flags.contains(Flags::HIDDEN) {
' '
} else {
next.c
};

// Allow expansion if next cell is empty space or wide char spacer
next_content == ' '
|| next.flags.contains(Flags::WIDE_CHAR_SPACER)
};

if should_expand_width {
style.width = 2.0;
style.should_scale = true; // Mark for scaling
// Skip the next column since this character now occupies 2 cells
if !is_last {
skip_next_column = true;
}
}
}
}

Expand All @@ -386,13 +433,44 @@ impl Renderer {
}
style.width = width;

self.font_cache
.insert((square_content, font_attrs), (style.font_id, style.width));
let is_pua = is_private_user_area(&square_content);
self.font_cache.insert(
(square_content, font_attrs),
FontCacheData {
font_id: style.font_id,
width: style.width,
is_pua,
},
);
}
}

// Second pass: render the line using the resolved styles
for (style, square_content, column) in styles_and_chars {
for (mut style, square_content, column) in styles_and_chars {
let is_last = column == (columns - 1);

// Apply PUA width constraints if needed
if !style.drawable_char.is_some() {
if let Some(cached_data) = self.font_cache.get(&(square_content, style.font_attrs)) {
if cached_data.is_pua {
if is_last {
style.width = 2.0;
} else {
let next = &row.inner[column+1];
let next_content = if next.c == '\t' || next.flags.contains(Flags::HIDDEN) {
' '
} else {
next.c
};

if next_content == ' ' || next.flags.contains(Flags::WIDE_CHAR_SPACER) {
style.width = 2.0;
}
}
}
}
}

// Handle drawable characters
if style.drawable_char.is_some() {
if !content.is_empty() {
Expand Down Expand Up @@ -451,7 +529,7 @@ impl Renderer {
}

// Render last column and break row
if column == (columns - 1) {
if is_last {
if !content.is_empty() {
if let Some(line) = line_opt {
builder.add_text_on_line(line, &content, last_style);
Expand Down Expand Up @@ -708,11 +786,11 @@ impl Renderer {

for character in active_search_content.chars() {
let mut char_style = style;
if let Some((font_id, width)) =
if let Some(cached_data) =
self.font_cache.get(&(character, style.font_attrs))
{
char_style.font_id = *font_id;
char_style.width = *width;
char_style.font_id = cached_data.font_id;
char_style.width = cached_data.width;
} else {
font_lookups.push((char_styles.len(), character));
}
Expand Down
22 changes: 22 additions & 0 deletions sugarloaf/src/components/rich_text/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,28 @@ impl RichTextBrush {
}
}

// Calculate effective font size for characters that should be scaled
let effective_font_size = if run.span.should_scale {
// Scale font size for characters marked for scaling
run.size * 1.6 // Conservative scaling to fit within 2 cells
} else {
run.size
};

// Update font session if needed
if font != current_font || effective_font_size != current_font_size {
current_font = font;
current_font_size = effective_font_size;

session = glyphs_cache.session(
image_cache,
current_font,
font_library,
font_coords,
effective_font_size,
);
}

// Handle graphics if in layout mode
if !is_dimensions_only {
if let Some(graphic) = run.span.media {
Expand Down
6 changes: 3 additions & 3 deletions sugarloaf/src/font/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ pub const DEFAULT_FONT_FAMILY: &str = "cascadiacode";
// CascadiaCode-Italic.ttf
// CascadiaCode-Light.ttf
// CascadiaCode-LightItalic.ttf
// CascadiaCode-Regular.ttf
// CascadiaCodeNF-Regular.ttf
// CascadiaCode-SemiBold.ttf
// CascadiaCode-SemiBoldItalic.ttf
// CascadiaCode-SemiLight.ttf
Expand Down Expand Up @@ -43,7 +43,7 @@ pub const FONT_CASCADIAMONO_LIGHT_ITALIC: &[u8] =
font!("./resources/CascadiaCode/CascadiaCode-LightItalic.otf");

pub const FONT_CASCADIAMONO_REGULAR: &[u8] =
font!("./resources/CascadiaCode/CascadiaCode-Regular.otf");
font!("./resources/CascadiaCode/CascadiaCodeNF-Regular.otf");

pub const FONT_CASCADIAMONO_SEMI_BOLD: &[u8] =
font!("./resources/CascadiaCode/CascadiaCode-SemiBold.otf");
Expand All @@ -58,6 +58,6 @@ 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/CascadiaCode/CascadiaCodeNF-Regular.otf");

pub const FONT_TWEMOJI_EMOJI: &[u8] = font!("./resources/Twemoji/Twemoji.Mozilla.ttf");
Binary file not shown.
Binary file not shown.
Binary file not shown.
3 changes: 3 additions & 0 deletions sugarloaf/src/layout/content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,8 @@ pub struct FragmentStyle {
pub media: Option<Graphic>,
/// Drawable character
pub drawable_char: Option<DrawableChar>,
/// Whether this character should be scaled
pub should_scale: bool,
}

impl Default for FragmentStyle {
Expand All @@ -345,6 +347,7 @@ impl Default for FragmentStyle {
decoration_color: None,
media: None,
drawable_char: None,
should_scale: false,
}
}
}
Expand Down
Loading
Loading