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::fonts::{FONT_REF_LEN, FontRefs};
15use crate::schema::{Paint, TypeRole};
16use std::rc::Rc;
17
18/// Bytes of one span record: two ranges' worth of offsets, a role, flags, the text's paint,
19/// the background's paint and a handler. Four-byte aligned throughout, which is the
20/// protocol's invariant.
21pub const SPAN_LEN: usize = 36;
22
23/// Where each field of a span record starts. The Kotlin decoder is generated from these, so
24/// the two sides read the same bytes.
25pub const SPAN_START_AT: usize = 0;
26pub const SPAN_LENGTH_AT: usize = 4;
27pub const SPAN_ROLE_AT: usize = 8;
28pub const SPAN_FLAGS_AT: usize = 10;
29pub const SPAN_COLOR_AT: usize = 12;
30pub const SPAN_BACKGROUND_AT: usize = 20;
31pub const SPAN_HANDLER_AT: usize = 28;
32
33/// Bytes of one entry in a span font table: which run, then where its font list starts
34/// and how many candidates it holds.
35///
36/// The table is a blob of its own, beside the runs rather than inside their records, so a
37/// run record is the same 36 bytes whether or not anything names a font.
38pub const SPAN_FONT_ENTRY_LEN: usize = 12;
39pub const SPAN_FONT_SPAN_AT: usize = 0;
40pub const SPAN_FONT_OFFSET_AT: usize = 4;
41pub const SPAN_FONT_COUNT_AT: usize = 8;
42
43pub const FLAG_BOLD: u16 = 1;
44pub const FLAG_ITALIC: u16 = 2;
45pub const FLAG_UNDERLINE: u16 = 4;
46pub const FLAG_STRIKETHROUGH: u16 = 8;
47
48/// One run, given in bytes into the string rather than characters.
49///
50/// Bytes because that is what the string is measured in on both sides, and a character
51/// count would have to be recomputed against a UTF-8 buffer at every use.
52#[derive(Clone, Copy, Debug, Default, PartialEq)]
53pub struct TextSpan {
54    pub start: u32,
55    pub length: u32,
56    pub type_role: Option<TypeRole>,
57    pub color: Option<Paint>,
58    /// What is painted behind this run's letters, and only behind them.
59    ///
60    /// The word that actually changed inside a changed line is what this is for: the line
61    /// is the node's own background, and this marks the part of it that differs. A run
62    /// that wraps is painted on each line it reaches. There is no shape and no inset,
63    /// because nothing yet needs one, and when something does it is the design system's
64    /// to say.
65    pub background: Option<Paint>,
66    pub bold: bool,
67    pub italic: bool,
68    pub underline: bool,
69    pub strikethrough: bool,
70    /// The handler a press on this run reports to, if it is a link.
71    pub on_click: Option<u64>,
72}
73
74impl TextSpan {
75    /// A run of the given bytes with nothing said about it yet.
76    pub const fn new(start: u32, length: u32) -> Self {
77        Self {
78            start,
79            length,
80            type_role: None,
81            color: None,
82            background: None,
83            bold: false,
84            italic: false,
85            underline: false,
86            strikethrough: false,
87            on_click: None,
88        }
89    }
90
91    pub const fn bold(mut self) -> Self {
92        self.bold = true;
93        self
94    }
95
96    pub const fn italic(mut self) -> Self {
97        self.italic = true;
98        self
99    }
100
101    pub const fn underline(mut self) -> Self {
102        self.underline = true;
103        self
104    }
105
106    pub const fn strikethrough(mut self) -> Self {
107        self.strikethrough = true;
108        self
109    }
110
111    pub const fn with_role(mut self, role: TypeRole) -> Self {
112        self.type_role = Some(role);
113        self
114    }
115
116    pub const fn with_color(mut self, color: Paint) -> Self {
117        self.color = Some(color);
118        self
119    }
120
121    /// Paints behind this run. A role first, as everywhere else a colour is taken: the
122    /// diff's word backgrounds are roles, so they follow the scheme and the system.
123    pub const fn with_background(mut self, background: Paint) -> Self {
124        self.background = Some(background);
125        self
126    }
127
128    fn flags(&self) -> u16 {
129        let mut flags = 0;
130        if self.bold {
131            flags |= FLAG_BOLD;
132        }
133        if self.italic {
134            flags |= FLAG_ITALIC;
135        }
136        if self.underline {
137            flags |= FLAG_UNDERLINE;
138        }
139        if self.strikethrough {
140            flags |= FLAG_STRIKETHROUGH;
141        }
142        flags
143    }
144
145    fn write_into(&self, out: &mut Vec<u8>) {
146        out.extend_from_slice(&self.start.to_le_bytes());
147        out.extend_from_slice(&self.length.to_le_bytes());
148        out.extend_from_slice(&self.type_role.map_or(0, u16::from).to_le_bytes());
149        out.extend_from_slice(&self.flags().to_le_bytes());
150        // Zero is "none" for both paints, which no real paint encodes to: every kind
151        // has a non-zero tag in its high word.
152        let paint = self.color.map_or(0, Paint::to_bits);
153        out.extend_from_slice(&(paint as u32).to_le_bytes());
154        out.extend_from_slice(&((paint >> 32) as u32).to_le_bytes());
155        let background = self.background.map_or(0, Paint::to_bits);
156        out.extend_from_slice(&(background as u32).to_le_bytes());
157        out.extend_from_slice(&((background >> 32) as u32).to_le_bytes());
158        let handler = self.on_click.unwrap_or(0);
159        out.extend_from_slice(&(handler as u32).to_le_bytes());
160        out.extend_from_slice(&((handler >> 32) as u32).to_le_bytes());
161    }
162}
163
164/// What a marker turns its body into: the bytes that open and close it, and the span the
165/// enclosed run becomes.
166type Marked = (&'static [u8], fn(u32, u32) -> TextSpan);
167
168/// An encoded list of spans, ready to travel.
169///
170/// Shared rather than copied, for the same reason a draw list is: the same list redrawn
171/// every frame compares equal and produces no mutation at all.
172#[derive(Clone, Default, PartialEq)]
173pub struct TextSpans {
174    bytes: Option<Rc<[u8]>>,
175    /// The font table for runs that name one, which only text with no role reads.
176    fonts: Option<Rc<[u8]>>,
177}
178
179impl std::fmt::Debug for TextSpans {
180    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
181        formatter
182            .debug_struct("TextSpans")
183            .field("spans", &(self.as_bytes().len() / SPAN_LEN))
184            .field("fonts", &self.font_entry_count())
185            .finish()
186    }
187}
188
189impl TextSpans {
190    pub fn new(spans: impl IntoIterator<Item = TextSpan>) -> Self {
191        let mut bytes = Vec::new();
192        for span in spans {
193            span.write_into(&mut bytes);
194        }
195        Self::from_bytes(bytes)
196    }
197
198    /// Runs, each with the fonts it is set in where the text has no role.
199    ///
200    /// An empty list leaves that run in the text's own font. A run of text that has a
201    /// role is always in the theme's font, whatever it names here.
202    pub fn new_with_fonts(spans: impl IntoIterator<Item = (TextSpan, FontRefs)>) -> Self {
203        let mut bytes = Vec::new();
204        let mut named: Vec<(u32, FontRefs)> = Vec::new();
205        for (index, (span, fonts)) in spans.into_iter().enumerate() {
206            span.write_into(&mut bytes);
207            if !fonts.is_empty() {
208                named.push((index as u32, fonts));
209            }
210        }
211        let mut spans = Self::from_bytes(bytes);
212        spans.fonts = encode_font_table(&named);
213        spans
214    }
215
216    /// Adopts an already encoded list, which is what the protocol vectors carry.
217    pub fn from_bytes(bytes: impl Into<Rc<[u8]>>) -> Self {
218        let bytes = bytes.into();
219        Self {
220            bytes: (!bytes.is_empty()).then_some(bytes),
221            fonts: None,
222        }
223    }
224
225    /// The same runs with an already encoded font table beside them.
226    pub fn with_font_table(mut self, table: impl Into<Rc<[u8]>>) -> Self {
227        let table = table.into();
228        self.fonts = (!table.is_empty()).then_some(table);
229        self
230    }
231
232    pub fn as_bytes(&self) -> &[u8] {
233        self.bytes.as_deref().unwrap_or(&[])
234    }
235
236    /// The `SpanFonts` blob: a count, then one entry per run that names fonts, then the
237    /// font lists themselves. Every offset in it counts from the start of the blob.
238    pub fn font_table(&self) -> &[u8] {
239        self.fonts.as_deref().unwrap_or(&[])
240    }
241
242    pub fn is_empty(&self) -> bool {
243        self.bytes.is_none()
244    }
245
246    /// How many runs name fonts.
247    pub fn font_entry_count(&self) -> usize {
248        let table = self.font_table();
249        if table.len() < 4 {
250            return 0;
251        }
252        u32::from_le_bytes([table[0], table[1], table[2], table[3]]) as usize
253    }
254
255    /// The fonts each run that names any is set in, by the run's index.
256    pub fn span_fonts(&self) -> Vec<(u32, Vec<crate::fonts::FontRef>)> {
257        let table = self.font_table();
258        let word = |at: usize| -> Option<u32> {
259            let slice = table.get(at..at + 4)?;
260            Some(u32::from_le_bytes([slice[0], slice[1], slice[2], slice[3]]))
261        };
262        (0..self.font_entry_count())
263            .filter_map(|entry| {
264                let at = 4 + entry * SPAN_FONT_ENTRY_LEN;
265                let span = word(at + SPAN_FONT_SPAN_AT)?;
266                let offset = word(at + SPAN_FONT_OFFSET_AT)? as usize;
267                let count = word(at + SPAN_FONT_COUNT_AT)? as usize;
268                let refs = (0..count)
269                    .filter_map(|index| {
270                        crate::fonts::decode_record(table, offset + index * FONT_REF_LEN)
271                    })
272                    .collect();
273                Some((span, refs))
274            })
275            .collect()
276    }
277
278    /// The runs this list holds, decoded again.
279    pub fn spans(&self) -> impl Iterator<Item = TextSpan> + '_ {
280        self.as_bytes().chunks_exact(SPAN_LEN).map(|record| {
281            let word = |at: usize| {
282                u32::from_le_bytes([record[at], record[at + 1], record[at + 2], record[at + 3]])
283            };
284            let half = |at: usize| u16::from_le_bytes([record[at], record[at + 1]]);
285            let flags = half(SPAN_FLAGS_AT);
286            let paint = u64::from(word(SPAN_COLOR_AT)) | (u64::from(word(SPAN_COLOR_AT + 4)) << 32);
287            let background = u64::from(word(SPAN_BACKGROUND_AT))
288                | (u64::from(word(SPAN_BACKGROUND_AT + 4)) << 32);
289            let handler =
290                u64::from(word(SPAN_HANDLER_AT)) | (u64::from(word(SPAN_HANDLER_AT + 4)) << 32);
291            TextSpan {
292                start: word(SPAN_START_AT),
293                length: word(SPAN_LENGTH_AT),
294                type_role: TypeRole::try_from(half(SPAN_ROLE_AT)).ok(),
295                color: Paint::from_bits(paint),
296                background: Paint::from_bits(background),
297                bold: flags & FLAG_BOLD != 0,
298                italic: flags & FLAG_ITALIC != 0,
299                underline: flags & FLAG_UNDERLINE != 0,
300                strikethrough: flags & FLAG_STRIKETHROUGH != 0,
301                on_click: (handler != 0).then_some(handler),
302            }
303        })
304    }
305
306    /// Turns a small, closed subset of markdown into a plain string and its spans.
307    ///
308    /// `**bold**`, `*italic*` and `` `code` ``, and nothing else. It is a convenience, not
309    /// a parser: anything it does not recognise is left in the string exactly as written,
310    /// because silently eating a character a reader typed is worse than showing it.
311    ///
312    /// Links are not here. A link needs a handler, and a handler is something the screen
313    /// has rather than something a string can name.
314    pub fn from_markdown(source: &str) -> (String, Self) {
315        let mut text = String::with_capacity(source.len());
316        let mut spans = Vec::new();
317        let bytes = source.as_bytes();
318        let mut at = 0;
319        while at < bytes.len() {
320            let (marker, span_of): Marked = if bytes[at..].starts_with(b"**") {
321                (b"**", |start, length| TextSpan::new(start, length).bold())
322            } else if bytes[at] == b'*' {
323                (b"*", |start, length| TextSpan::new(start, length).italic())
324            } else if bytes[at] == b'`' {
325                (b"`", |start, length| {
326                    TextSpan::new(start, length).with_role(TypeRole::Mono)
327                })
328            } else {
329                text.push(source[at..].chars().next().expect("a char boundary"));
330                at += source[at..].chars().next().map_or(1, char::len_utf8);
331                continue;
332            };
333            let body = at + marker.len();
334            let Some(end) = find(&bytes[body..], marker).map(|offset| body + offset) else {
335                // No closing marker, so this is not a marker at all.
336                text.push_str(&source[at..body]);
337                at = body;
338                continue;
339            };
340            let start = text.len() as u32;
341            text.push_str(&source[body..end]);
342            spans.push(span_of(start, text.len() as u32 - start));
343            at = end + marker.len();
344        }
345        (text, Self::new(spans))
346    }
347}
348
349/// Lays out a span font table: the count, one entry per run, and after them each run's
350/// font records, each followed by the names its records point at.
351fn encode_font_table(named: &[(u32, FontRefs)]) -> Option<Rc<[u8]>> {
352    if named.is_empty() {
353        return None;
354    }
355    let mut entries = Vec::with_capacity(named.len() * SPAN_FONT_ENTRY_LEN);
356    let mut lists: Vec<u8> = Vec::new();
357    let lists_at = 4 + named.len() * SPAN_FONT_ENTRY_LEN;
358    for (span, fonts) in named {
359        let offset = (lists_at + lists.len()) as u32;
360        let count = fonts.len() as u32;
361        entries.extend_from_slice(&span.to_le_bytes());
362        entries.extend_from_slice(&offset.to_le_bytes());
363        entries.extend_from_slice(&count.to_le_bytes());
364        let names_at = offset + count * FONT_REF_LEN as u32;
365        fonts.write_relocated(&mut lists, names_at);
366    }
367    let mut table = Vec::with_capacity(lists_at + lists.len());
368    table.extend_from_slice(&(named.len() as u32).to_le_bytes());
369    table.extend_from_slice(&entries);
370    table.extend_from_slice(&lists);
371    Some(table.into())
372}
373
374fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
375    haystack
376        .windows(needle.len())
377        .position(|window| window == needle)
378}