kui_core/text.rs
1//! Core-owned text stack. Shaping and line layout run through cosmic-text and
2//! are cached across frames keyed by (content, style, scale) — the layout pass
3//! measures through this cache, so shaping survives resizes and static text
4//! costs a hash lookup per frame. Rasterization feeds the shared glyph atlas;
5//! renderers only ever see positioned atlas quads.
6
7use cosmic_text::{
8 Attrs, Buffer, CacheKeyFlags, Ellipsize, EllipsizeHeightLimit, FontSystem, Metrics, Shaping,
9 Style as FontStyle, SwashContent, Weight, Wrap,
10};
11use rustc_hash::FxHashMap;
12
13use crate::atlas::GlyphAtlas;
14use crate::color::Color;
15use crate::display::{Clip, ClipId, Quad, QuadKind};
16use crate::geom::{Rect, Size, Vec2};
17use crate::initial::Initial;
18use crate::key::Key;
19use crate::resources::Resources;
20use crate::retain::Kept;
21use crate::spec::{FontFamily, TextStyle, TextWrap, UnderlineStyle};
22use crate::tree::TextId;
23use crate::value::Value;
24
25/// The glyph rasterizer: swash's scaler, as cosmic-text's `SwashCache`
26/// drives it, for plain alpha masks, LCD subpixel masks and color bitmaps
27/// alike — our own so the subpixel format is ours to pick, and so a
28/// variable face is drawn at the `wght` coordinate its axis gives a CSS
29/// weight and a glyph marked for synthetic bold is drawn bold, neither of
30/// which cosmic-text's cache does.
31pub(crate) struct Raster {
32 ctx: swash::scale::ScaleContext,
33 /// Each face's `wght` axis, read once: `None` for a face without one.
34 axes: FxHashMap<cosmic_text::fontdb::ID, Option<crate::weights::WghtAxis>>,
35 /// Rasterize outline glyphs as per-channel subpixel coverage. Set by
36 /// the driver from what its renderer can blend; see
37 /// `Core::set_subpixel_text`.
38 pub(crate) subpixel: bool,
39}
40
41impl Raster {
42 fn new() -> Self {
43 Self {
44 ctx: swash::scale::ScaleContext::new(),
45 axes: FxHashMap::default(),
46 subpixel: false,
47 }
48 }
49
50 /// cosmic-text's `swash_image` in `format`: with `Format::Subpixel`,
51 /// three rasterizations shifted by a third of a pixel land in r, g and
52 /// b. Color sources are tried first so emoji still come out as bitmaps.
53 ///
54 /// A variable face is drawn where its `wght` axis puts the glyph's
55 /// weight ([`WghtAxis`](crate::weights::WghtAxis)) — cosmic-text takes
56 /// the CSS number itself as the coordinate, which draws Berkeley Mono
57 /// Variable's regular at its Bold — and a glyph marked
58 /// [`SYNTHETIC_BOLD`](crate::weights::SYNTHETIC_BOLD) at the axis's
59 /// bold, or, when the face has no heavier instance or no axis, with
60 /// its outline emboldened.
61 fn image(
62 &mut self,
63 fs: &mut FontSystem,
64 key: cosmic_text::CacheKey,
65 format: swash::zeno::Format,
66 ) -> Option<cosmic_text::SwashImage> {
67 use swash::scale::{Render, Source, StrikeWith};
68 use swash::zeno::{Angle, Transform, Vector};
69 let font = fs.get_font(key.font_id, key.font_weight)?;
70 let swash_font = font.as_swash();
71 let axis = self
72 .axes
73 .entry(key.font_id)
74 .or_insert_with(|| {
75 let weight = fs.db().face(key.font_id).map_or(400, |face| face.weight.0);
76 crate::weights::WghtAxis::read(swash_font, weight)
77 })
78 .as_ref();
79 let size = f32::from_bits(key.font_size_bits);
80 let face_weight = f32::from(key.font_weight.0);
81 let bold = key.flags.contains(crate::weights::SYNTHETIC_BOLD);
82 let asked = if bold {
83 f32::from(Weight::BOLD.0)
84 } else {
85 face_weight
86 };
87 let at = axis.map(|axis| axis.coordinate(asked));
88 // Bold that the axis cannot draw heavier than the face's own
89 // weight, or a face with no axis: the outline grown instead.
90 let embolden = bold
91 && match (axis, at) {
92 (Some(axis), Some(at)) => at <= axis.coordinate(face_weight),
93 _ => true,
94 };
95 // Hinting fits an outline to the pixel grid, which past
96 // `LARGE_GLYPH` moves nothing by a visible fraction of the glyph,
97 // and a face's hinting program runs in 26.6 fixed point: at a drop
98 // cap's tens of thousands of px skrifa's interpreter overflows it,
99 // a panic in a debug build and wrapped points in a release one
100 // (backlog FZ8). Past it, the outline as drawn.
101 let hint = !key.flags.contains(CacheKeyFlags::DISABLE_HINTING) && size <= LARGE_GLYPH;
102 let mut scaler = self.ctx.builder(swash_font).size(size).hint(hint);
103 if let Some(at) = at {
104 scaler = scaler.normalized_coords(
105 swash_font
106 .variations()
107 .normalized_coords([(swash::Tag::from_be_bytes(*b"wght"), at)]),
108 );
109 }
110 let mut scaler = scaler.build();
111 // A glyph wider or taller than an atlas page is refused there
112 // (`GlyphAtlas::get_or_insert`), but only once it is drawn, and a
113 // size is a plain number every binding passes through: at 10^6 px
114 // zeno sizes the mask as `w * h * 4` in a `u32`, which overflows,
115 // and the image it would draw is terabytes (backlog FZ5). Past
116 // the size at which any face's glyph still fits a page, its
117 // outline's box is read first; a glyph with no outline (a bitmap
118 // strike) goes by the size.
119 if size > LARGE_GLYPH {
120 let page = crate::atlas::MAX_ATLAS_SIZE as f32;
121 let fits = match scaler.scale_outline(key.glyph_id) {
122 Some(outline) => {
123 let b = outline.bounds();
124 b.width() <= page && b.height() <= page
125 }
126 None => size <= page,
127 };
128 if !fits {
129 return None;
130 }
131 }
132 let offset = if key.flags.contains(CacheKeyFlags::PIXEL_FONT) {
133 Vector::new(key.x_bin.as_float().round(), key.y_bin.as_float().round())
134 } else {
135 Vector::new(key.x_bin.as_float(), key.y_bin.as_float())
136 };
137 Render::new(&[
138 Source::ColorOutline(0),
139 Source::ColorBitmap(StrikeWith::BestFit),
140 Source::Outline,
141 ])
142 .format(format)
143 .offset(offset)
144 .embolden(if embolden {
145 crate::weights::embolden_strength(size)
146 } else {
147 0.0
148 })
149 .transform(
150 key.flags
151 .contains(CacheKeyFlags::FAKE_ITALIC)
152 .then(|| Transform::skew(Angle::from_degrees(14.0), Angle::from_degrees(0.0))),
153 )
154 .render(&mut scaler, key.glyph_id)
155 }
156}
157
158/// The size, in physical px, past which a glyph's outline is measured
159/// before it is drawn (backlog FZ5): below it even a glyph three em wide
160/// fits an atlas page. Past it, too, a glyph is drawn unhinted (FZ8).
161const LARGE_GLYPH: f32 = 1024.0;
162
163/// Rasterizes one glyph into the atlas (shared by static text and editors).
164pub(crate) fn raster_glyph(
165 key: cosmic_text::CacheKey,
166 fs: &mut FontSystem,
167 raster: &mut Raster,
168 atlas: &mut crate::atlas::GlyphAtlas,
169) -> Option<crate::atlas::GlyphSlot> {
170 atlas.get_or_insert(key, || {
171 let format = if raster.subpixel {
172 swash::zeno::Format::Subpixel
173 } else {
174 swash::zeno::Format::Alpha
175 };
176 let image = raster.image(fs, key, format)?;
177 if image.placement.width == 0 || image.placement.height == 0 {
178 return None;
179 }
180 let (data, is_color, subpixel) = match image.content {
181 SwashContent::Mask => {
182 let mut rgba = Vec::with_capacity(image.data.len() * 4);
183 for &a in image.data.iter() {
184 rgba.extend_from_slice(&[255, 255, 255, a]);
185 }
186 (rgba, false, false)
187 }
188 SwashContent::Color => (image.data.to_vec(), true, false),
189 SwashContent::SubpixelMask => {
190 // rgb carry the per-channel coverages; alpha (which zeno
191 // leaves untouched) becomes the union, the value a backend
192 // without per-channel blending falls back to.
193 let mut rgba = image.data.to_vec();
194 for px in rgba.as_chunks_mut::<4>().0 {
195 px[3] = px[0].max(px[1]).max(px[2]);
196 }
197 (rgba, false, true)
198 }
199 };
200 Some(crate::atlas::RasterGlyph {
201 w: image.placement.width,
202 h: image.placement.height,
203 left: image.placement.left,
204 top: image.placement.top,
205 color: is_color,
206 subpixel,
207 data,
208 })
209 })
210}
211
212/// The quad kind a rasterized glyph draws as.
213pub(crate) fn glyph_kind(slot: &crate::atlas::GlyphSlot) -> QuadKind {
214 if slot.color_glyph {
215 QuadKind::GlyphColor
216 } else if slot.subpixel {
217 QuadKind::GlyphSubpixel
218 } else {
219 QuadKind::GlyphMask
220 }
221}
222
223/// The default byte budget for the shaped-text cache. Sized
224/// so a screenful of code never meets it — two panes of 55 highlighted
225/// lines are ~900 short entries, a few megabytes — and a pane streaming
226/// new text meets it within seconds, which is when the clock alone let the
227/// cache reach gigabytes. `Core::set_text_cache_budget` changes it.
228pub const DEFAULT_TEXT_CACHE_BYTES: usize = 64 << 20;
229
230/// What one cache entry costs, estimated: a fixed floor (the `Buffer`, its
231/// line, the attrs list, the entry itself) plus a per-glyph rate that
232/// covers cosmic-text's `ShapeGlyph` and `LayoutGlyph`, our
233/// `GlyphTemplate`, and the entry's share of the shape-run cache. Measured
234/// on 2026-09-07 with a counting allocator around a `Core` drawing 50 new
235/// lines a frame: 7.6 KB per 10-glyph line, 22 KB per 40, 92 KB per 200
236/// (cosmic-text's own `Buffer` is 284 B/glyph of that and the shape-run
237/// cache 118 B/glyph). The estimate lands within ~10% of those, on the
238/// high side, so the budget errs toward evicting.
239const ENTRY_BASE_BYTES: usize = 4096;
240const ENTRY_GLYPH_BYTES: usize = 480;
241
242/// A non-wrapping text at least this long is shaped in chunks: a minified
243/// bundle, a log line with a blob in it, a base64 field.
244/// Shorter text takes the path it always took, so nothing below the line
245/// moves. Bytes, not characters, for the same reason a step line carries
246/// integers: every binding can count them.
247pub const LONG_LINE_BYTES: usize = 4096;
248/// How long a chunk is, at most: cut at the last whitespace in the second
249/// half of the window, else at a grapheme boundary. cosmic-text shapes
250/// per whitespace-delimited word and its shape-run cache keys on words, so
251/// a cut at whitespace loses nothing the whole line kept.
252const CHUNK_BYTES: usize = 1024;
253/// Mixed into a long line's key so it never collides with the entry a
254/// short text of the same content would get.
255const LONG_SALT: u64 = 0x5f5f_6c6f_6e67_5f5f;
256/// Mixed into the key of a long line's copies, one for each further node
257/// of a frame drawing the same line ([`TextSystem::claim_long`]).
258const COPY_SALT: u64 = 0x5f5f_636f_7079_5f5f;
259
260/// When the cache is over budget it is evicted down to this fraction of it,
261/// not to the line, so a stream that adds a little every frame walks the
262/// cache once per quarter-budget of new text rather than every frame.
263const EVICT_TO_NUMERATOR: usize = 3;
264const EVICT_TO_DENOMINATOR: usize = 4;
265
266pub(crate) struct CachedText {
267 buffer: Buffer,
268 /// The text itself (spans concatenated, for rich text): what the
269 /// access tree names a control by.
270 content: String,
271 /// Wrap width (physical px) the buffer is currently laid out at.
272 wrap: Option<f32>,
273 /// Unwrapped measurement, physical px.
274 intrinsic: Size,
275 /// Lines past this many are dropped (0 = unlimited): the buffer-wide
276 /// budget; cosmic-text's ellipsize limit is per paragraph.
277 max_lines: usize,
278 /// The content may run past the node's width (no-wrap, ellipsis):
279 /// report the box width and clip glyphs to it.
280 clamp_w: bool,
281 last_used: u64,
282 /// What this entry costs the budget (see `ENTRY_BASE_BYTES`).
283 bytes: usize,
284 /// Positioned glyph quads relative to the text origin, so steady-state
285 /// emission is a memcpy-style walk instead of per-glyph atlas lookups.
286 glyphs: Vec<GlyphTemplate>,
287 /// The decoration rects that go with them: a span's
288 /// background under its glyphs, its underline and strikethrough over
289 /// them, one rect per run of the span per line, so they wrap with it.
290 deco: Vec<DecoTemplate>,
291 /// What each span asked for, by the index its glyphs carry as
292 /// metadata; one entry for plain text.
293 span_deco: Vec<SpanDeco>,
294 /// (wrap, atlas stamp) the template cache was built for.
295 glyphs_built_for: Option<(Option<u32>, u64)>,
296 /// The frame a node last wrapped this run for, and the wrap it asked:
297 /// a second node of that frame asking another width wraps a copy of
298 /// its own ([`TextSystem::own_wrap`]), since the queries read the
299 /// buffer as it was left.
300 claimed: u64,
301 claimed_wrap: Option<f32>,
302 /// A break may fall between any two glyphs (`TextWrap::Glyph`), not
303 /// only where a line may break: what the min-content reads.
304 breaks_anywhere: bool,
305 /// The widest stretch no break falls inside, physical px — CSS's
306 /// min-content — measured the first time a shrink asks and kept for
307 /// the entry's life, since no width changes it.
308 min_content: Option<f32>,
309 /// The first letter as an initial, while the text has one
310 /// (`TextStyle::initial_letter`, backlog F164): the buffer's first
311 /// line, and where it and every other line's runs are drawn. Boxed:
312 /// it holds a paragraph line, and few texts have one.
313 initial: Option<Box<Initial>>,
314}
315
316#[derive(Clone, Copy)]
317struct GlyphTemplate {
318 x: f32,
319 y: f32,
320 w: f32,
321 h: f32,
322 uv: [u32; 4],
323 kind: QuadKind,
324 /// Per-span color override (rich text); falls back to the node color.
325 /// A glyph whose ink lies over the places of spans painted apart is
326 /// several templates, a slice of its columns each
327 /// ([`Places::push_painted`]).
328 color: Option<Color>,
329 /// The byte the glyph starts at in the entry's content: what puts it
330 /// on a row when a wrapped long line's chunk is drawn. Every slice of
331 /// a glyph carries the glyph's, so they go to its row together.
332 byte: u32,
333}
334
335/// One decoration rect relative to the text origin, physical px.
336struct DecoTemplate {
337 x: f32,
338 y: f32,
339 w: f32,
340 h: f32,
341 /// The rect's colour; `None` is the text's own (an underline in the
342 /// text colour), which a background never is.
343 color: Option<Color>,
344 /// Painted before the glyphs (a background) rather than after (a line).
345 under: bool,
346 /// The shape, for an underline: the rect is where a solid line goes,
347 /// and a wave or dots are built around it at emission.
348 style: UnderlineStyle,
349 /// A background's radius, logical px; above zero the frame joins it
350 /// with the ones it meets ([`JoinBg`]).
351 radius: f32,
352}
353
354/// A rounded span background emitted this frame: the quad
355/// it is, square for now, which the frame's last pass turns into its part
356/// of one shape once every text has been painted and each can be told the
357/// ones it meets (`crate::join`). The radius is logical px. `outer` is the
358/// clip the text was given, and `own` its own box's physical x-range when
359/// it clips to it, as a no-wrap text does: a piece is no wider than that
360/// box shows, but a fillet past the end of a short line lies outside the
361/// box, beside it, and is clipped only as the text's parent is.
362#[derive(Clone, Copy, Debug)]
363pub(crate) struct JoinBg {
364 pub quad: u32,
365 pub radius: f32,
366 pub outer: ClipId,
367 pub own: Option<(f32, f32)>,
368}
369
370/// What one span (or a plain text's whole content) asked for past its
371/// shaping: where it starts, its colour, and its decorations.
372#[derive(Clone, Copy, Default)]
373struct SpanDeco {
374 /// The byte it starts at in the entry's content.
375 start: u32,
376 /// Its colour as its glyphs carry it ([`shaped_color`]); `None` is
377 /// the text's own.
378 color: Option<Color>,
379 underline: bool,
380 underline_color: Option<Color>,
381 underline_style: UnderlineStyle,
382 strikethrough: bool,
383 bg: Option<Color>,
384 /// The background's radius, logical px: above zero it is joined with
385 /// the backgrounds it meets.
386 bg_radius: f32,
387}
388
389impl SpanDeco {
390 fn of_style(style: &TextStyle) -> Self {
391 Self {
392 start: 0,
393 color: None,
394 underline: style.underline,
395 underline_color: style.underline_color,
396 underline_style: style.underline_style,
397 strikethrough: style.strikethrough,
398 bg: None,
399 bg_radius: 0.0,
400 }
401 }
402
403 fn any(&self) -> bool {
404 self.underline || self.strikethrough || self.bg.is_some()
405 }
406}
407
408/// One styled run inside a rich-text paragraph. Spans are shaped and wrapped
409/// together as a single flow; plain data, so every frontend can build them.
410#[derive(Clone, Copy, Debug)]
411pub struct Span<'a> {
412 pub text: &'a str,
413 pub color: Option<Color>,
414 pub bold: bool,
415 pub italic: bool,
416 /// A line under the span, where the face puts its underline.
417 pub underline: bool,
418 /// The underline's own colour; `None` is the span's.
419 pub underline_color: Option<Color>,
420 /// The underline's shape.
421 pub underline_style: UnderlineStyle,
422 /// A line through the span, where the face puts its strikeout.
423 pub strikethrough: bool,
424 /// A background behind the span's glyphs, one rect per line it spans,
425 /// so it follows the span across a wrap the way a box cannot.
426 pub bg: Option<Color>,
427 /// The background's corner radius, logical px. Above
428 /// zero, every background of the same colour and radius that meets
429 /// another edge to edge on the line above or below — in this text or
430 /// another — is one shape with it: its corners convex where a line
431 /// reaches past its neighbour, concave where it falls short, round
432 /// where nothing meets it. What a selection over lines looks like.
433 pub bg_radius: f32,
434 /// The span's own face, in place of the paragraph's: inline `code` in
435 /// `FontFamily::Mono` inside a sans line. `None` is the paragraph's.
436 /// The span keeps the paragraph's weights for its family (bold is the
437 /// family's bold), and its glyphs, carets and selection are measured
438 /// in the face they are drawn in, so byte positions stay exact across
439 /// the change.
440 pub family: Option<FontFamily>,
441 /// The span's own size in logical px, in place of the paragraph's: a
442 /// heading's larger first word, a smaller footnote mark. Its line
443 /// height scales with it at the paragraph's ratio, and a line is as
444 /// tall as its tallest span. `None` is the paragraph's. A paragraph
445 /// with a sized span is shaped whole, never chunked as a long line.
446 pub size: Option<f32>,
447}
448
449impl<'a> Span<'a> {
450 pub fn new(text: &'a str) -> Self {
451 Self {
452 text,
453 color: None,
454 bold: false,
455 italic: false,
456 underline: false,
457 underline_color: None,
458 underline_style: UnderlineStyle::Solid,
459 strikethrough: false,
460 bg: None,
461 bg_radius: 0.0,
462 family: None,
463 size: None,
464 }
465 }
466
467 /// The span in its own face (see the `family` field).
468 pub fn family(mut self, family: FontFamily) -> Self {
469 self.family = Some(family);
470 self
471 }
472
473 /// The span in the monospace face: inline code.
474 ///
475 /// ```
476 /// # use kui_core::{FontFamily, text::Span};
477 /// let code = Span::new("cargo test").mono();
478 /// assert_eq!(code.family, Some(FontFamily::Mono));
479 /// ```
480 pub fn mono(self) -> Self {
481 self.family(FontFamily::Mono)
482 }
483
484 /// The span at its own size in logical px (see the `size` field); a
485 /// size that is not a positive number is the paragraph's.
486 pub fn size(mut self, px: f32) -> Self {
487 self.size = (px.is_finite() && px > 0.0).then_some(px);
488 self
489 }
490
491 pub fn color(mut self, c: Color) -> Self {
492 self.color = Some(c);
493 self
494 }
495
496 pub fn bold(mut self) -> Self {
497 self.bold = true;
498 self
499 }
500
501 pub fn italic(mut self) -> Self {
502 self.italic = true;
503 self
504 }
505
506 pub fn underline(mut self) -> Self {
507 self.underline = true;
508 self
509 }
510
511 /// An underline in its own colour; turns it on.
512 pub fn underline_color(mut self, c: Color) -> Self {
513 self.underline = true;
514 self.underline_color = Some(c);
515 self
516 }
517
518 /// An underline of this shape; turns it on.
519 pub fn underline_style(mut self, s: UnderlineStyle) -> Self {
520 self.underline = true;
521 self.underline_style = s;
522 self
523 }
524
525 pub fn strikethrough(mut self) -> Self {
526 self.strikethrough = true;
527 self
528 }
529
530 pub fn bg(mut self, c: Color) -> Self {
531 self.bg = Some(c);
532 self
533 }
534
535 /// Rounds the background, joined with the ones it meets (see
536 /// [`Span::bg_radius`](Self#structfield.bg_radius)).
537 pub fn bg_radius(mut self, r: f32) -> Self {
538 self.bg_radius = r.max(0.0);
539 self
540 }
541
542 /// The span without its text: what a long line keeps per span so a
543 /// chunk can be rebuilt from the content and the ranges.
544 fn attrs_only(&self) -> SpanAttrs {
545 SpanAttrs {
546 color: self.color,
547 bold: self.bold,
548 italic: self.italic,
549 underline: self.underline,
550 underline_color: self.underline_color,
551 underline_style: self.underline_style,
552 strikethrough: self.strikethrough,
553 bg: self.bg,
554 bg_radius: self.bg_radius,
555 family: self.family,
556 size: self.size,
557 }
558 }
559
560 fn attrs<'r>(
561 &self,
562 family: cosmic_text::Family<'r>,
563 weights: crate::weights::Weights,
564 ) -> Attrs<'r> {
565 // Pin the family (the paragraph base's) so weight/style variants
566 // stay in one typeface instead of falling back to whatever face
567 // matches first — at weights it has faces for (backlog F100).
568 let mut attrs = weights.apply(Attrs::new().family(family), self.bold);
569 if self.italic {
570 attrs = attrs.style(FontStyle::Italic);
571 }
572 if let Some(c) = self.color {
573 attrs = attrs.color(shaper_color(c));
574 }
575 attrs
576 }
577}
578
579/// A colour as the shaper is handed it, eight bits a channel.
580fn shaper_color(c: Color) -> cosmic_text::Color {
581 cosmic_text::Color::rgba(
582 (c.r * 255.0) as u8,
583 (c.g * 255.0) as u8,
584 (c.b * 255.0) as u8,
585 (c.a * 255.0) as u8,
586 )
587}
588
589/// A colour as a span's glyphs come back carrying it ([`shaper_color`]
590/// and back), so a colour read off a span and one read off its glyphs
591/// compare equal.
592fn shaped_color(c: Color) -> Color {
593 let c = shaper_color(c);
594 Color::rgba8(c.r(), c.g(), c.b(), c.a())
595}
596
597/// A [`Span`]'s attributes without its text, owned: what a long rich
598/// line keeps per span, with the range in [`OwnedSpan`], so a chunk can
599/// be handed the spans that intersect it, sliced.
600#[derive(Clone, Copy)]
601struct SpanAttrs {
602 color: Option<Color>,
603 bold: bool,
604 italic: bool,
605 underline: bool,
606 underline_color: Option<Color>,
607 underline_style: UnderlineStyle,
608 strikethrough: bool,
609 bg: Option<Color>,
610 bg_radius: f32,
611 family: Option<FontFamily>,
612 size: Option<f32>,
613}
614
615impl SpanAttrs {
616 fn span<'a>(&self, text: &'a str) -> Span<'a> {
617 Span {
618 text,
619 color: self.color,
620 bold: self.bold,
621 italic: self.italic,
622 underline: self.underline,
623 underline_color: self.underline_color,
624 underline_style: self.underline_style,
625 strikethrough: self.strikethrough,
626 bg: self.bg,
627 bg_radius: self.bg_radius,
628 family: self.family,
629 size: self.size,
630 }
631 }
632}
633
634/// One span of a long rich line: its byte range in the content and its
635/// attributes. The ranges are contiguous and cover the content.
636#[derive(Clone)]
637struct OwnedSpan {
638 start: usize,
639 end: usize,
640 attrs: SpanAttrs,
641}
642
643/// The spans of `spans` that intersect `start..end` of `content`, sliced
644/// to it and with the empty dropped: what a chunk of a long rich line is
645/// shaped from. A span cut by the chunk boundary becomes two, one a
646/// side, with the same attributes — its background rects meet at the cut.
647fn chunk_spans<'a>(
648 content: &'a str,
649 spans: &[OwnedSpan],
650 start: usize,
651 end: usize,
652) -> Vec<Span<'a>> {
653 let first = spans.partition_point(|s| s.end <= start);
654 spans[first..]
655 .iter()
656 .take_while(|s| s.start < end)
657 .filter_map(|s| {
658 let (a, b) = (s.start.max(start), s.end.min(end));
659 (a < b).then(|| s.attrs.span(&content[a..b]))
660 })
661 .collect()
662}
663
664struct FrameText {
665 cache_key: u64,
666 color: Color,
667}
668
669/// One entry of the text cache: a shaped run, or a long line whose chunks
670/// are runs of their own in the same map. Whether a text is
671/// long is decided once, at [`TextSystem::add`], and lives here as the
672/// variant; every operation after it asks the entry rather than carrying
673/// a flag beside the key. A long line joins a selection scope's
674/// concatenation like any run: being long is how it was shaped, not
675/// something a reader dragging across it should feel.
676pub(crate) enum Entry {
677 Run(CachedText),
678 Long(LongLine),
679}
680
681impl Entry {
682 pub(crate) fn content(&self) -> &str {
683 match self {
684 Entry::Run(e) => &e.content,
685 Entry::Long(l) => &l.content,
686 }
687 }
688
689 fn last_used(&self) -> u64 {
690 match self {
691 Entry::Run(e) => e.last_used,
692 Entry::Long(l) => l.last_used,
693 }
694 }
695
696 fn touch(&mut self, frame_no: u64) {
697 match self {
698 Entry::Run(e) => e.last_used = frame_no,
699 Entry::Long(l) => l.last_used = frame_no,
700 }
701 }
702
703 /// What the entry costs the budget.
704 fn bytes(&self) -> usize {
705 match self {
706 Entry::Run(e) => e.bytes,
707 Entry::Long(l) => l.bytes,
708 }
709 }
710
711 fn run(&self) -> Option<&CachedText> {
712 match self {
713 Entry::Run(e) => Some(e),
714 Entry::Long(_) => None,
715 }
716 }
717
718 fn run_mut(&mut self) -> Option<&mut CachedText> {
719 match self {
720 Entry::Run(e) => Some(e),
721 Entry::Long(_) => None,
722 }
723 }
724
725 fn long(&self) -> Option<&LongLine> {
726 match self {
727 Entry::Long(l) => Some(l),
728 Entry::Run(_) => None,
729 }
730 }
731
732 fn long_mut(&mut self) -> Option<&mut LongLine> {
733 match self {
734 Entry::Long(l) => Some(l),
735 Entry::Run(_) => None,
736 }
737 }
738}
739
740/// One chunk of a long line: its byte range in the content, the cache key
741/// its shaped entry has (an ordinary `CachedText`, budgeted like any), and
742/// its width once shaped.
743#[derive(Clone)]
744struct Chunk {
745 start: usize,
746 end: usize,
747 key: u64,
748 /// The run's own width: its tabs' stops measured from its start.
749 width: Option<f32>,
750 /// For a shaped chunk that starts off the line's tab stops and ends in
751 /// its one tab (`chunk_ranges`): where the tab starts in the run, and
752 /// the stops' interval, physical px. The run's width put that tab on
753 /// a stop of the chunk's own; the line puts it on one of its own, from
754 /// where the chunk starts (`LongLine::reprefix`, backlog RG76).
755 tab: Option<(f32, f32)>,
756 /// While the line is wrapped and this chunk is shaped: where each of
757 /// its rows starts. Empty otherwise (one row, or an estimate).
758 rows: Vec<RowStart>,
759}
760
761/// Where a row of a wrapped long line's chunk starts, relative to the
762/// chunk: the byte, and the x of that glyph in the chunk's unwrapped run,
763/// which the row's glyphs are shifted back by when drawn.
764#[derive(Clone, Copy, Debug)]
765struct RowStart {
766 byte: u32,
767 x: f32,
768}
769
770/// A non-wrapping text past [`LONG_LINE_BYTES`], shaped in chunks on demand.
771/// The line itself holds no buffer: its chunks are cache
772/// entries interned when emission, a hit-test or a caret query lands in
773/// them, and `prefix` is where each chunk starts — an estimate from the
774/// first chunk's mean advance until the chunk shapes, exact after. So a
775/// 100k-character line costs the screenful it shows, a keystroke into it
776/// costs the chunk it lands in, and the width the scrollbar sees can move
777/// a little as chunks fill in, exact under monospace.
778///
779/// Its rows are its own, so a line is laid out for one node a frame: a
780/// second node drawing the same content takes a copy
781/// ([`TextSystem::claim_long`]), which shares the chunks' shaped runs.
782#[derive(Clone)]
783pub(crate) struct LongLine {
784 content: String,
785 /// The spans, for a rich line: each chunk is shaped as a
786 /// rich run of the spans that intersect it, sliced. Empty for a plain
787 /// line, whose chunks are plain runs.
788 spans: Vec<OwnedSpan>,
789 /// The chunks' style: the text's with `wrap` set to `None`, since a
790 /// chunk is always shaped as one run and the line breaks it itself.
791 style: TextStyle,
792 /// The text's own line breaking. `None` is one row; `Word` and
793 /// `Glyph` break the chunks into rows once the line does not fit its
794 /// box (C19's step 5): a one-direction prefix computation over glyph
795 /// positions, each chunk's first row starting where the previous
796 /// chunk's last row ended, so it costs positions and not shaping.
797 wrap: TextWrap,
798 /// The physical width the rows are broken to, while they are; `None`
799 /// is one row — the line fits, or its style never wraps.
800 wrap_w: Option<f32>,
801 /// While wrapped: where each chunk begins, the row and the x on it —
802 /// one more than there are chunks, the last being where the text
803 /// ends. The wrapped counterpart of `prefix`, estimated the same way
804 /// for a chunk that never showed.
805 starts: Vec<(u32, f32)>,
806 chunks: Vec<Chunk>,
807 /// Physical px from the line's origin to each chunk's start, one more
808 /// than there are chunks: the last is the line's width.
809 prefix: Vec<f32>,
810 /// Physical line height, from the first chunk's buffer.
811 line_h: f32,
812 /// Physical px per byte, from the first chunk: the estimate an
813 /// unshaped chunk's width is.
814 avg: f32,
815 last_used: u64,
816 /// The frame a node last took this line to draw, `u64::MAX` before
817 /// one has.
818 claimed: u64,
819 /// The rows layout gave the node this frame, and the rows the last
820 /// frame asked for was owed for (`u32::MAX` before any): emission
821 /// breaks the rows again once the chunks it shapes are known, and
822 /// rows that differ from layout's owe a frame laid out on them —
823 /// once per count, so a cache too small to keep the chunks cannot
824 /// ask forever.
825 laid_rows: u32,
826 asked_rows: u32,
827 bytes: usize,
828}
829
830impl LongLine {
831 /// The chunk `x` (physical, from the line's origin) falls in.
832 fn chunk_at(&self, x: f32) -> usize {
833 match self.prefix[1..].partition_point(|&p| p <= x) {
834 i if i >= self.chunks.len() => self.chunks.len().saturating_sub(1),
835 i => i,
836 }
837 }
838
839 /// The chunk byte `byte` falls in (the last for the end).
840 fn chunk_of_byte(&self, byte: usize) -> usize {
841 match self.chunks.partition_point(|c| c.end <= byte) {
842 i if i >= self.chunks.len() => self.chunks.len().saturating_sub(1),
843 i => i,
844 }
845 }
846
847 fn width(&self) -> f32 {
848 self.prefix.last().copied().unwrap_or(0.0)
849 }
850
851 /// Rows while wrapped; one otherwise.
852 fn rows(&self) -> u32 {
853 match self.starts.last() {
854 Some(&(r, _)) if self.wrap_w.is_some() => r + 1,
855 _ => 1,
856 }
857 }
858
859 /// The chunks whose rows touch `ra..=rb`, while wrapped: chunk `i`
860 /// spans `starts[i].0..=starts[i + 1].0`.
861 fn chunks_on_rows(&self, ra: u32, rb: u32) -> Option<(usize, usize)> {
862 let n = self.chunks.len();
863 if n == 0 || self.starts.len() != n + 1 {
864 return None;
865 }
866 let first = self.starts[1..].partition_point(|s| s.0 < ra);
867 let last = self.starts[..n].partition_point(|s| s.0 <= rb);
868 (first < last).then_some((first, last - 1))
869 }
870
871 /// Recomputes `prefix` from the chunk widths, estimating the unshaped.
872 /// A chunk ending in a tab it started off the stops for is as wide as
873 /// takes that tab to the line's next stop (backlog RG76), so the chunk
874 /// after it starts on one; the stop is exact once every chunk since
875 /// the line's previous tab is shaped, and otherwise as near as the
876 /// estimate `prefix` already is.
877 fn reprefix(&mut self) {
878 let mut at = 0.0f32;
879 self.prefix.clear();
880 self.prefix.push(0.0);
881 for c in &self.chunks {
882 at = match (c.width, c.tab) {
883 (Some(_), Some((tab_x, every))) => next_tab_stop(at + tab_x, every),
884 (Some(w), None) => at + w,
885 (None, _) => at + (c.end - c.start) as f32 * self.avg,
886 };
887 self.prefix.push(at);
888 }
889 }
890
891 /// How far the line moves chunk `i`'s end from where its run ends:
892 /// nonzero only for a chunk whose tab the line places (`reprefix`).
893 fn tab_shift(&self, i: usize) -> f32 {
894 let c = &self.chunks[i];
895 match (c.width, c.tab) {
896 (Some(w), Some(_)) => self.prefix[i + 1] - self.prefix[i] - w,
897 _ => 0.0,
898 }
899 }
900
901 /// `tab_shift` for a caret `local` bytes into chunk `i`: only the
902 /// chunk's end is past a tab the line places, which is its last
903 /// character, so only the end caret moves — and with it the line's end
904 /// caret, which then agrees with the line's width (backlog RG122).
905 fn end_shift(&self, i: usize, local: usize) -> f32 {
906 let c = &self.chunks[i];
907 match local >= c.end - c.start {
908 true => self.tab_shift(i),
909 false => 0.0,
910 }
911 }
912
913 /// The byte a point at `x` along chunk `i`'s run answers when it is on
914 /// a tab the line places: the run's tab is as wide as the chunk's own
915 /// stops make it and the drawn one as wide as the line's (`reprefix`),
916 /// so the run's hit test splits it at the wrong place. The tab before
917 /// the drawn tab's middle, the byte after it from there (backlog
918 /// RG122). `None` off such a tab.
919 fn placed_tab_hit(&self, i: usize, x: f32) -> Option<usize> {
920 let c = &self.chunks[i];
921 let (tab_x, _) = c.tab?;
922 c.width?;
923 if x < tab_x {
924 return None;
925 }
926 let drawn_end = self.prefix[i + 1] - self.prefix[i];
927 Some(match x < (tab_x + drawn_end) / 2.0 {
928 true => c.end - 1,
929 false => c.end,
930 })
931 }
932}
933
934/// The tab stop after `x` (physical px from the line's start), stops
935/// `every` px apart: cosmic-text's rule, which moves a tab already on a
936/// stop to the next one. `x` is a sum of chunk widths, so a stop within a
937/// sixteenth of a pixel ahead counts as reached — the drift that sum has
938/// over a stretch without a tab, which a whole line's own sum has too.
939fn next_tab_stop(x: f32, every: f32) -> f32 {
940 if every <= 0.0 {
941 return x;
942 }
943 (((x + 1.0 / 16.0) / every).floor() + 1.0) * every
944}
945
946/// Where a long line is cut: after the last tab in the second half of each
947/// window, else after the last whitespace there, else at the last grapheme
948/// boundary inside it — except that a chunk starting anywhere but after a
949/// tab ends after its first tab, wherever in the window that falls. A line
950/// under [`LONG_LINE_BYTES`] — long only by its `break-spaces` — is one
951/// chunk, shaped whole as the run it would otherwise be.
952///
953/// Tab stops are why: cosmic-text measures them from where
954/// the shaped text starts, so a chunk shaped alone put a tab after its
955/// start at a stop measured from there and not from the line's. A chunk
956/// cut just after a tab ends on a stop, so the next one starts on one and
957/// its stops are the line's (RG75). A chunk that starts off the stops —
958/// after a stretch of more than half a window without a tab — holds at
959/// most one tab, its last character, whose advance the line places from
960/// where the chunk starts (`Chunk::tab`, backlog RG76): nothing in the
961/// chunk follows that tab, so its run needs no reshaping for the line's x.
962fn chunk_ranges(content: &str) -> Vec<(usize, usize)> {
963 use unicode_segmentation::UnicodeSegmentation;
964 if content.len() < LONG_LINE_BYTES {
965 return vec![(0, content.len())];
966 }
967 let bytes = content.as_bytes();
968 let mut out = Vec::with_capacity(content.len() / CHUNK_BYTES + 1);
969 let mut start = 0usize;
970 while start < content.len() {
971 // The window's end floored to a char boundary: a multibyte
972 // character straddling `start + CHUNK_BYTES` is the window's, not
973 // the next one's, and slicing inside it was a panic (backlog C44).
974 let mut window_end = (start + CHUNK_BYTES).min(content.len());
975 while !content.is_char_boundary(window_end) {
976 window_end -= 1;
977 }
978 let window = &content[start..window_end];
979 // Where the window's second half starts, on a char boundary.
980 let mut from = (CHUNK_BYTES / 2).min(window.len());
981 while !window.is_char_boundary(from) {
982 from += 1;
983 }
984 // Off the stops (after anything but a tab), a tab in the first
985 // half ends the chunk: `find` is a memchr, so a tab-free line pays
986 // a search per window and not a second walk of it (RG75's cost).
987 let off_stops = start > 0 && bytes[start - 1] != b'\t';
988 let early_tab = match off_stops {
989 true => window[..from].find('\t'),
990 false => None,
991 };
992 let end = if let Some(t) = early_tab {
993 start + t + 1
994 } else if window_end == content.len() && !(off_stops && window[from..].contains('\t')) {
995 window_end
996 } else {
997 // One pass over the second half, noting its first and last
998 // tab and its last whitespace: a line re-cut on every edit
999 // (C19) must not walk each window twice.
1000 let (mut first_tab, mut last_tab, mut space) = (None, None, None);
1001 for (i, c) in window[from..].char_indices() {
1002 if c.is_whitespace() {
1003 let end = from + i + c.len_utf8();
1004 space = Some(end);
1005 if c == '\t' {
1006 first_tab = first_tab.or(Some(end));
1007 last_tab = Some(end);
1008 }
1009 }
1010 }
1011 let tab = if off_stops { first_tab } else { last_tab };
1012 match tab.or(space) {
1013 Some(i) => start + i,
1014 None => {
1015 // The last grapheme boundary at or before the window's end.
1016 let mut cut = 0usize;
1017 for (i, _) in window.grapheme_indices(true) {
1018 if i == 0 {
1019 continue;
1020 }
1021 cut = i;
1022 }
1023 if cut == 0 { window_end } else { start + cut }
1024 }
1025 }
1026 };
1027 out.push((start, end));
1028 start = end;
1029 }
1030 out
1031}
1032
1033/// How many enclosing keys a place remembers: a text run answers to its
1034/// own key and to any of this many ancestors, which is a `line` row, a
1035/// selection wrapper around a run, and two to spare. A text further than
1036/// this below its `line` raises `text-beyond-line`.
1037pub(crate) const PLACE_ANCESTORS: usize = 4;
1038
1039/// The keys above a text node, nearest first, as many as a place
1040/// remembers, and where among them a `role="none"` ancestor sits — what
1041/// `Core::text_ancestors` gathers for [`TextSystem::place`].
1042pub(crate) struct Ancestry {
1043 pub(crate) keys: [Key; PLACE_ANCESTORS],
1044 pub(crate) depth: usize,
1045 pub(crate) none_at: Option<usize>,
1046}
1047
1048/// Where a text node was drawn: what `Core::text_hit` and
1049/// `Core::caret_rect` answer from. Recorded at emission, so
1050/// a node the frame culled — scrolled out of its clip — has no place and
1051/// answers nothing, which is also true of a point nobody can click.
1052pub(crate) struct TextPlace {
1053 pub(crate) key: Key,
1054 /// The node's index in the frame's tree: what puts an `over_siblings`
1055 /// subtree's runs back in tree order after it painted late
1056 /// (`TextSystem::order_places`).
1057 node: u32,
1058 /// The keys above it, nearest first, as many as `depth` says.
1059 ancestors: [Key; PLACE_ANCESTORS],
1060 depth: u8,
1061 /// The nearest ancestor (an index into `ancestors`) declaring
1062 /// `role="none"`, or `u8::MAX` for none within reach: a query by a key
1063 /// above it does not reach this run, the way the access tree skips a
1064 /// gutter's text when it reads a `line`; a query by the
1065 /// gutter itself still does.
1066 none_at: u8,
1067 cache_key: u64,
1068 /// The node's origin, logical viewport px.
1069 origin: Vec2,
1070 /// The innermost `selectable` node above this run, when there is one.
1071 /// What `scope_runs` gathers by, and the reason a place
1072 /// is recorded for a run that was never drawn — see `drawn`.
1073 scope: Option<Key>,
1074 /// Whether the run was painted. False for a run inside a selection
1075 /// scope that the frame culled: emission skips a node clipped
1076 /// entirely away, but a selection reaching past the viewport needs
1077 /// that node's content and its place in the order, so a scoped run
1078 /// records where it *would* have been. Hit tests and the public
1079 /// `text_hit` / `caret_rect` queries ignore these, because a point
1080 /// nobody can click still answers nothing.
1081 drawn: bool,
1082}
1083
1084impl TextPlace {
1085 fn answers_to(&self, key: Key) -> bool {
1086 if self.key == key {
1087 return true;
1088 }
1089 // Found among the ancestors, and not through a `role="none"`
1090 // subtree below the key: a gutter's text is not the line's, and
1091 // is still the gutter's own.
1092 self.ancestors[..self.depth as usize]
1093 .iter()
1094 .position(|k| *k == key)
1095 .is_some_and(|d| d <= self.none_at as usize)
1096 }
1097}
1098
1099/// One run inside a selection scope: where it was placed, its text, and
1100/// the byte offset its content starts at in the scope's concatenation.
1101/// The bases count content only — a separator between two runs is a
1102/// decision the *copy* makes (see `TextSystem::scope_slice`), so an
1103/// offset means the same thing whoever asks for it.
1104pub(crate) struct ScopeRun<'a> {
1105 pub place: &'a TextPlace,
1106 /// The run's entry, whichever way it was shaped.
1107 pub text: &'a Entry,
1108 pub base: usize,
1109}
1110
1111impl ScopeRun<'_> {
1112 /// The half-open byte range this run occupies in the concatenation.
1113 /// The run's `[start, end)` in the scope's concatenation.
1114 pub(crate) fn span(&self) -> (usize, usize) {
1115 (self.base, self.base + self.text.content().len())
1116 }
1117}
1118
1119/// Where a point landed in the text a keyed node drew: a byte offset and
1120/// the visual row it is on. `byte` is a caret position: between two
1121/// characters, past the last one at the end, and cosmic-text's rule for
1122/// which side of a glyph the point fell on. For a node holding several
1123/// text runs the offset runs across them in tree order, the way the
1124/// access tree reads a `line`. `line` is the **visual row** within the
1125/// node, 0-based, counted across every run the key covers by where the
1126/// rows sit: a `line` row of three inline runs is one row, a run that
1127/// wrapped is as many as it wrapped to, and two runs stacked are two —
1128/// not the wrapped line within one run's buffer, and not the ordinal `role="line"` node a pointer
1129/// event's `line` names (that one counts rows of the editor, this one
1130/// rows of the text asked about).
1131#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1132pub struct TextHit {
1133 pub byte: usize,
1134 pub line: u32,
1135}
1136
1137impl TextHit {
1138 /// `{byte, line}`.
1139 pub fn to_value(self) -> Value {
1140 Value::map([
1141 ("byte", Value::Int(self.byte as i64)),
1142 ("line", Value::Int(self.line as i64)),
1143 ])
1144 }
1145}
1146
1147/// What a piece of text measures, in logical px at the current scale —
1148/// the same numbers layout uses for a text node with that content and
1149/// style, so a view can size a column to its widest label or pick a tier
1150/// that fits without hand-tuned magic numbers.
1151#[derive(Clone, Copy, Debug, Default, PartialEq)]
1152pub struct TextMetrics {
1153 pub width: f32,
1154 pub height: f32,
1155 /// Lines after wrapping (capped by `max_lines`).
1156 pub lines: u32,
1157}
1158
1159impl TextMetrics {
1160 /// `{width, height, lines}`.
1161 pub fn to_value(self) -> Value {
1162 Value::map([
1163 ("width", Value::float(self.width)),
1164 ("height", Value::float(self.height)),
1165 ("lines", Value::Int(self.lines as i64)),
1166 ])
1167 }
1168}
1169
1170/// The shaping and rasterization state of one window. The font database
1171/// it shapes against is not in here — that is the session's
1172/// ([`crate::session::Session`]), passed in as `fs` — because a font
1173/// registered in one window has to shape in every window of the session.
1174/// What is here is coupled to this window's glyph atlas: the shaped-buffer
1175/// cache stamps its positioned glyphs with the atlas stamp they were
1176/// packed against, and the frame lists are what `TextId` indexes.
1177pub struct TextSystem {
1178 raster: Raster,
1179 /// The rounded span backgrounds this frame emitted, for the pass that
1180 /// joins them once every text is painted.
1181 joins: Vec<JoinBg>,
1182 /// Every shaped run and every long line, by key — a long line's key
1183 /// is salted (`LONG_SALT`) and its chunks are runs beside it.
1184 entries: FxHashMap<u64, Entry>,
1185 /// The sum of the entries' `bytes`, kept exact against inserts and
1186 /// removals so a frame inside its budget costs one comparison.
1187 bytes: usize,
1188 budget: usize,
1189 /// This frame's text nodes, and the previous frame's kept the same way
1190 /// and on the same condition as `Core`'s previous tree: a departing
1191 /// subtree's text nodes carry that frame's `TextId`s, and this is what
1192 /// they index (see [`Self::prev_frame_text`]).
1193 frame: Kept<FrameText>,
1194 /// Where this frame's text nodes were drawn, and where the last
1195 /// frame's were — always kept: a query during a build answers from the
1196 /// frame that finished, which is the layout a click was made against.
1197 places: Kept<TextPlace>,
1198 scale: f32,
1199 frame_no: u64,
1200 /// Emission broke a long line into rows other than the ones layout
1201 /// used: the frame laid out on them is owed. Taken by [`Self::take_owed`].
1202 owed: bool,
1203}
1204
1205/// A style's features in cosmic-text's terms. Empty stays empty, which is
1206/// the shaper's own defaults.
1207pub(crate) fn cosmic_features(f: &crate::spec::FontFeatures) -> cosmic_text::FontFeatures {
1208 let mut out = cosmic_text::FontFeatures::new();
1209 for (tag, value) in f.iter() {
1210 out.set(cosmic_text::FeatureTag::new(tag), value);
1211 }
1212 out
1213}
1214
1215/// The faces the three generic families resolve to, per platform, in
1216/// order of preference. cosmic-text's own defaults are `Open Sans`,
1217/// `DejaVu Serif` and `Noto Sans Mono`, none of which a stock Windows or
1218/// macOS machine has; sans survives that through the platform fallback
1219/// list, but a missing monospace family falls to the *lowest-id*
1220/// monospaced face in the database — style is not in that ranking's key,
1221/// so on a machine whose first monospaced face is an italic instance every
1222/// `Mono` glyph is italic. The first installed name in each
1223/// list wins; when none is, cosmic-text's own name stays so its fallback
1224/// still runs.
1225#[cfg(target_os = "macos")]
1226const DEFAULT_FAMILIES: [&[&str]; 3] = [
1227 &["Helvetica Neue", "Helvetica"],
1228 &["Times New Roman", "Times"],
1229 &["SF Mono", "Menlo", "Monaco"],
1230];
1231#[cfg(target_os = "windows")]
1232const DEFAULT_FAMILIES: [&[&str]; 3] = [
1233 &["Segoe UI", "Arial"],
1234 &["Times New Roman"],
1235 &["Cascadia Mono", "Consolas", "Courier New"],
1236];
1237#[cfg(not(any(target_os = "macos", target_os = "windows")))]
1238const DEFAULT_FAMILIES: [&[&str]; 3] = [
1239 &["DejaVu Sans", "Noto Sans", "Liberation Sans", "Ubuntu"],
1240 &["DejaVu Serif", "Noto Serif", "Liberation Serif"],
1241 &[
1242 "DejaVu Sans Mono",
1243 "Noto Sans Mono",
1244 "Liberation Mono",
1245 "Ubuntu Mono",
1246 ],
1247];
1248
1249/// The first family on `list` that some installed face is a member of,
1250/// matched the way `fontdb::Database::query` matches a name.
1251fn first_installed<'a>(db: &cosmic_text::fontdb::Database, list: &[&'a str]) -> Option<&'a str> {
1252 list.iter().copied().find(|name| {
1253 db.faces()
1254 .any(|face| face.families.iter().any(|(f, _)| f == name))
1255 })
1256}
1257
1258/// The session's font database, set up the way kui shapes against it: the
1259/// generic sans, serif and monospace families pinned to an installed face
1260/// (see [`DEFAULT_FAMILIES`]), so weight and style matching starts from a
1261/// face with real variants rather than whatever the fallback pops.
1262/// Faces whose glyphs cannot be measured are taken out first.
1263///
1264/// The system's faces are scanned and checked once a process
1265/// ([`system_fonts`]); every session after the first starts from a copy of
1266/// that database. The scan opens every font file twice (fontdb's, then
1267/// [`keep_measurable`]'s) — on a Mac 1312 faces in ~850 files, 20–70 ms and
1268/// most of it `open`, paid again by every `Core::new` — where a copy is a
1269/// list of names. A font installed while the process runs is seen when
1270/// something asks for a new scan (`Core::reload_system_fonts`).
1271pub(crate) fn new_font_system() -> (FontSystem, std::sync::Arc<SystemFonts>) {
1272 let system = system_fonts();
1273 let fonts = font_system_over(system.locale.clone(), system.db.clone(), &[]);
1274 (fonts, system)
1275}
1276
1277/// A font system over `db` whose fallback asks `first`'s families before
1278/// the platform's ([`AppFallback`]). cosmic-text reads its fallback lists
1279/// when the system is built and keeps them, so a new list is a new system
1280/// over the same database: the ids stay.
1281pub(crate) fn font_system_over(
1282 locale: String,
1283 db: cosmic_text::fontdb::Database,
1284 first: &[String],
1285) -> FontSystem {
1286 FontSystem::new_with_locale_and_db_and_fallback(locale, db, AppFallback::new(first))
1287}
1288
1289/// The platform's fallback lists with the app's families ahead of them
1290/// (`Core::set_fallback_fonts`, backlog F121): a character the text's own
1291/// family has no glyph for is asked of each of the app's, in order, before
1292/// the script's faces and the platform's common ones — on macOS the
1293/// system's proportional face, which is no editor's second choice.
1294/// With none it is the platform's lists as they are.
1295pub(crate) struct AppFallback {
1296 platform: cosmic_text::PlatformFallback,
1297 first: &'static [&'static str],
1298 common: &'static [&'static str],
1299 /// A script's list with `first` ahead of it, made when first asked
1300 /// for.
1301 scripts: std::sync::Mutex<
1302 rustc_hash::FxHashMap<(unicode_script::Script, String), &'static [&'static str]>,
1303 >,
1304}
1305
1306/// `names` for the life of the process, each spelling and each list kept
1307/// once: cosmic-text's lists are `&'static`, and an app sets a handful.
1308fn keep(names: Vec<&'static str>) -> &'static [&'static str] {
1309 static LISTS: std::sync::Mutex<Vec<&'static [&'static str]>> =
1310 std::sync::Mutex::new(Vec::new());
1311 let mut lists = LISTS.lock().unwrap_or_else(|e| e.into_inner());
1312 if let Some(&kept) = lists.iter().find(|l| ***l == names[..]) {
1313 return kept;
1314 }
1315 let kept: &'static [&'static str] = Box::leak(names.into_boxed_slice());
1316 lists.push(kept);
1317 kept
1318}
1319
1320fn keep_name(name: &str) -> &'static str {
1321 static NAMES: std::sync::Mutex<Vec<&'static str>> = std::sync::Mutex::new(Vec::new());
1322 let mut names = NAMES.lock().unwrap_or_else(|e| e.into_inner());
1323 if let Some(&kept) = names.iter().find(|n| **n == name) {
1324 return kept;
1325 }
1326 let kept: &'static str = Box::leak(name.to_string().into_boxed_str());
1327 names.push(kept);
1328 kept
1329}
1330
1331impl AppFallback {
1332 pub(crate) fn new(first: &[String]) -> Self {
1333 use cosmic_text::Fallback;
1334 let platform = cosmic_text::PlatformFallback;
1335 let first = keep(first.iter().map(|n| keep_name(n)).collect());
1336 let common = keep(
1337 first
1338 .iter()
1339 .chain(platform.common_fallback())
1340 .copied()
1341 .collect(),
1342 );
1343 Self {
1344 platform,
1345 first,
1346 common,
1347 scripts: Default::default(),
1348 }
1349 }
1350}
1351
1352impl cosmic_text::Fallback for AppFallback {
1353 fn common_fallback(&self) -> &[&'static str] {
1354 self.common
1355 }
1356
1357 fn forbidden_fallback(&self) -> &[&'static str] {
1358 self.platform.forbidden_fallback()
1359 }
1360
1361 fn script_fallback(&self, script: unicode_script::Script, locale: &str) -> &[&'static str] {
1362 let theirs = self.platform.script_fallback(script, locale);
1363 if self.first.is_empty() {
1364 return theirs;
1365 }
1366 let mut scripts = self.scripts.lock().unwrap_or_else(|e| e.into_inner());
1367 scripts
1368 .entry((script, locale.to_string()))
1369 .or_insert_with(|| keep(self.first.iter().chain(theirs).copied().collect()))
1370 }
1371}
1372
1373/// One scan of the system's fonts, checked and pinned the way kui shapes
1374/// against them, and which files' faces it found — what a session started
1375/// from, so a later scan can say what came and went since.
1376pub(crate) struct SystemFonts {
1377 locale: String,
1378 db: cosmic_text::fontdb::Database,
1379 /// Each face the scan kept, by its file and its index in the file.
1380 pub(crate) faces: rustc_hash::FxHashSet<(std::path::PathBuf, u32)>,
1381}
1382
1383/// The process's scan; `None` until the first session asks.
1384static SYSTEM: std::sync::Mutex<Option<std::sync::Arc<SystemFonts>>> = std::sync::Mutex::new(None);
1385
1386/// The process's scan of the system's fonts, made on the first call. Held
1387/// under the lock while it scans, so sessions made at once on several
1388/// threads wait for one scan rather than each making its own.
1389pub(crate) fn system_fonts() -> std::sync::Arc<SystemFonts> {
1390 let mut held = SYSTEM.lock().unwrap_or_else(|e| e.into_inner());
1391 held.get_or_insert_with(|| std::sync::Arc::new(scan_system_fonts()))
1392 .clone()
1393}
1394
1395/// Scans the system's fonts again and makes that the process's scan, so
1396/// sessions made from here on start from it; returns it.
1397pub(crate) fn rescan_system_fonts() -> std::sync::Arc<SystemFonts> {
1398 let fresh = std::sync::Arc::new(scan_system_fonts());
1399 *SYSTEM.lock().unwrap_or_else(|e| e.into_inner()) = Some(fresh.clone());
1400 fresh
1401}
1402
1403fn scan_system_fonts() -> SystemFonts {
1404 let (locale, db) = FontSystem::new().into_locale_and_db();
1405 SystemFonts::from_db(locale, db)
1406}
1407
1408impl SystemFonts {
1409 /// A scan from a database already loaded: the unmeasurable faces taken
1410 /// out and the generic families pinned.
1411 pub(crate) fn from_db(locale: String, mut db: cosmic_text::fontdb::Database) -> Self {
1412 let all: Vec<_> = db.faces().map(|face| face.id).collect();
1413 keep_measurable(&mut db, all);
1414 pin_default_families(&mut db);
1415 let faces = db.faces().filter_map(face_file).collect();
1416 Self { locale, db, faces }
1417 }
1418
1419 /// This scan's database, to build a test's own scan from.
1420 #[cfg(test)]
1421 pub(crate) fn db(&self) -> &cosmic_text::fontdb::Database {
1422 &self.db
1423 }
1424
1425 #[cfg(test)]
1426 pub(crate) fn locale(&self) -> &str {
1427 &self.locale
1428 }
1429}
1430
1431/// The file a face was read from and its index there; `None` for a face
1432/// loaded from bytes.
1433pub(crate) fn face_file(face: &cosmic_text::fontdb::FaceInfo) -> Option<(std::path::PathBuf, u32)> {
1434 use cosmic_text::fontdb::Source;
1435 match &face.source {
1436 Source::File(path) | Source::SharedFile(path, _) => Some((path.clone(), face.index)),
1437 Source::Binary(_) => None,
1438 }
1439}
1440
1441/// Points the generic sans, serif and monospace families at the first
1442/// installed family of [`DEFAULT_FAMILIES`]' lists; a list with none
1443/// installed leaves its generic as it was.
1444pub(crate) fn pin_default_families(db: &mut cosmic_text::fontdb::Database) {
1445 let [sans, serif, mono] = DEFAULT_FAMILIES;
1446 if let Some(name) = first_installed(db, sans) {
1447 db.set_sans_serif_family(name);
1448 }
1449 if let Some(name) = first_installed(db, serif) {
1450 db.set_serif_family(name);
1451 }
1452 if let Some(name) = first_installed(db, mono) {
1453 db.set_monospace_family(name);
1454 }
1455}
1456
1457/// [`new_font_system`] over a database already loaded. The font system is
1458/// built after the unmeasurable faces are out, so cosmic-text's list of
1459/// monospaced faces, which `Mono`'s fallback walks, never names one.
1460#[cfg(test)]
1461fn font_system_with(locale: String, db: cosmic_text::fontdb::Database) -> FontSystem {
1462 let system = SystemFonts::from_db(locale, db);
1463 FontSystem::new_with_locale_and_db(system.locale, system.db)
1464}
1465
1466/// Whether the shaper can say how wide a face's glyphs are:
1467/// a `head` whose units per em are in the range the spec allows
1468/// (16–16384), an `hhea` with at least one horizontal metric, and an
1469/// `hmtx` as long as that says. Every advance is design units over units
1470/// per em, so a face without a readable `head` shapes to infinitely wide
1471/// glyphs — macOS's GB18030 Bitmap, which carries Apple's `bhed` in its
1472/// place — and swash, which reads `hmtx` for the raster, subtracts one
1473/// from a zero count. Read with skrifa, the reader the shaper measures
1474/// with, from the table directory, two fixed-size headers and `hmtx`'s
1475/// length: nothing that can panic on the face it is there to catch.
1476pub(crate) fn measurable(data: &[u8], index: u32) -> bool {
1477 use cosmic_text::skrifa::raw::{FontRef, TableProvider};
1478 let Ok(font) = FontRef::from_index(data, index) else {
1479 return false;
1480 };
1481 font.head()
1482 .is_ok_and(|head| (16..=16384).contains(&head.units_per_em()))
1483 && font.hhea().is_ok_and(|hhea| hhea.number_of_h_metrics() > 0)
1484 && font.hmtx().is_ok()
1485}
1486
1487/// Files per checking thread, below which [`keep_measurable`] checks on
1488/// the caller's thread: a font added from bytes, or a folder of a few.
1489const FILES_PER_CHECKER: usize = 64;
1490
1491/// Takes the faces of `ids` the shaper cannot measure out of `db` (see
1492/// [`measurable`]) and returns the rest, so a face that would shape to
1493/// infinitely wide glyphs is not a family to list, to name or to fall
1494/// back to. A face whose file cannot be read goes too: the
1495/// shaper could not load it either.
1496///
1497/// Each file is opened once for all its faces, and the files are spread
1498/// over a few threads: opening one is most of the cost. Measured over the
1499/// 1312 faces in 850 files of a Mac, the check adds about 4.5 ms to the
1500/// ~20 ms fontdb takes to scan them warm (release build); on one thread
1501/// it took 15–22 ms.
1502pub(crate) fn keep_measurable(
1503 db: &mut cosmic_text::fontdb::Database,
1504 ids: Vec<cosmic_text::fontdb::ID>,
1505) -> Vec<cosmic_text::fontdb::ID> {
1506 use cosmic_text::fontdb::{ID, Source};
1507 // The faces of one file together; bytes already in memory each alone.
1508 let mut by_file = FxHashMap::<&std::path::Path, Vec<ID>>::default();
1509 let mut groups = Vec::new();
1510 for &id in &ids {
1511 match db.face(id).map(|face| &face.source) {
1512 Some(Source::File(path) | Source::SharedFile(path, _)) => {
1513 by_file.entry(path).or_default().push(id);
1514 }
1515 Some(Source::Binary(_)) => groups.push(vec![id]),
1516 None => {}
1517 }
1518 }
1519 groups.extend(by_file.into_values());
1520 let db_ref = &*db;
1521 let unmeasurable_in = |groups: &[Vec<ID>]| -> Vec<ID> {
1522 let mut out = Vec::new();
1523 for group in groups {
1524 let read = db_ref.with_face_data(group[0], |data, _| {
1525 group
1526 .iter()
1527 .filter(|&&id| {
1528 db_ref
1529 .face(id)
1530 .is_none_or(|face| !measurable(data, face.index))
1531 })
1532 .copied()
1533 .collect::<Vec<_>>()
1534 });
1535 out.extend(read.unwrap_or_else(|| group.clone()));
1536 }
1537 out
1538 };
1539 let checkers = std::thread::available_parallelism()
1540 .map_or(1, |n| n.get())
1541 .min(groups.len().div_ceil(FILES_PER_CHECKER));
1542 let unmeasurable = if checkers <= 1 {
1543 unmeasurable_in(&groups)
1544 } else {
1545 let per = groups.len().div_ceil(checkers);
1546 std::thread::scope(|scope| {
1547 let parts: Vec<_> = groups
1548 .chunks(per)
1549 .map(|part| {
1550 let spawned = std::thread::Builder::new()
1551 .name("kui-font-check".into())
1552 .spawn_scoped(scope, move || unmeasurable_in(part));
1553 (part, spawned.ok())
1554 })
1555 .collect();
1556 let mut out = Vec::new();
1557 for (part, checker) in parts {
1558 out.extend(match checker {
1559 Some(checker) => checker
1560 .join()
1561 .unwrap_or_else(|panic| std::panic::resume_unwind(panic)),
1562 // No thread to be had: check that part here.
1563 None => unmeasurable_in(part),
1564 });
1565 }
1566 out
1567 })
1568 };
1569 for &id in &unmeasurable {
1570 db.remove_face(id);
1571 }
1572 ids.into_iter()
1573 .filter(|id| !unmeasurable.contains(id))
1574 .collect()
1575}
1576
1577/// The family names `Sans`, `Serif` and `Mono` shape with, in that order —
1578/// what [`new_font_system`] pinned, or cosmic-text's own name where nothing
1579/// on the list was installed.
1580pub(crate) fn default_families(fs: &FontSystem) -> [&str; 3] {
1581 let db = fs.db();
1582 [
1583 db.family_name(&cosmic_text::Family::SansSerif),
1584 db.family_name(&cosmic_text::Family::Serif),
1585 db.family_name(&cosmic_text::Family::Monospace),
1586 ]
1587}
1588
1589impl TextSystem {
1590 pub fn new() -> Self {
1591 Self {
1592 raster: Raster::new(),
1593 joins: Vec::new(),
1594 entries: FxHashMap::default(),
1595 bytes: 0,
1596 budget: DEFAULT_TEXT_CACHE_BYTES,
1597 frame: Kept::default(),
1598 places: Kept::default(),
1599 scale: 1.0,
1600 frame_no: 0,
1601 owed: false,
1602 }
1603 }
1604
1605 /// Whether this frame's emission owes another frame,
1606 /// clearing it.
1607 pub(crate) fn take_owed(&mut self) -> bool {
1608 std::mem::take(&mut self.owed)
1609 }
1610
1611 /// The rounded backgrounds emitted since the last call: what the
1612 /// frame's join pass shapes (`crate::join`).
1613 pub(crate) fn take_joins(&mut self) -> Vec<JoinBg> {
1614 std::mem::take(&mut self.joins)
1615 }
1616
1617 /// This window's rasterizer, for glyph raster against its atlas.
1618 pub(crate) fn raster_mut(&mut self) -> &mut Raster {
1619 &mut self.raster
1620 }
1621
1622 pub(crate) fn subpixel(&self) -> bool {
1623 self.raster.subpixel
1624 }
1625
1626 /// Switches outline rasterization between alpha masks and LCD subpixel
1627 /// coverage. Returns whether it changed (the caller resets the atlas).
1628 pub(crate) fn set_subpixel(&mut self, on: bool) -> bool {
1629 let changed = self.raster.subpixel != on;
1630 self.raster.subpixel = on;
1631 changed
1632 }
1633
1634 /// The byte budget the cache is evicted to; see
1635 /// `Core::set_text_cache_budget`.
1636 pub fn budget(&self) -> usize {
1637 self.budget
1638 }
1639
1640 /// Sets the budget. Takes effect at the next frame's start, where
1641 /// eviction runs; nothing is evicted here.
1642 pub fn set_budget(&mut self, bytes: usize) {
1643 self.budget = bytes;
1644 }
1645
1646 /// The estimated bytes the cache holds (see `ENTRY_BASE_BYTES`).
1647 pub fn bytes(&self) -> usize {
1648 self.bytes
1649 }
1650
1651 /// How many shaped texts the cache holds (a long line's chunks each
1652 /// count; the line itself does not).
1653 pub fn len(&self) -> usize {
1654 self.entries.len() - self.long_lines()
1655 }
1656
1657 /// How many long lines are held.
1658 pub fn long_lines(&self) -> usize {
1659 self.entries.values().filter(|e| e.long().is_some()).count()
1660 }
1661
1662 pub fn is_empty(&self) -> bool {
1663 self.len() == 0
1664 }
1665
1666 /// Evicts the least recently used entries until the cache is under
1667 /// three quarters of its budget. Never an entry the frame that just
1668 /// finished drew: what is on screen stays shaped whatever the budget
1669 /// says, the way F26's declared state is never evicted. Ordered by
1670 /// last use and then by key, so the same history evicts the same
1671 /// entries whatever order the map iterated.
1672 fn evict_to_budget(&mut self, fs: &mut FontSystem) {
1673 if self.bytes <= self.budget {
1674 return;
1675 }
1676 let floor = self.budget / EVICT_TO_DENOMINATOR * EVICT_TO_NUMERATOR;
1677 let drawn_last_frame = self.frame_no.saturating_sub(1);
1678 // Oldest first, runs and long lines alike: a long line's record is
1679 // charged to `bytes` like any entry (its shaped chunks are entries
1680 // in their own right), so the budget reaches it too rather than
1681 // leaving the records to the 300-frame sweep alone.
1682 let mut order: Vec<(u64, u64)> = self
1683 .entries
1684 .iter()
1685 .filter(|(_, e)| e.last_used() < drawn_last_frame)
1686 .map(|(k, e)| (e.last_used(), *k))
1687 .collect();
1688 order.sort_unstable();
1689 for (_, key) in order {
1690 if self.bytes <= floor {
1691 break;
1692 }
1693 let freed = self.entries.remove(&key).map(|e| e.bytes());
1694 self.bytes -= freed.unwrap_or(0);
1695 }
1696 // The words those entries shaped are still in cosmic-text's
1697 // shape-run cache, which has no byte budget of its own and ages
1698 // only when trimmed: drop what has not been used since the last
1699 // eviction. Measured with the 2 MB budget in `tests/text_budget.rs`,
1700 // leaving it on its 240-frame clock held nine times the budget in
1701 // words alone. A line still on screen keeps its shaped entry
1702 // whatever happens here; only a line edited afterwards shapes its
1703 // words again, once.
1704 fs.shape_run_cache.trim(0);
1705 }
1706
1707 /// Starts a frame. `keep_prev` retains the list just finished so the
1708 /// next frame can still read its texts — `Core` sets it exactly when it
1709 /// keeps the previous tree, and the two are read together.
1710 pub(crate) fn begin_frame(
1711 &mut self,
1712 fs: &mut FontSystem,
1713 scale: f32,
1714 keep_prev: bool,
1715 frame_no: u64,
1716 ) {
1717 self.joins.clear();
1718 // Scale change invalidates every physical-px measurement.
1719 if (scale - self.scale).abs() > f32::EPSILON {
1720 self.entries.clear();
1721 self.bytes = 0;
1722 }
1723 self.scale = scale;
1724 self.frame.begin(keep_prev);
1725 // Always kept, unlike `frame`: a query while this frame builds
1726 // answers from the last one, and this is what it answers from.
1727 self.places.begin(true);
1728 // The core's counter, not one of this store's own (backlog AR45).
1729 self.frame_no = frame_no;
1730 if let Some(cutoff) = crate::retain::sweep_cutoff(self.frame_no) {
1731 let mut freed = 0usize;
1732 self.entries.retain(|_, e| {
1733 let keep = e.last_used() >= cutoff;
1734 if !keep {
1735 freed += e.bytes();
1736 }
1737 keep
1738 });
1739 self.bytes -= freed;
1740 // The shape-run cache makes single-line reshapes ~free while
1741 // editing; trim it so long sessions don't grow unboundedly.
1742 fs.shape_run_cache.trim(2);
1743 }
1744 // The clock above frees what nobody has shown for five seconds;
1745 // the budget frees what a stream of new text piles up faster than
1746 // that (backlog C16).
1747 self.evict_to_budget(fs);
1748 }
1749
1750 /// Drops every shaped entry, to shape again on its next draw: the
1751 /// weights a family is asked at changed under them.
1752 pub(crate) fn forget_shaped(&mut self) {
1753 self.entries.clear();
1754 self.bytes = 0;
1755 }
1756
1757 /// Inserts a fresh entry, charging it to the budget.
1758 fn insert(&mut self, key: u64, entry: Entry) {
1759 self.bytes += entry.bytes();
1760 if let Some(old) = self.entries.insert(key, entry) {
1761 self.bytes -= old.bytes();
1762 }
1763 }
1764
1765 /// The shaped run under `key`, if the entry there is one.
1766 fn run(&self, key: u64) -> Option<&CachedText> {
1767 self.entries.get(&key)?.run()
1768 }
1769
1770 fn run_mut(&mut self, key: u64) -> Option<&mut CachedText> {
1771 self.entries.get_mut(&key)?.run_mut()
1772 }
1773
1774 /// The long line under `key`, if the entry there is one.
1775 fn long(&self, key: u64) -> Option<&LongLine> {
1776 self.entries.get(&key)?.long()
1777 }
1778
1779 fn long_mut(&mut self, key: u64) -> Option<&mut LongLine> {
1780 self.entries.get_mut(&key)?.long_mut()
1781 }
1782
1783 pub(crate) fn style_key(content: &str, style: &TextStyle, scale: f32) -> u64 {
1784 // The content (word-wide past a few bytes, backlog C43) and the
1785 // shaping-relevant style bits (color excluded).
1786 let mut h = crate::key::mix_content(crate::key::FNV_OFFSET, content.as_bytes());
1787 let mut mix = |bytes: &[u8]| h = crate::key::fnv(h, bytes);
1788 mix(&style.size.to_bits().to_le_bytes());
1789 mix(&style.line_height.to_bits().to_le_bytes());
1790 mix(&scale.to_bits().to_le_bytes());
1791 mix(&[style.wrap as u8, style.ellipsis as u8]);
1792 mix(&style.max_lines.to_le_bytes());
1793 mix(&style.initial_letter.to_le_bytes());
1794 let (tag, font) = match style.family {
1795 FontFamily::Sans => (0u8, 0u64),
1796 FontFamily::Serif => (1, 0),
1797 FontFamily::Mono => (2, 0),
1798 FontFamily::Custom(id) => (3, id.to_ffi()),
1799 };
1800 mix(&[tag]);
1801 mix(&font.to_le_bytes());
1802 for (t, v) in style.features.iter() {
1803 mix(t);
1804 mix(&v.to_le_bytes());
1805 }
1806 // Paint only, but a decorated text is a different entry: the
1807 // decoration rects are built beside the glyph templates — and
1808 // their shape and colour with them (backlog K4).
1809 // Bold is a different face, so a different shape.
1810 mix(&[style.bold as u8]);
1811 mix(&[
1812 style.underline as u8,
1813 style.strikethrough as u8,
1814 style.underline_style as u8,
1815 style.underline_color.is_some() as u8,
1816 ]);
1817 if let Some(c) = style.underline_color {
1818 mix(&c.to_hex().to_le_bytes());
1819 }
1820 h
1821 }
1822
1823 /// Shapes (or reuses) the buffer for `content` in `style`; returns its
1824 /// cache key. Shared by text nodes and measurement, so measuring a
1825 /// string and then drawing it shapes once.
1826 fn intern(
1827 &mut self,
1828 content: &str,
1829 style: &TextStyle,
1830 res: &Resources,
1831 fs: &mut FontSystem,
1832 ) -> u64 {
1833 let key = Self::style_key(content, style, self.scale);
1834 self.intern_keyed(key, content, style, res, fs)
1835 }
1836
1837 /// [`Self::intern`] under a key already computed.
1838 fn intern_keyed(
1839 &mut self,
1840 key: u64,
1841 content: &str,
1842 style: &TextStyle,
1843 res: &Resources,
1844 fs: &mut FontSystem,
1845 ) -> u64 {
1846 let frame_no = self.frame_no;
1847 let scale = self.scale;
1848 if !self.entries.contains_key(&key) {
1849 let mut buffer = new_buffer(fs, style, scale);
1850 // At the family's regular (or its bold, F150), as a span or a
1851 // cell is: a family with no 400 face asked at 400 falls back
1852 // to another family (F100).
1853 buffer.set_text(
1854 &one_paragraph(content),
1855 &res.weights_of(style.family).apply(
1856 Attrs::new()
1857 .family(res.family_of(style.family))
1858 .font_features(cosmic_features(&style.features)),
1859 style.bold,
1860 ),
1861 Shaping::Advanced,
1862 None,
1863 );
1864 let entry = CachedText::new(
1865 buffer,
1866 content.to_string(),
1867 style,
1868 vec![SpanDeco::of_style(style)],
1869 fs,
1870 frame_no,
1871 );
1872 self.insert(key, Entry::Run(entry));
1873 }
1874 self.entries
1875 .get_mut(&key)
1876 .expect("just inserted")
1877 .touch(frame_no);
1878 key
1879 }
1880
1881 /// The content of one of this frame's texts (spans concatenated).
1882 pub(crate) fn content(&self, id: TextId) -> &str {
1883 let key = self.frame[id.0 as usize].cache_key;
1884 self.entries.get(&key).map_or("", |e| e.content())
1885 }
1886
1887 /// Appends the access runs of one of this frame's texts, as `src`
1888 /// places them (a custom editor's `line`s are read this way): a run's
1889 /// laid-out lines, or a long line's rows of the chunks it has shaped —
1890 /// one that never showed is not walked, the way a tall document's
1891 /// off-screen lines are not. Every `break-spaces` text is a long line.
1892 pub(crate) fn access_runs(
1893 &self,
1894 id: TextId,
1895 src: crate::access::RunSource,
1896 run_no: &mut usize,
1897 out: &mut Vec<crate::access::AccessRun>,
1898 ) {
1899 use crate::access::{RowGlyphs, push_row_runs};
1900 let Some(ft) = self.frame.get(id.0 as usize) else {
1901 return;
1902 };
1903 let line = match self.entries.get(&ft.cache_key) {
1904 Some(Entry::Run(e)) => {
1905 crate::access::runs_of_buffer(&e.buffer, src, run_no, out, |i| e.shift(i));
1906 return;
1907 }
1908 Some(Entry::Long(line)) => line,
1909 None => return,
1910 };
1911 let whole = [RowStart { byte: 0, x: 0.0 }];
1912 for (i, c) in line.chunks.iter().enumerate() {
1913 let Some(run) = c
1914 .width
1915 .and_then(|_| self.run(c.key))
1916 .and_then(|e| e.buffer.layout_runs().next())
1917 else {
1918 continue;
1919 };
1920 // Wrapped, the chunk's rows from where its first begins;
1921 // else one row at its place along the line.
1922 let (rows, row0, head_x) = match (line.wrap_w, line.starts.get(i)) {
1923 (Some(_), Some(&(row0, head_x))) if !c.rows.is_empty() => {
1924 (&c.rows[..], row0, head_x)
1925 }
1926 (Some(_), _) => continue,
1927 (None, _) => (&whole[..], 0, line.prefix[i]),
1928 };
1929 for (r, rs) in rows.iter().enumerate() {
1930 let hi = rows.get(r + 1).map_or(usize::MAX, |n| n.byte as usize);
1931 let a = run.glyphs.partition_point(|g| g.start < rs.byte as usize);
1932 let b = run.glyphs.partition_point(|g| g.start < hi).max(a);
1933 if a == b {
1934 continue;
1935 }
1936 let last = i + 1 == line.chunks.len() && r + 1 == rows.len();
1937 push_row_runs(
1938 &RowGlyphs {
1939 glyphs: &run.glyphs[a..b],
1940 text: run.text,
1941 line: 0,
1942 top: (row0 as usize + r) as f32 * line.line_h,
1943 height: line.line_h,
1944 rtl: run.rtl,
1945 dx: if r == 0 { head_x } else { 0.0 } - rs.x,
1946 base: c.start,
1947 newline: last && src.newline_after_last,
1948 },
1949 &src,
1950 run_no,
1951 out,
1952 );
1953 }
1954 }
1955 }
1956
1957 /// The cache key and colour behind one of the *previous* frame's texts
1958 /// — what a departing subtree keeps instead of its `TextId`, which
1959 /// indexes a list rebuilt every frame. The subtree is copied out of the
1960 /// previous frame's tree, so this is the list its ids belong to (see
1961 /// [`crate::depart`]).
1962 pub(crate) fn prev_frame_text(&self, id: TextId) -> (u64, Color) {
1963 match self.frame.prev().get(id.0 as usize) {
1964 Some(t) => (t.cache_key, t.color),
1965 None => (0, Color::TRANSPARENT),
1966 }
1967 }
1968
1969 /// Registers an already-shaped buffer as one of this frame's texts, by
1970 /// the cache key [`Self::frame_text`] handed out. None once the entry
1971 /// has been evicted — a ghost older than the cache draws no text
1972 /// rather than a wrong one. Touching it here keeps it alive for as
1973 /// long as something still draws it.
1974 pub(crate) fn readd(&mut self, cache_key: u64, color: Color) -> Option<TextId> {
1975 let frame_no = self.frame_no;
1976 let e = self.entries.get_mut(&cache_key)?;
1977 e.touch(frame_no);
1978 let cache_key = if e.long().is_some() {
1979 self.claim_long(cache_key)
1980 } else {
1981 cache_key
1982 };
1983 self.frame.push(FrameText { cache_key, color });
1984 Some(TextId((self.frame.len() - 1) as u32))
1985 }
1986
1987 /// Registers a text for this frame, shaping (or reusing) its buffer.
1988 pub fn add(
1989 &mut self,
1990 content: &str,
1991 style: &TextStyle,
1992 res: &Resources,
1993 fs: &mut FontSystem,
1994 ) -> TextId {
1995 // The one place the long/short decision is made: the entry's
1996 // variant carries it from here.
1997 let key = self.intern_any(content, style, res, fs);
1998 let key = if could_be_long(content.len(), style) {
1999 self.claim_long(key)
2000 } else {
2001 key
2002 };
2003 self.frame.push(FrameText {
2004 cache_key: key,
2005 color: style.color_or_default(),
2006 });
2007 TextId((self.frame.len() - 1) as u32)
2008 }
2009
2010 /// The key a node drawing the entry `key` holds it by. A run is
2011 /// shared, re-wrapped to each node as it is drawn; a long line's rows
2012 /// are the line's own and every query reads them, so the first node
2013 /// of a frame takes the line and each further one a copy of its own —
2014 /// the n-th node the n-th copy, so a frame like the last finds the
2015 /// copies it made laid out already. The copies share the chunks'
2016 /// shaped runs. Without it one `break-spaces` file open in two panes
2017 /// of different widths would draw and answer at the width laid out last.
2018 #[inline(never)]
2019 fn claim_long(&mut self, key: u64) -> u64 {
2020 let frame_no = self.frame_no;
2021 let mut at = key;
2022 let mut n = 0u64;
2023 loop {
2024 match self.entries.get_mut(&at) {
2025 Some(Entry::Long(l)) if l.claimed != frame_no => {
2026 l.claimed = frame_no;
2027 l.last_used = frame_no;
2028 return at;
2029 }
2030 // A run is never a long line's copy: its key is.
2031 Some(Entry::Run(_)) if n == 0 => return key,
2032 Some(_) => {}
2033 None => {
2034 let Some(line) = self.long(key) else {
2035 return key;
2036 };
2037 let mut copy = line.clone();
2038 copy.claimed = frame_no;
2039 copy.last_used = frame_no;
2040 self.insert(at, Entry::Long(copy));
2041 return at;
2042 }
2043 }
2044 n += 1;
2045 at = crate::key::fnv(key ^ COPY_SALT, &n.to_le_bytes());
2046 }
2047 }
2048
2049 /// Interns `content` as the long line it is or the run it is, one
2050 /// hash either way: a text that could be long by its length and style
2051 /// is looked up under both keys before its bytes are scanned for a
2052 /// line break, so the steady state of a megabyte line is its hash and
2053 /// two lookups.
2054 fn intern_any(
2055 &mut self,
2056 content: &str,
2057 style: &TextStyle,
2058 res: &Resources,
2059 fs: &mut FontSystem,
2060 ) -> u64 {
2061 if !could_be_long(content.len(), style) {
2062 return self.intern(content, style, res, fs);
2063 }
2064 let key = Self::style_key(content, style, self.scale);
2065 let long_key = key ^ LONG_SALT;
2066 let frame_no = self.frame_no;
2067 if let Some(e) = self.entries.get_mut(&long_key) {
2068 e.touch(frame_no);
2069 return long_key;
2070 }
2071 if let Some(e) = self.entries.get_mut(&key) {
2072 e.touch(frame_no);
2073 return key;
2074 }
2075 if has_line_break(content) || content.len() < LONG_LINE_BYTES && has_rtl(content) {
2076 self.intern_keyed(key, content, style, res, fs)
2077 } else {
2078 self.build_long(long_key, content.to_string(), Vec::new(), style, res, fs)
2079 }
2080 }
2081
2082 /// The rich counterpart of [`Self::intern_any`]: a paragraph past the
2083 /// threshold with no line break is a long line whose chunks are rich
2084 /// runs; its content is concatenated only when the line
2085 /// is built.
2086 fn intern_rich_any(
2087 &mut self,
2088 spans: &[Span<'_>],
2089 base: &TextStyle,
2090 res: &Resources,
2091 fs: &mut FontSystem,
2092 ) -> u64 {
2093 let len: usize = spans.iter().map(|s| s.text.len()).sum();
2094 if !could_be_long(len, base) {
2095 return self.intern_rich(spans, base, res, fs);
2096 }
2097 let key = Self::rich_key(spans, base, self.scale);
2098 let long_key = key ^ LONG_SALT;
2099 let frame_no = self.frame_no;
2100 if let Some(e) = self.entries.get_mut(&long_key) {
2101 e.touch(frame_no);
2102 return long_key;
2103 }
2104 if let Some(e) = self.entries.get_mut(&key) {
2105 e.touch(frame_no);
2106 return key;
2107 }
2108 // A sized span makes a line as tall as it is, which the long
2109 // line's one estimated row height cannot follow: shaped whole.
2110 if spans
2111 .iter()
2112 .any(|s| has_line_break(s.text) || s.size.is_some())
2113 || len < LONG_LINE_BYTES && spans.iter().any(|s| has_rtl(s.text))
2114 {
2115 return self.intern_rich_keyed(key, spans, base, res, fs);
2116 }
2117 let mut content = String::with_capacity(len);
2118 let mut owned = Vec::with_capacity(spans.len());
2119 for s in spans {
2120 let start = content.len();
2121 content.push_str(s.text);
2122 owned.push(OwnedSpan {
2123 start,
2124 end: content.len(),
2125 attrs: s.attrs_only(),
2126 });
2127 }
2128 self.build_long(long_key, content, owned, base, res, fs)
2129 }
2130
2131 /// Builds the long line `content` is under `key` — plain, or rich
2132 /// with `spans` — shaping its first chunk for the line
2133 /// height and the advance the rest are estimated from; see
2134 /// [`LongLine`]. The callers have looked `key` up already.
2135 fn build_long(
2136 &mut self,
2137 key: u64,
2138 content: String,
2139 spans: Vec<OwnedSpan>,
2140 style: &TextStyle,
2141 res: &Resources,
2142 fs: &mut FontSystem,
2143 ) -> u64 {
2144 let frame_no = self.frame_no;
2145 let wrap = style.wrap;
2146 let style = &TextStyle {
2147 wrap: TextWrap::None,
2148 ..*style
2149 };
2150 let scale = self.scale;
2151 let chunks: Vec<Chunk> = chunk_ranges(&content)
2152 .into_iter()
2153 .map(|(start, end)| Chunk {
2154 start,
2155 end,
2156 key: if spans.is_empty() {
2157 Self::style_key(&content[start..end], style, scale)
2158 } else {
2159 Self::rich_key(&chunk_spans(&content, &spans, start, end), style, scale)
2160 },
2161 width: None,
2162 tab: None,
2163 rows: Vec::new(),
2164 })
2165 .collect();
2166 // The first chunk is shaped now: the line's height and the mean
2167 // advance the estimates need come from it.
2168 let (w0, line_h) = match chunks.first() {
2169 Some(c) => {
2170 let k = self.shape_chunk(&content, &spans, c.start, c.end, style, res, fs);
2171 let e = self.run(k).expect("just interned");
2172 (e.intrinsic.w, e.buffer.metrics().line_height)
2173 }
2174 None => (0.0, style.line_height * self.scale),
2175 };
2176 let avg = chunks
2177 .first()
2178 .map_or(0.0, |c| w0 / (c.end - c.start).max(1) as f32);
2179 let mut line = LongLine {
2180 content,
2181 spans,
2182 style: *style,
2183 wrap,
2184 wrap_w: None,
2185 starts: Vec::new(),
2186 chunks,
2187 prefix: Vec::new(),
2188 line_h,
2189 avg,
2190 last_used: frame_no,
2191 claimed: u64::MAX,
2192 laid_rows: 1,
2193 asked_rows: u32::MAX,
2194 bytes: 0,
2195 };
2196 if let Some(c) = line.chunks.first_mut() {
2197 c.width = Some(w0);
2198 }
2199 line.reprefix();
2200 line.bytes = ENTRY_BASE_BYTES
2201 + line.content.len()
2202 + line.chunks.len() * 48
2203 + line.spans.len() * std::mem::size_of::<OwnedSpan>();
2204 // A line long only by its `break-spaces` is one chunk
2205 // (`chunk_ranges`), shaped now: its first frame is laid out on the
2206 // rows it has, not on an estimate (RG68).
2207 self.insert(key, Entry::Long(line));
2208 key
2209 }
2210
2211 /// Shapes (or touches) the run for `start..end` of a long line's
2212 /// content: a plain run, or a rich one of the spans that intersect
2213 /// the range. Returns its key.
2214 #[allow(clippy::too_many_arguments)]
2215 fn shape_chunk(
2216 &mut self,
2217 content: &str,
2218 spans: &[OwnedSpan],
2219 start: usize,
2220 end: usize,
2221 style: &TextStyle,
2222 res: &Resources,
2223 fs: &mut FontSystem,
2224 ) -> u64 {
2225 if spans.is_empty() {
2226 self.intern(&content[start..end], style, res, fs)
2227 } else {
2228 let spans = chunk_spans(content, spans, start, end);
2229 self.intern_rich(&spans, style, res, fs)
2230 }
2231 }
2232
2233 /// Makes sure chunk `i` of the long line `key` is shaped, and moves
2234 /// the prefix sums if its width was an estimate. Returns whether it
2235 /// shaped now — what tells a wrapped line its rows need breaking.
2236 fn ensure_chunk(&mut self, key: u64, i: usize, res: &Resources, fs: &mut FontSystem) -> bool {
2237 let (chunk_key, known) = {
2238 let c = &self.long(key).expect("a long line").chunks[i];
2239 (c.key, c.width)
2240 };
2241 let frame_no = self.frame_no;
2242 if known.is_some()
2243 && let Some(e) = self.run_mut(chunk_key)
2244 {
2245 e.last_used = frame_no;
2246 return false;
2247 }
2248 // Shaping now: the chunk's text and its spans rebased to the
2249 // slice, copied out so the line stays in the map meanwhile.
2250 let (text, spans, style, off_stops) = {
2251 let line = self.long(key).expect("a long line");
2252 let c = &line.chunks[i];
2253 let first = line.spans.partition_point(|s| s.end <= c.start);
2254 let spans: Vec<OwnedSpan> = line.spans[first..]
2255 .iter()
2256 .take_while(|s| s.start < c.end)
2257 .map(|s| OwnedSpan {
2258 start: s.start.max(c.start) - c.start,
2259 end: s.end.min(c.end) - c.start,
2260 attrs: s.attrs,
2261 })
2262 .collect();
2263 let bytes = line.content.as_bytes();
2264 let off_stops = c.start > 0 && bytes[c.start - 1] != b'\t';
2265 (
2266 line.content[c.start..c.end].to_string(),
2267 spans,
2268 line.style,
2269 off_stops,
2270 )
2271 };
2272 let k = self.shape_chunk(&text, &spans, 0, text.len(), &style, res, fs);
2273 let w = self.run(k).expect("just interned").intrinsic.w;
2274 let tab = match off_stops && text.ends_with('\t') {
2275 true => self.chunk_tab(k, &text, &spans, &style, res, fs),
2276 false => None,
2277 };
2278 let line = self.long_mut(key).expect("just read");
2279 line.chunks[i].key = k;
2280 line.chunks[i].tab = tab;
2281 if line.chunks[i].width != Some(w) {
2282 line.chunks[i].width = Some(w);
2283 line.reprefix();
2284 }
2285 true
2286 }
2287
2288 /// For a chunk that starts off the line's tab stops and ends in its one
2289 /// tab, shaped as `chunk`: where the tab starts in that run, and the
2290 /// stops' interval — the width of the tab shaped alone in its own
2291 /// span's style, which starts on a stop (backlog RG76). `None` for a
2292 /// right-to-left run, whose tab is not at its end.
2293 #[inline(never)]
2294 #[allow(clippy::too_many_arguments)]
2295 fn chunk_tab(
2296 &mut self,
2297 chunk: u64,
2298 text: &str,
2299 spans: &[OwnedSpan],
2300 style: &TextStyle,
2301 res: &Resources,
2302 fs: &mut FontSystem,
2303 ) -> Option<(f32, f32)> {
2304 let at = text.len() - 1;
2305 let tab_x = {
2306 let run = self.run(chunk)?.buffer.layout_runs().next()?;
2307 if run.rtl {
2308 return None;
2309 }
2310 run.glyphs.iter().rev().find(|g| g.start == at)?.x
2311 };
2312 let alone = self.shape_chunk(text, spans, at, text.len(), style, res, fs);
2313 let every = self.run(alone)?.intrinsic.w;
2314 Some((tab_x, every))
2315 }
2316
2317 /// Breaks the long line `key` into rows at `w` (physical px), or lays
2318 /// it as one row for `None`. Positions, not glyphs: each chunk's rows
2319 /// start where the previous chunk's last row ended; a shaped chunk
2320 /// breaks exactly ([`break_rows`]), one that never showed contributes
2321 /// the estimate its width is, corrected when it shapes — the way
2322 /// `prefix` is, so the height the scrollbar sees can move a little as
2323 /// chunks fill in.
2324 fn relayout_long(&mut self, key: u64, w: Option<f32>) {
2325 let Some(w) = w else {
2326 let line = self.long_mut(key).expect("a long line");
2327 line.wrap_w = None;
2328 line.starts.clear();
2329 for c in &mut line.chunks {
2330 c.rows.clear();
2331 }
2332 return;
2333 };
2334 let w = w.max(1.0);
2335 // Read every chunk's rows off its shaped run first — the runs and
2336 // the line are entries of one map — then write them back.
2337 let line = self.long(key).expect("a long line");
2338 let mut starts = Vec::with_capacity(line.chunks.len() + 1);
2339 starts.push((0, 0.0));
2340 let (mut row, mut x) = (0u32, 0.0f32);
2341 let rows: Vec<Vec<RowStart>> = line
2342 .chunks
2343 .iter()
2344 .enumerate()
2345 .map(|(i, c)| {
2346 let shaped = c.width.and_then(|_| self.run(c.key));
2347 let rows = match shaped {
2348 Some(e) => {
2349 let text = &line.content[c.start..c.end];
2350 let (rows, end) = break_rows(&e.buffer, text, line.wrap, w, x);
2351 row += rows.len() as u32 - 1;
2352 // A tab the line places ends the chunk where the
2353 // unwrapped line puts it, as a whole run's tab is
2354 // measured before its rows are broken (RG76).
2355 x = end + line.tab_shift(i);
2356 rows
2357 }
2358 None => {
2359 let est = x + c
2360 .width
2361 .unwrap_or_else(|| (c.end - c.start) as f32 * line.avg);
2362 let added = (est / w).floor();
2363 row += added as u32;
2364 x = est - added * w;
2365 Vec::new()
2366 }
2367 };
2368 starts.push((row, x));
2369 rows
2370 })
2371 .collect();
2372 let line = self.long_mut(key).expect("a long line");
2373 line.wrap_w = Some(w);
2374 line.starts = starts;
2375 for (c, rows) in line.chunks.iter_mut().zip(rows) {
2376 c.rows = rows;
2377 }
2378 }
2379
2380 /// A long line's size at `max_w` (physical px): one row clamped to it,
2381 /// or, when the style wraps and the line does not fit, its rows broken
2382 /// to it. Returns the physical size and the row count.
2383 fn long_size(&mut self, key: u64, max_w: Option<f32>) -> (Size, u32) {
2384 let (wrap, width, line_h, cur) = {
2385 let l = self.long(key).expect("a long line");
2386 (l.wrap, l.width(), l.line_h, l.wrap_w)
2387 };
2388 let target = match max_w {
2389 Some(w) if wrap != TextWrap::None && width > w + 0.5 => Some(w.max(1.0)),
2390 _ => None,
2391 };
2392 let differs = match (cur, target) {
2393 (None, None) => false,
2394 (Some(a), Some(b)) => (a - b).abs() > 0.5,
2395 _ => true,
2396 };
2397 if differs {
2398 self.relayout_long(key, target);
2399 }
2400 match target {
2401 Some(w) => {
2402 let rows = self.long(key).expect("a long line").rows();
2403 (Size::new(w, rows as f32 * line_h), rows)
2404 }
2405 None => (Size::new(max_w.map_or(width, |m| width.min(m)), line_h), 1),
2406 }
2407 }
2408
2409 /// Measures `content` in `style` without adding a node: its unwrapped
2410 /// size, or with `max_w` (logical px) its size once wrapped to that
2411 /// width. The answer is what layout would give a text node with the
2412 /// same content and style at the current scale, `wrap` / `max_lines` /
2413 /// `ellipsis` included.
2414 pub fn measure(
2415 &mut self,
2416 content: &str,
2417 style: &TextStyle,
2418 res: &Resources,
2419 fs: &mut FontSystem,
2420 max_w: Option<f32>,
2421 ) -> TextMetrics {
2422 let key = self.intern_any(content, style, res, fs);
2423 self.measure_any(key, fs, max_w)
2424 }
2425
2426 /// `measure` for the entry under `key`, a long line or a run.
2427 fn measure_any(&mut self, key: u64, fs: &mut FontSystem, max_w: Option<f32>) -> TextMetrics {
2428 if self.long(key).is_some() {
2429 let scale = self.scale;
2430 // Without a width nothing is broken, and the layout the frame
2431 // holds is left as it is.
2432 let (size, lines) = match max_w {
2433 Some(m) => {
2434 // Nor with one: the rows are put back for the node
2435 // that drew the line, which queries read.
2436 let held = self.long(key).expect("checked").wrap_w;
2437 let measured = self.long_size(key, Some(m * scale));
2438 if wrap_differs(held, self.long(key).expect("checked").wrap_w) {
2439 self.relayout_long(key, held);
2440 }
2441 measured
2442 }
2443 None => {
2444 let line = self.long(key).expect("checked");
2445 (Size::new(line.width(), line.line_h), 1)
2446 }
2447 };
2448 return TextMetrics {
2449 width: size.w / scale,
2450 height: size.h / scale,
2451 lines,
2452 };
2453 }
2454 self.measure_key(key, fs, max_w)
2455 }
2456
2457 /// `measure` for a rich-text paragraph.
2458 pub fn measure_rich(
2459 &mut self,
2460 spans: &[Span<'_>],
2461 base: &TextStyle,
2462 res: &Resources,
2463 fs: &mut FontSystem,
2464 max_w: Option<f32>,
2465 ) -> TextMetrics {
2466 let key = self.intern_rich_any(spans, base, res, fs);
2467 self.measure_any(key, fs, max_w)
2468 }
2469
2470 fn measure_key(&mut self, key: u64, fs: &mut FontSystem, max_w: Option<f32>) -> TextMetrics {
2471 let scale = self.scale;
2472 let entry = self.run_mut(key).expect("just interned");
2473 let target = wrap_target(entry, max_w, scale);
2474 // A run a node has drawn keeps the rows it was drawn at: its
2475 // `text_hit` and `caret_rect` read them until the next frame, which
2476 // would only wrap them back. A measure at another width lays out a
2477 // copy at that width instead, kept for the next measure there —
2478 // between frames as well as within one, where RG72's claim is
2479 // (backlog RG76).
2480 let key = if entry.claimed != u64::MAX && wrap_differs(entry.wrap, target) {
2481 self.wrap_copy(key, target, fs)
2482 } else {
2483 key
2484 };
2485 let entry = self.run_mut(key).expect("just interned");
2486 wrap_entry(entry, fs, target);
2487 let (mut m, lines) = entry.measure();
2488 if entry.clamp_w
2489 && let Some(t) = target
2490 {
2491 m.w = m.w.min(t);
2492 }
2493 TextMetrics {
2494 width: m.w / scale,
2495 height: m.h / scale,
2496 lines,
2497 }
2498 }
2499
2500 /// Registers a rich-text paragraph for this frame. Spans shape as one
2501 /// flow, so wrapping crosses style boundaries correctly; past the
2502 /// long-line threshold the flow is chunked like a plain line's.
2503 pub fn add_rich(
2504 &mut self,
2505 spans: &[Span<'_>],
2506 base: &TextStyle,
2507 res: &Resources,
2508 fs: &mut FontSystem,
2509 ) -> TextId {
2510 let key = self.intern_rich_any(spans, base, res, fs);
2511 let len = spans.iter().map(|s| s.text.len()).sum();
2512 let key = if could_be_long(len, base) {
2513 self.claim_long(key)
2514 } else {
2515 key
2516 };
2517 self.frame.push(FrameText {
2518 cache_key: key,
2519 color: base.color_or_default(),
2520 });
2521 TextId((self.frame.len() - 1) as u32)
2522 }
2523
2524 fn intern_rich(
2525 &mut self,
2526 spans: &[Span<'_>],
2527 base: &TextStyle,
2528 res: &Resources,
2529 fs: &mut FontSystem,
2530 ) -> u64 {
2531 let key = Self::rich_key(spans, base, self.scale);
2532 self.intern_rich_keyed(key, spans, base, res, fs)
2533 }
2534
2535 /// The cache key of a rich paragraph: the base style's, then every
2536 /// span's text and attributes in order. Never a plain text's key for
2537 /// the same content.
2538 fn rich_key(spans: &[Span<'_>], base: &TextStyle, scale: f32) -> u64 {
2539 let mut key = Self::style_key("", base, scale) ^ 0x9e37_79b9_7f4a_7c15;
2540 for s in spans {
2541 key = crate::key::mix_content(key, s.text.as_bytes());
2542 let mut mix = |bytes: &[u8]| key = crate::key::fnv(key, bytes);
2543 mix(&[
2544 s.bold as u8,
2545 s.italic as u8,
2546 s.color.is_some() as u8,
2547 s.underline as u8,
2548 s.strikethrough as u8,
2549 s.bg.is_some() as u8,
2550 s.underline_style as u8,
2551 s.underline_color.is_some() as u8,
2552 ]);
2553 mix(&s.bg_radius.to_bits().to_le_bytes());
2554 // The span's own face and size: a different shaping, so a
2555 // different entry.
2556 let (tag, font) = match s.family {
2557 None => (0u8, 0u64),
2558 Some(FontFamily::Sans) => (1, 0),
2559 Some(FontFamily::Serif) => (2, 0),
2560 Some(FontFamily::Mono) => (3, 0),
2561 Some(FontFamily::Custom(id)) => (4, id.to_ffi()),
2562 };
2563 mix(&[tag]);
2564 mix(&font.to_le_bytes());
2565 mix(&s.size.map_or(0, f32::to_bits).to_le_bytes());
2566 for c in [s.color, s.bg, s.underline_color].into_iter().flatten() {
2567 mix(&c.r.to_bits().to_le_bytes());
2568 mix(&c.g.to_bits().to_le_bytes());
2569 mix(&c.b.to_bits().to_le_bytes());
2570 mix(&c.a.to_bits().to_le_bytes());
2571 }
2572 }
2573 key
2574 }
2575
2576 /// [`Self::intern_rich`] under a key already computed.
2577 fn intern_rich_keyed(
2578 &mut self,
2579 key: u64,
2580 spans: &[Span<'_>],
2581 base: &TextStyle,
2582 res: &Resources,
2583 fs: &mut FontSystem,
2584 ) -> u64 {
2585 let frame_no = self.frame_no;
2586 let scale = self.scale;
2587 if !self.entries.contains_key(&key) {
2588 let mut buffer = new_buffer(fs, base, scale);
2589 let family = res.family_of(base.family);
2590 let weights = res.weights_of(base.family);
2591 let features = cosmic_features(&base.features);
2592 // Once one span has a size of its own every span carries its
2593 // metrics: cosmic-text makes a line as tall as the tallest
2594 // glyph that names its own, so a line holding only a smaller
2595 // span would otherwise come out shorter than the paragraph's.
2596 let sized = spans.iter().any(|s| s.size.is_some());
2597 let texts: Vec<_> = spans.iter().map(|s| one_paragraph(s.text)).collect();
2598 let metrics_of = |size: Option<f32>| {
2599 let size = size.unwrap_or(base.size);
2600 let ratio = if base.size > 0.0 {
2601 base.line_height / base.size
2602 } else {
2603 1.0
2604 };
2605 shaper_metrics(size * scale, size * ratio * scale)
2606 };
2607 // Each span's index rides its glyphs as metadata, which is how
2608 // the decoration rects find their span after layout. A span
2609 // with a face of its own is shaped in it, at its weights.
2610 buffer.set_rich_text(
2611 spans.iter().enumerate().map(|(i, s)| {
2612 let (family, weights) = match s.family {
2613 Some(f) => (res.family_of(f), res.weights_of(f)),
2614 None => (family, weights),
2615 };
2616 let mut attrs = s
2617 .attrs(family, weights)
2618 .font_features(features.clone())
2619 .metadata(i);
2620 // A bold paragraph's spans are bold: a span cannot be
2621 // lighter than its text (backlog F150).
2622 if base.bold && !s.bold {
2623 attrs = weights.apply(attrs, true);
2624 }
2625 if sized {
2626 attrs = attrs.metrics(metrics_of(s.size));
2627 }
2628 (&*texts[i], attrs)
2629 }),
2630 &{
2631 let attrs = weights.apply(
2632 Attrs::new().family(family).font_features(features.clone()),
2633 base.bold,
2634 );
2635 if sized {
2636 attrs.metrics(metrics_of(None))
2637 } else {
2638 attrs
2639 }
2640 },
2641 Shaping::Advanced,
2642 None,
2643 );
2644 let content = spans.iter().map(|s| s.text).collect::<String>();
2645 let mut start = 0u32;
2646 let decos = spans
2647 .iter()
2648 .map(|s| SpanDeco {
2649 start: {
2650 let at = start;
2651 start += s.text.len() as u32;
2652 at
2653 },
2654 color: s.color.map(shaped_color),
2655 underline: s.underline || base.underline,
2656 // The span's own where it says, else the paragraph's.
2657 underline_color: s.underline_color.or(base.underline_color),
2658 underline_style: if s.underline {
2659 s.underline_style
2660 } else {
2661 base.underline_style
2662 },
2663 strikethrough: s.strikethrough || base.strikethrough,
2664 bg: s.bg,
2665 bg_radius: s.bg_radius,
2666 })
2667 .collect();
2668 let entry = CachedText::new(buffer, content, base, decos, fs, frame_no);
2669 self.insert(key, Entry::Run(entry));
2670 }
2671 self.entries
2672 .get_mut(&key)
2673 .expect("just inserted")
2674 .touch(frame_no);
2675 key
2676 }
2677
2678 /// The shaped run behind one of this frame's texts; callers have
2679 /// already taken the long line's path when it is one.
2680 fn entry_mut(&mut self, id: TextId) -> &mut CachedText {
2681 let key = self.frame[id.0 as usize].cache_key;
2682 self.run_mut(key).expect("frame text missing from cache")
2683 }
2684
2685 /// Wraps text `id`'s run to `max_w_logical`: layout at the node's
2686 /// width, and emission again, since the run is shared by every node
2687 /// drawing the same text. The first node of a frame claims it; one
2688 /// asking another width the same frame moves to a copy of its own.
2689 fn ensure_wrap(&mut self, id: TextId, max_w_logical: f32, fs: &mut FontSystem) {
2690 let (scale, frame_no) = (self.scale, self.frame_no);
2691 let entry = self.entry_mut(id);
2692 let target = wrap_target(entry, Some(max_w_logical), scale);
2693 if entry.claimed == frame_no && wrap_differs(entry.claimed_wrap, target) {
2694 self.own_wrap(id, target, fs);
2695 return;
2696 }
2697 entry.claimed = frame_no;
2698 entry.claimed_wrap = target;
2699 wrap_entry(entry, fs, target);
2700 }
2701
2702 /// `ensure_wrap` for a node drawing a run another node of this frame
2703 /// wrapped to another width — the same label in two panes: the node
2704 /// takes the copy of the run at its width, made the first time and
2705 /// found by width after, so each answers `text_hit` and `caret_rect`
2706 /// at its own rows and neither re-wraps the other's (a
2707 /// long line's twin is [`Self::claim_long`]). Off the path every other
2708 /// text takes: a copy costs a buffer clone once and a lookup a frame.
2709 #[inline(never)]
2710 fn own_wrap(&mut self, id: TextId, target: Option<f32>, fs: &mut FontSystem) {
2711 let frame_no = self.frame_no;
2712 let key = self.frame[id.0 as usize].cache_key;
2713 let copy = self.wrap_copy(key, target, fs);
2714 self.frame[id.0 as usize].cache_key = copy;
2715 let entry = self.run_mut(copy).expect("just made");
2716 entry.claimed = frame_no;
2717 entry.claimed_wrap = target;
2718 }
2719
2720 /// The copy of run `key` laid out at `target`, made the first time a
2721 /// width asks for it and found by width after: what a second node
2722 /// drawing the run at another width ([`Self::own_wrap`]) and a
2723 /// `measure_text` at a width no node drew it at ([`Self::measure_key`])
2724 /// lay out instead of the run a node drew.
2725 #[inline(never)]
2726 fn wrap_copy(&mut self, key: u64, target: Option<f32>, fs: &mut FontSystem) -> u64 {
2727 let frame_no = self.frame_no;
2728 // By the half pixel `wrap_differs` tells widths apart by.
2729 let slot = target.map_or(u64::MAX, |t| (t * 2.0).round() as u64);
2730 let copy = crate::key::fnv(key ^ COPY_SALT, &slot.to_le_bytes());
2731 if self.run(copy).is_none() {
2732 let fork = self.run(key).expect("a run").fork(frame_no);
2733 self.insert(copy, Entry::Run(fork));
2734 }
2735 let entry = self.run_mut(copy).expect("just made");
2736 entry.last_used = frame_no;
2737 wrap_entry(entry, fs, target);
2738 copy
2739 }
2740
2741 /// Emits positioned glyph quads for a laid-out text node.
2742 /// `origin` and `node` are logical; output quads are physical px. The
2743 /// rounded backgrounds it notes are clipped as its parent is, whatever
2744 /// its own box does (`JoinBg::outer`).
2745 #[allow(clippy::too_many_arguments)]
2746 pub(crate) fn emit(
2747 &mut self,
2748 id: TextId,
2749 origin: Vec2,
2750 node: Size,
2751 clip: Clip,
2752 clip_id: ClipId,
2753 clips: &mut Vec<Clip>,
2754 res: &Resources,
2755 fs: &mut FontSystem,
2756 atlas: &mut GlyphAtlas,
2757 out: &mut Vec<Quad>,
2758 sel: Option<((usize, usize), Color)>,
2759 ) {
2760 let from = self.joins.len();
2761 self.emit_text(
2762 id, origin, node, clip, clip_id, clips, res, fs, atlas, out, sel,
2763 );
2764 if self.joins.len() == from {
2765 return;
2766 }
2767 // The box a long line or an overflowing text clips to, as
2768 // `emit_text` works it out.
2769 let key = self.frame[id.0 as usize].cache_key;
2770 let clamps = match self.entries.get(&key) {
2771 Some(Entry::Long(l)) => l.wrap == TextWrap::None,
2772 Some(Entry::Run(e)) => e.clamp_w,
2773 None => false,
2774 };
2775 let own = clamps.then(|| {
2776 let ox = crate::geom::snap_px(origin.x * self.scale);
2777 (ox, ox + (node.w * self.scale).ceil())
2778 });
2779 for j in &mut self.joins[from..] {
2780 j.outer = clip_id;
2781 j.own = own;
2782 }
2783 }
2784
2785 /// `emit_text` for a long line, kept off the run's path: it draws the
2786 /// chunks inside the clip plus one either side, shaping them now if
2787 /// this is the first time they show. Layout broke its
2788 /// rows at the node's final width (`wrapped`), and the line is this
2789 /// node's alone (`claim_long`).
2790 #[allow(clippy::too_many_arguments)]
2791 #[inline(never)]
2792 fn emit_long(
2793 &mut self,
2794 key: u64,
2795 node: Size,
2796 ox: f32,
2797 oy: f32,
2798 nudge: Vec2,
2799 color: Color,
2800 clip: Clip,
2801 clip_id: ClipId,
2802 clips: &mut Vec<Clip>,
2803 res: &Resources,
2804 fs: &mut FontSystem,
2805 atlas: &mut GlyphAtlas,
2806 out: &mut Vec<Quad>,
2807 sel: Option<((usize, usize), Color)>,
2808 ) {
2809 let scale = self.scale;
2810 let line = self.long(key).expect("a long line");
2811 // A line that does not wrap owns its box, as a run that does not
2812 // does; one that wraps draws under its parent's clip, as a
2813 // wrapped run does, so a glyph overhanging a tight row is not cut
2814 // (the alpha.22 regression pass: every `break-spaces` text is a
2815 // long line).
2816 let (clip, clip_id) = if line.wrap == TextWrap::None {
2817 let own = Rect::new(ox, oy, (node.w * scale).ceil(), (node.h * scale).ceil());
2818 let clip = clip.intersect(own, crate::display::SQUARE);
2819 let vis = clip.visible();
2820 if vis.w <= 0.0 || vis.h <= 0.0 {
2821 return;
2822 }
2823 (clip, crate::display::intern_clip(clips, clip))
2824 } else {
2825 (clip, clip_id)
2826 };
2827 // What can show, in the glyphs' own space (ADR 0043).
2828 let vis = clip.visible();
2829 if let Some(((from, to), tint)) = sel {
2830 let rects = self.long_highlight(line, from, to);
2831 push_highlight(&rects, ox, oy, nudge, tint, clip_id, out);
2832 }
2833 if let Some(w) = line.wrap_w {
2834 self.emit_long_rows(
2835 key, w, ox, oy, nudge, color, clip, clip_id, res, fs, atlas, out,
2836 );
2837 return;
2838 }
2839 let (first, last) = {
2840 if line.chunks.is_empty() {
2841 return;
2842 }
2843 let a = line.chunk_at(vis.x - ox).saturating_sub(1);
2844 let b = (line.chunk_at(vis.x + vis.w - ox) + 1).min(line.chunks.len() - 1);
2845 (a, b)
2846 };
2847 for i in first..=last {
2848 self.ensure_chunk(key, i, res, fs);
2849 }
2850 let frame_no = self.frame_no;
2851 self.long_mut(key).expect("checked").last_used = frame_no;
2852 for i in first..=last {
2853 let (chunk_key, x) = {
2854 let line = self.long(key).expect("checked");
2855 (line.chunks[i].key, line.prefix[i])
2856 };
2857 let raster = &mut self.raster;
2858 let entry = self
2859 .entries
2860 .get_mut(&chunk_key)
2861 .and_then(Entry::run_mut)
2862 .expect("just ensured");
2863 emit_entry(
2864 entry,
2865 ox + x,
2866 oy,
2867 nudge,
2868 color,
2869 clip,
2870 clip_id,
2871 raster,
2872 fs,
2873 atlas,
2874 out,
2875 &mut self.joins,
2876 );
2877 }
2878 }
2879
2880 /// [`Self::emit`]'s body.
2881 #[allow(clippy::too_many_arguments)]
2882 fn emit_text(
2883 &mut self,
2884 id: TextId,
2885 origin: Vec2,
2886 node: Size,
2887 clip: Clip,
2888 clip_id: ClipId,
2889 clips: &mut Vec<Clip>,
2890 res: &Resources,
2891 fs: &mut FontSystem,
2892 atlas: &mut GlyphAtlas,
2893 out: &mut Vec<Quad>,
2894 // `sel`: the bytes of this run the window's selection covers and
2895 // the colour to paint them under (ADR 0017). Resolved by the
2896 // caller, the only place that knows this run's ordinal in its
2897 // scope.
2898 sel: Option<((usize, usize), Color)>,
2899 ) {
2900 let FrameText {
2901 cache_key: key,
2902 color,
2903 } = self.frame[id.0 as usize];
2904 let scale = self.scale;
2905 let ox = crate::geom::snap_px(origin.x * scale);
2906 let oy = crate::geom::snap_px(origin.y * scale);
2907 // Where layout put the text, less where it is drawn: what a
2908 // background's edges are snapped from (`on_pixels`).
2909 let nudge = Vec2::new(origin.x * scale - ox, origin.y * scale - oy);
2910 // A long line is drawn by chunks, off the run's path (backlog
2911 // C19).
2912 if self.long(key).is_some() {
2913 self.emit_long(
2914 key, node, ox, oy, nudge, color, clip, clip_id, clips, res, fs, atlas, out, sel,
2915 );
2916 return;
2917 }
2918 self.ensure_wrap(id, node.w, fs);
2919 let raster = &mut self.raster;
2920 let entry = self
2921 .entries
2922 .get_mut(&key)
2923 .and_then(Entry::run_mut)
2924 .expect("frame text missing from cache");
2925
2926 // Overflowing modes own their box: a line that runs past the node's
2927 // width is clipped there rather than painted over siblings.
2928 let (clip, clip_id) = if entry.clamp_w {
2929 let own = Rect::new(ox, oy, (node.w * scale).ceil(), (node.h * scale).ceil());
2930 let clip = clip.intersect(own, crate::display::SQUARE);
2931 (clip, crate::display::intern_clip(clips, clip))
2932 } else {
2933 (clip, clip_id)
2934 };
2935 if let Some(((from, to), tint)) = sel {
2936 let rects = Self::run_highlight(entry, from, to);
2937 push_highlight(&rects, ox, oy, nudge, tint, clip_id, out);
2938 }
2939 emit_entry(
2940 entry,
2941 ox,
2942 oy,
2943 nudge,
2944 color,
2945 clip,
2946 clip_id,
2947 raster,
2948 fs,
2949 atlas,
2950 out,
2951 &mut self.joins,
2952 );
2953 }
2954}
2955
2956/// A text's background rect at physical (`x`, `y`) with each edge
2957/// snapped to a whole pixel, so it meets another text's — the next run's,
2958/// the next row's — on a pixel line and not inside one, where each drew
2959/// part of the pixel and the two left a seam. A box is drawn where layout
2960/// put it, so a gap between two boxes is there at any scale, unless it
2961/// asks for this too (`pixelSnap`), from its layout rect.
2962///
2963/// The edges are snapped from where layout put them, `nudge` (layout's
2964/// origin less the drawn, snapped one) past `x` and `y`, so the next
2965/// text's edge, snapped from its own layout origin, lands on the same
2966/// pixel. From the drawn origin a row's rect ended a pixel into the next
2967/// row wherever their pitch was not whole pixels, and the join drew
2968/// twice.
2969fn on_pixels(x: f32, y: f32, w: f32, h: f32, nudge: Vec2) -> Rect {
2970 Rect::new(x + nudge.x, y + nudge.y, w, h).on_pixels()
2971}
2972
2973/// Pushes selection rects as quads at a run's origin — before its glyphs,
2974/// so the text stays on top of its own highlight.
2975fn push_highlight(
2976 rects: &[Rect],
2977 ox: f32,
2978 oy: f32,
2979 nudge: Vec2,
2980 tint: Color,
2981 clip: ClipId,
2982 out: &mut Vec<Quad>,
2983) {
2984 for r in rects {
2985 out.push(Quad {
2986 rect: on_pixels(ox + r.x, oy + r.y, r.w, r.h, nudge),
2987 color: tint,
2988 border_color: Color::TRANSPARENT,
2989 radius: [0.0; 4],
2990 border_w: 0.0,
2991 blur: 0.0,
2992 kind: QuadKind::Solid,
2993 uv: [0; 4],
2994 clip,
2995 });
2996 }
2997}
2998
2999impl TextSystem {
3000 /// `emit` for a wrapped long line: the chunks whose rows touch the
3001 /// clip, plus one either side, each drawn row by row from its
3002 /// unwrapped templates.
3003 #[allow(clippy::too_many_arguments)]
3004 fn emit_long_rows(
3005 &mut self,
3006 key: u64,
3007 w: f32,
3008 ox: f32,
3009 oy: f32,
3010 nudge: Vec2,
3011 color: Color,
3012 clip: Clip,
3013 clip_id: ClipId,
3014 res: &Resources,
3015 fs: &mut FontSystem,
3016 atlas: &mut GlyphAtlas,
3017 out: &mut Vec<Quad>,
3018 ) {
3019 let vis = clip.visible();
3020 let (first, last) = {
3021 let line = self.long(key).expect("a long line");
3022 let ra = ((vis.y - oy) / line.line_h).floor().max(0.0) as u32;
3023 let rb = ((vis.y + vis.h - oy) / line.line_h).floor().max(0.0) as u32;
3024 let Some((a, b)) = line.chunks_on_rows(ra, rb) else {
3025 return;
3026 };
3027 (a.saturating_sub(1), (b + 1).min(line.chunks.len() - 1))
3028 };
3029 let mut fresh = false;
3030 for i in first..=last {
3031 fresh |= self.ensure_chunk(key, i, res, fs);
3032 }
3033 if fresh {
3034 // A chunk shaped now moves every row after it — and the box
3035 // layout gave the line, which was the estimate's.
3036 self.relayout_long(key, Some(w));
3037 let line = self.long_mut(key).expect("checked");
3038 let rows = line.rows();
3039 if rows != line.laid_rows && rows != line.asked_rows {
3040 line.asked_rows = rows;
3041 self.owed = true;
3042 }
3043 }
3044 let frame_no = self.frame_no;
3045 self.long_mut(key).expect("checked").last_used = frame_no;
3046 for i in first..=last {
3047 let chunk_key = self.long(key).expect("checked").chunks[i].key;
3048 // The line and the chunk's run are two entries of one map, so
3049 // the pair is borrowed together; a chunk's key is never the
3050 // line's own (`LONG_SALT`).
3051 let [Some(Entry::Long(line)), Some(Entry::Run(entry))] =
3052 self.entries.get_disjoint_mut([&key, &chunk_key])
3053 else {
3054 continue;
3055 };
3056 let (row0, head_x) = line.starts[i];
3057 let chunk = &line.chunks[i];
3058 if chunk.rows.is_empty() {
3059 continue;
3060 }
3061 let raster = &mut self.raster;
3062 emit_entry_rows(
3063 entry,
3064 ox,
3065 oy + row0 as f32 * line.line_h,
3066 nudge,
3067 head_x,
3068 &chunk.rows,
3069 line.line_h,
3070 color,
3071 clip,
3072 clip_id,
3073 raster,
3074 fs,
3075 atlas,
3076 out,
3077 &mut self.joins,
3078 );
3079 }
3080 }
3081}
3082
3083/// `emit_entry` for one chunk of a wrapped long line: the templates are
3084/// the chunk's unwrapped run, and each row's glyphs are shifted back by
3085/// where the row starts in it and down by the row. `oy` is the chunk's
3086/// first row; `head_x` where that row begins.
3087#[allow(clippy::too_many_arguments)]
3088fn emit_entry_rows(
3089 entry: &mut CachedText,
3090 ox: f32,
3091 oy: f32,
3092 nudge: Vec2,
3093 head_x: f32,
3094 rows: &[RowStart],
3095 line_h: f32,
3096 color: Color,
3097 clip: Clip,
3098 clip_id: ClipId,
3099 raster: &mut Raster,
3100 fs: &mut FontSystem,
3101 atlas: &mut GlyphAtlas,
3102 out: &mut Vec<Quad>,
3103 joins: &mut Vec<JoinBg>,
3104) {
3105 build_templates(entry, raster, fs, atlas);
3106 // What can show, in the glyphs' own space (ADR 0043).
3107 let vis = clip.visible();
3108 // A decoration rect spans glyphs of the unwrapped run; the row a
3109 // glyph is on is decided by its byte, and a rect by its x — row `r`
3110 // covers `rows[r].x..rows[r + 1].x` of the run — so a rect a row
3111 // break falls inside is cut there, one piece a row, each shifted as
3112 // the row's glyphs are (backlog C42: a rich chunk's spans).
3113 let mut row_pieces = |d: &DecoTemplate, out: &mut Vec<Quad>| {
3114 let (x0, x1) = (d.x, d.x + d.w);
3115 for r in 0..rows.len() {
3116 let ra = rows[r].x;
3117 let rb = rows.get(r + 1).map_or(f32::INFINITY, |n| n.x);
3118 let (a, b) = (x0.max(ra), x1.min(rb));
3119 if b <= a {
3120 continue;
3121 }
3122 let dx = if r == 0 { head_x } else { 0.0 } - ra;
3123 let x = ox + a + dx;
3124 let y = oy + d.y + r as f32 * line_h;
3125 if y >= vis.y + vis.h {
3126 break;
3127 }
3128 if y + d.h <= vis.y || x >= vis.x + vis.w || x + (b - a) <= vis.x {
3129 continue;
3130 }
3131 let quad = Quad {
3132 rect: if d.under {
3133 on_pixels(x, y, b - a, d.h, nudge)
3134 } else {
3135 Rect::new(x, y, b - a, d.h)
3136 },
3137 color: d.color.unwrap_or(color),
3138 border_color: Color::TRANSPARENT,
3139 radius: [0.0; 4],
3140 border_w: 0.0,
3141 blur: 0.0,
3142 kind: QuadKind::Solid,
3143 clip: clip_id,
3144 uv: [0; 4],
3145 };
3146 // A background or a solid line is its rect; a wave or dots
3147 // are pieces built around it, as in `emit_entry`. A rounded
3148 // background is noted for the join pass (backlog F101).
3149 if d.under && d.radius > 0.0 {
3150 joins.push(JoinBg {
3151 quad: out.len() as u32,
3152 radius: d.radius,
3153 outer: clip_id,
3154 own: None,
3155 });
3156 }
3157 if d.under || d.style == UnderlineStyle::Solid {
3158 out.push(quad);
3159 } else {
3160 crate::deco::push_line(
3161 out,
3162 d.style,
3163 x,
3164 y,
3165 b - a,
3166 d.h,
3167 d.color.unwrap_or(color),
3168 clip_id,
3169 );
3170 }
3171 }
3172 };
3173 for d in entry.deco.iter().filter(|d| d.under) {
3174 row_pieces(d, out);
3175 }
3176 let mut r = 0usize;
3177 for g in &entry.glyphs {
3178 while r + 1 < rows.len() && g.byte >= rows[r + 1].byte {
3179 r += 1;
3180 }
3181 let dx = if r == 0 { head_x } else { 0.0 } - rows[r].x;
3182 let x = ox + g.x + dx;
3183 let y = oy + g.y + r as f32 * line_h;
3184 // Rows only go down: past the clip's bottom nothing comes back.
3185 // Both edges are inclusive, as the x test's are: a glyph whose ink
3186 // ends exactly at the clip's top or starts exactly at its bottom
3187 // has no pixel inside it, and which glyph that is depends on the
3188 // face the machine resolved `Mono` to (the alpha.12 Windows round
3189 // saw two, after C32 moved the face).
3190 if y >= vis.y + vis.h {
3191 break;
3192 }
3193 if y + g.h <= vis.y || x >= vis.x + vis.w || x + g.w <= vis.x {
3194 continue;
3195 }
3196 out.push(Quad {
3197 rect: Rect::new(x, y, g.w, g.h),
3198 color: g.color.unwrap_or(color),
3199 border_color: Color::TRANSPARENT,
3200 radius: [0.0; 4],
3201 border_w: 0.0,
3202 blur: 0.0,
3203 kind: g.kind,
3204 clip: clip_id,
3205 uv: g.uv,
3206 });
3207 }
3208 for d in entry.deco.iter().filter(|d| !d.under) {
3209 row_pieces(d, out);
3210 }
3211}
3212
3213/// The rows a chunk's unwrapped run breaks into at width `w` when its
3214/// first row starts `head_x` in: greedy, at the break opportunities UAX
3215/// #14 gives (what cosmic-text's `WordOrGlyph` takes) or at every glyph
3216/// for `Glyph`, a piece wider than a row breaking by glyph, trailing
3217/// whitespace hanging past the edge as cosmic-text lets it — except under
3218/// `BreakSpaces`, where each whitespace character is a piece of its own
3219/// and takes its room like ink, so one that does not fit starts the next
3220/// row and none hangs (CSS's `break-spaces`, a break before the run too).
3221/// Returns the
3222/// row starts — the first is the chunk's own — and the x its last row
3223/// ends at. Positions are read left to right: a bidi run breaks by its
3224/// glyph order.
3225fn break_rows(
3226 buffer: &Buffer,
3227 text: &str,
3228 wrap: TextWrap,
3229 w: f32,
3230 head_x: f32,
3231) -> (Vec<RowStart>, f32) {
3232 let mut rows = vec![RowStart { byte: 0, x: 0.0 }];
3233 let Some(run) = buffer.layout_runs().next() else {
3234 return (rows, head_x);
3235 };
3236 let glyphs = run.glyphs;
3237 let blank = |g: &cosmic_text::LayoutGlyph| {
3238 text.get(g.start..g.end)
3239 .is_some_and(|s| s.chars().all(char::is_whitespace))
3240 };
3241 let spaces = wrap == TextWrap::BreakSpaces;
3242 let mut pieces: Vec<(usize, usize)> = Vec::new();
3243 match wrap {
3244 TextWrap::Word | TextWrap::BreakSpaces => {
3245 let mut at = 0usize;
3246 let mut piece = |from: usize, to: usize| {
3247 if !spaces {
3248 pieces.push((from, to));
3249 return;
3250 }
3251 // The trailing whitespace a break follows, a piece a
3252 // character: the break is before each of them as well.
3253 let ink = text[from..to].trim_end_matches(char::is_whitespace).len();
3254 if ink > 0 {
3255 pieces.push((from, from + ink));
3256 }
3257 for (i, c) in text[from + ink..to].char_indices() {
3258 let at = from + ink + i;
3259 pieces.push((at, at + c.len_utf8()));
3260 }
3261 };
3262 for (i, _) in unicode_linebreak::linebreaks(text) {
3263 if i > at {
3264 piece(at, i);
3265 at = i;
3266 }
3267 }
3268 if at < text.len() {
3269 piece(at, text.len());
3270 }
3271 }
3272 TextWrap::Glyph | TextWrap::None => {
3273 pieces.extend(glyphs.iter().map(|g| (g.start, g.end)));
3274 }
3275 }
3276 let mut row_x0 = 0.0f32;
3277 let mut avail = w - head_x;
3278 let mut gi = 0usize;
3279 for (s, e) in pieces {
3280 let g0 = gi;
3281 while gi < glyphs.len() && glyphs[gi].start < e {
3282 gi += 1;
3283 }
3284 if g0 == gi {
3285 continue;
3286 }
3287 let piece = &glyphs[g0..gi];
3288 let x0 = piece[0].x;
3289 let ink_end = piece
3290 .iter()
3291 .rev()
3292 .find(|g| spaces || !blank(g))
3293 .map_or(x0, |g| g.x + g.w);
3294 // A piece at the row's start has nowhere to go — except the
3295 // chunk's first, whose row is the one the previous chunk left off
3296 // on `head_x` in: that row holds something, so the piece can open
3297 // the next (the alpha.22 regression pass; the chunk's first row
3298 // is then empty, which the rest reads by byte).
3299 let opens = x0 > row_x0 || (rows.len() == 1 && head_x > 0.0);
3300 if ink_end - row_x0 > avail && opens {
3301 rows.push(RowStart {
3302 byte: s as u32,
3303 x: x0,
3304 });
3305 row_x0 = x0;
3306 avail = w;
3307 }
3308 if ink_end - row_x0 > avail {
3309 // Wider than a row on its own: by glyph.
3310 for g in piece {
3311 if g.x + g.w - row_x0 > avail && g.x > row_x0 && (spaces || !blank(g)) {
3312 rows.push(RowStart {
3313 byte: g.start as u32,
3314 x: g.x,
3315 });
3316 row_x0 = g.x;
3317 avail = w;
3318 }
3319 }
3320 }
3321 }
3322 let end = if rows.len() == 1 {
3323 head_x + run.line_w
3324 } else {
3325 run.line_w - row_x0
3326 };
3327 (rows, end)
3328}
3329
3330/// Builds the entry's positioned templates if the wrap or the atlas moved
3331/// since they were: the steady state reuses them.
3332fn build_templates(
3333 entry: &mut CachedText,
3334 raster: &mut Raster,
3335 fs: &mut FontSystem,
3336 atlas: &mut GlyphAtlas,
3337) {
3338 {
3339 // Steady state: same wrap, same atlas — reuse positioned templates.
3340 let built_for = (entry.wrap.map(f32::to_bits), atlas.stamp);
3341 if entry.glyphs_built_for != Some(built_for) {
3342 entry.glyphs.clear();
3343 entry.deco.clear();
3344 let lines = line_cap(entry.max_lines);
3345 let decorated = entry.span_deco.iter().any(SpanDeco::any);
3346 // Spans painted apart: a glyph's ink may lie over another's
3347 // place (backlog F162). One colour throughout costs nothing.
3348 let painted = entry.span_deco.windows(2).any(|w| w[0].color != w[1].color);
3349 let starts = if (decorated || painted) && entry.span_deco.len() > 1 {
3350 line_starts(&entry.buffer, &entry.content)
3351 } else {
3352 Vec::new()
3353 };
3354 let mut places = Places::default();
3355 for run in entry.buffer.layout_runs().take(lines) {
3356 // Where this run's templates start: moved together once
3357 // built, when the run is drawn away from where the buffer
3358 // lays it (an initial's, F164).
3359 let (g0, d0) = (entry.glyphs.len(), entry.deco.len());
3360 if decorated || painted {
3361 let base = starts.get(run.line_i).copied().unwrap_or(0);
3362 places.of_line(&run, base, &entry.span_deco, painted);
3363 }
3364 if decorated {
3365 build_decorations(&run, &places.parts, &entry.span_deco, fs, &mut entry.deco);
3366 }
3367 for (i, glyph) in run.glyphs.iter().enumerate() {
3368 let physical = glyph.physical((0.0, 0.0), 1.0);
3369 let Some(slot) = raster_glyph(physical.cache_key, fs, raster, atlas) else {
3370 continue;
3371 };
3372 let template = GlyphTemplate {
3373 x: physical.x as f32 + slot.left as f32,
3374 y: run.line_y.round() + physical.y as f32 - slot.top as f32,
3375 w: slot.w as f32,
3376 h: slot.h as f32,
3377 uv: [slot.x, slot.y, slot.w, slot.h],
3378 kind: glyph_kind(&slot),
3379 color: glyph
3380 .color_opt
3381 .map(|c| Color::rgba8(c.r(), c.g(), c.b(), c.a())),
3382 byte: glyph.start as u32,
3383 };
3384 // A colour bitmap (an emoji) paints its own colours.
3385 if painted && template.kind != QuadKind::GlyphColor {
3386 places.push_painted(
3387 template,
3388 i,
3389 run.glyphs,
3390 &entry.span_deco,
3391 &mut entry.glyphs,
3392 );
3393 } else {
3394 entry.glyphs.push(template);
3395 }
3396 }
3397 let (dx, dy) = entry.shift(run.line_i);
3398 if dx != 0.0 || dy != 0.0 {
3399 for g in &mut entry.glyphs[g0..] {
3400 (g.x, g.y) = (g.x + dx, g.y + dy);
3401 }
3402 for d in &mut entry.deco[d0..] {
3403 (d.x, d.y) = (d.x + dx, d.y + dy);
3404 }
3405 }
3406 // An initial's background is as tall as the lines it
3407 // drops through, as its caret and selection are, and
3408 // meets theirs.
3409 if let Some(i) = entry.initial.as_ref().filter(|_| run.line_i == 0) {
3410 for d in entry.deco[d0..].iter_mut().filter(|d| d.under) {
3411 (d.y, d.h) = (i.rect.y, i.rect.h);
3412 }
3413 }
3414 }
3415 // Emission stops at the first glyph below the clip, so the
3416 // templates go top to bottom: an initial's run comes first and
3417 // reaches down beside the rows after it.
3418 if entry.initial.is_some() {
3419 entry.glyphs.sort_by(|a, b| a.y.total_cmp(&b.y));
3420 }
3421 // Rasterizing may have extended the page mid-build, which
3422 // keeps every slot where it was but moves the stamp: keep the
3423 // one we ended on.
3424 entry.glyphs_built_for = Some((entry.wrap.map(f32::to_bits), atlas.stamp));
3425 }
3426 }
3427}
3428
3429/// Emits one cache entry's glyphs and decorations at the physical origin
3430/// (`ox`, `oy`): the steady-state template walk, shared by a text node and
3431/// by each chunk of a long line.
3432#[allow(clippy::too_many_arguments)]
3433fn emit_entry(
3434 entry: &mut CachedText,
3435 ox: f32,
3436 oy: f32,
3437 nudge: Vec2,
3438 color: Color,
3439 clip: Clip,
3440 clip_id: ClipId,
3441 raster: &mut Raster,
3442 fs: &mut FontSystem,
3443 atlas: &mut GlyphAtlas,
3444 out: &mut Vec<Quad>,
3445 joins: &mut Vec<JoinBg>,
3446) {
3447 build_templates(entry, raster, fs, atlas);
3448 // What can show, in the glyphs' own space (ADR 0043).
3449 let vis = clip.visible();
3450 {
3451 let inside = |x: f32, y: f32, w: f32, h: f32| {
3452 oy + y + h >= vis.y
3453 && oy + y <= vis.y + vis.h
3454 && ox + x < vis.x + vis.w
3455 && ox + x + w > vis.x
3456 };
3457 let deco_quad = |d: &DecoTemplate| Quad {
3458 rect: Rect::new(ox + d.x, oy + d.y, d.w, d.h),
3459 color: d.color.unwrap_or(color),
3460 border_color: Color::TRANSPARENT,
3461 radius: [0.0; 4],
3462 border_w: 0.0,
3463 blur: 0.0,
3464 kind: QuadKind::Solid,
3465 clip: clip_id,
3466 uv: [0; 4],
3467 };
3468 // A span's background goes under its glyphs, on whole pixels;
3469 // its lines go over. A rounded one is noted for the join pass,
3470 // which gives it its corners once every text is painted (backlog
3471 // F101).
3472 for d in entry
3473 .deco
3474 .iter()
3475 .filter(|d| d.under && inside(d.x, d.y, d.w, d.h))
3476 {
3477 if d.radius > 0.0 {
3478 joins.push(JoinBg {
3479 quad: out.len() as u32,
3480 radius: d.radius,
3481 outer: clip_id,
3482 own: None,
3483 });
3484 }
3485 out.push(Quad {
3486 rect: on_pixels(ox + d.x, oy + d.y, d.w, d.h, nudge),
3487 ..deco_quad(d)
3488 });
3489 }
3490
3491 // Glyph templates are in layout order; skip everything above the clip
3492 // and stop at the first glyph past it (rows below never come back).
3493 // Horizontally clipped glyphs (an unwrapped line) are dropped too.
3494 out.extend(
3495 entry
3496 .glyphs
3497 .iter()
3498 .filter(|g| {
3499 oy + g.y + g.h >= vis.y && ox + g.x < vis.x + vis.w && ox + g.x + g.w > vis.x
3500 })
3501 .take_while(|g| oy + g.y <= vis.y + vis.h)
3502 .map(|g| Quad {
3503 rect: Rect::new(ox + g.x, oy + g.y, g.w, g.h),
3504 color: g.color.unwrap_or(color),
3505 border_color: Color::TRANSPARENT,
3506 radius: [0.0; 4],
3507 border_w: 0.0,
3508 blur: 0.0,
3509 kind: g.kind,
3510 clip: clip_id,
3511 uv: g.uv,
3512 }),
3513 );
3514 for d in entry
3515 .deco
3516 .iter()
3517 .filter(|d| !d.under && inside(d.x, d.y, d.w, d.h))
3518 {
3519 // A solid line is its rect; a wave or dots are pieces built
3520 // around it (`crate::deco`, backlog K4).
3521 match d.style {
3522 UnderlineStyle::Solid => out.push(deco_quad(d)),
3523 style => crate::deco::push_line(
3524 out,
3525 style,
3526 ox + d.x,
3527 oy + d.y,
3528 d.w,
3529 d.h,
3530 d.color.unwrap_or(color),
3531 clip_id,
3532 ),
3533 }
3534 }
3535 }
3536}
3537
3538/// Whether a text of `len` bytes in `style` is shaped in chunks, by what
3539/// costs nothing to ask: past `LONG_LINE_BYTES`, with no `max_lines` or
3540/// `ellipsis`, since a line budget is a property of the whole. The
3541/// other half is [`has_line_break`] — and [`has_rtl`] for a text long
3542/// only by its `break-spaces` — asked only when the cache has not
3543/// seen the text — a wrapped one breaks its chunks into rows
3544/// ([`LongLine::starts`]), whatever its `wrap`. Plain or rich alike.
3545fn could_be_long(len: usize, style: &TextStyle) -> bool {
3546 (len >= LONG_LINE_BYTES || len > 0 && style.wrap == TextWrap::BreakSpaces)
3547 && style.max_lines == 0
3548 && !style.ellipsis
3549 && style.initial_letter < 2
3550}
3551
3552/// Whether the content breaks lines of its own — a byte scan, since the
3553/// `str` pattern search is per character.
3554fn has_line_break(content: &str) -> bool {
3555 content.bytes().any(|b| b == b'\n' || b == b'\r')
3556}
3557
3558/// Whether the content holds a right-to-left character — a strong one of
3559/// the right-to-left scripts' blocks, or a mark or embedding asking for
3560/// that direction. A text long only by its `break-spaces` and holding one
3561/// is shaped as the run it would otherwise be, wrapping as `word` does
3562/// (as one with line breaks of its own does): the long line breaks rows
3563/// over glyph positions left to right, which a right-to-left run's are
3564/// not, and a Hebrew paragraph that had to wrap would draw one glyph.
3565fn has_rtl(content: &str) -> bool {
3566 !content.is_ascii()
3567 && content.chars().any(|c| {
3568 matches!(c as u32,
3569 0x0590..=0x08FF
3570 | 0x200F | 0x202B | 0x202E | 0x2067
3571 | 0xFB1D..=0xFDFF
3572 | 0xFE70..=0xFEFF
3573 | 0x10800..=0x10FFF
3574 | 0x1E800..=0x1EFFF)
3575 })
3576}
3577
3578/// One span's place on a laid-out line, physical px: a glyph's advance,
3579/// or the share of it one span's graphemes take when the glyph is a
3580/// ligature of characters from more than one span.
3581#[derive(Clone, Copy, Debug)]
3582struct Part {
3583 x0: f32,
3584 x1: f32,
3585 /// The span's index into the entry's `span_deco`.
3586 span: usize,
3587 /// The glyph's index into the run.
3588 glyph: usize,
3589}
3590
3591/// The places of one laid-out line and what painting by them needs,
3592/// kept across the lines of a build so their buffers are allocated once.
3593#[derive(Default)]
3594struct Places {
3595 /// In glyph order, a glyph's own contiguous.
3596 parts: Vec<Part>,
3597 /// Indices into `parts` by `x0`, those with a width alone: what a
3598 /// point of the line is looked up in. Filled for a painted entry.
3599 by_x: Vec<u32>,
3600 /// A glyph's cut columns, reused.
3601 cuts: Vec<u32>,
3602}
3603
3604impl Places {
3605 /// The places `run` gives its spans, in glyph order: a glyph's advance
3606 /// is its span's, and a glyph whose bytes run over more than one span
3607 /// — a ligature of two characters painted apart — shares its advance
3608 /// among its graphemes evenly, as a caret inside it is placed
3609 /// (cosmic-text's `cursor_glyph`), each share its own span's, right
3610 /// to left in a right-to-left glyph. `base` is the byte the run's
3611 /// paragraph starts at in the content, where the spans' starts are.
3612 fn of_line(
3613 &mut self,
3614 run: &cosmic_text::LayoutRun<'_>,
3615 base: usize,
3616 spans: &[SpanDeco],
3617 painted: bool,
3618 ) {
3619 use unicode_segmentation::UnicodeSegmentation;
3620 self.parts.clear();
3621 let last = spans.len().saturating_sub(1);
3622 for (gi, g) in run.glyphs.iter().enumerate() {
3623 // A glyph's metadata is the span its first byte is in.
3624 let span = g.metadata.min(last);
3625 let crosses = spans
3626 .get(span + 1)
3627 .is_some_and(|next| (next.start as usize) < base + g.end);
3628 let cluster = run.text.get(g.start..g.end).filter(|_| crosses);
3629 let Some(cluster) = cluster else {
3630 self.parts.push(Part {
3631 x0: g.x,
3632 x1: g.x + g.w,
3633 span,
3634 glyph: gi,
3635 });
3636 continue;
3637 };
3638 let share = g.w / cluster.graphemes(true).count().max(1) as f32;
3639 for (k, (i, _)) in cluster.grapheme_indices(true).enumerate() {
3640 let byte = base + g.start + i;
3641 let span = spans
3642 .partition_point(|s| s.start as usize <= byte)
3643 .saturating_sub(1);
3644 let (x0, x1) = if g.level.is_rtl() {
3645 let x1 = g.x + g.w - k as f32 * share;
3646 (x1 - share, x1)
3647 } else {
3648 let x0 = g.x + k as f32 * share;
3649 (x0, x0 + share)
3650 };
3651 match self.parts.last_mut() {
3652 Some(p) if p.glyph == gi && p.span == span => {
3653 p.x0 = p.x0.min(x0);
3654 p.x1 = p.x1.max(x1);
3655 }
3656 _ => self.parts.push(Part {
3657 x0,
3658 x1,
3659 span,
3660 glyph: gi,
3661 }),
3662 }
3663 }
3664 }
3665 self.by_x.clear();
3666 if painted {
3667 let parts = &self.parts;
3668 self.by_x.extend(
3669 (0..parts.len() as u32).filter(|&k| parts[k as usize].x1 > parts[k as usize].x0),
3670 );
3671 // A left-to-right line is in order already.
3672 if !self
3673 .by_x
3674 .is_sorted_by(|&a, &b| parts[a as usize].x0 <= parts[b as usize].x0)
3675 {
3676 self.by_x
3677 .sort_by(|&a, &b| parts[a as usize].x0.total_cmp(&parts[b as usize].x0));
3678 }
3679 }
3680 }
3681
3682 /// Pushes glyph `gi`'s template painted by the places its ink lies
3683 /// over (backlog F162). A colour belongs to the places of the
3684 /// characters that carry it — a span's background is drawn over
3685 /// exactly those — and a glyph's ink is not always over its own: a
3686 /// coding font's ligature is often one glyph drawing two characters
3687 /// beside an empty one (`->` in Fira Code, `##` in Berkeley Mono), or
3688 /// one glyph for both whose advance is shared. So the colour of each
3689 /// column of the glyph's ink is the colour of the place under it —
3690 /// its own, or a neighbour's shaped with it in one face, weight, size
3691 /// and style (a run cosmic-text could have joined into a ligature;
3692 /// an italic's lean into the upright text after it keeps its own) —
3693 /// and where those differ the template is cut into slices at whole
3694 /// pixel columns, each the same quad with its rect and its uv
3695 /// narrowed alike. A glyph over one colour stays one template.
3696 fn push_painted(
3697 &mut self,
3698 t: GlyphTemplate,
3699 gi: usize,
3700 glyphs: &[cosmic_text::LayoutGlyph],
3701 spans: &[SpanDeco],
3702 out: &mut Vec<GlyphTemplate>,
3703 ) {
3704 let parts = &self.parts;
3705 let own = {
3706 let lo = parts.partition_point(|p| p.glyph < gi);
3707 let hi = parts.partition_point(|p| p.glyph <= gi);
3708 &parts[lo..hi]
3709 };
3710 if own.is_empty() || t.w < 2.0 {
3711 out.push(t);
3712 return;
3713 }
3714 let g = &glyphs[gi];
3715 let kin = |p: &Part| {
3716 let n = &glyphs[p.glyph];
3717 p.glyph == gi
3718 || n.font_id == g.font_id
3719 && n.font_weight == g.font_weight
3720 && n.font_size == g.font_size
3721 && n.cache_key_flags == g.cache_key_flags
3722 };
3723 let color_of = |p: &Part| spans.get(p.span).and_then(|s| s.color);
3724 // The colour at `x`: the place under it when that is the glyph's
3725 // own or a kin's, else the glyph's own place nearest it.
3726 let at = |x: f32| {
3727 let k = self
3728 .by_x
3729 .partition_point(|&k| parts[k as usize].x0 <= x)
3730 .checked_sub(1)
3731 .map(|k| &parts[self.by_x[k] as usize]);
3732 match k {
3733 Some(p) if x < p.x1 && kin(p) => color_of(p),
3734 _ => {
3735 let near = own
3736 .iter()
3737 .min_by(|a, b| {
3738 let d = |p: &Part| (p.x0 - x).max(x - p.x1).max(0.0);
3739 d(a).total_cmp(&d(b))
3740 })
3741 .expect("not empty");
3742 color_of(near)
3743 }
3744 }
3745 };
3746 let (x0, x1) = (t.x, t.x + t.w);
3747 let w = t.w as u32;
3748 self.cuts.clear();
3749 let from = self
3750 .by_x
3751 .partition_point(|&k| parts[k as usize].x0 < x0)
3752 .saturating_sub(1);
3753 for &k in &self.by_x[from..] {
3754 let p = &parts[k as usize];
3755 if p.x0 >= x1 {
3756 break;
3757 }
3758 for edge in [p.x0, p.x1] {
3759 let c = (edge - x0).round();
3760 if c >= 1.0 && c < t.w {
3761 self.cuts.push(c as u32);
3762 }
3763 }
3764 }
3765 if self.cuts.is_empty() {
3766 out.push(GlyphTemplate {
3767 color: at(x0 + t.w / 2.0),
3768 ..t
3769 });
3770 return;
3771 }
3772 self.cuts.sort_unstable();
3773 self.cuts.dedup();
3774 self.cuts.push(w);
3775 let first = out.len();
3776 let mut a = 0u32;
3777 for &b in &self.cuts {
3778 let color = at(x0 + (a + b) as f32 / 2.0);
3779 let sliced = out.len() > first;
3780 match out.last_mut() {
3781 // The slice before is the same colour: it grows.
3782 Some(prev) if sliced && prev.color == color => {
3783 prev.w += (b - a) as f32;
3784 prev.uv[2] += b - a;
3785 }
3786 _ => out.push(GlyphTemplate {
3787 x: x0 + a as f32,
3788 w: (b - a) as f32,
3789 uv: [t.uv[0] + a, t.uv[1], b - a, t.uv[3]],
3790 color,
3791 ..t
3792 }),
3793 }
3794 a = b;
3795 }
3796 }
3797}
3798
3799/// The decoration rects for one laid-out line: consecutive places of one
3800/// span ([`Places::of_line`]: a glyph's advance, or its graphemes' share
3801/// of a ligature's) become one background rect, one
3802/// underline and one strikethrough, as the span asked. Where the lines go
3803/// is the face's own recommendation — swash's `underline_offset`,
3804/// `strikeout_offset` and `stroke_size`, scaled to the glyph's size — read
3805/// from the run's first glyph, so a fallback glyph in the middle of a
3806/// span does not move the line. Neighbouring spans of one background are
3807/// one rect: an editor's selection is its syntax runs, a span each, and
3808/// a rect each drew a seam between every two of them wherever the join
3809/// fell inside a pixel.
3810fn build_decorations(
3811 run: &cosmic_text::LayoutRun<'_>,
3812 parts: &[Part],
3813 spans: &[SpanDeco],
3814 fs: &mut FontSystem,
3815 out: &mut Vec<DecoTemplate>,
3816) {
3817 // The background rect the last group drew, which the next one of the
3818 // same colour starting where it ends extends.
3819 let mut last_bg: Option<usize> = None;
3820 let mut i = 0;
3821 while i < parts.len() {
3822 let span_no = parts[i].span;
3823 let mut j = i + 1;
3824 while j < parts.len() && parts[j].span == span_no {
3825 j += 1;
3826 }
3827 let deco = spans.get(span_no).copied().unwrap_or_default();
3828 if deco.any() {
3829 let group = &parts[i..j];
3830 let x0 = group.iter().map(|p| p.x0).fold(f32::INFINITY, f32::min);
3831 let x1 = group.iter().map(|p| p.x1).fold(f32::NEG_INFINITY, f32::max);
3832 let head = &run.glyphs[group[0].glyph];
3833 let w = (x1 - x0).max(0.0);
3834 match (deco.bg, last_bg.map(|k| &mut out[k])) {
3835 (Some(bg), Some(prev))
3836 if prev.color == Some(bg)
3837 && prev.radius == deco.bg_radius
3838 && (prev.x + prev.w - x0).abs() < 0.01 =>
3839 {
3840 prev.w = (x1 - prev.x).max(prev.w);
3841 }
3842 (Some(bg), _) => {
3843 // A span shorter than its line (`Span::size`, a line
3844 // a larger span made taller) has a background its own
3845 // height, around its glyphs, not the line's.
3846 let (y, h) = match head.line_height_opt {
3847 Some(lh) if lh < run.line_height - 0.01 => {
3848 let g = head;
3849 let (asc, desc) = fs
3850 .get_font(g.font_id, g.font_weight)
3851 .map(|font| {
3852 let m = font.as_swash().metrics(&[]).scale(g.font_size);
3853 (m.ascent, m.descent)
3854 })
3855 .unwrap_or((0.8 * g.font_size, 0.2 * g.font_size));
3856 let mid = run.line_y - (asc - desc) / 2.0;
3857 (mid - lh / 2.0, lh)
3858 }
3859 _ => (run.line_top, run.line_height),
3860 };
3861 last_bg = Some(out.len());
3862 out.push(DecoTemplate {
3863 x: x0,
3864 y,
3865 w,
3866 h,
3867 color: Some(bg),
3868 under: true,
3869 style: UnderlineStyle::Solid,
3870 radius: deco.bg_radius,
3871 });
3872 }
3873 (None, _) => last_bg = None,
3874 }
3875 if deco.underline || deco.strikethrough {
3876 let first = head;
3877 let metrics = fs
3878 .get_font(first.font_id, first.font_weight)
3879 .map(|font| font.as_swash().metrics(&[]).scale(first.font_size));
3880 // Without the face (it was unloaded between frames), a
3881 // line under the descent and one through the x-height.
3882 let (under_off, strike_off, stroke) = match metrics {
3883 Some(m) => (m.underline_offset, m.strikeout_offset, m.stroke_size),
3884 None => (
3885 -0.1 * first.font_size,
3886 0.3 * first.font_size,
3887 0.06 * first.font_size,
3888 ),
3889 };
3890 let stroke = stroke.max(1.0).round();
3891 let baseline = run.line_y.round();
3892 if deco.underline {
3893 out.push(DecoTemplate {
3894 x: x0,
3895 y: (baseline - under_off).round(),
3896 w,
3897 h: stroke,
3898 color: deco.underline_color.or(deco.color),
3899 under: false,
3900 style: deco.underline_style,
3901 radius: 0.0,
3902 });
3903 }
3904 if deco.strikethrough {
3905 out.push(DecoTemplate {
3906 x: x0,
3907 y: (baseline - strike_off).round(),
3908 w,
3909 h: stroke,
3910 color: deco.color,
3911 under: false,
3912 style: UnderlineStyle::Solid,
3913 radius: 0.0,
3914 });
3915 }
3916 }
3917 } else {
3918 last_bg = None;
3919 }
3920 i = j;
3921 }
3922}
3923
3924impl TextSystem {
3925 /// Records where a live text node was drawn, for the queries below,
3926 /// with the keys above it so a query by a `line` row or a wrapper
3927 /// finds the runs inside. Called beside [`Self::emit`] for the frame's
3928 /// own nodes and not for ghosts, which take no input.
3929 #[allow(clippy::too_many_arguments)]
3930 pub(crate) fn place(
3931 &mut self,
3932 key: Key,
3933 node: usize,
3934 ancestry: &Ancestry,
3935 id: TextId,
3936 origin: Vec2,
3937 scope: Option<Key>,
3938 drawn: bool,
3939 ) {
3940 let cache_key = self.frame[id.0 as usize].cache_key;
3941 self.places.push(TextPlace {
3942 key,
3943 node: node as u32,
3944 ancestors: ancestry.keys,
3945 depth: ancestry.depth.min(PLACE_ANCESTORS) as u8,
3946 none_at: ancestry.none_at.map_or(u8::MAX, |n| n as u8),
3947 cache_key,
3948 origin,
3949 scope,
3950 drawn,
3951 });
3952 }
3953
3954 /// How many runs this frame has placed so far.
3955 pub(crate) fn places_len(&self) -> usize {
3956 self.places.len()
3957 }
3958
3959 /// Puts the runs placed since `from` back in tree order. Emission
3960 /// places them as it paints, and an `over_siblings` subtree paints after its
3961 /// siblings; a selection reads them in reading order, which is the
3962 /// tree's (`scope_runs`). Stable, so one node's runs keep theirs.
3963 pub(crate) fn order_places(&mut self, from: usize) {
3964 if let Some(tail) = self.places.get_mut(from..) {
3965 tail.sort_by_key(|p| p.node);
3966 }
3967 }
3968
3969 /// The runs `key` names, in tree order, each with its entry and the
3970 /// byte offset its content starts at in the concatenation — from the
3971 /// frame that finished (`prev`) or the one being emitted. A long line
3972 /// is a run among them: every `break-spaces` text is one, and a `line`
3973 /// row of several would otherwise answer with the last one's bytes alone.
3974 fn runs_of(&self, key: Key, prev: bool) -> Vec<(&TextPlace, &Entry, usize)> {
3975 let list = if prev {
3976 self.places.prev()
3977 } else {
3978 &self.places
3979 };
3980 let mut base = 0usize;
3981 let mut out = Vec::new();
3982 for place in list.iter().filter(|p| p.drawn && p.answers_to(key)) {
3983 let Some(entry) = self.entries.get(&place.cache_key) else {
3984 continue;
3985 };
3986 out.push((place, entry, base));
3987 base += entry.content().len();
3988 }
3989 out
3990 }
3991
3992 /// How many bytes of content this frame's text `id` holds — what a
3993 /// selection range over it is measured against.
3994 pub(crate) fn content_len(&self, id: TextId) -> usize {
3995 let key = self.frame[id.0 as usize].cache_key;
3996 self.entries.get(&key).map_or(0, |e| e.content().len())
3997 }
3998
3999 /// Every run inside the selection scope `scope`, in emission order —
4000 /// which is tree order, which is reading order — each with the byte
4001 /// offset its content starts at in the scope's concatenation.
4002 ///
4003 /// Undrawn runs are included: a scoped run the frame culled recorded
4004 /// a place precisely so a selection can reach past the viewport, and
4005 /// leaving it out here would renumber everything after it the moment
4006 /// it scrolled away.
4007 pub(crate) fn scope_runs(&self, scope: Key, prev: bool) -> Vec<ScopeRun<'_>> {
4008 let list = if prev {
4009 self.places.prev()
4010 } else {
4011 &self.places
4012 };
4013 let mut base = 0usize;
4014 let mut out = Vec::new();
4015 for place in list.iter().filter(|p| p.scope == Some(scope)) {
4016 let Some(text) = self.entries.get(&place.cache_key) else {
4017 continue;
4018 };
4019 let len = text.content().len();
4020 out.push(ScopeRun { place, text, base });
4021 base += len;
4022 }
4023 out
4024 }
4025
4026 /// Where `point` (logical viewport px) lands in a selection scope, as
4027 /// the address a selection endpoint is made of: the node whose run it
4028 /// landed in and the byte offset inside *that node's* text. Nearest
4029 /// drawn run wins, vertically first, exactly as [`Self::hit_at`]
4030 /// resolves a point inside one node — an undrawn run is not under any
4031 /// pointer, so it is not a candidate.
4032 pub(crate) fn scope_hit(&self, scope: Key, point: Vec2, prev: bool) -> Option<(Key, usize)> {
4033 let runs = self.scope_runs(scope, prev);
4034 let px = point.x * self.scale;
4035 let py = point.y * self.scale;
4036 let gap = |r: &Rect| {
4037 let dy = (r.y - py).max(py - (r.y + r.h)).max(0.0);
4038 let dx = (r.x - px).max(px - (r.x + r.w)).max(0.0);
4039 (dy, dx)
4040 };
4041 let run = runs.iter().filter(|r| r.place.drawn).min_by(|a, b| {
4042 let ga = gap(&self.scope_box(a));
4043 let gb = gap(&self.scope_box(b));
4044 ga.partial_cmp(&gb).unwrap_or(std::cmp::Ordering::Equal)
4045 })?;
4046 let byte = self.hit_in_run(run, point)?;
4047 Some((run.place.key, byte))
4048 }
4049
4050 /// A scoped run's laid-out box, physical px in viewport space.
4051 fn scope_box(&self, run: &ScopeRun<'_>) -> Rect {
4052 self.entry_box(run.place, run.text)
4053 }
4054
4055 /// A run's laid-out box, physical px in viewport space, whichever way
4056 /// it was shaped. A wrapped long line is as wide as its rows were
4057 /// broken to — its unwrapped width would claim the text beside it in a
4058 /// selection scope.
4059 fn entry_box(&self, place: &TextPlace, entry: &Entry) -> Rect {
4060 match entry {
4061 Entry::Run(e) => self.physical_box(place, e),
4062 Entry::Long(l) => {
4063 let (ox, oy) = self.physical_origin(place);
4064 let w = l.wrap_w.unwrap_or_else(|| l.width());
4065 Rect::new(ox, oy, w, l.line_h * l.rows() as f32)
4066 }
4067 }
4068 }
4069
4070 /// The tops of a run's visual rows, physical px in viewport space.
4071 fn row_tops(&self, place: &TextPlace, entry: &Entry) -> Vec<f32> {
4072 let (_, oy) = self.physical_origin(place);
4073 match entry {
4074 // An initial is beside the rows, not one of them.
4075 Entry::Run(e) => e
4076 .buffer
4077 .layout_runs()
4078 .skip(e.initial.is_some() as usize)
4079 .map(|r| oy + r.line_top + e.shift(r.line_i).1)
4080 .collect(),
4081 Entry::Long(l) => (0..l.rows()).map(|r| oy + r as f32 * l.line_h).collect(),
4082 }
4083 }
4084
4085 /// Where `point` lands inside one scoped run, as a byte offset into
4086 /// that run's own content.
4087 fn hit_in_run(&self, run: &ScopeRun<'_>, point: Vec2) -> Option<usize> {
4088 match run.text {
4089 Entry::Long(l) => Some(self.long_hit(run.place, l, point)?.byte),
4090 Entry::Run(e) => {
4091 let (ox, oy) = self.physical_origin(run.place);
4092 let cursor = e.hit(point.x * self.scale - ox, point.y * self.scale - oy)?;
4093 Some(e.byte_of(cursor))
4094 }
4095 }
4096 }
4097
4098 /// The word around `byte` in one node's own text, as a byte range in
4099 /// that node — what a double click (and a force click) selects.
4100 /// Words are runs of alphanumerics and underscores; anything else is
4101 /// selected as the run of like characters around it, so a double
4102 /// click in whitespace takes the whitespace and one in `->` takes
4103 /// both arrows. `None` when the node holds no text at all.
4104 pub(crate) fn word_at(
4105 &self,
4106 scope: Key,
4107 node: Key,
4108 byte: usize,
4109 prev: bool,
4110 ) -> Option<(usize, usize)> {
4111 let run = self
4112 .scope_runs(scope, prev)
4113 .into_iter()
4114 .find(|r| r.place.key == node)?;
4115 let content = run.text.content();
4116 Some(word_range(content, byte))
4117 }
4118
4119 /// A selection endpoint's address — the node and a byte inside its
4120 /// text — as an offset in the scope's concatenation, which is what
4121 /// orders two endpoints against each other. `None` when that node is
4122 /// not (or no longer) in the scope: an address the frame cannot
4123 /// resolve resolves to nothing rather than to whatever took its place.
4124 pub(crate) fn scope_offset(
4125 &self,
4126 scope: Key,
4127 node: Key,
4128 byte: usize,
4129 prev: bool,
4130 ) -> Option<usize> {
4131 let run = self
4132 .scope_runs(scope, prev)
4133 .into_iter()
4134 .find(|r| r.place.key == node)?;
4135 Some(run.base + byte.min(run.text.content().len()))
4136 }
4137
4138 /// The selection `from..to` as HTML, carrying the styling that is
4139 /// *the text's* rather than the theme's: bold, italic, and a span's
4140 /// own colour where one was declared.
4141 ///
4142 /// What it deliberately leaves behind is the node's colour. A
4143 /// paragraph drawn light grey on a dark card is grey because of the
4144 /// app's theme, not because the words are grey; pasting it into a
4145 /// white document as grey-on-white is how "copy with formatting"
4146 /// earns its reputation. A `rich_text` span that declared a colour is
4147 /// the other case — that colour is authored, the way a highlighted
4148 /// keyword is — and it travels.
4149 ///
4150 /// Runs are joined the way [`Self::scope_slice`] joins them: a `<br>`
4151 /// where the plain text gets a newline.
4152 pub(crate) fn scope_html(&self, scope: Key, from: usize, to: usize, prev: bool) -> String {
4153 let (from, to) = (from.min(to), from.max(to));
4154 let mut out = String::new();
4155 let mut prev_run: Option<&ScopeRun<'_>> = None;
4156 for run in &self.scope_runs(scope, prev) {
4157 let (start, end) = run.span();
4158 if end <= from || start >= to {
4159 continue;
4160 }
4161 if let Some(p) = prev_run
4162 && (p.place.origin.y - run.place.origin.y).abs() > f32::EPSILON
4163 {
4164 out.push_str("<br>");
4165 }
4166 let content = run.text.content();
4167 let lo = floor_boundary(content, from.saturating_sub(start));
4168 let hi = floor_boundary(content, (to - start).min(content.len()));
4169 match run.text {
4170 Entry::Long(line) => self.long_html(line, lo, hi, &mut out),
4171 Entry::Run(entry) => html_of_run(entry, lo, hi, &mut out),
4172 }
4173 prev_run = Some(run);
4174 }
4175 out
4176 }
4177
4178 /// `html_of_run` for `lo..hi` of a long line: each chunk the range
4179 /// touches through its own shaped run, so a rich line's spans copy as
4180 /// a short rich text's do — every `break-spaces` text is a long line. A chunk that never
4181 /// showed, or whose run the cache dropped, is plain escaped text: its
4182 /// spans are the line's, but shaping it is not a copy's to do.
4183 fn long_html(&self, line: &LongLine, lo: usize, hi: usize, out: &mut String) {
4184 for c in &line.chunks {
4185 let (a, b) = (lo.max(c.start), hi.min(c.end));
4186 if a >= b {
4187 continue;
4188 }
4189 match c.width.and_then(|_| self.run(c.key)) {
4190 Some(e) => html_of_run(e, a - c.start, b - c.start, out),
4191 None => escape_into(&line.content[a..b], out),
4192 }
4193 }
4194 }
4195
4196 /// Where a platform panel about the selection `from..to` should point:
4197 /// the **baseline origin of its first line**, logical viewport px.
4198 ///
4199 /// Not the box's corner, and not the union's bottom. macOS's
4200 /// `showDefinitionForAttributedString:atPoint:` takes the point the
4201 /// string's own baseline starts at and draws the term back over the
4202 /// text there — so a box's bottom puts the term a line low, and a
4203 /// multi-run selection's union puts it at the bottom of the last line
4204 /// while the panel shows the first.
4205 pub(crate) fn scope_selection_anchor(
4206 &self,
4207 scope: Key,
4208 from: usize,
4209 to: usize,
4210 prev: bool,
4211 ) -> Option<Vec2> {
4212 let (from, to) = (from.min(to), from.max(to));
4213 for run in self.scope_runs(scope, prev) {
4214 let (start, end) = run.span();
4215 if !run.place.drawn || end <= from || start >= to {
4216 continue;
4217 }
4218 let content = run.text.content();
4219 let lo = floor_boundary(content, from.saturating_sub(start));
4220 let hi = floor_boundary(content, (to - start).min(content.len()));
4221 let (ox, oy) = self.physical_origin(run.place);
4222 match run.text {
4223 Entry::Run(e) => {
4224 if let Some((x, base)) = Self::run_anchor(e, lo, hi) {
4225 return Some(Vec2::new((ox + x) / self.scale, (oy + base) / self.scale));
4226 }
4227 }
4228 Entry::Long(l) => {
4229 if let Some((x, y)) = self.long_caret_local(l, lo) {
4230 return Some(Vec2::new(
4231 (ox + x) / self.scale,
4232 (oy + y + self.long_baseline(l)) / self.scale,
4233 ));
4234 }
4235 }
4236 }
4237 }
4238 None
4239 }
4240
4241 /// The x and baseline of the first row of `lo..hi` inside one run,
4242 /// physical px from the run's origin.
4243 fn run_anchor(entry: &CachedText, lo: usize, hi: usize) -> Option<(f32, f32)> {
4244 let (a, b) = (entry.cursor_of(lo), entry.cursor_of(hi));
4245 for run in entry.buffer.layout_runs() {
4246 if run.line_i < a.line || run.line_i > b.line {
4247 continue;
4248 }
4249 if let Some((x, _)) = run.highlight(a, b).next() {
4250 let (dx, dy) = entry.shift(run.line_i);
4251 return Some((x + dx, run.line_y + dy));
4252 }
4253 }
4254 None
4255 }
4256
4257 /// The box the selection `from..to` occupies in `scope`, logical
4258 /// viewport px: the union of the drawn runs it touches, clipped to
4259 /// the part of each run that is actually selected in x only where the
4260 /// run holds both ends. Rough on purpose — it anchors a platform
4261 /// panel, and a panel wants the block of text, not its outline.
4262 pub(crate) fn scope_selection_rect(
4263 &self,
4264 scope: Key,
4265 from: usize,
4266 to: usize,
4267 prev: bool,
4268 ) -> Option<Rect> {
4269 let (from, to) = (from.min(to), from.max(to));
4270 let mut out: Option<Rect> = None;
4271 for run in self.scope_runs(scope, prev) {
4272 let (start, end) = run.span();
4273 if !run.place.drawn || end <= from || start >= to {
4274 continue;
4275 }
4276 let content = run.text.content();
4277 let lo = floor_boundary(content, from.saturating_sub(start));
4278 let hi = floor_boundary(content, (to - start).min(content.len()));
4279 // The rects the *painter* would draw, not the run's box: a
4280 // paragraph that wraps is one run four rows tall, and its box
4281 // is four rows tall with it. Anchoring a panel to that puts it
4282 // under the whole paragraph instead of under the word, which
4283 // is what a reader sees as the popover pointing at nothing.
4284 let (ox, oy) = self.physical_origin(run.place);
4285 let rects = match run.text {
4286 Entry::Long(l) => self.long_highlight(l, lo, hi),
4287 Entry::Run(e) => Self::run_highlight(e, lo, hi),
4288 };
4289 for r in rects {
4290 let r = Rect::new(
4291 (ox + r.x) / self.scale,
4292 (oy + r.y) / self.scale,
4293 r.w / self.scale,
4294 r.h / self.scale,
4295 );
4296 out = Some(match out {
4297 None => r,
4298 Some(o) => o.union(&r),
4299 });
4300 }
4301 }
4302 out
4303 }
4304
4305 /// The scope's text between two offsets in its concatenation, with a
4306 /// newline between two runs that were laid out on different lines and
4307 /// nothing between two that share one. The exact length is known before a byte
4308 /// is copied, so this reserves once and grows never.
4309 pub(crate) fn scope_slice(&self, scope: Key, from: usize, to: usize, prev: bool) -> String {
4310 let (from, to) = (from.min(to), from.max(to));
4311 let runs = self.scope_runs(scope, prev);
4312 let mut out = String::new();
4313 let mut reserved = false;
4314 let mut prev_run: Option<&ScopeRun<'_>> = None;
4315 for run in &runs {
4316 let (start, end) = run.span();
4317 if end <= from || start >= to {
4318 continue;
4319 }
4320 if !reserved {
4321 out.reserve(to - from + runs.len());
4322 reserved = true;
4323 }
4324 if let Some(p) = prev_run
4325 && (p.place.origin.y - run.place.origin.y).abs() > f32::EPSILON
4326 {
4327 out.push('\n');
4328 }
4329 let content = run.text.content();
4330 let lo = from.saturating_sub(start).min(content.len());
4331 let hi = (to - start).min(content.len());
4332 out.push_str(&content[floor_boundary(content, lo)..floor_boundary(content, hi)]);
4333 prev_run = Some(run);
4334 }
4335 out
4336 }
4337
4338 /// `hit_at` for a long line: the chunk under the point answers through
4339 /// its shaped entry, an unshaped one (never on screen) by the mean
4340 /// advance.
4341 fn long_hit(&self, place: &TextPlace, line: &LongLine, point: Vec2) -> Option<TextHit> {
4342 let (ox, oy) = self.physical_origin(place);
4343 let px = point.x * self.scale - ox;
4344 let py = point.y * self.scale - oy;
4345 if line.chunks.is_empty() {
4346 return Some(TextHit { byte: 0, line: 0 });
4347 }
4348 if let Some(w) = line.wrap_w {
4349 return self.long_hit_wrapped(line, w, px, py);
4350 }
4351 if px >= line.width() {
4352 return Some(TextHit {
4353 byte: line.content.len(),
4354 line: 0,
4355 });
4356 }
4357 let i = line.chunk_at(px.max(0.0));
4358 let c = &line.chunks[i];
4359 let local = px - line.prefix[i];
4360 let byte = match self.run(c.key) {
4361 Some(_) if let Some(b) = line.placed_tab_hit(i, local) => b,
4362 Some(e) if c.width.is_some() => {
4363 let cursor = e.buffer.hit(local, py.clamp(0.0, line.line_h - 0.01))?;
4364 c.start + cursor.index.min(c.end - c.start)
4365 }
4366 _ => {
4367 let est = (local / line.avg.max(f32::EPSILON)).round() as usize;
4368 let mut b = (c.start + est).min(c.end);
4369 while !line.content.is_char_boundary(b) {
4370 b -= 1;
4371 }
4372 b
4373 }
4374 };
4375 Some(TextHit { byte, line: 0 })
4376 }
4377
4378 /// `long_hit` while the line is wrapped: the row under the point, the
4379 /// chunk on that row the point is over, and the chunk's unwrapped run
4380 /// asked at the x the row was shifted from.
4381 fn long_hit_wrapped(&self, line: &LongLine, w: f32, px: f32, py: f32) -> Option<TextHit> {
4382 let row = ((py / line.line_h).floor().max(0.0) as u32).min(line.rows() - 1);
4383 let Some((a, b)) = line.chunks_on_rows(row, row) else {
4384 return Some(TextHit {
4385 byte: line.content.len(),
4386 line: row,
4387 });
4388 };
4389 // The first chunk whose span on this row reaches past the point,
4390 // else the last one on it.
4391 let i = (a..=b)
4392 .find(|&j| {
4393 let (r1, end_x) = line.starts[j + 1];
4394 px < if r1 == row { end_x } else { w }
4395 })
4396 .unwrap_or(b);
4397 let c = &line.chunks[i];
4398 let (row0, head_x) = line.starts[i];
4399 let r = (row - row0) as usize;
4400 let byte = match self.run(c.key) {
4401 Some(e) if r < c.rows.len() => {
4402 let rs = c.rows[r];
4403 let local_x = px - if r == 0 { head_x } else { 0.0 } + rs.x;
4404 let lo = rs.byte as usize;
4405 let hi = c
4406 .rows
4407 .get(r + 1)
4408 .map_or(c.end - c.start, |n| n.byte as usize);
4409 // A tab the line places, when it is on this row.
4410 let tab_here = (lo..hi).contains(&(c.end - c.start - 1));
4411 match line.placed_tab_hit(i, local_x) {
4412 Some(b) if tab_here => b,
4413 _ => {
4414 let cursor = e.buffer.hit(local_x, line.line_h / 2.0)?;
4415 c.start + cursor.index.clamp(lo, hi)
4416 }
4417 }
4418 }
4419 _ => {
4420 // The inverse of `long_caret`'s estimate: rows back into
4421 // one linear run from the chunk's start.
4422 let linear = r as f32 * w + px - head_x;
4423 let est = (linear / line.avg.max(f32::EPSILON)).round() as usize;
4424 let mut b = (c.start + est).min(c.end);
4425 while !line.content.is_char_boundary(b) {
4426 b -= 1;
4427 }
4428 b
4429 }
4430 };
4431 // The byte a row breaks at is the next row's first, and its caret
4432 // is drawn there: a point past this row's end answers the byte
4433 // before the row's last character, which is on this row, as a
4434 // `word` run's hanging space does (the alpha.22 regression pass).
4435 let next_row = self
4436 .long_caret_local(line, byte)
4437 .is_some_and(|(_, y)| y > (row as f32 + 0.5) * line.line_h);
4438 let byte = if next_row && byte > 0 {
4439 floor_boundary(&line.content, byte - 1)
4440 } else {
4441 byte
4442 };
4443 Some(TextHit { byte, line: row })
4444 }
4445
4446 /// `caret_at` for a long line, exact in a shaped chunk and by the mean
4447 /// advance in one that never showed.
4448 fn long_caret(&self, place: &TextPlace, line: &LongLine, byte: usize) -> Option<Rect> {
4449 let (ox, oy) = self.physical_origin(place);
4450 let scale = self.scale;
4451 let (x, y) = self.long_caret_local(line, byte)?;
4452 Some(Rect::new(
4453 (ox + x) / scale,
4454 (oy + y) / scale,
4455 0.0,
4456 line.line_h / scale,
4457 ))
4458 }
4459
4460 /// A long line's first baseline, physical px below its top: its first
4461 /// chunk's, shaped when the line was built, rounded as its glyphs are
4462 /// drawn at it — a fifth of the row above the row's foot only while
4463 /// that chunk is not shaped. On that estimate alone a `break-spaces`
4464 /// text in a baseline row would sit a pixel off a `word` one.
4465 fn long_baseline(&self, line: &LongLine) -> f32 {
4466 line.chunks
4467 .first()
4468 .and_then(|c| self.run(c.key))
4469 .and_then(|e| e.buffer.layout_runs().next())
4470 .map_or(line.line_h * 0.8, |r| r.line_y.round())
4471 }
4472
4473 /// The caret for `byte`, in physical px from the long line's own
4474 /// origin: its x on its row, and that row's top. What `long_caret`
4475 /// places in the viewport and what a selection highlight measures
4476 /// between.
4477 fn long_caret_local(&self, line: &LongLine, byte: usize) -> Option<(f32, f32)> {
4478 let byte = byte.min(line.content.len());
4479 if let Some(w) = line.wrap_w
4480 && !line.chunks.is_empty()
4481 {
4482 let i = line.chunk_of_byte(byte);
4483 let c = &line.chunks[i];
4484 let local = byte - c.start;
4485 let (row0, head_x) = line.starts[i];
4486 let (r, x) = match self.run(c.key) {
4487 Some(e) if !c.rows.is_empty() => {
4488 let r = c.rows.partition_point(|rs| rs.byte as usize <= local) - 1;
4489 let run = e.buffer.layout_runs().next()?;
4490 let cx = caret_x(&run, local.min(c.end - c.start)) + line.end_shift(i, local);
4491 (r, cx - c.rows[r].x + if r == 0 { head_x } else { 0.0 })
4492 }
4493 _ => {
4494 let linear = head_x + local as f32 * line.avg;
4495 let r = (linear / w).floor();
4496 (r as usize, linear - r * w)
4497 }
4498 };
4499 let y = (row0 as usize + r) as f32 * line.line_h;
4500 return Some((x, y));
4501 }
4502 let x = if line.chunks.is_empty() {
4503 0.0
4504 } else {
4505 let i = line.chunk_of_byte(byte);
4506 let c = &line.chunks[i];
4507 let local = byte - c.start;
4508 match self.run(c.key) {
4509 Some(e) if c.width.is_some() => {
4510 let run = e.buffer.layout_runs().next()?;
4511 let x = caret_x(&run, local.min(c.end - c.start));
4512 line.prefix[i] + x + line.end_shift(i, local)
4513 }
4514 _ => line.prefix[i] + local as f32 * line.avg,
4515 }
4516 };
4517 Some((x, 0.0))
4518 }
4519
4520 /// Selection rects for the byte range `from..to` of a long line, in
4521 /// physical px from the line's origin. One rect per row it covers:
4522 /// the first from the start caret to the row's end, whole rows
4523 /// between, and the last from the row's start to the end caret — the
4524 /// shape any selection over wrapped text has.
4525 fn long_highlight(&self, line: &LongLine, from: usize, to: usize) -> Vec<Rect> {
4526 let Some((x0, y0)) = self.long_caret_local(line, from) else {
4527 return Vec::new();
4528 };
4529 let Some((x1, y1)) = self.long_caret_local(line, to) else {
4530 return Vec::new();
4531 };
4532 let h = line.line_h;
4533 if (y1 - y0).abs() < f32::EPSILON {
4534 return vec![Rect::new(x0, y0, (x1 - x0).max(0.0), h)];
4535 }
4536 // Several rows: only a wrapped line has any, so it has a width.
4537 let w = line.wrap_w.unwrap_or_else(|| line.width());
4538 let mut out = vec![Rect::new(x0, y0, (w - x0).max(0.0), h)];
4539 let mut y = y0 + h;
4540 while y < y1 - h / 2.0 {
4541 out.push(Rect::new(0.0, y, w, h));
4542 y += h;
4543 }
4544 out.push(Rect::new(0.0, y1, x1.max(0.0), h));
4545 out
4546 }
4547
4548 /// Selection rects for the byte range `from..to` of an ordinary
4549 /// shaped run, in physical px from the run's origin — cosmic-text's
4550 /// own `highlight`, which is what the editor paints with
4551 /// (`crates/kui-core/src/edit.rs`), so a selection over a label and
4552 /// one over a field are the same shape.
4553 fn run_highlight(entry: &CachedText, from: usize, to: usize) -> Vec<Rect> {
4554 let (a, b) = (entry.cursor_of(from), entry.cursor_of(to));
4555 let mut out = Vec::new();
4556 for run in entry.buffer.layout_runs() {
4557 if run.line_i < a.line || run.line_i > b.line {
4558 continue;
4559 }
4560 // The run's own height: a line holding a larger span is taller
4561 // than the paragraph's metric. An initial's is the lines it
4562 // drops through.
4563 let (dx, dy) = entry.shift(run.line_i);
4564 let (top, h) = match &entry.initial {
4565 Some(i) if run.line_i == 0 => (i.rect.y, i.rect.h),
4566 _ => (run.line_top + dy, run.line_height),
4567 };
4568 let mut any = false;
4569 for (x, w) in run.highlight(a, b) {
4570 any = true;
4571 out.push(Rect::new(x + dx, top, w.max(2.0), h));
4572 }
4573 // A selected newline on an empty line keeps the highlight
4574 // continuous, the way the editor's does.
4575 if !any && run.glyphs.is_empty() && b.line > run.line_i {
4576 out.push(Rect::new(dx, top, 2.0, h));
4577 }
4578 }
4579 out
4580 }
4581
4582 /// The physical origin `emit` drew a place at: what a point is
4583 /// measured from and a rect is measured to.
4584 fn physical_origin(&self, place: &TextPlace) -> (f32, f32) {
4585 (
4586 crate::geom::snap_px(place.origin.x * self.scale),
4587 crate::geom::snap_px(place.origin.y * self.scale),
4588 )
4589 }
4590
4591 /// A run's laid-out box, physical px in viewport space.
4592 fn physical_box(&self, place: &TextPlace, entry: &CachedText) -> Rect {
4593 let (ox, oy) = self.physical_origin(place);
4594 let (size, _) = entry.measure();
4595 Rect::new(ox, oy, size.w, size.h)
4596 }
4597
4598 /// Where `point` (logical viewport px) lands in the text `key` drew;
4599 /// see `Core::text_hit`. With several runs, the run under the point,
4600 /// else the nearest one on the point's line, else the nearest line.
4601 pub(crate) fn hit_at(&self, key: Key, point: Vec2, prev: bool) -> Option<TextHit> {
4602 let runs = self.runs_of(key, prev);
4603 if runs.is_empty() {
4604 return None;
4605 }
4606 let px = point.x * self.scale;
4607 let py = point.y * self.scale;
4608 // Distance from a run's box: vertical first, so a point on a line
4609 // of runs picks among that line, then horizontal.
4610 let gap = |r: &Rect| {
4611 let dy = (r.y - py).max(py - (r.y + r.h)).max(0.0);
4612 let dx = (r.x - px).max(px - (r.x + r.w)).max(0.0);
4613 (dy, dx)
4614 };
4615 let (place, entry, base) = runs
4616 .iter()
4617 .min_by(|a, b| {
4618 let ga = gap(&self.entry_box(a.0, a.1));
4619 let gb = gap(&self.entry_box(b.0, b.1));
4620 ga.partial_cmp(&gb).unwrap_or(std::cmp::Ordering::Equal)
4621 })
4622 .copied()?;
4623 let (ox, oy) = self.physical_origin(place);
4624 let (byte, hit_top) = match entry {
4625 Entry::Long(line) => {
4626 let hit = self.long_hit(place, line, point)?;
4627 (hit.byte, oy + hit.line as f32 * line.line_h)
4628 }
4629 Entry::Run(entry) => {
4630 // Into the run's box vertically: the nearest run was
4631 // chosen for a point outside every run, and cosmic-text
4632 // answers a point above its first row with byte 0 whatever
4633 // the x — so a press in the padding above a line's text
4634 // placed the caret at its start (backlog C34). Clamped, it
4635 // hits the nearest row at that x.
4636 let bx = self.physical_box(place, entry);
4637 let py = py.clamp(bx.y, (bx.y + bx.h - 0.01).max(bx.y));
4638 let cursor = entry.hit(px - ox, py - oy)?;
4639 let row =
4640 visual_line(&entry.buffer, cursor.line, cursor.index).map_or(0, |(l, _)| l);
4641 // An initial is on the first row.
4642 let top = entry.buffer.layout_runs().nth(row).map_or(0.0, |r| {
4643 match entry.initial.is_some() && r.line_i == 0 {
4644 true => 0.0,
4645 false => r.line_top + entry.shift(r.line_i).1,
4646 }
4647 });
4648 (entry.byte_of(cursor), oy + top)
4649 }
4650 };
4651 // The visual row within the node (AR30): the row the hit landed
4652 // on, placed among every row of every run the key covers by its
4653 // top edge, so runs side by side share a row and runs stacked
4654 // count in turn.
4655 let mut tops: Vec<f32> = runs
4656 .iter()
4657 .flat_map(|(p, e, _)| self.row_tops(p, e))
4658 .collect();
4659 tops.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
4660 tops.dedup_by(|a, b| (*a - *b).abs() < 0.5);
4661 let line = tops.iter().filter(|t| **t < hit_top - 0.5).count();
4662 Some(TextHit {
4663 byte: base + byte,
4664 line: line as u32,
4665 })
4666 }
4667
4668 /// The caret rect for `byte` in the text `key` drew; see
4669 /// `Core::caret_rect`. A byte on the seam between two runs is the
4670 /// start of the later one, except at the very end.
4671 pub(crate) fn caret_at(&self, key: Key, byte: usize, prev: bool) -> Option<Rect> {
4672 let runs = self.runs_of(key, prev);
4673 let total = runs.last().map(|(_, e, base)| base + e.content().len())?;
4674 let byte = byte.min(total);
4675 let (place, entry, base) = runs
4676 .iter()
4677 .find(|(_, e, base)| byte < base + e.content().len())
4678 .or_else(|| runs.last())
4679 .copied()?;
4680 let byte = (byte - base).min(entry.content().len());
4681 let entry = match entry {
4682 Entry::Long(line) => return self.long_caret(place, line, byte),
4683 Entry::Run(entry) => entry,
4684 };
4685 let (ox, oy) = self.physical_origin(place);
4686 let cursor = entry.cursor_of(byte);
4687 let (_, run_no) = visual_line(&entry.buffer, cursor.line, cursor.index)?;
4688 let run = entry.buffer.layout_runs().nth(run_no)?;
4689 let (dx, dy) = entry.shift(run.line_i);
4690 let x = caret_x(&run, cursor.index) + dx;
4691 // Beside an initial the caret is as tall as the letter.
4692 let (top, h) = match &entry.initial {
4693 Some(i) if run.line_i == 0 => (i.rect.y, i.rect.h),
4694 _ => (run.line_top + dy, run.line_height),
4695 };
4696 let scale = self.scale;
4697 Some(Rect::new(
4698 (ox + x) / scale,
4699 (oy + top) / scale,
4700 0.0,
4701 h / scale,
4702 ))
4703 }
4704}
4705
4706/// The byte offset in `content` at which each of the buffer's paragraphs
4707/// starts. cosmic-text splits on every line ending it knows and keeps the
4708/// ending out of the paragraph's text, so each start is found by matching
4709/// the paragraph back onto the content and skipping the ending after it.
4710/// The word around `byte`: the run of like characters it sits in, where
4711/// "like" is one of three classes — word characters (alphanumeric or
4712/// `_`), whitespace, and everything else. At the very end of the text the
4713/// word before it, so a double click past the last character selects the
4714/// last word rather than nothing.
4715fn word_range(content: &str, byte: usize) -> (usize, usize) {
4716 if content.is_empty() {
4717 return (0, 0);
4718 }
4719 #[derive(PartialEq)]
4720 enum Class {
4721 Word,
4722 Space,
4723 Other,
4724 }
4725 let class = |c: char| {
4726 if c.is_alphanumeric() || c == '_' {
4727 Class::Word
4728 } else if c.is_whitespace() {
4729 Class::Space
4730 } else {
4731 Class::Other
4732 }
4733 };
4734 // Past the end (or on the boundary after the last character), the
4735 // word being pointed at is the one just passed, so step back into it.
4736 let at = floor_boundary(content, byte.min(content.len()));
4737 let at = if at >= content.len() {
4738 content.char_indices().next_back().map_or(0, |(i, _)| i)
4739 } else {
4740 at
4741 };
4742 let Some(here) = content[at..].chars().next().map(class) else {
4743 return (at, at);
4744 };
4745 let mut start = at;
4746 for (i, c) in content[..at].char_indices().rev() {
4747 if class(c) != here {
4748 break;
4749 }
4750 start = i;
4751 }
4752 let mut end = at;
4753 for (i, c) in content[at..].char_indices() {
4754 if class(c) != here {
4755 break;
4756 }
4757 end = at + i + c.len_utf8();
4758 }
4759 (start, end)
4760}
4761
4762/// One shaped run's `lo..hi` as HTML, span by span: cosmic-text keeps the
4763/// attributes the text was shaped with, so the bold in a rich paragraph is
4764/// still readable off the buffer long after the spans that declared it are
4765/// gone.
4766fn html_of_run(entry: &CachedText, lo: usize, hi: usize, out: &mut String) {
4767 let starts = line_starts(&entry.buffer, &entry.content);
4768 for (li, line) in entry.buffer.lines.iter().enumerate() {
4769 let base = starts.get(li).copied().unwrap_or(0);
4770 let text = line.text();
4771 // A line an initial cut from its paragraph goes on unbroken.
4772 if li > 0 && !crate::initial::joined(&entry.buffer, li - 1) {
4773 // A newline inside one node's text is a line break in the
4774 // copy, the same way it is on screen.
4775 if base > lo && base <= hi {
4776 out.push_str("<br>");
4777 }
4778 }
4779 // `spans_iter` lists only the ranges something *changed* — a
4780 // plain paragraph has none at all and reads its line's defaults —
4781 // so this walks the line and asks per character, coalescing runs
4782 // that answer the same. `get_span` falls back to the defaults
4783 // itself, which is exactly the case a rich paragraph's gaps are.
4784 let attrs_list = line.attrs_list();
4785 let mut spans: Vec<(usize, usize, cosmic_text::AttrsOwned)> = Vec::new();
4786 for (i, _) in text.char_indices() {
4787 let a = cosmic_text::AttrsOwned::new(&attrs_list.get_span(i));
4788 match spans.last_mut() {
4789 Some((_, end, prev)) if *prev == a && *end == i => {
4790 *end = i + text[i..].chars().next().map_or(1, char::len_utf8);
4791 }
4792 _ => spans.push((i, i + text[i..].chars().next().map_or(1, char::len_utf8), a)),
4793 }
4794 }
4795 for (start, end, attrs) in &spans {
4796 let (s, e) = (base + start, base + end);
4797 let (s, e) = (s.max(lo), e.min(hi));
4798 if s >= e {
4799 continue;
4800 }
4801 let piece = &entry.content[s..e];
4802 // Heavier than the line's default, which is the family's
4803 // regular (F100): a family whose bold face is its SemiBold
4804 // bolds at 600, and one whose only face is 700 is regular
4805 // there.
4806 let bold = attrs.weight > attrs_list.defaults().weight
4807 || attrs
4808 .cache_key_flags
4809 .contains(crate::weights::SYNTHETIC_BOLD);
4810 let italic = attrs.style != cosmic_text::Style::Normal;
4811 if let Some(c) = attrs.color_opt {
4812 let _ = std::fmt::Write::write_fmt(
4813 out,
4814 format_args!(
4815 "<span style=\"color:#{:02x}{:02x}{:02x}\">",
4816 c.r(),
4817 c.g(),
4818 c.b()
4819 ),
4820 );
4821 }
4822 if bold {
4823 out.push_str("<b>");
4824 }
4825 if italic {
4826 out.push_str("<i>");
4827 }
4828 escape_into(piece, out);
4829 if italic {
4830 out.push_str("</i>");
4831 }
4832 if bold {
4833 out.push_str("</b>");
4834 }
4835 if attrs.color_opt.is_some() {
4836 out.push_str("</span>");
4837 }
4838 }
4839 }
4840}
4841
4842/// HTML-escapes into `out`. The four that matter in element content and
4843/// nothing else: a clipboard flavour is not a document, and over-escaping
4844/// is what turns a copied apostrophe into `'` in someone's email.
4845fn escape_into(s: &str, out: &mut String) {
4846 for ch in s.chars() {
4847 match ch {
4848 '&' => out.push_str("&"),
4849 '<' => out.push_str("<"),
4850 '>' => out.push_str(">"),
4851 _ => out.push(ch),
4852 }
4853 }
4854}
4855
4856/// The nearest char boundary at or below `i`, so a slice taken from a/// The
4857/// nearest char boundary at or below `i`, so a slice taken from a
4858/// selection never splits a character. A selection's ends come from
4859/// shaping and are boundaries already; this is for the arithmetic around
4860/// them (a clamp, a saturating subtraction) that has no such guarantee.
4861fn floor_boundary(s: &str, i: usize) -> usize {
4862 let mut i = i.min(s.len());
4863 while i > 0 && !s.is_char_boundary(i) {
4864 i -= 1;
4865 }
4866 i
4867}
4868
4869fn line_starts(buffer: &Buffer, content: &str) -> Vec<usize> {
4870 paragraph_starts(buffer, content).collect()
4871}
4872
4873/// Where each of `buffer`'s paragraphs starts in `content`, in order.
4874/// cosmic-text splits the text at line endings and keeps the pieces, so
4875/// the ending bytes between two paragraphs are found again in `content`
4876/// — any of the four spellings — and skipped. No allocation: the two
4877/// conversions below walk this once and stop.
4878fn paragraph_starts<'a>(buffer: &'a Buffer, content: &'a str) -> impl Iterator<Item = usize> + 'a {
4879 let mut at = 0usize;
4880 buffer.lines.iter().map(move |line| {
4881 let start = at.min(content.len());
4882 at += line.text().len();
4883 let rest = &content[at.min(content.len())..];
4884 for ending in ["\r\n", "\n\r", "\n", "\r"] {
4885 if rest.starts_with(ending) {
4886 at += ending.len();
4887 break;
4888 }
4889 }
4890 start
4891 })
4892}
4893
4894/// The visual line (0-based over every wrapped line of the buffer) that
4895/// byte `index` of paragraph `line_i` lays out on, and that run's ordinal
4896/// in `layout_runs()` (the same number, kept apart for reading): the run
4897/// whose glyphs cover the byte, else the last run that starts before it —
4898/// a byte with no glyph inside the paragraph is whitespace a `Word` break
4899/// swallowed, which ends the row it broke, and the end of the paragraph
4900/// ends its last — else the paragraph's last run, for a paragraph with no
4901/// glyphs. The paragraph's last run whatever the byte, as it was, put a
4902/// caret on a swallowed space at the end of the whole paragraph.
4903fn visual_line(buffer: &Buffer, line_i: usize, index: usize) -> Option<(usize, usize)> {
4904 let mut last_of_line = None;
4905 let mut before = None;
4906 for (n, run) in buffer.layout_runs().enumerate() {
4907 if run.line_i != line_i {
4908 if last_of_line.is_some() {
4909 break;
4910 }
4911 continue;
4912 }
4913 last_of_line = Some(n);
4914 if run.glyphs.iter().any(|g| g.start <= index && index < g.end) {
4915 return Some((n, n));
4916 }
4917 if run
4918 .glyphs
4919 .iter()
4920 .map(|g| g.start)
4921 .min()
4922 .is_some_and(|s| s <= index)
4923 {
4924 before = Some(n);
4925 }
4926 }
4927 before.or(last_of_line).map(|n| (n, n))
4928}
4929
4930/// Where the caret sits for byte `index` inside `run`, physical px from the
4931/// text origin: the leading edge of the glyph covering it — its right edge
4932/// in a right-to-left run — and past the last glyph at the run's end.
4933fn caret_x(run: &cosmic_text::LayoutRun<'_>, index: usize) -> f32 {
4934 if let Some(g) = run
4935 .glyphs
4936 .iter()
4937 .find(|g| g.start <= index && index < g.end)
4938 {
4939 return if g.level.is_rtl() { g.x + g.w } else { g.x };
4940 }
4941 match run.glyphs.last() {
4942 Some(g) if !g.level.is_rtl() => g.x + g.w,
4943 Some(g) => g.x,
4944 None => 0.0,
4945 }
4946}
4947
4948impl Default for TextSystem {
4949 fn default() -> Self {
4950 Self::new()
4951 }
4952}
4953
4954/// The physical wrap width an entry needs for a logical `max_w`: none when
4955/// the unwrapped text already fits (or no width was given).
4956fn wrap_target(entry: &CachedText, max_w_logical: Option<f32>, scale: f32) -> Option<f32> {
4957 let max_w = max_w_logical?;
4958 let intrinsic_fits = entry.intrinsic.w <= max_w * scale + 0.5;
4959 (!intrinsic_fits).then_some(max_w * scale)
4960}
4961
4962/// Re-lays the entry's buffer out at `target` (physical px) if it is not
4963/// already there.
4964/// Whether a buffer laid out at `current` needs laying out again for
4965/// `target`: a wrap width that moved by half a physical pixel or less is
4966/// the same wrap, which is what keeps a box that jitters by float error
4967/// from reshaping its text every frame. The one rule for the text cache
4968/// and the editors alike.
4969pub(crate) fn wrap_differs(current: Option<f32>, target: Option<f32>) -> bool {
4970 match (current, target) {
4971 (None, None) => false,
4972 (Some(a), Some(b)) => (a - b).abs() > 0.5,
4973 _ => true,
4974 }
4975}
4976
4977fn wrap_entry(entry: &mut CachedText, fs: &mut FontSystem, target: Option<f32>) {
4978 if wrap_differs(entry.wrap, target) {
4979 if let Some(i) = &mut entry.initial {
4980 i.uncut(&mut entry.buffer);
4981 }
4982 entry.buffer.set_size(target, None);
4983 entry.buffer.shape_until_scroll(fs, false);
4984 entry.wrap = target;
4985 if let Some(i) = &mut entry.initial {
4986 i.place(&mut entry.buffer, fs);
4987 }
4988 }
4989}
4990
4991impl CachedText {
4992 /// The byte offset in `content` that `cursor` (a paragraph and an
4993 /// index into it) names, clamped to the content.
4994 fn byte_of(&self, cursor: cosmic_text::Cursor) -> usize {
4995 let start = paragraph_starts(&self.buffer, &self.content)
4996 .nth(cursor.line)
4997 .unwrap_or(0);
4998 (start + cursor.index).min(self.content.len())
4999 }
5000
5001 /// The cursor at `byte`: the paragraph starting at or before it — the
5002 /// last one that does — and the offset inside that paragraph's own
5003 /// text, clamped to it. The inverse of [`Self::byte_of`]; the one
5004 /// place the byte ↔ paragraph arithmetic lives, where three copies
5005 /// used to (`run_anchor`, `run_highlight`, `caret_at`).
5006 fn cursor_of(&self, byte: usize) -> cosmic_text::Cursor {
5007 let mut li = 0;
5008 let mut start = 0;
5009 for (i, s) in paragraph_starts(&self.buffer, &self.content).enumerate() {
5010 if s <= byte {
5011 (li, start) = (i, s);
5012 } else {
5013 break;
5014 }
5015 }
5016 let len = self.buffer.lines.get(li).map_or(0, |l| l.text().len());
5017 cosmic_text::Cursor::new(li, (byte - start).min(len))
5018 }
5019
5020 /// How far the runs of buffer line `line_i` are drawn from where the
5021 /// buffer lays them, physical px: nothing, but for a text with an
5022 /// initial ([`Initial::shift`]).
5023 fn shift(&self, line_i: usize) -> (f32, f32) {
5024 self.initial
5025 .as_ref()
5026 .map_or((0.0, 0.0), |i| i.shift(line_i))
5027 }
5028
5029 /// The buffer's hit test at a point drawn where `shift` draws its
5030 /// runs, physical px from the text's origin.
5031 fn hit(&self, x: f32, y: f32) -> Option<cosmic_text::Cursor> {
5032 match &self.initial {
5033 Some(i) => {
5034 let (x, y) = i.to_buffer(x, y);
5035 self.buffer.hit(x, y)
5036 }
5037 None => self.buffer.hit(x, y),
5038 }
5039 }
5040
5041 /// The text's size and rows as laid out: [`measure_run`].
5042 fn measure(&self) -> (Size, u32) {
5043 measure_run(&self.buffer, self.max_lines, self.initial.as_deref())
5044 }
5045
5046 fn new(
5047 mut buffer: Buffer,
5048 content: String,
5049 style: &TextStyle,
5050 span_deco: Vec<SpanDeco>,
5051 fs: &mut FontSystem,
5052 frame_no: u64,
5053 ) -> Self {
5054 buffer.shape_until_scroll(fs, false);
5055 let max_lines = line_budget(style);
5056 // A drop cap where the text asks for one and can have it: not
5057 // under a line budget, which counts the lines it moves, and not
5058 // right to left.
5059 let gap = buffer.metrics().font_size * INITIAL_GAP;
5060 let initial = (style.initial_letter >= 2 && max_lines == 0 && !has_rtl(&content))
5061 .then(|| Initial::cut(&mut buffer, fs, style.initial_letter, gap))
5062 .flatten()
5063 .map(Box::new);
5064 let (intrinsic, _) = measure_run(&buffer, max_lines, initial.as_deref());
5065 let glyphs: usize = buffer.layout_runs().map(|r| r.glyphs.len()).sum();
5066 Self {
5067 buffer,
5068 bytes: ENTRY_BASE_BYTES + content.len() + ENTRY_GLYPH_BYTES * glyphs,
5069 content,
5070 wrap: None,
5071 intrinsic,
5072 max_lines,
5073 clamp_w: style.wrap == TextWrap::None || style.ellipsis,
5074 last_used: frame_no,
5075 glyphs: Vec::new(),
5076 deco: Vec::new(),
5077 span_deco,
5078 glyphs_built_for: None,
5079 claimed: u64::MAX,
5080 claimed_wrap: None,
5081 breaks_anywhere: style.wrap == TextWrap::Glyph,
5082 min_content: None,
5083 initial,
5084 }
5085 }
5086
5087 /// A copy laid out as this one is, for a second node that draws the
5088 /// same text at another width ([`TextSystem::own_wrap`]): the shaped
5089 /// buffer, not the templates, which the copy builds when drawn.
5090 fn fork(&self, frame_no: u64) -> Self {
5091 Self {
5092 buffer: self.buffer.clone(),
5093 content: self.content.clone(),
5094 wrap: self.wrap,
5095 intrinsic: self.intrinsic,
5096 max_lines: self.max_lines,
5097 clamp_w: self.clamp_w,
5098 last_used: frame_no,
5099 bytes: self.bytes,
5100 glyphs: Vec::new(),
5101 deco: Vec::new(),
5102 span_deco: self.span_deco.clone(),
5103 glyphs_built_for: None,
5104 claimed: u64::MAX,
5105 claimed_wrap: None,
5106 breaks_anywhere: self.breaks_anywhere,
5107 min_content: self.min_content,
5108 initial: self.initial.clone(),
5109 }
5110 }
5111
5112 /// The widest stretch of this text that no break opportunity falls
5113 /// inside, physical px — CSS's min-content: the longest word under
5114 /// `word` and `break-spaces` (a word longer than the box still breaks
5115 /// between glyphs when drawn, as `overflow-wrap: break-word` does in
5116 /// CSS, which leaves the min-content alone), the widest glyph under
5117 /// `glyph`, and the whole line under `none` or an ellipsis, which
5118 /// never wrap. Trailing whitespace hangs, as in CSS. Read off the
5119 /// shaped glyphs of whatever width the buffer was last laid out at —
5120 /// a glyph's advance is the same at every width — with the breaks
5121 /// `unicode-linebreak` finds, so asking lays nothing out.
5122 fn min_content(&mut self) -> f32 {
5123 if let Some(w) = self.min_content {
5124 return w;
5125 }
5126 let w = if self.clamp_w {
5127 self.intrinsic.w
5128 } else {
5129 // Beside an initial a row is narrower by its indent.
5130 widest_unbreakable(&self.buffer, self.breaks_anywhere)
5131 + self.initial.as_ref().map_or(0.0, |i| i.indent)
5132 };
5133 self.min_content = Some(w);
5134 w
5135 }
5136}
5137
5138/// [`CachedText::min_content`]'s walk: every run of a line in order, a
5139/// stretch closing at each glyph a break falls before, and a stretch's
5140/// width up to its last glyph that is not whitespace.
5141fn widest_unbreakable(buffer: &Buffer, anywhere: bool) -> f32 {
5142 let mut widest = 0.0f32;
5143 let mut line = usize::MAX;
5144 let mut breaks: Vec<usize> = Vec::new();
5145 let (mut run_w, mut ink) = (0.0f32, 0.0f32);
5146 for run in buffer.layout_runs() {
5147 if run.line_i != line {
5148 widest = widest.max(ink);
5149 (run_w, ink) = (0.0, 0.0);
5150 line = run.line_i;
5151 breaks.clear();
5152 if !anywhere {
5153 breaks.extend(unicode_linebreak::linebreaks(run.text).map(|(i, _)| i));
5154 }
5155 }
5156 for g in run.glyphs {
5157 if anywhere || breaks.binary_search(&g.start).is_ok() {
5158 widest = widest.max(ink);
5159 (run_w, ink) = (0.0, 0.0);
5160 }
5161 run_w += g.w;
5162 let blank = run
5163 .text
5164 .get(g.start..g.end)
5165 .is_some_and(|t| t.chars().all(char::is_whitespace));
5166 if !blank {
5167 ink = run_w;
5168 }
5169 }
5170 }
5171 widest.max(ink)
5172}
5173
5174/// The shaper's metrics for a font size and line height in physical px,
5175/// each at least one pixel: cosmic-text asserts a line height is not 0,
5176/// which aborted a Node or Lua process over `lineHeight = 0` (or a size
5177/// under 0.4, whose derived height rounds to 0), and a negative one spun
5178/// its layout without end. A NaN or an infinity is a pixel too. Every
5179/// buffer kui shapes into is made through here.
5180pub(crate) fn shaper_metrics(size: f32, line_height: f32) -> Metrics {
5181 let px = |v: f32| if v.is_finite() { v.max(1.0) } else { 1.0 };
5182 Metrics::new(px(size), px(line_height))
5183}
5184
5185/// `s` as the shaper can take it: one bidi paragraph a line (backlog
5186/// FZ6). cosmic-text breaks lines at `\n` and `\r` alone, but Unicode's
5187/// bidi algorithm ends a paragraph at every character of class B — the
5188/// information separators U+001C–U+001E, NEL (U+0085) and the paragraph
5189/// separator (U+2029) too — and cosmic-text asserts that each paragraph
5190/// it is handed in one line runs the way the first does: `a\u{1e}א`
5191/// panicked the shaper. Each is swapped for a character of the same
5192/// length in UTF-8 that ends nothing, so every byte offset into the text
5193/// — a caret's, a selection's, a span's — still lands where it did: the
5194/// unit separator (U+001F) for the other three, a C1 control (U+0080) for
5195/// NEL, the line separator (U+2028) for the paragraph one. They draw as
5196/// what they are, nothing. Borrowed when there is none, which is always
5197/// but for text that carries them.
5198pub(crate) fn one_paragraph(s: &str) -> std::borrow::Cow<'_, str> {
5199 let ends = |c: char| matches!(c, '\u{1c}'..='\u{1e}' | '\u{85}' | '\u{2029}');
5200 if !s.contains(ends) {
5201 return std::borrow::Cow::Borrowed(s);
5202 }
5203 std::borrow::Cow::Owned(
5204 s.chars()
5205 .map(|c| match c {
5206 '\u{1c}'..='\u{1e}' => '\u{1f}',
5207 '\u{85}' => '\u{80}',
5208 '\u{2029}' => '\u{2028}',
5209 c => c,
5210 })
5211 .collect(),
5212 )
5213}
5214
5215/// A buffer set up for the style's line breaking: cosmic-text's wrap mode,
5216/// plus tail ellipsizing at the line budget when asked for.
5217fn new_buffer(fs: &mut FontSystem, style: &TextStyle, scale: f32) -> Buffer {
5218 let metrics = shaper_metrics(style.size * scale, style.line_height * scale);
5219 let mut buffer = Buffer::new(fs, metrics);
5220 buffer.set_wrap(match style.wrap {
5221 TextWrap::Word | TextWrap::BreakSpaces => Wrap::WordOrGlyph,
5222 TextWrap::Glyph => Wrap::Glyph,
5223 TextWrap::None => Wrap::None,
5224 });
5225 if style.ellipsis {
5226 let lines = line_budget(style).max(1);
5227 buffer.set_ellipsize(Ellipsize::End(EllipsizeHeightLimit::Lines(lines)));
5228 }
5229 buffer.set_size(None, None);
5230 buffer
5231}
5232
5233/// The line budget a style implies: `max_lines`, or one line when only
5234/// `ellipsis` is set (0 = unlimited).
5235fn line_budget(style: &TextStyle) -> usize {
5236 if style.max_lines > 0 {
5237 style.max_lines as usize
5238 } else if style.ellipsis {
5239 1
5240 } else {
5241 0
5242 }
5243}
5244
5245fn line_cap(max_lines: usize) -> usize {
5246 if max_lines == 0 {
5247 usize::MAX
5248 } else {
5249 max_lines
5250 }
5251}
5252
5253/// The room after an initial letter, in ems of the text's size.
5254const INITIAL_GAP: f32 = 0.25;
5255
5256/// [`measure_buffer`] for a text node's run: with an initial, its rows
5257/// where they are drawn and the letter's box, which is as tall as the
5258/// lines it drops through however few rows are beside it. The letter is
5259/// not a row of its own.
5260fn measure_run(buffer: &Buffer, max_lines: usize, initial: Option<&Initial>) -> (Size, u32) {
5261 let Some(i) = initial else {
5262 return measure_buffer(buffer, max_lines);
5263 };
5264 let (mut w, mut h, mut rows) = (i.rect.w, i.rect.h, 0u32);
5265 for run in buffer.layout_runs().skip(1) {
5266 let (dx, dy) = i.shift(run.line_i);
5267 w = w.max(dx + run.line_w);
5268 h = h.max(run.line_top + dy + run.line_height);
5269 rows += 1;
5270 }
5271 (Size::new(w, h), rows.max(i.lines))
5272}
5273
5274/// Physical size of the laid-out buffer plus its line count (both capped
5275/// by the line budget).
5276/// The widest laid-out line and the line count of `buffer`, up to
5277/// `max_lines` of them (0 = all), as physical px: what a text node and an
5278/// editor both measure themselves by.
5279pub(crate) fn measure_buffer(buffer: &Buffer, max_lines: usize) -> (Size, u32) {
5280 let mut w = 0.0f32;
5281 let mut lines = 0u32;
5282 // Lines as tall as their tallest span (`Span::size`): the sum of the
5283 // runs' own heights, and the metric times the count when every run is
5284 // the metric's, exactly as before.
5285 let lh = buffer.metrics().line_height;
5286 let (mut sum, mut uniform) = (0.0f32, true);
5287 for run in buffer.layout_runs().take(line_cap(max_lines)) {
5288 w = w.max(run.line_w);
5289 lines += 1;
5290 sum += run.line_height;
5291 uniform &= run.line_height == lh;
5292 }
5293 (
5294 Size::new(w, if uniform { lines as f32 * lh } else { sum }),
5295 lines,
5296 )
5297}
5298
5299impl TextSystem {
5300 pub(crate) fn intrinsic(&mut self, id: TextId) -> Size {
5301 let scale = self.scale;
5302 if let Some(line) = self.long_of(id) {
5303 return Size::new(line.width() / scale, line.line_h / scale);
5304 }
5305 let e = self.entry_mut(id);
5306 Size::new(e.intrinsic.w / scale, e.intrinsic.h / scale)
5307 }
5308
5309 /// Text `id`'s min-content width, logical px: see
5310 /// [`CachedText::min_content`]. A long line is 0 — its rows are broken
5311 /// to any width, and measuring its words would shape what C19 exists
5312 /// not to.
5313 pub(crate) fn min_content(&mut self, id: TextId) -> f32 {
5314 let scale = self.scale;
5315 if self.long_of(id).is_some() {
5316 return 0.0;
5317 }
5318 self.entry_mut(id).min_content() / scale
5319 }
5320
5321 /// The long line behind one of this frame's texts, if it is one.
5322 fn long_of(&self, id: TextId) -> Option<&LongLine> {
5323 self.long(self.frame[id.0 as usize].cache_key)
5324 }
5325
5326 pub(crate) fn wrapped(&mut self, id: TextId, max_w: f32, fs: &mut FontSystem) -> Size {
5327 let scale = self.scale;
5328 if self.long_of(id).is_some() {
5329 // The box, not the line, is the node's width: emission clips a
5330 // single row to it, or the rows are broken to it.
5331 let key = self.frame[id.0 as usize].cache_key;
5332 let (size, rows) = self.long_size(key, Some(max_w * scale));
5333 self.long_mut(key).expect("a long line").laid_rows = rows;
5334 return Size::new(size.w / scale, size.h / scale);
5335 }
5336 self.ensure_wrap(id, max_w, fs);
5337 let e = self.entry_mut(id);
5338 let (mut m, _) = e.measure();
5339 if e.clamp_w {
5340 // The line may run past the box; the box, not the line, is
5341 // the node's width (emission clips to it).
5342 m.w = m.w.min(max_w * scale);
5343 }
5344 Size::new(m.w / scale, m.h / scale)
5345 }
5346}
5347
5348impl TextSystem {
5349 /// The first line's baseline of text `id` as `wrapped` last laid it
5350 /// out, logical px below its top. A run's `line_y`, rounded as the glyphs are drawn at
5351 /// it; a long line's is [`Self::long_baseline`]. An empty text is one
5352 /// line of its own metrics.
5353 pub(crate) fn baseline(&mut self, id: TextId) -> f32 {
5354 let scale = self.scale;
5355 if let Some(line) = self.long_of(id) {
5356 return self.long_baseline(line) / scale;
5357 }
5358 let e = self.entry_mut(id);
5359 // Past an initial, the first row's: the letter's foot is lines down.
5360 let b = e
5361 .buffer
5362 .layout_runs()
5363 .find(|r| e.initial.is_none() || r.line_i > 0)
5364 .map_or(e.buffer.metrics().line_height * 0.8, |r| {
5365 r.line_y.round() + e.shift(r.line_i).1
5366 });
5367 b / scale
5368 }
5369}
5370
5371/// The generic families resolve to a face that is what its name says:
5372/// upright, and monospaced for `Mono`.
5373#[cfg(test)]
5374mod default_families {
5375 use super::*;
5376 use cosmic_text::Family;
5377 use cosmic_text::fontdb::FaceInfo;
5378
5379 /// The face `M` shapes with under `family`, on the database as
5380 /// `new_font_system` set it up.
5381 fn resolved(fs: &mut FontSystem, family: Family<'_>) -> Option<FaceInfo> {
5382 let mut buffer = Buffer::new(fs, Metrics::new(14.0, 18.0));
5383 buffer.set_text("M", &Attrs::new().family(family), Shaping::Advanced, None);
5384 buffer.shape_until_scroll(fs, false);
5385 let id = buffer.layout_runs().next()?.glyphs.first()?.font_id;
5386 fs.db().face(id).cloned()
5387 }
5388
5389 #[test]
5390 fn mono_is_an_upright_monospaced_face() {
5391 let mut fs = new_font_system().0;
5392 if !fs.db().faces().any(|f| f.monospaced) {
5393 eprintln!("skipped: no monospaced face installed");
5394 return;
5395 }
5396 let face = resolved(&mut fs, Family::Monospace).expect("M shapes in Mono");
5397 let mono = default_families(&fs)[2].to_string();
5398 assert_eq!(
5399 face.style,
5400 cosmic_text::Style::Normal,
5401 "Mono ({mono}) resolved to {:?}",
5402 face.post_script_name
5403 );
5404 assert!(
5405 face.monospaced,
5406 "Mono ({mono}) resolved to {:?}, which is not monospaced",
5407 face.post_script_name
5408 );
5409 }
5410
5411 #[test]
5412 fn sans_and_serif_are_upright_and_not_monospaced() {
5413 let mut fs = new_font_system().0;
5414 if fs.db().faces().next().is_none() {
5415 eprintln!("skipped: no face installed");
5416 return;
5417 }
5418 let [sans, serif, _] = default_families(&fs).map(str::to_string);
5419 for (family, name) in [(Family::SansSerif, sans), (Family::Serif, serif)] {
5420 let face = resolved(&mut fs, family).expect("M shapes");
5421 assert_eq!(
5422 face.style,
5423 cosmic_text::Style::Normal,
5424 "{name} resolved to {:?}",
5425 face.post_script_name
5426 );
5427 assert!(
5428 !face.monospaced,
5429 "{name} resolved to {:?}, which is monospaced",
5430 face.post_script_name
5431 );
5432 }
5433 }
5434
5435 /// Every name pinned is one an installed face answers to, so
5436 /// `Family::Name(pinned)` and the generic family agree.
5437 #[test]
5438 fn pinned_names_are_installed_or_cosmic_texts_own() {
5439 let fs = new_font_system().0;
5440 let db = fs.db();
5441 for (name, list) in default_families(&fs).into_iter().zip(DEFAULT_FAMILIES) {
5442 let installed = db
5443 .faces()
5444 .any(|f| f.families.iter().any(|(fam, _)| fam == name));
5445 let on_list = list.contains(&name);
5446 assert!(
5447 installed == on_list,
5448 "{name}: installed {installed}, on the platform list {on_list}"
5449 );
5450 }
5451 }
5452}
5453
5454/// A face whose glyph advances cannot be measured never reaches the shaper.
5455/// macOS's GB18030 Bitmap has no `head`, `hhea` or `hmtx`;
5456/// it says it is fixed-pitch and maps the ideographs, so Han text in
5457/// `Mono` fell back to it, and its advances came out infinite: every glyph
5458/// after one was placed at infinity, and a debug build overflowed in
5459/// `LayoutGlyph::physical`. The fixture faces stand in for it here.
5460#[cfg(test)]
5461mod unmeasurable_faces {
5462 use super::*;
5463 use crate::testing::{font_face, han_face, unmeasurable_face};
5464 use cosmic_text::Family;
5465 use cosmic_text::fontdb::{Database, Source};
5466
5467 /// A session font system over the fixture faces alone: a monospaced
5468 /// family without Han, pinned as `Mono`, the face without metrics that
5469 /// maps 字 and says it is fixed-pitch, and a proportional face that
5470 /// maps it too.
5471 fn font_system() -> FontSystem {
5472 let mut db = Database::new();
5473 for bytes in [
5474 font_face("Kui Mono", 400, false, true),
5475 unmeasurable_face("Kui Bitmap"),
5476 han_face("Kui Han"),
5477 ] {
5478 db.load_font_source(Source::Binary(std::sync::Arc::new(bytes)));
5479 }
5480 let mut fs = font_system_with("en-US".into(), db);
5481 fs.db_mut().set_monospace_family("Kui Mono");
5482 fs
5483 }
5484
5485 fn family(fs: &FontSystem, id: cosmic_text::fontdb::ID) -> String {
5486 fs.db()
5487 .face(id)
5488 .map_or_else(String::new, |f| f.families[0].0.clone())
5489 }
5490
5491 #[test]
5492 fn a_face_without_metrics_is_not_in_the_database() {
5493 let fs = font_system();
5494 let families: Vec<String> = fs.db().faces().map(|f| family(&fs, f.id)).collect();
5495 assert_eq!(families, ["Kui Mono", "Kui Han"]);
5496 }
5497
5498 /// "字 a" in `Mono`: 字 from the face that can say how wide it is, the
5499 /// space and the `a` from the monospaced family after it, every glyph
5500 /// at a finite place.
5501 #[test]
5502 fn han_in_mono_falls_back_to_a_face_that_measures() {
5503 let mut fs = font_system();
5504 let mut buffer = Buffer::new(&mut fs, Metrics::new(14.0, 21.0));
5505 let attrs = Attrs::new().family(Family::Monospace);
5506 buffer.set_text("字 a", &attrs, Shaping::Advanced, None);
5507 buffer.shape_until_scroll(&mut fs, false);
5508 let run = buffer.layout_runs().next().expect("one line");
5509 let glyphs: Vec<(&str, f32, f32, String)> = run
5510 .glyphs
5511 .iter()
5512 .map(|g| (&run.text[g.start..g.end], g.x, g.w, family(&fs, g.font_id)))
5513 .collect();
5514 for &(text, x, w, _) in &glyphs {
5515 assert!(x.is_finite() && w.is_finite(), "{text:?} at {x}, {w} wide");
5516 }
5517 let faces: Vec<(&str, &str)> = glyphs.iter().map(|g| (g.0, g.3.as_str())).collect();
5518 assert_eq!(
5519 faces,
5520 [("字", "Kui Han"), (" ", "Kui Mono"), ("a", "Kui Mono")]
5521 );
5522 // The fixture's advance is half an em, 7 px at 14.
5523 let xs: Vec<f32> = glyphs.iter().map(|g| g.1).collect();
5524 assert_eq!(xs, [0.0, 7.0, 14.0]);
5525 for glyph in run.glyphs {
5526 glyph.physical((0.0, 0.0), 1.0);
5527 }
5528 }
5529}