use ratatui::{
style::{Modifier, Style},
text::{Line, Span},
};
use super::{BOX_WIDTH, Focus, Model, State};
use crate::{
shortcut_hints, style,
theme::{Palette, Skin},
};
const SLIDER_MARK: &str = "\u{2502}";
const PRESET_MARK: &str = "\u{25cf}";
pub(super) fn body_lines(skin: &Skin, state: &State) -> Vec<Line<'static>> {
let palette = &skin.palette;
let width = BOX_WIDTH as usize - 2;
let mut lines = vec![tab_line(state, palette), Line::from("")];
for index in 0..3 {
lines.push(channel_line(state, palette, index, width));
}
lines.push(Line::from(""));
lines.push(hex_line(state, palette, width));
lines.push(Line::from(""));
lines.extend(preview_lines(state, palette));
lines.push(Line::from(""));
lines.push(preset_line(state, palette));
lines.push(Line::from(""));
lines.extend(hint_lines(state, palette, width));
lines
}
fn tab_line(state: &State, palette: &Palette) -> Line<'static> {
let mut spans = vec![Span::raw(" ")];
for (index, model) in Model::ALL.iter().enumerate() {
if index > 0 {
spans.push(Span::styled(" \u{b7} ", style::dim()));
}
let text = model.label().to_string();
if *model == state.model {
spans.push(Span::styled(
text,
style::fg(palette.accent).add_modifier(Modifier::BOLD),
));
} else {
spans.push(Span::styled(text, style::secondary(palette)));
}
}
Line::from(spans)
}
fn channel_line(
state: &State,
palette: &Palette,
index: usize,
width: usize,
) -> Line<'static> {
let channel = state.model.channels()[index];
let value = state.channels[index];
let focused = state.focus == Focus::Channel(index);
let label = format!(" {} {:>3} ", channel.label, value.round() as i32);
let bar_width = width.saturating_sub(label.len() + 1).max(1);
let fraction =
((value - channel.min) / (channel.max - channel.min)).clamp(0.0, 1.0);
let denominator = bar_width.saturating_sub(1).max(1) as f32;
let mark_at = (fraction * denominator).round() as usize;
let mut spans = vec![Span::styled(label, label_style(palette, focused))];
for x in 0..bar_width {
let cell_fraction = x as f32 / denominator;
let mut channels = state.channels;
channels[index] =
channel.min + (channel.max - channel.min) * cell_fraction;
let color = state.model.to_color(channels);
if x == mark_at {
let fg = style::to_ratatui(color.readable_on(color));
spans.push(Span::styled(
SLIDER_MARK.to_string(),
style::bg(color).fg(fg),
));
} else {
spans.push(Span::styled(" ".to_string(), style::bg(color)));
}
}
Line::from(spans)
}
fn hex_line(state: &State, palette: &Palette, width: usize) -> Line<'static> {
let focused = state.focus == Focus::Hex;
let label = " hex ";
let field_width = width.saturating_sub(label.len());
let mut spans = vec![Span::styled(label, label_style(palette, focused))];
spans.extend(state.hex.render_line(palette, field_width, focused).spans);
Line::from(spans)
}
fn preview_lines(state: &State, palette: &Palette) -> Vec<Line<'static>> {
let color = state.current_color();
let (red, green, blue) = color.rgb().unwrap_or((0, 0, 0));
let (hue, sat, light) = color.to_hsl().unwrap_or((0.0, 0.0, 0.0));
let swatch = Line::from(vec![
Span::styled(" ", style::bg(color)),
Span::styled(
format!(" {}", color.to_hex()),
style::fg(palette.accent).add_modifier(Modifier::BOLD),
),
]);
let readout = Line::from(Span::styled(
format!(
" rgb {red} {green} {blue} hsl {} {} {}",
hue.round() as i32,
(sat * 100.0).round() as i32,
(light * 100.0).round() as i32,
),
style::secondary(palette),
));
let contrast = Line::from(vec![
Span::styled(
" Ab ",
style::fg(color).bg(style::to_ratatui(style::LIGHT_BG)),
),
Span::raw(" "),
Span::styled(
" Ab ",
style::fg(color).bg(style::to_ratatui(style::DARK_BG)),
),
Span::styled(
format!(" lum {:.2}", color.luminance()),
style::secondary(palette),
),
]);
vec![swatch, readout, contrast]
}
fn preset_line(state: &State, palette: &Palette) -> Line<'static> {
let focused = state.focus == Focus::Presets;
let mut spans = vec![Span::styled(" set ", label_style(palette, focused))];
for (index, &color) in state.presets.iter().enumerate() {
let fill = style::bg(color);
if focused && index == state.preset {
let fg = style::to_ratatui(color.readable_on(color));
spans.push(Span::styled(" ".to_string(), fill));
spans.push(Span::styled(PRESET_MARK.to_string(), fill.fg(fg)));
spans.push(Span::styled(" ".to_string(), fill));
} else {
spans.push(Span::styled(" ".to_string(), fill));
}
spans.push(Span::raw(" "));
}
Line::from(spans)
}
fn hint_lines(
state: &State,
palette: &Palette,
width: usize,
) -> Vec<Line<'static>> {
let hints: &[(&str, &str)] = match state.focus {
Focus::Hex => &[
("type", "hex"),
("m", "model"),
("\u{2191}/\u{2193}", "focus"),
("enter", "ok"),
("esc", "back"),
],
Focus::Presets => &[
("\u{2190}/\u{2192}", "choose"),
("m", "model"),
("s", "swatches"),
("y", "copy"),
("enter", "ok"),
("esc", "back"),
],
Focus::Channel(_) => &[
("\u{2190}/\u{2192}", "adjust"),
("shift", "fine"),
("m", "model"),
("s", "swatches"),
("y", "copy"),
("enter", "ok"),
("esc", "back"),
],
};
shortcut_hints::lines(hints, palette.accent_dim, width)
}
fn label_style(palette: &Palette, focused: bool) -> Style {
if focused {
style::fg(palette.accent).add_modifier(Modifier::BOLD)
} else {
style::secondary(palette)
}
}