Skip to main content

ftui_style/
lib.rs

1#![forbid(unsafe_code)]
2
3//! Style types for FrankenTUI with CSS-like cascading semantics.
4//!
5//! # Role in FrankenTUI
6//! `ftui-style` is the shared vocabulary for colors and styling. Widgets,
7//! render, and extras use these types to stay visually consistent without
8//! dragging in rendering or runtime dependencies.
9//!
10//! # This crate provides
11//! - [`Style`] for unified text styling with CSS-like inheritance.
12//! - [`StyleSheet`] for named style registration (CSS-like classes).
13//! - [`Theme`] for semantic color slots with light/dark mode support.
14//! - Color types and downgrade utilities.
15//! - Table themes and effects used by widgets and markdown rendering.
16//!
17//! # How it fits in the system
18//! `ftui-render` stores style values in cells, `ftui-widgets` computes styles
19//! for UI components, and `ftui-extras` uses themes for richer rendering
20//! (markdown, charts, and demo visuals). This crate keeps that style layer
21//! deterministic and reusable.
22
23/// Color types, profiles, and downgrade utilities.
24pub mod color;
25/// Interactive style variants for stateful widgets.
26pub mod interactive;
27/// Style types with CSS-like cascading semantics.
28pub mod style;
29/// StyleSheet registry for named styles.
30pub mod stylesheet;
31/// Table theme types and presets.
32pub mod table_theme;
33/// Theme system with semantic color slots.
34pub mod theme;
35
36pub use color::{
37    // Color types
38    Ansi16,
39    Color,
40    ColorCache,
41    ColorProfile,
42    MonoColor,
43    Rgb,
44    // WCAG constants
45    WCAG_AA_LARGE_TEXT,
46    WCAG_AA_NORMAL_TEXT,
47    WCAG_AAA_LARGE_TEXT,
48    WCAG_AAA_NORMAL_TEXT,
49    // WCAG contrast utilities
50    best_text_color,
51    best_text_color_packed,
52    contrast_ratio,
53    contrast_ratio_packed,
54    meets_wcag_aa,
55    meets_wcag_aa_large_text,
56    meets_wcag_aa_packed,
57    meets_wcag_aaa,
58    relative_luminance,
59    relative_luminance_packed,
60};
61pub use interactive::{InteractionState, InteractiveStyle};
62pub use style::{
63    LineClamp, Overflow, Style, StyleFlags, TextAlign, TextOverflow, TextTransform, WhiteSpaceMode,
64};
65pub use stylesheet::{StyleId, StyleSheet};
66pub use table_theme::{
67    BlendMode, Gradient, StyleMask, TableEffect, TableEffectResolver, TableEffectRule,
68    TableEffectScope, TableEffectTarget, TablePresetId, TableSection, TableTheme,
69    TableThemeDiagnostics, TableThemeSpec,
70};
71pub use theme::{AdaptiveColor, ResolvedTheme, Theme, ThemeBuilder};
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76    use ftui_render::cell::{CellAttrs, PackedRgba, StyleFlags as CellFlags};
77
78    #[test]
79    fn theme_builder_from_theme_preserves_base_fields() {
80        let base = Theme::builder()
81            .primary(Color::rgb(10, 20, 30))
82            .text(Color::rgb(40, 50, 60))
83            .build();
84
85        let updated = ThemeBuilder::from_theme(base.clone())
86            .text(Color::rgb(70, 80, 90))
87            .build();
88
89        assert_eq!(updated.primary, base.primary);
90        assert_eq!(updated.background, base.background);
91        assert_eq!(updated.text, AdaptiveColor::from(Color::rgb(70, 80, 90)));
92    }
93
94    #[test]
95    fn adaptive_color_resolves_by_mode() {
96        let adaptive = AdaptiveColor::adaptive(Color::rgb(1, 2, 3), Color::rgb(4, 5, 6));
97        assert_eq!(adaptive.resolve(false), Color::rgb(1, 2, 3));
98        assert_eq!(adaptive.resolve(true), Color::rgb(4, 5, 6));
99    }
100
101    #[test]
102    fn packed_rgba_round_trip_channels() {
103        let packed = PackedRgba::rgba(12, 34, 56, 78);
104        assert_eq!(packed.r(), 12);
105        assert_eq!(packed.g(), 34);
106        assert_eq!(packed.b(), 56);
107        assert_eq!(packed.a(), 78);
108
109        let rgb: Rgb = packed.into();
110        assert_eq!(rgb, Rgb::new(12, 34, 56));
111
112        let color: Color = packed.into();
113        assert_eq!(color.to_rgb(), Rgb::new(12, 34, 56));
114    }
115
116    #[test]
117    fn packed_rgba_rgb_defaults_to_opaque() {
118        let packed = PackedRgba::rgb(1, 2, 3);
119        assert_eq!(packed.a(), 255);
120    }
121
122    #[test]
123    fn color_profile_defaults_to_ansi16() {
124        let profile = ColorProfile::detect_from_env(None, None, None);
125        assert_eq!(profile, ColorProfile::Ansi16);
126    }
127
128    #[test]
129    fn style_flags_round_trip_to_cell_flags() {
130        let style_flags = StyleFlags::BOLD
131            .union(StyleFlags::ITALIC)
132            .union(StyleFlags::UNDERLINE)
133            .union(StyleFlags::BLINK);
134
135        let cell_flags: CellFlags = style_flags.into();
136        assert!(cell_flags.contains(CellFlags::BOLD));
137        assert!(cell_flags.contains(CellFlags::ITALIC));
138        assert!(cell_flags.contains(CellFlags::UNDERLINE));
139        assert!(cell_flags.contains(CellFlags::BLINK));
140
141        let round_trip = StyleFlags::from(cell_flags);
142        assert!(round_trip.contains(StyleFlags::BOLD));
143        assert!(round_trip.contains(StyleFlags::ITALIC));
144        assert!(round_trip.contains(StyleFlags::UNDERLINE));
145        assert!(round_trip.contains(StyleFlags::BLINK));
146    }
147
148    #[test]
149    fn extended_underlines_map_to_cell_underline() {
150        let style_flags = StyleFlags::DOUBLE_UNDERLINE.union(StyleFlags::CURLY_UNDERLINE);
151        let cell_flags: CellFlags = style_flags.into();
152        assert!(cell_flags.contains(CellFlags::UNDERLINE));
153    }
154
155    #[test]
156    fn cell_attrs_preserve_link_id_with_flags() {
157        let flags = CellFlags::BOLD | CellFlags::ITALIC | CellFlags::UNDERLINE | CellFlags::BLINK;
158        let attrs = CellAttrs::new(flags, 4242);
159        assert_eq!(attrs.link_id(), 4242);
160        assert!(attrs.has_flag(CellFlags::BOLD));
161        assert!(attrs.has_flag(CellFlags::ITALIC));
162        assert!(attrs.has_flag(CellFlags::UNDERLINE));
163        assert!(attrs.has_flag(CellFlags::BLINK));
164    }
165}