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