Skip to main content

feather_tui/components/
text.rs

1use crate::{error::{FtuiError, FtuiResult}, util::ansi};
2use bitflags::bitflags;
3use unicode_segmentation::UnicodeSegmentation;
4
5bitflags! {
6    /// Flags used to style a `Text` component. Multiple flags can be combined 
7    /// using the bitwise OR operator to apply multiple styles simultaneously.
8    ///
9    /// # Note
10    /// The bitwise OR operator combines flags like this: `flag1 | flag2 | flag3`
11    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12    pub struct TextFlags: u32 {
13        // NONE can't be 0
14        /// No flags.
15        const NONE          = 1 << 0;
16
17        /// Aligns text to the right of the renderer.
18        const ALIGN_RIGHT   = 1 << 1;
19        /// Centers text horizontally in the renderer.
20        const ALIGN_MIDDLE  = 1 << 2;
21        /// Aligns text to the bottom of the renderer.
22        const ALIGN_BOTTOM  = 1 << 3;
23
24        // Applies colors to the background of the text instead of foreground.
25        const COLOR_BACK    = 1 << 4;
26
27        /// Apply the black color to the text component.
28        const COLOR_BLACK   = 1 << 6;
29        /// Apply the red color to the text component.
30        const COLOR_RED     = 1 << 7;
31        /// Apply the green color to the text component.
32        const COLOR_GREEN   = 1 << 8;
33        /// Apply the yellow color to the text component.
34        const COLOR_YELLOW  = 1 << 9;
35        /// Apply the blue color to the text component.
36        const COLOR_BLUE    = 1 << 10;
37        /// Apply the magenta color to the text component.
38        const COLOR_MAGENTA = 1 << 11;
39        /// Apply the cyan color to the text component.
40        const COLOR_CYAN    = 1 << 12;
41        /// Apply the white color to the text component.
42        const COLOR_WHITE   = 1 << 13;
43
44        /// Apply the black color to the text component background.
45        const COLOR_BLACK_BACK = 
46            Self::COLOR_BLACK.bits() | Self::COLOR_BACK.bits();
47        /// Apply the red color to the text component background.
48        const COLOR_RED_BACK = 
49            Self::COLOR_RED.bits() | Self::COLOR_BACK.bits();
50        /// Apply the green color to the text component background.
51        const COLOR_GREEN_BACK = 
52            Self::COLOR_GREEN.bits() | Self::COLOR_BACK.bits();
53        /// Apply the yellow color to the text component background.
54        const COLOR_YELLOW_BACK = 
55            Self::COLOR_YELLOW.bits() | Self::COLOR_BACK.bits();
56        /// Apply the blue color to the text component background.
57        const COLOR_BLUE_BACK =
58            Self::COLOR_BLUE.bits() | Self::COLOR_BACK.bits();
59        /// Apply the magneta color to the text component background.
60        const COLOR_MAGENTA_BACK =
61            Self::COLOR_MAGENTA.bits() | Self::COLOR_BACK.bits();
62        /// Apply the cyan color to the text component background.
63        const COLOR_CYAN_BACK =
64            Self::COLOR_CYAN.bits() | Self::COLOR_BACK.bits();
65        /// Apply the white color to the text component background.
66        const COLOR_WHITE_BACK =
67            Self::COLOR_WHITE.bits() | Self::COLOR_BACK.bits();
68
69        /// Make the text component bold
70        const STYLE_BOLD   = 1 << 14;
71        /// Dim the text component
72        const STYLE_DIM    = 1 << 15;
73        /// Make the text component italic.
74        const STYLE_ITALIC = 1 << 16;
75        /// Underlined the text component.
76        const STYLE_UNDER  = 1 << 17;
77        /// Applies strike through to the text component.
78        const STYLE_STRIKE = 1 << 18;
79    }
80}
81
82impl Default for TextFlags {
83    fn default() -> Self {
84        Self::NONE
85    }
86}
87
88/// A UI component representing a text element in a `Container`. `Text` components
89/// are displayed in the order they are added to the `Container`. They can be
90/// customized using `TextFlags` to adjust alignment, color, and other styling options.
91///
92/// # Usage
93/// The `Text` component is used within a `Container` to display static text elements.  
94///
95/// # Derives
96/// `Debug`, `Clone`, `PartialEq`
97///
98/// # PartialEq Implementation
99///
100/// Only the `label` and `flags` are considered when comparing `Text` instances.
101#[derive(Debug, Clone)]
102pub struct Text {
103    label: String,
104    len: usize,
105    id: u16,
106    line: u16,
107    flags: TextFlags,
108    pos: u16,
109    style: Vec<&'static str>,
110}
111
112/// Implementation of the `PartialEq` trait for the `Text` struct. This implementation
113/// defines equality based on two fields: `label` and `flags`.
114///
115/// # Notes
116/// - Only the `label` and `flags` are considered when comparing `Text` instances.
117///
118/// # Example
119/// ```rust
120/// use feather_tui as tui; 
121///
122/// // Create two `Text` instances with identical `label` and `flags`
123/// let text1 = tui::cpn::Text::new("Text", tui::cpn::TextFlags::NONE);
124/// let text2 = tui::cpn::Text::new("Text", tui::cpn::TextFlags::NONE);
125///
126/// // Assert that both `text1` and `text2` are equal, since they have the same `label` and `flags`
127/// assert_eq!(text1, text2); // This will evaluate to true
128/// 
129/// // Create a new `Text` instance with a different flag (ALIGN_RIGHT)
130/// let text3 = tui::cpn::Text::new("Text", tui::cpn::TextFlags::ALIGN_RIGHT);
131/// 
132/// // Assert that `text1` and `text3` are not equal, since their `flags` are different
133/// assert_ne!(text1, text3); // This will evaluate to true (text1 != text3)
134/// 
135/// // Create a new `Text` instance with a different `label`
136/// let text4 = tui::cpn::Text::new("Hello", tui::cpn::TextFlags::ALIGN_RIGHT);
137/// 
138/// // Assert that `text3` and `text4` are not equal, since their `label` is different
139/// assert_ne!(text3, text4); // This will evaluate to true (text3 != text4)
140/// ```
141impl PartialEq for Text {
142    fn eq(&self, others: &Self) -> bool {
143        self.label == others.label &&
144        self.flags == others.flags
145    }
146}
147
148impl Text {
149    /// Creates a new `Text` component with the specified label and flags.
150    ///
151    /// # Parameters
152    /// - `label`: A `&str` representing the text content.
153    /// - `flags`: A set of `TextFlags` combined using the bitwise OR operator.
154    ///
155    /// # Notes
156    /// The bitwise OR operator combines flags like this: `flag1 | flag2 | flag3`
157    ///
158    /// # Returns
159    /// - `Ok(Text)`: Returns a `Text` instance
160    /// - `Err(FtuiError)`: Returns an error.
161    ///
162    /// # Example
163    /// ```rust
164    /// // Create a `Text` component labeled "Text".
165    /// // The text is right-aligned.
166    /// // The background color is red.
167    /// let _ = Text::new(
168    ///     "Text", TextFlags::ALIGN_RIGHT | TextFlags::COLOR_RED_BACK)?;
169    /// ```
170    pub fn new(label: &str, flags: impl Into<Option<TextFlags>>) -> FtuiResult<Self> {
171        let flags = flags.into().unwrap_or(TextFlags::NONE);
172
173        Self::ensure_compatible_flags(&flags)?; 
174        
175        Ok(Text {
176            label: label.to_string(),
177            len: label.graphemes(true).count(),
178            id: 0,
179            line: 0,
180            flags,
181            pos: 0,
182            style: Self::resolve_style(flags),
183        })
184    }
185
186    pub(crate) fn with_id(
187        label: &str, flags: impl Into<Option<TextFlags>>, id: u16
188    ) -> FtuiResult<Self> {
189        let mut text = Text::new(label, flags)?;
190        text.set_id(id);
191        Ok(text)
192    }
193
194    pub(crate) fn ensure_compatible_flags(flags: &TextFlags) -> FtuiResult<()> {
195        // NONE Flags alone is always compatible.
196        if *flags == TextFlags::NONE {
197            return Ok(());
198        }
199
200        // NONE Flags should not be combined with any other flags.
201        if flags.contains(TextFlags::NONE) && *flags != TextFlags::NONE {
202            return Err(FtuiError::TextFlagNoneWithOther);
203        }
204
205        // Only one color can be set.
206        if flags
207            .intersection(
208                TextFlags::COLOR_BLACK |
209                TextFlags::COLOR_RED |
210                TextFlags::COLOR_GREEN |
211                TextFlags::COLOR_YELLOW |
212                TextFlags::COLOR_BLUE |
213                TextFlags::COLOR_MAGENTA |
214                TextFlags::COLOR_CYAN |
215                TextFlags::COLOR_WHITE)
216            .bits()
217            .count_ones() > 1 
218        {
219            return Err(FtuiError::TextFlagMultipleColor);
220        }
221
222        Ok(())
223    }
224
225    #[inline]
226    fn color_f_or_b(flags: TextFlags, b: &'static str, f: &'static str) -> &'static str {
227        // if COLOR_BACK flag is not set text will default to foreground color
228        if flags.contains(TextFlags::COLOR_BACK) { b } else { f }
229    }
230
231    fn resolve_color(flags: TextFlags) -> Option<&'static str> {
232        if flags.contains(TextFlags::COLOR_BLACK) {
233            Some(Self::color_f_or_b(flags, ansi::ESC_BLACK_B, ansi::ESC_BLACK_F))
234        } else if flags.contains(TextFlags::COLOR_RED) {
235            Some(Self::color_f_or_b(flags, ansi::ESC_RED_B, ansi::ESC_RED_F))
236        } else if flags.contains(TextFlags::COLOR_GREEN) {
237            Some(Self::color_f_or_b(flags, ansi::ESC_GREEN_B, ansi::ESC_GREEN_F))
238        } else if flags.contains(TextFlags::COLOR_YELLOW) {
239            Some(Self::color_f_or_b(flags, ansi::ESC_YELLOW_B, ansi::ESC_YELLOW_F))
240        } else if flags.contains(TextFlags::COLOR_BLUE) {
241            Some(Self::color_f_or_b(flags, ansi::ESC_BLUE_B, ansi::ESC_BLUE_F))
242        } else if flags.contains(TextFlags::COLOR_MAGENTA) {
243            Some(Self::color_f_or_b(flags, ansi::ESC_MAGENTA_B, ansi::ESC_MAGENTA_F))
244        } else if flags.contains(TextFlags::COLOR_CYAN) {
245            Some(Self::color_f_or_b(flags, ansi::ESC_CYAN_B, ansi::ESC_CYAN_F))
246        } else if flags.contains(TextFlags::COLOR_WHITE) {
247            Some(Self::color_f_or_b(flags, ansi::ESC_WHITE_B, ansi::ESC_WHITE_F))
248        } else {
249            None
250        }
251    }
252
253    fn resolve_style(flags: TextFlags) -> Vec<&'static str> {
254        let mut style: Vec<&'static str> = vec![];
255
256        if let Some(color) = Self::resolve_color(flags) {
257            style.push(color);
258        }
259        if flags.contains(TextFlags::STYLE_BOLD) {
260            style.push(ansi::ESC_BOLD);
261        }
262        if flags.contains(TextFlags::STYLE_DIM) {
263            style.push(ansi::ESC_DIM);
264        }
265        if flags.contains(TextFlags::STYLE_ITALIC) {
266            style.push(ansi::ESC_ITALIC);
267        }
268        if flags.contains(TextFlags::STYLE_UNDER) {
269            style.push(ansi::ESC_UNDERLINE);
270        }
271        if flags.contains(TextFlags::STYLE_STRIKE) {
272            style.push(ansi::ESC_STRIKETHROUGH);
273        }
274
275        return style;
276    }
277
278    pub fn label(&self) -> &String {
279        return &self.label;
280    }
281
282    /// Updates the label of the `Text` component.
283    ///
284    /// # Parameters
285    /// - `label`: The new label.
286    ///
287    /// # Example
288    /// ```rust
289    /// // Create a `Text` component with the label "Text" and no flags.
290    /// let mut text = Text::new("Text", None)?;
291    ///
292    /// // Update the label to "New Label".
293    /// text.set_label("New Label");
294    /// ```
295    pub fn set_label(&mut self, label: impl Into<String>) {
296        let label = label.into();
297
298        self.len = label.graphemes(true).count();
299        self.label = label;
300    }
301
302    pub(crate) fn set_line(&mut self, line: u16) {
303        self.line = line;
304    }
305
306    pub(crate) fn line(&self) -> u16 {
307        return self.line;
308    }
309
310    pub(crate) fn len(&self) -> usize {
311        return self.len;
312    }
313
314    pub(crate) fn set_pos(&mut self, pos: u16) {
315        self.pos = pos;
316    }
317
318    pub(crate) fn pos(&self) -> u16 {
319        return self.pos;
320    }
321
322    pub(crate) fn flags(&self) -> &TextFlags {
323        return &self.flags;
324    }
325
326    pub(crate) fn styles(&self) -> &[&'static str] {
327        return &self.style;
328    }
329
330    pub(crate) fn id(&self) -> u16 {
331        self.id
332    }
333
334    pub(crate) fn set_id(&mut self, value: u16) {
335        self.id = value;
336    }
337}