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}