1mod font_fallbacks;
2mod font_features;
3mod line;
4mod line_layout;
5mod line_wrapper;
6
7pub use font_fallbacks::*;
8pub use font_features::*;
9pub use line::*;
10pub use line_layout::*;
11pub use line_wrapper::*;
12use schemars::JsonSchema;
13use serde::{Deserialize, Serialize};
14
15use crate::{
16 Bounds, DevicePixels, Hsla, Pixels, PlatformTextSystem, Point, Result, SharedString, Size,
17 StrikethroughStyle, TextRenderingMode, UnderlineStyle, px,
18};
19use anyhow::{Context as _, anyhow};
20use collections::FxHashMap;
21use core::fmt;
22use derive_more::{Add, Deref, FromStr, Sub};
23use itertools::Itertools;
24use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
25use smallvec::{SmallVec, smallvec};
26use std::{
27 borrow::Cow,
28 cmp,
29 fmt::{Debug, Display, Formatter},
30 hash::{Hash, Hasher},
31 ops::{Deref, DerefMut, Range},
32 sync::Arc,
33};
34
35/// An opaque identifier for a specific font.
36#[derive(Hash, PartialEq, Eq, Clone, Copy, Debug)]
37#[repr(C)]
38pub struct FontId(pub usize);
39
40/// An opaque identifier for a specific font family.
41#[derive(Hash, PartialEq, Eq, Clone, Copy, Debug)]
42pub struct FontFamilyId(pub usize);
43
44/// Number of subpixel glyph variants along the X axis.
45pub const SUBPIXEL_VARIANTS_X: u8 = 4;
46
47/// Number of subpixel glyph variants along the Y axis.
48pub const SUBPIXEL_VARIANTS_Y: u8 = if cfg!(target_os = "windows") || cfg!(target_os = "linux") {
49 1
50} else {
51 SUBPIXEL_VARIANTS_X
52};
53
54/// The GPUI text rendering sub system.
55pub struct TextSystem {
56 platform_text_system: Arc<dyn PlatformTextSystem>,
57 font_ids_by_font: RwLock<FxHashMap<Font, Result<FontId>>>,
58 font_metrics: RwLock<FxHashMap<FontId, FontMetrics>>,
59 raster_bounds: RwLock<FxHashMap<RenderGlyphParams, Bounds<DevicePixels>>>,
60 wrapper_pool: Mutex<FxHashMap<FontIdWithSize, Vec<LineWrapper>>>,
61 font_runs_pool: Mutex<Vec<Vec<FontRun>>>,
62 fallback_font_stack: SmallVec<[Font; 2]>,
63}
64
65impl TextSystem {
66 /// Create a new TextSystem with the given platform text system.
67 pub fn new(platform_text_system: Arc<dyn PlatformTextSystem>) -> Self {
68 TextSystem {
69 platform_text_system,
70 font_metrics: RwLock::default(),
71 raster_bounds: RwLock::default(),
72 font_ids_by_font: RwLock::default(),
73 wrapper_pool: Mutex::default(),
74 font_runs_pool: Mutex::default(),
75 fallback_font_stack: smallvec![
76 // TODO: Remove this when Linux have implemented setting fallbacks.
77 font(".ZedMono"),
78 font(".ZedSans"),
79 font("Helvetica"),
80 font("Segoe UI"), // Windows
81 font("Ubuntu"), // Gnome (Ubuntu)
82 font("Adwaita Sans"), // Gnome 47
83 font("Cantarell"), // Gnome
84 font("Noto Sans"), // KDE
85 font("DejaVu Sans"),
86 font("Arial"), // macOS, Windows
87 ],
88 }
89 }
90
91 /// Get a list of all available font names from the operating system.
92 pub fn all_font_names(&self) -> Vec<String> {
93 let mut names = self.platform_text_system.all_font_names();
94 names.extend(
95 self.fallback_font_stack
96 .iter()
97 .map(|font| font.family.to_string()),
98 );
99 names.push(".SystemUIFont".to_string());
100 names.sort();
101 names.dedup();
102 names
103 }
104
105 /// Add a font's data to the text system.
106 pub fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
107 self.platform_text_system.add_fonts(fonts)
108 }
109
110 /// Get the FontId for the configure font family and style.
111 fn font_id(&self, font: &Font) -> Result<FontId> {
112 fn clone_font_id_result(font_id: &Result<FontId>) -> Result<FontId> {
113 match font_id {
114 Ok(font_id) => Ok(*font_id),
115 Err(err) => Err(anyhow!("{err}")),
116 }
117 }
118
119 let font_id = self
120 .font_ids_by_font
121 .read()
122 .get(font)
123 .map(clone_font_id_result);
124 if let Some(font_id) = font_id {
125 font_id
126 } else {
127 let font_id = self.platform_text_system.font_id(font);
128 self.font_ids_by_font
129 .write()
130 .insert(font.clone(), clone_font_id_result(&font_id));
131 font_id
132 }
133 }
134
135 /// Get the Font for the Font Id.
136 pub fn get_font_for_id(&self, id: FontId) -> Option<Font> {
137 let lock = self.font_ids_by_font.read();
138 lock.iter()
139 .filter_map(|(font, result)| match result {
140 Ok(font_id) if *font_id == id => Some(font.clone()),
141 _ => None,
142 })
143 .next()
144 }
145
146 /// Resolves the specified font, falling back to the default font stack if
147 /// the font fails to load.
148 ///
149 /// # Panics
150 ///
151 /// Panics if the font and none of the fallbacks can be resolved.
152 pub fn resolve_font(&self, font: &Font) -> FontId {
153 if let Ok(font_id) = self.font_id(font) {
154 return font_id;
155 }
156 for fallback in &self.fallback_font_stack {
157 if let Ok(font_id) = self.font_id(fallback) {
158 return font_id;
159 }
160 }
161
162 panic!(
163 "failed to resolve font '{}' or any of the fallbacks: {}",
164 font.family,
165 self.fallback_font_stack
166 .iter()
167 .map(|fallback| &fallback.family)
168 .join(", ")
169 );
170 }
171
172 /// Get the bounding box for the given font and font size.
173 /// A font's bounding box is the smallest rectangle that could enclose all glyphs
174 /// in the font. superimposed over one another.
175 pub fn bounding_box(&self, font_id: FontId, font_size: Pixels) -> Bounds<Pixels> {
176 self.read_metrics(font_id, |metrics| metrics.bounding_box(font_size))
177 }
178
179 /// Get the typographic bounds for the given character, in the given font and size.
180 pub fn typographic_bounds(
181 &self,
182 font_id: FontId,
183 font_size: Pixels,
184 character: char,
185 ) -> Result<Bounds<Pixels>> {
186 let glyph_id = self
187 .platform_text_system
188 .glyph_for_char(font_id, character)
189 .with_context(|| format!("glyph not found for character '{character}'"))?;
190 let bounds = self
191 .platform_text_system
192 .typographic_bounds(font_id, glyph_id)?;
193 Ok(self.read_metrics(font_id, |metrics| {
194 (bounds / metrics.units_per_em as f32 * font_size.0).map(px)
195 }))
196 }
197
198 /// Get the advance width for the given character, in the given font and size.
199 pub fn advance(&self, font_id: FontId, font_size: Pixels, ch: char) -> Result<Size<Pixels>> {
200 let glyph_id = self
201 .platform_text_system
202 .glyph_for_char(font_id, ch)
203 .with_context(|| format!("glyph not found for character '{ch}'"))?;
204 let result = self.platform_text_system.advance(font_id, glyph_id)?
205 / self.units_per_em(font_id) as f32;
206
207 Ok(result * font_size)
208 }
209
210 // Consider removing this?
211 /// Returns the shaped layout width of for the given character, in the given font and size.
212 pub fn layout_width(&self, font_id: FontId, font_size: Pixels, ch: char) -> Pixels {
213 let mut buffer = [0; 4];
214 let buffer = ch.encode_utf8(&mut buffer);
215 self.platform_text_system
216 .layout_line(
217 buffer,
218 font_size,
219 &[FontRun {
220 len: buffer.len(),
221 font_id,
222 }],
223 )
224 .width
225 }
226
227 /// Returns the width of an `em`.
228 ///
229 /// Uses the width of the `m` character in the given font and size.
230 pub fn em_width(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
231 Ok(self.typographic_bounds(font_id, font_size, 'm')?.size.width)
232 }
233
234 /// Returns the advance width of an `em`.
235 ///
236 /// Uses the advance width of the `m` character in the given font and size.
237 pub fn em_advance(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
238 Ok(self.advance(font_id, font_size, 'm')?.width)
239 }
240
241 // Consider removing this?
242 /// Returns the shaped layout width of an `em`.
243 pub fn em_layout_width(&self, font_id: FontId, font_size: Pixels) -> Pixels {
244 self.layout_width(font_id, font_size, 'm')
245 }
246
247 /// Returns the width of an `ch`.
248 ///
249 /// Uses the width of the `0` character in the given font and size.
250 pub fn ch_width(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
251 Ok(self.typographic_bounds(font_id, font_size, '0')?.size.width)
252 }
253
254 /// Returns the advance width of an `ch`.
255 ///
256 /// Uses the advance width of the `0` character in the given font and size.
257 pub fn ch_advance(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
258 Ok(self.advance(font_id, font_size, '0')?.width)
259 }
260
261 /// Get the number of font size units per 'em square',
262 /// Per MDN: "an abstract square whose height is the intended distance between
263 /// lines of type in the same type size"
264 pub fn units_per_em(&self, font_id: FontId) -> u32 {
265 self.read_metrics(font_id, |metrics| metrics.units_per_em)
266 }
267
268 /// Get the height of a capital letter in the given font and size.
269 pub fn cap_height(&self, font_id: FontId, font_size: Pixels) -> Pixels {
270 self.read_metrics(font_id, |metrics| metrics.cap_height(font_size))
271 }
272
273 /// Get the height of the x character in the given font and size.
274 pub fn x_height(&self, font_id: FontId, font_size: Pixels) -> Pixels {
275 self.read_metrics(font_id, |metrics| metrics.x_height(font_size))
276 }
277
278 /// Get the recommended distance from the baseline for the given font
279 pub fn ascent(&self, font_id: FontId, font_size: Pixels) -> Pixels {
280 self.read_metrics(font_id, |metrics| metrics.ascent(font_size))
281 }
282
283 /// Get the recommended distance below the baseline for the given font,
284 /// in single spaced text.
285 pub fn descent(&self, font_id: FontId, font_size: Pixels) -> Pixels {
286 self.read_metrics(font_id, |metrics| metrics.descent(font_size))
287 }
288
289 /// Get the recommended baseline offset for the given font and line height.
290 pub fn baseline_offset(
291 &self,
292 font_id: FontId,
293 font_size: Pixels,
294 line_height: Pixels,
295 ) -> Pixels {
296 let ascent = self.ascent(font_id, font_size);
297 let descent = self.descent(font_id, font_size);
298 let padding_top = (line_height - ascent - descent) / 2.;
299 padding_top + ascent
300 }
301
302 fn read_metrics<T>(&self, font_id: FontId, read: impl FnOnce(&FontMetrics) -> T) -> T {
303 let lock = self.font_metrics.upgradable_read();
304
305 if let Some(metrics) = lock.get(&font_id) {
306 read(metrics)
307 } else {
308 let mut lock = RwLockUpgradableReadGuard::upgrade(lock);
309 let metrics = lock
310 .entry(font_id)
311 .or_insert_with(|| self.platform_text_system.font_metrics(font_id));
312 read(metrics)
313 }
314 }
315
316 /// Returns a handle to a line wrapper, for the given font and font size.
317 pub fn line_wrapper(self: &Arc<Self>, font: Font, font_size: Pixels) -> LineWrapperHandle {
318 let lock = &mut self.wrapper_pool.lock();
319 let font_id = self.resolve_font(&font);
320 let wrappers = lock
321 .entry(FontIdWithSize { font_id, font_size })
322 .or_default();
323 let wrapper = wrappers
324 .pop()
325 .unwrap_or_else(|| LineWrapper::new(font_id, font_size, self.clone()));
326
327 LineWrapperHandle {
328 wrapper: Some(wrapper),
329 text_system: self.clone(),
330 }
331 }
332
333 /// Get the rasterized size and location of a specific, rendered glyph.
334 pub(crate) fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
335 let raster_bounds = self.raster_bounds.upgradable_read();
336 if let Some(bounds) = raster_bounds.get(params) {
337 Ok(*bounds)
338 } else {
339 let mut raster_bounds = RwLockUpgradableReadGuard::upgrade(raster_bounds);
340 let bounds = self.platform_text_system.glyph_raster_bounds(params)?;
341 raster_bounds.insert(params.clone(), bounds);
342 Ok(bounds)
343 }
344 }
345
346 pub(crate) fn rasterize_glyph(
347 &self,
348 params: &RenderGlyphParams,
349 ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
350 let raster_bounds = self.raster_bounds(params)?;
351 self.platform_text_system
352 .rasterize_glyph(params, raster_bounds)
353 }
354
355 /// Returns the text rendering mode recommended by the platform for the given font and size.
356 /// The return value will never be [`TextRenderingMode::PlatformDefault`].
357 pub(crate) fn recommended_rendering_mode(
358 &self,
359 font_id: FontId,
360 font_size: Pixels,
361 ) -> TextRenderingMode {
362 self.platform_text_system
363 .recommended_rendering_mode(font_id, font_size)
364 }
365}
366
367/// The GPUI text layout subsystem.
368#[derive(Deref)]
369pub struct WindowTextSystem {
370 line_layout_cache: LineLayoutCache,
371 #[deref]
372 text_system: Arc<TextSystem>,
373}
374
375impl WindowTextSystem {
376 /// Create a new WindowTextSystem with the given TextSystem.
377 pub fn new(text_system: Arc<TextSystem>) -> Self {
378 Self {
379 line_layout_cache: LineLayoutCache::new(text_system.platform_text_system.clone()),
380 text_system,
381 }
382 }
383
384 pub(crate) fn layout_index(&self) -> LineLayoutIndex {
385 self.line_layout_cache.layout_index()
386 }
387
388 pub(crate) fn reuse_layouts(&self, index: Range<LineLayoutIndex>) {
389 self.line_layout_cache.reuse_layouts(index)
390 }
391
392 pub(crate) fn truncate_layouts(&self, index: LineLayoutIndex) {
393 self.line_layout_cache.truncate_layouts(index)
394 }
395
396 /// Shape the given line, at the given font_size, for painting to the screen.
397 /// Subsets of the line can be styled independently with the `runs` parameter.
398 ///
399 /// Note that this method can only shape a single line of text. It will panic
400 /// if the text contains newlines. If you need to shape multiple lines of text,
401 /// use [`Self::shape_text`] instead.
402 pub fn shape_line(
403 &self,
404 text: SharedString,
405 font_size: Pixels,
406 runs: &[TextRun],
407 force_width: Option<Pixels>,
408 ) -> ShapedLine {
409 debug_assert!(
410 text.find('\n').is_none(),
411 "text argument should not contain newlines"
412 );
413
414 let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new();
415 for run in runs {
416 if let Some(last_run) = decoration_runs.last_mut()
417 && last_run.color == run.color
418 && last_run.underline == run.underline
419 && last_run.strikethrough == run.strikethrough
420 && last_run.background_color == run.background_color
421 {
422 last_run.len += run.len as u32;
423 continue;
424 }
425 decoration_runs.push(DecorationRun {
426 len: run.len as u32,
427 color: run.color,
428 background_color: run.background_color,
429 underline: run.underline,
430 strikethrough: run.strikethrough,
431 });
432 }
433
434 let layout = self.layout_line(&text, font_size, runs, force_width);
435
436 ShapedLine {
437 layout,
438 text,
439 decoration_runs,
440 }
441 }
442
443 /// Shape the given line using a caller-provided content hash as the cache key.
444 ///
445 /// This enables cache hits without materializing a contiguous `SharedString` for the text.
446 /// If the cache misses, `materialize_text` is invoked to produce the `SharedString` for shaping.
447 ///
448 /// Contract (caller enforced):
449 /// - Same `text_hash` implies identical text content (collision risk accepted by caller).
450 /// - `text_len` should be the UTF-8 byte length of the text (helps reduce accidental collisions).
451 ///
452 /// Like [`Self::shape_line`], this must be used only for single-line text (no `\n`).
453 pub fn shape_line_by_hash(
454 &self,
455 text_hash: u64,
456 text_len: usize,
457 font_size: Pixels,
458 runs: &[TextRun],
459 force_width: Option<Pixels>,
460 materialize_text: impl FnOnce() -> SharedString,
461 ) -> ShapedLine {
462 let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new();
463 for run in runs {
464 if let Some(last_run) = decoration_runs.last_mut()
465 && last_run.color == run.color
466 && last_run.underline == run.underline
467 && last_run.strikethrough == run.strikethrough
468 && last_run.background_color == run.background_color
469 {
470 last_run.len += run.len as u32;
471 continue;
472 }
473 decoration_runs.push(DecorationRun {
474 len: run.len as u32,
475 color: run.color,
476 background_color: run.background_color,
477 underline: run.underline,
478 strikethrough: run.strikethrough,
479 });
480 }
481
482 let mut used_force_width = force_width;
483 let layout = self.layout_line_by_hash(
484 text_hash,
485 text_len,
486 font_size,
487 runs,
488 used_force_width,
489 || {
490 let text = materialize_text();
491 debug_assert!(
492 text.find('\n').is_none(),
493 "text argument should not contain newlines"
494 );
495 text
496 },
497 );
498
499 // We only materialize actual text on cache miss; on hit we avoid allocations.
500 // Since `ShapedLine` carries a `SharedString`, use an empty placeholder for hits.
501 // NOTE: Callers must not rely on `ShapedLine.text` for content when using this API.
502 let text: SharedString = SharedString::new_static("");
503
504 ShapedLine {
505 layout,
506 text,
507 decoration_runs,
508 }
509 }
510
511 /// Shape a multi line string of text, at the given font_size, for painting to the screen.
512 /// Subsets of the text can be styled independently with the `runs` parameter.
513 /// If `wrap_width` is provided, the line breaks will be adjusted to fit within the given width.
514 pub fn shape_text(
515 &self,
516 text: SharedString,
517 font_size: Pixels,
518 runs: &[TextRun],
519 wrap_width: Option<Pixels>,
520 line_clamp: Option<usize>,
521 ) -> Result<SmallVec<[WrappedLine; 1]>> {
522 let mut runs = runs.iter().filter(|run| run.len > 0).cloned().peekable();
523 let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();
524
525 let mut lines = SmallVec::new();
526 let mut max_wrap_lines = line_clamp;
527 let mut wrapped_lines = 0;
528
529 let mut process_line = |line_text: SharedString, line_start, line_end| {
530 font_runs.clear();
531
532 let mut decoration_runs = <Vec<DecorationRun>>::with_capacity(32);
533 let mut run_start = line_start;
534 while run_start < line_end {
535 let Some(run) = runs.peek_mut() else {
536 log::warn!("`TextRun`s do not cover the entire to be shaped text");
537 break;
538 };
539
540 let run_len_within_line = cmp::min(line_end - run_start, run.len);
541
542 let decoration_changed = if let Some(last_run) = decoration_runs.last_mut()
543 && last_run.color == run.color
544 && last_run.underline == run.underline
545 && last_run.strikethrough == run.strikethrough
546 && last_run.background_color == run.background_color
547 {
548 last_run.len += run_len_within_line as u32;
549 false
550 } else {
551 decoration_runs.push(DecorationRun {
552 len: run_len_within_line as u32,
553 color: run.color,
554 background_color: run.background_color,
555 underline: run.underline,
556 strikethrough: run.strikethrough,
557 });
558 true
559 };
560
561 let font_id = self.resolve_font(&run.font);
562 if let Some(font_run) = font_runs.last_mut()
563 && font_id == font_run.font_id
564 && !decoration_changed
565 {
566 font_run.len += run_len_within_line;
567 } else {
568 font_runs.push(FontRun {
569 len: run_len_within_line,
570 font_id,
571 });
572 }
573
574 // Preserve the remainder of the run for the next line
575 run.len -= run_len_within_line;
576 if run.len == 0 {
577 runs.next();
578 }
579 run_start += run_len_within_line;
580 }
581
582 let layout = self.line_layout_cache.layout_wrapped_line(
583 &line_text,
584 font_size,
585 &font_runs,
586 wrap_width,
587 max_wrap_lines.map(|max| max.saturating_sub(wrapped_lines)),
588 );
589 wrapped_lines += layout.wrap_boundaries.len();
590
591 lines.push(WrappedLine {
592 layout,
593 decoration_runs,
594 text: line_text,
595 });
596
597 // Skip `\n` character.
598 if let Some(run) = runs.peek_mut() {
599 run.len -= 1;
600 if run.len == 0 {
601 runs.next();
602 }
603 }
604 };
605
606 let mut split_lines = text.split('\n');
607
608 // Special case single lines to prevent allocating a sharedstring
609 if let Some(first_line) = split_lines.next()
610 && let Some(second_line) = split_lines.next()
611 {
612 let mut line_start = 0;
613 process_line(
614 SharedString::new(first_line),
615 line_start,
616 line_start + first_line.len(),
617 );
618 line_start += first_line.len() + '\n'.len_utf8();
619 process_line(
620 SharedString::new(second_line),
621 line_start,
622 line_start + second_line.len(),
623 );
624 for line_text in split_lines {
625 line_start += line_text.len() + '\n'.len_utf8();
626 process_line(
627 SharedString::new(line_text),
628 line_start,
629 line_start + line_text.len(),
630 );
631 }
632 } else {
633 let end = text.len();
634 process_line(text, 0, end);
635 }
636
637 self.font_runs_pool.lock().push(font_runs);
638
639 Ok(lines)
640 }
641
642 pub(crate) fn finish_frame(&self) {
643 self.line_layout_cache.finish_frame()
644 }
645
646 /// Layout the given line of text, at the given font_size.
647 /// Subsets of the line can be styled independently with the `runs` parameter.
648 /// Generally, you should prefer to use [`Self::shape_line`] instead, which
649 /// can be painted directly.
650 pub fn layout_line(
651 &self,
652 text: &str,
653 font_size: Pixels,
654 runs: &[TextRun],
655 force_width: Option<Pixels>,
656 ) -> Arc<LineLayout> {
657 let mut last_run = None::<&TextRun>;
658 let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();
659 font_runs.clear();
660
661 for run in runs.iter() {
662 let decoration_changed = if let Some(last_run) = last_run
663 && last_run.color == run.color
664 && last_run.underline == run.underline
665 && last_run.strikethrough == run.strikethrough
666 // we do not consider differing background color relevant, as it does not affect glyphs
667 // && last_run.background_color == run.background_color
668 {
669 false
670 } else {
671 last_run = Some(run);
672 true
673 };
674
675 let font_id = self.resolve_font(&run.font);
676 if let Some(font_run) = font_runs.last_mut()
677 && font_id == font_run.font_id
678 && !decoration_changed
679 {
680 font_run.len += run.len;
681 } else {
682 font_runs.push(FontRun {
683 len: run.len,
684 font_id,
685 });
686 }
687 }
688
689 let layout = self.line_layout_cache.layout_line(
690 &SharedString::new(text),
691 font_size,
692 &font_runs,
693 force_width,
694 );
695
696 self.font_runs_pool.lock().push(font_runs);
697
698 layout
699 }
700
701 /// Probe the line layout cache using a caller-provided content hash, without allocating.
702 ///
703 /// Returns `Some(layout)` if the layout is already cached in either the current frame
704 /// or the previous frame. Returns `None` if it is not cached.
705 ///
706 /// Contract (caller enforced):
707 /// - Same `text_hash` implies identical text content (collision risk accepted by caller).
708 /// - `text_len` should be the UTF-8 byte length of the text (helps reduce accidental collisions).
709 pub fn try_layout_line_by_hash(
710 &self,
711 text_hash: u64,
712 text_len: usize,
713 font_size: Pixels,
714 runs: &[TextRun],
715 force_width: Option<Pixels>,
716 ) -> Option<Arc<LineLayout>> {
717 let mut last_run = None::<&TextRun>;
718 let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();
719 font_runs.clear();
720
721 for run in runs.iter() {
722 let decoration_changed = if let Some(last_run) = last_run
723 && last_run.color == run.color
724 && last_run.underline == run.underline
725 && last_run.strikethrough == run.strikethrough
726 // we do not consider differing background color relevant, as it does not affect glyphs
727 // && last_run.background_color == run.background_color
728 {
729 false
730 } else {
731 last_run = Some(run);
732 true
733 };
734
735 let font_id = self.resolve_font(&run.font);
736 if let Some(font_run) = font_runs.last_mut()
737 && font_id == font_run.font_id
738 && !decoration_changed
739 {
740 font_run.len += run.len;
741 } else {
742 font_runs.push(FontRun {
743 len: run.len,
744 font_id,
745 });
746 }
747 }
748
749 let layout = self.line_layout_cache.try_layout_line_by_hash(
750 text_hash,
751 text_len,
752 font_size,
753 &font_runs,
754 force_width,
755 );
756
757 self.font_runs_pool.lock().push(font_runs);
758
759 layout
760 }
761
762 /// Layout the given line of text using a caller-provided content hash as the cache key.
763 ///
764 /// This enables cache hits without materializing a contiguous `SharedString` for the text.
765 /// If the cache misses, `materialize_text` is invoked to produce the `SharedString` for shaping.
766 ///
767 /// Contract (caller enforced):
768 /// - Same `text_hash` implies identical text content (collision risk accepted by caller).
769 /// - `text_len` should be the UTF-8 byte length of the text (helps reduce accidental collisions).
770 pub fn layout_line_by_hash(
771 &self,
772 text_hash: u64,
773 text_len: usize,
774 font_size: Pixels,
775 runs: &[TextRun],
776 force_width: Option<Pixels>,
777 materialize_text: impl FnOnce() -> SharedString,
778 ) -> Arc<LineLayout> {
779 let mut last_run = None::<&TextRun>;
780 let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();
781 font_runs.clear();
782
783 for run in runs.iter() {
784 let decoration_changed = if let Some(last_run) = last_run
785 && last_run.color == run.color
786 && last_run.underline == run.underline
787 && last_run.strikethrough == run.strikethrough
788 // we do not consider differing background color relevant, as it does not affect glyphs
789 // && last_run.background_color == run.background_color
790 {
791 false
792 } else {
793 last_run = Some(run);
794 true
795 };
796
797 let font_id = self.resolve_font(&run.font);
798 if let Some(font_run) = font_runs.last_mut()
799 && font_id == font_run.font_id
800 && !decoration_changed
801 {
802 font_run.len += run.len;
803 } else {
804 font_runs.push(FontRun {
805 len: run.len,
806 font_id,
807 });
808 }
809 }
810
811 let layout = self.line_layout_cache.layout_line_by_hash(
812 text_hash,
813 text_len,
814 font_size,
815 &font_runs,
816 force_width,
817 materialize_text,
818 );
819
820 self.font_runs_pool.lock().push(font_runs);
821
822 layout
823 }
824}
825
826#[derive(Hash, Eq, PartialEq)]
827struct FontIdWithSize {
828 font_id: FontId,
829 font_size: Pixels,
830}
831
832/// A handle into the text system, which can be used to compute the wrapped layout of text
833pub struct LineWrapperHandle {
834 wrapper: Option<LineWrapper>,
835 text_system: Arc<TextSystem>,
836}
837
838impl Drop for LineWrapperHandle {
839 fn drop(&mut self) {
840 let mut state = self.text_system.wrapper_pool.lock();
841 let wrapper = self.wrapper.take().unwrap();
842 state
843 .get_mut(&FontIdWithSize {
844 font_id: wrapper.font_id,
845 font_size: wrapper.font_size,
846 })
847 .unwrap()
848 .push(wrapper);
849 }
850}
851
852impl Deref for LineWrapperHandle {
853 type Target = LineWrapper;
854
855 fn deref(&self) -> &Self::Target {
856 self.wrapper.as_ref().unwrap()
857 }
858}
859
860impl DerefMut for LineWrapperHandle {
861 fn deref_mut(&mut self) -> &mut Self::Target {
862 self.wrapper.as_mut().unwrap()
863 }
864}
865
866/// The degree of blackness or stroke thickness of a font. This value ranges from 100.0 to 900.0,
867/// with 400.0 as normal.
868#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Serialize, Deserialize, Add, Sub, FromStr)]
869#[serde(transparent)]
870pub struct FontWeight(pub f32);
871
872impl Display for FontWeight {
873 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
874 write!(f, "{}", self.0)
875 }
876}
877
878impl From<f32> for FontWeight {
879 fn from(weight: f32) -> Self {
880 FontWeight(weight)
881 }
882}
883
884impl Default for FontWeight {
885 #[inline]
886 fn default() -> FontWeight {
887 FontWeight::NORMAL
888 }
889}
890
891impl Hash for FontWeight {
892 fn hash<H: Hasher>(&self, state: &mut H) {
893 state.write_u32(u32::from_be_bytes(self.0.to_be_bytes()));
894 }
895}
896
897impl Eq for FontWeight {}
898
899impl FontWeight {
900 /// Thin weight (100), the thinnest value.
901 pub const THIN: FontWeight = FontWeight(100.0);
902 /// Extra light weight (200).
903 pub const EXTRA_LIGHT: FontWeight = FontWeight(200.0);
904 /// Light weight (300).
905 pub const LIGHT: FontWeight = FontWeight(300.0);
906 /// Normal (400).
907 pub const NORMAL: FontWeight = FontWeight(400.0);
908 /// Medium weight (500, higher than normal).
909 pub const MEDIUM: FontWeight = FontWeight(500.0);
910 /// Semibold weight (600).
911 pub const SEMIBOLD: FontWeight = FontWeight(600.0);
912 /// Bold weight (700).
913 pub const BOLD: FontWeight = FontWeight(700.0);
914 /// Extra-bold weight (800).
915 pub const EXTRA_BOLD: FontWeight = FontWeight(800.0);
916 /// Black weight (900), the thickest value.
917 pub const BLACK: FontWeight = FontWeight(900.0);
918
919 /// All of the font weights, in order from thinnest to thickest.
920 pub const ALL: [FontWeight; 9] = [
921 Self::THIN,
922 Self::EXTRA_LIGHT,
923 Self::LIGHT,
924 Self::NORMAL,
925 Self::MEDIUM,
926 Self::SEMIBOLD,
927 Self::BOLD,
928 Self::EXTRA_BOLD,
929 Self::BLACK,
930 ];
931}
932
933impl schemars::JsonSchema for FontWeight {
934 fn schema_name() -> std::borrow::Cow<'static, str> {
935 "FontWeight".into()
936 }
937
938 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
939 use schemars::json_schema;
940 json_schema!({
941 "type": "number",
942 "minimum": Self::THIN,
943 "maximum": Self::BLACK,
944 "default": Self::default(),
945 "description": "Font weight value between 100 (thin) and 900 (black)"
946 })
947 }
948}
949
950/// Allows italic or oblique faces to be selected.
951#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash, Default, Serialize, Deserialize, JsonSchema)]
952pub enum FontStyle {
953 /// A face that is neither italic not obliqued.
954 #[default]
955 Normal,
956 /// A form that is generally cursive in nature.
957 Italic,
958 /// A typically-sloped version of the regular face.
959 Oblique,
960}
961
962impl Display for FontStyle {
963 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
964 Debug::fmt(self, f)
965 }
966}
967
968/// A styled run of text, for use in [`crate::TextLayout`].
969#[derive(Clone, Debug, PartialEq, Eq, Default)]
970pub struct TextRun {
971 /// A number of utf8 bytes
972 pub len: usize,
973 /// The font to use for this run.
974 pub font: Font,
975 /// The color
976 pub color: Hsla,
977 /// The background color (if any)
978 pub background_color: Option<Hsla>,
979 /// The underline style (if any)
980 pub underline: Option<UnderlineStyle>,
981 /// The strikethrough style (if any)
982 pub strikethrough: Option<StrikethroughStyle>,
983}
984
985#[cfg(all(target_os = "macos", test))]
986impl TextRun {
987 fn with_len(&self, len: usize) -> Self {
988 let mut this = self.clone();
989 this.len = len;
990 this
991 }
992}
993
994/// An identifier for a specific glyph, as returned by [`WindowTextSystem::layout_line`].
995#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
996#[repr(C)]
997pub struct GlyphId(pub u32);
998
999/// Parameters for rendering a glyph, used as cache keys for raster bounds.
1000///
1001/// This struct identifies a specific glyph rendering configuration including
1002/// font, size, subpixel positioning, and scale factor. It's used to look up
1003/// cached raster bounds and sprite atlas entries.
1004#[derive(Clone, Debug, PartialEq)]
1005#[expect(missing_docs)]
1006pub struct RenderGlyphParams {
1007 pub font_id: FontId,
1008 pub glyph_id: GlyphId,
1009 pub font_size: Pixels,
1010 pub subpixel_variant: Point<u8>,
1011 pub scale_factor: f32,
1012 pub is_emoji: bool,
1013 pub subpixel_rendering: bool,
1014}
1015
1016impl Eq for RenderGlyphParams {}
1017
1018impl Hash for RenderGlyphParams {
1019 fn hash<H: Hasher>(&self, state: &mut H) {
1020 self.font_id.0.hash(state);
1021 self.glyph_id.0.hash(state);
1022 self.font_size.0.to_bits().hash(state);
1023 self.subpixel_variant.hash(state);
1024 self.scale_factor.to_bits().hash(state);
1025 self.is_emoji.hash(state);
1026 self.subpixel_rendering.hash(state);
1027 }
1028}
1029
1030/// The configuration details for identifying a specific font.
1031#[derive(Clone, Debug, Eq, PartialEq, Hash)]
1032pub struct Font {
1033 /// The font family name.
1034 ///
1035 /// The special name ".SystemUIFont" is used to identify the system UI font, which varies based on platform.
1036 pub family: SharedString,
1037
1038 /// The font features to use.
1039 pub features: FontFeatures,
1040
1041 /// The fallbacks fonts to use.
1042 pub fallbacks: Option<FontFallbacks>,
1043
1044 /// The font weight.
1045 pub weight: FontWeight,
1046
1047 /// The font style.
1048 pub style: FontStyle,
1049}
1050
1051impl Default for Font {
1052 fn default() -> Self {
1053 font(".SystemUIFont")
1054 }
1055}
1056
1057/// Get a [`Font`] for a given name.
1058pub fn font(family: impl Into<SharedString>) -> Font {
1059 Font {
1060 family: family.into(),
1061 features: FontFeatures::default(),
1062 weight: FontWeight::default(),
1063 style: FontStyle::default(),
1064 fallbacks: None,
1065 }
1066}
1067
1068impl Font {
1069 /// Set this Font to be bold
1070 pub fn bold(mut self) -> Self {
1071 self.weight = FontWeight::BOLD;
1072 self
1073 }
1074
1075 /// Set this Font to be italic
1076 pub fn italic(mut self) -> Self {
1077 self.style = FontStyle::Italic;
1078 self
1079 }
1080}
1081
1082/// A struct for storing font metrics.
1083/// It is used to define the measurements of a typeface.
1084#[derive(Clone, Copy, Debug)]
1085pub struct FontMetrics {
1086 /// The number of font units that make up the "em square",
1087 /// a scalable grid for determining the size of a typeface.
1088 pub units_per_em: u32,
1089
1090 /// The vertical distance from the baseline of the font to the top of the glyph covers.
1091 pub ascent: f32,
1092
1093 /// The vertical distance from the baseline of the font to the bottom of the glyph covers.
1094 pub descent: f32,
1095
1096 /// The recommended additional space to add between lines of type.
1097 pub line_gap: f32,
1098
1099 /// The suggested position of the underline.
1100 pub underline_position: f32,
1101
1102 /// The suggested thickness of the underline.
1103 pub underline_thickness: f32,
1104
1105 /// The height of a capital letter measured from the baseline of the font.
1106 pub cap_height: f32,
1107
1108 /// The height of a lowercase x.
1109 pub x_height: f32,
1110
1111 /// The outer limits of the area that the font covers.
1112 /// Corresponds to the xMin / xMax / yMin / yMax values in the OpenType `head` table
1113 pub bounding_box: Bounds<f32>,
1114}
1115
1116impl FontMetrics {
1117 /// Returns the vertical distance from the baseline of the font to the top of the glyph covers in pixels.
1118 pub fn ascent(&self, font_size: Pixels) -> Pixels {
1119 Pixels((self.ascent / self.units_per_em as f32) * font_size.0)
1120 }
1121
1122 /// Returns the vertical distance from the baseline of the font to the bottom of the glyph covers in pixels.
1123 pub fn descent(&self, font_size: Pixels) -> Pixels {
1124 Pixels((self.descent / self.units_per_em as f32) * font_size.0)
1125 }
1126
1127 /// Returns the recommended additional space to add between lines of type in pixels.
1128 pub fn line_gap(&self, font_size: Pixels) -> Pixels {
1129 Pixels((self.line_gap / self.units_per_em as f32) * font_size.0)
1130 }
1131
1132 /// Returns the suggested position of the underline in pixels.
1133 pub fn underline_position(&self, font_size: Pixels) -> Pixels {
1134 Pixels((self.underline_position / self.units_per_em as f32) * font_size.0)
1135 }
1136
1137 /// Returns the suggested thickness of the underline in pixels.
1138 pub fn underline_thickness(&self, font_size: Pixels) -> Pixels {
1139 Pixels((self.underline_thickness / self.units_per_em as f32) * font_size.0)
1140 }
1141
1142 /// Returns the height of a capital letter measured from the baseline of the font in pixels.
1143 pub fn cap_height(&self, font_size: Pixels) -> Pixels {
1144 Pixels((self.cap_height / self.units_per_em as f32) * font_size.0)
1145 }
1146
1147 /// Returns the height of a lowercase x in pixels.
1148 pub fn x_height(&self, font_size: Pixels) -> Pixels {
1149 Pixels((self.x_height / self.units_per_em as f32) * font_size.0)
1150 }
1151
1152 /// Returns the outer limits of the area that the font covers in pixels.
1153 pub fn bounding_box(&self, font_size: Pixels) -> Bounds<Pixels> {
1154 (self.bounding_box / self.units_per_em as f32 * font_size.0).map(px)
1155 }
1156}
1157
1158/// Maps well-known virtual font names to their concrete equivalents.
1159#[allow(unused)]
1160pub fn font_name_with_fallbacks<'a>(name: &'a str, system: &'a str) -> &'a str {
1161 // Note: the "Zed Plex" fonts were deprecated as we are not allowed to use "Plex"
1162 // in a derived font name. They are essentially indistinguishable from IBM Plex/Lilex,
1163 // and so retained here for backward compatibility.
1164 match name {
1165 ".SystemUIFont" => system,
1166 ".ZedSans" | "Zed Plex Sans" => "IBM Plex Sans",
1167 ".ZedMono" | "Zed Plex Mono" => "Lilex",
1168 _ => name,
1169 }
1170}
1171
1172/// Like [`font_name_with_fallbacks`] but accepts and returns [`SharedString`] references.
1173#[allow(unused)]
1174pub fn font_name_with_fallbacks_shared<'a>(
1175 name: &'a SharedString,
1176 system: &'a SharedString,
1177) -> &'a SharedString {
1178 // Note: the "Zed Plex" fonts were deprecated as we are not allowed to use "Plex"
1179 // in a derived font name. They are essentially indistinguishable from IBM Plex/Lilex,
1180 // and so retained here for backward compatibility.
1181 match name.as_str() {
1182 ".SystemUIFont" => system,
1183 ".ZedSans" | "Zed Plex Sans" => const { &SharedString::new_static("IBM Plex Sans") },
1184 ".ZedMono" | "Zed Plex Mono" => const { &SharedString::new_static("Lilex") },
1185 _ => name,
1186 }
1187}