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