Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-core/src/text_layout.rs
StratoSDK / crates / strato-ui-core / src / text_layout.rs
1use crate::elements::{Fill, DEFAULT_UI_LINE_HEIGHT_RATIO};
2use crate::fonts::{
3 Cache as FontCache, FamilyId, Properties, RequestedFallbackFontSource, TextLayoutSystem,
4};
5use crate::geometry::rect::RectF;
6use crate::geometry::vector::vec2f;
7use crate::platform::LineStyle;
8use crate::scene::{Border, CornerRadius, Dash};
9use crate::{
10 fonts::{FontId, GlyphId},
11 scene::GlyphFade,
12 Scene,
13};
14use itertools::Itertools;
15use ordered_float::OrderedFloat;
16use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
17use pathfinder_color::ColorU;
18use pathfinder_geometry::vector::Vector2F;
19use rangemap::RangeMap;
20use smallvec::SmallVec;
21use std::{
22 borrow::Borrow,
23 collections::HashMap,
24 hash::{Hash, Hasher},
25 ops::Range,
26 sync::Arc,
27};
28use vec1::{vec1, Vec1};
29 
30type StyleRun = (Range<usize>, StyleAndFont);
31 
32/// The maximum width of the fade applied to text that overflows its
33/// container's maximum width.
34const LINE_FADE_MAX_PIXELS: f32 = 25.0;
35/// The scaling factor applied to the overflow amount to determine the fade
36/// width.
37const LINE_FADE_SCALE_FACTOR: f32 = 3.0;
38/// Minimum overflow threshold before applying clipping effects.
39/// This prevents jitter or odd behavior when text is very close to overflowing.
40const MIN_OVERFLOW_FOR_CLIPPING: f32 = 0.1;
41 
42// How far below the origin the baseline should fall.
43// This means that within the em-box for the line, 80% is above the baseline and 20% is below the baseline.
44pub const DEFAULT_TOP_BOTTOM_RATIO: f32 = 0.8;
45 
46pub const UNDERLINE_THICKNESS: f32 = 2.;
47pub const STRIKETHROUGH_THICKNESSS: f32 = 2.;
48pub const UNDERLINE_BOTTOM_PADDING: f32 = 2.;
49// TODO: Ideally, we would use DEFAULT_MONOSPACE_FONT_SIZE here, however that
50// should stay app crate-specific. Hence, we're using 13.0 as a magic number
51// for the purposes of scaling underline padding correctly.
52const DEFAULT_FONT_SIZE: f32 = 13.;
53 
54// The offset for where on the text glyph the strikethrough should be drawn.
55const STRIKETHROUGH_FONT_OFFSET: f32 = 2.5;
56 
57#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)]
58pub enum TextAlignment {
59 #[default]
60 Left,
61 Center,
62 Right,
63}
64 
65struct TextCache<T> {
66 prev_frame: Mutex<HashMap<CacheKeyValue, Arc<T>>>,
67 curr_frame: RwLock<HashMap<CacheKeyValue, Arc<T>>>,
68}
69 
70impl<T> TextCache<T> {
71 pub fn new() -> Self {
72 Self {
73 prev_frame: Mutex::new(HashMap::new()),
74 curr_frame: RwLock::new(HashMap::new()),
75 }
76 }
77 
78 pub fn finish_frame(&self) {
79 let mut prev_frame = self.prev_frame.lock();
80 let mut curr_frame = self.curr_frame.write();
81 std::mem::swap(&mut *prev_frame, &mut *curr_frame);
82 curr_frame.clear();
83 }
84 
85 pub fn get(&self, key: &dyn CacheKey) -> Option<Arc<T>> {
86 let curr_frame = self.curr_frame.upgradable_read();
87 if let Some(val) = curr_frame.get(key) {
88 return Some(val.clone());
89 }
90 
91 let mut curr_frame = RwLockUpgradableReadGuard::upgrade(curr_frame);
92 if let Some((key, val)) = self.prev_frame.lock().remove_entry(key) {
93 curr_frame.insert(key, val.clone());
94 Some(val)
95 } else {
96 None
97 }
98 }
99 
100 pub fn insert(&self, key: CacheKeyValue, val: Arc<T>) {
101 let mut curr_frame = self.curr_frame.write();
102 curr_frame.insert(key, val);
103 }
104 
105 pub fn remove(&self, key: &dyn CacheKey) {
106 self.prev_frame.lock().remove(key);
107 self.curr_frame.write().remove(key);
108 }
109}
110 
111pub struct LayoutCache {
112 line_cache: TextCache<Line>,
113 text_frame_cache: TextCache<TextFrame>,
114}
115 
116impl Default for LayoutCache {
117 fn default() -> Self {
118 Self::new()
119 }
120}
121 
122impl LayoutCache {
123 pub fn new() -> Self {
124 Self {
125 line_cache: TextCache::new(),
126 text_frame_cache: TextCache::new(),
127 }
128 }
129 
130 pub fn finish_frame(&self) {
131 self.line_cache.finish_frame();
132 self.text_frame_cache.finish_frame();
133 }
134 
135 pub fn remove_line(&self, key: &dyn CacheKey) {
136 self.line_cache.remove(key);
137 }
138 
139 pub fn remove_text_frame(&self, key: &dyn CacheKey) {
140 self.text_frame_cache.remove(key);
141 }
142 
143 #[allow(clippy::too_many_arguments)]
144 pub fn layout_text<'a>(
145 &'a self,
146 text: &'a str,
147 line_style: LineStyle,
148 styles: &'a [StyleRun],
149 max_width: f32,
150 max_height: f32,
151 alignment: TextAlignment,
152 first_line_head_indent: Option<f32>,
153 text_layout_system: &'a TextLayoutSystem<'a>,
154 ) -> Arc<TextFrame> {
155 let (text, adjusted_styles) = strip_leading_unicode_bom(text, styles);
156 let styles = adjusted_styles
157 .as_ref()
158 .map_or(styles, |adjusted_styles| adjusted_styles.as_slice());
159 let key = &CacheKeyRef {
160 text,
161 font_size: OrderedFloat(line_style.font_size),
162 line_height_ratio: line_style.line_height_ratio.into(),
163 fixed_width_tab_size: line_style.fixed_width_tab_size,
164 style_runs: styles,
165 max_width: OrderedFloat(max_width),
166 max_height: Some(max_height.into()),
167 alignment,
168 first_line_head_indent: first_line_head_indent
169 .map(|first_line_head_indent_value| first_line_head_indent_value.into()),
170 clip_config: None,
171 } as &dyn CacheKey;
172 if let Some(text_frame) = self.text_frame_cache.get(key) {
173 text_frame
174 } else {
175 let text_frame = Arc::new(text_layout_system.layout_text(
176 text,
177 line_style,
178 styles,
179 max_width,
180 max_height,
181 alignment,
182 first_line_head_indent,
183 ));
184 let key = CacheKeyValue {
185 text: text.into(),
186 font_size: line_style.font_size.into(),
187 line_height_ratio: line_style.line_height_ratio.into(),
188 fixed_width_tab_size: line_style.fixed_width_tab_size,
189 style_runs: styles.into(),
190 max_width: max_width.into(),
191 max_height: Some(max_height.into()),
192 alignment,
193 first_line_head_indent: first_line_head_indent
194 .map(|first_line_head_indent_value| first_line_head_indent_value.into()),
195 clip_config: None,
196 };
197 for line in text_frame.lines() {
198 for ch in &line.chars_with_missing_glyphs {
199 text_layout_system.request_fallback_font_for_char(
200 *ch,
201 RequestedFallbackFontSource::TextFrame(key.clone()),
202 );
203 }
204 }
205 self.text_frame_cache.insert(key, text_frame.clone());
206 text_frame
207 }
208 }
209 
210 pub fn layout_line<'a>(
211 &'a self,
212 text: &'a str,
213 line_style: LineStyle,
214 style_runs: &'a [StyleRun],
215 max_width: f32,
216 clip_config: ClipConfig,
217 text_layout_system: &TextLayoutSystem<'a>,
218 ) -> Arc<Line> {
219 let (text, adjusted_style_runs) = strip_leading_unicode_bom(text, style_runs);
220 let style_runs = adjusted_style_runs
221 .as_ref()
222 .map_or(style_runs, |adjusted_style_runs| {
223 adjusted_style_runs.as_slice()
224 });
225 let key = &CacheKeyRef {
226 text,
227 font_size: line_style.font_size.into(),
228 line_height_ratio: line_style.line_height_ratio.into(),
229 fixed_width_tab_size: line_style.fixed_width_tab_size,
230 style_runs,
231 max_width: max_width.into(),
232 max_height: None,
233 clip_config: Some(clip_config),
234 alignment: Default::default(),
235 first_line_head_indent: None,
236 } as &dyn CacheKey;
237 
238 if let Some(line) = self.line_cache.get(key) {
239 line
240 } else {
241 let line = Arc::new(text_layout_system.layout_line(
242 text,
243 line_style,
244 style_runs,
245 max_width,
246 clip_config,
247 ));
248 
249 let key = CacheKeyValue {
250 text: text.into(),
251 font_size: line_style.font_size.into(),
252 line_height_ratio: line_style.line_height_ratio.into(),
253 fixed_width_tab_size: line_style.fixed_width_tab_size,
254 style_runs: style_runs.into(),
255 max_width: max_width.into(),
256 max_height: None,
257 alignment: Default::default(),
258 first_line_head_indent: None,
259 clip_config: Some(clip_config),
260 };
261 for ch in &line.chars_with_missing_glyphs {
262 text_layout_system.request_fallback_font_for_char(
263 *ch,
264 RequestedFallbackFontSource::Line(key.clone()),
265 );
266 }
267 self.line_cache.insert(key, line.clone());
268 line
269 }
270 }
271}
272 
273/// Removes a leading UTF-8 BOM from the text and adjusts the style run offsets accordingly.
274/// We throw away the styling of the BOM character.
275fn strip_leading_unicode_bom<'a>(
276 text: &'a str,
277 style_runs: &'a [(Range<usize>, StyleAndFont)],
278) -> (&'a str, Option<Vec<StyleRun>>) {
279 let bom = '\u{FEFF}';
280 if text
281 .chars()
282 .next()
283 .is_none_or(|first_character| first_character != bom)
284 {
285 // There is no leading BOM.
286 return (text, None);
287 }
288 
289 let mut style_runs = style_runs.to_vec();
290 for style_run in style_runs.iter_mut() {
291 let range = (style_run.0.start.saturating_sub(1))..(style_run.0.end.saturating_sub(1));
292 style_run.0 = range;
293 }
294 let text = text.get(bom.len_utf8()..).unwrap_or_else(|| {
295 log::warn!("Unable to get the a substring of the text without a leading BOM");
296 text
297 });
298 (text, Some(style_runs))
299}
300 
301pub trait CacheKey {
302 fn key(&self) -> CacheKeyRef<'_>;
303}
304 
305impl PartialEq for dyn CacheKey + '_ {
306 fn eq(&self, other: &dyn CacheKey) -> bool {
307 self.key() == other.key()
308 }
309}
310 
311impl Eq for dyn CacheKey + '_ {}
312 
313impl Hash for dyn CacheKey + '_ {
314 fn hash<H: Hasher>(&self, state: &mut H) {
315 self.key().hash(state)
316 }
317}
318 
319#[derive(Clone, Eq)]
320pub struct CacheKeyValue {
321 text: String,
322 font_size: OrderedFloat<f32>,
323 line_height_ratio: OrderedFloat<f32>,
324 fixed_width_tab_size: Option<u8>,
325 style_runs: SmallVec<[(Range<usize>, StyleAndFont); 1]>,
326 max_width: OrderedFloat<f32>,
327 max_height: Option<OrderedFloat<f32>>,
328 alignment: TextAlignment,
329 first_line_head_indent: Option<OrderedFloat<f32>>,
330 clip_config: Option<ClipConfig>,
331}
332 
333impl PartialEq for CacheKeyValue {
334 fn eq(&self, other: &Self) -> bool {
335 self.key().eq(&other.key())
336 }
337}
338 
339impl CacheKey for CacheKeyValue {
340 fn key(&self) -> CacheKeyRef<'_> {
341 CacheKeyRef {
342 text: self.text.as_str(),
343 font_size: self.font_size,
344 line_height_ratio: self.line_height_ratio,
345 fixed_width_tab_size: self.fixed_width_tab_size,
346 style_runs: self.style_runs.as_slice(),
347 max_width: self.max_width,
348 max_height: self.max_height,
349 alignment: self.alignment,
350 first_line_head_indent: self.first_line_head_indent,
351 clip_config: self.clip_config,
352 }
353 }
354}
355 
356impl Hash for CacheKeyValue {
357 fn hash<H: Hasher>(&self, state: &mut H) {
358 self.key().hash(state);
359 }
360}
361 
362impl<'a> Borrow<dyn CacheKey + 'a> for CacheKeyValue {
363 fn borrow(&self) -> &(dyn CacheKey + 'a) {
364 self as &dyn CacheKey
365 }
366}
367 
368/// A style override that is applied on paint time without relaying out the text line/frame.
369///
370/// This should be used with caution since the overrides is applied on the character-level
371/// but the character indices might not map to the laid out glyphs 1-1 when there is presence of
372/// ligatures. To give an example, if a text frame has the content "[fi]nal" with fi laid out one
373/// ligature, trying to apply color on only the first character "f" will cause "fi" to be colored.
374#[derive(Default)]
375pub struct PaintStyleOverride {
376 color: RangeMap<usize, ColorU>,
377 underline: RangeMap<usize, ColorU>,
378}
379 
380impl PaintStyleOverride {
381 pub fn with_color(mut self, color_override: RangeMap<usize, ColorU>) -> Self {
382 self.color = color_override;
383 self
384 }
385 
386 pub fn with_underline(mut self, underline_override: RangeMap<usize, ColorU>) -> Self {
387 self.underline = underline_override;
388 self
389 }
390}
391 
392#[derive(Copy, Clone, PartialEq, Eq, Hash)]
393pub struct CacheKeyRef<'a> {
394 text: &'a str,
395 font_size: OrderedFloat<f32>,
396 line_height_ratio: OrderedFloat<f32>,
397 fixed_width_tab_size: Option<u8>,
398 style_runs: &'a [(Range<usize>, StyleAndFont)],
399 max_width: OrderedFloat<f32>,
400 max_height: Option<OrderedFloat<f32>>,
401 alignment: TextAlignment,
402 first_line_head_indent: Option<OrderedFloat<f32>>,
403 clip_config: Option<ClipConfig>,
404}
405 
406impl CacheKey for CacheKeyRef<'_> {
407 fn key(&self) -> CacheKeyRef<'_> {
408 *self
409 }
410}
411 
412/// Enum describing which way to clip the text.
413#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
414pub enum ClipDirection {
415 /// Clip at the end of the text (default)
416 #[default]
417 End,
418 /// Clip at the front of the text
419 Start,
420}
421 
422#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
423pub enum ClipStyle {
424 /// Fade out the clipped text (default)
425 #[default]
426 Fade,
427 /// Show an ellipsis (…) at the clipped edge
428 Ellipsis,
429}
430 
431/// Configuration for clipping text that overflows the available width.
432#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
433pub struct ClipConfig {
434 pub direction: ClipDirection,
435 pub style: ClipStyle,
436}
437 
438impl Default for ClipConfig {
439 fn default() -> Self {
440 Self {
441 direction: ClipDirection::End,
442 style: ClipStyle::Fade,
443 }
444 }
445}
446 
447impl ClipConfig {
448 pub fn end() -> Self {
449 Self {
450 direction: ClipDirection::End,
451 style: ClipStyle::Fade,
452 }
453 }
454 
455 pub fn start() -> Self {
456 Self {
457 direction: ClipDirection::Start,
458 style: ClipStyle::Fade,
459 }
460 }
461 
462 pub fn ellipsis() -> Self {
463 Self {
464 direction: ClipDirection::End,
465 style: ClipStyle::Ellipsis,
466 }
467 }
468}
469 
470pub struct ComputeBaselinePositionArgs<'a> {
471 pub font_cache: &'a FontCache,
472 pub font_size: f32,
473 /// Defines how tall a line should be, used in conjunction with font size. The height of a line is defined to be
474 /// line height ratio * font size (if we are not using glyph-based metrics e.g. ascent/descent).
475 pub line_height_ratio: f32,
476 /// Baseline ratio defines how far below the origin the baseline should fall.
477 /// For example, with a ratio of 0.8, it means that within the em-box for the line, 80% is above the baseline
478 /// and 20% is below the baseline.
479 pub baseline_ratio: f32,
480 /// Ascent measures the distance from the baseline to the top of the em-box.
481 pub ascent: f32,
482 /// Descent measures the distance from the baseline to the bottom of the em-box.
483 pub descent: f32,
484}
485 
486/// Closure to compute baseline position from given arguments. Note that this concept of "baseline position"
487/// is distinct from Core Text's concept of "baseline offset". Specifically, this is the position of the baseline
488/// used to render glyphs. This position is relative to the top of a given Line (see paint() method in Line for
489/// further details on usage).
490pub type ComputeBaselinePositionFn =
491 Box<dyn Fn(ComputeBaselinePositionArgs) -> f32 + 'static + Send + Sync>;
492 
493#[derive(Default, Debug, Clone)]
494pub struct Line {
495 pub width: f32,
496 pub trailing_whitespace_width: f32,
497 pub runs: Vec<Run>,
498 pub font_size: f32,
499 pub line_height_ratio: f32,
500 pub baseline_ratio: f32,
501 pub clip_config: Option<ClipConfig>,
502 
503 pub ascent: f32,
504 pub descent: f32,
505 
506 /// Caret positions represent locations the cursor and selection endpoints
507 /// can snap to when selecting text.
508 /// On MacOS, CoreText gives us one caret position per visible glyphs,
509 /// meaning that ligatures will have a single caret position.
510 /// On winit platforms, cosmic-text gives us one caret position per
511 /// codepoint, meaning ligatures will have multiple caret positions.
512 pub caret_positions: Vec<CaretPosition>,
513 pub chars_with_missing_glyphs: Vec<char>,
514}
515 
516/// Default baseline offset calculation for Lines.
517pub fn default_compute_baseline_position(
518 font_size: f32,
519 line_height_ratio: f32,
520 ascent: f32,
521 descent: f32,
522) -> f32 {
523 let line_height = font_size * line_height_ratio;
524 // Text height is the distance from top of em-box to
525 // baseline + distance from baseline to bottom of em-box.
526 let text_height = ascent + descent;
527 // We want the text to be vertically centered within the line.
528 let padding_top = (line_height - text_height) / 2.0;
529 // Baseline position is the distance from top of line to top
530 // of em-box + distance from top of em-box to baseline.
531 padding_top + ascent
532}
533 
534/// Returns closure computing the default baseline offset for given font metrics.
535pub fn default_compute_baseline_position_fn() -> ComputeBaselinePositionFn {
536 Box::new(|font_metrics| {
537 default_compute_baseline_position(
538 font_metrics.font_size,
539 font_metrics.line_height_ratio,
540 font_metrics.ascent,
541 font_metrics.descent,
542 )
543 })
544}
545 
546/// A caret position within a line. Generally, there is a caret position after
547/// every grapheme. This does not always correspond to glyphs (for example, a
548/// ligature is one glyph but contains multiple caret positions). It also does
549/// not necessarily correspond to characters (many emoji are represented by
550/// multiple Unicode scalar values, but only produce a single caret position).
551#[derive(Debug, Default, Clone)]
552pub struct CaretPosition {
553 /// The x-position of this caret location, relative to the line's origin.
554 pub position_in_line: f32,
555 /// The starting character index corresponding to this location in the input string.
556 /// In the case of RTL text, this may not correspond to `position_in_line`.
557 /// That is, a caret position that's visually to the right of another may
558 /// actually have a _lower_ `start_offset`.
559 pub start_offset: usize,
560 /// The index of the last character in this caret position. This is _inclusive_.
561 pub last_offset: usize,
562}
563 
564#[derive(Debug, Default, Copy, Clone, Hash, PartialEq, Eq)]
565pub struct TextStyle {
566 pub foreground_color: Option<ColorU>,
567 // syntax_color is similar to foreground_color except it isn't inheritable.
568 pub syntax_color: Option<ColorU>,
569 pub background_color: Option<ColorU>,
570 pub border: Option<TextBorder>,
571 pub error_underline_color: Option<ColorU>,
572 pub show_strikethrough: bool,
573 // Underline color (of either text underline or hyperlink).
574 pub underline_color: Option<ColorU>,
575 // Unique id for each hyperlink in a frame, used to group parts of a hyperlink together if a hyperlink is soft-wrapped.
576 pub hyperlink_id: Option<i32>,
577}
578 
579#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
580pub struct TextBorder {
581 pub color: ColorU,
582 pub radius: u8,
583 pub width: u8,
584 // The line height ratio override to determine the size of the border and the background color (if there is one).
585 // By default, the border will fit the entire line height.
586 pub line_height_ratio_override: Option<u8>,
587}
588 
589#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
590pub struct StyleAndFont {
591 pub font_family: FamilyId,
592 pub properties: Properties,
593 pub style: TextStyle,
594}
595 
596impl StyleAndFont {
597 pub fn new(font_family: FamilyId, properties: Properties, style: TextStyle) -> Self {
598 StyleAndFont {
599 font_family,
600 properties,
601 style,
602 }
603 }
604}
605 
606impl TextStyle {
607 pub fn new() -> Self {
608 Default::default()
609 }
610 
611 /// Returns a new TextStyle containing only the inheritable styles from
612 /// the current TextStyle (self). Note that this is the source of truth
613 /// for which text styles are inheritable.
614 pub fn filter_inheritable_styles(self) -> Self {
615 TextStyle {
616 foreground_color: self.foreground_color,
617 syntax_color: None,
618 background_color: self.background_color,
619 error_underline_color: None,
620 border: None,
621 show_strikethrough: false,
622 underline_color: None,
623 hyperlink_id: None,
624 }
625 }
626 
627 pub fn with_foreground_color(mut self, foreground_color: ColorU) -> Self {
628 self.foreground_color = Some(foreground_color);
629 self
630 }
631 
632 pub fn with_syntax_color(mut self, syntax_color: ColorU) -> Self {
633 self.syntax_color = Some(syntax_color);
634 self
635 }
636 
637 pub fn with_border(mut self, border: TextBorder) -> Self {
638 self.border = Some(border);
639 self
640 }
641 
642 pub fn with_background_color(mut self, background_color: ColorU) -> Self {
643 self.background_color = Some(background_color);
644 self
645 }
646 
647 pub fn with_error_underline_color(mut self, error_underline_color: ColorU) -> Self {
648 self.error_underline_color = Some(error_underline_color);
649 self
650 }
651 
652 pub fn with_show_strikethrough(mut self, show_strikethrough: bool) -> Self {
653 self.show_strikethrough = show_strikethrough;
654 self
655 }
656 
657 pub fn with_underline_color(mut self, underline_color: ColorU) -> Self {
658 self.underline_color = Some(underline_color);
659 self
660 }
661 
662 pub fn with_hyperlink_id(mut self, hyperlink_id: i32) -> Self {
663 self.hyperlink_id = Some(hyperlink_id);
664 self
665 }
666}
667 
668/// A series of consecutive glyphs within a run that have the same styles.
669#[derive(Debug, Clone)]
670pub struct Run {
671 pub font_id: FontId,
672 pub glyphs: Vec<Glyph>,
673 pub styles: TextStyle,
674 pub width: f32,
675}
676 
677#[derive(Debug, Clone)]
678pub struct Glyph {
679 pub id: GlyphId,
680 /// Position of the glyph on its baseline.
681 pub position_along_baseline: Vector2F,
682 /// The starting index of the character in the original string where this glyph starts.
683 pub index: usize,
684 /// The width of the glyph (its advance), in pixels.
685 pub width: f32,
686}
687 
688/// On MacOS, CoreText includes line separators in the TextFrame's lines.
689/// On winit, cosmic-text strips line separators, so they do not have their
690/// own glyphs in the TextFrame's lines.
691#[derive(Default, Debug)]
692pub struct TextFrame {
693 lines: Vec1<Line>,
694 /// The max width of any line in the text frame.
695 max_width: f32,
696 alignment: TextAlignment,
697}
698 
699impl TextFrame {
700 pub fn new(lines: Vec1<Line>, max_width: f32, alignment: TextAlignment) -> Self {
701 TextFrame {
702 lines,
703 max_width,
704 alignment,
705 }
706 }
707 
708 /// A text frame with no text. It has a single line with no runs which
709 /// is created by `Line#empty`.
710 ///
711 /// System APIs may return text frames with all sorts of different values so
712 /// this API helps standardize these values for our application logic.
713 pub fn empty(font_size: f32, line_height_ratio: f32) -> Self {
714 TextFrame {
715 lines: vec1![Line::empty(font_size, line_height_ratio, 0)],
716 max_width: 0.0,
717 alignment: Default::default(),
718 }
719 }
720 
721 // Returns the absolute bounds of all hyperlinks in the frame. The position of each link is offset by the provided `origin.`
722 pub fn hyperlink_bounds(&self, origin: Vector2F) -> Vec<Vec<RectF>> {
723 let mut positions: Vec<Vec<RectF>> = Vec::new();
724 let mut height = origin.y();
725 let mut prev_hyperlink_id: Option<i32> = None;
726 
727 // If the current rectangle is part of the previous hyperlink frame, just push it in a running vector. Otherwise create a new entry.
728 for line in &self.lines {
729 let mut width = origin.x() + self.line_x_offset(line);
730 for run in &line.runs {
731 if let Some(curr_hyperlink_id) = run.styles.hyperlink_id {
732 let curr_rectangle = RectF::new(
733 Vector2F::new(width, height),
734 Vector2F::new(run.width, line.font_size * line.line_height_ratio),
735 );
736 
737 let mut soft_wrapped = false;
738 if let Some(prev_id) = prev_hyperlink_id {
739 if prev_id == curr_hyperlink_id {
740 positions
741 .last_mut()
742 .expect("Positions should be non-empty")
743 .push(curr_rectangle);
744 soft_wrapped = true;
745 }
746 }
747 
748 if !soft_wrapped {
749 positions.push(vec![curr_rectangle]);
750 }
751 
752 prev_hyperlink_id = Some(curr_hyperlink_id);
753 }
754 width += run.width;
755 }
756 height += line.font_size * line.line_height_ratio;
757 }
758 
759 positions
760 }
761 
762 /// We can't mark this as cfg(test) because we need this in the warp crate tests.
763 pub fn mock(text: &str) -> Self {
764 let mut acc = 0;
765 let lines = text
766 .split('\n')
767 .map(|line| {
768 let glyphs: Vec<_> = line
769 .chars()
770 .enumerate()
771 .map(|(index, _)| Glyph {
772 id: Default::default(),
773 position_along_baseline: Default::default(),
774 index: index + acc,
775 width: 10.0, // dummy width
776 })
777 .collect();
778 acc += glyphs.len();
779 let runs = vec![Run {
780 font_id: FontId(0),
781 glyphs,
782 styles: TextStyle::new(),
783 width: Default::default(),
784 }];
785 Line::mock(runs)
786 })
787 .collect();
788 
789 match Vec1::try_from_vec(lines) {
790 Ok(lines) => TextFrame {
791 lines,
792 max_width: Default::default(),
793 alignment: Default::default(),
794 },
795 Err(_) => TextFrame::empty(Default::default(), Default::default()),
796 }
797 }
798 
799 pub fn lines(&self) -> &Vec<Line> {
800 self.lines.as_ref()
801 }
802 
803 pub fn max_width(&self) -> f32 {
804 self.max_width
805 }
806 
807 pub fn height(&self) -> f32 {
808 self.lines()
809 .iter()
810 .fold(0., |prev, line| prev + line.height())
811 }
812 
813 /// Returns the height of the frame up to the row, inclusive
814 pub fn height_up_to_row(&self, row: usize) -> f32 {
815 self.lines()
816 .iter()
817 .take(row + 1)
818 .fold(0., |prev, line| prev + line.height())
819 }
820 
821 /// Given an index, returns the row the corresponding glyph is in the text frame.
822 /// If the index is beyond the bounds of the text frame, we return the last row
823 /// in the text frame.
824 ///
825 /// `clamp_above` is used to disambiguate whether the index is the last index
826 /// of a line or the first index of the next line. If `clamp_above` is true,
827 /// it's the last index of the above line and if it's falase, it's the first
828 /// index of the below line.
829 pub fn row_within_frame(&self, index: usize, clamp_above: bool) -> usize {
830 for (i, line) in self.lines.iter().enumerate() {
831 if let Some(last_glyph) = line.last_glyph() {
832 let last_index_in_row = match clamp_above {
833 true => last_glyph.index + 1,
834 false => last_glyph.index,
835 };
836 if last_index_in_row >= index {
837 return i;
838 }
839 }
840 }
841 self.lines.len() - 1
842 }
843 
844 pub fn paint(
845 &self,
846 bounds: RectF,
847 style_overrides: &PaintStyleOverride,
848 default_color: ColorU,
849 scene: &mut Scene,
850 font_cache: &FontCache,
851 ) {
852 for (index, line) in self.lines.iter().enumerate() {
853 let origin =
854 bounds.origin() + vec2f(self.line_x_offset(line), index as f32 * line.height());
855 let bounds = RectF::from_points(origin, bounds.lower_right());
856 line.paint(bounds, style_overrides, default_color, font_cache, scene);
857 }
858 }
859 
860 // The x offset relative to the text frame origin to paint the line.
861 pub(crate) fn line_x_offset(&self, line: &Line) -> f32 {
862 let line_width = line.width;
863 
864 match self.alignment {
865 TextAlignment::Left => 0.,
866 TextAlignment::Right => self.max_width - line_width,
867 TextAlignment::Center => (self.max_width - line_width) / 2.,
868 }
869 }
870 
871 pub fn paint_with_baseline_position(
872 &self,
873 bounds: RectF,
874 style_overrides: &PaintStyleOverride,
875 default_color: ColorU,
876 scene: &mut Scene,
877 font_cache: &FontCache,
878 baseline_position_fn: &ComputeBaselinePositionFn,
879 ) {
880 for (index, line) in self.lines.iter().enumerate() {
881 let origin = bounds.origin()
882 + vec2f(
883 0.,
884 index as f32 * font_cache.line_height(line.font_size, line.line_height_ratio),
885 );
886 let bounds = RectF::from_points(origin, bounds.lower_right());
887 line.paint_with_baseline_position(
888 bounds,
889 style_overrides,
890 default_color,
891 font_cache,
892 scene,
893 baseline_position_fn,
894 );
895 }
896 }
897}
898 
899impl Line {
900 /// A line with no text. It has zero width and a height equivalent to the
901 /// line height.
902 ///
903 /// System APIs may return empty lines with all sorts of different values for
904 /// width, height, etc. so we can use this API to help standardize these
905 /// values for our application logic.
906 pub fn empty(font_size: f32, line_height_ratio: f32, glyph_index: usize) -> Self {
907 Line {
908 width: 0.0,
909 trailing_whitespace_width: 0.0,
910 runs: vec![],
911 font_size,
912 line_height_ratio,
913 baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO,
914 ascent: font_size * DEFAULT_TOP_BOTTOM_RATIO,
915 descent: font_size * (1. - DEFAULT_TOP_BOTTOM_RATIO),
916 clip_config: None,
917 caret_positions: vec![CaretPosition {
918 position_in_line: 0.0,
919 start_offset: glyph_index,
920 last_offset: glyph_index,
921 }],
922 chars_with_missing_glyphs: vec![],
923 }
924 }
925 
926 /// We can't mark this as cfg(test) because we need this in the warp crate tests.
927 pub fn mock(runs: Vec<Run>) -> Self {
928 Line {
929 width: Default::default(),
930 trailing_whitespace_width: Default::default(),
931 runs,
932 font_size: DEFAULT_FONT_SIZE,
933 line_height_ratio: DEFAULT_UI_LINE_HEIGHT_RATIO,
934 baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO,
935 ascent: DEFAULT_FONT_SIZE * DEFAULT_TOP_BOTTOM_RATIO,
936 descent: DEFAULT_FONT_SIZE * (1. - DEFAULT_TOP_BOTTOM_RATIO),
937 clip_config: None,
938 caret_positions: Default::default(),
939 chars_with_missing_glyphs: Default::default(),
940 }
941 }
942 
943 #[cfg(test)]
944 pub fn mock_from_str(line: &str) -> Self {
945 assert!(!line.contains('\n'));
946 let glyphs: Vec<_> = line
947 .chars()
948 .enumerate()
949 .map(|(index, _)| Glyph {
950 id: Default::default(),
951 position_along_baseline: Default::default(),
952 index,
953 width: 10.0, // dummy width
954 })
955 .collect();
956 let runs = vec![Run {
957 font_id: FontId(0),
958 glyphs,
959 styles: TextStyle::new(),
960 width: Default::default(),
961 }];
962 Line::mock(runs)
963 }
964 
965 pub fn height(&self) -> f32 {
966 self.line_height_ratio * self.font_size
967 }
968 
969 pub fn first_glyph(&self) -> Option<&Glyph> {
970 let first_run = self.runs.first()?;
971 first_run.glyphs.first()
972 }
973 
974 pub fn last_glyph(&self) -> Option<&Glyph> {
975 let last_run = self.runs.last()?;
976 last_run.glyphs.last()
977 }
978 
979 pub fn x_for_index(&self, index: usize) -> f32 {
980 for run in &self.runs {
981 for glyph in &run.glyphs {
982 if glyph.index == index {
983 return glyph.position_along_baseline.x();
984 }
985 }
986 }
987 
988 self.width
989 }
990 
991 /// The width in pixels of the glyph at this index. Returns None if the index is invalid.
992 pub fn width_for_index(&self, index: usize) -> Option<f32> {
993 let mut prev_glyph = self.runs.first().and_then(|run| run.glyphs.first())?;
994 for run in &self.runs {
995 for glyph in &run.glyphs {
996 if glyph.index == index + 1 {
997 return Some(
998 glyph.position_along_baseline.x() - prev_glyph.position_along_baseline.x(),
999 );
1000 }
1001 prev_glyph = glyph;
1002 }
1003 }
1004 
1005 if index == self.last_index() {
1006 return Some(self.width - prev_glyph.position_along_baseline.x());
1007 }
1008 
1009 None
1010 }
1011 
1012 /// Finds the nearest caret position for a character index. This is similar
1013 /// to [`Self::x_for_index`], but accounts for multi-character glyphs such
1014 /// as ligatures and many emojis.
1015 pub fn caret_position_for_index(&self, index: usize) -> f32 {
1016 for caret in self.caret_positions.iter() {
1017 if caret.contains_index(index) {
1018 return caret.position_in_line;
1019 }
1020 }
1021 
1022 // If `index` is out of bounds or at the extremes of the line, clamp to
1023 // either 0 or the line width. Which we choose depends on whether `index`
1024 // is before or after the line's range.
1025 if self
1026 .caret_positions
1027 .first()
1028 .is_some_and(|caret| index < caret.start_offset)
1029 {
1030 0.
1031 } else {
1032 self.width
1033 }
1034 }
1035 
1036 fn is_x_in_bound(&self, x: f32) -> bool {
1037 x >= 0. && x < self.width
1038 }
1039 
1040 /// Returns the character index for the glyph best corresponding to `x`.
1041 pub fn index_for_x(&self, x: f32) -> Option<usize> {
1042 if !self.is_x_in_bound(x) {
1043 None
1044 } else {
1045 for run in self.runs.iter().rev() {
1046 for glyph in run.glyphs.iter().rev() {
1047 if glyph.position_along_baseline.x() <= x {
1048 return Some(glyph.index);
1049 }
1050 }
1051 }
1052 
1053 Some(0)
1054 }
1055 }
1056 
1057 /// Returns the caret index closest to the (relative) `x` position,
1058 /// but returns the first or end index if the `x` position is out of bounds.
1059 pub fn caret_index_for_x_unbounded(&self, x: f32) -> usize {
1060 let max_line_x = self.x_for_index(self.end_index());
1061 
1062 if !self.is_x_in_bound(x) {
1063 // max_line_x should be smaller than self.width, but we check both just in case.
1064 return if x >= max_line_x || x >= self.width {
1065 self.end_index()
1066 } else {
1067 self.first_index()
1068 };
1069 }
1070 
1071 // Handle special case where x is on the second half of the last glyph: `caret_index_for_x`
1072 // checks against glyph boundaries, but there's no glyph to the right when x lies past the
1073 // midpoint of the last glyph, so it'll always return the start of the last glyph instead of
1074 // the end. We need to handle this case separately and return the end of the last glyph.
1075 let tail_caret_position = match self.caret_positions.last() {
1076 Some(caret) => caret.position_in_line,
1077 None => return self.first_index(),
1078 };
1079 // Note that if the text has been truncated, `max_line_x` could potentially be smaller than
1080 // `tail_caret_position`. In such cases, the below math for handling this special case becomes
1081 // incorrect, so we skip the check.
1082 let is_text_truncated = max_line_x <= tail_caret_position;
1083 if !is_text_truncated && (x - max_line_x).abs() < (x - tail_caret_position).abs() {
1084 return self.end_index();
1085 }
1086 
1087 // Default case - we can unwrap safely since we've covered the edge cases resulting in `None` above.
1088 self.caret_index_for_x(x)
1089 .expect("None conditions should be already checked & handled")
1090 }
1091 
1092 /// Returns the starting character index for the caret position best corresponding to `x`.
1093 /// Returns `None` if `x` is out of bounds.
1094 /// Max return value is `self.last_index()` (`self.end_index() - 1`).
1095 ///
1096 /// *Important*: if you change the condition for returning `None`, make sure to update the
1097 /// checks in `caret_index_for_x_unbounded` as well.
1098 pub fn caret_index_for_x(&self, x: f32) -> Option<usize> {
1099 if !self.is_x_in_bound(x) {
1100 None
1101 } else {
1102 // Iterate backwards through the list of caret positions, and bias to the start of the
1103 // line if the search fails. Equivalently, we could iterate forwards and bias to the
1104 // end of the line.
1105 for (right, left) in self.caret_positions.iter().rev().tuple_windows() {
1106 // We want to find the two caret positions adjacent to x, and then chose the closest.
1107 // This is the first window from the back where the left caret position starts before x.
1108 if left.position_in_line <= x {
1109 if (left.position_in_line - x).abs() < (right.position_in_line - x).abs() {
1110 return Some(left.start_offset);
1111 } else {
1112 return Some(right.start_offset);
1113 }
1114 }
1115 }
1116 
1117 Some(0)
1118 }
1119 }
1120 
1121 /// The first character index that's within this line.
1122 pub fn first_index(&self) -> usize {
1123 self.caret_positions
1124 .first()
1125 .map_or(0, |caret| caret.start_offset)
1126 }
1127 
1128 /// The last character index that's within this line (inclusive).
1129 pub fn last_index(&self) -> usize {
1130 self.caret_positions
1131 .last()
1132 .map_or(0, |caret| caret.last_offset)
1133 }
1134 
1135 /// The first character index that's after this line.
1136 pub fn end_index(&self) -> usize {
1137 self.last_index() + 1
1138 }
1139 
1140 #[allow(clippy::too_many_arguments)]
1141 fn paint_run_decorations(
1142 &self,
1143 glyph_color: ColorU,
1144 run: &Run,
1145 origin: Vector2F,
1146 visible_bounds: RectF,
1147 font_cache: &FontCache,
1148 scene: &mut Scene,
1149 baseline_position_fn: &ComputeBaselinePositionFn,
1150 ) {
1151 if let Some(first_glyph) = run.glyphs.first() {
1152 // We only need to draw text background if
1153 // 1) there is at least one glyph in the run
1154 // 2) either the border or the background color is present
1155 if run.styles.border.is_some() || run.styles.background_color.is_some() {
1156 // Scale the block padding based on the font size.
1157 let block_padding = if run.styles.border.is_some() {
1158 self.font_size / 10.
1159 } else {
1160 0.
1161 };
1162 
1163 // If the border has a line height ratio override, convert it from u8 to f32.
1164 // Else use the current text's line height ratio.
1165 let line_height_ratio = run
1166 .styles
1167 .border
1168 .and_then(|border| {
1169 border
1170 .line_height_ratio_override
1171 .map(|val| val as f32 / 100.)
1172 })
1173 .unwrap_or(self.line_height_ratio);
1174 
1175 // Compute the origin of where the first glyph was rendered. The position reported
1176 // by the glyph is along it's baseline, so we need to offset it by the baseline
1177 // offset to get back to the top of the glyph.
1178 // We also need to shift the position horizontally to account for kerning when bordering
1179 // is turned on.
1180 let rect_origin = origin + first_glyph.position_along_baseline
1181 - vec2f(
1182 2. * block_padding,
1183 (baseline_position_fn)(ComputeBaselinePositionArgs {
1184 font_cache,
1185 font_size: self.font_size,
1186 line_height_ratio,
1187 baseline_ratio: self.baseline_ratio,
1188 ascent: self.ascent,
1189 descent: self.descent,
1190 }) + 2. * block_padding,
1191 );
1192 let text_rect = RectF::new(
1193 rect_origin,
1194 vec2f(
1195 run.width + 2. * block_padding,
1196 font_cache.line_height(self.font_size, line_height_ratio)
1197 + 2. * block_padding,
1198 ),
1199 );
1200 
1201 let Some(clipped_rect) = text_rect.intersection(visible_bounds) else {
1202 // If there is no intersection, there's no need to paint the rect
1203 // (as it won't be visible).
1204 return;
1205 };
1206 let rendered_background = scene.draw_rect_with_hit_recording(clipped_rect);
1207 
1208 if let Some(border) = run.styles.border {
1209 rendered_background
1210 .with_corner_radius(CornerRadius::with_all(crate::scene::Radius::Pixels(
1211 border.radius as f32,
1212 )))
1213 .with_border(
1214 Border::all(border.width as f32).with_border_color(border.color),
1215 );
1216 }
1217 
1218 if let Some(color) = run.styles.background_color {
1219 rendered_background.with_background(Fill::Solid(color));
1220 }
1221 }
1222 }
1223 
1224 if let Some((error_underline_color, first_glyph)) =
1225 run.styles.error_underline_color.zip(run.glyphs.first())
1226 {
1227 // We draw the error underline at the baseline.
1228 let underline_origin = origin + first_glyph.position_along_baseline;
1229 
1230 let scaled_underline_bottom_padding =
1231 UNDERLINE_BOTTOM_PADDING * (self.font_size / DEFAULT_FONT_SIZE);
1232 
1233 let dash = Dash {
1234 dash_length: 4.,
1235 gap_length: 3.,
1236 // We don't want to adjust the gap width based on the length of
1237 // the run, as that would cause the dashes to wiggle as the run
1238 // changes.
1239 force_consistent_gap_length: true,
1240 };
1241 
1242 let underline_rect = RectF::new(
1243 underline_origin,
1244 vec2f(
1245 run.width,
1246 scaled_underline_bottom_padding + UNDERLINE_THICKNESS,
1247 ),
1248 );
1249 
1250 if let Some(clipped_rect) = underline_rect.intersection(visible_bounds) {
1251 scene
1252 .draw_rect_without_hit_recording(clipped_rect)
1253 .with_border(
1254 Border::bottom(UNDERLINE_THICKNESS)
1255 .with_dashed_border(dash)
1256 .with_border_color(error_underline_color),
1257 );
1258 }
1259 }
1260 
1261 // Draw a strikethrough through text if boolean flag is set.
1262 if run.styles.show_strikethrough {
1263 if let Some(first_glyph) = run.glyphs.first() {
1264 let mut strikethrough_origin = origin + first_glyph.position_along_baseline;
1265 strikethrough_origin
1266 .set_y(strikethrough_origin.y() - self.font_size / STRIKETHROUGH_FONT_OFFSET);
1267 let strikethrough_rect = RectF::new(
1268 strikethrough_origin,
1269 vec2f(run.width, STRIKETHROUGH_THICKNESSS),
1270 );
1271 
1272 if let Some(clipped_rect) = strikethrough_rect.intersection(visible_bounds) {
1273 scene
1274 .draw_rect_without_hit_recording(clipped_rect)
1275 .with_background(Fill::Solid(glyph_color));
1276 }
1277 }
1278 }
1279 }
1280 
1281 /// Paints the line of text using given parameters. Uses default baseline offset calculation for a Line.
1282 pub fn paint(
1283 &self,
1284 bounds: RectF,
1285 style_overrides: &PaintStyleOverride,
1286 default_color: ColorU,
1287 font_cache: &FontCache,
1288 scene: &mut Scene,
1289 ) {
1290 self.paint_internal(
1291 bounds,
1292 style_overrides,
1293 default_color,
1294 font_cache,
1295 scene,
1296 &default_compute_baseline_position_fn(),
1297 )
1298 }
1299 
1300 /// Paints the line of text using given parameters. Note that the caller can provide a custom
1301 /// closure to compute the baseline position used for the text.
1302 pub fn paint_with_baseline_position(
1303 &self,
1304 bounds: RectF,
1305 style_overrides: &PaintStyleOverride,
1306 default_color: ColorU,
1307 font_cache: &FontCache,
1308 scene: &mut Scene,
1309 baseline_position_fn: &ComputeBaselinePositionFn,
1310 ) {
1311 self.paint_internal(
1312 bounds,
1313 style_overrides,
1314 default_color,
1315 font_cache,
1316 scene,
1317 baseline_position_fn,
1318 )
1319 }
1320 
1321 fn paint_internal(
1322 &self,
1323 bounds: RectF,
1324 style_overrides: &PaintStyleOverride,
1325 default_color: ColorU,
1326 font_cache: &FontCache,
1327 scene: &mut Scene,
1328 baseline_position_fn: &ComputeBaselinePositionFn,
1329 ) {
1330 let origin = bounds.origin();
1331 let available_width = bounds.width();
1332 
1333 // Fade out the line if the text in it has been clipped to max_width
1334 let width_without_trailing_whitespace = self.width - self.trailing_whitespace_width;
1335 let overflow = width_without_trailing_whitespace - available_width;
1336 
1337 let (clip_direction, clip_style) = self
1338 .clip_config
1339 .map(|config| (config.direction, config.style))
1340 .unwrap_or_default();
1341 
1342 let ellipsis_glyph: Option<(GlyphId, FontId, f32)> =
1343 if clip_style == ClipStyle::Ellipsis && overflow > MIN_OVERFLOW_FOR_CLIPPING {
1344 let ellipsis_run = match clip_direction {
1345 ClipDirection::Start => self.runs.last(),
1346 ClipDirection::End => self.runs.first(),
1347 };
1348 
1349 ellipsis_run.and_then(|run| {
1350 font_cache.glyph_for_char(run.font_id, '…', false).and_then(
1351 |(glyph_id, font_id)| {
1352 font_cache
1353 .glyph_advance(font_id, self.font_size, glyph_id)
1354 .ok()
1355 .map(|advance| (glyph_id, font_id, advance.x()))
1356 },
1357 )
1358 })
1359 } else {
1360 None
1361 };
1362 let ellipsis_width = ellipsis_glyph
1363 .as_ref()
1364 .map(|(_, _, width)| *width)
1365 .unwrap_or_default();
1366 
1367 // Set the length of the fade based on how much text is overflowing.
1368 let fade_width = LINE_FADE_MAX_PIXELS.min(overflow * LINE_FADE_SCALE_FACTOR);
1369 let fade = if overflow < MIN_OVERFLOW_FOR_CLIPPING
1370 || clip_style == ClipStyle::Ellipsis
1371 || self.clip_config.is_none()
1372 {
1373 None
1374 } else {
1375 match clip_direction {
1376 ClipDirection::End => {
1377 let fade_end = bounds.upper_right().x();
1378 let fade_start = fade_end - fade_width;
1379 Some(GlyphFade::horizontal(fade_start, fade_end))
1380 }
1381 ClipDirection::Start => {
1382 let fade_end = bounds.origin().x();
1383 let fade_start = fade_end + fade_width;
1384 Some(GlyphFade::horizontal(fade_start, fade_end))
1385 }
1386 }
1387 };
1388 
1389 // Adjust the origin to be the baseline of the line, not the top of
1390 // the line. Note that the baseline position is consistent across the entire line,
1391 // even if we have different fonts on a single line.
1392 let baseline_position = (baseline_position_fn)(ComputeBaselinePositionArgs {
1393 font_cache,
1394 font_size: self.font_size,
1395 line_height_ratio: self.line_height_ratio,
1396 baseline_ratio: self.baseline_ratio,
1397 ascent: self.ascent,
1398 descent: self.descent,
1399 });
1400 
1401 let line_origin = origin + vec2f(0., baseline_position);
1402 let is_start_clipping =
1403 self.clip_config.is_some() && clip_direction == ClipDirection::Start;
1404 let run_iter = if is_start_clipping {
1405 itertools::Either::Left(self.runs.iter().rev())
1406 } else {
1407 itertools::Either::Right(self.runs.iter())
1408 };
1409 
1410 let mut remaining_width = match clip_style {
1411 // For ellipsis, reserve space on the side where we will draw the ellipsis.
1412 ClipStyle::Ellipsis if ellipsis_width > 0. => match clip_direction {
1413 ClipDirection::End => (available_width - ellipsis_width).max(0.),
1414 ClipDirection::Start => (available_width - ellipsis_width).max(0.),
1415 },
1416 _ => available_width,
1417 };
1418 
1419 'runs: for run in run_iter {
1420 let mut glyph_color = default_color;
1421 // We define foreground_color to overwrite syntax_color since the
1422 // foreground color was likely set explicitly somewhere (by the user
1423 // or system), whereas the syntax color is automatically added.
1424 if let Some(syntax_color) = run.styles.syntax_color {
1425 glyph_color = syntax_color;
1426 }
1427 if let Some(foreground_color) = run.styles.foreground_color {
1428 glyph_color = foreground_color;
1429 }
1430 
1431 let glyph_iter = if is_start_clipping {
1432 itertools::Either::Left(run.glyphs.iter().rev())
1433 } else {
1434 itertools::Either::Right(run.glyphs.iter())
1435 };
1436 let mut should_stop_after_run = false;
1437 for glyph in glyph_iter {
1438 let index = glyph.index;
1439 let override_color = style_overrides.color.get(&index).cloned();
1440 
1441 // If we've started truncating in ellipsis mode, draw the ellipsis and stop painting glyphs.
1442 if clip_style == ClipStyle::Ellipsis
1443 && ellipsis_width > 0.
1444 && remaining_width < glyph.width
1445 {
1446 if let Some((glyph_id, font_id, _)) = ellipsis_glyph {
1447 let ellipsis_x = match clip_direction {
1448 ClipDirection::End => {
1449 (available_width - ellipsis_width - remaining_width).max(0.)
1450 }
1451 ClipDirection::Start => remaining_width,
1452 };
1453 let ellipsis_origin = line_origin + vec2f(ellipsis_x, 0.);
1454 
1455 scene.draw_glyph(
1456 ellipsis_origin,
1457 glyph_id,
1458 font_id,
1459 self.font_size,
1460 default_color,
1461 );
1462 }
1463 break 'runs;
1464 }
1465 
1466 // If there is not enough space to paint even part of the glyph,
1467 // stop painting glyphs but still paint run decorations
1468 // (so that the decorations are still visible even if the glyphs are partially hidden).
1469 if remaining_width <= 0. {
1470 should_stop_after_run = true;
1471 break;
1472 }
1473 remaining_width -= glyph.width;
1474 
1475 let glyph_origin = if is_start_clipping {
1476 line_origin + vec2f(remaining_width, glyph.position_along_baseline.y())
1477 } else {
1478 line_origin + glyph.position_along_baseline
1479 };
1480 
1481 scene
1482 .draw_glyph(
1483 glyph_origin,
1484 glyph.id,
1485 run.font_id,
1486 self.font_size,
1487 override_color.unwrap_or(glyph_color),
1488 )
1489 .with_fade(fade);
1490 
1491 // Draw the underline under the tag (e.g. for a hyperlink).
1492 if let Some(underline_color) = style_overrides
1493 .underline
1494 .get(&index)
1495 .copied()
1496 .or(run.styles.underline_color)
1497 {
1498 let scaled_underline_bottom_padding =
1499 UNDERLINE_BOTTOM_PADDING * (self.font_size / DEFAULT_FONT_SIZE);
1500 let underline_origin = line_origin
1501 + glyph.position_along_baseline
1502 + vec2f(0., scaled_underline_bottom_padding);
1503 
1504 scene
1505 .draw_rect_without_hit_recording(RectF::new(
1506 underline_origin,
1507 vec2f(glyph.width, UNDERLINE_THICKNESS),
1508 ))
1509 .with_background(Fill::Solid(underline_color));
1510 }
1511 }
1512 
1513 self.paint_run_decorations(
1514 glyph_color,
1515 run,
1516 line_origin,
1517 bounds,
1518 font_cache,
1519 scene,
1520 baseline_position_fn,
1521 );
1522 
1523 if should_stop_after_run {
1524 break;
1525 }
1526 }
1527 }
1528}
1529 
1530impl CaretPosition {
1531 /// The number of characters covered by this caret position.
1532 pub fn char_count(&self) -> usize {
1533 self.last_offset - self.start_offset + 1
1534 }
1535 
1536 /// Whether or not a given character offset is within this caret position.
1537 pub fn contains_index(&self, index: usize) -> bool {
1538 index >= self.start_offset && index <= self.last_offset
1539 }
1540}
1541 
1542#[cfg(test)]
1543#[path = "text_layout_test.rs"]
1544mod tests;
1545