summaryrefslogtreecommitdiff
path: root/src/edit.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/edit.rs')
-rw-r--r--src/edit.rs600
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)
+ })
+ }
+}