diff options
Diffstat (limited to 'src/edit.rs')
-rw-r--r-- | src/edit.rs | 600 |
1 files changed, 600 insertions, 0 deletions
diff --git a/src/edit.rs b/src/edit.rs new file mode 100644 index 0000000..f21cf73 --- /dev/null +++ b/src/edit.rs @@ -0,0 +1,600 @@ +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<int> 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<string> 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<int> base: 0; + + squares := HorizontalBox { + width : 100%; + height : 20px; + + for col[i] in colors : psquare := Rectangle { + property <color> 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 <int> val; + property <string> 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<color> 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 <int> 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 : "<unnamed>"; + 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<String>, + scheme: Scheme, +} + +impl Edit +{ + pub fn new(name: Option<String>, scheme: Scheme) -> Self + { + Self { name, scheme } + } + + pub fn run(self) -> Result<()> + { + let Self { name, scheme } = self; + + let pal = Palette::try_from(&scheme)?.iter().collect::<Vec<Rgb>>(); + + let primary = pal[0..8] + .iter() + .map(|Rgb(r, g, b)| Color::from_rgb_u8(*r, *g, *b)) + .collect::<Vec<Color>>(); + + let secondary = pal[8..] + .iter() + .map(|Rgb(r, g, b)| Color::from_rgb_u8(*r, *g, *b)) + .collect::<Vec<Color>>(); + + 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::<Aux>().on_select(move |i: i32| { + let i = (i as usize % npal) as i32; + guiw.unwrap().global::<Aux>().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::<Aux>().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::<Aux>().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::<Aux>().on_format_rgb_component(|val| { + let val = 0xff & val; + format!("#{:02x} ({})", val, val).into() + }); + + gui.global::<Aux>().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::<Aux>().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::<Aux>().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::<Vec<Color>>(); + 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<PathBuf>), +} + +impl TryFrom<&str> for Command +{ + type Error = anyhow::Error; + + fn try_from(text: &str) -> Result<Self> + { + 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) + }) + } +} |