Skip to main content

lv_tui/
style.rs

1use crate::buffer::CellStyle;
2use crate::geom::Insets;
3
4/// Theme mode for terminal colors.
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum Theme {
7    Dark,
8    Light,
9}
10
11thread_local! {
12    /// Current global theme. Set via [`set_theme`].
13    pub static CURRENT_THEME: std::cell::RefCell<Theme> = std::cell::RefCell::new(Theme::Dark);
14}
15
16/// Sets the global theme (Dark or Light) and forces a full repaint.
17pub fn set_theme(theme: Theme) {
18    CURRENT_THEME.with(|t| *t.borrow_mut() = theme);
19    // Force a full repaint so color changes take effect immediately.
20    crate::runtime::FORCE_FULL_PAINT.with(|f| *f.borrow_mut() = true);
21}
22
23/// Terminal color palette.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum Color {
26    // Standard ANSI (16)
27    Black,
28    Red,
29    Green,
30    Yellow,
31    Blue,
32    Magenta,
33    Cyan,
34    White,
35    Gray,
36    LightRed,
37    LightGreen,
38    LightYellow,
39    LightBlue,
40    LightMagenta,
41    LightCyan,
42    DarkGray,
43    /// 256-color palette index (0–255)
44    Indexed(u8),
45    /// 24-bit true color
46    Rgb(u8, u8, u8),
47}
48
49impl Color {
50    /// Constructs from hex string. Accepts `#rgb`, `#rrggbb`, `rgb`, `rrggbb`.
51    pub fn hex(hex: &str) -> Option<Color> {
52        let s = hex.trim().trim_start_matches('#');
53        if s.is_empty() {
54            return None;
55        }
56        match s.len() {
57            3 => {
58                let chars: Vec<char> = s.chars().collect();
59                let r = u8::from_str_radix(&chars[0].to_string().repeat(2), 16).ok()?;
60                let g = u8::from_str_radix(&chars[1].to_string().repeat(2), 16).ok()?;
61                let b = u8::from_str_radix(&chars[2].to_string().repeat(2), 16).ok()?;
62                Some(Color::Rgb(r, g, b))
63            }
64            6 => {
65                let r = u8::from_str_radix(&s[0..2], 16).ok()?;
66                let g = u8::from_str_radix(&s[2..4], 16).ok()?;
67                let b = u8::from_str_radix(&s[4..6], 16).ok()?;
68                Some(Color::Rgb(r, g, b))
69            }
70            _ => None,
71        }
72    }
73
74    pub fn rgb(r: u8, g: u8, b: u8) -> Color {
75        Color::Rgb(r, g, b)
76    }
77
78    pub fn indexed(i: u8) -> Color {
79        Color::Indexed(i)
80    }
81}
82
83impl From<Color> for crossterm::style::Color {
84    fn from(c: Color) -> Self {
85        let dark = CURRENT_THEME.with(|t| *t.borrow() == Theme::Dark);
86        match c {
87            Color::Rgb(r, g, b) => crossterm::style::Color::Rgb { r, g, b },
88            Color::Indexed(i) => crossterm::style::Color::AnsiValue(i),
89            Color::Black => crossterm::style::Color::Black,
90            Color::Red if dark => crossterm::style::Color::DarkRed,
91            Color::Red => crossterm::style::Color::Red,
92            Color::Green if dark => crossterm::style::Color::DarkGreen,
93            Color::Green => crossterm::style::Color::Green,
94            Color::Yellow if dark => crossterm::style::Color::DarkYellow,
95            Color::Yellow => crossterm::style::Color::Yellow,
96            Color::Blue if dark => crossterm::style::Color::DarkBlue,
97            Color::Blue => crossterm::style::Color::Blue,
98            Color::Magenta if dark => crossterm::style::Color::DarkMagenta,
99            Color::Magenta => crossterm::style::Color::Magenta,
100            Color::Cyan if dark => crossterm::style::Color::DarkCyan,
101            Color::Cyan => crossterm::style::Color::Cyan,
102            Color::White => crossterm::style::Color::Grey,
103            Color::Gray => crossterm::style::Color::DarkGrey,
104            Color::LightRed => crossterm::style::Color::Red,
105            Color::LightGreen => crossterm::style::Color::Green,
106            Color::LightYellow => crossterm::style::Color::Yellow,
107            Color::LightBlue => crossterm::style::Color::Blue,
108            Color::LightMagenta => crossterm::style::Color::Magenta,
109            Color::LightCyan => crossterm::style::Color::Cyan,
110            Color::DarkGray => crossterm::style::Color::DarkGrey,
111        }
112    }
113}
114
115/// 长度规格
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub enum Length {
118    /// 内容自适应
119    Auto,
120    /// 固定字符格数
121    Fixed(u16),
122    /// 父容器可用空间的百分比
123    Percent(u16),
124    /// 参与剩余空间按比例分配
125    Fraction(u16),
126}
127
128impl Default for Length {
129    fn default() -> Self {
130        Length::Auto
131    }
132}
133
134/// 布局方向
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136pub enum Layout {
137    None,
138    Vertical,
139    Horizontal,
140}
141
142impl Default for Layout {
143    fn default() -> Self {
144        Layout::None
145    }
146}
147
148/// 文本换行模式
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
150pub enum TextWrap {
151    /// 不换行,超出 clip 截断
152    #[default]
153    None,
154    /// 按字符边界换行
155    Char,
156    /// 按单词边界换行(英文空格分词,CJK 按字符)
157    Word,
158}
159
160/// 文本对齐
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
162pub enum TextAlign {
163    #[default]
164    Left,
165    Center,
166    Right,
167}
168
169/// 文本截断模式
170#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
171pub enum TextTruncate {
172    /// 不截断
173    #[default]
174    None,
175    /// 末尾追加省略号 …
176    Ellipsis,
177}
178
179/// 边框样式
180#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
181pub enum Border {
182    #[default]
183    None,
184    Plain,
185    Rounded,
186    Double,
187}
188
189/// 组件样式
190#[derive(Debug, Clone, PartialEq, Eq)]
191pub struct Style {
192    // 视觉效果
193    pub fg: Option<Color>,
194    pub bg: Option<Color>,
195    pub bold: bool,
196    pub italic: bool,
197    pub underline: bool,
198
199    // 布局
200    pub width: Length,
201    pub height: Length,
202    pub padding: Insets,
203    pub margin: Insets,
204    pub layout: Layout,
205    pub gap: u16,
206    pub flex_grow: u16,
207    pub flex_shrink: bool,
208    pub border: Border,
209}
210
211impl Default for Style {
212    fn default() -> Self {
213        Self {
214            fg: None,
215            bg: None,
216            bold: false,
217            italic: false,
218            underline: false,
219            width: Length::Auto,
220            height: Length::Auto,
221            padding: Insets::ZERO,
222            margin: Insets::ZERO,
223            layout: Layout::None,
224            gap: 0,
225            flex_grow: 0,
226            flex_shrink: true,
227            border: Border::None,
228        }
229    }
230}
231
232impl Style {
233    /// Builder: sets the foreground color.
234    pub fn fg(mut self, color: Color) -> Self {
235        self.fg = Some(color);
236        self
237    }
238
239    /// Builder: sets the background color.
240    pub fn bg(mut self, color: Color) -> Self {
241        self.bg = Some(color);
242        self
243    }
244
245    /// Builder: enables bold.
246    pub fn bold(mut self) -> Self {
247        self.bold = true;
248        self
249    }
250
251    /// Builder: enables italic.
252    pub fn italic(mut self) -> Self {
253        self.italic = true;
254        self
255    }
256
257    /// Builder: enables underline.
258    pub fn underline(mut self) -> Self {
259        self.underline = true;
260        self
261    }
262
263    /// Builder: sets the width constraint.
264    pub fn width(mut self, width: Length) -> Self {
265        self.width = width;
266        self
267    }
268
269    /// Builder: sets the height constraint.
270    pub fn height(mut self, height: Length) -> Self {
271        self.height = height;
272        self
273    }
274
275    /// Builder: sets uniform padding on all four sides.
276    pub fn padding(mut self, value: u16) -> Self {
277        self.padding = Insets::all(value);
278        self
279    }
280
281    /// Builder: sets padding on each side independently.
282    pub fn padding_each(mut self, top: u16, right: u16, bottom: u16, left: u16) -> Self {
283        self.padding = Insets { top, right, bottom, left };
284        self
285    }
286
287    /// Builder: sets uniform margin on all four sides.
288    pub fn margin(mut self, value: u16) -> Self {
289        self.margin = Insets::all(value);
290        self
291    }
292
293    /// Builder: sets the layout direction.
294    pub fn layout(mut self, layout: Layout) -> Self {
295        self.layout = layout;
296        self
297    }
298
299    /// Builder: sets the gap between children.
300    pub fn gap(mut self, gap: u16) -> Self {
301        self.gap = gap;
302        self
303    }
304
305    /// Builder: sets flex grow factor (0 = don't grow).
306    pub fn flex_grow(mut self, grow: u16) -> Self {
307        self.flex_grow = grow;
308        self
309    }
310
311    /// Builder: sets whether the item can shrink below intrinsic size.
312    pub fn flex_shrink(mut self, shrink: bool) -> Self {
313        self.flex_shrink = shrink;
314        self
315    }
316
317    /// Builder: sets the border style.
318    pub fn border(mut self, border: Border) -> Self {
319        self.border = border;
320        self
321    }
322
323    /// 转换为 CellStyle
324    pub fn into_cell_style(&self) -> CellStyle {
325        CellStyle {
326            fg: self.fg,
327            bg: self.bg,
328            bold: self.bold,
329            italic: self.italic,
330            underline: self.underline,
331        }
332    }
333}