gpui/text_system/paragraph.rs
1//! Optional paragraph interop. Layout providers retain their own interaction data.
2use crate::{Bounds, FontRun, Hsla, Pixels, Point, Result, ShapedRun, Size, Window};
3use std::sync::Arc;
4
5/// Side of a logical boundary at a wrap or bidirectional transition.
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7pub enum TextAffinity {
8 /// Position associated with the preceding logical cluster.
9 Upstream,
10 /// Position associated with the following logical cluster.
11 Downstream,
12}
13
14/// Paragraph-relative UTF-8 position; providers validate and snap cluster edges.
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub struct ParagraphPosition {
17 /// Logical byte offset in the original source.
18 pub byte_index: usize,
19 /// Visual side of this logical boundary.
20 pub affinity: TextAffinity,
21}
22
23/// Physical horizontal movement, independent of logical text direction.
24#[derive(Clone, Copy, Debug)]
25pub enum VisualDirection {
26 /// Move left in visual order.
27 Left,
28 /// Move right in visual order.
29 Right,
30}
31
32/// Immutable shaping result. Reflow must not mutate previously published layouts.
33pub trait PlatformParagraphShape: Send + Sync {
34 /// Break at a positive finite logical width, with automatic direction/start alignment.
35 fn layout(&self, width: Pixels) -> Result<Arc<dyn PlatformParagraphLayout>>;
36}
37
38/// Immutable paragraph paint and interaction data, all in paragraph-local pixels.
39///
40/// Glyph positions include their line baseline, measured from the paragraph top.
41/// Glyph indices remain logical UTF-8 offsets even when paint order is visual.
42pub trait PlatformParagraphLayout: Send + Sync {
43 /// Original unmodified source.
44 fn source(&self) -> &str;
45 /// Logical font size shared by every run in this initial interface.
46 fn font_size(&self) -> Pixels;
47 /// Paragraph layout size, including its allocated wrapping width.
48 fn size(&self) -> Size<Pixels>;
49 /// Resolved faces/glyphs in visual paint order; never requested descriptor IDs.
50 fn runs(&self) -> &[ShapedRun];
51 /// Find a logical boundary at a local point.
52 fn hit_test(&self, point: Point<Pixels>) -> Result<ParagraphPosition>;
53 /// A one-pixel caret; invalid positions return an error.
54 fn caret_bounds(&self, position: ParagraphPosition) -> Result<Bounds<Pixels>>;
55 /// Move to a neighboring visual cluster boundary.
56 fn move_visual(
57 &self,
58 position: ParagraphPosition,
59 direction: VisualDirection,
60 ) -> Result<ParagraphPosition>;
61 /// Possibly discontiguous selection rectangles.
62 fn selection_bounds(
63 &self,
64 anchor: ParagraphPosition,
65 focus: ParagraphPosition,
66 ) -> Result<Vec<Bounds<Pixels>>>;
67}
68
69/// Paint visual glyph runs through the normal bounds cache and sprite atlas.
70///
71/// Call during element paint. `color_for_index` resolves color by logical source
72/// index (not a forward-only style iterator). Backgrounds, selection and decoration
73/// geometry are caller-owned. No font resolution or text shaping occurs here.
74pub fn paint_paragraph(
75 layout: &dyn PlatformParagraphLayout,
76 origin: Point<Pixels>,
77 color_for_index: impl Fn(usize) -> Hsla,
78 window: &mut Window,
79) -> Result<()> {
80 for run in layout.runs() {
81 for glyph in &run.glyphs {
82 let position = origin + glyph.position;
83 if glyph.is_emoji {
84 window.paint_emoji(position, run.font_id, glyph.id, layout.font_size())?;
85 } else {
86 window.paint_glyph(
87 position,
88 run.font_id,
89 glyph.id,
90 layout.font_size(),
91 color_for_index(glyph.index),
92 )?;
93 }
94 }
95 }
96 Ok(())
97}
98
99// FontRun is intentionally the same descriptor identity boundary as flat lines.
100/// Borrowed shaping request for one logical paragraph. Direction is inferred and
101/// alignment is start; future shaping options must also be added to cache keys.
102pub struct ParagraphRequest<'a> {
103 /// Original paragraph UTF-8 source.
104 pub text: &'a str,
105 /// Shared logical font size.
106 pub font_size: Pixels,
107 /// Ordered runs covering source bytes, or empty for the backend default.
108 pub runs: &'a [FontRun],
109}