1use anyhow::Result;
4use derive_more::{Add, FromStr, Sub};
5use gpui_shared_string::SharedString;
6use gpui_types::{
7 Bounds, DevicePixels, Hsla, Pixels, Point, Size, StrikethroughStyle, UnderlineStyle, point, px,
8 size,
9};
10use schemars::JsonSchema;
11use serde::{Deserialize, Serialize};
12use smallvec::SmallVec;
13use std::borrow::Cow;
14use std::fmt::{Debug, Display, Formatter};
15use std::hash::{Hash, Hasher};
16use std::sync::Arc;
17
18pub const SUBPIXEL_VARIANTS_X: u8 = 4;
20
21pub const SUBPIXEL_VARIANTS_Y: u8 = 1;
23
24use crate::{FontFallbacks, FontFeatures, FontId, GlyphId, RenderGlyphParams};
25
26#[derive(Hash, PartialEq, Eq, Clone, Copy, Debug)]
28pub struct FontFamilyId(pub usize);
29
30#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Serialize, Deserialize, Add, Sub, FromStr)]
33#[serde(transparent)]
34pub struct FontWeight(pub f32);
35
36impl Display for FontWeight {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 write!(f, "{}", self.0)
39 }
40}
41
42impl From<f32> for FontWeight {
43 fn from(weight: f32) -> Self {
44 FontWeight(weight)
45 }
46}
47
48impl Default for FontWeight {
49 #[inline]
50 fn default() -> FontWeight {
51 FontWeight::NORMAL
52 }
53}
54
55impl Hash for FontWeight {
56 fn hash<H: Hasher>(&self, state: &mut H) {
57 state.write_u32(u32::from_be_bytes(self.0.to_be_bytes()));
58 }
59}
60
61impl Eq for FontWeight {}
62
63impl FontWeight {
64 pub const THIN: FontWeight = FontWeight(100.0);
66 pub const EXTRA_LIGHT: FontWeight = FontWeight(200.0);
68 pub const LIGHT: FontWeight = FontWeight(300.0);
70 pub const NORMAL: FontWeight = FontWeight(400.0);
72 pub const MEDIUM: FontWeight = FontWeight(500.0);
74 pub const SEMIBOLD: FontWeight = FontWeight(600.0);
76 pub const BOLD: FontWeight = FontWeight(700.0);
78 pub const EXTRA_BOLD: FontWeight = FontWeight(800.0);
80 pub const BLACK: FontWeight = FontWeight(900.0);
82
83 pub const ALL: [FontWeight; 9] = [
85 Self::THIN,
86 Self::EXTRA_LIGHT,
87 Self::LIGHT,
88 Self::NORMAL,
89 Self::MEDIUM,
90 Self::SEMIBOLD,
91 Self::BOLD,
92 Self::EXTRA_BOLD,
93 Self::BLACK,
94 ];
95}
96
97impl schemars::JsonSchema for FontWeight {
98 fn schema_name() -> Cow<'static, str> {
99 "FontWeight".into()
100 }
101
102 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
103 use schemars::json_schema;
104 json_schema!({
105 "type": "number",
106 "minimum": Self::THIN,
107 "maximum": Self::BLACK,
108 "default": Self::default(),
109 "description": "Font weight value between 100 (thin) and 900 (black)"
110 })
111 }
112}
113
114#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash, Default, Serialize, Deserialize, JsonSchema)]
116pub enum FontStyle {
117 #[default]
119 Normal,
120 Italic,
122 Oblique,
124}
125
126impl Display for FontStyle {
127 fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
128 Debug::fmt(self, f)
129 }
130}
131
132#[derive(Clone, Debug, Eq, PartialEq, Hash)]
134pub struct Font {
135 pub family: SharedString,
139
140 pub features: FontFeatures,
142
143 pub fallbacks: Option<FontFallbacks>,
145
146 pub weight: FontWeight,
148
149 pub style: FontStyle,
151}
152
153impl Default for Font {
154 fn default() -> Self {
155 font(".SystemUIFont")
156 }
157}
158
159pub fn font(family: impl Into<SharedString>) -> Font {
161 Font {
162 family: family.into(),
163 features: FontFeatures::default(),
164 weight: FontWeight::default(),
165 style: FontStyle::default(),
166 fallbacks: None,
167 }
168}
169
170impl Font {
171 pub fn bold(mut self) -> Self {
173 self.weight = FontWeight::BOLD;
174 self
175 }
176
177 pub fn italic(mut self) -> Self {
179 self.style = FontStyle::Italic;
180 self
181 }
182}
183
184#[derive(Clone, Copy, Debug)]
187pub struct FontMetrics {
188 pub units_per_em: u32,
191
192 pub ascent: f32,
194
195 pub descent: f32,
197
198 pub line_gap: f32,
200
201 pub underline_position: f32,
203
204 pub underline_thickness: f32,
206
207 pub cap_height: f32,
209
210 pub x_height: f32,
212
213 pub bounding_box: Bounds<f32>,
216}
217
218impl FontMetrics {
219 pub fn ascent(&self, font_size: Pixels) -> Pixels {
221 Pixels((self.ascent / self.units_per_em as f32) * font_size.0)
222 }
223
224 pub fn descent(&self, font_size: Pixels) -> Pixels {
226 Pixels((self.descent / self.units_per_em as f32) * font_size.0)
227 }
228
229 pub fn line_gap(&self, font_size: Pixels) -> Pixels {
231 Pixels((self.line_gap / self.units_per_em as f32) * font_size.0)
232 }
233
234 pub fn underline_position(&self, font_size: Pixels) -> Pixels {
236 Pixels((self.underline_position / self.units_per_em as f32) * font_size.0)
237 }
238
239 pub fn underline_thickness(&self, font_size: Pixels) -> Pixels {
241 Pixels((self.underline_thickness / self.units_per_em as f32) * font_size.0)
242 }
243
244 pub fn cap_height(&self, font_size: Pixels) -> Pixels {
246 Pixels((self.cap_height / self.units_per_em as f32) * font_size.0)
247 }
248
249 pub fn x_height(&self, font_size: Pixels) -> Pixels {
251 Pixels((self.x_height / self.units_per_em as f32) * font_size.0)
252 }
253
254 pub fn bounding_box(&self, font_size: Pixels) -> Bounds<Pixels> {
256 (self.bounding_box / self.units_per_em as f32 * font_size.0).map(px)
257 }
258}
259
260#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
262#[expect(missing_docs)]
263pub struct FontRun {
264 pub len: usize,
265 pub font_id: FontId,
266}
267
268#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
270pub enum TextRenderingMode {
271 #[default]
273 PlatformDefault,
274 Subpixel,
276 Grayscale,
278}
279
280#[derive(Default, Debug)]
282pub struct LineLayout {
283 pub font_size: Pixels,
285 pub width: Pixels,
287 pub ascent: Pixels,
289 pub descent: Pixels,
291 pub runs: Vec<ShapedRun>,
293 pub len: usize,
295}
296
297#[derive(Debug, Clone)]
299pub struct ShapedRun {
300 pub font_id: FontId,
302 pub glyphs: Vec<ShapedGlyph>,
304}
305
306#[derive(Clone, Debug)]
308pub struct ShapedGlyph {
309 pub id: GlyphId,
311
312 pub position: Point<Pixels>,
314
315 pub index: usize,
317
318 pub is_emoji: bool,
320}
321
322#[derive(Default, Debug)]
324pub struct WrappedLineLayout {
325 pub unwrapped_layout: Arc<LineLayout>,
327
328 pub wrap_boundaries: SmallVec<[WrapBoundary; 1]>,
330
331 pub wrap_width: Option<Pixels>,
333}
334
335#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
337pub struct WrapBoundary {
338 pub run_ix: usize,
340 pub glyph_ix: usize,
342}
343
344impl WrappedLineLayout {
345 #[allow(clippy::len_without_is_empty)]
347 pub fn len(&self) -> usize {
348 self.unwrapped_layout.len
349 }
350
351 pub fn width(&self) -> Pixels {
353 self.wrap_width
354 .unwrap_or(Pixels::MAX)
355 .min(self.unwrapped_layout.width)
356 }
357
358 pub fn size(&self, line_height: Pixels) -> Size<Pixels> {
361 Size {
362 width: self.width(),
363 height: line_height * (self.wrap_boundaries.len() + 1),
364 }
365 }
366
367 pub fn ascent(&self) -> Pixels {
369 self.unwrapped_layout.ascent
370 }
371
372 pub fn descent(&self) -> Pixels {
374 self.unwrapped_layout.descent
375 }
376
377 pub fn wrap_boundaries(&self) -> &[WrapBoundary] {
379 &self.wrap_boundaries
380 }
381
382 pub fn font_size(&self) -> Pixels {
384 self.unwrapped_layout.font_size
385 }
386
387 pub fn runs(&self) -> &[ShapedRun] {
389 &self.unwrapped_layout.runs
390 }
391
392 pub fn index_for_position(
396 &self,
397 position: Point<Pixels>,
398 line_height: Pixels,
399 ) -> Result<usize, usize> {
400 self._index_for_position(position, line_height, false)
401 }
402
403 pub fn closest_index_for_position(
409 &self,
410 position: Point<Pixels>,
411 line_height: Pixels,
412 ) -> Result<usize, usize> {
413 self._index_for_position(position, line_height, true)
414 }
415
416 fn _index_for_position(
417 &self,
418 position: Point<Pixels>,
419 line_height: Pixels,
420 closest: bool,
421 ) -> Result<usize, usize> {
422 let wrapped_line_ix = (position.y / line_height) as usize;
423
424 let wrapped_line_start_index;
425 let wrapped_line_start_x;
426 if wrapped_line_ix > 0 {
427 let Some(line_start_boundary) = self.wrap_boundaries.get(wrapped_line_ix - 1) else {
428 return Err(0);
429 };
430 let run = &self.unwrapped_layout.runs[line_start_boundary.run_ix];
431 let glyph = &run.glyphs[line_start_boundary.glyph_ix];
432 wrapped_line_start_index = glyph.index;
433 wrapped_line_start_x = glyph.position.x;
434 } else {
435 wrapped_line_start_index = 0;
436 wrapped_line_start_x = Pixels::ZERO;
437 };
438
439 let wrapped_line_end_index;
440 let wrapped_line_end_x;
441 if wrapped_line_ix < self.wrap_boundaries.len() {
442 let next_wrap_boundary_ix = wrapped_line_ix;
443 let next_wrap_boundary = self.wrap_boundaries[next_wrap_boundary_ix];
444 let run = &self.unwrapped_layout.runs[next_wrap_boundary.run_ix];
445 let glyph = &run.glyphs[next_wrap_boundary.glyph_ix];
446 wrapped_line_end_index = glyph.index;
447 wrapped_line_end_x = glyph.position.x;
448 } else {
449 wrapped_line_end_index = self.unwrapped_layout.len;
450 wrapped_line_end_x = self.unwrapped_layout.width;
451 };
452
453 let mut position_in_unwrapped_line = position;
454 position_in_unwrapped_line.x += wrapped_line_start_x;
455 if position_in_unwrapped_line.x < wrapped_line_start_x {
456 Err(wrapped_line_start_index)
457 } else if position_in_unwrapped_line.x >= wrapped_line_end_x {
458 Err(wrapped_line_end_index)
459 } else if closest {
460 Ok(self
461 .unwrapped_layout
462 .closest_index_for_x(position_in_unwrapped_line.x))
463 } else {
464 self.unwrapped_layout
467 .index_for_x(position_in_unwrapped_line.x)
468 .ok_or(wrapped_line_end_index)
469 }
470 }
471
472 pub fn position_for_index(&self, index: usize, line_height: Pixels) -> Option<Point<Pixels>> {
474 let mut line_start_ix = 0;
475 let line_end_indices = self
476 .wrap_boundaries
477 .iter()
478 .map(|wrap_boundary| {
479 let run = &self.unwrapped_layout.runs[wrap_boundary.run_ix];
480 let glyph = &run.glyphs[wrap_boundary.glyph_ix];
481 glyph.index
482 })
483 .chain([self.len()])
484 .enumerate();
485 for (ix, line_end_ix) in line_end_indices {
486 let line_y = ix as f32 * line_height;
487 if index < line_start_ix {
488 break;
489 } else if index > line_end_ix {
490 line_start_ix = line_end_ix;
491 continue;
492 } else {
493 let line_start_x = self.unwrapped_layout.x_for_index(line_start_ix);
494 let x = self.unwrapped_layout.x_for_index(index) - line_start_x;
495 return Some(point(x, line_y));
496 }
497 }
498
499 None
500 }
501}
502
503#[derive(Clone, Default)]
506pub struct LineLayoutIndex {
507 pub font_generation: usize,
509 pub lines_index: usize,
511 pub wrapped_lines_index: usize,
513 pub lines_by_hash_index: usize,
515 pub wrapped_lines_by_hash_index: usize,
517}
518
519impl LineLayout {
520 pub fn index_for_x(&self, x: Pixels) -> Option<usize> {
522 if x >= self.width {
523 None
524 } else {
525 for run in self.runs.iter().rev() {
526 for glyph in run.glyphs.iter().rev() {
527 if glyph.position.x <= x {
528 return Some(glyph.index);
529 }
530 }
531 }
532 Some(0)
533 }
534 }
535
536 pub fn closest_index_for_x(&self, x: Pixels) -> usize {
539 let mut prev_index = 0;
540 let mut prev_x = px(0.);
541
542 for run in self.runs.iter() {
543 for glyph in run.glyphs.iter() {
544 if glyph.position.x >= x {
545 if glyph.position.x - x < x - prev_x {
546 return glyph.index;
547 } else {
548 return prev_index;
549 }
550 }
551 prev_index = glyph.index;
552 prev_x = glyph.position.x;
553 }
554 }
555
556 if self.len == 1 {
557 if x > self.width / 2. {
558 return 1;
559 } else {
560 return 0;
561 }
562 }
563
564 self.len
565 }
566
567 pub fn x_for_index(&self, index: usize) -> Pixels {
569 for run in &self.runs {
570 for glyph in &run.glyphs {
571 if glyph.index >= index {
572 return glyph.position.x;
573 }
574 }
575 }
576 self.width
577 }
578
579 pub fn font_id_for_index(&self, index: usize) -> Option<FontId> {
581 for run in &self.runs {
582 for glyph in &run.glyphs {
583 if glyph.index >= index {
584 return Some(run.font_id);
585 }
586 }
587 }
588 None
589 }
590
591 pub fn split_at(&self, byte_index: usize) -> (LineLayout, LineLayout) {
599 let x_offset = self.x_for_index(byte_index);
600
601 let mut left_runs = Vec::new();
603 let mut right_runs = Vec::new();
604
605 for run in &self.runs {
606 let split_pos = run.glyphs.partition_point(|g| g.index < byte_index);
607
608 if split_pos > 0 {
609 left_runs.push(ShapedRun {
610 font_id: run.font_id,
611 glyphs: run.glyphs[..split_pos].to_vec(),
612 });
613 }
614
615 if split_pos < run.glyphs.len() {
616 let right_glyphs = run.glyphs[split_pos..]
617 .iter()
618 .map(|g| ShapedGlyph {
619 id: g.id,
620 position: point(g.position.x - x_offset, g.position.y),
621 index: g.index - byte_index,
622 is_emoji: g.is_emoji,
623 })
624 .collect();
625 right_runs.push(ShapedRun {
626 font_id: run.font_id,
627 glyphs: right_glyphs,
628 });
629 }
630 }
631
632 let left = LineLayout {
633 font_size: self.font_size,
634 width: x_offset,
635 ascent: self.ascent,
636 descent: self.descent,
637 runs: left_runs,
638 len: byte_index,
639 };
640
641 let right = LineLayout {
642 font_size: self.font_size,
643 width: self.width - x_offset,
644 ascent: self.ascent,
645 descent: self.descent,
646 runs: right_runs,
647 len: self.len - byte_index,
648 };
649
650 (left, right)
651 }
652}
653
654#[expect(missing_docs)]
655pub trait PlatformTextSystem: Send + Sync {
656 fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()>;
657 fn set_missing_glyph_sink(&self, _sink: Option<Arc<dyn MissingGlyphSink>>) {}
659 fn all_font_names(&self) -> Vec<String>;
661 fn font_id(&self, descriptor: &Font) -> Result<FontId>;
663 fn prewarm_fonts(&self, _font_ids: &[FontId]) {}
665 fn font_metrics(&self, font_id: FontId) -> FontMetrics;
667 fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>;
669 fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
671 fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
673 fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>>;
675 fn rasterize_glyph(
677 &self,
678 params: &RenderGlyphParams,
679 raster_bounds: Bounds<DevicePixels>,
680 ) -> Result<(Size<DevicePixels>, Vec<u8>)>;
681 fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
683 fn recommended_rendering_mode(&self, _font_id: FontId, _font_size: Pixels)
685 -> TextRenderingMode;
686 fn glyph_dilation_for_color(&self, _color: Hsla) -> u8 {
688 0
689 }
690}
691
692#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
694pub enum FallbackFontClass {
695 Proportional,
697 Monospace,
699}
700
701#[derive(Clone, Debug, Eq, Hash, PartialEq)]
703pub struct MissingGlyph {
704 grapheme: SharedString,
705 font_class: FallbackFontClass,
706}
707
708impl MissingGlyph {
709 pub fn new(grapheme: SharedString, font_class: FallbackFontClass) -> Self {
711 Self {
712 grapheme,
713 font_class,
714 }
715 }
716
717 pub fn grapheme(&self) -> &str {
719 &self.grapheme
720 }
721
722 pub fn font_class(&self) -> FallbackFontClass {
724 self.font_class
725 }
726}
727
728pub trait MissingGlyphSink: Send + Sync {
730 fn report(&self, missing_glyphs: Vec<MissingGlyph>);
732}
733
734#[expect(missing_docs)]
735pub struct NoopTextSystem;
736
737#[expect(missing_docs)]
738impl NoopTextSystem {
739 #[allow(dead_code)]
740 pub fn new() -> Self {
741 Self
742 }
743}
744
745impl PlatformTextSystem for NoopTextSystem {
746 fn add_fonts(&self, _fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
747 Ok(())
748 }
749
750 fn all_font_names(&self) -> Vec<String> {
751 Vec::new()
752 }
753
754 fn font_id(&self, _descriptor: &Font) -> Result<FontId> {
755 Ok(FontId(1))
756 }
757
758 fn font_metrics(&self, _font_id: FontId) -> FontMetrics {
759 FontMetrics {
760 units_per_em: 1000,
761 ascent: 1025.0,
762 descent: -275.0,
763 line_gap: 0.0,
764 underline_position: -95.0,
765 underline_thickness: 60.0,
766 cap_height: 698.0,
767 x_height: 516.0,
768 bounding_box: Bounds {
769 origin: Point {
770 x: -260.0,
771 y: -245.0,
772 },
773 size: Size {
774 width: 1501.0,
775 height: 1364.0,
776 },
777 },
778 }
779 }
780
781 fn typographic_bounds(&self, _font_id: FontId, _glyph_id: GlyphId) -> Result<Bounds<f32>> {
782 Ok(Bounds {
783 origin: Point { x: 54.0, y: 0.0 },
784 size: size(392.0, 528.0),
785 })
786 }
787
788 fn advance(&self, _font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
789 Ok(size(600.0 * glyph_id.0 as f32, 0.0))
790 }
791
792 fn glyph_for_char(&self, _font_id: FontId, ch: char) -> Option<GlyphId> {
793 Some(GlyphId(ch.len_utf16() as u32))
794 }
795
796 fn glyph_raster_bounds(&self, _params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
797 Ok(Default::default())
798 }
799
800 fn rasterize_glyph(
801 &self,
802 _params: &RenderGlyphParams,
803 raster_bounds: Bounds<DevicePixels>,
804 ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
805 Ok((raster_bounds.size, Vec::new()))
806 }
807
808 fn layout_line(&self, text: &str, font_size: Pixels, _runs: &[FontRun]) -> LineLayout {
809 let mut position = px(0.);
810 let metrics = self.font_metrics(FontId(0));
811 let em_width = font_size
812 * self
813 .advance(FontId(0), self.glyph_for_char(FontId(0), 'm').unwrap())
814 .unwrap()
815 .width
816 / metrics.units_per_em as f32;
817 let mut glyphs = Vec::new();
818 for (ix, c) in text.char_indices() {
819 if let Some(glyph) = self.glyph_for_char(FontId(0), c) {
820 glyphs.push(ShapedGlyph {
821 id: glyph,
822 position: point(position, px(0.)),
823 index: ix,
824 is_emoji: glyph.0 == 2,
825 });
826 if glyph.0 == 2 {
827 position += em_width * 2.0;
828 } else {
829 position += em_width;
830 }
831 } else {
832 position += em_width
833 }
834 }
835 let mut runs = Vec::default();
836 if !glyphs.is_empty() {
837 runs.push(ShapedRun {
838 font_id: FontId(0),
839 glyphs,
840 });
841 } else {
842 position = px(0.);
843 }
844
845 LineLayout {
846 font_size,
847 width: position,
848 ascent: font_size * (metrics.ascent / metrics.units_per_em as f32),
849 descent: font_size * (metrics.descent / metrics.units_per_em as f32),
850 runs,
851 len: text.len(),
852 }
853 }
854
855 fn recommended_rendering_mode(
856 &self,
857 _font_id: FontId,
858 _font_size: Pixels,
859 ) -> TextRenderingMode {
860 TextRenderingMode::Grayscale
861 }
862}
863
864#[allow(unused)]
866pub fn font_name_with_fallbacks<'a>(name: &'a str, system: &'a str) -> &'a str {
867 match name {
871 ".SystemUIFont" => system,
872 ".ZedSans" | "Zed Plex Sans" => "IBM Plex Sans",
873 ".ZedMono" | "Zed Plex Mono" => "Lilex",
874 _ => name,
875 }
876}
877
878#[allow(unused)]
880pub fn font_name_with_fallbacks_shared<'a>(
881 name: &'a SharedString,
882 system: &'a SharedString,
883) -> &'a SharedString {
884 match name.as_str() {
888 ".SystemUIFont" => system,
889 ".ZedSans" | "Zed Plex Sans" => const { &SharedString::new_static("IBM Plex Sans") },
890 ".ZedMono" | "Zed Plex Mono" => const { &SharedString::new_static("Lilex") },
891 _ => name,
892 }
893}
894
895#[derive(Clone, Debug, PartialEq, Eq, Default)]
897pub struct TextRun {
898 pub len: usize,
900 pub font: Font,
902 pub color: Hsla,
904 pub background_color: Option<Hsla>,
906 pub underline: Option<UnderlineStyle>,
908 pub strikethrough: Option<StrikethroughStyle>,
910}