use vtcol::{Palette, Rgb, Scheme}; use std::{fmt, path::{Path, PathBuf}, rc::Rc}; use anyhow::{anyhow, Result}; use slint::{re_exports::KeyEvent, Color, VecModel}; slint::slint! { import { HorizontalBox, VerticalBox } from "std-widgets.slint"; export global Aux := { property selected: 0; callback select(int); callback set-palette-color(int, color); callback format-rgb-hex(color) -> string; callback format-rgb-component(int) -> string; callback color-of-rgb-component(string, int) -> color; callback component-of-rgb(string, color) -> int; callback handle-command-buffer(KeyEvent, string, int) -> string; property mode: "normal"; /* normal | insert | command */ } Colors := Rectangle { width : 100%; border-width : 2px; background: @linear-gradient(90deg, #002b36 0%, #073642 100%); property <[color]> colors: [ rgb( 0, 0, 0), ]; property base: 0; squares := HorizontalBox { width : 100%; height : 20px; for col[i] in colors : psquare := Rectangle { property current-color : col; width : (squares.width / 8) - 12px; height : (squares.width / 8) - 12px; border-color : i == (Aux.selected - base) ? #cb4b16 : #839496; border-width : 3px; forward-focus: pval; ptouch := TouchArea { clicked => { Aux.select(base + i); } } prect := Rectangle { y : 3px; x : 3px; width : psquare.width - 6px; height : psquare.height - 6px; background : current-color; VerticalBox { Rectangle { pdesc := Text { /* Text will be set through callback from Rust. */ text : i; } } Rectangle { background : ptouch.has-hover ? #ffffff77 : #cccccc33; pval := Text { text : Aux.format-rgb-hex(current-color); font-family : "mono"; font-size : 9pt; } } Rectangle { background : green; } } } } } } ComponentEdit := HorizontalBox { height : 18pt; alignment : start; property val; property comp : ""; Text { width : 15%; height : 14.4pt; color : #a0a0a0; font-size : 12pt; font-weight : 700; text : Aux.format-rgb-component (val); } Rectangle { border-width : 2px; height : 14.4pt; width : 75%; background : Aux.color-of-rgb-component (comp, val); } } Edit := Rectangle { width : 100%; background : @linear-gradient(90deg, #002b36 0%, #073642 100%); border-color : Aux.mode == "insert" ? #cb4b16 : #839496; border-width : 3px; property rgb : Colors.white; callback update (color); update (col) => { debug("edit > update"); red.val = Aux.component-of-rgb("r", col); green.val = Aux.component-of-rgb("g", col); blue.val = Aux.component-of-rgb("b", col); } VerticalBox { alignment : start; red := ComponentEdit { val : Aux.component-of-rgb("r", rgb); comp: "r"; } green := ComponentEdit { val : Aux.component-of-rgb("g", rgb); comp: "g"; } blue := ComponentEdit { val : Aux.component-of-rgb("b", rgb); comp: "b"; } } } CommandBar := Rectangle { width : 100%; height : t.preferred-height + 3pt; background : @linear-gradient(90deg, #002b36 0%, #073642 100%); property cursor : 0; property text <=> t.text; callback input (KeyEvent) -> bool; callback clear; clear () => { t.text = ""; } input (k) => { t.text = Aux.handle-command-buffer (k, t.text, cursor); /* Buffer is empty if the user backspaced her way to the left. */ return t.text == ""; } t := Text { text: ""; color: #fdf6e3; vertical-alignment: center; } } GuiEdit := Window { property scheme-name <=> name.text; property <[color]> primary <=> primary-colors .colors; property <[color]> secondary <=> secondary-colors.colors; callback get-palette-color(int) -> color; callback set-palette-color(int, color); callback update-edit <=> edit.update; callback user-quit(); key-inputs := FocusScope { key-pressed(event) => { debug("input: got", event.text, "shift?", event.modifiers.shift); /* Escape always gets you back to normal mode. */ if (event.text == Keys.Escape) { debug("enter normal"); master.enter-normal-mode (); } else if (Aux.mode == "command") { if (command.input (event)) { master.enter-normal-mode (); } } else if (Aux.mode == "insert") { /* In insert mode, most selection bindings are frozen. */ } /* Insert mode. */ else { /* Normal mode bindings. */ if (event.text == "q") { user-quit(); } if (event.text == "h") { debug("select prev"); master.select-prev(); } else if (event.text == "l") { debug("select next"); master.select-next(); } else if (event.text == " ") { if (event.modifiers.shift) { debug("select prev"); master.select-prev(); } else { debug("select next"); master.select-next(); } } else if (event.text == "j" || event.text == "k") { debug("select other row"); master.select-other-row(); } else if (event.text == Keys.Return || event.text == "i") { debug("enter insert"); master.enter-insert-mode(); } else if (event.text == ":") { debug("enter command"); master.enter-command-mode(); } if (event.modifiers.control) { //debug("control was pressed during this event"); } } /* Normal mode. */ edit.update( Aux.selected < 8 ? primary-colors.colors [Aux.selected] : secondary-colors.colors [Aux.selected - 8] ); accept } } get-palette-color (i) => { i < 8 ? primary-colors.colors [i] : secondary-colors.colors [i - 8] } set-palette-color (i, col) => { if (i < 8) { primary-colors.colors [i] = col; } else { secondary-colors.colors [i - 8] = col; } } master := VerticalLayout { height : 100%; callback select-prev(); callback select-next(); callback select-other-row(); callback enter-command-mode(); callback enter-insert-mode(); callback enter-normal-mode(); select-prev () => { Aux.select (Aux.selected == 0 ? 15 : Aux.selected - 1); debug ("selected previous, now", Aux.selected); } select-next () => { Aux.select (mod (Aux.selected + 1, 16)); debug ("selected next, now", Aux.selected); } select-other-row () => { Aux.select (mod (Aux.selected + 8, 16)); debug ("selected row above/below, now", Aux.selected); } enter-command-mode () => { Aux.mode = "command"; command.text = ":"; } enter-normal-mode () => { Aux.mode = "normal" ; command.clear (); } enter-insert-mode () => { Aux.mode = "insert" ; } status := HorizontalBox { width : 100%; name := Text { text : ""; color : #a0a0a0; font-weight : 700; } } primary-colors := Colors { base : 0; } secondary-colors := Colors { base : 8; } edit := Edit { } /* invisible spacer */ Rectangle { vertical-stretch : 2; width : 100%; min-height : 1pt; //background : #ffFFffFF; background : #aaaaaaaa; } command := CommandBar { } } } } #[derive(Debug)] enum KeyInput { Printable(String), Escape, Return, Backspace, Delete, Tab, Left, Right, Up, Down, /** Not representable. */ Junk, } impl fmt::Display for KeyInput { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let k = match self { Self::Printable(s) => return write!(f, "‘{}’", s), Self::Escape => "ESC", Self::Return => "\\r", Self::Backspace => "\\b", Self::Delete => "DEL", Self::Tab => "\\t", Self::Left => "←", Self::Right => "→", Self::Up => "↑", Self::Down => "↓", Self::Junk => return write!(f, "{}", "Ø"), }; write!(f, "key:{}", k) } } /** Crude filter for rejecting strings containing control chars. */ fn is_printable(s: &str) -> bool { s.chars().find(|c| c.is_control()).is_none() } impl From<&KeyEvent> for KeyInput { fn from(ev: &KeyEvent) -> Self { match ev.text.as_str() { "\u{0008}" => KeyInput::Backspace, "\t" | "\u{000b}" => KeyInput::Tab, "\n" | "\r" => KeyInput::Return, "\u{000e}" => KeyInput::Left, "\u{000f}" => KeyInput::Right, "\u{0010}" => KeyInput::Up, "\u{0011}" => KeyInput::Down, "\u{001b}" => KeyInput::Escape, "\u{007F}" => KeyInput::Delete, any if is_printable(any) => KeyInput::Printable(any.into()), _ => KeyInput::Junk, } } } pub struct Edit { name: Option, scheme: Scheme, } impl Edit { pub fn new(name: Option, scheme: Scheme) -> Self { Self { name, scheme } } pub fn run(self) -> Result<()> { let Self { name, scheme } = self; let pal = Palette::try_from(&scheme)?.iter().collect::>(); let primary = pal[0..8] .iter() .map(|Rgb(r, g, b)| Color::from_rgb_u8(*r, *g, *b)) .collect::>(); let secondary = pal[8..] .iter() .map(|Rgb(r, g, b)| Color::from_rgb_u8(*r, *g, *b)) .collect::>(); let primary = Rc::new(VecModel::from(primary)); let secondary = Rc::new(VecModel::from(secondary)); let gui = GuiEdit::new(); gui.on_user_quit(move || { std::process::exit(0); }); { let guiw = gui.as_weak(); let npal = pal.len(); gui.global::().on_select(move |i: i32| { let i = (i as usize % npal) as i32; guiw.unwrap().global::().set_selected(i); let col = guiw.unwrap().invoke_get_palette_color(i); guiw.unwrap().invoke_update_edit(col); }); } { let guiw = gui.as_weak(); let npal = pal.len(); gui.global::().on_set_palette_color(move |i, col| { let i = (i as usize % npal) as i32; guiw.unwrap().invoke_set_palette_color(i, col); }); } gui.global::().on_format_rgb_hex(|col| { let x = (col.red() as u32) << 2 | (col.green() as u32) << 1 | (col.blue() as u32); format!("#{:06x}", x).into() }); gui.global::().on_format_rgb_component(|val| { let val = 0xff & val; format!("#{:02x} ({})", val, val).into() }); gui.global::().on_color_of_rgb_component(|comp, val| { match comp.as_str() { "r" => Color::from_rgb_u8(val as u8, 0, 0), "g" => Color::from_rgb_u8(0, val as u8, 0), "b" => Color::from_rgb_u8(0, 0, val as u8), _ => Color::from_rgb_u8(0, 0, 0), } }); /* Why the hell is there no API for this in .60‽ */ gui.global::().on_component_of_rgb(|comp, col| { match comp.as_str() { "r" => col.red() as i32, "g" => col.green() as i32, "b" => col.blue() as i32, _ => 0, } }); { let guiw = gui.as_weak(); gui.global::().on_handle_command_buffer(move |ev, text, _pos| { let text = match KeyInput::from(&ev) { KeyInput::Printable(s) => { let mut text = text.to_string(); text.push_str(&s); text }, KeyInput::Return => { match Command::try_from(text.as_str()) { Err(e) => { eprintln!("bad command [{}]: {}", text, e); String::new() }, Ok(cmd) => { use slint::Model; let scheme_name = guiw.unwrap().get_scheme_name().to_string(); let prim = guiw.unwrap().get_primary(); let secn = guiw.unwrap().get_secondary(); let pal = prim.iter().chain(secn.iter()) .collect::>(); let pal = Palette::from(pal.as_slice()); if let Err(e) = cmd .exec(scheme_name.as_str(), pal) { eprintln!( "error executing command [{}]: {}", text, e ); } String::new() }, } /* The empty string signals to leave command mode. */ }, KeyInput::Backspace => match text.char_indices().next_back() { Some((i, _)) => text[..i].into(), None => text.to_string(), }, other => { eprintln!( "»»» command mode input: “{}”", other.to_string() ); text.to_string() }, }; eprintln!("»»» command buffer: “{}”", text); text.into() }); } if let Some(name) = name { gui.set_scheme_name(name.into()); } gui.set_primary(primary.into()); gui.set_secondary(secondary.into()); gui.run(); Ok(()) } } #[derive(Debug)] enum Command { Noop, Save(Option), } impl TryFrom<&str> for Command { type Error = anyhow::Error; fn try_from(text: &str) -> Result { let text = text.trim_start_matches(':').trim(); if text.len() == 0 { return Ok(Self::Noop); } let (cmd, args) = if let Some(cmd) = text.get(..1) { (cmd, &text[1..]) } else { return Err(anyhow!("non-ascii command‽")); }; match (cmd, args) { ("w", "") => Ok(Self::Save(None)), ("w", rest) => { let rest = rest.trim(); let path = PathBuf::from(rest); Ok(Self::Save(Some(path))) }, _ => Err(anyhow!("only save (‘:w’) implemented for now")), } } } impl Command { fn exec(self, name: &str, pal: Palette) -> Result<()> { match self { Self::Noop => Ok(()), Self::Save(None) => Self::save(&PathBuf::from(&name), pal), Self::Save(Some(path)) => Self::save(&path, pal), } //todo!("got command: {:?}", self) } fn save(path: &Path, pal: Palette) -> Result<()> { use std::io::Write; let mut f = std::fs::OpenOptions::new().create(true).write(true).open(path)?; let pal: String = pal.into(); f.write_all(pal.as_bytes()).map_err(|e| { anyhow!("error saving Palette to file [{}]: {}", path.display(), e) }) } }