rat_text/
lib.rs

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