#![allow(missing_docs)]
use dracon_terminal_engine::compositor::{Cell, Color, Plane, Styles};
use dracon_terminal_engine::framework::hitzone::{HitZone, HitZoneGroup};
use dracon_terminal_engine::framework::keybindings::{actions, resolve_keybindings, KeybindingSet};
use dracon_terminal_engine::framework::prelude::*;
use dracon_terminal_engine::framework::theme::Theme;
use dracon_terminal_engine::framework::widget::Widget;
use dracon_terminal_engine::framework::widget::WidgetId;
use dracon_terminal_engine::input::event::{KeyCode, KeyEventKind, MouseEventKind};
use ratatui::layout::Rect;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PresetColor {
name: &'static str,
r: u8,
g: u8,
b: u8,
}
impl PresetColor {
const PRESETS: &'static [PresetColor] = &[
PresetColor {
name: "Red",
r: 255,
g: 0,
b: 0,
},
PresetColor {
name: "Green",
r: 0,
g: 255,
b: 0,
},
PresetColor {
name: "Blue",
r: 0,
g: 0,
b: 255,
},
PresetColor {
name: "Yellow",
r: 255,
g: 255,
b: 0,
},
PresetColor {
name: "Purple",
r: 128,
g: 0,
b: 128,
},
PresetColor {
name: "Cyan",
r: 0,
g: 255,
b: 255,
},
PresetColor {
name: "Orange",
r: 255,
g: 165,
b: 0,
},
PresetColor {
name: "Pink",
r: 255,
g: 192,
b: 203,
},
];
fn to_compositor_color(self) -> Color {
Color::Rgb(self.r, self.g, self.b)
}
fn rgb_string(&self) -> String {
format!("RGB({}, {}, {})", self.r, self.g, self.b)
}
}
pub struct ColorPicker {
id: WidgetId,
selected_index: usize,
theme: Theme,
area: std::cell::Cell<Rect>,
dirty: bool,
hitzones: HitZoneGroup<usize>,
}
impl ColorPicker {
pub fn new(theme: Theme) -> Self {
Self {
id: WidgetId::default_id(),
selected_index: 0,
theme,
area: std::cell::Cell::new(Rect::new(0, 0, 30, 5)),
dirty: true,
hitzones: HitZoneGroup::new(),
}
}
pub fn with_id(id: WidgetId, theme: Theme) -> Self {
Self {
id,
selected_index: 0,
theme,
area: std::cell::Cell::new(Rect::new(0, 0, 30, 5)),
dirty: true,
hitzones: HitZoneGroup::new(),
}
}
pub fn initial_color(mut self, color_name: &str) -> Self {
self.selected_index = PresetColor::PRESETS
.iter()
.position(|p| p.name.to_lowercase() == color_name.to_lowercase())
.unwrap_or(0);
self
}
pub fn with_theme(mut self, theme: Theme) -> Self {
self.theme = theme;
self
}
pub fn selected_color(&self) -> &'static PresetColor {
&PresetColor::PRESETS[self.selected_index]
}
pub fn cycle_next(&mut self) {
self.selected_index = (self.selected_index + 1) % PresetColor::PRESETS.len();
self.dirty = true;
}
pub fn cycle_prev(&mut self) {
if self.selected_index == 0 {
self.selected_index = PresetColor::PRESETS.len() - 1;
} else {
self.selected_index -= 1;
}
self.dirty = true;
}
fn rebuild_hitzones(&mut self) {
let area = self.area.get();
self.hitzones = HitZoneGroup::new();
let swatch_zone = HitZone::new(
0, area.x, area.y, 6, area.height, )
.on_click(|_click_kind| {
});
self.hitzones.zones_mut().push(swatch_zone);
}
}
impl Widget for ColorPicker {
fn id(&self) -> WidgetId {
self.id
}
fn set_id(&mut self, id: WidgetId) {
self.id = id;
}
fn area(&self) -> Rect {
self.area.get()
}
fn set_area(&mut self, area: Rect) {
self.area.set(area);
self.dirty = true;
self.rebuild_hitzones();
}
fn focusable(&self) -> bool {
true
}
fn z_index(&self) -> u16 {
0
}
fn needs_render(&self) -> bool {
self.dirty
}
fn mark_dirty(&mut self) {
self.dirty = true;
}
fn clear_dirty(&mut self) {
self.dirty = false;
}
fn render(&self, area: Rect) -> Plane {
let mut plane = Plane::new(self.id.0, area.width, area.height);
plane.z_index = 0;
let color = self.selected_color();
let color_cell = color.to_compositor_color();
let header = format!("[ {} ] {}", "●", color.name);
for (i, c) in header.chars().take(area.width as usize).enumerate() {
plane.cells[i] = Cell {
char: c,
fg: self.theme.fg,
bg: self.theme.bg,
style: Styles::BOLD,
transparent: false,
skip: false,
};
}
let swatch_row = 1usize;
for col in 0..area.width as usize {
let idx = swatch_row * area.width as usize + col;
if idx < plane.cells.len() {
plane.cells[idx] = Cell {
char: '█',
fg: color_cell,
bg: color_cell,
style: Styles::empty(),
transparent: false,
skip: false,
};
}
}
let rgb_text = color.rgb_string();
let rgb_row = 2usize;
let start_x = (area.width as i32 - rgb_text.len() as i32) / 2;
let start_x = start_x.max(0) as usize;
for (i, c) in rgb_text
.chars()
.take(area.width as usize - start_x)
.enumerate()
{
let idx = rgb_row * area.width as usize + start_x + i;
if idx < plane.cells.len() {
plane.cells[idx] = Cell {
char: c,
fg: color_cell,
bg: self.theme.bg,
style: Styles::empty(),
transparent: false,
skip: false,
};
}
}
let instruction_row = 3usize;
let instruction = if area.width >= 20 {
"←/→ or Click to change"
} else {
"←/→"
};
let instr_len = instruction.len().min(area.width as usize);
let instr_start = (area.width as usize - instr_len) / 2;
for (i, c) in instruction.chars().take(instr_len).enumerate() {
let idx = instruction_row * area.width as usize + instr_start + i;
if idx < plane.cells.len() {
plane.cells[idx] = Cell {
char: c,
fg: self.theme.fg_muted,
bg: self.theme.bg,
style: Styles::DIM,
transparent: false,
skip: false,
};
}
}
let index_row = 4usize;
let index_text = format!("{}/{}", self.selected_index + 1, PresetColor::PRESETS.len());
let index_start = (area.width as usize - index_text.len()) / 2;
for (i, c) in index_text
.chars()
.take(area.width as usize - index_start)
.enumerate()
{
let idx = index_row * area.width as usize + index_start + i;
if idx < plane.cells.len() {
plane.cells[idx] = Cell {
char: c,
fg: self.theme.primary,
bg: self.theme.bg,
style: Styles::empty(),
transparent: false,
skip: false,
};
}
}
plane
}
fn on_focus(&mut self) {
self.dirty = true;
}
fn on_blur(&mut self) {
self.dirty = true;
}
fn on_mount(&mut self) {
self.dirty = true;
self.rebuild_hitzones();
}
fn on_unmount(&mut self) {
}
fn on_theme_change(&mut self, theme: &Theme) {
self.theme = theme.clone();
self.dirty = true;
}
fn handle_key(&mut self, key: KeyEvent) -> bool {
if key.kind != KeyEventKind::Press {
return false;
}
match key.code {
KeyCode::Left => {
self.cycle_prev();
true
}
KeyCode::Right => {
self.cycle_next();
true
}
KeyCode::Enter | KeyCode::Char(' ') => {
self.cycle_next();
true
}
_ => false,
}
}
fn handle_mouse(&mut self, kind: MouseEventKind, local_col: u16, _local_row: u16) -> bool {
if let MouseEventKind::Down(_) = kind {
if local_col < 6 {
self.cycle_next();
return true;
}
}
false
}
fn commands(&self) -> Vec<dracon_terminal_engine::framework::command::BoundCommand> {
Vec::new()
}
fn apply_command_output(
&mut self,
_output: &dracon_terminal_engine::framework::command::ParsedOutput,
) {
}
}
impl Default for ColorPicker {
fn default() -> Self {
Self::new(Theme::default())
}
}
fn main() -> std::io::Result<()> {
let themes: Vec<Theme> = Theme::all().to_vec();
let mut app = App::new()?
.title("Widget Tutorial: ColorPicker")
.fps(30)
.theme(Theme::from_env_or(Theme::nord()));
let current_theme_idx = 0;
let current_theme = themes[current_theme_idx].clone();
let kb_config = resolve_keybindings();
let _keybindings = KeybindingSet::from_config(&kb_config);
let _kb_theme = kb_config.get(actions::THEME).unwrap_or("t");
let red_picker = ColorPicker::new(current_theme.clone())
.initial_color("Red");
let green_picker = ColorPicker::new(current_theme.clone())
.initial_color("Green");
let blue_picker = ColorPicker::new(current_theme.clone())
.initial_color("Blue");
let yellow_picker = ColorPicker::new(current_theme.clone())
.initial_color("Yellow");
let mut header = dracon_terminal_engine::framework::widgets::Label::new(
"←/→ to change color | Click swatch to cycle | Tab to navigate",
);
let mut footer = dracon_terminal_engine::framework::widgets::Label::new(&format!(
"Theme: {}",
&themes[current_theme_idx].display_name
));
header.on_theme_change(¤t_theme);
footer.on_theme_change(¤t_theme);
let picker_width = 25u16;
let picker_height = 6u16;
let _id1 = app.add_widget(
Box::new(red_picker),
Rect::new(0, 0, picker_width, picker_height),
);
let _id2 = app.add_widget(
Box::new(green_picker),
Rect::new(26, 0, picker_width, picker_height),
);
let _id3 = app.add_widget(
Box::new(blue_picker),
Rect::new(0, 7, picker_width, picker_height),
);
let _id4 = app.add_widget(
Box::new(yellow_picker),
Rect::new(26, 7, picker_width, picker_height),
);
let _header_id = app.add_widget(Box::new(header), Rect::new(0, 14, 80, 1));
let _footer_id = app.add_widget(Box::new(footer), Rect::new(0, 15, 80, 1));
app.run(|_| {})
}