Skip to main content

compose_rust/
spans.rs

1//! Runs of different treatment inside one `Text`.
2//!
3//! Not a widget. A paragraph with a bold phrase and a link in it is one piece of text, and
4//! splitting it into three widgets to say so would break line wrapping at the seams: the
5//! renderer would wrap each piece on its own and the phrase would never share a line with
6//! what surrounds it.
7//!
8//! So the spans travel beside the string, as a blob of fixed-length records in the same
9//! shape a `Canvas` carries its commands. Nothing new crosses the boundary to do it.
10//!
11//! Markdown is a convenience on this side. [`TextSpans::from_markdown`] turns a small,
12//! closed subset into spans, and the Renderer never hears the word.
13
14use crate::schema::{Paint, TypeRole};
15use std::rc::Rc;
16
17/// Bytes of one span record: two ranges' worth of offsets, a role, flags, the text's paint,
18/// the background's paint and a handler. Four-byte aligned throughout, which is the
19/// protocol's invariant.
20pub const SPAN_LEN: usize = 36;
21
22/// Where each field of a span record starts. The Kotlin decoder is generated from these, so
23/// the two sides read the same bytes.
24pub const SPAN_START_AT: usize = 0;
25pub const SPAN_LENGTH_AT: usize = 4;
26pub const SPAN_ROLE_AT: usize = 8;
27pub const SPAN_FLAGS_AT: usize = 10;
28pub const SPAN_COLOR_AT: usize = 12;
29pub const SPAN_BACKGROUND_AT: usize = 20;
30pub const SPAN_HANDLER_AT: usize = 28;
31
32pub const FLAG_BOLD: u16 = 1;
33pub const FLAG_ITALIC: u16 = 2;
34pub const FLAG_UNDERLINE: u16 = 4;
35pub const FLAG_STRIKETHROUGH: u16 = 8;
36
37/// One run, given in bytes into the string rather than characters.
38///
39/// Bytes because that is what the string is measured in on both sides, and a character
40/// count would have to be recomputed against a UTF-8 buffer at every use.
41#[derive(Clone, Copy, Debug, Default, PartialEq)]
42pub struct TextSpan {
43    pub start: u32,
44    pub length: u32,
45    pub type_role: Option<TypeRole>,
46    pub color: Option<Paint>,
47    /// What is painted behind this run's letters, and only behind them.
48    ///
49    /// The word that actually changed inside a changed line is what this is for: the line
50    /// is the node's own background, and this marks the part of it that differs. A run
51    /// that wraps is painted on each line it reaches. There is no shape and no inset,
52    /// because nothing yet needs one, and when something does it is the design system's
53    /// to say.
54    pub background: Option<Paint>,
55    pub bold: bool,
56    pub italic: bool,
57    pub underline: bool,
58    pub strikethrough: bool,
59    /// The handler a press on this run reports to, if it is a link.
60    pub on_click: Option<u64>,
61}
62
63impl TextSpan {
64    /// A run of the given bytes with nothing said about it yet.
65    pub const fn new(start: u32, length: u32) -> Self {
66        Self {
67            start,
68            length,
69            type_role: None,
70            color: None,
71            background: None,
72            bold: false,
73            italic: false,
74            underline: false,
75            strikethrough: false,
76            on_click: None,
77        }
78    }
79
80    pub const fn bold(mut self) -> Self {
81        self.bold = true;
82        self
83    }
84
85    pub const fn italic(mut self) -> Self {
86        self.italic = true;
87        self
88    }
89
90    pub const fn underline(mut self) -> Self {
91        self.underline = true;
92        self
93    }
94
95    pub const fn strikethrough(mut self) -> Self {
96        self.strikethrough = true;
97        self
98    }
99
100    pub const fn with_role(mut self, role: TypeRole) -> Self {
101        self.type_role = Some(role);
102        self
103    }
104
105    pub const fn with_color(mut self, color: Paint) -> Self {
106        self.color = Some(color);
107        self
108    }
109
110    /// Paints behind this run. A role first, as everywhere else a colour is taken: the
111    /// diff's word backgrounds are roles, so they follow the scheme and the system.
112    pub const fn with_background(mut self, background: Paint) -> Self {
113        self.background = Some(background);
114        self
115    }
116
117    fn flags(&self) -> u16 {
118        let mut flags = 0;
119        if self.bold {
120            flags |= FLAG_BOLD;
121        }
122        if self.italic {
123            flags |= FLAG_ITALIC;
124        }
125        if self.underline {
126            flags |= FLAG_UNDERLINE;
127        }
128        if self.strikethrough {
129            flags |= FLAG_STRIKETHROUGH;
130        }
131        flags
132    }
133
134    fn write_into(&self, out: &mut Vec<u8>) {
135        out.extend_from_slice(&self.start.to_le_bytes());
136        out.extend_from_slice(&self.length.to_le_bytes());
137        out.extend_from_slice(&self.type_role.map_or(0, u16::from).to_le_bytes());
138        out.extend_from_slice(&self.flags().to_le_bytes());
139        // Zero is "none" for both paints, which no real paint encodes to: every kind
140        // has a non-zero tag in its high word.
141        let paint = self.color.map_or(0, Paint::to_bits);
142        out.extend_from_slice(&(paint as u32).to_le_bytes());
143        out.extend_from_slice(&((paint >> 32) as u32).to_le_bytes());
144        let background = self.background.map_or(0, Paint::to_bits);
145        out.extend_from_slice(&(background as u32).to_le_bytes());
146        out.extend_from_slice(&((background >> 32) as u32).to_le_bytes());
147        let handler = self.on_click.unwrap_or(0);
148        out.extend_from_slice(&(handler as u32).to_le_bytes());
149        out.extend_from_slice(&((handler >> 32) as u32).to_le_bytes());
150    }
151}
152
153/// What a marker turns its body into: the bytes that open and close it, and the span the
154/// enclosed run becomes.
155type Marked = (&'static [u8], fn(u32, u32) -> TextSpan);
156
157/// An encoded list of spans, ready to travel.
158///
159/// Shared rather than copied, for the same reason a draw list is: the same list redrawn
160/// every frame compares equal and produces no mutation at all.
161#[derive(Clone, Default, PartialEq)]
162pub struct TextSpans {
163    bytes: Option<Rc<[u8]>>,
164}
165
166impl std::fmt::Debug for TextSpans {
167    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168        formatter
169            .debug_struct("TextSpans")
170            .field("spans", &(self.as_bytes().len() / SPAN_LEN))
171            .finish()
172    }
173}
174
175impl TextSpans {
176    pub fn new(spans: impl IntoIterator<Item = TextSpan>) -> Self {
177        let mut bytes = Vec::new();
178        for span in spans {
179            span.write_into(&mut bytes);
180        }
181        Self::from_bytes(bytes)
182    }
183
184    /// Adopts an already encoded list, which is what the protocol vectors carry.
185    pub fn from_bytes(bytes: impl Into<Rc<[u8]>>) -> Self {
186        let bytes = bytes.into();
187        Self {
188            bytes: (!bytes.is_empty()).then_some(bytes),
189        }
190    }
191
192    pub fn as_bytes(&self) -> &[u8] {
193        self.bytes.as_deref().unwrap_or(&[])
194    }
195
196    pub fn is_empty(&self) -> bool {
197        self.bytes.is_none()
198    }
199
200    /// The runs this list holds, decoded again.
201    pub fn spans(&self) -> impl Iterator<Item = TextSpan> + '_ {
202        self.as_bytes().chunks_exact(SPAN_LEN).map(|record| {
203            let word = |at: usize| {
204                u32::from_le_bytes([record[at], record[at + 1], record[at + 2], record[at + 3]])
205            };
206            let half = |at: usize| u16::from_le_bytes([record[at], record[at + 1]]);
207            let flags = half(SPAN_FLAGS_AT);
208            let paint = u64::from(word(SPAN_COLOR_AT)) | (u64::from(word(SPAN_COLOR_AT + 4)) << 32);
209            let background = u64::from(word(SPAN_BACKGROUND_AT))
210                | (u64::from(word(SPAN_BACKGROUND_AT + 4)) << 32);
211            let handler =
212                u64::from(word(SPAN_HANDLER_AT)) | (u64::from(word(SPAN_HANDLER_AT + 4)) << 32);
213            TextSpan {
214                start: word(SPAN_START_AT),
215                length: word(SPAN_LENGTH_AT),
216                type_role: TypeRole::try_from(half(SPAN_ROLE_AT)).ok(),
217                color: Paint::from_bits(paint),
218                background: Paint::from_bits(background),
219                bold: flags & FLAG_BOLD != 0,
220                italic: flags & FLAG_ITALIC != 0,
221                underline: flags & FLAG_UNDERLINE != 0,
222                strikethrough: flags & FLAG_STRIKETHROUGH != 0,
223                on_click: (handler != 0).then_some(handler),
224            }
225        })
226    }
227
228    /// Turns a small, closed subset of markdown into a plain string and its spans.
229    ///
230    /// `**bold**`, `*italic*` and `` `code` ``, and nothing else. It is a convenience, not
231    /// a parser: anything it does not recognise is left in the string exactly as written,
232    /// because silently eating a character a reader typed is worse than showing it.
233    ///
234    /// Links are not here. A link needs a handler, and a handler is something the screen
235    /// has rather than something a string can name.
236    pub fn from_markdown(source: &str) -> (String, Self) {
237        let mut text = String::with_capacity(source.len());
238        let mut spans = Vec::new();
239        let bytes = source.as_bytes();
240        let mut at = 0;
241        while at < bytes.len() {
242            let (marker, span_of): Marked = if bytes[at..].starts_with(b"**") {
243                (b"**", |start, length| TextSpan::new(start, length).bold())
244            } else if bytes[at] == b'*' {
245                (b"*", |start, length| TextSpan::new(start, length).italic())
246            } else if bytes[at] == b'`' {
247                (b"`", |start, length| {
248                    TextSpan::new(start, length).with_role(TypeRole::Mono)
249                })
250            } else {
251                text.push(source[at..].chars().next().expect("a char boundary"));
252                at += source[at..].chars().next().map_or(1, char::len_utf8);
253                continue;
254            };
255            let body = at + marker.len();
256            let Some(end) = find(&bytes[body..], marker).map(|offset| body + offset) else {
257                // No closing marker, so this is not a marker at all.
258                text.push_str(&source[at..body]);
259                at = body;
260                continue;
261            };
262            let start = text.len() as u32;
263            text.push_str(&source[body..end]);
264            spans.push(span_of(start, text.len() as u32 - start));
265            at = end + marker.len();
266        }
267        (text, Self::new(spans))
268    }
269}
270
271fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
272    haystack
273        .windows(needle.len())
274        .position(|window| window == needle)
275}