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, a paint and a
18/// handler. Four-byte aligned throughout, which is the protocol's invariant.
19pub const SPAN_LEN: usize = 28;
20
21const FLAG_BOLD: u16 = 1;
22const FLAG_ITALIC: u16 = 2;
23const FLAG_UNDERLINE: u16 = 4;
24const FLAG_STRIKETHROUGH: u16 = 8;
25
26/// One run, given in bytes into the string rather than characters.
27///
28/// Bytes because that is what the string is measured in on both sides, and a character
29/// count would have to be recomputed against a UTF-8 buffer at every use.
30#[derive(Clone, Copy, Debug, Default, PartialEq)]
31pub struct TextSpan {
32    pub start: u32,
33    pub length: u32,
34    pub type_role: Option<TypeRole>,
35    pub color: Option<Paint>,
36    pub bold: bool,
37    pub italic: bool,
38    pub underline: bool,
39    pub strikethrough: bool,
40    /// The handler a press on this run reports to, if it is a link.
41    pub on_click: Option<u64>,
42}
43
44impl TextSpan {
45    /// A run of the given bytes with nothing said about it yet.
46    pub const fn new(start: u32, length: u32) -> Self {
47        Self {
48            start,
49            length,
50            type_role: None,
51            color: None,
52            bold: false,
53            italic: false,
54            underline: false,
55            strikethrough: false,
56            on_click: None,
57        }
58    }
59
60    pub const fn bold(mut self) -> Self {
61        self.bold = true;
62        self
63    }
64
65    pub const fn italic(mut self) -> Self {
66        self.italic = true;
67        self
68    }
69
70    pub const fn underline(mut self) -> Self {
71        self.underline = true;
72        self
73    }
74
75    pub const fn strikethrough(mut self) -> Self {
76        self.strikethrough = true;
77        self
78    }
79
80    pub const fn with_role(mut self, role: TypeRole) -> Self {
81        self.type_role = Some(role);
82        self
83    }
84
85    pub const fn with_color(mut self, color: Paint) -> Self {
86        self.color = Some(color);
87        self
88    }
89
90    fn flags(&self) -> u16 {
91        let mut flags = 0;
92        if self.bold {
93            flags |= FLAG_BOLD;
94        }
95        if self.italic {
96            flags |= FLAG_ITALIC;
97        }
98        if self.underline {
99            flags |= FLAG_UNDERLINE;
100        }
101        if self.strikethrough {
102            flags |= FLAG_STRIKETHROUGH;
103        }
104        flags
105    }
106
107    fn write_into(&self, out: &mut Vec<u8>) {
108        out.extend_from_slice(&self.start.to_le_bytes());
109        out.extend_from_slice(&self.length.to_le_bytes());
110        out.extend_from_slice(&self.type_role.map_or(0, u16::from).to_le_bytes());
111        out.extend_from_slice(&self.flags().to_le_bytes());
112        let paint = self.color.map_or(0, Paint::to_bits);
113        out.extend_from_slice(&(paint as u32).to_le_bytes());
114        out.extend_from_slice(&((paint >> 32) as u32).to_le_bytes());
115        let handler = self.on_click.unwrap_or(0);
116        out.extend_from_slice(&(handler as u32).to_le_bytes());
117        out.extend_from_slice(&((handler >> 32) as u32).to_le_bytes());
118    }
119}
120
121/// What a marker turns its body into: the bytes that open and close it, and the span the
122/// enclosed run becomes.
123type Marked = (&'static [u8], fn(u32, u32) -> TextSpan);
124
125/// An encoded list of spans, ready to travel.
126///
127/// Shared rather than copied, for the same reason a draw list is: the same list redrawn
128/// every frame compares equal and produces no mutation at all.
129#[derive(Clone, Default, PartialEq)]
130pub struct TextSpans {
131    bytes: Option<Rc<[u8]>>,
132}
133
134impl std::fmt::Debug for TextSpans {
135    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136        formatter
137            .debug_struct("TextSpans")
138            .field("spans", &(self.as_bytes().len() / SPAN_LEN))
139            .finish()
140    }
141}
142
143impl TextSpans {
144    pub fn new(spans: impl IntoIterator<Item = TextSpan>) -> Self {
145        let mut bytes = Vec::new();
146        for span in spans {
147            span.write_into(&mut bytes);
148        }
149        Self::from_bytes(bytes)
150    }
151
152    /// Adopts an already encoded list, which is what the protocol vectors carry.
153    pub fn from_bytes(bytes: impl Into<Rc<[u8]>>) -> Self {
154        let bytes = bytes.into();
155        Self {
156            bytes: (!bytes.is_empty()).then_some(bytes),
157        }
158    }
159
160    pub fn as_bytes(&self) -> &[u8] {
161        self.bytes.as_deref().unwrap_or(&[])
162    }
163
164    pub fn is_empty(&self) -> bool {
165        self.bytes.is_none()
166    }
167
168    /// The runs this list holds, decoded again.
169    pub fn spans(&self) -> impl Iterator<Item = TextSpan> + '_ {
170        self.as_bytes().chunks_exact(SPAN_LEN).map(|record| {
171            let word = |at: usize| {
172                u32::from_le_bytes([record[at], record[at + 1], record[at + 2], record[at + 3]])
173            };
174            let half = |at: usize| u16::from_le_bytes([record[at], record[at + 1]]);
175            let flags = half(10);
176            let paint = u64::from(word(12)) | (u64::from(word(16)) << 32);
177            let handler = u64::from(word(20)) | (u64::from(word(24)) << 32);
178            TextSpan {
179                start: word(0),
180                length: word(4),
181                type_role: TypeRole::try_from(half(8)).ok(),
182                color: Paint::from_bits(paint),
183                bold: flags & FLAG_BOLD != 0,
184                italic: flags & FLAG_ITALIC != 0,
185                underline: flags & FLAG_UNDERLINE != 0,
186                strikethrough: flags & FLAG_STRIKETHROUGH != 0,
187                on_click: (handler != 0).then_some(handler),
188            }
189        })
190    }
191
192    /// Turns a small, closed subset of markdown into a plain string and its spans.
193    ///
194    /// `**bold**`, `*italic*` and `` `code` ``, and nothing else. It is a convenience, not
195    /// a parser: anything it does not recognise is left in the string exactly as written,
196    /// because silently eating a character a reader typed is worse than showing it.
197    ///
198    /// Links are not here. A link needs a handler, and a handler is something the screen
199    /// has rather than something a string can name.
200    pub fn from_markdown(source: &str) -> (String, Self) {
201        let mut text = String::with_capacity(source.len());
202        let mut spans = Vec::new();
203        let bytes = source.as_bytes();
204        let mut at = 0;
205        while at < bytes.len() {
206            let (marker, span_of): Marked = if bytes[at..].starts_with(b"**") {
207                (b"**", |start, length| TextSpan::new(start, length).bold())
208            } else if bytes[at] == b'*' {
209                (b"*", |start, length| TextSpan::new(start, length).italic())
210            } else if bytes[at] == b'`' {
211                (b"`", |start, length| {
212                    TextSpan::new(start, length).with_role(TypeRole::Mono)
213                })
214            } else {
215                text.push(source[at..].chars().next().expect("a char boundary"));
216                at += source[at..].chars().next().map_or(1, char::len_utf8);
217                continue;
218            };
219            let body = at + marker.len();
220            let Some(end) = find(&bytes[body..], marker).map(|offset| body + offset) else {
221                // No closing marker, so this is not a marker at all.
222                text.push_str(&source[at..body]);
223                at = body;
224                continue;
225            };
226            let start = text.len() as u32;
227            text.push_str(&source[body..end]);
228            spans.push(span_of(start, text.len() as u32 - start));
229            at = end + marker.len();
230        }
231        (text, Self::new(spans))
232    }
233}
234
235fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
236    haystack
237        .windows(needle.len())
238        .position(|window| window == needle)
239}
240
241impl dioxus_core::IntoAttributeValue for TextSpans {
242    fn into_value(self) -> dioxus_core::AttributeValue {
243        dioxus_core::AttributeValue::any_value(self)
244    }
245}