Skip to main content

tui_realm_stdlib/
prop_ext.rs

1//! Extra extensions to handle Properties.
2
3use tuirealm::props::{
4    AttrValue, AttrValueRef, Attribute, Borders, LineStatic, QueryResult, Style, TextModifiers,
5    Title,
6};
7use tuirealm::ratatui::text::Line;
8use tuirealm::ratatui::widgets::Block;
9
10use crate::utils::borrow_clone_line;
11
12/// Prop Store for very common props.
13///
14/// This structure helps to have a common way to handle the "very common" properties, reducing boilerplate
15/// and potential mis-matches.
16///
17/// Additionally, using this over [`Props`](tuirealm::props::Props), saves on indirection and heap-size.
18/// On usage (usually on `view`), it also saves on `unwraps` or "defaulting".
19#[derive(Debug, Clone, PartialEq, Eq)]
20#[non_exhaustive]
21pub struct CommonProps {
22    /// The main and common style for a given widget. This is mostly used unless overwritten by more specific styles.
23    pub style: Style,
24    /// Set a different style for the `border` when the current component is unfocused. This will effectively be merged with `style`.
25    pub border_unfocused_style: Style,
26
27    /// A Border to apply, if set.
28    pub border: Option<Borders>,
29    /// The title for the Border.
30    pub title: Option<Title>,
31
32    /// Determines if the current component should be drawn at all.
33    pub display: bool,
34    /// Determines if the current component is focused or not.
35    pub focused: bool,
36
37    /// Determines if the current component should always use the "active" style, regardless if it is focused or not.
38    pub always_active: bool,
39}
40
41impl Default for CommonProps {
42    fn default() -> Self {
43        Self {
44            style: Style::default(),
45            border_unfocused_style: Style::default(),
46            border: Option::default(),
47            title: Option::default(),
48            display: true,
49            focused: false,
50            always_active: false,
51        }
52    }
53}
54
55impl CommonProps {
56    /// Try to set a given [`Attribute`]. Returns `Some` if the value is unhandled. `None` if handled.
57    pub fn set(&mut self, attr: Attribute, value: AttrValue) -> Option<AttrValue> {
58        match (attr, value) {
59            // handle style attributes
60            (Attribute::Style, AttrValue::Style(val)) => self.style = val,
61            (Attribute::Foreground, AttrValue::Color(val)) => self.style = self.style.fg(val),
62            (Attribute::Background, AttrValue::Color(val)) => self.style = self.style.bg(val),
63            (Attribute::UnfocusedBorderStyle, AttrValue::Style(val)) => {
64                self.border_unfocused_style = val
65            }
66            (Attribute::TextProps, AttrValue::TextModifiers(val)) => {
67                self.style = self.style.add_modifier(val)
68            }
69
70            // handle flags
71            (Attribute::Display, AttrValue::Flag(val)) => self.display = val,
72            (Attribute::Focus, AttrValue::Flag(val)) => self.focused = val,
73            (Attribute::AlwaysActive, AttrValue::Flag(val)) => self.always_active = val,
74
75            // handle borders & titles
76            (Attribute::Borders, AttrValue::Borders(val)) => self.border = Some(val),
77            (Attribute::Title, AttrValue::Title(val)) => self.title = Some(val),
78
79            // other
80            (_, value) => return Some(value),
81        }
82
83        None
84    }
85
86    /// Try to get a given [`Attribute`].
87    pub fn get<'a>(&'a self, attr: Attribute) -> Option<AttrValueRef<'a>> {
88        match attr {
89            // handle style attributes
90            Attribute::Style => Some(AttrValueRef::Style(self.style)),
91            Attribute::Foreground => self.style.fg.map(AttrValueRef::Color),
92            Attribute::Background => self.style.bg.map(AttrValueRef::Color),
93            Attribute::UnfocusedBorderStyle => {
94                Some(AttrValueRef::Style(self.border_unfocused_style))
95            }
96            Attribute::TextProps => Some(AttrValueRef::TextModifiers(self.style.add_modifier)),
97
98            // handle flags
99            Attribute::Display => Some(AttrValueRef::Flag(self.display)),
100            Attribute::Focus => Some(AttrValueRef::Flag(self.focused)),
101            Attribute::AlwaysActive => Some(AttrValueRef::Flag(self.always_active)),
102
103            // handle borders & titles
104            Attribute::Borders => self.border.map(AttrValueRef::Borders),
105            Attribute::Title => self.title.as_ref().map(AttrValueRef::Title),
106
107            // other
108            _ => None,
109        }
110    }
111
112    /// Try to get a given [`Attribute`] as a type compatible with [`Component::query`](tuirealm::component::Component::query).
113    #[inline]
114    pub fn get_for_query<'a>(&'a self, attr: Attribute) -> Option<QueryResult<'a>> {
115        self.get(attr).map(QueryResult::Borrowed)
116    }
117
118    /// Get a [`Block`] with the configuration from the common props, if `borders` are defined.
119    ///
120    /// Uses [`get_block`](crate::utils::get_block).
121    pub fn get_block(&self) -> Option<Block<'_>> {
122        let borders = self.border?;
123        let title = self.title.as_ref();
124
125        let block = crate::utils::get_block(
126            borders,
127            title,
128            self.is_active(),
129            Some(self.border_unfocused_style),
130        );
131
132        Some(block)
133    }
134
135    /// Get if the current component is determined to be "active", either by having "Always active" active or being focused.
136    pub fn is_active(&self) -> bool {
137        self.always_active || self.focused
138    }
139}
140
141/// Prop Store for very common highlight props.
142///
143/// This structure helps to have a common way to handle the "very common" highlight properties, reducing boilerplate
144/// and potential mis-matches.
145///
146/// Additionally, using this over [`Props`](tuirealm::props::Props), saves on indirection and heap-size.
147/// On usage (usually on `view`), it also saves on `unwraps` or "defaulting"
148#[derive(Debug, Clone, PartialEq, Eq)]
149#[non_exhaustive]
150pub struct CommonHighlight {
151    /// The main style to patch [`CommonProps::style`] with for the currently active element.
152    pub style: Style,
153    /// A Style to patch on-top of [`style`](Self::style) when unfocused.
154    pub style_unfocused: Style,
155    /// The symbol to use to indicate the currently selected element.
156    pub symbol: LineStatic,
157}
158
159impl Default for CommonHighlight {
160    fn default() -> Self {
161        Self {
162            style: Style::default().add_modifier(TextModifiers::REVERSED),
163            style_unfocused: Style::default(),
164            symbol: LineStatic::default(),
165        }
166    }
167}
168
169impl CommonHighlight {
170    /// Try to set a given [`Attribute`]. Returns `Some` if the value is unhandled. `None` if handled.
171    pub fn set(&mut self, attr: Attribute, value: AttrValue) -> Option<AttrValue> {
172        match (attr, value) {
173            (Attribute::HighlightStyle, AttrValue::Style(val)) => self.style = val,
174            (Attribute::HighlightStyleUnfocused, AttrValue::Style(val)) => {
175                self.style_unfocused = val
176            }
177            (Attribute::HighlightedStr, AttrValue::TextLine(val)) => self.symbol = val,
178
179            // other
180            (_, value) => return Some(value),
181        }
182
183        None
184    }
185
186    /// Try to get a given [`Attribute`].
187    pub fn get<'a>(&'a self, attr: Attribute) -> Option<AttrValueRef<'a>> {
188        match attr {
189            Attribute::HighlightStyle => Some(AttrValueRef::Style(self.style)),
190            Attribute::HighlightStyleUnfocused => Some(AttrValueRef::Style(self.style_unfocused)),
191            Attribute::HighlightedStr => Some(AttrValueRef::TextLine(&self.symbol)),
192
193            // other
194            _ => None,
195        }
196    }
197
198    /// Try to get a given [`Attribute`] as a type compatible with [`Component::query`](tuirealm::component::Component::query).
199    #[inline]
200    pub fn get_for_query<'a>(&'a self, attr: Attribute) -> Option<QueryResult<'a>> {
201        self.get(attr).map(QueryResult::Borrowed)
202    }
203
204    /// Get the set highlight symbol as its own `Line` instance, but referencing the existing data.
205    pub fn get_symbol(&self) -> Option<Line<'_>> {
206        if self.symbol.spans.is_empty() {
207            None
208        } else {
209            Some(borrow_clone_line(&self.symbol))
210        }
211    }
212
213    /// Get the patched highlight style.
214    #[inline]
215    pub fn get_style(&self, normal_style: Style) -> Style {
216        normal_style.patch(self.style)
217    }
218
219    /// Get the patched highlight style with focused or unfocused style.
220    #[inline]
221    pub fn get_style_focus(&self, normal_style: Style, focus: bool) -> Style {
222        let style = normal_style.patch(self.style);
223
224        if focus {
225            style
226        } else {
227            style.patch(self.style_unfocused)
228        }
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use pretty_assertions::assert_eq;
235    use tuirealm::props::{
236        AttrValue, Attribute, BorderSides, BorderType, Borders, Color, HorizontalAlignment,
237        LineStatic, Style, TextModifiers, Title,
238    };
239    use tuirealm::ratatui::widgets::{Block, TitlePosition};
240
241    use crate::prop_ext::{CommonHighlight, CommonProps};
242
243    #[test]
244    fn common_should_have_expected_defaults() {
245        let props = CommonProps::default();
246
247        // test defaults
248        assert_eq!(
249            props.get(Attribute::Style).unwrap().unwrap_style(),
250            Style::new()
251        );
252        assert_eq!(
253            props
254                .get(Attribute::UnfocusedBorderStyle)
255                .unwrap()
256                .unwrap_style(),
257            Style::new()
258        );
259        assert!(props.get(Attribute::Foreground).is_none());
260        assert!(props.get(Attribute::Background).is_none());
261        assert_eq!(
262            props
263                .get(Attribute::TextProps)
264                .unwrap()
265                .unwrap_text_modifiers(),
266            TextModifiers::default()
267        );
268
269        assert!(props.get(Attribute::Display).unwrap().unwrap_flag());
270        assert!(!props.get(Attribute::Focus).unwrap().unwrap_flag());
271        assert!(!props.get(Attribute::AlwaysActive).unwrap().unwrap_flag());
272
273        assert!(props.get(Attribute::Borders).is_none());
274        assert!(props.get(Attribute::Title).is_none());
275    }
276
277    #[test]
278    fn common_should_get_set() {
279        let mut props = CommonProps::default();
280
281        // style via individual attributes
282        props.set(Attribute::Foreground, AttrValue::Color(Color::Black));
283        props.set(Attribute::Background, AttrValue::Color(Color::Gray));
284        props.set(
285            Attribute::TextProps,
286            AttrValue::TextModifiers(TextModifiers::BOLD),
287        );
288
289        assert_eq!(
290            props.get(Attribute::Style).unwrap().unwrap_style(),
291            Style::new()
292                .fg(Color::Black)
293                .bg(Color::Gray)
294                .add_modifier(TextModifiers::BOLD)
295        );
296        assert_eq!(
297            props.get(Attribute::Foreground).unwrap().unwrap_color(),
298            Color::Black
299        );
300        assert_eq!(
301            props.get(Attribute::Background).unwrap().unwrap_color(),
302            Color::Gray
303        );
304        assert_eq!(
305            props
306                .get(Attribute::TextProps)
307                .unwrap()
308                .unwrap_text_modifiers(),
309            TextModifiers::BOLD
310        );
311
312        // style via style attribute
313        props.set(
314            Attribute::Style,
315            AttrValue::Style(
316                Style::new()
317                    .fg(Color::Blue)
318                    .bg(Color::DarkGray)
319                    .add_modifier(TextModifiers::DIM),
320            ),
321        );
322
323        assert_eq!(
324            props.get(Attribute::Style).unwrap().unwrap_style(),
325            Style::new()
326                .fg(Color::Blue)
327                .bg(Color::DarkGray)
328                .add_modifier(TextModifiers::DIM)
329        );
330        assert_eq!(
331            props.get(Attribute::Foreground).unwrap().unwrap_color(),
332            Color::Blue
333        );
334        assert_eq!(
335            props.get(Attribute::Background).unwrap().unwrap_color(),
336            Color::DarkGray
337        );
338        assert_eq!(
339            props
340                .get(Attribute::TextProps)
341                .unwrap()
342                .unwrap_text_modifiers(),
343            TextModifiers::DIM
344        );
345
346        // focus style
347        props.set(
348            Attribute::UnfocusedBorderStyle,
349            AttrValue::Style(Style::new().add_modifier(TextModifiers::REVERSED)),
350        );
351
352        assert_eq!(
353            props
354                .get(Attribute::UnfocusedBorderStyle)
355                .unwrap()
356                .unwrap_style(),
357            Style::new().add_modifier(TextModifiers::REVERSED)
358        );
359
360        // flags
361
362        props.set(Attribute::Display, AttrValue::Flag(false));
363        props.set(Attribute::Focus, AttrValue::Flag(true));
364        props.set(Attribute::AlwaysActive, AttrValue::Flag(true));
365
366        assert!(!props.get(Attribute::Display).unwrap().unwrap_flag());
367        assert!(props.get(Attribute::Focus).unwrap().unwrap_flag());
368        assert!(props.get(Attribute::AlwaysActive).unwrap().unwrap_flag());
369
370        // border & title
371
372        props.set(
373            Attribute::Borders,
374            AttrValue::Borders(
375                Borders::default()
376                    .color(Color::Black)
377                    .modifiers(BorderType::Double)
378                    .sides(BorderSides::TOP),
379            ),
380        );
381        props.set(
382            Attribute::Title,
383            AttrValue::Title(
384                Title::default()
385                    .content("Hello".into())
386                    .alignment(HorizontalAlignment::Center)
387                    .position(TitlePosition::Bottom),
388            ),
389        );
390
391        assert_eq!(
392            props.get(Attribute::Borders).unwrap().unwrap_borders(),
393            Borders::default()
394                .color(Color::Black)
395                .modifiers(BorderType::Double)
396                .sides(BorderSides::TOP)
397        );
398        assert_eq!(
399            props.get(Attribute::Title).unwrap().unwrap_title(),
400            &Title::default()
401                .content("Hello".into())
402                .alignment(HorizontalAlignment::Center)
403                .position(TitlePosition::Bottom)
404        );
405    }
406
407    #[test]
408    fn common_should_get_block() {
409        let mut props = CommonProps::default();
410
411        assert!(props.get_block().is_none());
412
413        props.set(
414            Attribute::Borders,
415            AttrValue::Borders(
416                Borders::default()
417                    .color(Color::Black)
418                    .modifiers(BorderType::Double)
419                    .sides(BorderSides::TOP),
420            ),
421        );
422        props.set(
423            Attribute::Title,
424            AttrValue::Title(
425                Title::default()
426                    .content("Hello".into())
427                    .alignment(HorizontalAlignment::Center)
428                    .position(TitlePosition::Bottom),
429            ),
430        );
431
432        // unfocused, no set inactive style
433        let block = props.get_block().unwrap();
434        assert_eq!(
435            block,
436            Block::new()
437                .title_bottom(LineStatic::from("Hello").centered())
438                .borders(BorderSides::TOP)
439                .border_type(BorderType::Double)
440        );
441
442        // focused, no set inactive style
443        props.set(Attribute::Focus, AttrValue::Flag(true));
444
445        let block = props.get_block().unwrap();
446        assert_eq!(
447            block,
448            Block::new()
449                .border_style(Style::new().black())
450                .title_bottom(LineStatic::from("Hello").centered())
451                .borders(BorderSides::TOP)
452                .border_type(BorderType::Double)
453        );
454    }
455
456    #[test]
457    fn block_should_be_active_with_alwaysactive() {
458        let mut props = CommonProps::default();
459        props.set(
460            Attribute::Borders,
461            AttrValue::Borders(
462                Borders::default()
463                    .color(Color::Black)
464                    .modifiers(BorderType::Double)
465                    .sides(BorderSides::TOP),
466            ),
467        );
468        props.set(
469            Attribute::Title,
470            AttrValue::Title(
471                Title::default()
472                    .content("Hello".into())
473                    .alignment(HorizontalAlignment::Center)
474                    .position(TitlePosition::Bottom),
475            ),
476        );
477        props.set(
478            Attribute::UnfocusedBorderStyle,
479            AttrValue::Style(Style::new().gray()),
480        );
481        props.set(Attribute::AlwaysActive, AttrValue::Flag(true));
482
483        let block = props.get_block().unwrap();
484
485        assert_eq!(
486            block,
487            Block::new()
488                .border_style(Style::new().black())
489                .title_bottom(LineStatic::from("Hello").centered())
490                .borders(BorderSides::TOP)
491                .border_type(BorderType::Double)
492        );
493    }
494
495    #[test]
496    fn common_highlight_should_have_expected_defaults() {
497        let props = CommonHighlight::default();
498
499        // test defaults
500        assert_eq!(
501            props.get(Attribute::HighlightStyle).unwrap().unwrap_style(),
502            Style::new().add_modifier(TextModifiers::REVERSED)
503        );
504        assert_eq!(
505            props
506                .get(Attribute::HighlightStyleUnfocused)
507                .unwrap()
508                .unwrap_style(),
509            Style::new()
510        );
511        assert_eq!(
512            props
513                .get(Attribute::HighlightedStr)
514                .unwrap()
515                .unwrap_textline(),
516            &LineStatic::default()
517        );
518    }
519
520    #[test]
521    fn common_highlight_should_get_set() {
522        let mut props = CommonHighlight::default();
523
524        // style via highlight style attribute
525        props.set(
526            Attribute::HighlightStyle,
527            AttrValue::Style(
528                Style::new()
529                    .fg(Color::Blue)
530                    .add_modifier(TextModifiers::DIM),
531            ),
532        );
533
534        assert_eq!(
535            props.get(Attribute::HighlightStyle).unwrap().unwrap_style(),
536            Style::new()
537                .fg(Color::Blue)
538                .add_modifier(TextModifiers::DIM)
539        );
540
541        // style unfocused via highlight style attribute
542        props.set(
543            Attribute::HighlightStyleUnfocused,
544            AttrValue::Style(Style::new().remove_modifier(TextModifiers::DIM)),
545        );
546
547        assert_eq!(
548            props
549                .get(Attribute::HighlightStyleUnfocused)
550                .unwrap()
551                .unwrap_style(),
552            Style::new().remove_modifier(TextModifiers::DIM)
553        );
554
555        // symbol via highlight symbol attribute
556        props.set(
557            Attribute::HighlightedStr,
558            AttrValue::TextLine(LineStatic::raw(">>")),
559        );
560
561        assert_eq!(
562            props
563                .get(Attribute::HighlightedStr)
564                .unwrap()
565                .unwrap_textline(),
566            &LineStatic::raw(">>")
567        );
568    }
569}