Skip to main content

textual_rs/css/
types.rs

1//! Core CSS value types used throughout the TCSS styling engine.
2
3use std::collections::HashSet;
4
5/// Controls how a widget participates in layout.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum TcssDisplay {
8    /// Flexbox layout (the default).
9    Flex,
10    /// CSS grid layout.
11    Grid,
12    /// Block layout (stacked vertically).
13    Block,
14    /// Widget is not rendered and takes no space.
15    None,
16}
17
18/// A CSS dimension value for sizing properties.
19#[derive(Debug, Clone, Copy, PartialEq)]
20pub enum TcssDimension {
21    /// Size is determined by the layout algorithm.
22    Auto,
23    /// Fixed cell-count size.
24    Length(f32),
25    /// Size as a percentage of the parent container.
26    Percent(f32),
27    /// Fractional unit for proportional flex sizing.
28    Fraction(f32),
29}
30
31/// Layout flow direction for flex containers.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum LayoutDirection {
34    /// Children are stacked top-to-bottom.
35    Vertical,
36    /// Children are arranged left-to-right.
37    Horizontal,
38}
39
40/// Border rendering style for widgets.
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum BorderStyle {
43    /// No border is drawn.
44    None,
45    /// Standard single-line box-drawing border.
46    Solid,
47    /// Rounded corners using arc box-drawing characters.
48    Rounded,
49    /// Heavy double-width box-drawing border.
50    Heavy,
51    /// Double-line box-drawing border.
52    Double,
53    /// ASCII-art border using `+`, `-`, and `|` characters.
54    Ascii,
55    /// Half-block border (▀▄▐▌) — thin frames using half-block characters.
56    Tall,
57    /// McGugan Box — 1/8-cell-thick borders with independent inside/outside colors.
58    /// Uses one-eighth block characters (▁▔▎) for the thinnest possible border lines.
59    /// The signature Textual rendering technique.
60    McguganBox,
61}
62
63/// A color value in the TCSS engine.
64#[derive(Debug, Clone, Copy, PartialEq)]
65pub enum TcssColor {
66    /// Use the terminal's default color (transparent).
67    Reset,
68    /// An opaque RGB color.
69    Rgb(u8, u8, u8),
70    /// An RGBA color with an alpha channel (0–255).
71    Rgba(u8, u8, u8, u8),
72    /// A named color string (e.g., `"red"`).
73    Named(&'static str),
74}
75
76/// A CSS pseudo-class state flag for a widget.
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
78pub enum PseudoClass {
79    /// The widget currently holds keyboard focus.
80    Focus,
81    /// The mouse cursor is positioned over the widget.
82    Hover,
83    /// The widget is disabled and not interactable.
84    Disabled,
85}
86
87/// A set of active pseudo-classes for a widget.
88#[derive(Debug, Clone, Default, PartialEq, Eq)]
89pub struct PseudoClassSet(pub HashSet<PseudoClass>);
90
91impl PseudoClassSet {
92    /// Add a pseudo-class to the set.
93    pub fn insert(&mut self, cls: PseudoClass) {
94        self.0.insert(cls);
95    }
96
97    /// Remove a pseudo-class from the set.
98    pub fn remove(&mut self, cls: &PseudoClass) {
99        self.0.remove(cls);
100    }
101
102    /// Returns true if the given pseudo-class is active.
103    pub fn contains(&self, cls: &PseudoClass) -> bool {
104        self.0.contains(cls)
105    }
106}
107
108/// Horizontal text alignment within a widget.
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub enum TextAlign {
111    /// Align text to the left edge.
112    Left,
113    /// Center text horizontally.
114    Center,
115    /// Align text to the right edge.
116    Right,
117}
118
119/// Controls how content overflowing a widget's bounds is handled.
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub enum Overflow {
122    /// Overflowing content is visible outside the widget bounds.
123    Visible,
124    /// Overflowing content is clipped.
125    Hidden,
126    /// A scrollbar is always shown.
127    Scroll,
128    /// A scrollbar appears only when content overflows.
129    Auto,
130}
131
132/// Controls whether a widget is rendered.
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub enum Visibility {
135    /// The widget is rendered normally.
136    Visible,
137    /// The widget is hidden but still occupies layout space.
138    Hidden,
139}
140
141/// Inset amounts for the four sides of a widget (padding or margin).
142#[derive(Debug, Clone, Copy, PartialEq)]
143pub struct Sides<T> {
144    /// Top inset value.
145    pub top: T,
146    /// Right inset value.
147    pub right: T,
148    /// Bottom inset value.
149    pub bottom: T,
150    /// Left inset value.
151    pub left: T,
152}
153
154impl<T: Default> Default for Sides<T> {
155    fn default() -> Self {
156        Sides {
157            top: T::default(),
158            right: T::default(),
159            bottom: T::default(),
160            left: T::default(),
161        }
162    }
163}
164
165/// An edge of the screen where a docked widget is anchored.
166#[derive(Debug, Clone, PartialEq)]
167pub enum DockEdge {
168    /// Widget is docked to the top of its container.
169    Top,
170    /// Widget is docked to the bottom of its container.
171    Bottom,
172    /// Widget is docked to the left of its container.
173    Left,
174    /// Widget is docked to the right of its container.
175    Right,
176}
177
178/// The resolved set of CSS properties for a widget after cascade application.
179#[derive(Debug, Clone, PartialEq)]
180pub struct ComputedStyle {
181    /// Layout mode for this widget's children.
182    pub display: TcssDisplay,
183    /// Flow direction for flex layout children.
184    pub layout_direction: LayoutDirection,
185    /// Explicit width constraint.
186    pub width: TcssDimension,
187    /// Explicit height constraint.
188    pub height: TcssDimension,
189    /// Minimum width constraint.
190    pub min_width: TcssDimension,
191    /// Minimum height constraint.
192    pub min_height: TcssDimension,
193    /// Maximum width constraint.
194    pub max_width: TcssDimension,
195    /// Maximum height constraint.
196    pub max_height: TcssDimension,
197    /// Inner spacing between border and content.
198    pub padding: Sides<f32>,
199    /// Outer spacing between this widget and siblings.
200    pub margin: Sides<f32>,
201    /// Border drawing style.
202    pub border: BorderStyle,
203    /// Optional title text shown in the border.
204    pub border_title: Option<String>,
205    /// Foreground text color.
206    pub color: TcssColor,
207    /// Background fill color.
208    pub background: TcssColor,
209    /// Horizontal text alignment.
210    pub text_align: TextAlign,
211    /// Content overflow behavior.
212    pub overflow: Overflow,
213    /// Whether to reserve space for a scrollbar even when not scrolling.
214    pub scrollbar_gutter: bool,
215    /// Whether the widget is rendered or hidden.
216    pub visibility: Visibility,
217    /// Transparency multiplier (0.0 = fully transparent, 1.0 = opaque).
218    pub opacity: f32,
219    /// Edge this widget is docked to, if any.
220    pub dock: Option<DockEdge>,
221    /// Flex grow factor for proportional size allocation.
222    pub flex_grow: f32,
223    /// Grid column track definitions.
224    pub grid_columns: Option<Vec<TcssDimension>>,
225    /// Grid row track definitions.
226    pub grid_rows: Option<Vec<TcssDimension>>,
227    /// Hatch pattern background fill.
228    pub hatch: Option<HatchStyle>,
229    /// Keyline color for grid separators.
230    pub keyline: Option<TcssColor>,
231}
232
233impl Default for ComputedStyle {
234    fn default() -> Self {
235        ComputedStyle {
236            display: TcssDisplay::Flex,
237            layout_direction: LayoutDirection::Vertical,
238            width: TcssDimension::Auto,
239            height: TcssDimension::Auto,
240            min_width: TcssDimension::Auto,
241            min_height: TcssDimension::Auto,
242            max_width: TcssDimension::Auto,
243            max_height: TcssDimension::Auto,
244            padding: Sides::default(),
245            margin: Sides::default(),
246            border: BorderStyle::None,
247            border_title: None,
248            color: TcssColor::Reset,
249            background: TcssColor::Reset,
250            text_align: TextAlign::Left,
251            overflow: Overflow::Visible,
252            scrollbar_gutter: false,
253            visibility: Visibility::Visible,
254            opacity: 1.0,
255            dock: None,
256            flex_grow: 0.0,
257            grid_columns: None,
258            grid_rows: None,
259            hatch: None,
260            keyline: None,
261        }
262    }
263}
264
265/// Hatch pattern style for background fills using Unicode characters.
266#[derive(Debug, Clone, Copy, PartialEq, Eq)]
267pub enum HatchStyle {
268    /// Cross-hatch pattern (using braille dots)
269    Cross,
270    /// Horizontal lines
271    Horizontal,
272    /// Vertical lines
273    Vertical,
274    /// Diagonal lines going left (top-right to bottom-left)
275    Left,
276    /// Diagonal lines going right (top-left to bottom-right)
277    Right,
278}
279
280/// A parsed CSS property value before or after theme variable resolution.
281#[derive(Debug, Clone, PartialEq)]
282pub enum TcssValue {
283    /// A sizing dimension (length, percent, fraction, or auto).
284    Dimension(TcssDimension),
285    /// A resolved RGB/RGBA color.
286    Color(TcssColor),
287    /// A border style without an explicit color.
288    Border(BorderStyle),
289    /// Border style + color shorthand (e.g. "border: solid #4a4a5a")
290    BorderWithColor(BorderStyle, TcssColor),
291    /// A display mode value.
292    Display(TcssDisplay),
293    /// A text alignment value.
294    TextAlign(TextAlign),
295    /// An overflow behavior value.
296    Overflow(Overflow),
297    /// A visibility value.
298    Visibility(Visibility),
299    /// A bare floating-point number (opacity, flex-grow, padding cell count).
300    Float(f32),
301    /// A quoted string value.
302    String(String),
303    /// A boolean flag value.
304    Bool(bool),
305    /// A dock-edge placement value.
306    DockEdge(DockEdge),
307    /// A layout direction value.
308    LayoutDirection(LayoutDirection),
309    /// Shorthand with all 4 sides (padding/margin with 2+ values)
310    Sides(Sides<f32>),
311    /// List of dimensions (grid-template-columns/rows)
312    Dimensions(Vec<TcssDimension>),
313    /// Border style + unresolved theme variable (e.g. "border: tall $primary").
314    /// Resolved to BorderWithColor during cascade via Theme::resolve().
315    BorderWithVariable(BorderStyle, String),
316    /// Unresolved theme variable reference (e.g., "primary", "accent-darken-1").
317    /// Stored during parsing, resolved to Color during cascade via Theme::resolve().
318    Variable(String),
319    /// Hatch pattern fill (e.g., "hatch: cross")
320    Hatch(HatchStyle),
321    /// Keyline separator color between grid children (e.g., "keyline: $primary")
322    Keyline(TcssColor),
323    /// Keyline with unresolved theme variable
324    KeylineVariable(String),
325}
326
327/// A single parsed CSS property-value pair.
328#[derive(Debug, Clone, PartialEq)]
329pub struct Declaration {
330    /// The CSS property name (e.g., `"color"`, `"width"`).
331    pub property: String,
332    /// The parsed value for this property.
333    pub value: TcssValue,
334}
335
336impl ComputedStyle {
337    /// Apply a list of CSS declarations to this style, overwriting any previously set properties.
338    pub fn apply_declarations(&mut self, decls: &[Declaration]) {
339        for decl in decls {
340            match decl.property.as_str() {
341                "color" => {
342                    if let TcssValue::Color(c) = decl.value {
343                        self.color = c;
344                    }
345                }
346                "background" => {
347                    if let TcssValue::Color(c) = decl.value {
348                        self.background = c;
349                    }
350                }
351                "border" => match &decl.value {
352                    TcssValue::Border(b) => self.border = *b,
353                    TcssValue::BorderWithColor(b, c) => {
354                        self.border = *b;
355                        self.color = *c;
356                    }
357                    _ => {}
358                },
359                "border-title" => {
360                    if let TcssValue::String(ref s) = decl.value {
361                        self.border_title = Some(s.clone());
362                    }
363                }
364                "padding" => match &decl.value {
365                    TcssValue::Float(v) => {
366                        self.padding = Sides {
367                            top: *v,
368                            right: *v,
369                            bottom: *v,
370                            left: *v,
371                        };
372                    }
373                    TcssValue::Sides(s) => {
374                        self.padding = *s;
375                    }
376                    _ => {}
377                },
378                "margin" => match &decl.value {
379                    TcssValue::Float(v) => {
380                        self.margin = Sides {
381                            top: *v,
382                            right: *v,
383                            bottom: *v,
384                            left: *v,
385                        };
386                    }
387                    TcssValue::Sides(s) => {
388                        self.margin = *s;
389                    }
390                    _ => {}
391                },
392                "width" => {
393                    if let TcssValue::Dimension(d) = decl.value {
394                        self.width = d;
395                    }
396                }
397                "height" => {
398                    if let TcssValue::Dimension(d) = decl.value {
399                        self.height = d;
400                    }
401                }
402                "min-width" => {
403                    if let TcssValue::Dimension(d) = decl.value {
404                        self.min_width = d;
405                    }
406                }
407                "min-height" => {
408                    if let TcssValue::Dimension(d) = decl.value {
409                        self.min_height = d;
410                    }
411                }
412                "max-width" => {
413                    if let TcssValue::Dimension(d) = decl.value {
414                        self.max_width = d;
415                    }
416                }
417                "max-height" => {
418                    if let TcssValue::Dimension(d) = decl.value {
419                        self.max_height = d;
420                    }
421                }
422                "display" => {
423                    if let TcssValue::Display(d) = decl.value {
424                        self.display = d;
425                    }
426                }
427                "visibility" => {
428                    if let TcssValue::Visibility(v) = decl.value {
429                        self.visibility = v;
430                    }
431                }
432                "opacity" => {
433                    if let TcssValue::Float(v) = decl.value {
434                        self.opacity = v;
435                    }
436                }
437                "text-align" => {
438                    if let TcssValue::TextAlign(a) = decl.value {
439                        self.text_align = a;
440                    }
441                }
442                "overflow" => {
443                    if let TcssValue::Overflow(o) = decl.value {
444                        self.overflow = o;
445                    }
446                }
447                "scrollbar-gutter" => {
448                    if let TcssValue::Bool(b) = decl.value {
449                        self.scrollbar_gutter = b;
450                    }
451                }
452                "dock" => {
453                    if let TcssValue::DockEdge(ref d) = decl.value {
454                        self.dock = Some(d.clone());
455                    }
456                }
457                "flex-grow" => {
458                    if let TcssValue::Float(v) = decl.value {
459                        self.flex_grow = v;
460                    }
461                }
462                "grid-template-columns" => {
463                    if let TcssValue::Dimensions(dims) = &decl.value {
464                        self.grid_columns = Some(dims.clone());
465                    }
466                }
467                "grid-template-rows" => {
468                    if let TcssValue::Dimensions(dims) = &decl.value {
469                        self.grid_rows = Some(dims.clone());
470                    }
471                }
472                "layout-direction" => {
473                    if let TcssValue::LayoutDirection(d) = decl.value {
474                        self.layout_direction = d;
475                    }
476                }
477                "hatch" => {
478                    if let TcssValue::Hatch(h) = decl.value {
479                        self.hatch = Some(h);
480                    }
481                }
482                "keyline" => match &decl.value {
483                    TcssValue::Keyline(c) => self.keyline = Some(*c),
484                    TcssValue::Color(c) => self.keyline = Some(*c),
485                    _ => {}
486                },
487                _ => {
488                    #[cfg(debug_assertions)]
489                    eprintln!(
490                        "[textual-rs] warning: unknown CSS property '{}' (ignored)",
491                        decl.property
492                    );
493                }
494            }
495        }
496    }
497}
498
499#[cfg(test)]
500mod tests {
501    use super::*;
502
503    #[test]
504    fn computed_style_default_values() {
505        let style = ComputedStyle::default();
506        assert_eq!(style.display, TcssDisplay::Flex);
507        assert_eq!(style.width, TcssDimension::Auto);
508        assert_eq!(style.height, TcssDimension::Auto);
509        assert_eq!(style.border, BorderStyle::None);
510        assert_eq!(style.color, TcssColor::Reset);
511        assert_eq!(style.background, TcssColor::Reset);
512        assert_eq!(style.layout_direction, LayoutDirection::Vertical);
513        assert_eq!(style.opacity, 1.0);
514        assert_eq!(style.flex_grow, 0.0);
515        assert!(!style.scrollbar_gutter);
516        assert!(style.dock.is_none());
517        assert!(style.grid_columns.is_none());
518        assert!(style.grid_rows.is_none());
519    }
520
521    #[test]
522    fn pseudo_class_set_insert_contains_remove() {
523        let mut set = PseudoClassSet::default();
524        assert!(!set.contains(&PseudoClass::Focus));
525        set.insert(PseudoClass::Focus);
526        assert!(set.contains(&PseudoClass::Focus));
527        set.insert(PseudoClass::Hover);
528        assert!(set.contains(&PseudoClass::Hover));
529        set.remove(&PseudoClass::Focus);
530        assert!(!set.contains(&PseudoClass::Focus));
531        assert!(set.contains(&PseudoClass::Hover));
532        set.insert(PseudoClass::Disabled);
533        assert!(set.contains(&PseudoClass::Disabled));
534    }
535
536    #[test]
537    fn apply_declarations_modifies_style() {
538        let mut style = ComputedStyle::default();
539        let decls = vec![
540            Declaration {
541                property: "color".to_string(),
542                value: TcssValue::Color(TcssColor::Rgb(255, 0, 0)),
543            },
544            Declaration {
545                property: "display".to_string(),
546                value: TcssValue::Display(TcssDisplay::Block),
547            },
548            Declaration {
549                property: "opacity".to_string(),
550                value: TcssValue::Float(0.5),
551            },
552        ];
553        style.apply_declarations(&decls);
554        assert_eq!(style.color, TcssColor::Rgb(255, 0, 0));
555        assert_eq!(style.display, TcssDisplay::Block);
556        assert_eq!(style.opacity, 0.5);
557    }
558
559    #[test]
560    fn unknown_property_does_not_panic() {
561        // Unknown properties should be silently ignored (with a debug warning).
562        // This test verifies no panic occurs.
563        let mut style = ComputedStyle::default();
564        let decls = vec![
565            Declaration {
566                property: "nonexistent-prop".to_string(),
567                value: TcssValue::Float(1.0),
568            },
569            Declaration {
570                property: "color".to_string(),
571                value: TcssValue::Color(TcssColor::Rgb(0, 255, 0)),
572            },
573        ];
574        style.apply_declarations(&decls);
575        // Known property should still be applied after the unknown one
576        assert_eq!(style.color, TcssColor::Rgb(0, 255, 0));
577    }
578}