use anyhow::Result;
use derive_more::{Add, FromStr, Sub};
use gpui_shared_string::SharedString;
use gpui_types::{
Bounds, DevicePixels, Hsla, Pixels, Point, Size, StrikethroughStyle, UnderlineStyle, point, px,
size,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use std::borrow::Cow;
use std::fmt::{Debug, Display, Formatter};
use std::hash::{Hash, Hasher};
use std::sync::Arc;
pub const SUBPIXEL_VARIANTS_X: u8 = 4;
pub const SUBPIXEL_VARIANTS_Y: u8 = 1;
use crate::{FontFallbacks, FontFeatures, FontId, GlyphId, RenderGlyphParams};
#[derive(Hash, PartialEq, Eq, Clone, Copy, Debug)]
pub struct FontFamilyId(pub usize);
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Serialize, Deserialize, Add, Sub, FromStr)]
#[serde(transparent)]
pub struct FontWeight(pub f32);
impl Display for FontWeight {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<f32> for FontWeight {
fn from(weight: f32) -> Self {
FontWeight(weight)
}
}
impl Default for FontWeight {
#[inline]
fn default() -> FontWeight {
FontWeight::NORMAL
}
}
impl Hash for FontWeight {
fn hash<H: Hasher>(&self, state: &mut H) {
state.write_u32(u32::from_be_bytes(self.0.to_be_bytes()));
}
}
impl Eq for FontWeight {}
impl FontWeight {
pub const THIN: FontWeight = FontWeight(100.0);
pub const EXTRA_LIGHT: FontWeight = FontWeight(200.0);
pub const LIGHT: FontWeight = FontWeight(300.0);
pub const NORMAL: FontWeight = FontWeight(400.0);
pub const MEDIUM: FontWeight = FontWeight(500.0);
pub const SEMIBOLD: FontWeight = FontWeight(600.0);
pub const BOLD: FontWeight = FontWeight(700.0);
pub const EXTRA_BOLD: FontWeight = FontWeight(800.0);
pub const BLACK: FontWeight = FontWeight(900.0);
pub const ALL: [FontWeight; 9] = [
Self::THIN,
Self::EXTRA_LIGHT,
Self::LIGHT,
Self::NORMAL,
Self::MEDIUM,
Self::SEMIBOLD,
Self::BOLD,
Self::EXTRA_BOLD,
Self::BLACK,
];
}
impl schemars::JsonSchema for FontWeight {
fn schema_name() -> Cow<'static, str> {
"FontWeight".into()
}
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
use schemars::json_schema;
json_schema!({
"type": "number",
"minimum": Self::THIN,
"maximum": Self::BLACK,
"default": Self::default(),
"description": "Font weight value between 100 (thin) and 900 (black)"
})
}
}
#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash, Default, Serialize, Deserialize, JsonSchema)]
pub enum FontStyle {
#[default]
Normal,
Italic,
Oblique,
}
impl Display for FontStyle {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
Debug::fmt(self, f)
}
}
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct Font {
pub family: SharedString,
pub features: FontFeatures,
pub fallbacks: Option<FontFallbacks>,
pub weight: FontWeight,
pub style: FontStyle,
}
impl Default for Font {
fn default() -> Self {
font(".SystemUIFont")
}
}
pub fn font(family: impl Into<SharedString>) -> Font {
Font {
family: family.into(),
features: FontFeatures::default(),
weight: FontWeight::default(),
style: FontStyle::default(),
fallbacks: None,
}
}
impl Font {
pub fn bold(mut self) -> Self {
self.weight = FontWeight::BOLD;
self
}
pub fn italic(mut self) -> Self {
self.style = FontStyle::Italic;
self
}
}
#[derive(Clone, Copy, Debug)]
pub struct FontMetrics {
pub units_per_em: u32,
pub ascent: f32,
pub descent: f32,
pub line_gap: f32,
pub underline_position: f32,
pub underline_thickness: f32,
pub cap_height: f32,
pub x_height: f32,
pub bounding_box: Bounds<f32>,
}
impl FontMetrics {
pub fn ascent(&self, font_size: Pixels) -> Pixels {
Pixels((self.ascent / self.units_per_em as f32) * font_size.0)
}
pub fn descent(&self, font_size: Pixels) -> Pixels {
Pixels((self.descent / self.units_per_em as f32) * font_size.0)
}
pub fn line_gap(&self, font_size: Pixels) -> Pixels {
Pixels((self.line_gap / self.units_per_em as f32) * font_size.0)
}
pub fn underline_position(&self, font_size: Pixels) -> Pixels {
Pixels((self.underline_position / self.units_per_em as f32) * font_size.0)
}
pub fn underline_thickness(&self, font_size: Pixels) -> Pixels {
Pixels((self.underline_thickness / self.units_per_em as f32) * font_size.0)
}
pub fn cap_height(&self, font_size: Pixels) -> Pixels {
Pixels((self.cap_height / self.units_per_em as f32) * font_size.0)
}
pub fn x_height(&self, font_size: Pixels) -> Pixels {
Pixels((self.x_height / self.units_per_em as f32) * font_size.0)
}
pub fn bounding_box(&self, font_size: Pixels) -> Bounds<Pixels> {
(self.bounding_box / self.units_per_em as f32 * font_size.0).map(px)
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
#[expect(missing_docs)]
pub struct FontRun {
pub len: usize,
pub font_id: FontId,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub enum TextRenderingMode {
#[default]
PlatformDefault,
Subpixel,
Grayscale,
}
#[derive(Default, Debug)]
pub struct LineLayout {
pub font_size: Pixels,
pub width: Pixels,
pub ascent: Pixels,
pub descent: Pixels,
pub runs: Vec<ShapedRun>,
pub len: usize,
}
#[derive(Debug, Clone)]
pub struct ShapedRun {
pub font_id: FontId,
pub glyphs: Vec<ShapedGlyph>,
}
#[derive(Clone, Debug)]
pub struct ShapedGlyph {
pub id: GlyphId,
pub position: Point<Pixels>,
pub index: usize,
pub is_emoji: bool,
}
#[derive(Default, Debug)]
pub struct WrappedLineLayout {
pub unwrapped_layout: Arc<LineLayout>,
pub wrap_boundaries: SmallVec<[WrapBoundary; 1]>,
pub wrap_width: Option<Pixels>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct WrapBoundary {
pub run_ix: usize,
pub glyph_ix: usize,
}
impl WrappedLineLayout {
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> usize {
self.unwrapped_layout.len
}
pub fn width(&self) -> Pixels {
self.wrap_width
.unwrap_or(Pixels::MAX)
.min(self.unwrapped_layout.width)
}
pub fn size(&self, line_height: Pixels) -> Size<Pixels> {
Size {
width: self.width(),
height: line_height * (self.wrap_boundaries.len() + 1),
}
}
pub fn ascent(&self) -> Pixels {
self.unwrapped_layout.ascent
}
pub fn descent(&self) -> Pixels {
self.unwrapped_layout.descent
}
pub fn wrap_boundaries(&self) -> &[WrapBoundary] {
&self.wrap_boundaries
}
pub fn font_size(&self) -> Pixels {
self.unwrapped_layout.font_size
}
pub fn runs(&self) -> &[ShapedRun] {
&self.unwrapped_layout.runs
}
pub fn index_for_position(
&self,
position: Point<Pixels>,
line_height: Pixels,
) -> Result<usize, usize> {
self._index_for_position(position, line_height, false)
}
pub fn closest_index_for_position(
&self,
position: Point<Pixels>,
line_height: Pixels,
) -> Result<usize, usize> {
self._index_for_position(position, line_height, true)
}
fn _index_for_position(
&self,
position: Point<Pixels>,
line_height: Pixels,
closest: bool,
) -> Result<usize, usize> {
let wrapped_line_ix = (position.y / line_height) as usize;
let wrapped_line_start_index;
let wrapped_line_start_x;
if wrapped_line_ix > 0 {
let Some(line_start_boundary) = self.wrap_boundaries.get(wrapped_line_ix - 1) else {
return Err(0);
};
let run = &self.unwrapped_layout.runs[line_start_boundary.run_ix];
let glyph = &run.glyphs[line_start_boundary.glyph_ix];
wrapped_line_start_index = glyph.index;
wrapped_line_start_x = glyph.position.x;
} else {
wrapped_line_start_index = 0;
wrapped_line_start_x = Pixels::ZERO;
};
let wrapped_line_end_index;
let wrapped_line_end_x;
if wrapped_line_ix < self.wrap_boundaries.len() {
let next_wrap_boundary_ix = wrapped_line_ix;
let next_wrap_boundary = self.wrap_boundaries[next_wrap_boundary_ix];
let run = &self.unwrapped_layout.runs[next_wrap_boundary.run_ix];
let glyph = &run.glyphs[next_wrap_boundary.glyph_ix];
wrapped_line_end_index = glyph.index;
wrapped_line_end_x = glyph.position.x;
} else {
wrapped_line_end_index = self.unwrapped_layout.len;
wrapped_line_end_x = self.unwrapped_layout.width;
};
let mut position_in_unwrapped_line = position;
position_in_unwrapped_line.x += wrapped_line_start_x;
if position_in_unwrapped_line.x < wrapped_line_start_x {
Err(wrapped_line_start_index)
} else if position_in_unwrapped_line.x >= wrapped_line_end_x {
Err(wrapped_line_end_index)
} else if closest {
Ok(self
.unwrapped_layout
.closest_index_for_x(position_in_unwrapped_line.x))
} else {
self.unwrapped_layout
.index_for_x(position_in_unwrapped_line.x)
.ok_or(wrapped_line_end_index)
}
}
pub fn position_for_index(&self, index: usize, line_height: Pixels) -> Option<Point<Pixels>> {
let mut line_start_ix = 0;
let line_end_indices = self
.wrap_boundaries
.iter()
.map(|wrap_boundary| {
let run = &self.unwrapped_layout.runs[wrap_boundary.run_ix];
let glyph = &run.glyphs[wrap_boundary.glyph_ix];
glyph.index
})
.chain([self.len()])
.enumerate();
for (ix, line_end_ix) in line_end_indices {
let line_y = ix as f32 * line_height;
if index < line_start_ix {
break;
} else if index > line_end_ix {
line_start_ix = line_end_ix;
continue;
} else {
let line_start_x = self.unwrapped_layout.x_for_index(line_start_ix);
let x = self.unwrapped_layout.x_for_index(index) - line_start_x;
return Some(point(x, line_y));
}
}
None
}
}
#[derive(Clone, Default)]
pub struct LineLayoutIndex {
pub font_generation: usize,
pub lines_index: usize,
pub wrapped_lines_index: usize,
pub lines_by_hash_index: usize,
pub wrapped_lines_by_hash_index: usize,
}
impl LineLayout {
pub fn index_for_x(&self, x: Pixels) -> Option<usize> {
if x >= self.width {
None
} else {
for run in self.runs.iter().rev() {
for glyph in run.glyphs.iter().rev() {
if glyph.position.x <= x {
return Some(glyph.index);
}
}
}
Some(0)
}
}
pub fn closest_index_for_x(&self, x: Pixels) -> usize {
let mut prev_index = 0;
let mut prev_x = px(0.);
for run in self.runs.iter() {
for glyph in run.glyphs.iter() {
if glyph.position.x >= x {
if glyph.position.x - x < x - prev_x {
return glyph.index;
} else {
return prev_index;
}
}
prev_index = glyph.index;
prev_x = glyph.position.x;
}
}
if self.len == 1 {
if x > self.width / 2. {
return 1;
} else {
return 0;
}
}
self.len
}
pub fn x_for_index(&self, index: usize) -> Pixels {
for run in &self.runs {
for glyph in &run.glyphs {
if glyph.index >= index {
return glyph.position.x;
}
}
}
self.width
}
pub fn font_id_for_index(&self, index: usize) -> Option<FontId> {
for run in &self.runs {
for glyph in &run.glyphs {
if glyph.index >= index {
return Some(run.font_id);
}
}
}
None
}
pub fn split_at(&self, byte_index: usize) -> (LineLayout, LineLayout) {
let x_offset = self.x_for_index(byte_index);
let mut left_runs = Vec::new();
let mut right_runs = Vec::new();
for run in &self.runs {
let split_pos = run.glyphs.partition_point(|g| g.index < byte_index);
if split_pos > 0 {
left_runs.push(ShapedRun {
font_id: run.font_id,
glyphs: run.glyphs[..split_pos].to_vec(),
});
}
if split_pos < run.glyphs.len() {
let right_glyphs = run.glyphs[split_pos..]
.iter()
.map(|g| ShapedGlyph {
id: g.id,
position: point(g.position.x - x_offset, g.position.y),
index: g.index - byte_index,
is_emoji: g.is_emoji,
})
.collect();
right_runs.push(ShapedRun {
font_id: run.font_id,
glyphs: right_glyphs,
});
}
}
let left = LineLayout {
font_size: self.font_size,
width: x_offset,
ascent: self.ascent,
descent: self.descent,
runs: left_runs,
len: byte_index,
};
let right = LineLayout {
font_size: self.font_size,
width: self.width - x_offset,
ascent: self.ascent,
descent: self.descent,
runs: right_runs,
len: self.len - byte_index,
};
(left, right)
}
}
#[expect(missing_docs)]
pub trait PlatformTextSystem: Send + Sync {
fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()>;
fn set_missing_glyph_sink(&self, _sink: Option<Arc<dyn MissingGlyphSink>>) {}
fn all_font_names(&self) -> Vec<String>;
fn font_id(&self, descriptor: &Font) -> Result<FontId>;
fn prewarm_fonts(&self, _font_ids: &[FontId]) {}
fn font_metrics(&self, font_id: FontId) -> FontMetrics;
fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>;
fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>>;
fn rasterize_glyph(
&self,
params: &RenderGlyphParams,
raster_bounds: Bounds<DevicePixels>,
) -> Result<(Size<DevicePixels>, Vec<u8>)>;
fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
fn recommended_rendering_mode(&self, _font_id: FontId, _font_size: Pixels)
-> TextRenderingMode;
fn glyph_dilation_for_color(&self, _color: Hsla) -> u8 {
0
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum FallbackFontClass {
Proportional,
Monospace,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct MissingGlyph {
grapheme: SharedString,
font_class: FallbackFontClass,
}
impl MissingGlyph {
pub fn new(grapheme: SharedString, font_class: FallbackFontClass) -> Self {
Self {
grapheme,
font_class,
}
}
pub fn grapheme(&self) -> &str {
&self.grapheme
}
pub fn font_class(&self) -> FallbackFontClass {
self.font_class
}
}
pub trait MissingGlyphSink: Send + Sync {
fn report(&self, missing_glyphs: Vec<MissingGlyph>);
}
#[expect(missing_docs)]
pub struct NoopTextSystem;
#[expect(missing_docs)]
impl NoopTextSystem {
#[allow(dead_code)]
pub fn new() -> Self {
Self
}
}
impl PlatformTextSystem for NoopTextSystem {
fn add_fonts(&self, _fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
Ok(())
}
fn all_font_names(&self) -> Vec<String> {
Vec::new()
}
fn font_id(&self, _descriptor: &Font) -> Result<FontId> {
Ok(FontId(1))
}
fn font_metrics(&self, _font_id: FontId) -> FontMetrics {
FontMetrics {
units_per_em: 1000,
ascent: 1025.0,
descent: -275.0,
line_gap: 0.0,
underline_position: -95.0,
underline_thickness: 60.0,
cap_height: 698.0,
x_height: 516.0,
bounding_box: Bounds {
origin: Point {
x: -260.0,
y: -245.0,
},
size: Size {
width: 1501.0,
height: 1364.0,
},
},
}
}
fn typographic_bounds(&self, _font_id: FontId, _glyph_id: GlyphId) -> Result<Bounds<f32>> {
Ok(Bounds {
origin: Point { x: 54.0, y: 0.0 },
size: size(392.0, 528.0),
})
}
fn advance(&self, _font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
Ok(size(600.0 * glyph_id.0 as f32, 0.0))
}
fn glyph_for_char(&self, _font_id: FontId, ch: char) -> Option<GlyphId> {
Some(GlyphId(ch.len_utf16() as u32))
}
fn glyph_raster_bounds(&self, _params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
Ok(Default::default())
}
fn rasterize_glyph(
&self,
_params: &RenderGlyphParams,
raster_bounds: Bounds<DevicePixels>,
) -> Result<(Size<DevicePixels>, Vec<u8>)> {
Ok((raster_bounds.size, Vec::new()))
}
fn layout_line(&self, text: &str, font_size: Pixels, _runs: &[FontRun]) -> LineLayout {
let mut position = px(0.);
let metrics = self.font_metrics(FontId(0));
let em_width = font_size
* self
.advance(FontId(0), self.glyph_for_char(FontId(0), 'm').unwrap())
.unwrap()
.width
/ metrics.units_per_em as f32;
let mut glyphs = Vec::new();
for (ix, c) in text.char_indices() {
if let Some(glyph) = self.glyph_for_char(FontId(0), c) {
glyphs.push(ShapedGlyph {
id: glyph,
position: point(position, px(0.)),
index: ix,
is_emoji: glyph.0 == 2,
});
if glyph.0 == 2 {
position += em_width * 2.0;
} else {
position += em_width;
}
} else {
position += em_width
}
}
let mut runs = Vec::default();
if !glyphs.is_empty() {
runs.push(ShapedRun {
font_id: FontId(0),
glyphs,
});
} else {
position = px(0.);
}
LineLayout {
font_size,
width: position,
ascent: font_size * (metrics.ascent / metrics.units_per_em as f32),
descent: font_size * (metrics.descent / metrics.units_per_em as f32),
runs,
len: text.len(),
}
}
fn recommended_rendering_mode(
&self,
_font_id: FontId,
_font_size: Pixels,
) -> TextRenderingMode {
TextRenderingMode::Grayscale
}
}
#[allow(unused)]
pub fn font_name_with_fallbacks<'a>(name: &'a str, system: &'a str) -> &'a str {
match name {
".SystemUIFont" => system,
".ZedSans" | "Zed Plex Sans" => "IBM Plex Sans",
".ZedMono" | "Zed Plex Mono" => "Lilex",
_ => name,
}
}
#[allow(unused)]
pub fn font_name_with_fallbacks_shared<'a>(
name: &'a SharedString,
system: &'a SharedString,
) -> &'a SharedString {
match name.as_str() {
".SystemUIFont" => system,
".ZedSans" | "Zed Plex Sans" => const { &SharedString::new_static("IBM Plex Sans") },
".ZedMono" | "Zed Plex Mono" => const { &SharedString::new_static("Lilex") },
_ => name,
}
}
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct TextRun {
pub len: usize,
pub font: Font,
pub color: Hsla,
pub background_color: Option<Hsla>,
pub underline: Option<UnderlineStyle>,
pub strikethrough: Option<StrikethroughStyle>,
}