Skip to main content

ravenrs_widgets/
text.rs

1use std::fmt;
2use std::rc::Rc;
3
4use ravenrs_layout::LayoutStyle;
5use ravenrs_reactive::State;
6use ravenrs_style::{Semantic, TextStyle, Weight};
7use ravenrs_text::TextAttrs;
8
9use crate::context::{BuildCx, NodeId, Semantics};
10use crate::{Fill, View};
11
12enum Content {
13    Static(String),
14    Dynamic(Rc<dyn Fn() -> String>),
15}
16
17/// Text in the theme's type scale. It wraps to the width its container offers.
18#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
19pub struct Text {
20    content: Content,
21    style: TextStyle,
22    size: Option<f32>,
23    weight: Option<Weight>,
24    foreground: Fill,
25    truncate: bool,
26}
27
28impl fmt::Debug for Text {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        let content = match &self.content {
31            Content::Static(s) => s.as_str(),
32            Content::Dynamic(_) => "<dynamic>",
33        };
34        f.debug_struct("Text").field("content", &content).field("style", &self.style).finish()
35    }
36}
37
38#[cfg_attr(feature = "live", ravenrs_live::expose)]
39impl Text {
40    /// Static text.
41    pub fn new(text: impl Into<String>) -> Self {
42        Self::with_content(Content::Static(text.into()))
43    }
44
45    /// Text derived from a state; the view updates when the state changes.
46    pub fn bound<T: 'static>(state: &State<T>, format: impl Fn(&T) -> String + 'static) -> Self {
47        let state = state.clone();
48        Self::with_content(Content::Dynamic(Rc::new(move || state.with(&format))))
49    }
50
51    /// Text computed by a closure on every build; any state it reads is tracked.
52    pub fn dynamic(f: impl Fn() -> String + 'static) -> Self {
53        Self::with_content(Content::Dynamic(Rc::new(f)))
54    }
55
56    fn with_content(content: Content) -> Self {
57        Self {
58            content,
59            style: TextStyle::Body,
60            size: None,
61            weight: None,
62            foreground: Semantic::Ink.into(),
63            truncate: false,
64        }
65    }
66
67    /// A step of the type scale (`Body` by default).
68    pub fn style(mut self, style: TextStyle) -> Self {
69        self.style = style;
70        self
71    }
72
73    /// Overrides the style's size; line height scales with it.
74    pub fn font_size(mut self, size: f32) -> Self {
75        self.size = Some(size);
76        self
77    }
78
79    /// Overrides the style's weight.
80    pub fn font_weight(mut self, weight: Weight) -> Self {
81        self.weight = Some(weight);
82        self
83    }
84
85    /// Keeps the text on one line, ended with `…` when it does not fit. Screen
86    /// readers still get the whole text.
87    pub fn truncate(mut self) -> Self {
88        self.truncate = true;
89        self
90    }
91
92    /// Text color, as a theme role (`Ink` by default) or a literal color.
93    pub fn foreground(mut self, fill: impl Into<Fill>) -> Self {
94        self.foreground = fill.into();
95        self
96    }
97}
98
99impl View for Text {
100    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
101        let theme = cx.theme();
102        let base = theme.text(self.style);
103        let size = self.size.unwrap_or(base.size);
104        let attrs = TextAttrs {
105            size,
106            line_height: base.line_height * size / base.size,
107            weight: self.weight.unwrap_or(base.weight).0,
108            letter_spacing: base.letter_spacing,
109            family: None,
110        };
111        let color = self.foreground.resolve(theme);
112        let content = match &self.content {
113            Content::Static(s) => std::borrow::Cow::Borrowed(s.as_str()),
114            Content::Dynamic(f) => std::borrow::Cow::Owned(f()),
115        };
116        let node = if self.truncate {
117            cx.text_truncated(&content, &attrs, color, LayoutStyle::leaf())
118        } else {
119            cx.text(&content, &attrs, color, LayoutStyle::leaf())
120        };
121        if let Some(level) = heading_level(self.style) {
122            cx.semantics(node, Semantics::new(ravenrs_core::Role::Heading).level(level));
123        }
124        node
125    }
126}
127
128/// The heading level screen readers announce for large text styles.
129pub(crate) fn heading_level(style: TextStyle) -> Option<u32> {
130    match style {
131        TextStyle::Display => Some(1),
132        TextStyle::Title => Some(2),
133        TextStyle::Headline => Some(3),
134        _ => None,
135    }
136}