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}