Skip to main content

tca_ratatui/widgets/
color_picker.rs

1use crate::{ColorRamp, TcaTheme};
2use ratatui::{
3    buffer::Buffer,
4    layout::{Constraint, Layout, Rect},
5    style::{Style, Stylize},
6    text::Line,
7    widgets::{Block, Borders, Paragraph, Widget, Wrap},
8};
9
10/// Displays all color sections of a TCA theme.
11///
12/// Shows palette ramps, ANSI colors, semantic colors, and UI colors
13/// in a two-column layout. Borrows the theme for the widget lifetime.
14///
15/// # Examples
16///
17/// ```rust,no_run
18/// use tca_ratatui::{TcaTheme, ColorPicker};
19/// # use ratatui::Frame;
20/// # fn render(frame: &mut Frame, theme: &TcaTheme) {
21/// let picker = ColorPicker::new(theme)
22///     .title("Theme Preview")
23///     .instructions("Press Q to quit");
24///
25/// frame.render_widget(picker, frame.area());
26/// # }
27/// ```
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct ColorPicker<'a> {
30    theme: &'a TcaTheme,
31    title: Option<String>,
32    instructions: Option<String>,
33}
34
35impl<'a> ColorPicker<'a> {
36    /// Create a new color picker for the given theme.
37    pub const fn new(theme: &'a TcaTheme) -> Self {
38        Self {
39            theme,
40            title: None,
41            instructions: None,
42        }
43    }
44
45    /// Set the title displayed at the top of the widget.
46    pub fn title(mut self, title: impl Into<String>) -> Self {
47        self.title = Some(title.into());
48        self
49    }
50
51    /// Set the instructions displayed at the bottom of the widget.
52    pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
53        self.instructions = Some(instructions.into());
54        self
55    }
56}
57
58impl Widget for ColorPicker<'_> {
59    fn render(self, area: Rect, buf: &mut Buffer) {
60        let theme = self.theme;
61
62        let border_color = theme.ui.border_primary;
63        let title_color = theme.ui.fg_primary;
64        let bg = theme.ui.bg_primary;
65        let fg = theme.ui.fg_primary;
66
67        let mut block = Block::bordered()
68            .bg(bg)
69            .fg(fg)
70            .borders(Borders::ALL)
71            .border_style(Style::default().fg(border_color))
72            .title_style(Style::default().fg(title_color));
73
74        if let Some(title) = self.title {
75            block = block.title(Line::from(title).centered());
76        }
77
78        if let Some(instructions) = self.instructions {
79            block = block.title_bottom(Line::from(instructions).centered());
80        }
81
82        let inner = block.inner(area);
83        block.render(area, buf);
84
85        let chunks = Layout::horizontal([
86            Constraint::Percentage(40),
87            Constraint::Percentage(30),
88            Constraint::Percentage(30),
89        ])
90        .split(inner);
91
92        let mut left_lines = vec![Line::from(format!("Theme: {}", theme.meta.name))
93            .style(Style::default().fg(title_color))];
94        if let Some(author) = &theme.meta.author {
95            left_lines.push(Line::from(format!("Author: {}", author)));
96        }
97        if let Some(version) = &theme.meta.version {
98            left_lines.push(Line::from(format!("Version: {}", version)));
99        }
100        if let Some(description) = &theme.meta.description {
101            left_lines.push(Line::from(format!("Description: {}", description)));
102        }
103
104        left_lines.push(Line::from(""));
105        left_lines.push(Line::from("ANSI Colors:"));
106        left_lines.extend([
107            Line::from("  black").style(Style::default().fg(theme.ansi.black)),
108            Line::from("  red").style(Style::default().fg(theme.ansi.red)),
109            Line::from("  green").style(Style::default().fg(theme.ansi.green)),
110            Line::from("  yellow").style(Style::default().fg(theme.ansi.yellow)),
111            Line::from("  blue").style(Style::default().fg(theme.ansi.blue)),
112            Line::from("  magenta").style(Style::default().fg(theme.ansi.magenta)),
113            Line::from("  cyan").style(Style::default().fg(theme.ansi.cyan)),
114            Line::from("  white").style(Style::default().fg(theme.ansi.white)),
115            Line::from("  bright_black").style(Style::default().fg(theme.ansi.bright_black)),
116            Line::from("  bright_red").style(Style::default().fg(theme.ansi.bright_red)),
117            Line::from("  bright_green").style(Style::default().fg(theme.ansi.bright_green)),
118            Line::from("  bright_yellow").style(Style::default().fg(theme.ansi.bright_yellow)),
119            Line::from("  bright_blue").style(Style::default().fg(theme.ansi.bright_blue)),
120            Line::from("  bright_magenta").style(Style::default().fg(theme.ansi.bright_magenta)),
121            Line::from("  bright_cyan").style(Style::default().fg(theme.ansi.bright_cyan)),
122            Line::from("  bright_white").style(Style::default().fg(theme.ansi.bright_white)),
123        ]);
124
125        let mut center_lines = vec![Line::from("Semantic Colors:")];
126        center_lines.extend([
127            Line::from("  error").style(Style::default().fg(theme.semantic.error)),
128            Line::from("  warning").style(Style::default().fg(theme.semantic.warning)),
129            Line::from("  success").style(Style::default().fg(theme.semantic.success)),
130            Line::from("  info").style(Style::default().fg(theme.semantic.info)),
131            Line::from("  highlight").style(Style::default().fg(theme.semantic.highlight)),
132            Line::from("  link").style(Style::default().fg(theme.semantic.link)),
133        ]);
134
135        center_lines.push(Line::from(""));
136        center_lines.push(Line::from("UI Colors:"));
137        center_lines.extend([
138            Line::from("  bg_primary").style(
139                Style::default()
140                    .fg(theme.ui.bg_primary)
141                    .bg(theme.ui.fg_primary),
142            ),
143            Line::from("  bg_secondary").style(
144                Style::default()
145                    .fg(theme.ui.bg_secondary)
146                    .bg(theme.ui.fg_primary),
147            ),
148            Line::from("  fg_primary").style(Style::default().fg(theme.ui.fg_primary)),
149            Line::from("  fg_secondary").style(Style::default().fg(theme.ui.fg_secondary)),
150            Line::from("  fg_muted").style(Style::default().fg(theme.ui.fg_muted)),
151            Line::from("  border_primary").style(Style::default().fg(theme.ui.border_primary)),
152            Line::from("  border_muted").style(Style::default().fg(theme.ui.border_muted)),
153            Line::from("  cursor_primary").style(Style::default().bg(theme.ui.cursor_primary)),
154            Line::from("  cursor_muted").style(Style::default().bg(theme.ui.cursor_muted)),
155            Line::from("  selection_bg").style(
156                Style::default()
157                    .bg(theme.ui.selection_bg)
158                    .fg(theme.ui.fg_primary),
159            ),
160            Line::from("  selection_fg").style(Style::default().fg(theme.ui.selection_fg)),
161        ]);
162
163        let mut right_lines = vec![Line::from("Base16 Colors:")];
164        for (key, value) in theme.base16.entries() {
165            right_lines.push(Line::from(format!("  {}", key)).style(Style::default().fg(value)));
166        }
167
168        right_lines.push(Line::from(""));
169        right_lines.push(Line::from("Palette:"));
170        for name in theme.palette.ramp_names() {
171            if let Some(ramp) = theme.palette.get_ramp(name) {
172                right_lines.push(render_color_ramp(name, ramp));
173            }
174        }
175        Paragraph::new(left_lines)
176            .wrap(Wrap { trim: false })
177            .render(chunks[0], buf);
178        Paragraph::new(center_lines).render(chunks[1], buf);
179        Paragraph::new(right_lines).render(chunks[2], buf);
180    }
181}
182
183fn render_color_ramp(name: &str, ramp: &ColorRamp) -> Line<'static> {
184    let mut spans = vec![ratatui::text::Span::raw(format!("  {}: ", name))];
185    for &color in &ramp.colors {
186        spans.push(ratatui::text::Span::styled("█", Style::default().fg(color)));
187    }
188    Line::from(spans)
189}