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