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 Ok(self
465 .unwrapped_layout
466 .index_for_x(position_in_unwrapped_line.x)
467 .unwrap())
468 }
469 }
470
471 pub fn position_for_index(&self, index: usize, line_height: Pixels) -> Option<Point<Pixels>> {
473 let mut line_start_ix = 0;
474 let line_end_indices = self
475 .wrap_boundaries
476 .iter()
477 .map(|wrap_boundary| {
478 let run = &self.unwrapped_layout.runs[wrap_boundary.run_ix];
479 let glyph = &run.glyphs[wrap_boundary.glyph_ix];
480 glyph.index
481 })
482 .chain([self.len()])
483 .enumerate();
484 for (ix, line_end_ix) in line_end_indices {
485 let line_y = ix as f32 * line_height;
486 if index < line_start_ix {
487 break;
488 } else if index > line_end_ix {
489 line_start_ix = line_end_ix;
490 continue;
491 } else {
492 let line_start_x = self.unwrapped_layout.x_for_index(line_start_ix);
493 let x = self.unwrapped_layout.x_for_index(index) - line_start_x;
494 return Some(point(x, line_y));
495 }
496 }
497
498 None
499 }
500}
501
502#[derive(Clone, Default)]
505pub struct LineLayoutIndex {
506 pub font_generation: usize,
508 pub lines_index: usize,
510 pub wrapped_lines_index: usize,
512 pub lines_by_hash_index: usize,
514 pub wrapped_lines_by_hash_index: usize,
516}
517
518impl LineLayout {
519 pub fn index_for_x(&self, x: Pixels) -> Option<usize> {
521 if x >= self.width {
522 None
523 } else {
524 for run in self.runs.iter().rev() {
525 for glyph in run.glyphs.iter().rev() {
526 if glyph.position.x <= x {
527 return Some(glyph.index);
528 }
529 }
530 }
531 Some(0)
532 }
533 }
534
535 pub fn closest_index_for_x(&self, x: Pixels) -> usize {
538 let mut prev_index = 0;
539 let mut prev_x = px(0.);
540
541 for run in self.runs.iter() {
542 for glyph in run.glyphs.iter() {
543 if glyph.position.x >= x {
544 if glyph.position.x - x < x - prev_x {
545 return glyph.index;
546 } else {
547 return prev_index;
548 }
549 }
550 prev_index = glyph.index;
551 prev_x = glyph.position.x;
552 }
553 }
554
555 if self.len == 1 {
556 if x > self.width / 2. {
557 return 1;
558 } else {
559 return 0;
560 }
561 }
562
563 self.len
564 }
565
566 pub fn x_for_index(&self, index: usize) -> Pixels {
568 for run in &self.runs {
569 for glyph in &run.glyphs {
570 if glyph.index >= index {
571 return glyph.position.x;
572 }
573 }
574 }
575 self.width
576 }
577
578 pub fn font_id_for_index(&self, index: usize) -> Option<FontId> {
580 for run in &self.runs {
581 for glyph in &run.glyphs {
582 if glyph.index >= index {
583 return Some(run.font_id);
584 }
585 }
586 }
587 None
588 }
589
590 pub fn split_at(&self, byte_index: usize) -> (LineLayout, LineLayout) {
598 let x_offset = self.x_for_index(byte_index);
599
600 let mut left_runs = Vec::new();
602 let mut right_runs = Vec::new();
603
604 for run in &self.runs {
605 let split_pos = run.glyphs.partition_point(|g| g.index < byte_index);
606
607 if split_pos > 0 {
608 left_runs.push(ShapedRun {
609 font_id: run.font_id,
610 glyphs: run.glyphs[..split_pos].to_vec(),
611 });
612 }
613
614 if split_pos < run.glyphs.len() {
615 let right_glyphs = run.glyphs[split_pos..]
616 .iter()
617 .map(|g| ShapedGlyph {
618 id: g.id,
619 position: point(g.position.x - x_offset, g.position.y),
620 index: g.index - byte_index,
621 is_emoji: g.is_emoji,
622 })
623 .collect();
624 right_runs.push(ShapedRun {
625 font_id: run.font_id,
626 glyphs: right_glyphs,
627 });
628 }
629 }
630
631 let left = LineLayout {
632 font_size: self.font_size,
633 width: x_offset,
634 ascent: self.ascent,
635 descent: self.descent,
636 runs: left_runs,
637 len: byte_index,
638 };
639
640 let right = LineLayout {
641 font_size: self.font_size,
642 width: self.width - x_offset,
643 ascent: self.ascent,
644 descent: self.descent,
645 runs: right_runs,
646 len: self.len - byte_index,
647 };
648
649 (left, right)
650 }
651}
652
653#[expect(missing_docs)]
654pub trait PlatformTextSystem: Send + Sync {
655 fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()>;
656 fn set_missing_glyph_sink(&self, _sink: Option<Arc<dyn MissingGlyphSink>>) {}
658 fn all_font_names(&self) -> Vec<String>;
660 fn font_id(&self, descriptor: &Font) -> Result<FontId>;
662 fn prewarm_fonts(&self, _font_ids: &[FontId]) {}
664 fn font_metrics(&self, font_id: FontId) -> FontMetrics;
666 fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>;
668 fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
670 fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
672 fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>>;
674 fn rasterize_glyph(
676 &self,
677 params: &RenderGlyphParams,
678 raster_bounds: Bounds<DevicePixels>,
679 ) -> Result<(Size<DevicePixels>, Vec<u8>)>;
680 fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
682 fn recommended_rendering_mode(&self, _font_id: FontId, _font_size: Pixels)
684 -> TextRenderingMode;
685 fn glyph_dilation_for_color(&self, _color: Hsla) -> u8 {
687 0
688 }
689}
690
691#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
693pub enum FallbackFontClass {
694 Proportional,
696 Monospace,
698}
699
700#[derive(Clone, Debug, Eq, Hash, PartialEq)]
702pub struct MissingGlyph {
703 grapheme: SharedString,
704 font_class: FallbackFontClass,
705}
706
707impl MissingGlyph {
708 pub fn new(grapheme: SharedString, font_class: FallbackFontClass) -> Self {
710 Self {
711 grapheme,
712 font_class,
713 }
714 }
715
716 pub fn grapheme(&self) -> &str {
718 &self.grapheme
719 }
720
721 pub fn font_class(&self) -> FallbackFontClass {
723 self.font_class
724 }
725}
726
727pub trait MissingGlyphSink: Send + Sync {
729 fn report(&self, missing_glyphs: Vec<MissingGlyph>);
731}
732
733#[expect(missing_docs)]
734pub struct NoopTextSystem;
735
736#[expect(missing_docs)]
737impl NoopTextSystem {
738 #[allow(dead_code)]
739 pub fn new() -> Self {
740 Self
741 }
742}
743
744impl PlatformTextSystem for NoopTextSystem {
745 fn add_fonts(&self, _fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
746 Ok(())
747 }
748
749 fn all_font_names(&self) -> Vec<String> {
750 Vec::new()
751 }
752
753 fn font_id(&self, _descriptor: &Font) -> Result<FontId> {
754 Ok(FontId(1))
755 }
756
757 fn font_metrics(&self, _font_id: FontId) -> FontMetrics {
758 FontMetrics {
759 units_per_em: 1000,
760 ascent: 1025.0,
761 descent: -275.0,
762 line_gap: 0.0,
763 underline_position: -95.0,
764 underline_thickness: 60.0,
765 cap_height: 698.0,
766 x_height: 516.0,
767 bounding_box: Bounds {
768 origin: Point {
769 x: -260.0,
770 y: -245.0,
771 },
772 size: Size {
773 width: 1501.0,
774 height: 1364.0,
775 },
776 },
777 }
778 }
779
780 fn typographic_bounds(&self, _font_id: FontId, _glyph_id: GlyphId) -> Result<Bounds<f32>> {
781 Ok(Bounds {
782 origin: Point { x: 54.0, y: 0.0 },
783 size: size(392.0, 528.0),
784 })
785 }
786
787 fn advance(&self, _font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
788 Ok(size(600.0 * glyph_id.0 as f32, 0.0))
789 }
790
791 fn glyph_for_char(&self, _font_id: FontId, ch: char) -> Option<GlyphId> {
792 Some(GlyphId(ch.len_utf16() as u32))
793 }
794
795 fn glyph_raster_bounds(&self, _params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
796 Ok(Default::default())
797 }
798
799 fn rasterize_glyph(
800 &self,
801 _params: &RenderGlyphParams,
802 raster_bounds: Bounds<DevicePixels>,
803 ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
804 Ok((raster_bounds.size, Vec::new()))
805 }
806
807 fn layout_line(&self, text: &str, font_size: Pixels, _runs: &[FontRun]) -> LineLayout {
808 let mut position = px(0.);
809 let metrics = self.font_metrics(FontId(0));
810 let em_width = font_size
811 * self
812 .advance(FontId(0), self.glyph_for_char(FontId(0), 'm').unwrap())
813 .unwrap()
814 .width
815 / metrics.units_per_em as f32;
816 let mut glyphs = Vec::new();
817 for (ix, c) in text.char_indices() {
818 if let Some(glyph) = self.glyph_for_char(FontId(0), c) {
819 glyphs.push(ShapedGlyph {
820 id: glyph,
821 position: point(position, px(0.)),
822 index: ix,
823 is_emoji: glyph.0 == 2,
824 });
825 if glyph.0 == 2 {
826 position += em_width * 2.0;
827 } else {
828 position += em_width;
829 }
830 } else {
831 position += em_width
832 }
833 }
834 let mut runs = Vec::default();
835 if !glyphs.is_empty() {
836 runs.push(ShapedRun {
837 font_id: FontId(0),
838 glyphs,
839 });
840 } else {
841 position = px(0.);
842 }
843
844 LineLayout {
845 font_size,
846 width: position,
847 ascent: font_size * (metrics.ascent / metrics.units_per_em as f32),
848 descent: font_size * (metrics.descent / metrics.units_per_em as f32),
849 runs,
850 len: text.len(),
851 }
852 }
853
854 fn recommended_rendering_mode(
855 &self,
856 _font_id: FontId,
857 _font_size: Pixels,
858 ) -> TextRenderingMode {
859 TextRenderingMode::Grayscale
860 }
861}
862
863#[allow(unused)]
865pub fn font_name_with_fallbacks<'a>(name: &'a str, system: &'a str) -> &'a str {
866 match name {
870 ".SystemUIFont" => system,
871 ".ZedSans" | "Zed Plex Sans" => "IBM Plex Sans",
872 ".ZedMono" | "Zed Plex Mono" => "Lilex",
873 _ => name,
874 }
875}
876
877#[allow(unused)]
879pub fn font_name_with_fallbacks_shared<'a>(
880 name: &'a SharedString,
881 system: &'a SharedString,
882) -> &'a SharedString {
883 match name.as_str() {
887 ".SystemUIFont" => system,
888 ".ZedSans" | "Zed Plex Sans" => const { &SharedString::new_static("IBM Plex Sans") },
889 ".ZedMono" | "Zed Plex Mono" => const { &SharedString::new_static("Lilex") },
890 _ => name,
891 }
892}
893
894#[derive(Clone, Debug, PartialEq, Eq, Default)]
896pub struct TextRun {
897 pub len: usize,
899 pub font: Font,
901 pub color: Hsla,
903 pub background_color: Option<Hsla>,
905 pub underline: Option<UnderlineStyle>,
907 pub strikethrough: Option<StrikethroughStyle>,
909}