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/// Style types with CSS-like cascading semantics.
26pub mod style;
27/// StyleSheet registry for named styles.
28pub mod stylesheet;
29/// Table theme types and presets.
30pub mod table_theme;
31/// Theme system with semantic color slots.
32pub mod theme;
33
34pub use color::{
35    // Color types
36    Ansi16,
37    Color,
38    ColorCache,
39    ColorProfile,
40    MonoColor,
41    Rgb,
42    // WCAG constants
43    WCAG_AA_LARGE_TEXT,
44    WCAG_AA_NORMAL_TEXT,
45    WCAG_AAA_LARGE_TEXT,
46    WCAG_AAA_NORMAL_TEXT,
47    // WCAG contrast utilities
48    best_text_color,
49    best_text_color_packed,
50    contrast_ratio,
51    contrast_ratio_packed,
52    meets_wcag_aa,
53    meets_wcag_aa_large_text,
54    meets_wcag_aa_packed,
55    meets_wcag_aaa,
56    relative_luminance,
57    relative_luminance_packed,
58};
59pub use style::{Style, StyleFlags};
60pub use stylesheet::{StyleId, StyleSheet};
61pub use table_theme::{
62    BlendMode, Gradient, StyleMask, TableEffect, TableEffectResolver, TableEffectRule,
63    TableEffectScope, TableEffectTarget, TablePresetId, TableSection, TableTheme,
64    TableThemeDiagnostics, TableThemeSpec,
65};
66pub use theme::{AdaptiveColor, ResolvedTheme, Theme, ThemeBuilder};
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71    use ftui_render::cell::{CellAttrs, PackedRgba, StyleFlags as CellFlags};
72
73    #[test]
74    fn theme_builder_from_theme_preserves_base_fields() {
75        let base = Theme::builder()
76            .primary(Color::rgb(10, 20, 30))
77            .text(Color::rgb(40, 50, 60))
78            .build();
79
80        let updated = ThemeBuilder::from_theme(base.clone())
81            .text(Color::rgb(70, 80, 90))
82            .build();
83
84        assert_eq!(updated.primary, base.primary);
85        assert_eq!(updated.background, base.background);
86        assert_eq!(updated.text, AdaptiveColor::from(Color::rgb(70, 80, 90)));
87    }
88
89    #[test]
90    fn adaptive_color_resolves_by_mode() {
91        let adaptive = AdaptiveColor::adaptive(Color::rgb(1, 2, 3), Color::rgb(4, 5, 6));
92        assert_eq!(adaptive.resolve(false), Color::rgb(1, 2, 3));
93        assert_eq!(adaptive.resolve(true), Color::rgb(4, 5, 6));
94    }
95
96    #[test]
97    fn packed_rgba_round_trip_channels() {
98        let packed = PackedRgba::rgba(12, 34, 56, 78);
99        assert_eq!(packed.r(), 12);
100        assert_eq!(packed.g(), 34);
101        assert_eq!(packed.b(), 56);
102        assert_eq!(packed.a(), 78);
103
104        let rgb: Rgb = packed.into();
105        assert_eq!(rgb, Rgb::new(12, 34, 56));
106
107        let color: Color = packed.into();
108        assert_eq!(color.to_rgb(), Rgb::new(12, 34, 56));
109    }
110
111    #[test]
112    fn packed_rgba_rgb_defaults_to_opaque() {
113        let packed = PackedRgba::rgb(1, 2, 3);
114        assert_eq!(packed.a(), 255);
115    }
116
117    #[test]
118    fn color_profile_defaults_to_ansi16() {
119        let profile = ColorProfile::detect_from_env(None, None, None);
120        assert_eq!(profile, ColorProfile::Ansi16);
121    }
122
123    #[test]
124    fn style_flags_round_trip_to_cell_flags() {
125        let style_flags = StyleFlags::BOLD
126            .union(StyleFlags::ITALIC)
127            .union(StyleFlags::UNDERLINE)
128            .union(StyleFlags::BLINK);
129
130        let cell_flags: CellFlags = style_flags.into();
131        assert!(cell_flags.contains(CellFlags::BOLD));
132        assert!(cell_flags.contains(CellFlags::ITALIC));
133        assert!(cell_flags.contains(CellFlags::UNDERLINE));
134        assert!(cell_flags.contains(CellFlags::BLINK));
135
136        let round_trip = StyleFlags::from(cell_flags);
137        assert!(round_trip.contains(StyleFlags::BOLD));
138        assert!(round_trip.contains(StyleFlags::ITALIC));
139        assert!(round_trip.contains(StyleFlags::UNDERLINE));
140        assert!(round_trip.contains(StyleFlags::BLINK));
141    }
142
143    #[test]
144    fn extended_underlines_map_to_cell_underline() {
145        let style_flags = StyleFlags::DOUBLE_UNDERLINE.union(StyleFlags::CURLY_UNDERLINE);
146        let cell_flags: CellFlags = style_flags.into();
147        assert!(cell_flags.contains(CellFlags::UNDERLINE));
148    }
149
150    #[test]
151    fn cell_attrs_preserve_link_id_with_flags() {
152        let flags = CellFlags::BOLD | CellFlags::ITALIC | CellFlags::UNDERLINE | CellFlags::BLINK;
153        let attrs = CellAttrs::new(flags, 4242);
154        assert_eq!(attrs.link_id(), 4242);
155        assert!(attrs.has_flag(CellFlags::BOLD));
156        assert!(attrs.has_flag(CellFlags::ITALIC));
157        assert!(attrs.has_flag(CellFlags::UNDERLINE));
158        assert!(attrs.has_flag(CellFlags::BLINK));
159    }
160}