use cosmic_text::{
Attrs, Buffer, CacheKeyFlags, Ellipsize, EllipsizeHeightLimit, FontSystem, Metrics, Shaping,
Style as FontStyle, SwashContent, Weight, Wrap,
};
use rustc_hash::FxHashMap;
use crate::atlas::GlyphAtlas;
use crate::color::Color;
use crate::display::{Clip, ClipId, Quad, QuadKind};
use crate::geom::{Rect, Size, Vec2};
use crate::key::Key;
use crate::resources::Resources;
use crate::retain::Kept;
use crate::spec::{FontFamily, TextStyle, TextWrap, UnderlineStyle};
use crate::tree::TextId;
use crate::value::Value;
pub(crate) struct Raster {
ctx: swash::scale::ScaleContext,
axes: FxHashMap<cosmic_text::fontdb::ID, Option<crate::weights::WghtAxis>>,
pub(crate) subpixel: bool,
}
impl Raster {
fn new() -> Self {
Self {
ctx: swash::scale::ScaleContext::new(),
axes: FxHashMap::default(),
subpixel: false,
}
}
fn image(
&mut self,
fs: &mut FontSystem,
key: cosmic_text::CacheKey,
format: swash::zeno::Format,
) -> Option<cosmic_text::SwashImage> {
use swash::scale::{Render, Source, StrikeWith};
use swash::zeno::{Angle, Transform, Vector};
let font = fs.get_font(key.font_id, key.font_weight)?;
let swash_font = font.as_swash();
let axis = self
.axes
.entry(key.font_id)
.or_insert_with(|| {
let weight = fs.db().face(key.font_id).map_or(400, |face| face.weight.0);
crate::weights::WghtAxis::read(swash_font, weight)
})
.as_ref();
let size = f32::from_bits(key.font_size_bits);
let face_weight = f32::from(key.font_weight.0);
let bold = key.flags.contains(crate::weights::SYNTHETIC_BOLD);
let asked = if bold {
f32::from(Weight::BOLD.0)
} else {
face_weight
};
let at = axis.map(|axis| axis.coordinate(asked));
let embolden = bold
&& match (axis, at) {
(Some(axis), Some(at)) => at <= axis.coordinate(face_weight),
_ => true,
};
let mut scaler = self
.ctx
.builder(swash_font)
.size(size)
.hint(!key.flags.contains(CacheKeyFlags::DISABLE_HINTING));
if let Some(at) = at {
scaler = scaler.normalized_coords(
swash_font
.variations()
.normalized_coords([(swash::Tag::from_be_bytes(*b"wght"), at)]),
);
}
let mut scaler = scaler.build();
let offset = if key.flags.contains(CacheKeyFlags::PIXEL_FONT) {
Vector::new(key.x_bin.as_float().round(), key.y_bin.as_float().round())
} else {
Vector::new(key.x_bin.as_float(), key.y_bin.as_float())
};
Render::new(&[
Source::ColorOutline(0),
Source::ColorBitmap(StrikeWith::BestFit),
Source::Outline,
])
.format(format)
.offset(offset)
.embolden(if embolden {
crate::weights::embolden_strength(size)
} else {
0.0
})
.transform(
key.flags
.contains(CacheKeyFlags::FAKE_ITALIC)
.then(|| Transform::skew(Angle::from_degrees(14.0), Angle::from_degrees(0.0))),
)
.render(&mut scaler, key.glyph_id)
}
}
pub(crate) fn raster_glyph(
key: cosmic_text::CacheKey,
fs: &mut FontSystem,
raster: &mut Raster,
atlas: &mut crate::atlas::GlyphAtlas,
) -> Option<crate::atlas::GlyphSlot> {
atlas.get_or_insert(key, || {
let format = if raster.subpixel {
swash::zeno::Format::Subpixel
} else {
swash::zeno::Format::Alpha
};
let image = raster.image(fs, key, format)?;
if image.placement.width == 0 || image.placement.height == 0 {
return None;
}
let (data, is_color, subpixel) = match image.content {
SwashContent::Mask => {
let mut rgba = Vec::with_capacity(image.data.len() * 4);
for &a in image.data.iter() {
rgba.extend_from_slice(&[255, 255, 255, a]);
}
(rgba, false, false)
}
SwashContent::Color => (image.data.to_vec(), true, false),
SwashContent::SubpixelMask => {
let mut rgba = image.data.to_vec();
for px in rgba.as_chunks_mut::<4>().0 {
px[3] = px[0].max(px[1]).max(px[2]);
}
(rgba, false, true)
}
};
Some(crate::atlas::RasterGlyph {
w: image.placement.width,
h: image.placement.height,
left: image.placement.left,
top: image.placement.top,
color: is_color,
subpixel,
data,
})
})
}
pub(crate) fn glyph_kind(slot: &crate::atlas::GlyphSlot) -> QuadKind {
if slot.color_glyph {
QuadKind::GlyphColor
} else if slot.subpixel {
QuadKind::GlyphSubpixel
} else {
QuadKind::GlyphMask
}
}
pub const DEFAULT_TEXT_CACHE_BYTES: usize = 64 << 20;
const ENTRY_BASE_BYTES: usize = 4096;
const ENTRY_GLYPH_BYTES: usize = 480;
pub const LONG_LINE_BYTES: usize = 4096;
const CHUNK_BYTES: usize = 1024;
const LONG_SALT: u64 = 0x5f5f_6c6f_6e67_5f5f;
const COPY_SALT: u64 = 0x5f5f_636f_7079_5f5f;
const EVICT_TO_NUMERATOR: usize = 3;
const EVICT_TO_DENOMINATOR: usize = 4;
pub(crate) struct CachedText {
buffer: Buffer,
content: String,
wrap: Option<f32>,
intrinsic: Size,
max_lines: usize,
clamp_w: bool,
last_used: u64,
bytes: usize,
glyphs: Vec<GlyphTemplate>,
deco: Vec<DecoTemplate>,
span_deco: Vec<SpanDeco>,
glyphs_built_for: Option<(Option<u32>, u64)>,
claimed: u64,
claimed_wrap: Option<f32>,
breaks_anywhere: bool,
min_content: Option<f32>,
}
struct GlyphTemplate {
x: f32,
y: f32,
w: f32,
h: f32,
uv: [u32; 4],
kind: QuadKind,
color: Option<Color>,
byte: u32,
}
struct DecoTemplate {
x: f32,
y: f32,
w: f32,
h: f32,
color: Option<Color>,
under: bool,
style: UnderlineStyle,
radius: f32,
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct JoinBg {
pub quad: u32,
pub radius: f32,
pub outer: ClipId,
pub own: Option<(f32, f32)>,
}
#[derive(Clone, Copy, Default)]
struct SpanDeco {
underline: bool,
underline_color: Option<Color>,
underline_style: UnderlineStyle,
strikethrough: bool,
bg: Option<Color>,
bg_radius: f32,
}
impl SpanDeco {
fn of_style(style: &TextStyle) -> Self {
Self {
underline: style.underline,
underline_color: style.underline_color,
underline_style: style.underline_style,
strikethrough: style.strikethrough,
bg: None,
bg_radius: 0.0,
}
}
fn any(&self) -> bool {
self.underline || self.strikethrough || self.bg.is_some()
}
}
#[derive(Clone, Copy, Debug)]
pub struct Span<'a> {
pub text: &'a str,
pub color: Option<Color>,
pub bold: bool,
pub italic: bool,
pub underline: bool,
pub underline_color: Option<Color>,
pub underline_style: UnderlineStyle,
pub strikethrough: bool,
pub bg: Option<Color>,
pub bg_radius: f32,
pub family: Option<FontFamily>,
pub size: Option<f32>,
}
impl<'a> Span<'a> {
pub fn new(text: &'a str) -> Self {
Self {
text,
color: None,
bold: false,
italic: false,
underline: false,
underline_color: None,
underline_style: UnderlineStyle::Solid,
strikethrough: false,
bg: None,
bg_radius: 0.0,
family: None,
size: None,
}
}
pub fn family(mut self, family: FontFamily) -> Self {
self.family = Some(family);
self
}
pub fn mono(self) -> Self {
self.family(FontFamily::Mono)
}
pub fn size(mut self, px: f32) -> Self {
self.size = (px.is_finite() && px > 0.0).then_some(px);
self
}
pub fn color(mut self, c: Color) -> Self {
self.color = Some(c);
self
}
pub fn bold(mut self) -> Self {
self.bold = true;
self
}
pub fn italic(mut self) -> Self {
self.italic = true;
self
}
pub fn underline(mut self) -> Self {
self.underline = true;
self
}
pub fn underline_color(mut self, c: Color) -> Self {
self.underline = true;
self.underline_color = Some(c);
self
}
pub fn underline_style(mut self, s: UnderlineStyle) -> Self {
self.underline = true;
self.underline_style = s;
self
}
pub fn strikethrough(mut self) -> Self {
self.strikethrough = true;
self
}
pub fn bg(mut self, c: Color) -> Self {
self.bg = Some(c);
self
}
pub fn bg_radius(mut self, r: f32) -> Self {
self.bg_radius = r.max(0.0);
self
}
fn attrs_only(&self) -> SpanAttrs {
SpanAttrs {
color: self.color,
bold: self.bold,
italic: self.italic,
underline: self.underline,
underline_color: self.underline_color,
underline_style: self.underline_style,
strikethrough: self.strikethrough,
bg: self.bg,
bg_radius: self.bg_radius,
family: self.family,
size: self.size,
}
}
fn attrs<'r>(
&self,
family: cosmic_text::Family<'r>,
weights: crate::weights::Weights,
) -> Attrs<'r> {
let mut attrs = weights.apply(Attrs::new().family(family), self.bold);
if self.italic {
attrs = attrs.style(FontStyle::Italic);
}
if let Some(c) = self.color {
attrs = attrs.color(cosmic_text::Color::rgba(
(c.r * 255.0) as u8,
(c.g * 255.0) as u8,
(c.b * 255.0) as u8,
(c.a * 255.0) as u8,
));
}
attrs
}
}
#[derive(Clone, Copy)]
struct SpanAttrs {
color: Option<Color>,
bold: bool,
italic: bool,
underline: bool,
underline_color: Option<Color>,
underline_style: UnderlineStyle,
strikethrough: bool,
bg: Option<Color>,
bg_radius: f32,
family: Option<FontFamily>,
size: Option<f32>,
}
impl SpanAttrs {
fn span<'a>(&self, text: &'a str) -> Span<'a> {
Span {
text,
color: self.color,
bold: self.bold,
italic: self.italic,
underline: self.underline,
underline_color: self.underline_color,
underline_style: self.underline_style,
strikethrough: self.strikethrough,
bg: self.bg,
bg_radius: self.bg_radius,
family: self.family,
size: self.size,
}
}
}
#[derive(Clone)]
struct OwnedSpan {
start: usize,
end: usize,
attrs: SpanAttrs,
}
fn chunk_spans<'a>(
content: &'a str,
spans: &[OwnedSpan],
start: usize,
end: usize,
) -> Vec<Span<'a>> {
let first = spans.partition_point(|s| s.end <= start);
spans[first..]
.iter()
.take_while(|s| s.start < end)
.filter_map(|s| {
let (a, b) = (s.start.max(start), s.end.min(end));
(a < b).then(|| s.attrs.span(&content[a..b]))
})
.collect()
}
struct FrameText {
cache_key: u64,
color: Color,
}
pub(crate) enum Entry {
Run(CachedText),
Long(LongLine),
}
impl Entry {
pub(crate) fn content(&self) -> &str {
match self {
Entry::Run(e) => &e.content,
Entry::Long(l) => &l.content,
}
}
fn last_used(&self) -> u64 {
match self {
Entry::Run(e) => e.last_used,
Entry::Long(l) => l.last_used,
}
}
fn touch(&mut self, frame_no: u64) {
match self {
Entry::Run(e) => e.last_used = frame_no,
Entry::Long(l) => l.last_used = frame_no,
}
}
fn bytes(&self) -> usize {
match self {
Entry::Run(e) => e.bytes,
Entry::Long(l) => l.bytes,
}
}
fn run(&self) -> Option<&CachedText> {
match self {
Entry::Run(e) => Some(e),
Entry::Long(_) => None,
}
}
fn run_mut(&mut self) -> Option<&mut CachedText> {
match self {
Entry::Run(e) => Some(e),
Entry::Long(_) => None,
}
}
fn long(&self) -> Option<&LongLine> {
match self {
Entry::Long(l) => Some(l),
Entry::Run(_) => None,
}
}
fn long_mut(&mut self) -> Option<&mut LongLine> {
match self {
Entry::Long(l) => Some(l),
Entry::Run(_) => None,
}
}
}
#[derive(Clone)]
struct Chunk {
start: usize,
end: usize,
key: u64,
width: Option<f32>,
tab: Option<(f32, f32)>,
rows: Vec<RowStart>,
}
#[derive(Clone, Copy, Debug)]
struct RowStart {
byte: u32,
x: f32,
}
#[derive(Clone)]
pub(crate) struct LongLine {
content: String,
spans: Vec<OwnedSpan>,
style: TextStyle,
wrap: TextWrap,
wrap_w: Option<f32>,
starts: Vec<(u32, f32)>,
chunks: Vec<Chunk>,
prefix: Vec<f32>,
line_h: f32,
avg: f32,
last_used: u64,
claimed: u64,
laid_rows: u32,
asked_rows: u32,
bytes: usize,
}
impl LongLine {
fn chunk_at(&self, x: f32) -> usize {
match self.prefix[1..].partition_point(|&p| p <= x) {
i if i >= self.chunks.len() => self.chunks.len().saturating_sub(1),
i => i,
}
}
fn chunk_of_byte(&self, byte: usize) -> usize {
match self.chunks.partition_point(|c| c.end <= byte) {
i if i >= self.chunks.len() => self.chunks.len().saturating_sub(1),
i => i,
}
}
fn width(&self) -> f32 {
self.prefix.last().copied().unwrap_or(0.0)
}
fn rows(&self) -> u32 {
match self.starts.last() {
Some(&(r, _)) if self.wrap_w.is_some() => r + 1,
_ => 1,
}
}
fn chunks_on_rows(&self, ra: u32, rb: u32) -> Option<(usize, usize)> {
let n = self.chunks.len();
if n == 0 || self.starts.len() != n + 1 {
return None;
}
let first = self.starts[1..].partition_point(|s| s.0 < ra);
let last = self.starts[..n].partition_point(|s| s.0 <= rb);
(first < last).then_some((first, last - 1))
}
fn reprefix(&mut self) {
let mut at = 0.0f32;
self.prefix.clear();
self.prefix.push(0.0);
for c in &self.chunks {
at = match (c.width, c.tab) {
(Some(_), Some((tab_x, every))) => next_tab_stop(at + tab_x, every),
(Some(w), None) => at + w,
(None, _) => at + (c.end - c.start) as f32 * self.avg,
};
self.prefix.push(at);
}
}
fn tab_shift(&self, i: usize) -> f32 {
let c = &self.chunks[i];
match (c.width, c.tab) {
(Some(w), Some(_)) => self.prefix[i + 1] - self.prefix[i] - w,
_ => 0.0,
}
}
fn end_shift(&self, i: usize, local: usize) -> f32 {
let c = &self.chunks[i];
match local >= c.end - c.start {
true => self.tab_shift(i),
false => 0.0,
}
}
fn placed_tab_hit(&self, i: usize, x: f32) -> Option<usize> {
let c = &self.chunks[i];
let (tab_x, _) = c.tab?;
c.width?;
if x < tab_x {
return None;
}
let drawn_end = self.prefix[i + 1] - self.prefix[i];
Some(match x < (tab_x + drawn_end) / 2.0 {
true => c.end - 1,
false => c.end,
})
}
}
fn next_tab_stop(x: f32, every: f32) -> f32 {
if every <= 0.0 {
return x;
}
(((x + 1.0 / 16.0) / every).floor() + 1.0) * every
}
fn chunk_ranges(content: &str) -> Vec<(usize, usize)> {
use unicode_segmentation::UnicodeSegmentation;
if content.len() < LONG_LINE_BYTES {
return vec![(0, content.len())];
}
let bytes = content.as_bytes();
let mut out = Vec::with_capacity(content.len() / CHUNK_BYTES + 1);
let mut start = 0usize;
while start < content.len() {
let mut window_end = (start + CHUNK_BYTES).min(content.len());
while !content.is_char_boundary(window_end) {
window_end -= 1;
}
let window = &content[start..window_end];
let mut from = (CHUNK_BYTES / 2).min(window.len());
while !window.is_char_boundary(from) {
from += 1;
}
let off_stops = start > 0 && bytes[start - 1] != b'\t';
let early_tab = match off_stops {
true => window[..from].find('\t'),
false => None,
};
let end = if let Some(t) = early_tab {
start + t + 1
} else if window_end == content.len() && !(off_stops && window[from..].contains('\t')) {
window_end
} else {
let (mut first_tab, mut last_tab, mut space) = (None, None, None);
for (i, c) in window[from..].char_indices() {
if c.is_whitespace() {
let end = from + i + c.len_utf8();
space = Some(end);
if c == '\t' {
first_tab = first_tab.or(Some(end));
last_tab = Some(end);
}
}
}
let tab = if off_stops { first_tab } else { last_tab };
match tab.or(space) {
Some(i) => start + i,
None => {
let mut cut = 0usize;
for (i, _) in window.grapheme_indices(true) {
if i == 0 {
continue;
}
cut = i;
}
if cut == 0 { window_end } else { start + cut }
}
}
};
out.push((start, end));
start = end;
}
out
}
pub(crate) const PLACE_ANCESTORS: usize = 4;
pub(crate) struct Ancestry {
pub(crate) keys: [Key; PLACE_ANCESTORS],
pub(crate) depth: usize,
pub(crate) none_at: Option<usize>,
}
pub(crate) struct TextPlace {
pub(crate) key: Key,
ancestors: [Key; PLACE_ANCESTORS],
depth: u8,
none_at: u8,
cache_key: u64,
origin: Vec2,
scope: Option<Key>,
drawn: bool,
}
impl TextPlace {
fn answers_to(&self, key: Key) -> bool {
if self.key == key {
return true;
}
self.ancestors[..self.depth as usize]
.iter()
.position(|k| *k == key)
.is_some_and(|d| d <= self.none_at as usize)
}
}
pub(crate) struct ScopeRun<'a> {
pub place: &'a TextPlace,
pub text: &'a Entry,
pub base: usize,
}
impl ScopeRun<'_> {
pub(crate) fn span(&self) -> (usize, usize) {
(self.base, self.base + self.text.content().len())
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct TextHit {
pub byte: usize,
pub line: u32,
}
impl TextHit {
pub fn to_value(self) -> Value {
Value::map([
("byte", Value::Int(self.byte as i64)),
("line", Value::Int(self.line as i64)),
])
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct TextMetrics {
pub width: f32,
pub height: f32,
pub lines: u32,
}
impl TextMetrics {
pub fn to_value(self) -> Value {
Value::map([
("width", Value::float(self.width)),
("height", Value::float(self.height)),
("lines", Value::Int(self.lines as i64)),
])
}
}
pub struct TextSystem {
raster: Raster,
joins: Vec<JoinBg>,
entries: FxHashMap<u64, Entry>,
bytes: usize,
budget: usize,
frame: Kept<FrameText>,
places: Kept<TextPlace>,
scale: f32,
frame_no: u64,
owed: bool,
}
pub(crate) fn cosmic_features(f: &crate::spec::FontFeatures) -> cosmic_text::FontFeatures {
let mut out = cosmic_text::FontFeatures::new();
for (tag, value) in f.iter() {
out.set(cosmic_text::FeatureTag::new(tag), value);
}
out
}
#[cfg(target_os = "macos")]
const DEFAULT_FAMILIES: [&[&str]; 3] = [
&["Helvetica Neue", "Helvetica"],
&["Times New Roman", "Times"],
&["SF Mono", "Menlo", "Monaco"],
];
#[cfg(target_os = "windows")]
const DEFAULT_FAMILIES: [&[&str]; 3] = [
&["Segoe UI", "Arial"],
&["Times New Roman"],
&["Cascadia Mono", "Consolas", "Courier New"],
];
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
const DEFAULT_FAMILIES: [&[&str]; 3] = [
&["DejaVu Sans", "Noto Sans", "Liberation Sans", "Ubuntu"],
&["DejaVu Serif", "Noto Serif", "Liberation Serif"],
&[
"DejaVu Sans Mono",
"Noto Sans Mono",
"Liberation Mono",
"Ubuntu Mono",
],
];
fn first_installed<'a>(db: &cosmic_text::fontdb::Database, list: &[&'a str]) -> Option<&'a str> {
list.iter().copied().find(|name| {
db.faces()
.any(|face| face.families.iter().any(|(f, _)| f == name))
})
}
pub(crate) fn new_font_system() -> (FontSystem, std::sync::Arc<SystemFonts>) {
let system = system_fonts();
let fonts = font_system_over(system.locale.clone(), system.db.clone(), &[]);
(fonts, system)
}
pub(crate) fn font_system_over(
locale: String,
db: cosmic_text::fontdb::Database,
first: &[String],
) -> FontSystem {
FontSystem::new_with_locale_and_db_and_fallback(locale, db, AppFallback::new(first))
}
pub(crate) struct AppFallback {
platform: cosmic_text::PlatformFallback,
first: &'static [&'static str],
common: &'static [&'static str],
scripts: std::sync::Mutex<
rustc_hash::FxHashMap<(unicode_script::Script, String), &'static [&'static str]>,
>,
}
fn keep(names: Vec<&'static str>) -> &'static [&'static str] {
static LISTS: std::sync::Mutex<Vec<&'static [&'static str]>> =
std::sync::Mutex::new(Vec::new());
let mut lists = LISTS.lock().unwrap_or_else(|e| e.into_inner());
if let Some(&kept) = lists.iter().find(|l| ***l == names[..]) {
return kept;
}
let kept: &'static [&'static str] = Box::leak(names.into_boxed_slice());
lists.push(kept);
kept
}
fn keep_name(name: &str) -> &'static str {
static NAMES: std::sync::Mutex<Vec<&'static str>> = std::sync::Mutex::new(Vec::new());
let mut names = NAMES.lock().unwrap_or_else(|e| e.into_inner());
if let Some(&kept) = names.iter().find(|n| **n == name) {
return kept;
}
let kept: &'static str = Box::leak(name.to_string().into_boxed_str());
names.push(kept);
kept
}
impl AppFallback {
pub(crate) fn new(first: &[String]) -> Self {
use cosmic_text::Fallback;
let platform = cosmic_text::PlatformFallback;
let first = keep(first.iter().map(|n| keep_name(n)).collect());
let common = keep(
first
.iter()
.chain(platform.common_fallback())
.copied()
.collect(),
);
Self {
platform,
first,
common,
scripts: Default::default(),
}
}
}
impl cosmic_text::Fallback for AppFallback {
fn common_fallback(&self) -> &[&'static str] {
self.common
}
fn forbidden_fallback(&self) -> &[&'static str] {
self.platform.forbidden_fallback()
}
fn script_fallback(&self, script: unicode_script::Script, locale: &str) -> &[&'static str] {
let theirs = self.platform.script_fallback(script, locale);
if self.first.is_empty() {
return theirs;
}
let mut scripts = self.scripts.lock().unwrap_or_else(|e| e.into_inner());
scripts
.entry((script, locale.to_string()))
.or_insert_with(|| keep(self.first.iter().chain(theirs).copied().collect()))
}
}
pub(crate) struct SystemFonts {
locale: String,
db: cosmic_text::fontdb::Database,
pub(crate) faces: rustc_hash::FxHashSet<(std::path::PathBuf, u32)>,
}
static SYSTEM: std::sync::Mutex<Option<std::sync::Arc<SystemFonts>>> = std::sync::Mutex::new(None);
pub(crate) fn system_fonts() -> std::sync::Arc<SystemFonts> {
let mut held = SYSTEM.lock().unwrap_or_else(|e| e.into_inner());
held.get_or_insert_with(|| std::sync::Arc::new(scan_system_fonts()))
.clone()
}
pub(crate) fn rescan_system_fonts() -> std::sync::Arc<SystemFonts> {
let fresh = std::sync::Arc::new(scan_system_fonts());
*SYSTEM.lock().unwrap_or_else(|e| e.into_inner()) = Some(fresh.clone());
fresh
}
fn scan_system_fonts() -> SystemFonts {
let (locale, db) = FontSystem::new().into_locale_and_db();
SystemFonts::from_db(locale, db)
}
impl SystemFonts {
pub(crate) fn from_db(locale: String, mut db: cosmic_text::fontdb::Database) -> Self {
let all: Vec<_> = db.faces().map(|face| face.id).collect();
keep_measurable(&mut db, all);
pin_default_families(&mut db);
let faces = db.faces().filter_map(face_file).collect();
Self { locale, db, faces }
}
#[cfg(test)]
pub(crate) fn db(&self) -> &cosmic_text::fontdb::Database {
&self.db
}
#[cfg(test)]
pub(crate) fn locale(&self) -> &str {
&self.locale
}
}
pub(crate) fn face_file(face: &cosmic_text::fontdb::FaceInfo) -> Option<(std::path::PathBuf, u32)> {
use cosmic_text::fontdb::Source;
match &face.source {
Source::File(path) | Source::SharedFile(path, _) => Some((path.clone(), face.index)),
Source::Binary(_) => None,
}
}
pub(crate) fn pin_default_families(db: &mut cosmic_text::fontdb::Database) {
let [sans, serif, mono] = DEFAULT_FAMILIES;
if let Some(name) = first_installed(db, sans) {
db.set_sans_serif_family(name);
}
if let Some(name) = first_installed(db, serif) {
db.set_serif_family(name);
}
if let Some(name) = first_installed(db, mono) {
db.set_monospace_family(name);
}
}
#[cfg(test)]
fn font_system_with(locale: String, db: cosmic_text::fontdb::Database) -> FontSystem {
let system = SystemFonts::from_db(locale, db);
FontSystem::new_with_locale_and_db(system.locale, system.db)
}
pub(crate) fn measurable(data: &[u8], index: u32) -> bool {
use cosmic_text::skrifa::raw::{FontRef, TableProvider};
let Ok(font) = FontRef::from_index(data, index) else {
return false;
};
font.head()
.is_ok_and(|head| (16..=16384).contains(&head.units_per_em()))
&& font.hhea().is_ok_and(|hhea| hhea.number_of_h_metrics() > 0)
&& font.hmtx().is_ok()
}
const FILES_PER_CHECKER: usize = 64;
pub(crate) fn keep_measurable(
db: &mut cosmic_text::fontdb::Database,
ids: Vec<cosmic_text::fontdb::ID>,
) -> Vec<cosmic_text::fontdb::ID> {
use cosmic_text::fontdb::{ID, Source};
let mut by_file = FxHashMap::<&std::path::Path, Vec<ID>>::default();
let mut groups = Vec::new();
for &id in &ids {
match db.face(id).map(|face| &face.source) {
Some(Source::File(path) | Source::SharedFile(path, _)) => {
by_file.entry(path).or_default().push(id);
}
Some(Source::Binary(_)) => groups.push(vec![id]),
None => {}
}
}
groups.extend(by_file.into_values());
let db_ref = &*db;
let unmeasurable_in = |groups: &[Vec<ID>]| -> Vec<ID> {
let mut out = Vec::new();
for group in groups {
let read = db_ref.with_face_data(group[0], |data, _| {
group
.iter()
.filter(|&&id| {
db_ref
.face(id)
.is_none_or(|face| !measurable(data, face.index))
})
.copied()
.collect::<Vec<_>>()
});
out.extend(read.unwrap_or_else(|| group.clone()));
}
out
};
let checkers = std::thread::available_parallelism()
.map_or(1, |n| n.get())
.min(groups.len().div_ceil(FILES_PER_CHECKER));
let unmeasurable = if checkers <= 1 {
unmeasurable_in(&groups)
} else {
let per = groups.len().div_ceil(checkers);
std::thread::scope(|scope| {
let parts: Vec<_> = groups
.chunks(per)
.map(|part| {
let spawned = std::thread::Builder::new()
.name("kui-font-check".into())
.spawn_scoped(scope, move || unmeasurable_in(part));
(part, spawned.ok())
})
.collect();
let mut out = Vec::new();
for (part, checker) in parts {
out.extend(match checker {
Some(checker) => checker
.join()
.unwrap_or_else(|panic| std::panic::resume_unwind(panic)),
None => unmeasurable_in(part),
});
}
out
})
};
for &id in &unmeasurable {
db.remove_face(id);
}
ids.into_iter()
.filter(|id| !unmeasurable.contains(id))
.collect()
}
pub(crate) fn default_families(fs: &FontSystem) -> [&str; 3] {
let db = fs.db();
[
db.family_name(&cosmic_text::Family::SansSerif),
db.family_name(&cosmic_text::Family::Serif),
db.family_name(&cosmic_text::Family::Monospace),
]
}
impl TextSystem {
pub fn new() -> Self {
Self {
raster: Raster::new(),
joins: Vec::new(),
entries: FxHashMap::default(),
bytes: 0,
budget: DEFAULT_TEXT_CACHE_BYTES,
frame: Kept::default(),
places: Kept::default(),
scale: 1.0,
frame_no: 0,
owed: false,
}
}
pub(crate) fn take_owed(&mut self) -> bool {
std::mem::take(&mut self.owed)
}
pub(crate) fn take_joins(&mut self) -> Vec<JoinBg> {
std::mem::take(&mut self.joins)
}
pub(crate) fn raster_mut(&mut self) -> &mut Raster {
&mut self.raster
}
pub(crate) fn subpixel(&self) -> bool {
self.raster.subpixel
}
pub(crate) fn set_subpixel(&mut self, on: bool) -> bool {
let changed = self.raster.subpixel != on;
self.raster.subpixel = on;
changed
}
pub fn budget(&self) -> usize {
self.budget
}
pub fn set_budget(&mut self, bytes: usize) {
self.budget = bytes;
}
pub fn bytes(&self) -> usize {
self.bytes
}
pub fn len(&self) -> usize {
self.entries.len() - self.long_lines()
}
pub fn long_lines(&self) -> usize {
self.entries.values().filter(|e| e.long().is_some()).count()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
fn evict_to_budget(&mut self, fs: &mut FontSystem) {
if self.bytes <= self.budget {
return;
}
let floor = self.budget / EVICT_TO_DENOMINATOR * EVICT_TO_NUMERATOR;
let drawn_last_frame = self.frame_no.saturating_sub(1);
let mut order: Vec<(u64, u64)> = self
.entries
.iter()
.filter(|(_, e)| e.last_used() < drawn_last_frame)
.map(|(k, e)| (e.last_used(), *k))
.collect();
order.sort_unstable();
for (_, key) in order {
if self.bytes <= floor {
break;
}
let freed = self.entries.remove(&key).map(|e| e.bytes());
self.bytes -= freed.unwrap_or(0);
}
fs.shape_run_cache.trim(0);
}
pub(crate) fn begin_frame(
&mut self,
fs: &mut FontSystem,
scale: f32,
keep_prev: bool,
frame_no: u64,
) {
self.joins.clear();
if (scale - self.scale).abs() > f32::EPSILON {
self.entries.clear();
self.bytes = 0;
}
self.scale = scale;
self.frame.begin(keep_prev);
self.places.begin(true);
self.frame_no = frame_no;
if let Some(cutoff) = crate::retain::sweep_cutoff(self.frame_no) {
let mut freed = 0usize;
self.entries.retain(|_, e| {
let keep = e.last_used() >= cutoff;
if !keep {
freed += e.bytes();
}
keep
});
self.bytes -= freed;
fs.shape_run_cache.trim(2);
}
self.evict_to_budget(fs);
}
pub(crate) fn forget_shaped(&mut self) {
self.entries.clear();
self.bytes = 0;
}
fn insert(&mut self, key: u64, entry: Entry) {
self.bytes += entry.bytes();
if let Some(old) = self.entries.insert(key, entry) {
self.bytes -= old.bytes();
}
}
fn run(&self, key: u64) -> Option<&CachedText> {
self.entries.get(&key)?.run()
}
fn run_mut(&mut self, key: u64) -> Option<&mut CachedText> {
self.entries.get_mut(&key)?.run_mut()
}
fn long(&self, key: u64) -> Option<&LongLine> {
self.entries.get(&key)?.long()
}
fn long_mut(&mut self, key: u64) -> Option<&mut LongLine> {
self.entries.get_mut(&key)?.long_mut()
}
pub(crate) fn style_key(content: &str, style: &TextStyle, scale: f32) -> u64 {
let mut h = crate::key::mix_content(crate::key::FNV_OFFSET, content.as_bytes());
let mut mix = |bytes: &[u8]| h = crate::key::fnv(h, bytes);
mix(&style.size.to_bits().to_le_bytes());
mix(&style.line_height.to_bits().to_le_bytes());
mix(&scale.to_bits().to_le_bytes());
mix(&[style.wrap as u8, style.ellipsis as u8]);
mix(&style.max_lines.to_le_bytes());
let (tag, font) = match style.family {
FontFamily::Sans => (0u8, 0u64),
FontFamily::Serif => (1, 0),
FontFamily::Mono => (2, 0),
FontFamily::Custom(id) => (3, id.to_ffi()),
};
mix(&[tag]);
mix(&font.to_le_bytes());
for (t, v) in style.features.iter() {
mix(t);
mix(&v.to_le_bytes());
}
mix(&[
style.underline as u8,
style.strikethrough as u8,
style.underline_style as u8,
style.underline_color.is_some() as u8,
]);
if let Some(c) = style.underline_color {
mix(&c.to_hex().to_le_bytes());
}
h
}
fn intern(
&mut self,
content: &str,
style: &TextStyle,
res: &Resources,
fs: &mut FontSystem,
) -> u64 {
let key = Self::style_key(content, style, self.scale);
self.intern_keyed(key, content, style, res, fs)
}
fn intern_keyed(
&mut self,
key: u64,
content: &str,
style: &TextStyle,
res: &Resources,
fs: &mut FontSystem,
) -> u64 {
let frame_no = self.frame_no;
let scale = self.scale;
if !self.entries.contains_key(&key) {
let mut buffer = new_buffer(fs, style, scale);
buffer.set_text(
content,
&res.weights_of(style.family).apply(
Attrs::new()
.family(res.family_of(style.family))
.font_features(cosmic_features(&style.features)),
false,
),
Shaping::Advanced,
None,
);
let entry = CachedText::new(
buffer,
content.to_string(),
style,
vec![SpanDeco::of_style(style)],
fs,
frame_no,
);
self.insert(key, Entry::Run(entry));
}
self.entries
.get_mut(&key)
.expect("just inserted")
.touch(frame_no);
key
}
pub(crate) fn content(&self, id: TextId) -> &str {
let key = self.frame[id.0 as usize].cache_key;
self.entries.get(&key).map_or("", |e| e.content())
}
pub(crate) fn access_runs(
&self,
id: TextId,
src: crate::access::RunSource,
run_no: &mut usize,
out: &mut Vec<crate::access::AccessRun>,
) {
use crate::access::{RowGlyphs, push_row_runs};
let Some(ft) = self.frame.get(id.0 as usize) else {
return;
};
let line = match self.entries.get(&ft.cache_key) {
Some(Entry::Run(e)) => {
crate::access::runs_of_buffer(&e.buffer, src, run_no, out);
return;
}
Some(Entry::Long(line)) => line,
None => return,
};
let whole = [RowStart { byte: 0, x: 0.0 }];
for (i, c) in line.chunks.iter().enumerate() {
let Some(run) = c
.width
.and_then(|_| self.run(c.key))
.and_then(|e| e.buffer.layout_runs().next())
else {
continue;
};
let (rows, row0, head_x) = match (line.wrap_w, line.starts.get(i)) {
(Some(_), Some(&(row0, head_x))) if !c.rows.is_empty() => {
(&c.rows[..], row0, head_x)
}
(Some(_), _) => continue,
(None, _) => (&whole[..], 0, line.prefix[i]),
};
for (r, rs) in rows.iter().enumerate() {
let hi = rows.get(r + 1).map_or(usize::MAX, |n| n.byte as usize);
let a = run.glyphs.partition_point(|g| g.start < rs.byte as usize);
let b = run.glyphs.partition_point(|g| g.start < hi).max(a);
if a == b {
continue;
}
let last = i + 1 == line.chunks.len() && r + 1 == rows.len();
push_row_runs(
&RowGlyphs {
glyphs: &run.glyphs[a..b],
text: run.text,
line: 0,
top: (row0 as usize + r) as f32 * line.line_h,
height: line.line_h,
rtl: run.rtl,
dx: if r == 0 { head_x } else { 0.0 } - rs.x,
base: c.start,
newline: last && src.newline_after_last,
},
&src,
run_no,
out,
);
}
}
}
pub(crate) fn prev_frame_text(&self, id: TextId) -> (u64, Color) {
match self.frame.prev().get(id.0 as usize) {
Some(t) => (t.cache_key, t.color),
None => (0, Color::TRANSPARENT),
}
}
pub(crate) fn readd(&mut self, cache_key: u64, color: Color) -> Option<TextId> {
let frame_no = self.frame_no;
let e = self.entries.get_mut(&cache_key)?;
e.touch(frame_no);
let cache_key = if e.long().is_some() {
self.claim_long(cache_key)
} else {
cache_key
};
self.frame.push(FrameText { cache_key, color });
Some(TextId((self.frame.len() - 1) as u32))
}
pub fn add(
&mut self,
content: &str,
style: &TextStyle,
res: &Resources,
fs: &mut FontSystem,
) -> TextId {
let key = self.intern_any(content, style, res, fs);
let key = if could_be_long(content.len(), style) {
self.claim_long(key)
} else {
key
};
self.frame.push(FrameText {
cache_key: key,
color: style.color_or_default(),
});
TextId((self.frame.len() - 1) as u32)
}
#[inline(never)]
fn claim_long(&mut self, key: u64) -> u64 {
let frame_no = self.frame_no;
let mut at = key;
let mut n = 0u64;
loop {
match self.entries.get_mut(&at) {
Some(Entry::Long(l)) if l.claimed != frame_no => {
l.claimed = frame_no;
l.last_used = frame_no;
return at;
}
Some(Entry::Run(_)) if n == 0 => return key,
Some(_) => {}
None => {
let Some(line) = self.long(key) else {
return key;
};
let mut copy = line.clone();
copy.claimed = frame_no;
copy.last_used = frame_no;
self.insert(at, Entry::Long(copy));
return at;
}
}
n += 1;
at = crate::key::fnv(key ^ COPY_SALT, &n.to_le_bytes());
}
}
fn intern_any(
&mut self,
content: &str,
style: &TextStyle,
res: &Resources,
fs: &mut FontSystem,
) -> u64 {
if !could_be_long(content.len(), style) {
return self.intern(content, style, res, fs);
}
let key = Self::style_key(content, style, self.scale);
let long_key = key ^ LONG_SALT;
let frame_no = self.frame_no;
if let Some(e) = self.entries.get_mut(&long_key) {
e.touch(frame_no);
return long_key;
}
if let Some(e) = self.entries.get_mut(&key) {
e.touch(frame_no);
return key;
}
if has_line_break(content) || content.len() < LONG_LINE_BYTES && has_rtl(content) {
self.intern_keyed(key, content, style, res, fs)
} else {
self.build_long(long_key, content.to_string(), Vec::new(), style, res, fs)
}
}
fn intern_rich_any(
&mut self,
spans: &[Span<'_>],
base: &TextStyle,
res: &Resources,
fs: &mut FontSystem,
) -> u64 {
let len: usize = spans.iter().map(|s| s.text.len()).sum();
if !could_be_long(len, base) {
return self.intern_rich(spans, base, res, fs);
}
let key = Self::rich_key(spans, base, self.scale);
let long_key = key ^ LONG_SALT;
let frame_no = self.frame_no;
if let Some(e) = self.entries.get_mut(&long_key) {
e.touch(frame_no);
return long_key;
}
if let Some(e) = self.entries.get_mut(&key) {
e.touch(frame_no);
return key;
}
if spans
.iter()
.any(|s| has_line_break(s.text) || s.size.is_some())
|| len < LONG_LINE_BYTES && spans.iter().any(|s| has_rtl(s.text))
{
return self.intern_rich_keyed(key, spans, base, res, fs);
}
let mut content = String::with_capacity(len);
let mut owned = Vec::with_capacity(spans.len());
for s in spans {
let start = content.len();
content.push_str(s.text);
owned.push(OwnedSpan {
start,
end: content.len(),
attrs: s.attrs_only(),
});
}
self.build_long(long_key, content, owned, base, res, fs)
}
fn build_long(
&mut self,
key: u64,
content: String,
spans: Vec<OwnedSpan>,
style: &TextStyle,
res: &Resources,
fs: &mut FontSystem,
) -> u64 {
let frame_no = self.frame_no;
let wrap = style.wrap;
let style = &TextStyle {
wrap: TextWrap::None,
..*style
};
let scale = self.scale;
let chunks: Vec<Chunk> = chunk_ranges(&content)
.into_iter()
.map(|(start, end)| Chunk {
start,
end,
key: if spans.is_empty() {
Self::style_key(&content[start..end], style, scale)
} else {
Self::rich_key(&chunk_spans(&content, &spans, start, end), style, scale)
},
width: None,
tab: None,
rows: Vec::new(),
})
.collect();
let (w0, line_h) = match chunks.first() {
Some(c) => {
let k = self.shape_chunk(&content, &spans, c.start, c.end, style, res, fs);
let e = self.run(k).expect("just interned");
(e.intrinsic.w, e.buffer.metrics().line_height)
}
None => (0.0, style.line_height * self.scale),
};
let avg = chunks
.first()
.map_or(0.0, |c| w0 / (c.end - c.start).max(1) as f32);
let mut line = LongLine {
content,
spans,
style: *style,
wrap,
wrap_w: None,
starts: Vec::new(),
chunks,
prefix: Vec::new(),
line_h,
avg,
last_used: frame_no,
claimed: u64::MAX,
laid_rows: 1,
asked_rows: u32::MAX,
bytes: 0,
};
if let Some(c) = line.chunks.first_mut() {
c.width = Some(w0);
}
line.reprefix();
line.bytes = ENTRY_BASE_BYTES
+ line.content.len()
+ line.chunks.len() * 48
+ line.spans.len() * std::mem::size_of::<OwnedSpan>();
self.insert(key, Entry::Long(line));
key
}
#[allow(clippy::too_many_arguments)]
fn shape_chunk(
&mut self,
content: &str,
spans: &[OwnedSpan],
start: usize,
end: usize,
style: &TextStyle,
res: &Resources,
fs: &mut FontSystem,
) -> u64 {
if spans.is_empty() {
self.intern(&content[start..end], style, res, fs)
} else {
let spans = chunk_spans(content, spans, start, end);
self.intern_rich(&spans, style, res, fs)
}
}
fn ensure_chunk(&mut self, key: u64, i: usize, res: &Resources, fs: &mut FontSystem) -> bool {
let (chunk_key, known) = {
let c = &self.long(key).expect("a long line").chunks[i];
(c.key, c.width)
};
let frame_no = self.frame_no;
if known.is_some()
&& let Some(e) = self.run_mut(chunk_key)
{
e.last_used = frame_no;
return false;
}
let (text, spans, style, off_stops) = {
let line = self.long(key).expect("a long line");
let c = &line.chunks[i];
let first = line.spans.partition_point(|s| s.end <= c.start);
let spans: Vec<OwnedSpan> = line.spans[first..]
.iter()
.take_while(|s| s.start < c.end)
.map(|s| OwnedSpan {
start: s.start.max(c.start) - c.start,
end: s.end.min(c.end) - c.start,
attrs: s.attrs,
})
.collect();
let bytes = line.content.as_bytes();
let off_stops = c.start > 0 && bytes[c.start - 1] != b'\t';
(
line.content[c.start..c.end].to_string(),
spans,
line.style,
off_stops,
)
};
let k = self.shape_chunk(&text, &spans, 0, text.len(), &style, res, fs);
let w = self.run(k).expect("just interned").intrinsic.w;
let tab = match off_stops && text.ends_with('\t') {
true => self.chunk_tab(k, &text, &spans, &style, res, fs),
false => None,
};
let line = self.long_mut(key).expect("just read");
line.chunks[i].key = k;
line.chunks[i].tab = tab;
if line.chunks[i].width != Some(w) {
line.chunks[i].width = Some(w);
line.reprefix();
}
true
}
#[inline(never)]
#[allow(clippy::too_many_arguments)]
fn chunk_tab(
&mut self,
chunk: u64,
text: &str,
spans: &[OwnedSpan],
style: &TextStyle,
res: &Resources,
fs: &mut FontSystem,
) -> Option<(f32, f32)> {
let at = text.len() - 1;
let tab_x = {
let run = self.run(chunk)?.buffer.layout_runs().next()?;
if run.rtl {
return None;
}
run.glyphs.iter().rev().find(|g| g.start == at)?.x
};
let alone = self.shape_chunk(text, spans, at, text.len(), style, res, fs);
let every = self.run(alone)?.intrinsic.w;
Some((tab_x, every))
}
fn relayout_long(&mut self, key: u64, w: Option<f32>) {
let Some(w) = w else {
let line = self.long_mut(key).expect("a long line");
line.wrap_w = None;
line.starts.clear();
for c in &mut line.chunks {
c.rows.clear();
}
return;
};
let w = w.max(1.0);
let line = self.long(key).expect("a long line");
let mut starts = Vec::with_capacity(line.chunks.len() + 1);
starts.push((0, 0.0));
let (mut row, mut x) = (0u32, 0.0f32);
let rows: Vec<Vec<RowStart>> = line
.chunks
.iter()
.enumerate()
.map(|(i, c)| {
let shaped = c.width.and_then(|_| self.run(c.key));
let rows = match shaped {
Some(e) => {
let text = &line.content[c.start..c.end];
let (rows, end) = break_rows(&e.buffer, text, line.wrap, w, x);
row += rows.len() as u32 - 1;
x = end + line.tab_shift(i);
rows
}
None => {
let est = x + c
.width
.unwrap_or_else(|| (c.end - c.start) as f32 * line.avg);
let added = (est / w).floor();
row += added as u32;
x = est - added * w;
Vec::new()
}
};
starts.push((row, x));
rows
})
.collect();
let line = self.long_mut(key).expect("a long line");
line.wrap_w = Some(w);
line.starts = starts;
for (c, rows) in line.chunks.iter_mut().zip(rows) {
c.rows = rows;
}
}
fn long_size(&mut self, key: u64, max_w: Option<f32>) -> (Size, u32) {
let (wrap, width, line_h, cur) = {
let l = self.long(key).expect("a long line");
(l.wrap, l.width(), l.line_h, l.wrap_w)
};
let target = match max_w {
Some(w) if wrap != TextWrap::None && width > w + 0.5 => Some(w.max(1.0)),
_ => None,
};
let differs = match (cur, target) {
(None, None) => false,
(Some(a), Some(b)) => (a - b).abs() > 0.5,
_ => true,
};
if differs {
self.relayout_long(key, target);
}
match target {
Some(w) => {
let rows = self.long(key).expect("a long line").rows();
(Size::new(w, rows as f32 * line_h), rows)
}
None => (Size::new(max_w.map_or(width, |m| width.min(m)), line_h), 1),
}
}
pub fn measure(
&mut self,
content: &str,
style: &TextStyle,
res: &Resources,
fs: &mut FontSystem,
max_w: Option<f32>,
) -> TextMetrics {
let key = self.intern_any(content, style, res, fs);
self.measure_any(key, fs, max_w)
}
fn measure_any(&mut self, key: u64, fs: &mut FontSystem, max_w: Option<f32>) -> TextMetrics {
if self.long(key).is_some() {
let scale = self.scale;
let (size, lines) = match max_w {
Some(m) => {
let held = self.long(key).expect("checked").wrap_w;
let measured = self.long_size(key, Some(m * scale));
if wrap_differs(held, self.long(key).expect("checked").wrap_w) {
self.relayout_long(key, held);
}
measured
}
None => {
let line = self.long(key).expect("checked");
(Size::new(line.width(), line.line_h), 1)
}
};
return TextMetrics {
width: size.w / scale,
height: size.h / scale,
lines,
};
}
self.measure_key(key, fs, max_w)
}
pub fn measure_rich(
&mut self,
spans: &[Span<'_>],
base: &TextStyle,
res: &Resources,
fs: &mut FontSystem,
max_w: Option<f32>,
) -> TextMetrics {
let key = self.intern_rich_any(spans, base, res, fs);
self.measure_any(key, fs, max_w)
}
fn measure_key(&mut self, key: u64, fs: &mut FontSystem, max_w: Option<f32>) -> TextMetrics {
let scale = self.scale;
let entry = self.run_mut(key).expect("just interned");
let target = wrap_target(entry, max_w, scale);
let key = if entry.claimed != u64::MAX && wrap_differs(entry.wrap, target) {
self.wrap_copy(key, target, fs)
} else {
key
};
let entry = self.run_mut(key).expect("just interned");
wrap_entry(entry, fs, target);
let (mut m, lines) = measure_buffer(&entry.buffer, entry.max_lines);
if entry.clamp_w
&& let Some(t) = target
{
m.w = m.w.min(t);
}
TextMetrics {
width: m.w / scale,
height: m.h / scale,
lines,
}
}
pub fn add_rich(
&mut self,
spans: &[Span<'_>],
base: &TextStyle,
res: &Resources,
fs: &mut FontSystem,
) -> TextId {
let key = self.intern_rich_any(spans, base, res, fs);
let len = spans.iter().map(|s| s.text.len()).sum();
let key = if could_be_long(len, base) {
self.claim_long(key)
} else {
key
};
self.frame.push(FrameText {
cache_key: key,
color: base.color_or_default(),
});
TextId((self.frame.len() - 1) as u32)
}
fn intern_rich(
&mut self,
spans: &[Span<'_>],
base: &TextStyle,
res: &Resources,
fs: &mut FontSystem,
) -> u64 {
let key = Self::rich_key(spans, base, self.scale);
self.intern_rich_keyed(key, spans, base, res, fs)
}
fn rich_key(spans: &[Span<'_>], base: &TextStyle, scale: f32) -> u64 {
let mut key = Self::style_key("", base, scale) ^ 0x9e37_79b9_7f4a_7c15;
for s in spans {
key = crate::key::mix_content(key, s.text.as_bytes());
let mut mix = |bytes: &[u8]| key = crate::key::fnv(key, bytes);
mix(&[
s.bold as u8,
s.italic as u8,
s.color.is_some() as u8,
s.underline as u8,
s.strikethrough as u8,
s.bg.is_some() as u8,
s.underline_style as u8,
s.underline_color.is_some() as u8,
]);
mix(&s.bg_radius.to_bits().to_le_bytes());
let (tag, font) = match s.family {
None => (0u8, 0u64),
Some(FontFamily::Sans) => (1, 0),
Some(FontFamily::Serif) => (2, 0),
Some(FontFamily::Mono) => (3, 0),
Some(FontFamily::Custom(id)) => (4, id.to_ffi()),
};
mix(&[tag]);
mix(&font.to_le_bytes());
mix(&s.size.map_or(0, f32::to_bits).to_le_bytes());
for c in [s.color, s.bg, s.underline_color].into_iter().flatten() {
mix(&c.r.to_bits().to_le_bytes());
mix(&c.g.to_bits().to_le_bytes());
mix(&c.b.to_bits().to_le_bytes());
mix(&c.a.to_bits().to_le_bytes());
}
}
key
}
fn intern_rich_keyed(
&mut self,
key: u64,
spans: &[Span<'_>],
base: &TextStyle,
res: &Resources,
fs: &mut FontSystem,
) -> u64 {
let frame_no = self.frame_no;
let scale = self.scale;
if !self.entries.contains_key(&key) {
let mut buffer = new_buffer(fs, base, scale);
let family = res.family_of(base.family);
let weights = res.weights_of(base.family);
let features = cosmic_features(&base.features);
let sized = spans.iter().any(|s| s.size.is_some());
let metrics_of = |size: Option<f32>| {
let size = size.unwrap_or(base.size);
let ratio = if base.size > 0.0 {
base.line_height / base.size
} else {
1.0
};
shaper_metrics(size * scale, size * ratio * scale)
};
buffer.set_rich_text(
spans.iter().enumerate().map(|(i, s)| {
let (family, weights) = match s.family {
Some(f) => (res.family_of(f), res.weights_of(f)),
None => (family, weights),
};
let mut attrs = s
.attrs(family, weights)
.font_features(features.clone())
.metadata(i);
if sized {
attrs = attrs.metrics(metrics_of(s.size));
}
(s.text, attrs)
}),
&{
let attrs = weights.apply(
Attrs::new().family(family).font_features(features.clone()),
false,
);
if sized {
attrs.metrics(metrics_of(None))
} else {
attrs
}
},
Shaping::Advanced,
None,
);
let content = spans.iter().map(|s| s.text).collect::<String>();
let decos = spans
.iter()
.map(|s| SpanDeco {
underline: s.underline || base.underline,
underline_color: s.underline_color.or(base.underline_color),
underline_style: if s.underline {
s.underline_style
} else {
base.underline_style
},
strikethrough: s.strikethrough || base.strikethrough,
bg: s.bg,
bg_radius: s.bg_radius,
})
.collect();
let entry = CachedText::new(buffer, content, base, decos, fs, frame_no);
self.insert(key, Entry::Run(entry));
}
self.entries
.get_mut(&key)
.expect("just inserted")
.touch(frame_no);
key
}
fn entry_mut(&mut self, id: TextId) -> &mut CachedText {
let key = self.frame[id.0 as usize].cache_key;
self.run_mut(key).expect("frame text missing from cache")
}
fn ensure_wrap(&mut self, id: TextId, max_w_logical: f32, fs: &mut FontSystem) {
let (scale, frame_no) = (self.scale, self.frame_no);
let entry = self.entry_mut(id);
let target = wrap_target(entry, Some(max_w_logical), scale);
if entry.claimed == frame_no && wrap_differs(entry.claimed_wrap, target) {
self.own_wrap(id, target, fs);
return;
}
entry.claimed = frame_no;
entry.claimed_wrap = target;
wrap_entry(entry, fs, target);
}
#[inline(never)]
fn own_wrap(&mut self, id: TextId, target: Option<f32>, fs: &mut FontSystem) {
let frame_no = self.frame_no;
let key = self.frame[id.0 as usize].cache_key;
let copy = self.wrap_copy(key, target, fs);
self.frame[id.0 as usize].cache_key = copy;
let entry = self.run_mut(copy).expect("just made");
entry.claimed = frame_no;
entry.claimed_wrap = target;
}
#[inline(never)]
fn wrap_copy(&mut self, key: u64, target: Option<f32>, fs: &mut FontSystem) -> u64 {
let frame_no = self.frame_no;
let slot = target.map_or(u64::MAX, |t| (t * 2.0).round() as u64);
let copy = crate::key::fnv(key ^ COPY_SALT, &slot.to_le_bytes());
if self.run(copy).is_none() {
let fork = self.run(key).expect("a run").fork(frame_no);
self.insert(copy, Entry::Run(fork));
}
let entry = self.run_mut(copy).expect("just made");
entry.last_used = frame_no;
wrap_entry(entry, fs, target);
copy
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn emit(
&mut self,
id: TextId,
origin: Vec2,
node: Size,
clip: Clip,
clip_id: ClipId,
clips: &mut Vec<Clip>,
res: &Resources,
fs: &mut FontSystem,
atlas: &mut GlyphAtlas,
out: &mut Vec<Quad>,
sel: Option<((usize, usize), Color)>,
) {
let from = self.joins.len();
self.emit_text(
id, origin, node, clip, clip_id, clips, res, fs, atlas, out, sel,
);
if self.joins.len() == from {
return;
}
let key = self.frame[id.0 as usize].cache_key;
let clamps = match self.entries.get(&key) {
Some(Entry::Long(l)) => l.wrap == TextWrap::None,
Some(Entry::Run(e)) => e.clamp_w,
None => false,
};
let own = clamps.then(|| {
let ox = crate::geom::snap_px(origin.x * self.scale);
(ox, ox + (node.w * self.scale).ceil())
});
for j in &mut self.joins[from..] {
j.outer = clip_id;
j.own = own;
}
}
#[allow(clippy::too_many_arguments)]
#[inline(never)]
fn emit_long(
&mut self,
key: u64,
node: Size,
ox: f32,
oy: f32,
nudge: Vec2,
color: Color,
clip: Clip,
clip_id: ClipId,
clips: &mut Vec<Clip>,
res: &Resources,
fs: &mut FontSystem,
atlas: &mut GlyphAtlas,
out: &mut Vec<Quad>,
sel: Option<((usize, usize), Color)>,
) {
let scale = self.scale;
let line = self.long(key).expect("a long line");
let (clip, clip_id) = if line.wrap == TextWrap::None {
let own = Rect::new(ox, oy, (node.w * scale).ceil(), (node.h * scale).ceil());
let clip = clip.intersect(own, crate::display::SQUARE);
if clip.rect.w <= 0.0 || clip.rect.h <= 0.0 {
return;
}
(clip, crate::display::intern_clip(clips, clip))
} else {
(clip, clip_id)
};
if let Some(((from, to), tint)) = sel {
let rects = self.long_highlight(line, from, to);
push_highlight(&rects, ox, oy, nudge, tint, clip_id, out);
}
if let Some(w) = line.wrap_w {
self.emit_long_rows(
key, w, ox, oy, nudge, color, clip, clip_id, res, fs, atlas, out,
);
return;
}
let (first, last) = {
if line.chunks.is_empty() {
return;
}
let a = line.chunk_at(clip.rect.x - ox).saturating_sub(1);
let b = (line.chunk_at(clip.rect.x + clip.rect.w - ox) + 1).min(line.chunks.len() - 1);
(a, b)
};
for i in first..=last {
self.ensure_chunk(key, i, res, fs);
}
let frame_no = self.frame_no;
self.long_mut(key).expect("checked").last_used = frame_no;
for i in first..=last {
let (chunk_key, x) = {
let line = self.long(key).expect("checked");
(line.chunks[i].key, line.prefix[i])
};
let raster = &mut self.raster;
let entry = self
.entries
.get_mut(&chunk_key)
.and_then(Entry::run_mut)
.expect("just ensured");
emit_entry(
entry,
ox + x,
oy,
nudge,
color,
clip,
clip_id,
raster,
fs,
atlas,
out,
&mut self.joins,
);
}
}
#[allow(clippy::too_many_arguments)]
fn emit_text(
&mut self,
id: TextId,
origin: Vec2,
node: Size,
clip: Clip,
clip_id: ClipId,
clips: &mut Vec<Clip>,
res: &Resources,
fs: &mut FontSystem,
atlas: &mut GlyphAtlas,
out: &mut Vec<Quad>,
sel: Option<((usize, usize), Color)>,
) {
let FrameText {
cache_key: key,
color,
} = self.frame[id.0 as usize];
let scale = self.scale;
let ox = crate::geom::snap_px(origin.x * scale);
let oy = crate::geom::snap_px(origin.y * scale);
let nudge = Vec2::new(origin.x * scale - ox, origin.y * scale - oy);
if self.long(key).is_some() {
self.emit_long(
key, node, ox, oy, nudge, color, clip, clip_id, clips, res, fs, atlas, out, sel,
);
return;
}
self.ensure_wrap(id, node.w, fs);
let raster = &mut self.raster;
let entry = self
.entries
.get_mut(&key)
.and_then(Entry::run_mut)
.expect("frame text missing from cache");
let (clip, clip_id) = if entry.clamp_w {
let own = Rect::new(ox, oy, (node.w * scale).ceil(), (node.h * scale).ceil());
let clip = clip.intersect(own, crate::display::SQUARE);
(clip, crate::display::intern_clip(clips, clip))
} else {
(clip, clip_id)
};
if let Some(((from, to), tint)) = sel {
let rects = Self::run_highlight(entry, from, to);
push_highlight(&rects, ox, oy, nudge, tint, clip_id, out);
}
emit_entry(
entry,
ox,
oy,
nudge,
color,
clip,
clip_id,
raster,
fs,
atlas,
out,
&mut self.joins,
);
}
}
fn on_pixels(x: f32, y: f32, w: f32, h: f32, nudge: Vec2) -> Rect {
Rect::new(x + nudge.x, y + nudge.y, w, h).on_pixels()
}
fn push_highlight(
rects: &[Rect],
ox: f32,
oy: f32,
nudge: Vec2,
tint: Color,
clip: ClipId,
out: &mut Vec<Quad>,
) {
for r in rects {
out.push(Quad {
rect: on_pixels(ox + r.x, oy + r.y, r.w, r.h, nudge),
color: tint,
border_color: Color::TRANSPARENT,
radius: [0.0; 4],
border_w: 0.0,
blur: 0.0,
kind: QuadKind::Solid,
uv: [0; 4],
clip,
});
}
}
impl TextSystem {
#[allow(clippy::too_many_arguments)]
fn emit_long_rows(
&mut self,
key: u64,
w: f32,
ox: f32,
oy: f32,
nudge: Vec2,
color: Color,
clip: Clip,
clip_id: ClipId,
res: &Resources,
fs: &mut FontSystem,
atlas: &mut GlyphAtlas,
out: &mut Vec<Quad>,
) {
let (first, last) = {
let line = self.long(key).expect("a long line");
let ra = ((clip.rect.y - oy) / line.line_h).floor().max(0.0) as u32;
let rb = ((clip.rect.y + clip.rect.h - oy) / line.line_h)
.floor()
.max(0.0) as u32;
let Some((a, b)) = line.chunks_on_rows(ra, rb) else {
return;
};
(a.saturating_sub(1), (b + 1).min(line.chunks.len() - 1))
};
let mut fresh = false;
for i in first..=last {
fresh |= self.ensure_chunk(key, i, res, fs);
}
if fresh {
self.relayout_long(key, Some(w));
let line = self.long_mut(key).expect("checked");
let rows = line.rows();
if rows != line.laid_rows && rows != line.asked_rows {
line.asked_rows = rows;
self.owed = true;
}
}
let frame_no = self.frame_no;
self.long_mut(key).expect("checked").last_used = frame_no;
for i in first..=last {
let chunk_key = self.long(key).expect("checked").chunks[i].key;
let [Some(Entry::Long(line)), Some(Entry::Run(entry))] =
self.entries.get_disjoint_mut([&key, &chunk_key])
else {
continue;
};
let (row0, head_x) = line.starts[i];
let chunk = &line.chunks[i];
if chunk.rows.is_empty() {
continue;
}
let raster = &mut self.raster;
emit_entry_rows(
entry,
ox,
oy + row0 as f32 * line.line_h,
nudge,
head_x,
&chunk.rows,
line.line_h,
color,
clip,
clip_id,
raster,
fs,
atlas,
out,
&mut self.joins,
);
}
}
}
#[allow(clippy::too_many_arguments)]
fn emit_entry_rows(
entry: &mut CachedText,
ox: f32,
oy: f32,
nudge: Vec2,
head_x: f32,
rows: &[RowStart],
line_h: f32,
color: Color,
clip: Clip,
clip_id: ClipId,
raster: &mut Raster,
fs: &mut FontSystem,
atlas: &mut GlyphAtlas,
out: &mut Vec<Quad>,
joins: &mut Vec<JoinBg>,
) {
build_templates(entry, raster, fs, atlas);
let mut row_pieces = |d: &DecoTemplate, out: &mut Vec<Quad>| {
let (x0, x1) = (d.x, d.x + d.w);
for r in 0..rows.len() {
let ra = rows[r].x;
let rb = rows.get(r + 1).map_or(f32::INFINITY, |n| n.x);
let (a, b) = (x0.max(ra), x1.min(rb));
if b <= a {
continue;
}
let dx = if r == 0 { head_x } else { 0.0 } - ra;
let x = ox + a + dx;
let y = oy + d.y + r as f32 * line_h;
if y >= clip.rect.y + clip.rect.h {
break;
}
if y + d.h <= clip.rect.y
|| x >= clip.rect.x + clip.rect.w
|| x + (b - a) <= clip.rect.x
{
continue;
}
let quad = Quad {
rect: if d.under {
on_pixels(x, y, b - a, d.h, nudge)
} else {
Rect::new(x, y, b - a, d.h)
},
color: d.color.unwrap_or(color),
border_color: Color::TRANSPARENT,
radius: [0.0; 4],
border_w: 0.0,
blur: 0.0,
kind: QuadKind::Solid,
clip: clip_id,
uv: [0; 4],
};
if d.under && d.radius > 0.0 {
joins.push(JoinBg {
quad: out.len() as u32,
radius: d.radius,
outer: clip_id,
own: None,
});
}
if d.under || d.style == UnderlineStyle::Solid {
out.push(quad);
} else {
crate::deco::push_line(
out,
d.style,
x,
y,
b - a,
d.h,
d.color.unwrap_or(color),
clip_id,
);
}
}
};
for d in entry.deco.iter().filter(|d| d.under) {
row_pieces(d, out);
}
let mut r = 0usize;
for g in &entry.glyphs {
while r + 1 < rows.len() && g.byte >= rows[r + 1].byte {
r += 1;
}
let dx = if r == 0 { head_x } else { 0.0 } - rows[r].x;
let x = ox + g.x + dx;
let y = oy + g.y + r as f32 * line_h;
if y >= clip.rect.y + clip.rect.h {
break;
}
if y + g.h <= clip.rect.y || x >= clip.rect.x + clip.rect.w || x + g.w <= clip.rect.x {
continue;
}
out.push(Quad {
rect: Rect::new(x, y, g.w, g.h),
color: g.color.unwrap_or(color),
border_color: Color::TRANSPARENT,
radius: [0.0; 4],
border_w: 0.0,
blur: 0.0,
kind: g.kind,
clip: clip_id,
uv: g.uv,
});
}
for d in entry.deco.iter().filter(|d| !d.under) {
row_pieces(d, out);
}
}
fn break_rows(
buffer: &Buffer,
text: &str,
wrap: TextWrap,
w: f32,
head_x: f32,
) -> (Vec<RowStart>, f32) {
let mut rows = vec![RowStart { byte: 0, x: 0.0 }];
let Some(run) = buffer.layout_runs().next() else {
return (rows, head_x);
};
let glyphs = run.glyphs;
let blank = |g: &cosmic_text::LayoutGlyph| {
text.get(g.start..g.end)
.is_some_and(|s| s.chars().all(char::is_whitespace))
};
let spaces = wrap == TextWrap::BreakSpaces;
let mut pieces: Vec<(usize, usize)> = Vec::new();
match wrap {
TextWrap::Word | TextWrap::BreakSpaces => {
let mut at = 0usize;
let mut piece = |from: usize, to: usize| {
if !spaces {
pieces.push((from, to));
return;
}
let ink = text[from..to].trim_end_matches(char::is_whitespace).len();
if ink > 0 {
pieces.push((from, from + ink));
}
for (i, c) in text[from + ink..to].char_indices() {
let at = from + ink + i;
pieces.push((at, at + c.len_utf8()));
}
};
for (i, _) in unicode_linebreak::linebreaks(text) {
if i > at {
piece(at, i);
at = i;
}
}
if at < text.len() {
piece(at, text.len());
}
}
TextWrap::Glyph | TextWrap::None => {
pieces.extend(glyphs.iter().map(|g| (g.start, g.end)));
}
}
let mut row_x0 = 0.0f32;
let mut avail = w - head_x;
let mut gi = 0usize;
for (s, e) in pieces {
let g0 = gi;
while gi < glyphs.len() && glyphs[gi].start < e {
gi += 1;
}
if g0 == gi {
continue;
}
let piece = &glyphs[g0..gi];
let x0 = piece[0].x;
let ink_end = piece
.iter()
.rev()
.find(|g| spaces || !blank(g))
.map_or(x0, |g| g.x + g.w);
let opens = x0 > row_x0 || (rows.len() == 1 && head_x > 0.0);
if ink_end - row_x0 > avail && opens {
rows.push(RowStart {
byte: s as u32,
x: x0,
});
row_x0 = x0;
avail = w;
}
if ink_end - row_x0 > avail {
for g in piece {
if g.x + g.w - row_x0 > avail && g.x > row_x0 && (spaces || !blank(g)) {
rows.push(RowStart {
byte: g.start as u32,
x: g.x,
});
row_x0 = g.x;
avail = w;
}
}
}
}
let end = if rows.len() == 1 {
head_x + run.line_w
} else {
run.line_w - row_x0
};
(rows, end)
}
fn build_templates(
entry: &mut CachedText,
raster: &mut Raster,
fs: &mut FontSystem,
atlas: &mut GlyphAtlas,
) {
{
let built_for = (entry.wrap.map(f32::to_bits), atlas.stamp);
if entry.glyphs_built_for != Some(built_for) {
entry.glyphs.clear();
entry.deco.clear();
let lines = line_cap(entry.max_lines);
let decorated = entry.span_deco.iter().any(SpanDeco::any);
for run in entry.buffer.layout_runs().take(lines) {
if decorated {
build_decorations(&run, &entry.span_deco, fs, &mut entry.deco);
}
for glyph in run.glyphs.iter() {
let physical = glyph.physical((0.0, 0.0), 1.0);
let Some(slot) = raster_glyph(physical.cache_key, fs, raster, atlas) else {
continue;
};
entry.glyphs.push(GlyphTemplate {
x: physical.x as f32 + slot.left as f32,
y: run.line_y.round() + physical.y as f32 - slot.top as f32,
w: slot.w as f32,
h: slot.h as f32,
uv: [slot.x, slot.y, slot.w, slot.h],
kind: glyph_kind(&slot),
color: glyph
.color_opt
.map(|c| Color::rgba8(c.r(), c.g(), c.b(), c.a())),
byte: glyph.start as u32,
});
}
}
entry.glyphs_built_for = Some((entry.wrap.map(f32::to_bits), atlas.stamp));
}
}
}
#[allow(clippy::too_many_arguments)]
fn emit_entry(
entry: &mut CachedText,
ox: f32,
oy: f32,
nudge: Vec2,
color: Color,
clip: Clip,
clip_id: ClipId,
raster: &mut Raster,
fs: &mut FontSystem,
atlas: &mut GlyphAtlas,
out: &mut Vec<Quad>,
joins: &mut Vec<JoinBg>,
) {
build_templates(entry, raster, fs, atlas);
{
let inside = |x: f32, y: f32, w: f32, h: f32| {
oy + y + h >= clip.rect.y
&& oy + y <= clip.rect.y + clip.rect.h
&& ox + x < clip.rect.x + clip.rect.w
&& ox + x + w > clip.rect.x
};
let deco_quad = |d: &DecoTemplate| Quad {
rect: Rect::new(ox + d.x, oy + d.y, d.w, d.h),
color: d.color.unwrap_or(color),
border_color: Color::TRANSPARENT,
radius: [0.0; 4],
border_w: 0.0,
blur: 0.0,
kind: QuadKind::Solid,
clip: clip_id,
uv: [0; 4],
};
for d in entry
.deco
.iter()
.filter(|d| d.under && inside(d.x, d.y, d.w, d.h))
{
if d.radius > 0.0 {
joins.push(JoinBg {
quad: out.len() as u32,
radius: d.radius,
outer: clip_id,
own: None,
});
}
out.push(Quad {
rect: on_pixels(ox + d.x, oy + d.y, d.w, d.h, nudge),
..deco_quad(d)
});
}
out.extend(
entry
.glyphs
.iter()
.filter(|g| {
oy + g.y + g.h >= clip.rect.y
&& ox + g.x < clip.rect.x + clip.rect.w
&& ox + g.x + g.w > clip.rect.x
})
.take_while(|g| oy + g.y <= clip.rect.y + clip.rect.h)
.map(|g| Quad {
rect: Rect::new(ox + g.x, oy + g.y, g.w, g.h),
color: g.color.unwrap_or(color),
border_color: Color::TRANSPARENT,
radius: [0.0; 4],
border_w: 0.0,
blur: 0.0,
kind: g.kind,
clip: clip_id,
uv: g.uv,
}),
);
for d in entry
.deco
.iter()
.filter(|d| !d.under && inside(d.x, d.y, d.w, d.h))
{
match d.style {
UnderlineStyle::Solid => out.push(deco_quad(d)),
style => crate::deco::push_line(
out,
style,
ox + d.x,
oy + d.y,
d.w,
d.h,
d.color.unwrap_or(color),
clip_id,
),
}
}
}
}
fn could_be_long(len: usize, style: &TextStyle) -> bool {
(len >= LONG_LINE_BYTES || len > 0 && style.wrap == TextWrap::BreakSpaces)
&& style.max_lines == 0
&& !style.ellipsis
}
fn has_line_break(content: &str) -> bool {
content.bytes().any(|b| b == b'\n' || b == b'\r')
}
fn has_rtl(content: &str) -> bool {
!content.is_ascii()
&& content.chars().any(|c| {
matches!(c as u32,
0x0590..=0x08FF
| 0x200F | 0x202B | 0x202E | 0x2067
| 0xFB1D..=0xFDFF
| 0xFE70..=0xFEFF
| 0x10800..=0x10FFF
| 0x1E800..=0x1EFFF)
})
}
fn build_decorations(
run: &cosmic_text::LayoutRun<'_>,
spans: &[SpanDeco],
fs: &mut FontSystem,
out: &mut Vec<DecoTemplate>,
) {
let mut last_bg: Option<usize> = None;
let mut i = 0;
while i < run.glyphs.len() {
let span_no = run.glyphs[i].metadata;
let mut j = i + 1;
while j < run.glyphs.len() && run.glyphs[j].metadata == span_no {
j += 1;
}
let deco = spans.get(span_no).copied().unwrap_or_default();
if deco.any() {
let group = &run.glyphs[i..j];
let x0 = group.iter().map(|g| g.x).fold(f32::INFINITY, f32::min);
let x1 = group
.iter()
.map(|g| g.x + g.w)
.fold(f32::NEG_INFINITY, f32::max);
let w = (x1 - x0).max(0.0);
match (deco.bg, last_bg.map(|k| &mut out[k])) {
(Some(bg), Some(prev))
if prev.color == Some(bg)
&& prev.radius == deco.bg_radius
&& (prev.x + prev.w - x0).abs() < 0.01 =>
{
prev.w = (x1 - prev.x).max(prev.w);
}
(Some(bg), _) => {
let (y, h) = match group[0].line_height_opt {
Some(lh) if lh < run.line_height - 0.01 => {
let g = &group[0];
let (asc, desc) = fs
.get_font(g.font_id, g.font_weight)
.map(|font| {
let m = font.as_swash().metrics(&[]).scale(g.font_size);
(m.ascent, m.descent)
})
.unwrap_or((0.8 * g.font_size, 0.2 * g.font_size));
let mid = run.line_y - (asc - desc) / 2.0;
(mid - lh / 2.0, lh)
}
_ => (run.line_top, run.line_height),
};
last_bg = Some(out.len());
out.push(DecoTemplate {
x: x0,
y,
w,
h,
color: Some(bg),
under: true,
style: UnderlineStyle::Solid,
radius: deco.bg_radius,
});
}
(None, _) => last_bg = None,
}
if deco.underline || deco.strikethrough {
let first = &group[0];
let metrics = fs
.get_font(first.font_id, first.font_weight)
.map(|font| font.as_swash().metrics(&[]).scale(first.font_size));
let (under_off, strike_off, stroke) = match metrics {
Some(m) => (m.underline_offset, m.strikeout_offset, m.stroke_size),
None => (
-0.1 * first.font_size,
0.3 * first.font_size,
0.06 * first.font_size,
),
};
let stroke = stroke.max(1.0).round();
let baseline = run.line_y.round();
if deco.underline {
out.push(DecoTemplate {
x: x0,
y: (baseline - under_off).round(),
w,
h: stroke,
color: deco.underline_color.or_else(|| {
first
.color_opt
.map(|c| Color::rgba8(c.r(), c.g(), c.b(), c.a()))
}),
under: false,
style: deco.underline_style,
radius: 0.0,
});
}
if deco.strikethrough {
out.push(DecoTemplate {
x: x0,
y: (baseline - strike_off).round(),
w,
h: stroke,
color: first
.color_opt
.map(|c| Color::rgba8(c.r(), c.g(), c.b(), c.a())),
under: false,
style: UnderlineStyle::Solid,
radius: 0.0,
});
}
}
} else {
last_bg = None;
}
i = j;
}
}
impl TextSystem {
pub(crate) fn place(
&mut self,
key: Key,
ancestry: &Ancestry,
id: TextId,
origin: Vec2,
scope: Option<Key>,
drawn: bool,
) {
let cache_key = self.frame[id.0 as usize].cache_key;
self.places.push(TextPlace {
key,
ancestors: ancestry.keys,
depth: ancestry.depth.min(PLACE_ANCESTORS) as u8,
none_at: ancestry.none_at.map_or(u8::MAX, |n| n as u8),
cache_key,
origin,
scope,
drawn,
});
}
fn runs_of(&self, key: Key, prev: bool) -> Vec<(&TextPlace, &Entry, usize)> {
let list = if prev {
self.places.prev()
} else {
&self.places
};
let mut base = 0usize;
let mut out = Vec::new();
for place in list.iter().filter(|p| p.drawn && p.answers_to(key)) {
let Some(entry) = self.entries.get(&place.cache_key) else {
continue;
};
out.push((place, entry, base));
base += entry.content().len();
}
out
}
pub(crate) fn content_len(&self, id: TextId) -> usize {
let key = self.frame[id.0 as usize].cache_key;
self.entries.get(&key).map_or(0, |e| e.content().len())
}
pub(crate) fn scope_runs(&self, scope: Key, prev: bool) -> Vec<ScopeRun<'_>> {
let list = if prev {
self.places.prev()
} else {
&self.places
};
let mut base = 0usize;
let mut out = Vec::new();
for place in list.iter().filter(|p| p.scope == Some(scope)) {
let Some(text) = self.entries.get(&place.cache_key) else {
continue;
};
let len = text.content().len();
out.push(ScopeRun { place, text, base });
base += len;
}
out
}
pub(crate) fn scope_hit(&self, scope: Key, point: Vec2, prev: bool) -> Option<(Key, usize)> {
let runs = self.scope_runs(scope, prev);
let px = point.x * self.scale;
let py = point.y * self.scale;
let gap = |r: &Rect| {
let dy = (r.y - py).max(py - (r.y + r.h)).max(0.0);
let dx = (r.x - px).max(px - (r.x + r.w)).max(0.0);
(dy, dx)
};
let run = runs.iter().filter(|r| r.place.drawn).min_by(|a, b| {
let ga = gap(&self.scope_box(a));
let gb = gap(&self.scope_box(b));
ga.partial_cmp(&gb).unwrap_or(std::cmp::Ordering::Equal)
})?;
let byte = self.hit_in_run(run, point)?;
Some((run.place.key, byte))
}
fn scope_box(&self, run: &ScopeRun<'_>) -> Rect {
self.entry_box(run.place, run.text)
}
fn entry_box(&self, place: &TextPlace, entry: &Entry) -> Rect {
match entry {
Entry::Run(e) => self.physical_box(place, e),
Entry::Long(l) => {
let (ox, oy) = self.physical_origin(place);
let w = l.wrap_w.unwrap_or_else(|| l.width());
Rect::new(ox, oy, w, l.line_h * l.rows() as f32)
}
}
}
fn row_tops(&self, place: &TextPlace, entry: &Entry) -> Vec<f32> {
let (_, oy) = self.physical_origin(place);
match entry {
Entry::Run(e) => e.buffer.layout_runs().map(|r| oy + r.line_top).collect(),
Entry::Long(l) => (0..l.rows()).map(|r| oy + r as f32 * l.line_h).collect(),
}
}
fn hit_in_run(&self, run: &ScopeRun<'_>, point: Vec2) -> Option<usize> {
match run.text {
Entry::Long(l) => Some(self.long_hit(run.place, l, point)?.byte),
Entry::Run(e) => {
let (ox, oy) = self.physical_origin(run.place);
let cursor = e
.buffer
.hit(point.x * self.scale - ox, point.y * self.scale - oy)?;
Some(e.byte_of(cursor))
}
}
}
pub(crate) fn word_at(
&self,
scope: Key,
node: Key,
byte: usize,
prev: bool,
) -> Option<(usize, usize)> {
let run = self
.scope_runs(scope, prev)
.into_iter()
.find(|r| r.place.key == node)?;
let content = run.text.content();
Some(word_range(content, byte))
}
pub(crate) fn scope_offset(
&self,
scope: Key,
node: Key,
byte: usize,
prev: bool,
) -> Option<usize> {
let run = self
.scope_runs(scope, prev)
.into_iter()
.find(|r| r.place.key == node)?;
Some(run.base + byte.min(run.text.content().len()))
}
pub(crate) fn scope_html(&self, scope: Key, from: usize, to: usize, prev: bool) -> String {
let (from, to) = (from.min(to), from.max(to));
let mut out = String::new();
let mut prev_run: Option<&ScopeRun<'_>> = None;
for run in &self.scope_runs(scope, prev) {
let (start, end) = run.span();
if end <= from || start >= to {
continue;
}
if let Some(p) = prev_run
&& (p.place.origin.y - run.place.origin.y).abs() > f32::EPSILON
{
out.push_str("<br>");
}
let content = run.text.content();
let lo = floor_boundary(content, from.saturating_sub(start));
let hi = floor_boundary(content, (to - start).min(content.len()));
match run.text {
Entry::Long(line) => self.long_html(line, lo, hi, &mut out),
Entry::Run(entry) => html_of_run(entry, lo, hi, &mut out),
}
prev_run = Some(run);
}
out
}
fn long_html(&self, line: &LongLine, lo: usize, hi: usize, out: &mut String) {
for c in &line.chunks {
let (a, b) = (lo.max(c.start), hi.min(c.end));
if a >= b {
continue;
}
match c.width.and_then(|_| self.run(c.key)) {
Some(e) => html_of_run(e, a - c.start, b - c.start, out),
None => escape_into(&line.content[a..b], out),
}
}
}
pub(crate) fn scope_selection_anchor(
&self,
scope: Key,
from: usize,
to: usize,
prev: bool,
) -> Option<Vec2> {
let (from, to) = (from.min(to), from.max(to));
for run in self.scope_runs(scope, prev) {
let (start, end) = run.span();
if !run.place.drawn || end <= from || start >= to {
continue;
}
let content = run.text.content();
let lo = floor_boundary(content, from.saturating_sub(start));
let hi = floor_boundary(content, (to - start).min(content.len()));
let (ox, oy) = self.physical_origin(run.place);
match run.text {
Entry::Run(e) => {
if let Some((x, base)) = Self::run_anchor(e, lo, hi) {
return Some(Vec2::new((ox + x) / self.scale, (oy + base) / self.scale));
}
}
Entry::Long(l) => {
if let Some((x, y)) = self.long_caret_local(l, lo) {
return Some(Vec2::new(
(ox + x) / self.scale,
(oy + y + self.long_baseline(l)) / self.scale,
));
}
}
}
}
None
}
fn run_anchor(entry: &CachedText, lo: usize, hi: usize) -> Option<(f32, f32)> {
let (a, b) = (entry.cursor_of(lo), entry.cursor_of(hi));
for run in entry.buffer.layout_runs() {
if run.line_i < a.line || run.line_i > b.line {
continue;
}
if let Some((x, _)) = run.highlight(a, b).next() {
return Some((x, run.line_y));
}
}
None
}
pub(crate) fn scope_selection_rect(
&self,
scope: Key,
from: usize,
to: usize,
prev: bool,
) -> Option<Rect> {
let (from, to) = (from.min(to), from.max(to));
let mut out: Option<Rect> = None;
for run in self.scope_runs(scope, prev) {
let (start, end) = run.span();
if !run.place.drawn || end <= from || start >= to {
continue;
}
let content = run.text.content();
let lo = floor_boundary(content, from.saturating_sub(start));
let hi = floor_boundary(content, (to - start).min(content.len()));
let (ox, oy) = self.physical_origin(run.place);
let rects = match run.text {
Entry::Long(l) => self.long_highlight(l, lo, hi),
Entry::Run(e) => Self::run_highlight(e, lo, hi),
};
for r in rects {
let r = Rect::new(
(ox + r.x) / self.scale,
(oy + r.y) / self.scale,
r.w / self.scale,
r.h / self.scale,
);
out = Some(match out {
None => r,
Some(o) => o.union(&r),
});
}
}
out
}
pub(crate) fn scope_slice(&self, scope: Key, from: usize, to: usize, prev: bool) -> String {
let (from, to) = (from.min(to), from.max(to));
let runs = self.scope_runs(scope, prev);
let mut out = String::new();
let mut reserved = false;
let mut prev_run: Option<&ScopeRun<'_>> = None;
for run in &runs {
let (start, end) = run.span();
if end <= from || start >= to {
continue;
}
if !reserved {
out.reserve(to - from + runs.len());
reserved = true;
}
if let Some(p) = prev_run
&& (p.place.origin.y - run.place.origin.y).abs() > f32::EPSILON
{
out.push('\n');
}
let content = run.text.content();
let lo = from.saturating_sub(start).min(content.len());
let hi = (to - start).min(content.len());
out.push_str(&content[floor_boundary(content, lo)..floor_boundary(content, hi)]);
prev_run = Some(run);
}
out
}
fn long_hit(&self, place: &TextPlace, line: &LongLine, point: Vec2) -> Option<TextHit> {
let (ox, oy) = self.physical_origin(place);
let px = point.x * self.scale - ox;
let py = point.y * self.scale - oy;
if line.chunks.is_empty() {
return Some(TextHit { byte: 0, line: 0 });
}
if let Some(w) = line.wrap_w {
return self.long_hit_wrapped(line, w, px, py);
}
if px >= line.width() {
return Some(TextHit {
byte: line.content.len(),
line: 0,
});
}
let i = line.chunk_at(px.max(0.0));
let c = &line.chunks[i];
let local = px - line.prefix[i];
let byte = match self.run(c.key) {
Some(_) if let Some(b) = line.placed_tab_hit(i, local) => b,
Some(e) if c.width.is_some() => {
let cursor = e.buffer.hit(local, py.clamp(0.0, line.line_h - 0.01))?;
c.start + cursor.index.min(c.end - c.start)
}
_ => {
let est = (local / line.avg.max(f32::EPSILON)).round() as usize;
let mut b = (c.start + est).min(c.end);
while !line.content.is_char_boundary(b) {
b -= 1;
}
b
}
};
Some(TextHit { byte, line: 0 })
}
fn long_hit_wrapped(&self, line: &LongLine, w: f32, px: f32, py: f32) -> Option<TextHit> {
let row = ((py / line.line_h).floor().max(0.0) as u32).min(line.rows() - 1);
let Some((a, b)) = line.chunks_on_rows(row, row) else {
return Some(TextHit {
byte: line.content.len(),
line: row,
});
};
let i = (a..=b)
.find(|&j| {
let (r1, end_x) = line.starts[j + 1];
px < if r1 == row { end_x } else { w }
})
.unwrap_or(b);
let c = &line.chunks[i];
let (row0, head_x) = line.starts[i];
let r = (row - row0) as usize;
let byte = match self.run(c.key) {
Some(e) if r < c.rows.len() => {
let rs = c.rows[r];
let local_x = px - if r == 0 { head_x } else { 0.0 } + rs.x;
let lo = rs.byte as usize;
let hi = c
.rows
.get(r + 1)
.map_or(c.end - c.start, |n| n.byte as usize);
let tab_here = (lo..hi).contains(&(c.end - c.start - 1));
match line.placed_tab_hit(i, local_x) {
Some(b) if tab_here => b,
_ => {
let cursor = e.buffer.hit(local_x, line.line_h / 2.0)?;
c.start + cursor.index.clamp(lo, hi)
}
}
}
_ => {
let linear = r as f32 * w + px - head_x;
let est = (linear / line.avg.max(f32::EPSILON)).round() as usize;
let mut b = (c.start + est).min(c.end);
while !line.content.is_char_boundary(b) {
b -= 1;
}
b
}
};
let next_row = self
.long_caret_local(line, byte)
.is_some_and(|(_, y)| y > (row as f32 + 0.5) * line.line_h);
let byte = if next_row && byte > 0 {
floor_boundary(&line.content, byte - 1)
} else {
byte
};
Some(TextHit { byte, line: row })
}
fn long_caret(&self, place: &TextPlace, line: &LongLine, byte: usize) -> Option<Rect> {
let (ox, oy) = self.physical_origin(place);
let scale = self.scale;
let (x, y) = self.long_caret_local(line, byte)?;
Some(Rect::new(
(ox + x) / scale,
(oy + y) / scale,
0.0,
line.line_h / scale,
))
}
fn long_baseline(&self, line: &LongLine) -> f32 {
line.chunks
.first()
.and_then(|c| self.run(c.key))
.and_then(|e| e.buffer.layout_runs().next())
.map_or(line.line_h * 0.8, |r| r.line_y.round())
}
fn long_caret_local(&self, line: &LongLine, byte: usize) -> Option<(f32, f32)> {
let byte = byte.min(line.content.len());
if let Some(w) = line.wrap_w
&& !line.chunks.is_empty()
{
let i = line.chunk_of_byte(byte);
let c = &line.chunks[i];
let local = byte - c.start;
let (row0, head_x) = line.starts[i];
let (r, x) = match self.run(c.key) {
Some(e) if !c.rows.is_empty() => {
let r = c.rows.partition_point(|rs| rs.byte as usize <= local) - 1;
let run = e.buffer.layout_runs().next()?;
let cx = caret_x(&run, local.min(c.end - c.start)) + line.end_shift(i, local);
(r, cx - c.rows[r].x + if r == 0 { head_x } else { 0.0 })
}
_ => {
let linear = head_x + local as f32 * line.avg;
let r = (linear / w).floor();
(r as usize, linear - r * w)
}
};
let y = (row0 as usize + r) as f32 * line.line_h;
return Some((x, y));
}
let x = if line.chunks.is_empty() {
0.0
} else {
let i = line.chunk_of_byte(byte);
let c = &line.chunks[i];
let local = byte - c.start;
match self.run(c.key) {
Some(e) if c.width.is_some() => {
let run = e.buffer.layout_runs().next()?;
let x = caret_x(&run, local.min(c.end - c.start));
line.prefix[i] + x + line.end_shift(i, local)
}
_ => line.prefix[i] + local as f32 * line.avg,
}
};
Some((x, 0.0))
}
fn long_highlight(&self, line: &LongLine, from: usize, to: usize) -> Vec<Rect> {
let Some((x0, y0)) = self.long_caret_local(line, from) else {
return Vec::new();
};
let Some((x1, y1)) = self.long_caret_local(line, to) else {
return Vec::new();
};
let h = line.line_h;
if (y1 - y0).abs() < f32::EPSILON {
return vec![Rect::new(x0, y0, (x1 - x0).max(0.0), h)];
}
let w = line.wrap_w.unwrap_or_else(|| line.width());
let mut out = vec![Rect::new(x0, y0, (w - x0).max(0.0), h)];
let mut y = y0 + h;
while y < y1 - h / 2.0 {
out.push(Rect::new(0.0, y, w, h));
y += h;
}
out.push(Rect::new(0.0, y1, x1.max(0.0), h));
out
}
fn run_highlight(entry: &CachedText, from: usize, to: usize) -> Vec<Rect> {
let (a, b) = (entry.cursor_of(from), entry.cursor_of(to));
let mut out = Vec::new();
for run in entry.buffer.layout_runs() {
if run.line_i < a.line || run.line_i > b.line {
continue;
}
let h = run.line_height;
let mut any = false;
for (x, w) in run.highlight(a, b) {
any = true;
out.push(Rect::new(x, run.line_top, w.max(2.0), h));
}
if !any && run.glyphs.is_empty() && b.line > run.line_i {
out.push(Rect::new(0.0, run.line_top, 2.0, h));
}
}
out
}
fn physical_origin(&self, place: &TextPlace) -> (f32, f32) {
(
crate::geom::snap_px(place.origin.x * self.scale),
crate::geom::snap_px(place.origin.y * self.scale),
)
}
fn physical_box(&self, place: &TextPlace, entry: &CachedText) -> Rect {
let (ox, oy) = self.physical_origin(place);
let (size, _) = measure_buffer(&entry.buffer, entry.max_lines);
Rect::new(ox, oy, size.w, size.h)
}
pub(crate) fn hit_at(&self, key: Key, point: Vec2, prev: bool) -> Option<TextHit> {
let runs = self.runs_of(key, prev);
if runs.is_empty() {
return None;
}
let px = point.x * self.scale;
let py = point.y * self.scale;
let gap = |r: &Rect| {
let dy = (r.y - py).max(py - (r.y + r.h)).max(0.0);
let dx = (r.x - px).max(px - (r.x + r.w)).max(0.0);
(dy, dx)
};
let (place, entry, base) = runs
.iter()
.min_by(|a, b| {
let ga = gap(&self.entry_box(a.0, a.1));
let gb = gap(&self.entry_box(b.0, b.1));
ga.partial_cmp(&gb).unwrap_or(std::cmp::Ordering::Equal)
})
.copied()?;
let (ox, oy) = self.physical_origin(place);
let (byte, hit_top) = match entry {
Entry::Long(line) => {
let hit = self.long_hit(place, line, point)?;
(hit.byte, oy + hit.line as f32 * line.line_h)
}
Entry::Run(entry) => {
let bx = self.physical_box(place, entry);
let py = py.clamp(bx.y, (bx.y + bx.h - 0.01).max(bx.y));
let cursor = entry.buffer.hit(px - ox, py - oy)?;
let row =
visual_line(&entry.buffer, cursor.line, cursor.index).map_or(0, |(l, _)| l);
let top = entry
.buffer
.layout_runs()
.nth(row)
.map_or(0.0, |r| r.line_top);
(entry.byte_of(cursor), oy + top)
}
};
let mut tops: Vec<f32> = runs
.iter()
.flat_map(|(p, e, _)| self.row_tops(p, e))
.collect();
tops.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
tops.dedup_by(|a, b| (*a - *b).abs() < 0.5);
let line = tops.iter().filter(|t| **t < hit_top - 0.5).count();
Some(TextHit {
byte: base + byte,
line: line as u32,
})
}
pub(crate) fn caret_at(&self, key: Key, byte: usize, prev: bool) -> Option<Rect> {
let runs = self.runs_of(key, prev);
let total = runs.last().map(|(_, e, base)| base + e.content().len())?;
let byte = byte.min(total);
let (place, entry, base) = runs
.iter()
.find(|(_, e, base)| byte < base + e.content().len())
.or_else(|| runs.last())
.copied()?;
let byte = (byte - base).min(entry.content().len());
let entry = match entry {
Entry::Long(line) => return self.long_caret(place, line, byte),
Entry::Run(entry) => entry,
};
let (ox, oy) = self.physical_origin(place);
let cursor = entry.cursor_of(byte);
let (_, run_no) = visual_line(&entry.buffer, cursor.line, cursor.index)?;
let run = entry.buffer.layout_runs().nth(run_no)?;
let x = caret_x(&run, cursor.index);
let scale = self.scale;
Some(Rect::new(
(ox + x) / scale,
(oy + run.line_top) / scale,
0.0,
run.line_height / scale,
))
}
}
fn word_range(content: &str, byte: usize) -> (usize, usize) {
if content.is_empty() {
return (0, 0);
}
#[derive(PartialEq)]
enum Class {
Word,
Space,
Other,
}
let class = |c: char| {
if c.is_alphanumeric() || c == '_' {
Class::Word
} else if c.is_whitespace() {
Class::Space
} else {
Class::Other
}
};
let at = floor_boundary(content, byte.min(content.len()));
let at = if at >= content.len() {
content.char_indices().next_back().map_or(0, |(i, _)| i)
} else {
at
};
let Some(here) = content[at..].chars().next().map(class) else {
return (at, at);
};
let mut start = at;
for (i, c) in content[..at].char_indices().rev() {
if class(c) != here {
break;
}
start = i;
}
let mut end = at;
for (i, c) in content[at..].char_indices() {
if class(c) != here {
break;
}
end = at + i + c.len_utf8();
}
(start, end)
}
fn html_of_run(entry: &CachedText, lo: usize, hi: usize, out: &mut String) {
let starts = line_starts(&entry.buffer, &entry.content);
for (li, line) in entry.buffer.lines.iter().enumerate() {
let base = starts.get(li).copied().unwrap_or(0);
let text = line.text();
if li > 0 {
if base > lo && base <= hi {
out.push_str("<br>");
}
}
let attrs_list = line.attrs_list();
let mut spans: Vec<(usize, usize, cosmic_text::AttrsOwned)> = Vec::new();
for (i, _) in text.char_indices() {
let a = cosmic_text::AttrsOwned::new(&attrs_list.get_span(i));
match spans.last_mut() {
Some((_, end, prev)) if *prev == a && *end == i => {
*end = i + text[i..].chars().next().map_or(1, char::len_utf8);
}
_ => spans.push((i, i + text[i..].chars().next().map_or(1, char::len_utf8), a)),
}
}
for (start, end, attrs) in &spans {
let (s, e) = (base + start, base + end);
let (s, e) = (s.max(lo), e.min(hi));
if s >= e {
continue;
}
let piece = &entry.content[s..e];
let bold = attrs.weight > attrs_list.defaults().weight
|| attrs
.cache_key_flags
.contains(crate::weights::SYNTHETIC_BOLD);
let italic = attrs.style != cosmic_text::Style::Normal;
if let Some(c) = attrs.color_opt {
let _ = std::fmt::Write::write_fmt(
out,
format_args!(
"<span style=\"color:#{:02x}{:02x}{:02x}\">",
c.r(),
c.g(),
c.b()
),
);
}
if bold {
out.push_str("<b>");
}
if italic {
out.push_str("<i>");
}
escape_into(piece, out);
if italic {
out.push_str("</i>");
}
if bold {
out.push_str("</b>");
}
if attrs.color_opt.is_some() {
out.push_str("</span>");
}
}
}
}
fn escape_into(s: &str, out: &mut String) {
for ch in s.chars() {
match ch {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
_ => out.push(ch),
}
}
}
fn floor_boundary(s: &str, i: usize) -> usize {
let mut i = i.min(s.len());
while i > 0 && !s.is_char_boundary(i) {
i -= 1;
}
i
}
fn line_starts(buffer: &Buffer, content: &str) -> Vec<usize> {
paragraph_starts(buffer, content).collect()
}
fn paragraph_starts<'a>(buffer: &'a Buffer, content: &'a str) -> impl Iterator<Item = usize> + 'a {
let mut at = 0usize;
buffer.lines.iter().map(move |line| {
let start = at.min(content.len());
at += line.text().len();
let rest = &content[at.min(content.len())..];
for ending in ["\r\n", "\n\r", "\n", "\r"] {
if rest.starts_with(ending) {
at += ending.len();
break;
}
}
start
})
}
fn visual_line(buffer: &Buffer, line_i: usize, index: usize) -> Option<(usize, usize)> {
let mut last_of_line = None;
let mut before = None;
for (n, run) in buffer.layout_runs().enumerate() {
if run.line_i != line_i {
if last_of_line.is_some() {
break;
}
continue;
}
last_of_line = Some(n);
if run.glyphs.iter().any(|g| g.start <= index && index < g.end) {
return Some((n, n));
}
if run
.glyphs
.iter()
.map(|g| g.start)
.min()
.is_some_and(|s| s <= index)
{
before = Some(n);
}
}
before.or(last_of_line).map(|n| (n, n))
}
fn caret_x(run: &cosmic_text::LayoutRun<'_>, index: usize) -> f32 {
if let Some(g) = run
.glyphs
.iter()
.find(|g| g.start <= index && index < g.end)
{
return if g.level.is_rtl() { g.x + g.w } else { g.x };
}
match run.glyphs.last() {
Some(g) if !g.level.is_rtl() => g.x + g.w,
Some(g) => g.x,
None => 0.0,
}
}
impl Default for TextSystem {
fn default() -> Self {
Self::new()
}
}
fn wrap_target(entry: &CachedText, max_w_logical: Option<f32>, scale: f32) -> Option<f32> {
let max_w = max_w_logical?;
let intrinsic_fits = entry.intrinsic.w <= max_w * scale + 0.5;
(!intrinsic_fits).then_some(max_w * scale)
}
pub(crate) fn wrap_differs(current: Option<f32>, target: Option<f32>) -> bool {
match (current, target) {
(None, None) => false,
(Some(a), Some(b)) => (a - b).abs() > 0.5,
_ => true,
}
}
fn wrap_entry(entry: &mut CachedText, fs: &mut FontSystem, target: Option<f32>) {
if wrap_differs(entry.wrap, target) {
entry.buffer.set_size(target, None);
entry.buffer.shape_until_scroll(fs, false);
entry.wrap = target;
}
}
impl CachedText {
fn byte_of(&self, cursor: cosmic_text::Cursor) -> usize {
let start = paragraph_starts(&self.buffer, &self.content)
.nth(cursor.line)
.unwrap_or(0);
(start + cursor.index).min(self.content.len())
}
fn cursor_of(&self, byte: usize) -> cosmic_text::Cursor {
let mut li = 0;
let mut start = 0;
for (i, s) in paragraph_starts(&self.buffer, &self.content).enumerate() {
if s <= byte {
(li, start) = (i, s);
} else {
break;
}
}
let len = self.buffer.lines.get(li).map_or(0, |l| l.text().len());
cosmic_text::Cursor::new(li, (byte - start).min(len))
}
fn new(
mut buffer: Buffer,
content: String,
style: &TextStyle,
span_deco: Vec<SpanDeco>,
fs: &mut FontSystem,
frame_no: u64,
) -> Self {
buffer.shape_until_scroll(fs, false);
let max_lines = line_budget(style);
let (intrinsic, _) = measure_buffer(&buffer, max_lines);
let glyphs: usize = buffer.layout_runs().map(|r| r.glyphs.len()).sum();
Self {
buffer,
bytes: ENTRY_BASE_BYTES + content.len() + ENTRY_GLYPH_BYTES * glyphs,
content,
wrap: None,
intrinsic,
max_lines,
clamp_w: style.wrap == TextWrap::None || style.ellipsis,
last_used: frame_no,
glyphs: Vec::new(),
deco: Vec::new(),
span_deco,
glyphs_built_for: None,
claimed: u64::MAX,
claimed_wrap: None,
breaks_anywhere: style.wrap == TextWrap::Glyph,
min_content: None,
}
}
fn fork(&self, frame_no: u64) -> Self {
Self {
buffer: self.buffer.clone(),
content: self.content.clone(),
wrap: self.wrap,
intrinsic: self.intrinsic,
max_lines: self.max_lines,
clamp_w: self.clamp_w,
last_used: frame_no,
bytes: self.bytes,
glyphs: Vec::new(),
deco: Vec::new(),
span_deco: self.span_deco.clone(),
glyphs_built_for: None,
claimed: u64::MAX,
claimed_wrap: None,
breaks_anywhere: self.breaks_anywhere,
min_content: self.min_content,
}
}
fn min_content(&mut self) -> f32 {
if let Some(w) = self.min_content {
return w;
}
let w = if self.clamp_w {
self.intrinsic.w
} else {
widest_unbreakable(&self.buffer, self.breaks_anywhere)
};
self.min_content = Some(w);
w
}
}
fn widest_unbreakable(buffer: &Buffer, anywhere: bool) -> f32 {
let mut widest = 0.0f32;
let mut line = usize::MAX;
let mut breaks: Vec<usize> = Vec::new();
let (mut run_w, mut ink) = (0.0f32, 0.0f32);
for run in buffer.layout_runs() {
if run.line_i != line {
widest = widest.max(ink);
(run_w, ink) = (0.0, 0.0);
line = run.line_i;
breaks.clear();
if !anywhere {
breaks.extend(unicode_linebreak::linebreaks(run.text).map(|(i, _)| i));
}
}
for g in run.glyphs {
if anywhere || breaks.binary_search(&g.start).is_ok() {
widest = widest.max(ink);
(run_w, ink) = (0.0, 0.0);
}
run_w += g.w;
let blank = run
.text
.get(g.start..g.end)
.is_some_and(|t| t.chars().all(char::is_whitespace));
if !blank {
ink = run_w;
}
}
}
widest.max(ink)
}
pub(crate) fn shaper_metrics(size: f32, line_height: f32) -> Metrics {
let px = |v: f32| if v.is_finite() { v.max(1.0) } else { 1.0 };
Metrics::new(px(size), px(line_height))
}
fn new_buffer(fs: &mut FontSystem, style: &TextStyle, scale: f32) -> Buffer {
let metrics = shaper_metrics(style.size * scale, style.line_height * scale);
let mut buffer = Buffer::new(fs, metrics);
buffer.set_wrap(match style.wrap {
TextWrap::Word | TextWrap::BreakSpaces => Wrap::WordOrGlyph,
TextWrap::Glyph => Wrap::Glyph,
TextWrap::None => Wrap::None,
});
if style.ellipsis {
let lines = line_budget(style).max(1);
buffer.set_ellipsize(Ellipsize::End(EllipsizeHeightLimit::Lines(lines)));
}
buffer.set_size(None, None);
buffer
}
fn line_budget(style: &TextStyle) -> usize {
if style.max_lines > 0 {
style.max_lines as usize
} else if style.ellipsis {
1
} else {
0
}
}
fn line_cap(max_lines: usize) -> usize {
if max_lines == 0 {
usize::MAX
} else {
max_lines
}
}
pub(crate) fn measure_buffer(buffer: &Buffer, max_lines: usize) -> (Size, u32) {
let mut w = 0.0f32;
let mut lines = 0u32;
let lh = buffer.metrics().line_height;
let (mut sum, mut uniform) = (0.0f32, true);
for run in buffer.layout_runs().take(line_cap(max_lines)) {
w = w.max(run.line_w);
lines += 1;
sum += run.line_height;
uniform &= run.line_height == lh;
}
(
Size::new(w, if uniform { lines as f32 * lh } else { sum }),
lines,
)
}
impl TextSystem {
pub(crate) fn intrinsic(&mut self, id: TextId) -> Size {
let scale = self.scale;
if let Some(line) = self.long_of(id) {
return Size::new(line.width() / scale, line.line_h / scale);
}
let e = self.entry_mut(id);
Size::new(e.intrinsic.w / scale, e.intrinsic.h / scale)
}
pub(crate) fn min_content(&mut self, id: TextId) -> f32 {
let scale = self.scale;
if self.long_of(id).is_some() {
return 0.0;
}
self.entry_mut(id).min_content() / scale
}
fn long_of(&self, id: TextId) -> Option<&LongLine> {
self.long(self.frame[id.0 as usize].cache_key)
}
pub(crate) fn wrapped(&mut self, id: TextId, max_w: f32, fs: &mut FontSystem) -> Size {
let scale = self.scale;
if self.long_of(id).is_some() {
let key = self.frame[id.0 as usize].cache_key;
let (size, rows) = self.long_size(key, Some(max_w * scale));
self.long_mut(key).expect("a long line").laid_rows = rows;
return Size::new(size.w / scale, size.h / scale);
}
self.ensure_wrap(id, max_w, fs);
let e = self.entry_mut(id);
let (mut m, _) = measure_buffer(&e.buffer, e.max_lines);
if e.clamp_w {
m.w = m.w.min(max_w * scale);
}
Size::new(m.w / scale, m.h / scale)
}
}
impl TextSystem {
pub(crate) fn baseline(&mut self, id: TextId) -> f32 {
let scale = self.scale;
if let Some(line) = self.long_of(id) {
return self.long_baseline(line) / scale;
}
let e = self.entry_mut(id);
let b = e
.buffer
.layout_runs()
.next()
.map_or(e.buffer.metrics().line_height * 0.8, |r| r.line_y.round());
b / scale
}
}
#[cfg(test)]
mod default_families {
use super::*;
use cosmic_text::Family;
use cosmic_text::fontdb::FaceInfo;
fn resolved(fs: &mut FontSystem, family: Family<'_>) -> Option<FaceInfo> {
let mut buffer = Buffer::new(fs, Metrics::new(14.0, 18.0));
buffer.set_text("M", &Attrs::new().family(family), Shaping::Advanced, None);
buffer.shape_until_scroll(fs, false);
let id = buffer.layout_runs().next()?.glyphs.first()?.font_id;
fs.db().face(id).cloned()
}
#[test]
fn mono_is_an_upright_monospaced_face() {
let mut fs = new_font_system().0;
if !fs.db().faces().any(|f| f.monospaced) {
eprintln!("skipped: no monospaced face installed");
return;
}
let face = resolved(&mut fs, Family::Monospace).expect("M shapes in Mono");
let mono = default_families(&fs)[2].to_string();
assert_eq!(
face.style,
cosmic_text::Style::Normal,
"Mono ({mono}) resolved to {:?}",
face.post_script_name
);
assert!(
face.monospaced,
"Mono ({mono}) resolved to {:?}, which is not monospaced",
face.post_script_name
);
}
#[test]
fn sans_and_serif_are_upright_and_not_monospaced() {
let mut fs = new_font_system().0;
if fs.db().faces().next().is_none() {
eprintln!("skipped: no face installed");
return;
}
let [sans, serif, _] = default_families(&fs).map(str::to_string);
for (family, name) in [(Family::SansSerif, sans), (Family::Serif, serif)] {
let face = resolved(&mut fs, family).expect("M shapes");
assert_eq!(
face.style,
cosmic_text::Style::Normal,
"{name} resolved to {:?}",
face.post_script_name
);
assert!(
!face.monospaced,
"{name} resolved to {:?}, which is monospaced",
face.post_script_name
);
}
}
#[test]
fn pinned_names_are_installed_or_cosmic_texts_own() {
let fs = new_font_system().0;
let db = fs.db();
for (name, list) in default_families(&fs).into_iter().zip(DEFAULT_FAMILIES) {
let installed = db
.faces()
.any(|f| f.families.iter().any(|(fam, _)| fam == name));
let on_list = list.contains(&name);
assert!(
installed == on_list,
"{name}: installed {installed}, on the platform list {on_list}"
);
}
}
}
#[cfg(test)]
mod unmeasurable_faces {
use super::*;
use crate::testing::{font_face, han_face, unmeasurable_face};
use cosmic_text::Family;
use cosmic_text::fontdb::{Database, Source};
fn font_system() -> FontSystem {
let mut db = Database::new();
for bytes in [
font_face("Kui Mono", 400, false, true),
unmeasurable_face("Kui Bitmap"),
han_face("Kui Han"),
] {
db.load_font_source(Source::Binary(std::sync::Arc::new(bytes)));
}
let mut fs = font_system_with("en-US".into(), db);
fs.db_mut().set_monospace_family("Kui Mono");
fs
}
fn family(fs: &FontSystem, id: cosmic_text::fontdb::ID) -> String {
fs.db()
.face(id)
.map_or_else(String::new, |f| f.families[0].0.clone())
}
#[test]
fn a_face_without_metrics_is_not_in_the_database() {
let fs = font_system();
let families: Vec<String> = fs.db().faces().map(|f| family(&fs, f.id)).collect();
assert_eq!(families, ["Kui Mono", "Kui Han"]);
}
#[test]
fn han_in_mono_falls_back_to_a_face_that_measures() {
let mut fs = font_system();
let mut buffer = Buffer::new(&mut fs, Metrics::new(14.0, 21.0));
let attrs = Attrs::new().family(Family::Monospace);
buffer.set_text("字 a", &attrs, Shaping::Advanced, None);
buffer.shape_until_scroll(&mut fs, false);
let run = buffer.layout_runs().next().expect("one line");
let glyphs: Vec<(&str, f32, f32, String)> = run
.glyphs
.iter()
.map(|g| (&run.text[g.start..g.end], g.x, g.w, family(&fs, g.font_id)))
.collect();
for &(text, x, w, _) in &glyphs {
assert!(x.is_finite() && w.is_finite(), "{text:?} at {x}, {w} wide");
}
let faces: Vec<(&str, &str)> = glyphs.iter().map(|g| (g.0, g.3.as_str())).collect();
assert_eq!(
faces,
[("字", "Kui Han"), (" ", "Kui Mono"), ("a", "Kui Mono")]
);
let xs: Vec<f32> = glyphs.iter().map(|g| g.1).collect();
assert_eq!(xs, [0.0, 7.0, 14.0]);
for glyph in run.glyphs {
glyph.physical((0.0, 0.0), 1.0);
}
}
}