Skip to main content

a3s_tui/
element.rs

1//! Element tree and styling primitives for declarative UI.
2//!
3//! This module provides the core building blocks for constructing terminal UIs:
4//! - [`Element`]: The main UI tree structure (Box, Text, Spacer)
5//! - [`BoxElement`]: Container with Flexbox layout
6//! - [`TextElement`]: Styled text content
7//! - Layout enums: [`FlexDirection`], [`AlignItems`], [`JustifyContent`]
8//! - Styling types: [`Dimension`], [`Edges`], [`BorderStyle`]
9
10use crate::style::Color;
11use std::marker::PhantomData;
12
13/// Flexbox layout direction (row or column).
14///
15/// # Examples
16///
17/// ```
18/// use a3s_tui::element::{BoxElement, FlexDirection};
19///
20/// let row: BoxElement<()> = BoxElement::new().direction(FlexDirection::Row);
21/// let col: BoxElement<()> = BoxElement::new().direction(FlexDirection::Column);
22/// ```
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
24pub enum FlexDirection {
25    /// Horizontal layout (left to right).
26    #[default]
27    Row,
28    /// Vertical layout (top to bottom).
29    Column,
30}
31
32/// Cross-axis alignment for flex items.
33///
34/// Controls how items are aligned perpendicular to the flex direction.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
36pub enum AlignItems {
37    /// Align to the start of the cross axis.
38    Start,
39    /// Center items on the cross axis.
40    Center,
41    /// Align to the end of the cross axis.
42    End,
43    /// Stretch items to fill the cross axis (default).
44    #[default]
45    Stretch,
46}
47
48/// Main-axis alignment for flex items.
49///
50/// Controls how items are distributed along the flex direction.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
52pub enum JustifyContent {
53    /// Pack items at the start (default).
54    #[default]
55    Start,
56    /// Center items.
57    Center,
58    /// Pack items at the end.
59    End,
60    /// Distribute items with space between them.
61    SpaceBetween,
62    /// Distribute items with space around them.
63    SpaceAround,
64    /// Distribute items with equal space around them.
65    SpaceEvenly,
66}
67
68/// Size dimension (auto, fixed points, or percentage).
69///
70/// # Examples
71///
72/// ```
73/// use a3s_tui::element::Dimension;
74///
75/// let auto = Dimension::Auto;
76/// let fixed = Dimension::Points(100.0);
77/// let percent = Dimension::Percent(50.0);
78/// ```
79#[derive(Debug, Clone, Copy, PartialEq, Default)]
80pub enum Dimension {
81    /// Automatically calculate size based on content.
82    #[default]
83    Auto,
84    /// Fixed size in terminal cells.
85    Points(f32),
86    /// Percentage of parent size (0.0 to 100.0).
87    Percent(f32),
88}
89
90/// Overflow behavior for content that exceeds container bounds.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
92pub enum Overflow {
93    /// Content overflows visibly (default).
94    #[default]
95    Visible,
96    /// Clip content at container bounds.
97    Hidden,
98}
99
100/// Text wrapping behavior.
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum TextWrap {
103    /// Wrap text at word boundaries.
104    Wrap,
105    /// Truncate text with ellipsis.
106    Truncate,
107    /// No wrapping (overflow).
108    NoWrap,
109}
110
111/// Border style for box elements.
112///
113/// # Examples
114///
115/// ```
116/// use a3s_tui::element::{BoxElement, BorderStyle};
117///
118/// let box_elem: BoxElement<()> = BoxElement::new().border(BorderStyle::Rounded);
119/// ```
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub enum BorderStyle {
122    /// Single-line border (─│┌┐└┘).
123    Single,
124    /// Double-line border (═║╔╗╚╝).
125    Double,
126    /// Rounded corners (─│╭╮╰╯).
127    Rounded,
128    /// Thick border (━┃┏┓┗┛).
129    Thick,
130}
131
132/// Spacing values for padding and margin (top, right, bottom, left).
133#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
134pub struct Edges {
135    pub top: u16,
136    pub right: u16,
137    pub bottom: u16,
138    pub left: u16,
139}
140
141impl Edges {
142    /// Set all four edges to the same value.
143    pub fn all(v: u16) -> Self {
144        Self {
145            top: v,
146            right: v,
147            bottom: v,
148            left: v,
149        }
150    }
151    /// Set horizontal (x) and vertical (y) edges separately.
152    pub fn xy(x: u16, y: u16) -> Self {
153        Self {
154            top: y,
155            right: x,
156            bottom: y,
157            left: x,
158        }
159    }
160}
161
162/// Text styling properties (color, weight, decoration).
163#[derive(Debug, Clone, Default)]
164pub struct TextStyle {
165    pub fg: Option<Color>,
166    pub bg: Option<Color>,
167    pub bold: bool,
168    pub italic: bool,
169    pub underline: bool,
170    pub reverse: bool,
171    pub dim: bool,
172    pub strikethrough: bool,
173}
174
175/// Flexbox container style properties.
176#[derive(Debug, Clone, Default)]
177pub struct BoxStyle {
178    pub flex_direction: FlexDirection,
179    pub padding: Edges,
180    pub margin: Edges,
181    pub border: Option<BorderStyle>,
182    pub border_color: Option<Color>,
183    pub gap: u16,
184    pub align_items: AlignItems,
185    pub justify_content: JustifyContent,
186    pub width: Dimension,
187    pub height: Dimension,
188    pub flex_grow: f32,
189    pub flex_shrink: f32,
190    pub flex_basis: Dimension,
191    pub min_width: Dimension,
192    pub max_width: Dimension,
193    pub min_height: Dimension,
194    pub max_height: Dimension,
195    pub bg: Option<Color>,
196    pub overflow: Overflow,
197}
198
199/// A node in the UI element tree.
200///
201/// Elements are the building blocks of the declarative UI. Use `col![]` and `row![]`
202/// macros for concise construction.
203pub enum Element<Msg> {
204    /// A container with Flexbox layout.
205    Box(BoxElement<Msg>),
206    /// Styled text content.
207    Text(TextElement),
208    /// Flexible space that expands to fill available room.
209    Spacer,
210    _Phantom(PhantomData<Msg>),
211}
212
213impl<Msg> Element<Msg> {
214    /// Create a text element.
215    pub fn text(content: impl Into<String>) -> Self {
216        Element::Text(TextElement::new(content))
217    }
218
219    /// Check if this is a Box element.
220    pub fn is_box(&self) -> bool {
221        matches!(self, Element::Box(_))
222    }
223
224    /// Check if this is a Text element.
225    pub fn is_text(&self) -> bool {
226        matches!(self, Element::Text(_))
227    }
228
229    /// Check if this is a Spacer.
230    pub fn is_spacer(&self) -> bool {
231        matches!(self, Element::Spacer)
232    }
233
234    /// Get the text content if this is a Text element.
235    pub fn text_content(&self) -> Option<&str> {
236        match self {
237            Element::Text(t) => Some(&t.content),
238            _ => None,
239        }
240    }
241
242    /// Get the children if this is a Box element.
243    pub fn children(&self) -> Option<&[Element<Msg>]> {
244        match self {
245            Element::Box(b) => Some(&b.children),
246            _ => None,
247        }
248    }
249
250    /// Count total children (0 for non-Box elements).
251    pub fn child_count(&self) -> usize {
252        match self {
253            Element::Box(b) => b.children.len(),
254            _ => 0,
255        }
256    }
257}
258
259/// A Flexbox container element that holds child elements.
260///
261/// Use the builder pattern to configure layout and styling:
262///
263/// ```
264/// use a3s_tui::element::{BoxElement, FlexDirection, BorderStyle, Dimension};
265///
266/// let container = BoxElement::<()>::new()
267///     .direction(FlexDirection::Column)
268///     .padding(1)
269///     .gap(1)
270///     .border(BorderStyle::Rounded)
271///     .width(Dimension::Percent(100.0));
272/// ```
273pub struct BoxElement<Msg> {
274    pub children: Vec<Element<Msg>>,
275    pub style: BoxStyle,
276    _phantom: PhantomData<Msg>,
277}
278
279/// A styled text element.
280///
281/// ```
282/// use a3s_tui::element::TextElement;
283/// use a3s_tui::style::Color;
284///
285/// let text = TextElement::new("Hello, world!")
286///     .bold()
287///     .fg(Color::Cyan);
288/// ```
289pub struct TextElement {
290    pub content: String,
291    pub style: TextStyle,
292    pub wrap: TextWrap,
293}
294
295impl<Msg> BoxElement<Msg> {
296    pub fn new() -> Self {
297        Self {
298            children: Vec::new(),
299            style: BoxStyle::default(),
300            _phantom: PhantomData,
301        }
302    }
303
304    /// Create a row container (flex-direction: row).
305    pub fn row() -> Self {
306        Self::new().direction(FlexDirection::Row)
307    }
308
309    /// Create a column container (flex-direction: column).
310    pub fn col() -> Self {
311        Self::new().direction(FlexDirection::Column)
312    }
313
314    pub fn direction(mut self, d: FlexDirection) -> Self {
315        self.style.flex_direction = d;
316        self
317    }
318
319    pub fn child(mut self, el: Element<Msg>) -> Self {
320        self.children.push(el);
321        self
322    }
323
324    pub fn children(mut self, els: Vec<Element<Msg>>) -> Self {
325        self.children = els;
326        self
327    }
328
329    pub fn padding(mut self, all: u16) -> Self {
330        self.style.padding = Edges::all(all);
331        self
332    }
333
334    pub fn padding_xy(mut self, x: u16, y: u16) -> Self {
335        self.style.padding = Edges::xy(x, y);
336        self
337    }
338
339    pub fn margin(mut self, all: u16) -> Self {
340        self.style.margin = Edges::all(all);
341        self
342    }
343
344    pub fn border(mut self, style: BorderStyle) -> Self {
345        self.style.border = Some(style);
346        self
347    }
348
349    pub fn border_color(mut self, c: Color) -> Self {
350        self.style.border_color = Some(c);
351        self
352    }
353
354    pub fn gap(mut self, g: u16) -> Self {
355        self.style.gap = g;
356        self
357    }
358
359    pub fn flex_grow(mut self, g: f32) -> Self {
360        self.style.flex_grow = g;
361        self
362    }
363
364    pub fn flex_shrink(mut self, s: f32) -> Self {
365        self.style.flex_shrink = s;
366        self
367    }
368
369    pub fn flex_basis(mut self, d: Dimension) -> Self {
370        self.style.flex_basis = d;
371        self
372    }
373
374    pub fn width(mut self, d: Dimension) -> Self {
375        self.style.width = d;
376        self
377    }
378
379    pub fn height(mut self, d: Dimension) -> Self {
380        self.style.height = d;
381        self
382    }
383
384    pub fn min_width(mut self, d: Dimension) -> Self {
385        self.style.min_width = d;
386        self
387    }
388
389    pub fn max_width(mut self, d: Dimension) -> Self {
390        self.style.max_width = d;
391        self
392    }
393
394    pub fn min_height(mut self, d: Dimension) -> Self {
395        self.style.min_height = d;
396        self
397    }
398
399    pub fn max_height(mut self, d: Dimension) -> Self {
400        self.style.max_height = d;
401        self
402    }
403
404    pub fn bg(mut self, c: Color) -> Self {
405        self.style.bg = Some(c);
406        self
407    }
408
409    pub fn align_items(mut self, a: AlignItems) -> Self {
410        self.style.align_items = a;
411        self
412    }
413
414    pub fn justify_content(mut self, j: JustifyContent) -> Self {
415        self.style.justify_content = j;
416        self
417    }
418
419    pub fn overflow(mut self, o: Overflow) -> Self {
420        self.style.overflow = o;
421        self
422    }
423}
424
425impl<Msg> Default for BoxElement<Msg> {
426    fn default() -> Self {
427        Self::new()
428    }
429}
430
431impl TextElement {
432    pub fn new(content: impl Into<String>) -> Self {
433        Self {
434            content: content.into(),
435            style: TextStyle::default(),
436            wrap: TextWrap::Wrap,
437        }
438    }
439
440    pub fn fg(mut self, c: Color) -> Self {
441        self.style.fg = Some(c);
442        self
443    }
444
445    pub fn bg(mut self, c: Color) -> Self {
446        self.style.bg = Some(c);
447        self
448    }
449
450    pub fn bold(mut self) -> Self {
451        self.style.bold = true;
452        self
453    }
454
455    pub fn italic(mut self) -> Self {
456        self.style.italic = true;
457        self
458    }
459
460    pub fn underline(mut self) -> Self {
461        self.style.underline = true;
462        self
463    }
464
465    pub fn reverse(mut self) -> Self {
466        self.style.reverse = true;
467        self
468    }
469
470    pub fn dim(mut self) -> Self {
471        self.style.dim = true;
472        self
473    }
474
475    pub fn strikethrough(mut self) -> Self {
476        self.style.strikethrough = true;
477        self
478    }
479
480    pub fn wrap(mut self, w: TextWrap) -> Self {
481        self.wrap = w;
482        self
483    }
484}
485
486#[cfg(test)]
487mod tests {
488    use super::*;
489
490    #[test]
491    fn edges_all() {
492        let e = Edges::all(5);
493        assert_eq!(e.top, 5);
494        assert_eq!(e.right, 5);
495        assert_eq!(e.bottom, 5);
496        assert_eq!(e.left, 5);
497    }
498
499    #[test]
500    fn edges_xy() {
501        let e = Edges::xy(3, 7);
502        assert_eq!(e.left, 3);
503        assert_eq!(e.right, 3);
504        assert_eq!(e.top, 7);
505        assert_eq!(e.bottom, 7);
506    }
507
508    #[test]
509    fn text_element_builder() {
510        let t = TextElement::new("hello")
511            .bold()
512            .italic()
513            .reverse()
514            .fg(Color::Red)
515            .bg(Color::Blue);
516        assert_eq!(t.content, "hello");
517        assert!(t.style.bold);
518        assert!(t.style.italic);
519        assert!(t.style.reverse);
520        assert_eq!(t.style.fg, Some(Color::Red));
521        assert_eq!(t.style.bg, Some(Color::Blue));
522    }
523
524    #[test]
525    fn box_element_builder() {
526        let b = BoxElement::<()>::new()
527            .direction(FlexDirection::Column)
528            .padding(2)
529            .gap(1)
530            .border(BorderStyle::Rounded)
531            .bg(Color::Black);
532        assert_eq!(b.style.flex_direction, FlexDirection::Column);
533        assert_eq!(b.style.padding, Edges::all(2));
534        assert_eq!(b.style.gap, 1);
535        assert_eq!(b.style.border, Some(BorderStyle::Rounded));
536        assert_eq!(b.style.bg, Some(Color::Black));
537    }
538
539    #[test]
540    fn box_element_children() {
541        let b = BoxElement::<()>::new()
542            .child(Element::Text(TextElement::new("a")))
543            .child(Element::Text(TextElement::new("b")))
544            .child(Element::Spacer);
545        assert_eq!(b.children.len(), 3);
546    }
547
548    #[test]
549    fn box_element_flex_properties() {
550        let b = BoxElement::<()>::new()
551            .flex_grow(2.0)
552            .flex_shrink(0.5)
553            .flex_basis(Dimension::Points(20.0))
554            .width(Dimension::Points(100.0))
555            .height(Dimension::Percent(50.0))
556            .min_width(Dimension::Points(10.0))
557            .max_width(Dimension::Points(120.0))
558            .min_height(Dimension::Points(4.0))
559            .max_height(Dimension::Points(60.0));
560        assert_eq!(b.style.flex_grow, 2.0);
561        assert_eq!(b.style.flex_shrink, 0.5);
562        assert_eq!(b.style.flex_basis, Dimension::Points(20.0));
563        assert_eq!(b.style.width, Dimension::Points(100.0));
564        assert_eq!(b.style.height, Dimension::Percent(50.0));
565        assert_eq!(b.style.min_width, Dimension::Points(10.0));
566        assert_eq!(b.style.max_width, Dimension::Points(120.0));
567        assert_eq!(b.style.min_height, Dimension::Points(4.0));
568        assert_eq!(b.style.max_height, Dimension::Points(60.0));
569    }
570}