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    /// The symbol to use to indicate the currently selected element.
154    pub symbol: LineStatic,
155}
156
157impl Default for CommonHighlight {
158    fn default() -> Self {
159        Self {
160            style: Style::default().add_modifier(TextModifiers::REVERSED),
161            symbol: LineStatic::default(),
162        }
163    }
164}
165
166impl CommonHighlight {
167    /// Try to set a given [`Attribute`]. Returns `Some` if the value is unhandled. `None` if handled.
168    pub fn set(&mut self, attr: Attribute, value: AttrValue) -> Option<AttrValue> {
169        match (attr, value) {
170            (Attribute::HighlightStyle, AttrValue::Style(val)) => self.style = val,
171            (Attribute::HighlightedStr, AttrValue::TextLine(val)) => self.symbol = val,
172
173            // other
174            (_, value) => return Some(value),
175        }
176
177        None
178    }
179
180    /// Try to get a given [`Attribute`].
181    pub fn get<'a>(&'a self, attr: Attribute) -> Option<AttrValueRef<'a>> {
182        match attr {
183            Attribute::HighlightStyle => Some(AttrValueRef::Style(self.style)),
184            Attribute::HighlightedStr => Some(AttrValueRef::TextLine(&self.symbol)),
185
186            // other
187            _ => None,
188        }
189    }
190
191    /// Try to get a given [`Attribute`] as a type compatible with [`Component::query`](tuirealm::component::Component::query).
192    #[inline]
193    pub fn get_for_query<'a>(&'a self, attr: Attribute) -> Option<QueryResult<'a>> {
194        self.get(attr).map(QueryResult::Borrowed)
195    }
196
197    pub fn get_symbol(&self) -> Option<Line<'_>> {
198        if self.symbol.spans.is_empty() {
199            None
200        } else {
201            Some(borrow_clone_line(&self.symbol))
202        }
203    }
204
205    #[inline]
206    pub fn get_style(&self, normal_style: Style) -> Style {
207        normal_style.patch(self.style)
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use pretty_assertions::assert_eq;
214    use tuirealm::props::{
215        AttrValue, Attribute, BorderSides, BorderType, Borders, Color, HorizontalAlignment,
216        LineStatic, Style, TextModifiers, Title,
217    };
218    use tuirealm::ratatui::widgets::{Block, TitlePosition};
219
220    use crate::prop_ext::{CommonHighlight, CommonProps};
221
222    #[test]
223    fn common_should_have_expected_defaults() {
224        let props = CommonProps::default();
225
226        // test defaults
227        assert_eq!(
228            props.get(Attribute::Style).unwrap().unwrap_style(),
229            Style::new()
230        );
231        assert_eq!(
232            props
233                .get(Attribute::UnfocusedBorderStyle)
234                .unwrap()
235                .unwrap_style(),
236            Style::new()
237        );
238        assert!(props.get(Attribute::Foreground).is_none());
239        assert!(props.get(Attribute::Background).is_none());
240        assert_eq!(
241            props
242                .get(Attribute::TextProps)
243                .unwrap()
244                .unwrap_text_modifiers(),
245            TextModifiers::default()
246        );
247
248        assert!(props.get(Attribute::Display).unwrap().unwrap_flag());
249        assert!(!props.get(Attribute::Focus).unwrap().unwrap_flag());
250        assert!(!props.get(Attribute::AlwaysActive).unwrap().unwrap_flag());
251
252        assert!(props.get(Attribute::Borders).is_none());
253        assert!(props.get(Attribute::Title).is_none());
254    }
255
256    #[test]
257    fn common_should_get_set() {
258        let mut props = CommonProps::default();
259
260        // style via individual attributes
261        props.set(Attribute::Foreground, AttrValue::Color(Color::Black));
262        props.set(Attribute::Background, AttrValue::Color(Color::Gray));
263        props.set(
264            Attribute::TextProps,
265            AttrValue::TextModifiers(TextModifiers::BOLD),
266        );
267
268        assert_eq!(
269            props.get(Attribute::Style).unwrap().unwrap_style(),
270            Style::new()
271                .fg(Color::Black)
272                .bg(Color::Gray)
273                .add_modifier(TextModifiers::BOLD)
274        );
275        assert_eq!(
276            props.get(Attribute::Foreground).unwrap().unwrap_color(),
277            Color::Black
278        );
279        assert_eq!(
280            props.get(Attribute::Background).unwrap().unwrap_color(),
281            Color::Gray
282        );
283        assert_eq!(
284            props
285                .get(Attribute::TextProps)
286                .unwrap()
287                .unwrap_text_modifiers(),
288            TextModifiers::BOLD
289        );
290
291        // style via style attribute
292        props.set(
293            Attribute::Style,
294            AttrValue::Style(
295                Style::new()
296                    .fg(Color::Blue)
297                    .bg(Color::DarkGray)
298                    .add_modifier(TextModifiers::DIM),
299            ),
300        );
301
302        assert_eq!(
303            props.get(Attribute::Style).unwrap().unwrap_style(),
304            Style::new()
305                .fg(Color::Blue)
306                .bg(Color::DarkGray)
307                .add_modifier(TextModifiers::DIM)
308        );
309        assert_eq!(
310            props.get(Attribute::Foreground).unwrap().unwrap_color(),
311            Color::Blue
312        );
313        assert_eq!(
314            props.get(Attribute::Background).unwrap().unwrap_color(),
315            Color::DarkGray
316        );
317        assert_eq!(
318            props
319                .get(Attribute::TextProps)
320                .unwrap()
321                .unwrap_text_modifiers(),
322            TextModifiers::DIM
323        );
324
325        // focus style
326        props.set(
327            Attribute::UnfocusedBorderStyle,
328            AttrValue::Style(Style::new().add_modifier(TextModifiers::REVERSED)),
329        );
330
331        assert_eq!(
332            props
333                .get(Attribute::UnfocusedBorderStyle)
334                .unwrap()
335                .unwrap_style(),
336            Style::new().add_modifier(TextModifiers::REVERSED)
337        );
338
339        // flags
340
341        props.set(Attribute::Display, AttrValue::Flag(false));
342        props.set(Attribute::Focus, AttrValue::Flag(true));
343        props.set(Attribute::AlwaysActive, AttrValue::Flag(true));
344
345        assert!(!props.get(Attribute::Display).unwrap().unwrap_flag());
346        assert!(props.get(Attribute::Focus).unwrap().unwrap_flag());
347        assert!(props.get(Attribute::AlwaysActive).unwrap().unwrap_flag());
348
349        // border & title
350
351        props.set(
352            Attribute::Borders,
353            AttrValue::Borders(
354                Borders::default()
355                    .color(Color::Black)
356                    .modifiers(BorderType::Double)
357                    .sides(BorderSides::TOP),
358            ),
359        );
360        props.set(
361            Attribute::Title,
362            AttrValue::Title(
363                Title::default()
364                    .content("Hello".into())
365                    .alignment(HorizontalAlignment::Center)
366                    .position(TitlePosition::Bottom),
367            ),
368        );
369
370        assert_eq!(
371            props.get(Attribute::Borders).unwrap().unwrap_borders(),
372            Borders::default()
373                .color(Color::Black)
374                .modifiers(BorderType::Double)
375                .sides(BorderSides::TOP)
376        );
377        assert_eq!(
378            props.get(Attribute::Title).unwrap().unwrap_title(),
379            &Title::default()
380                .content("Hello".into())
381                .alignment(HorizontalAlignment::Center)
382                .position(TitlePosition::Bottom)
383        );
384    }
385
386    #[test]
387    fn common_should_get_block() {
388        let mut props = CommonProps::default();
389
390        assert!(props.get_block().is_none());
391
392        props.set(
393            Attribute::Borders,
394            AttrValue::Borders(
395                Borders::default()
396                    .color(Color::Black)
397                    .modifiers(BorderType::Double)
398                    .sides(BorderSides::TOP),
399            ),
400        );
401        props.set(
402            Attribute::Title,
403            AttrValue::Title(
404                Title::default()
405                    .content("Hello".into())
406                    .alignment(HorizontalAlignment::Center)
407                    .position(TitlePosition::Bottom),
408            ),
409        );
410
411        // unfocused, no set inactive style
412        let block = props.get_block().unwrap();
413        assert_eq!(
414            block,
415            Block::new()
416                .title_bottom(LineStatic::from("Hello").centered())
417                .borders(BorderSides::TOP)
418                .border_type(BorderType::Double)
419        );
420
421        // focused, no set inactive style
422        props.set(Attribute::Focus, AttrValue::Flag(true));
423
424        let block = props.get_block().unwrap();
425        assert_eq!(
426            block,
427            Block::new()
428                .border_style(Style::new().black())
429                .title_bottom(LineStatic::from("Hello").centered())
430                .borders(BorderSides::TOP)
431                .border_type(BorderType::Double)
432        );
433    }
434
435    #[test]
436    fn block_should_be_active_with_alwaysactive() {
437        let mut props = CommonProps::default();
438        props.set(
439            Attribute::Borders,
440            AttrValue::Borders(
441                Borders::default()
442                    .color(Color::Black)
443                    .modifiers(BorderType::Double)
444                    .sides(BorderSides::TOP),
445            ),
446        );
447        props.set(
448            Attribute::Title,
449            AttrValue::Title(
450                Title::default()
451                    .content("Hello".into())
452                    .alignment(HorizontalAlignment::Center)
453                    .position(TitlePosition::Bottom),
454            ),
455        );
456        props.set(
457            Attribute::UnfocusedBorderStyle,
458            AttrValue::Style(Style::new().gray()),
459        );
460        props.set(Attribute::AlwaysActive, AttrValue::Flag(true));
461
462        let block = props.get_block().unwrap();
463
464        assert_eq!(
465            block,
466            Block::new()
467                .border_style(Style::new().black())
468                .title_bottom(LineStatic::from("Hello").centered())
469                .borders(BorderSides::TOP)
470                .border_type(BorderType::Double)
471        );
472    }
473
474    #[test]
475    fn common_highlight_should_have_expected_defaults() {
476        let props = CommonHighlight::default();
477
478        // test defaults
479        assert_eq!(
480            props.get(Attribute::HighlightStyle).unwrap().unwrap_style(),
481            Style::new().add_modifier(TextModifiers::REVERSED)
482        );
483        assert_eq!(
484            props
485                .get(Attribute::HighlightedStr)
486                .unwrap()
487                .unwrap_textline(),
488            &LineStatic::default()
489        );
490    }
491
492    #[test]
493    fn common_highlight_should_get_set() {
494        let mut props = CommonHighlight::default();
495
496        // style via highlight style attribute
497        props.set(
498            Attribute::HighlightStyle,
499            AttrValue::Style(
500                Style::new()
501                    .fg(Color::Blue)
502                    .add_modifier(TextModifiers::DIM),
503            ),
504        );
505
506        assert_eq!(
507            props.get(Attribute::HighlightStyle).unwrap().unwrap_style(),
508            Style::new()
509                .fg(Color::Blue)
510                .add_modifier(TextModifiers::DIM)
511        );
512
513        // symbol via highlight symbol attribute
514        props.set(
515            Attribute::HighlightedStr,
516            AttrValue::TextLine(LineStatic::raw(">>")),
517        );
518
519        assert_eq!(
520            props
521                .get(Attribute::HighlightedStr)
522                .unwrap()
523                .unwrap_textline(),
524            &LineStatic::raw(">>")
525        );
526    }
527}