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/mod.rs
1use crate::text_offsets::{ByteOffset, CharCounter, CharOffset};
2use anyhow::{anyhow, Result};
3use itertools::Itertools;
4 
5use crate::event::ModifiersState;
6 
7use self::point::Point;
8 
9use self::word_boundaries::WordBoundaries;
10 
11pub mod header;
12pub mod point;
13pub mod word_boundaries;
14pub mod words;
15 
16pub use header::BlockHeaderSize;
17 
18#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
19pub enum SelectionType {
20 #[default]
21 Simple,
22 Semantic,
23 Lines,
24 Rect,
25}
26 
27impl SelectionType {
28 pub fn from_click_count(click_count: u32) -> Self {
29 match click_count {
30 0 => SelectionType::Simple,
31 1 => SelectionType::Simple,
32 2 => SelectionType::Semantic,
33 3 => SelectionType::Lines,
34 _ => SelectionType::Lines,
35 }
36 }
37 
38 pub fn from_mouse_event(modifiers: ModifiersState, click_count: u32) -> Self {
39 let is_rect = if cfg!(target_os = "macos") {
40 modifiers.cmd && modifiers.alt
41 } else {
42 modifiers.ctrl && modifiers.alt
43 };
44 
45 if is_rect {
46 return SelectionType::Rect;
47 }
48 
49 SelectionType::from_click_count(click_count)
50 }
51}
52 
53impl From<SelectionType> for IsRect {
54 fn from(selection_type: SelectionType) -> Self {
55 match selection_type {
56 SelectionType::Rect => IsRect::True,
57 _ => IsRect::False,
58 }
59 }
60}
61 
62#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)]
63pub enum IsRect {
64 True,
65 #[default]
66 False,
67}
68 
69#[derive(Copy, Clone, Debug, Default)]
70pub enum SelectionDirection {
71 #[default]
72 Forward,
73 Backward,
74}
75 
76/// A buffer of text characters. This trait acts as a base layer to implement text segmentation
77/// on top of. Currently, it supports word navigation.
78pub trait TextBuffer {
79 type Chars<'a>: Iterator<Item = char> + 'a
80 where
81 Self: 'a;
82 
83 type CharsReverse<'a>: Iterator<Item = char> + 'a
84 where
85 Self: 'a;
86 
87 /// Begin iterating over the characters at `offset`, continuing to the end of the buffer.
88 ///
89 /// The maximum valid `offset` is the length of the buffer (not 1 less than the length). This
90 /// allows starting just after the last character.
91 fn chars_at(&self, offset: CharOffset) -> Result<Self::Chars<'_>>;
92 
93 /// Begin iterating backwards over the characters from `offset` to the start of the buffer.
94 ///
95 /// Note that this is _different_ from the semantics of `Iterator::rev`, which would instead
96 /// start at the very end of the buffer.
97 ///
98 /// The maximum valid `offset` is the length of the buffer (not 1 less than the length). This
99 /// allows starting just after the last character.
100 fn chars_rev_at(&self, offset: CharOffset) -> Result<Self::CharsReverse<'_>>;
101 
102 /// Converts a character offset to a buffer [`Point`], if it is in bounds.
103 fn to_point(&self, offset: CharOffset) -> Result<Point>;
104 
105 /// Convert a point to its offset within the buffer.
106 fn to_offset(&self, point: Point) -> Result<CharOffset>;
107 
108 /// Get an iterator of word starting points forward from the given offset
109 fn word_starts_from_offset<T: BufferIndex>(
110 &self,
111 position: T,
112 ) -> Result<WordBoundaries<'_, Self>> {
113 let offset = position.to_char_offset(self)?;
114 Ok(WordBoundaries::forward_starts(
115 offset,
116 self.chars_at(offset)?,
117 self,
118 ))
119 }
120 
121 /// Get an iterator of word ending points forward from the given offset, excluding the current
122 /// location if it is a word boundary.
123 ///
124 /// Example: For a buffer of "word one two three", with an offset of `4` (immediately after
125 /// the 'word'), this will yield columns [8, 12, 18], the ends of `one`, `two`, and `three`,
126 /// but _excluding_ the initial position at the end of `word`.
127 fn word_ends_from_offset_exclusive<T: BufferIndex>(
128 &self,
129 position: T,
130 ) -> Result<WordBoundaries<'_, Self>> {
131 let offset = position.to_char_offset(self)?;
132 Ok(WordBoundaries::forward_ends_exclusive(
133 offset,
134 self.chars_at(offset)?,
135 self,
136 ))
137 }
138 
139 /// Get an iterator of word ending points forward from the given offset, including the current
140 /// location if appropriate.
141 ///
142 /// Example: For a buffer of "word one two three", with an offset of `4` (immediately after
143 /// the 'word'), this will yield columns [4, 8, 12, 18], the ends of all four words,
144 /// _including_ the initial position at the end of `word`.
145 fn word_ends_from_offset_inclusive<T: BufferIndex>(
146 &self,
147 position: T,
148 ) -> Result<WordBoundaries<'_, Self>> {
149 let offset = position.to_char_offset(self)?;
150 Ok(WordBoundaries::forward_ends_inclusive(
151 offset,
152 self.chars_at(offset)?,
153 self,
154 ))
155 }
156 
157 /// Get an iterator of word starting points backwards from the given offset, excluding the
158 /// current location if it is a word boundary.
159 ///
160 /// Example: For a buffer of "word one two three", with an offset of `13` (immediately before
161 /// the 'three'), this will yield columns [9, 5, 0], the starts of `two`, `one`, and `word`,
162 /// but _excluding_ the initial position at the start of `three`.
163 fn word_starts_backward_from_offset_exclusive<T: BufferIndex>(
164 &self,
165 position: T,
166 ) -> Result<WordBoundaries<'_, Self>> {
167 let offset = position.to_char_offset(self)?;
168 Ok(WordBoundaries::backward_starts_exclusive(
169 offset,
170 self.chars_rev_at(offset)?,
171 self,
172 ))
173 }
174 
175 /// Get an iterator of word starting points backwards from the given offset, including the
176 /// current location if appropriate.
177 ///
178 /// Example: For a buffer of "word one two three", with an offset of `13` (immediately before
179 /// the 'three'), this will yield columns [13, 9, 5, 0], the starts of all four words,
180 /// _including_ the initial position at the start of `three`.
181 fn word_starts_backward_from_offset_inclusive<T: BufferIndex>(
182 &self,
183 position: T,
184 ) -> Result<WordBoundaries<'_, Self>> {
185 let offset = position.to_char_offset(self)?;
186 Ok(WordBoundaries::backward_starts_inclusive(
187 offset,
188 self.chars_rev_at(offset)?,
189 self,
190 ))
191 }
192}
193 
194/// A type which can index into a text buffer.
195pub trait BufferIndex {
196 fn to_char_offset<B: TextBuffer + ?Sized>(&self, buffer: &B) -> Result<CharOffset>;
197}
198 
199impl BufferIndex for CharOffset {
200 fn to_char_offset<B: TextBuffer + ?Sized>(&self, _: &B) -> Result<CharOffset> {
201 Ok(*self)
202 }
203}
204 
205impl BufferIndex for Point {
206 fn to_char_offset<B: TextBuffer + ?Sized>(&self, buffer: &B) -> Result<CharOffset> {
207 buffer.to_offset(*self)
208 }
209}
210 
211impl TextBuffer for str {
212 type Chars<'a> = std::str::Chars<'a>;
213 type CharsReverse<'a> = std::iter::Rev<std::str::Chars<'a>>;
214 
215 fn chars_at(&self, offset: CharOffset) -> Result<Self::Chars<'_>> {
216 let chars = self.chars().count();
217 if offset.as_usize() <= chars {
218 Ok(self.chars().dropping(offset.as_usize()))
219 } else {
220 Err(anyhow!(
221 "Offset {offset} out of bounds; char length is {chars}"
222 ))
223 }
224 }
225 
226 fn chars_rev_at(&self, offset: CharOffset) -> Result<Self::CharsReverse<'_>> {
227 let chars = self.chars().count();
228 if offset.as_usize() <= chars {
229 Ok(self.chars().rev().dropping(chars - offset.as_usize()))
230 } else {
231 Err(anyhow!(
232 "Offset {offset} out of bounds; char length is {chars}"
233 ))
234 }
235 }
236 
237 fn to_point(&self, offset: CharOffset) -> Result<Point> {
238 let chars = self.chars().count();
239 if offset.as_usize() <= chars {
240 Ok(Point::new(0, offset.as_usize() as u32))
241 } else {
242 Err(anyhow!(
243 "Offset {offset} out of bounds; char length is {chars}"
244 ))
245 }
246 }
247 
248 fn to_offset(&self, point: Point) -> Result<CharOffset> {
249 if point.row == 0 {
250 let chars = self.chars().count();
251 if (point.column as usize) <= chars {
252 Ok(CharOffset::from(point.column as usize))
253 } else {
254 Err(anyhow!(
255 "Column {} out of bounds; char length is {chars}",
256 point.column
257 ))
258 }
259 } else {
260 Err(anyhow!(
261 "Row {} out of bounds; str only has 1 row",
262 point.row
263 ))
264 }
265 }
266}
267 
268/// Convert a slice of text into a `Vec` of UTF-8 bytes.
269pub fn str_to_byte_vec(text: &str) -> Vec<u8> {
270 text.as_bytes().iter().cloned().collect_vec()
271}
272 
273/// Slice a string by [`char`] offsets, rather than byte offsets.
274///
275/// The starting index is inclusive, while the ending index is exclusive.
276pub fn char_slice(s: &str, start: usize, end: usize) -> Option<&str> {
277 if end < start {
278 return None;
279 }
280 
281 if start == end {
282 return Some("");
283 }
284 
285 let mut indices = s.char_indices();
286 let (start_index, _) = indices.nth(start)?;
287 // Why not just use `nth()` again? We need to distinguish between a `None` because `end`
288 // is out of bounds and a `None` because `end` is the end of the string.
289 // If/when Iterator::advance_by (https://github.com/rust-lang/rust/issues/77404) stabilizes,
290 // we should use that. In the meantime, this doesn't hurt performance because `nth()`
291 // also has to advance character-by-character.
292 for _ in start + 1..end {
293 indices.next()?;
294 }
295 
296 let end_index = match indices.next() {
297 Some((index, _)) => index,
298 None => s.len(),
299 };
300 
301 s.get(start_index..end_index)
302}
303 
304pub fn count_chars_up_to_byte(text: &str, byte_offset: ByteOffset) -> Option<CharOffset> {
305 if byte_offset.as_usize() == text.len() {
306 return Some(CharOffset::from(text.chars().count()));
307 }
308 let mut counter = CharCounter::new(text);
309 counter.char_offset(byte_offset)
310}
311 
312#[cfg(test)]
313#[path = "mod_tests.rs"]
314mod tests;
315