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#[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 pub fn new(text: impl Into<String>) -> Self {
42 Self::with_content(Content::Static(text.into()))
43 }
44
45 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 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 pub fn style(mut self, style: TextStyle) -> Self {
69 self.style = style;
70 self
71 }
72
73 pub fn font_size(mut self, size: f32) -> Self {
75 self.size = Some(size);
76 self
77 }
78
79 pub fn font_weight(mut self, weight: Weight) -> Self {
81 self.weight = Some(weight);
82 self
83 }
84
85 pub fn truncate(mut self) -> Self {
88 self.truncate = true;
89 self
90 }
91
92 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
128pub(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}