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