use std::io;
use crossterm::event::{KeyCode, KeyModifiers};
use ratatui::{
Frame,
style::{Modifier, Style},
text::{Line, Span},
widgets::Paragraph,
};
use super::{
footer,
layout::centered_rect,
modal::ModalSignal,
overlay::{self, PopupFlow, popup},
style,
terminal::Tui,
};
use crate::theme::{Color, Skin};
const BOX_WIDTH: u16 = 40;
const CHANNELS: [&str; 3] = ["R", "G", "B"];
const COARSE_STEP: i32 = 10;
const FINE_STEP: i32 = 1;
struct Rgb {
channels: [u8; 3],
focus: usize,
}
pub fn color_picker(
tui: &mut Tui,
skin: &Skin,
title: &str,
initial: Option<Color>,
render_bg: impl Fn(&mut Frame),
) -> io::Result<ModalSignal<Color>> {
let channels = initial
.and_then(Color::rgb)
.or_else(|| skin.palette.accent.rgb())
.map_or([128, 128, 128], |(r, g, b)| [r, g, b]);
let mut state = Rgb { channels, focus: 0 };
popup(
tui,
&mut state,
|area, state: &Rgb| {
let rows =
body_lines(skin, state.channels, state.focus).len() as u16 + 2;
centered_rect(overlay::box_width(BOX_WIDTH, skin), rows, area)
},
|frame, _| render_bg(frame),
|frame, rect, state: &Rgb| {
let inner = overlay::framed(frame, rect, skin, title);
let lines = body_lines(skin, state.channels, state.focus);
frame.render_widget(Paragraph::new(lines), inner);
},
|state, key| {
let step = if key.modifiers.contains(KeyModifiers::SHIFT) {
FINE_STEP
} else {
COARSE_STEP
};
match key.code {
KeyCode::Up | KeyCode::Char('k') | KeyCode::BackTab => {
state.focus = (state.focus + 2) % 3;
PopupFlow::Continue
}
KeyCode::Down | KeyCode::Char('j') | KeyCode::Tab => {
state.focus = (state.focus + 1) % 3;
PopupFlow::Continue
}
KeyCode::Left | KeyCode::Char('h' | 'H') => {
state.channels[state.focus] =
adjust(state.channels[state.focus], -step);
PopupFlow::Continue
}
KeyCode::Right | KeyCode::Char('l' | 'L') => {
state.channels[state.focus] =
adjust(state.channels[state.focus], step);
PopupFlow::Continue
}
KeyCode::Enter => PopupFlow::Done(Color::Rgb(
state.channels[0],
state.channels[1],
state.channels[2],
)),
KeyCode::Esc => PopupFlow::Cancelled,
_ => PopupFlow::Continue,
}
},
)
}
pub(crate) fn adjust(channel: u8, delta: i32) -> u8 {
(i32::from(channel) + delta).clamp(0, 255) as u8
}
fn body_lines(skin: &Skin, rgb: [u8; 3], channel: usize) -> Vec<Line<'static>> {
let palette = &skin.palette;
let color = Color::Rgb(rgb[0], rgb[1], rgb[2]);
let bar_width = (BOX_WIDTH as usize).saturating_sub(12).max(1);
let accent = style::to_ratatui(palette.accent);
let track = style::to_ratatui(palette.selection_bg);
let mut lines: Vec<Line> = CHANNELS
.iter()
.enumerate()
.map(|(index, name)| {
let value = rgb[index];
let filled = usize::from(value) * bar_width / 255;
let label_style = if index == channel {
style::fg(palette.accent).add_modifier(Modifier::BOLD)
} else {
style::dim()
};
let mut spans =
vec![Span::styled(format!(" {name} {value:>3} "), label_style)];
spans.extend((0..bar_width).map(|x| {
let background = if x < filled { accent } else { track };
Span::styled(" ", Style::default().bg(background))
}));
let line = Line::from(spans);
if index == channel {
line.style(style::bg(palette.selection_bg))
} else {
line
}
})
.collect();
lines.push(Line::from(""));
lines.push(Line::from(vec![
Span::styled(" ", style::bg(color)),
Span::raw(" "),
Span::styled(
color.to_hex(),
style::fg(palette.accent).add_modifier(Modifier::BOLD),
),
]));
lines.push(Line::from(""));
lines.extend(footer::lines(
&[
("\u{2190}/\u{2192}", "\u{b1}10"),
("shift", "fine"),
("\u{2191}/\u{2193}", "channel"),
("enter", "ok"),
],
palette.accent_dim,
BOX_WIDTH as usize - 2,
));
lines
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn adjust_saturates_at_bounds() {
assert_eq!(adjust(250, 16), 255);
assert_eq!(adjust(5, -16), 0);
assert_eq!(adjust(100, 20), 120);
}
}