Skip to main content

epaint/text/
index.rs

1//! Strongly-typed offsets into text.
2//!
3//! UTF-8 text can be indexed either by _byte_ offset or by _character_
4//! (Unicode scalar) offset. Mixing the two is a common source of bugs,
5//! so we use distinct types to keep them apart.
6
7use core::ops::Range;
8
9/// A byte offset into a UTF-8 string.
10///
11/// This is what you use to slice a [`str`] (e.g. `&text[range.start.0..range.end.0]`).
12/// Not to be confused with [`CharIndex`], which counts characters instead of bytes.
13#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
14#[cfg_attr(
15    feature = "serde",
16    derive(serde::Deserialize, serde::Serialize),
17    serde(transparent)
18)]
19pub struct ByteIndex(pub usize);
20
21/// A character (Unicode scalar) offset into a string.
22///
23/// Counts characters, not bytes, so it is independent of the UTF-8 encoding.
24/// Not to be confused with [`ByteIndex`]. See also [`super::cursor::CCursor`].
25#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
26#[cfg_attr(
27    feature = "serde",
28    derive(serde::Deserialize, serde::Serialize),
29    serde(transparent)
30)]
31pub struct CharIndex(pub usize);
32
33macro_rules! impl_text_index {
34    ($Type:ident) => {
35        impl $Type {
36            /// The zero offset, i.e. the very start of the text.
37            pub const ZERO: Self = Self(0);
38
39            /// Saturating integer addition.
40            #[inline]
41            pub fn saturating_add(self, rhs: usize) -> Self {
42                Self(self.0.saturating_add(rhs))
43            }
44
45            /// Saturating integer subtraction.
46            #[inline]
47            pub fn saturating_sub(self, rhs: usize) -> Self {
48                Self(self.0.saturating_sub(rhs))
49            }
50        }
51
52        impl From<usize> for $Type {
53            #[inline]
54            fn from(index: usize) -> Self {
55                Self(index)
56            }
57        }
58
59        impl From<$Type> for usize {
60            #[inline]
61            fn from(index: $Type) -> Self {
62                index.0
63            }
64        }
65
66        impl core::ops::Add<usize> for $Type {
67            type Output = Self;
68
69            #[inline]
70            fn add(self, rhs: usize) -> Self {
71                Self(self.0 + rhs)
72            }
73        }
74
75        /// Compose offsets, e.g. a base position plus a relative one.
76        impl core::ops::Add<$Type> for $Type {
77            type Output = Self;
78
79            #[inline]
80            fn add(self, rhs: Self) -> Self {
81                Self(self.0 + rhs.0)
82            }
83        }
84
85        impl core::ops::Sub<usize> for $Type {
86            type Output = Self;
87
88            #[inline]
89            fn sub(self, rhs: usize) -> Self {
90                Self(self.0 - rhs)
91            }
92        }
93
94        impl core::ops::Sub<$Type> for $Type {
95            type Output = Self;
96
97            #[inline]
98            fn sub(self, rhs: Self) -> Self {
99                Self(self.0 - rhs.0)
100            }
101        }
102
103        impl core::ops::AddAssign<usize> for $Type {
104            #[inline]
105            fn add_assign(&mut self, rhs: usize) {
106                self.0 += rhs;
107            }
108        }
109
110        impl core::ops::AddAssign<$Type> for $Type {
111            #[inline]
112            fn add_assign(&mut self, rhs: Self) {
113                self.0 += rhs.0;
114            }
115        }
116
117        impl core::ops::SubAssign<usize> for $Type {
118            #[inline]
119            fn sub_assign(&mut self, rhs: usize) {
120                self.0 -= rhs;
121            }
122        }
123
124        impl core::fmt::Display for $Type {
125            #[inline]
126            fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
127                self.0.fmt(f)
128            }
129        }
130    };
131}
132
133impl_text_index!(ByteIndex);
134impl_text_index!(CharIndex);
135
136/// A range of [`ByteIndex`], i.e. a byte range into a [`str`].
137pub type ByteRange = Range<ByteIndex>;
138
139/// A range of [`CharIndex`], i.e. a character range into a [`str`].
140pub type CharRange = Range<CharIndex>;
141
142/// Extension methods for a [`ByteRange`].
143pub trait ByteRangeExt {
144    /// The full byte range covering `text`, i.e. `0..text.len()`.
145    fn full(text: &str) -> Self;
146
147    /// The `start..end` byte range as plain `usize`, for slicing a [`str`].
148    fn as_usize(&self) -> Range<usize>;
149
150    /// Slice the given string by this byte range.
151    fn slice<'s>(&self, text: &'s str) -> &'s str;
152}
153
154impl ByteRangeExt for ByteRange {
155    #[inline]
156    fn full(text: &str) -> Self {
157        ByteIndex::ZERO..ByteIndex(text.len())
158    }
159
160    #[inline]
161    fn as_usize(&self) -> Range<usize> {
162        self.start.0..self.end.0
163    }
164
165    #[inline]
166    fn slice<'s>(&self, text: &'s str) -> &'s str {
167        &text[self.as_usize()]
168    }
169}
170
171/// Extension methods for a [`CharRange`].
172pub trait CharRangeExt {
173    /// The full character range covering `text`, i.e. `0..text.chars().count()`.
174    fn full(text: &str) -> Self;
175}
176
177impl CharRangeExt for CharRange {
178    #[inline]
179    fn full(text: &str) -> Self {
180        CharIndex::ZERO..CharIndex(text.chars().count())
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::CharIndex;
187
188    #[test]
189    fn arithmetic() {
190        // Add a relative offset to a base position:
191        assert_eq!(CharIndex(2) + CharIndex(3), CharIndex(5));
192        assert_eq!(CharIndex(2) + 3, CharIndex(5));
193
194        let mut idx = CharIndex(2);
195        idx += CharIndex(3);
196        assert_eq!(idx, CharIndex(5));
197
198        // Subtract a relative offset from a position:
199        assert_eq!(CharIndex(5) - CharIndex(2), CharIndex(3));
200        assert_eq!(CharIndex(5) - 2, CharIndex(3));
201    }
202}