rat_text/
lib.rs

1#![doc = include_str!("../readme.md")]
2
3use std::error::Error;
4use std::fmt::{Debug, Display, Formatter};
5use std::ops::Range;
6
7pub mod clipboard;
8pub mod date_input;
9pub mod line_number;
10pub mod number_input;
11pub mod text_area;
12pub mod text_input;
13pub mod text_input_mask;
14pub mod undo_buffer;
15
16mod grapheme;
17mod range_map;
18mod text_core;
19mod text_mask_core;
20mod text_store;
21
22pub use grapheme::{Glyph, Grapheme};
23
24use crate::_private::NonExhaustive;
25pub use pure_rust_locales::Locale;
26pub use rat_cursor::{impl_screen_cursor, screen_cursor, HasScreenCursor};
27use rat_scrolled::ScrollStyle;
28use ratatui::style::Style;
29use ratatui::widgets::Block;
30
31pub mod event {
32    //!
33    //! Event-handler traits and Keybindings.
34    //!
35
36    pub use rat_event::*;
37
38    /// Runs only the navigation events, not any editing.
39    #[derive(Debug)]
40    pub struct ReadOnly;
41
42    /// Result of event handling.
43    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
44    pub enum TextOutcome {
45        /// The given event has not been used at all.
46        Continue,
47        /// The event has been recognized, but the result was nil.
48        /// Further processing for this event may stop.
49        Unchanged,
50        /// The event has been recognized and there is some change
51        /// due to it.
52        /// Further processing for this event may stop.
53        /// Rendering the ui is advised.
54        Changed,
55        /// Text content has changed.
56        TextChanged,
57    }
58
59    impl ConsumedEvent for TextOutcome {
60        fn is_consumed(&self) -> bool {
61            *self != TextOutcome::Continue
62        }
63    }
64
65    // Useful for converting most navigation/edit results.
66    impl From<bool> for TextOutcome {
67        fn from(value: bool) -> Self {
68            if value {
69                TextOutcome::Changed
70            } else {
71                TextOutcome::Unchanged
72            }
73        }
74    }
75
76    impl From<Outcome> for TextOutcome {
77        fn from(value: Outcome) -> Self {
78            match value {
79                Outcome::Continue => TextOutcome::Continue,
80                Outcome::Unchanged => TextOutcome::Unchanged,
81                Outcome::Changed => TextOutcome::Changed,
82            }
83        }
84    }
85
86    impl From<TextOutcome> for Outcome {
87        fn from(value: TextOutcome) -> Self {
88            match value {
89                TextOutcome::Continue => Outcome::Continue,
90                TextOutcome::Unchanged => Outcome::Unchanged,
91                TextOutcome::Changed => Outcome::Changed,
92                TextOutcome::TextChanged => Outcome::Changed,
93            }
94        }
95    }
96}
97
98/// Behavior modifiers.
99#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
100pub enum TextFocusGained {
101    /// None
102    #[default]
103    None,
104    /// Flag for overwrite.
105    Overwrite,
106    /// Select all text.
107    SelectAll,
108}
109
110/// Behaviour modifiers.
111#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
112pub enum TextFocusLost {
113    /// None
114    #[default]
115    None,
116    /// Sets the Position back to 0.
117    Position0,
118}
119
120/// Combined style for the widget.
121#[derive(Debug, Clone)]
122pub struct TextStyle {
123    pub style: Style,
124    pub focus: Option<Style>,
125    pub select: Option<Style>,
126    pub invalid: Option<Style>,
127
128    /// Focus behaviour.
129    pub on_focus_gained: Option<TextFocusGained>,
130    /// Focus behaviour.
131    pub on_focus_lost: Option<TextFocusLost>,
132
133    pub scroll: Option<ScrollStyle>,
134    pub block: Option<Block<'static>>,
135    pub border_style: Option<Style>,
136
137    pub non_exhaustive: NonExhaustive,
138}
139
140impl Default for TextStyle {
141    fn default() -> Self {
142        Self {
143            style: Default::default(),
144            focus: None,
145            select: None,
146            invalid: None,
147            on_focus_gained: None,
148            on_focus_lost: None,
149            scroll: None,
150            block: None,
151            border_style: None,
152            non_exhaustive: NonExhaustive,
153        }
154    }
155}
156
157pub mod core {
158    //!
159    //! Core structs for text-editing.
160    //! Used to implement the widgets.
161    //!
162
163    pub use crate::text_core::TextCore;
164    pub use crate::text_mask_core::MaskedCore;
165    pub use crate::text_store::text_rope::TextRope;
166    pub use crate::text_store::text_string::TextString;
167    pub use crate::text_store::TextStore;
168}
169
170#[derive(Debug, PartialEq)]
171pub enum TextError {
172    /// Invalid text.
173    InvalidText(String),
174    /// Clipboard error occurred.
175    Clipboard,
176    /// Indicates that the passed text-range was out of bounds.
177    TextRangeOutOfBounds(TextRange),
178    /// Indicates that the passed text-position was out of bounds.
179    TextPositionOutOfBounds(TextPosition),
180    /// Indicates that the passed line index was out of bounds.
181    ///
182    /// Contains the index attempted and the actual length of the
183    /// `Rope`/`RopeSlice` in lines, in that order.
184    LineIndexOutOfBounds(upos_type, upos_type),
185    /// Column index is out of bounds.
186    ColumnIndexOutOfBounds(upos_type, upos_type),
187    /// Indicates that the passed byte index was out of bounds.
188    ///
189    /// Contains the index attempted and the actual length of the
190    /// `Rope`/`RopeSlice` in bytes, in that order.
191    ByteIndexOutOfBounds(usize, usize),
192    /// Indicates that the passed char index was out of bounds.
193    ///
194    /// Contains the index attempted and the actual length of the
195    /// `Rope`/`RopeSlice` in chars, in that order.
196    CharIndexOutOfBounds(usize, usize),
197    /// out of bounds.
198    ///
199    /// Contains the [start, end) byte indices of the range and the actual
200    /// length of the `Rope`/`RopeSlice` in bytes, in that order.  When
201    /// either the start or end are `None`, that indicates a half-open range.
202    ByteRangeOutOfBounds(Option<usize>, Option<usize>, usize),
203    /// Indicates that the passed char-index range was partially or fully
204    /// out of bounds.
205    ///
206    /// Contains the [start, end) char indices of the range and the actual
207    /// length of the `Rope`/`RopeSlice` in chars, in that order.  When
208    /// either the start or end are `None`, that indicates a half-open range.
209    CharRangeOutOfBounds(Option<usize>, Option<usize>, usize),
210    /// Indicates that the passed byte index was not a char boundary.
211    ///
212    /// Contains the passed byte index.
213    ByteIndexNotCharBoundary(usize),
214    /// Indicates that the passed byte range didn't line up with char
215    /// boundaries.
216    ///
217    /// Contains the [start, end) byte indices of the range, in that order.
218    /// When either the start or end are `None`, that indicates a half-open
219    /// range.
220    ByteRangeNotCharBoundary(
221        Option<usize>, // Start.
222        Option<usize>, // End.
223    ),
224    /// Indicates that a reversed byte-index range (end < start) was
225    /// encountered.
226    ///
227    /// Contains the [start, end) byte indices of the range, in that order.
228    ByteRangeInvalid(
229        usize, // Start.
230        usize, // End.
231    ),
232    /// Indicates that a reversed char-index range (end < start) was
233    /// encountered.
234    ///
235    /// Contains the [start, end) char indices of the range, in that order.
236    CharRangeInvalid(
237        usize, // Start.
238        usize, // End.
239    ),
240}
241
242impl Display for TextError {
243    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
244        write!(f, "{:?}", self)
245    }
246}
247
248impl Error for TextError {}
249
250/// Row/Column type.
251#[allow(non_camel_case_types)]
252pub type upos_type = u32;
253/// Row/Column type.
254#[allow(non_camel_case_types)]
255pub type ipos_type = i32;
256
257/// Text position.
258#[derive(Default, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
259pub struct TextPosition {
260    pub y: upos_type,
261    pub x: upos_type,
262}
263
264impl TextPosition {
265    /// New position.
266    pub const fn new(x: upos_type, y: upos_type) -> TextPosition {
267        Self { y, x }
268    }
269}
270
271impl Debug for TextPosition {
272    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
273        write!(f, "{}|{}", self.x, self.y)
274    }
275}
276
277impl From<(upos_type, upos_type)> for TextPosition {
278    fn from(value: (upos_type, upos_type)) -> Self {
279        Self {
280            y: value.1,
281            x: value.0,
282        }
283    }
284}
285
286impl From<TextPosition> for (upos_type, upos_type) {
287    fn from(value: TextPosition) -> Self {
288        (value.x, value.y)
289    }
290}
291
292/// Exclusive range for text ranges.
293#[derive(Default, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
294pub struct TextRange {
295    /// column, row
296    pub start: TextPosition,
297    /// column, row
298    pub end: TextPosition,
299}
300
301impl Debug for TextRange {
302    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
303        write!(
304            f,
305            "{}|{}-{}|{}",
306            self.start.x, self.start.y, self.end.x, self.end.y
307        )
308    }
309}
310
311impl From<Range<TextPosition>> for TextRange {
312    fn from(value: Range<TextPosition>) -> Self {
313        assert!(value.start <= value.end);
314        Self {
315            start: value.start,
316            end: value.end,
317        }
318    }
319}
320
321impl From<TextRange> for Range<TextPosition> {
322    fn from(value: TextRange) -> Self {
323        value.start..value.end
324    }
325}
326
327impl TextRange {
328    /// Maximum text range.
329    pub const MAX: TextRange = TextRange {
330        start: TextPosition {
331            y: upos_type::MAX,
332            x: upos_type::MAX,
333        },
334        end: TextPosition {
335            y: upos_type::MAX,
336            x: upos_type::MAX,
337        },
338    };
339
340    /// New text range.
341    ///
342    /// Panic
343    /// Panics if start > end.
344    pub fn new(start: impl Into<TextPosition>, end: impl Into<TextPosition>) -> Self {
345        let start = start.into();
346        let end = end.into();
347
348        assert!(start <= end);
349
350        TextRange { start, end }
351    }
352
353    /// Empty range
354    #[inline]
355    pub fn is_empty(&self) -> bool {
356        self.start == self.end
357    }
358
359    /// Range contains the given position.
360    #[inline]
361    pub fn contains_pos(&self, pos: impl Into<TextPosition>) -> bool {
362        let pos = pos.into();
363        pos >= self.start && pos < self.end
364    }
365
366    /// Range fully before the given position.
367    #[inline]
368    pub fn before_pos(&self, pos: impl Into<TextPosition>) -> bool {
369        let pos = pos.into();
370        pos >= self.end
371    }
372
373    /// Range fully after the given position.
374    #[inline]
375    pub fn after_pos(&self, pos: impl Into<TextPosition>) -> bool {
376        let pos = pos.into();
377        pos < self.start
378    }
379
380    /// Range contains the other range.
381    #[inline(always)]
382    pub fn contains(&self, other: TextRange) -> bool {
383        other.start >= self.start && other.end <= self.end
384    }
385
386    /// Range before the other range.
387    #[inline(always)]
388    pub fn before(&self, other: TextRange) -> bool {
389        other.start > self.end
390    }
391
392    /// Range after the other range.
393    #[inline(always)]
394    pub fn after(&self, other: TextRange) -> bool {
395        other.end < self.start
396    }
397
398    /// Range overlaps with other range.
399    #[inline(always)]
400    pub fn intersects(&self, other: TextRange) -> bool {
401        other.start <= self.end && other.end >= self.start
402    }
403
404    /// Return the modified value range, that accounts for a
405    /// text insertion of range.
406    #[inline]
407    pub fn expand(&self, range: TextRange) -> TextRange {
408        TextRange::new(self.expand_pos(range.start), self.expand_pos(range.end))
409    }
410
411    /// Return the modified position, that accounts for a
412    /// text insertion of range.
413    #[inline]
414    pub fn expand_pos(&self, pos: TextPosition) -> TextPosition {
415        let delta_lines = self.end.y - self.start.y;
416
417        // swap x and y to enable tuple comparison
418        if pos < self.start {
419            pos
420        } else if pos == self.start {
421            self.end
422        } else {
423            if pos.y > self.start.y {
424                TextPosition::new(pos.x, pos.y + delta_lines)
425            } else if pos.y == self.start.y {
426                if pos.x >= self.start.x {
427                    TextPosition::new(pos.x - self.start.x + self.end.x, pos.y + delta_lines)
428                } else {
429                    pos
430                }
431            } else {
432                pos
433            }
434        }
435    }
436
437    /// Return the modified value range, that accounts for a
438    /// text deletion of range.
439    #[inline]
440    pub fn shrink(&self, range: TextRange) -> TextRange {
441        TextRange::new(self.shrink_pos(range.start), self.shrink_pos(range.end))
442    }
443
444    /// Return the modified position, that accounts for a
445    /// text deletion of the range.
446    #[inline]
447    pub fn shrink_pos(&self, pos: TextPosition) -> TextPosition {
448        let delta_lines = self.end.y - self.start.y;
449
450        // swap x and y to enable tuple comparison
451        if pos < self.start {
452            pos
453        } else if pos >= self.start && pos <= self.end {
454            self.start
455        } else {
456            // after row
457            if pos.y > self.end.y {
458                TextPosition::new(pos.x, pos.y - delta_lines)
459            } else if pos.y == self.end.y {
460                if pos.x >= self.end.x {
461                    TextPosition::new(pos.x - self.end.x + self.start.x, pos.y - delta_lines)
462                } else {
463                    pos
464                }
465            } else {
466                pos
467            }
468        }
469    }
470}
471
472/// Trait for a cursor (akin to an Iterator, not the blinking thing).
473///
474/// This is not a [DoubleEndedIterator] which can iterate from both ends of
475/// the iterator, but moves a cursor forward/back over the collection.
476pub trait Cursor: Iterator {
477    /// Return the previous item.
478    fn prev(&mut self) -> Option<Self::Item>;
479
480    /// Return a cursor with prev/next reversed.
481    /// All iterator functions work backwards.
482    fn rev_cursor(self) -> impl Cursor<Item = Self::Item>
483    where
484        Self: Sized;
485
486    /// Offset of the current cursor position into the underlying text.
487    fn text_offset(&self) -> usize;
488}
489
490mod _private {
491    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
492    pub struct NonExhaustive;
493}