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