Skip to main content

fission_core/ui/widgets/
text.rs

1use crate::lowering::{LoweringContext, NodeBuilder};
2use crate::ui::traits::Lower;
3use fission_ir::{
4    op::{Color as IrColor, LayoutOp, Op, PaintOp},
5    NodeId, Semantics,
6};
7use serde::{Deserialize, Serialize};
8
9/// The content source for a [`Text`] widget.
10///
11/// `Literal` renders a plain string. `Key` looks up a localised string in the
12/// i18n registry at build time.
13///
14/// # Example
15///
16/// ```rust,ignore
17/// // Literal text
18/// Text::new("Hello, world!");
19///
20/// // i18n key (resolved via the active locale)
21/// Text { content: TextContent::Key("greeting_label".into()), ..Default::default() }
22/// ```
23#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
24pub enum TextContent {
25    /// A plain, inline string.
26    Literal(String),
27    /// An i18n key resolved at build time.
28    Key(String),
29}
30
31impl From<&str> for TextContent {
32    fn from(value: &str) -> Self {
33        TextContent::Literal(value.to_string())
34    }
35}
36
37impl From<String> for TextContent {
38    fn from(value: String) -> Self {
39        TextContent::Literal(value)
40    }
41}
42
43impl Default for TextContent {
44    fn default() -> Self {
45        TextContent::Literal(String::new())
46    }
47}
48
49/// A read-only text label.
50///
51/// Renders a single run of styled text. Supports literal strings and i18n
52/// keys, custom font size, colour, underline, and flex properties.
53///
54/// # Example
55///
56/// ```rust,ignore
57/// Text::new("Total: 42")
58///     .size(18.0)
59///     .color(theme.tokens.colors.primary)
60///     .underline(true)
61///     .flex_grow(1.0)
62/// ```
63#[derive(Debug, Default, Clone, Serialize, Deserialize)]
64pub struct Text {
65    /// Explicit node identity.
66    pub id: Option<NodeId>,
67    /// The text content (literal string or i18n key).
68    pub content: TextContent,
69    /// Custom semantics for accessibility.
70    pub semantics: Option<Semantics>,
71    /// Fixed width in layout points.
72    pub width: Option<f32>,
73    /// Fixed height in layout points.
74    pub height: Option<f32>,
75    /// Minimum width constraint.
76    pub min_width: Option<f32>,
77    /// Maximum width constraint.
78    pub max_width: Option<f32>,
79    /// Minimum height constraint.
80    pub min_height: Option<f32>,
81    /// Maximum height constraint.
82    pub max_height: Option<f32>,
83    /// Font size in points (falls back to the theme's body size).
84    pub font_size: Option<f32>,
85    /// Text colour (falls back to the theme's primary text colour).
86    pub color: Option<IrColor>,
87    /// Whether to render an underline decoration.
88    pub underline: bool,
89    /// Flex grow factor (0.0 by default -- does not stretch).
90    pub flex_grow: f32,
91    /// Flex shrink factor (0.0 by default -- does not shrink).
92    pub flex_shrink: f32,
93}
94
95impl Text {
96    pub fn new(content: impl Into<TextContent>) -> Self {
97        Self {
98            content: content.into(),
99            ..Default::default()
100        }
101    }
102
103    pub fn width(mut self, w: f32) -> Self {
104        self.width = Some(w);
105        self
106    }
107
108    pub fn height(mut self, h: f32) -> Self {
109        self.height = Some(h);
110        self
111    }
112
113    pub fn min_width(mut self, w: f32) -> Self {
114        self.min_width = Some(w);
115        self
116    }
117    
118    pub fn max_width(mut self, w: f32) -> Self {
119        self.max_width = Some(w);
120        self
121    }
122
123    pub fn min_height(mut self, h: f32) -> Self {
124        self.min_height = Some(h);
125        self
126    }
127
128    pub fn max_height(mut self, h: f32) -> Self {
129        self.max_height = Some(h);
130        self
131    }
132
133    pub fn flex_grow(mut self, grow: f32) -> Self {
134        self.flex_grow = grow;
135        self
136    }
137
138    pub fn flex_shrink(mut self, shrink: f32) -> Self {
139        self.flex_shrink = shrink;
140        self
141    }
142
143    pub fn color(mut self, color: IrColor) -> Self {
144        self.color = Some(color);
145        self
146    }
147
148    pub fn underline(mut self, u: bool) -> Self {
149        self.underline = u;
150        self
151    }
152
153    pub fn size(mut self, size: f32) -> Self {
154        self.font_size = Some(size);
155        self
156    }
157
158    
159    // Stub for weight until we add font support to IR
160    pub fn weight(self, _w: impl std::fmt::Debug) -> Self {
161        self
162    }
163    
164    pub fn into_node(self) -> crate::ui::Node {
165        crate::ui::Node::Text(self)
166    }
167}
168
169impl Lower for Text {
170    fn lower(&self, cx: &mut LoweringContext) -> NodeId {
171        let layout_node_id = self.id.unwrap_or_else(|| cx.next_node_id());
172
173        let resolved_text = match &self.content {
174            TextContent::Literal(s) => s.clone(),
175            TextContent::Key(key) => cx
176                .env
177                .i18n
178                .get(&cx.env.locale, key)
179                .map(|s| s.to_string())
180                .unwrap_or_else(|| format!("MISSING:{}", key)),
181        };
182
183        let paint_node_id = NodeBuilder::new(
184            cx.next_node_id(),
185            Op::Paint(PaintOp::DrawText {
186                text: resolved_text,
187                size: self.font_size.unwrap_or(cx.env.theme.tokens.typography.body_medium_size),
188                color: self.color.unwrap_or(cx.env.theme.tokens.colors.text_primary),
189                underline: self.underline,
190                caret_index: None,
191            }),
192        )
193        .build(cx);
194
195        let mut layout_builder = NodeBuilder::new(
196            layout_node_id,
197            Op::Layout(LayoutOp::Box {
198                width: self.width,
199                height: self.height,
200                min_width: self.min_width,
201                max_width: self.max_width,
202                min_height: self.min_height,
203                max_height: self.max_height,
204                padding: [0.0; 4],
205                flex_grow: self.flex_grow,
206                flex_shrink: self.flex_shrink,
207                aspect_ratio: None,
208            }),
209        );
210        layout_builder.add_child(paint_node_id);
211        let layout_node_id = layout_builder.build(cx);
212
213        if let Some(mut s) = self.semantics.clone() {
214            s.multiline = false;
215            let mut semantics_builder =
216                NodeBuilder::new(cx.next_node_id(), Op::Semantics(s));
217            semantics_builder.add_child(layout_node_id);
218            return semantics_builder.build(cx);
219        }
220
221        layout_node_id
222    }
223}