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