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