use crate::components::{Box, Text};
use crate::core::{Color, Element, FlexDirection};
#[derive(Debug, Clone)]
pub struct ColorPalette {
pub colors: Vec<Color>,
pub name: String,
}
impl ColorPalette {
pub fn new(name: impl Into<String>, colors: Vec<Color>) -> Self {
Self {
name: name.into(),
colors,
}
}
pub fn basic() -> Self {
Self::new(
"Basic",
vec![
Color::Black,
Color::Red,
Color::Green,
Color::Yellow,
Color::Blue,
Color::Magenta,
Color::Cyan,
Color::White,
Color::BrightBlack,
Color::BrightRed,
Color::BrightGreen,
Color::BrightYellow,
Color::BrightBlue,
Color::BrightMagenta,
Color::BrightCyan,
Color::BrightWhite,
],
)
}
pub fn grayscale() -> Self {
let colors: Vec<Color> = (0..24)
.map(|i| {
let gray = 8 + i * 10;
Color::Rgb(gray, gray, gray)
})
.collect();
Self::new("Grayscale", colors)
}
pub fn rainbow() -> Self {
Self::new(
"Rainbow",
vec![
Color::Rgb(255, 0, 0), Color::Rgb(255, 127, 0), Color::Rgb(255, 255, 0), Color::Rgb(0, 255, 0), Color::Rgb(0, 255, 255), Color::Rgb(0, 0, 255), Color::Rgb(127, 0, 255), Color::Rgb(255, 0, 255), ],
)
}
pub fn pastel() -> Self {
Self::new(
"Pastel",
vec![
Color::Rgb(255, 179, 186), Color::Rgb(255, 223, 186), Color::Rgb(255, 255, 186), Color::Rgb(186, 255, 201), Color::Rgb(186, 225, 255), Color::Rgb(218, 186, 255), ],
)
}
pub fn material() -> Self {
Self::new(
"Material",
vec![
Color::Rgb(244, 67, 54), Color::Rgb(233, 30, 99), Color::Rgb(156, 39, 176), Color::Rgb(103, 58, 183), Color::Rgb(63, 81, 181), Color::Rgb(33, 150, 243), Color::Rgb(3, 169, 244), Color::Rgb(0, 188, 212), Color::Rgb(0, 150, 136), Color::Rgb(76, 175, 80), Color::Rgb(139, 195, 74), Color::Rgb(205, 220, 57), Color::Rgb(255, 235, 59), Color::Rgb(255, 193, 7), Color::Rgb(255, 152, 0), Color::Rgb(255, 87, 34), ],
)
}
}
impl Default for ColorPalette {
fn default() -> Self {
Self::basic()
}
}
#[derive(Debug, Clone, Default)]
pub struct ColorPickerState {
pub selected: usize,
pub open: bool,
}
impl ColorPickerState {
pub fn new() -> Self {
Self::default()
}
pub fn open(&mut self) {
self.open = true;
}
pub fn close(&mut self) {
self.open = false;
}
pub fn toggle(&mut self) {
self.open = !self.open;
}
pub fn select(&mut self, index: usize) {
self.selected = index;
}
pub fn select_prev(&mut self, max: usize) {
if self.selected > 0 {
self.selected -= 1;
} else if max > 0 {
self.selected = max - 1;
}
}
pub fn select_next(&mut self, max: usize) {
if max > 0 && self.selected < max - 1 {
self.selected += 1;
} else {
self.selected = 0;
}
}
}
#[derive(Debug, Clone)]
pub struct ColorPickerStyle {
pub colors_per_row: usize,
pub show_names: bool,
pub show_hex: bool,
pub selection_indicator: String,
pub border_color: Color,
}
impl Default for ColorPickerStyle {
fn default() -> Self {
Self {
colors_per_row: 8,
show_names: false,
show_hex: false,
selection_indicator: "â–¼".to_string(),
border_color: Color::White,
}
}
}
impl ColorPickerStyle {
pub fn new() -> Self {
Self::default()
}
pub fn colors_per_row(mut self, count: usize) -> Self {
self.colors_per_row = count;
self
}
pub fn show_names(mut self, show: bool) -> Self {
self.show_names = show;
self
}
pub fn show_hex(mut self, show: bool) -> Self {
self.show_hex = show;
self
}
pub fn compact() -> Self {
Self::new()
.colors_per_row(16)
.show_names(false)
.show_hex(false)
}
pub fn detailed() -> Self {
Self::new()
.colors_per_row(4)
.show_names(true)
.show_hex(true)
}
}
#[derive(Debug)]
pub struct ColorPicker {
palette: ColorPalette,
state: ColorPickerState,
style: ColorPickerStyle,
title: Option<String>,
}
impl ColorPicker {
pub fn new() -> Self {
Self {
palette: ColorPalette::basic(),
state: ColorPickerState::new(),
style: ColorPickerStyle::default(),
title: None,
}
}
pub fn palette(mut self, palette: ColorPalette) -> Self {
self.palette = palette;
self
}
pub fn state(mut self, state: ColorPickerState) -> Self {
self.state = state;
self
}
pub fn style(mut self, style: ColorPickerStyle) -> Self {
self.style = style;
self
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn selected(mut self, index: usize) -> Self {
self.state.selected = index;
self
}
pub fn selected_color(&self) -> Option<Color> {
self.palette.colors.get(self.state.selected).copied()
}
fn color_to_hex(color: &Color) -> String {
match color {
Color::Rgb(r, g, b) => format!("#{:02X}{:02X}{:02X}", r, g, b),
_ => "#??????".to_string(),
}
}
fn render_swatch(&self, color: &Color, is_selected: bool) -> String {
let block = "██";
let indicator = if is_selected {
&self.style.selection_indicator
} else {
" "
};
format!(
"{}{}{}{}",
color.to_ansi_fg(),
block,
"\x1b[0m",
indicator
)
}
pub fn into_element(self) -> Element {
let mut container = Box::new().flex_direction(FlexDirection::Column);
if let Some(title) = &self.title {
container = container.child(Text::new(title).into_element());
}
let colors = &self.palette.colors;
let per_row = self.style.colors_per_row;
for (row_idx, chunk) in colors.chunks(per_row).enumerate() {
let mut row_str = String::new();
for (col_idx, color) in chunk.iter().enumerate() {
let idx = row_idx * per_row + col_idx;
let is_selected = idx == self.state.selected;
row_str.push_str(&self.render_swatch(color, is_selected));
}
container = container.child(Text::new(row_str).into_element());
}
if self.style.show_hex || self.style.show_names {
if let Some(color) = self.selected_color() {
let mut info = String::new();
if self.style.show_hex {
info.push_str(&Self::color_to_hex(&color));
}
if !info.is_empty() {
container = container.child(Text::new(info).into_element());
}
}
}
container.into_element()
}
}
impl Default for ColorPicker {
fn default() -> Self {
Self::new()
}
}
pub fn color_picker() -> ColorPicker {
ColorPicker::new()
}
pub fn color_picker_with_palette(palette: ColorPalette) -> ColorPicker {
ColorPicker::new().palette(palette)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_color_palette_basic() {
let palette = ColorPalette::basic();
assert_eq!(palette.colors.len(), 16);
}
#[test]
fn test_color_palette_grayscale() {
let palette = ColorPalette::grayscale();
assert_eq!(palette.colors.len(), 24);
}
#[test]
fn test_color_palette_presets() {
let _ = ColorPalette::rainbow();
let _ = ColorPalette::pastel();
let _ = ColorPalette::material();
}
#[test]
fn test_color_picker_state() {
let mut state = ColorPickerState::new();
assert!(!state.open);
state.open();
assert!(state.open);
state.close();
assert!(!state.open);
}
#[test]
fn test_color_picker_state_navigation() {
let mut state = ColorPickerState::new();
state.selected = 0;
state.select_next(5);
assert_eq!(state.selected, 1);
state.select_prev(5);
assert_eq!(state.selected, 0);
state.select_prev(5);
assert_eq!(state.selected, 4);
}
#[test]
fn test_color_picker_creation() {
let picker = ColorPicker::new();
assert_eq!(picker.palette.colors.len(), 16);
}
#[test]
fn test_color_picker_selected_color() {
let picker = ColorPicker::new().selected(0);
let color = picker.selected_color();
assert!(color.is_some());
}
#[test]
fn test_color_picker_style() {
let style = ColorPickerStyle::new()
.colors_per_row(4)
.show_hex(true);
assert_eq!(style.colors_per_row, 4);
assert!(style.show_hex);
}
#[test]
fn test_color_picker_into_element() {
let picker = ColorPicker::new();
let _ = picker.into_element();
}
#[test]
fn test_color_to_hex() {
let hex = ColorPicker::color_to_hex(&Color::Rgb(255, 0, 128));
assert_eq!(hex, "#FF0080");
}
#[test]
fn test_color_picker_helpers() {
let _ = color_picker();
let _ = color_picker_with_palette(ColorPalette::rainbow());
}
}