From b0d45a5eec03b30dcefc502381e20f662cb227ca Mon Sep 17 00:00:00 2001 From: Joe Bellus Date: Thu, 13 Oct 2022 14:39:48 -0400 Subject: [PATCH] sync commit --- Cargo.lock | 303 +++++++++++++++++++- abacus-core/Cargo.toml | 1 + abacus-core/src/dataframe.rs | 24 +- abacus-core/src/engine.rs | 33 +++ abacus-core/src/lib.rs | 4 + abacus-core/src/save_file.rs | 45 +++ abacus-ui/Cargo.toml | 5 +- abacus-ui/assets/ban.svg | 38 +++ abacus-ui/assets/floppy-disk.svg | 38 +++ abacus-ui/assets/folder-open.svg | 38 +++ abacus-ui/assets/play.svg | 38 +++ abacus-ui/assets/plus.svg | 38 +++ abacus-ui/assets/trash.svg | 38 +++ abacus-ui/src/app_delegate.rs | 93 ++++++ abacus-ui/src/app_header.rs | 176 ++++++++++++ abacus-ui/src/commands.rs | 5 + abacus-ui/src/data.rs | 71 +++++ abacus-ui/src/editor.rs | 474 +++++++++++++++++++++---------- abacus-ui/src/main.rs | 50 +++- abacus-ui/src/output_block.rs | 76 +++++ abacus-ui/src/toolbar_button.rs | 68 +++++ 21 files changed, 1491 insertions(+), 165 deletions(-) create mode 100644 abacus-core/src/save_file.rs create mode 100644 abacus-ui/assets/ban.svg create mode 100644 abacus-ui/assets/floppy-disk.svg create mode 100644 abacus-ui/assets/folder-open.svg create mode 100644 abacus-ui/assets/play.svg create mode 100644 abacus-ui/assets/plus.svg create mode 100644 abacus-ui/assets/trash.svg create mode 100644 abacus-ui/src/app_delegate.rs create mode 100644 abacus-ui/src/app_header.rs create mode 100644 abacus-ui/src/commands.rs create mode 100644 abacus-ui/src/data.rs create mode 100644 abacus-ui/src/output_block.rs create mode 100644 abacus-ui/src/toolbar_button.rs diff --git a/Cargo.lock b/Cargo.lock index d6a4de6..502ff2b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,6 +9,7 @@ dependencies = [ "polars", "rhai", "serde", + "serde_json", "syntect", "tracing-subscriber", ] @@ -18,6 +19,7 @@ name = "abacus-ui" version = "0.1.0" dependencies = [ "abacus-core", + "clipboard", "druid", "ropey", "syntect", @@ -160,6 +162,15 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitmaps" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" +dependencies = [ + "typenum", +] + [[package]] name = "block" version = "0.1.6" @@ -252,6 +263,28 @@ dependencies = [ "winapi", ] +[[package]] +name = "clipboard" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a904646c0340239dcf7c51677b33928bf24fdf424b79a57909c0109075b2e7" +dependencies = [ + "clipboard-win", + "objc", + "objc-foundation", + "objc_id", + "x11-clipboard", +] + +[[package]] +name = "clipboard-win" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a093d6fed558e5fe24c3dfc85a68bb68f1c824f440d3ba5aca189e2998786b" +dependencies = [ + "winapi", +] + [[package]] name = "cocoa" version = "0.24.0" @@ -444,6 +477,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "data-url" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a30bfce702bcfa94e906ef82421f2c0e61c076ad76030c16ee5d2e9a32fe193" +dependencies = [ + "matches", +] + [[package]] name = "dirs" version = "4.0.0" @@ -476,12 +518,14 @@ dependencies = [ "fluent-langneg", "fluent-syntax", "fnv", + "im", "instant", "tracing", "tracing-subscriber", "tracing-wasm", "unic-langid", "unicode-segmentation", + "usvg", "xi-unicode", ] @@ -578,6 +622,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "float-cmp" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75224bec9bfe1a65e2d34132933f2de7fe79900c96a0174307554244ece8150e" + [[package]] name = "fluent-bundle" version = "0.15.2" @@ -618,6 +668,17 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "fontdb" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e58903f4f8d5b58c7d300908e4ebe5289c1bfdf5587964330f12023b8ff17fd1" +dependencies = [ + "log", + "memmap2 0.2.3", + "ttf-parser 0.12.3", +] + [[package]] name = "foreign-types" version = "0.3.2" @@ -953,6 +1014,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "im" +version = "15.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0acd33ff0285af998aaf9b57342af478078f53492322fafc47450e09397e0e9" +dependencies = [ + "bitmaps", + "rand_core", + "rand_xoshiro", + "sized-chunks", + "typenum", + "version_check", +] + [[package]] name = "indexmap" version = "1.9.1" @@ -1183,6 +1258,15 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memmap2" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "723e3ebdcdc5c023db1df315364573789f8857c11b631a2fdfad7c00f5c046b4" +dependencies = [ + "libc", +] + [[package]] name = "memmap2" version = "0.5.7" @@ -1357,6 +1441,26 @@ dependencies = [ "malloc_buf", ] +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + [[package]] name = "once_cell" version = "1.15.0" @@ -1476,6 +1580,12 @@ dependencies = [ "ucd-trie", ] +[[package]] +name = "pico-args" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468" + [[package]] name = "piet" version = "0.5.0" @@ -1659,7 +1769,7 @@ dependencies = [ "lexical", "lexical-core", "memchr", - "memmap2", + "memmap2 0.5.7", "num", "once_cell", "polars-arrow", @@ -1820,6 +1930,15 @@ dependencies = [ "rand", ] +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core", +] + [[package]] name = "rayon" version = "1.5.3" @@ -1844,6 +1963,12 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "rctree" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be9e29cb19c8fe84169fcb07f8f11e66bc9e6e0280efd4715c54818296f8a4a8" + [[package]] name = "redox_syscall" version = "0.2.16" @@ -1917,6 +2042,15 @@ dependencies = [ "str_indices", ] +[[package]] +name = "roxmltree" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "921904a62e410e37e215c40381b7117f830d9d89ba60ab5236170541dd25646b" +dependencies = [ + "xmlparser", +] + [[package]] name = "rustc-hash" version = "1.1.0" @@ -1938,6 +2072,22 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8" +[[package]] +name = "rustybuzz" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab463a295d00f3692e0974a0bfd83c7a9bcd119e27e07c2beecdb1b44a09d10" +dependencies = [ + "bitflags", + "bytemuck", + "smallvec", + "ttf-parser 0.9.0", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-general-category", + "unicode-script", +] + [[package]] name = "ryu" version = "1.0.11" @@ -2011,9 +2161,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.85" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" +checksum = "41feea4228a6f1cd09ec7a3593a682276702cd67b5273544757dae23c096f074" dependencies = [ "itoa", "ryu", @@ -2065,6 +2215,31 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" +[[package]] +name = "simplecss" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a11be7c62927d9427e9f40f3444d5499d868648e2edbc4e2116de69e7ec0e89d" +dependencies = [ + "log", +] + +[[package]] +name = "siphasher" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" + +[[package]] +name = "sized-chunks" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" +dependencies = [ + "bitmaps", + "typenum", +] + [[package]] name = "slab" version = "0.4.7" @@ -2146,6 +2321,16 @@ dependencies = [ "syn", ] +[[package]] +name = "svgtypes" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c536faaff1a10837cfe373142583f6e27d81e96beba339147e77b67c9f260ff" +dependencies = [ + "float-cmp", + "siphasher", +] + [[package]] name = "syn" version = "1.0.102" @@ -2333,6 +2518,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ttf-parser" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ddb402ac6c2af6f7a2844243887631c4e94b51585b229fcfddb43958cd55ca" + +[[package]] +name = "ttf-parser" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ae2f58a822f08abdaf668897e96a5656fe72f5a9ce66422423e8849384872e6" + [[package]] name = "type-map" version = "0.4.0" @@ -2342,6 +2539,12 @@ dependencies = [ "rustc-hash", ] +[[package]] +name = "typenum" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" + [[package]] name = "ucd-trie" version = "0.1.5" @@ -2417,24 +2620,87 @@ dependencies = [ "unic-common", ] +[[package]] +name = "unicode-bidi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d12260fb92d52f9008be7e4bca09f584780eb2266dc8fecc6a192bec561694" + +[[package]] +name = "unicode-ccc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2520efa644f8268dce4dcd3050eaa7fc044fca03961e9998ac7e2e92b77cf1" + +[[package]] +name = "unicode-general-category" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9af028e052a610d99e066b33304625dea9613170a2563314490a4e6ec5cf7f" + [[package]] name = "unicode-ident" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" +[[package]] +name = "unicode-script" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d817255e1bed6dfd4ca47258685d14d2bdcfbc64fdc9e3819bd5848057b8ecc" + [[package]] name = "unicode-segmentation" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" +[[package]] +name = "unicode-vo" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" + [[package]] name = "unicode-width" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +[[package]] +name = "usvg" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8352f317d8f9a918ba5154797fb2a93e2730244041cf7d5be35148266adfa5" +dependencies = [ + "base64", + "data-url", + "flate2", + "fontdb", + "kurbo", + "log", + "memmap2 0.2.3", + "pico-args", + "rctree", + "roxmltree", + "rustybuzz", + "simplecss", + "siphasher", + "svgtypes", + "ttf-parser 0.12.3", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + [[package]] name = "utf16_lit" version = "2.0.2" @@ -2629,6 +2895,25 @@ dependencies = [ "winapi", ] +[[package]] +name = "x11-clipboard" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89bd49c06c9eb5d98e6ba6536cf64ac9f7ee3a009b2f53996d405b3944f6bcea" +dependencies = [ + "xcb", +] + +[[package]] +name = "xcb" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e917a3f24142e9ff8be2414e36c649d47d6cc2ba81f16201cdef96e533e02de" +dependencies = [ + "libc", + "log", +] + [[package]] name = "xi-unicode" version = "0.3.0" @@ -2641,6 +2926,18 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" +[[package]] +name = "xmlparser" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "114ba2b24d2167ef6d67d7d04c8cc86522b87f490025f39f0303b7db5bf5e3d8" + +[[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + [[package]] name = "yaml-rust" version = "0.4.5" diff --git a/abacus-core/Cargo.toml b/abacus-core/Cargo.toml index d67445f..deddaab 100644 --- a/abacus-core/Cargo.toml +++ b/abacus-core/Cargo.toml @@ -11,5 +11,6 @@ syntect = "5.0.0" rhai = "1.10.1" polars = { version = "0.24.3", features = ["lazy", "rows"] } tracing-subscriber = "0.3" +serde_json = "1.0.86" diff --git a/abacus-core/src/dataframe.rs b/abacus-core/src/dataframe.rs index 14dbd26..679679b 100644 --- a/abacus-core/src/dataframe.rs +++ b/abacus-core/src/dataframe.rs @@ -18,6 +18,9 @@ pub fn setup_engine(engine: &mut rhai::Engine) { engine.register_type::(); engine.register_indexer_get(Series::s_get); engine.register_fn("series", script_functions::series); + engine.register_fn("to_series", script_functions::to_series); + engine.register_fn("to_series", script_functions::to_series_unnamed); + engine.register_fn("series", script_functions::series_unnamed); engine.register_fn("head", Series::s_head); engine.register_fn("+", Series::add); engine.register_fn("sort", Series::s_sort); @@ -51,7 +54,7 @@ pub fn setup_engine(engine: &mut rhai::Engine) { engine.register_fn("first", script_functions::first); } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct DataFrame(polars::frame::DataFrame); impl Deref for DataFrame { @@ -72,6 +75,8 @@ impl DataFrame { pub fn load_csv(path: &str) -> ScriptResult { let df = polars::io::csv::CsvReader::from_path(path) .map_err(|e| e.to_string())? + .infer_schema(Some(1)) + .with_ignore_parser_errors(true) .has_header(true) .finish() .map_err(|e| e.to_string())?; @@ -158,7 +163,7 @@ impl From for DataFrameExpression { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct Series(polars::series::Series); impl Deref for Series { @@ -288,6 +293,21 @@ mod script_functions { } } + pub fn series_unnamed(arr: rhai::Array) -> std::result::Result> { + series("Unnamed", arr) + } + + pub fn to_series_unnamed(arr: rhai::Array) -> std::result::Result> { + series("Unnamed", arr) + } + + pub fn to_series( + arr: rhai::Array, + name: &str, + ) -> std::result::Result> { + series(name, arr) + } + pub fn series(name: &str, arr: rhai::Array) -> std::result::Result> { if let Some(i) = arr.first() { let series = if i.type_id() == TypeId::of::() { diff --git a/abacus-core/src/engine.rs b/abacus-core/src/engine.rs index 7a17714..62f2eba 100644 --- a/abacus-core/src/engine.rs +++ b/abacus-core/src/engine.rs @@ -40,6 +40,21 @@ impl Engine { } } } + + pub fn process_script(&mut self, script: &str) -> Output { + match self.engine.eval::(script) { + Ok(res) if res.is::() => { + let frame = rhai::Dynamic::cast::(res); + Output::DataFrame(frame) + } + Ok(res) if res.is::() => { + let frame = rhai::Dynamic::cast::(res); + Output::Series(frame) + } + Ok(res) => Output::Scalar(res), + Err(e) => Output::Error(e.to_string()), + } + } } #[derive(Debug, Clone)] @@ -51,6 +66,24 @@ pub enum Output { Error(String), } +impl Default for Output { + fn default() -> Self { + Output::None + } +} + +impl PartialEq for Output { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Scalar(l0), Self::Scalar(r0)) => l0.to_string() == r0.to_string(), + (Self::DataFrame(l0), Self::DataFrame(r0)) => l0 == r0, + (Self::Series(l0), Self::Series(r0)) => l0 == r0, + (Self::Error(l0), Self::Error(r0)) => l0 == r0, + _ => core::mem::discriminant(self) == core::mem::discriminant(other), + } + } +} + impl Output { pub fn into_frame(self) -> dataframe::DataFrame { if let Self::DataFrame(v) = self { diff --git a/abacus-core/src/lib.rs b/abacus-core/src/lib.rs index ee4f77d..b9119eb 100644 --- a/abacus-core/src/lib.rs +++ b/abacus-core/src/lib.rs @@ -1,7 +1,11 @@ mod dataframe; mod engine; +mod save_file; +pub use engine::Block; pub use engine::Engine; +pub use engine::Output; +pub use save_file::{SaveBlock, SaveFile}; use rhai::EvalAltResult; type ScriptResult = std::result::Result>; diff --git a/abacus-core/src/save_file.rs b/abacus-core/src/save_file.rs new file mode 100644 index 0000000..7da372f --- /dev/null +++ b/abacus-core/src/save_file.rs @@ -0,0 +1,45 @@ +use std::fmt::Display; + +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Deserialize, Serialize, Debug)] +pub struct SaveBlock { + pub name: String, + pub content: String, +} + +impl SaveBlock { + pub fn new(name: &str, content: S) -> Self { + Self { + name: name.to_string(), + content: content.to_string(), + } + } +} + +#[derive(Clone, Deserialize, Serialize, Debug)] +pub struct SaveFile { + #[serde(skip)] + pub filepath: String, + pub blocks: Vec, +} + +impl SaveFile { + pub fn new(filepath: String, blocks: Vec) -> Self { + Self { filepath, blocks } + } + + pub fn open(filepath: &str) -> Result { + let data = std::fs::read_to_string(filepath)?; + let mut slf = serde_json::from_str::(&data) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?; + slf.filepath = filepath.to_string(); + Ok(slf) + } + + pub fn save(&self) -> Result<(), std::io::Error> { + let s = serde_json::to_vec_pretty(&self) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?; + std::fs::write(&self.filepath, s) + } +} diff --git a/abacus-ui/Cargo.toml b/abacus-ui/Cargo.toml index e0bb4b1..e3dbeec 100644 --- a/abacus-ui/Cargo.toml +++ b/abacus-ui/Cargo.toml @@ -6,7 +6,8 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -druid = { git = "https://github.com/linebender/druid.git" } +druid = { git = "https://github.com/linebender/druid.git", features=["im", "svg"] } abacus-core = { path = "../abacus-core" } syntect = "5.0.0" -ropey = "1.5.0" \ No newline at end of file +ropey = "1.5.0" +clipboard = "0.5.0" \ No newline at end of file diff --git a/abacus-ui/assets/ban.svg b/abacus-ui/assets/ban.svg new file mode 100644 index 0000000..1db0217 --- /dev/null +++ b/abacus-ui/assets/ban.svg @@ -0,0 +1,38 @@ + + + + + + + diff --git a/abacus-ui/assets/floppy-disk.svg b/abacus-ui/assets/floppy-disk.svg new file mode 100644 index 0000000..0c27a3b --- /dev/null +++ b/abacus-ui/assets/floppy-disk.svg @@ -0,0 +1,38 @@ + + + + + + + diff --git a/abacus-ui/assets/folder-open.svg b/abacus-ui/assets/folder-open.svg new file mode 100644 index 0000000..c966947 --- /dev/null +++ b/abacus-ui/assets/folder-open.svg @@ -0,0 +1,38 @@ + + + + + + + diff --git a/abacus-ui/assets/play.svg b/abacus-ui/assets/play.svg new file mode 100644 index 0000000..b89eab3 --- /dev/null +++ b/abacus-ui/assets/play.svg @@ -0,0 +1,38 @@ + + + + + + + diff --git a/abacus-ui/assets/plus.svg b/abacus-ui/assets/plus.svg new file mode 100644 index 0000000..438f12d --- /dev/null +++ b/abacus-ui/assets/plus.svg @@ -0,0 +1,38 @@ + + + + + + + diff --git a/abacus-ui/assets/trash.svg b/abacus-ui/assets/trash.svg new file mode 100644 index 0000000..b25a4a6 --- /dev/null +++ b/abacus-ui/assets/trash.svg @@ -0,0 +1,38 @@ + + + + + + + diff --git a/abacus-ui/src/app_delegate.rs b/abacus-ui/src/app_delegate.rs new file mode 100644 index 0000000..f4591fb --- /dev/null +++ b/abacus-ui/src/app_delegate.rs @@ -0,0 +1,93 @@ +use druid::AppDelegate; + +use crate::{ + commands, + data::{AppData, Block}, +}; + +pub struct Delegate; + +impl AppDelegate for Delegate { + fn command( + &mut self, + _ctx: &mut druid::DelegateCtx, + _target: druid::Target, + cmd: &druid::Command, + data: &mut AppData, + _env: &druid::Env, + ) -> druid::Handled { + if let Some(file_info) = cmd.get(druid::commands::SAVE_FILE_AS) { + let filepath = file_info.path().as_os_str().to_str().unwrap().to_string(); + let _ = abacus_core::SaveFile::new( + filepath.clone(), + data.blocks + .iter() + .map(|d| abacus_core::SaveBlock::new(&d.name, &d.editor_data.content)) + .collect(), + ) + .save(); + data.filename = Some(filepath); + return druid::Handled::Yes; + } + + if let Some(file_info) = cmd.get(druid::commands::OPEN_FILE) { + let filepath = file_info.path().as_os_str().to_str().unwrap(); + match abacus_core::SaveFile::open(filepath) { + Ok(save_file) => { + data.blocks = save_file + .blocks + .iter() + .enumerate() + .map(|(i, b)| Block::new_with_content(&b.name, i, &b.content)) + .collect(); + } + Err(e) => { + println!("{}", e); + } + } + data.filename = Some(filepath.to_string()); + return druid::Handled::Yes; + } + + if cmd.is(commands::PROCESS_WORKBOOK) { + data.process(); + return druid::Handled::Yes; + } + + if cmd.is(commands::PROCESS_BLOCK) { + if let Some(index) = cmd.get(commands::PROCESS_BLOCK) { + data.process_block(*index); + return druid::Handled::Yes; + } + } + + if cmd.is(commands::DELETE_BLOCK) { + if let Some(index) = cmd.get(commands::DELETE_BLOCK) { + data.blocks.remove(*index); + return druid::Handled::Yes; + } + } + + druid::Handled::No + } + + fn event( + &mut self, + _ctx: &mut druid::DelegateCtx, + _window_id: druid::WindowId, + event: druid::Event, + data: &mut AppData, + _env: &druid::Env, + ) -> Option { + if let druid::Event::KeyDown(ref e) = event { + if druid::keyboard_types::Key::Character(String::from("n")) == e.key && e.mods.ctrl() { + data.blocks.push_back(Block::new( + &format!("Block #{}", data.blocks.len() + 1), + data.blocks.len(), + )); + return None; + } + } + Some(event) + } +} diff --git a/abacus-ui/src/app_header.rs b/abacus-ui/src/app_header.rs new file mode 100644 index 0000000..b6d122c --- /dev/null +++ b/abacus-ui/src/app_header.rs @@ -0,0 +1,176 @@ +use druid::{ + widget::{Container, Controller, Flex, Label, Padding, Painter, Svg, SvgData}, + Color, Data, LifeCycle, RenderContext, Widget, WidgetExt, +}; + +use crate::AppData; + +pub fn app_header_ui() -> impl Widget { + let open_svg = include_str!("../assets/folder-open.svg") + .parse::() + .unwrap_or_default(); + + let disk_svg = include_str!("../assets/floppy-disk.svg") + .parse::() + .unwrap_or_default(); + + let run_svg = include_str!("../assets/play.svg") + .parse::() + .unwrap_or_default(); + + let plus_svg = include_str!("../assets/plus.svg") + .parse::() + .unwrap_or_default(); + + Container::new( + Flex::row() + .must_fill_main_axis(true) + .with_spacer(20.0) + .with_child( + Container::new(Padding::new(10.0, Svg::new(open_svg).fix_width(10.0))) + .controller(ToolbarButtonController::new(Color::rgb8(50, 50, 50))) + .on_click(|ctx, _, _| { + let abacus = druid::FileSpec::new("Abacus File", &["abacus"]); + let json = druid::FileSpec::new("JSON File", &["json"]); + + let open_dialog_options = druid::FileDialogOptions::new() + .allowed_types(vec![abacus, json]) + .default_type(abacus) + .name_label("Source") + .title("Open Workbook") + .button_text("Open"); + + ctx.submit_command( + druid::commands::SHOW_OPEN_PANEL.with(open_dialog_options), + ); + }), + ) + .with_spacer(10.0) + .with_child( + Container::new(Padding::new(10.0, Svg::new(disk_svg).fix_width(10.0))) + .controller(ToolbarButtonController::new(Color::rgb8(50, 50, 50))) + .on_click(|ctx, _, _| { + let abacus = druid::FileSpec::new("Abacus File", &["abacus"]); + let json = druid::FileSpec::new("JSON File", &["json"]); + + let save_dialog_options = druid::FileDialogOptions::new() + .allowed_types(vec![abacus, json]) + .default_type(abacus) + .name_label("Target") + .title("Save workbook") + .button_text("Save"); + + ctx.submit_command( + druid::commands::SHOW_SAVE_PANEL.with(save_dialog_options), + ); + }), + ) + .with_spacer(20.0) + .with_flex_child( + Padding::new( + 5.0, + Container::new(Padding::new( + 3.0, + Label::dynamic(|data: &AppData, _| { + data.filename + .clone() + .unwrap_or_else(|| String::from("Scratch file")) + }) + .with_text_size(12.0) + .center(), + )) + .background(Color::rgb8(35, 35, 35)) + .rounded(4.0) + .border(Color::rgb8(10, 10, 10), 1.0) + .expand_width(), + ), + 1.0, + ) + .with_spacer(20.0) + .with_child( + Container::new(Padding::new(10.0, Svg::new(plus_svg).fix_width(10.0))) + .controller(ToolbarButtonController::new(Color::rgb8(50, 50, 50))) + .on_click(|_ctx, data: &mut AppData, _env| { + data.blocks.push_back(crate::Block::new( + &format!("Block #{}", data.blocks.len() + 1), + data.blocks.len(), + )); + }), + ) + .with_spacer(10.0) + .with_child( + Container::new(Padding::new(10.0, Svg::new(run_svg).fix_width(10.0))) + .controller(ToolbarButtonController::new(Color::rgb8(50, 50, 50))) + .on_click(|_ctx, data: &mut AppData, _env| data.process()), + ) + .with_spacer(20.0) + .expand_width(), + ) + .background(Color::rgb8(20, 20, 20)) +} + +pub fn header_separater() -> impl Widget { + Painter::new(|ctx, _data, _env| { + let rect = ctx.size().to_rect(); + ctx.fill(rect, &Color::rgb8(0, 0, 0)) + }) + .fix_height(1.0) + .expand_width() +} + +pub struct ToolbarButtonController { + color: Color, +} + +impl ToolbarButtonController { + pub fn new(color: Color) -> Self { + Self { color } + } +} + +impl Controller> for ToolbarButtonController { + fn event( + &mut self, + child: &mut Container, + ctx: &mut druid::EventCtx, + event: &druid::Event, + data: &mut T, + env: &druid::Env, + ) { + ctx.set_cursor(&druid::Cursor::Pointer); + child.event(ctx, event, data, env) + } + + fn lifecycle( + &mut self, + child: &mut Container, + ctx: &mut druid::LifeCycleCtx, + event: &druid::LifeCycle, + data: &T, + env: &druid::Env, + ) { + match event { + LifeCycle::HotChanged(true) => { + child.set_background(self.color.clone()); + ctx.request_paint(); + } + LifeCycle::HotChanged(false) => { + child.set_background(Color::TRANSPARENT); + ctx.request_paint(); + } + _ => {} + } + child.lifecycle(ctx, event, data, env) + } + + fn update( + &mut self, + child: &mut Container, + ctx: &mut druid::UpdateCtx, + old_data: &T, + data: &T, + env: &druid::Env, + ) { + child.update(ctx, old_data, data, env) + } +} diff --git a/abacus-ui/src/commands.rs b/abacus-ui/src/commands.rs new file mode 100644 index 0000000..a2d89e1 --- /dev/null +++ b/abacus-ui/src/commands.rs @@ -0,0 +1,5 @@ +use druid::Selector; + +pub const PROCESS_WORKBOOK: Selector<()> = Selector::new("process-workbook"); +pub const PROCESS_BLOCK: Selector = Selector::new("process-block"); +pub const DELETE_BLOCK: Selector = Selector::new("delete-block"); diff --git a/abacus-ui/src/data.rs b/abacus-ui/src/data.rs new file mode 100644 index 0000000..a5da69d --- /dev/null +++ b/abacus-ui/src/data.rs @@ -0,0 +1,71 @@ +use abacus_core::Output; +use druid::{ + im::{vector, Vector}, + Data, Lens, +}; + +use crate::editor; + +#[derive(Clone, Data, Lens, Debug)] +pub struct AppData { + #[data(same_fn = "PartialEq::eq")] + pub filename: Option, + pub blocks: Vector, +} + +impl Default for AppData { + fn default() -> Self { + Self { + filename: None, + blocks: vector![Block::new("Block #1", 0)], + } + } +} + +impl AppData { + pub fn process(&mut self) { + let mut engine = abacus_core::Engine::default(); + for mut block in self.blocks.iter_mut() { + let start = std::time::Instant::now(); + block.output = engine.process_script(&block.editor_data.content.to_string()); + println!("block executed in {}", start.elapsed().as_millis()); + } + } + pub fn process_block(&mut self, index: usize) { + let mut engine = abacus_core::Engine::default(); + if let Some(block) = self.blocks.get_mut(index) { + let start = std::time::Instant::now(); + block.output = engine.process_script(&block.editor_data.content.to_string()); + println!("block executed in {}", start.elapsed().as_millis()); + } + } +} + +#[derive(Clone, Data, Lens, Default, PartialEq, Debug)] +pub struct Block { + pub name: String, + pub editor_data: editor::EditorData, + #[data(same_fn = "PartialEq::eq")] + pub output: Output, + pub index: usize, +} + +impl Block { + pub fn new(name: &str, index: usize) -> Self { + Self { + name: name.to_string(), + editor_data: Default::default(), + output: Default::default(), + index, + } + } + + pub fn new_with_content(name: &str, index: usize, content: &str) -> Self { + Self { + name: name.to_string(), + editor_data: editor::EditorData::new(content), + output: Default::default(), + index, + } + } +} diff --git a/abacus-ui/src/editor.rs b/abacus-ui/src/editor.rs index 5c35288..92d6ba7 100644 --- a/abacus-ui/src/editor.rs +++ b/abacus-ui/src/editor.rs @@ -1,8 +1,12 @@ use std::ops::Range; +use abacus_core::Output; +use clipboard::ClipboardProvider; use druid::{ piet::{CairoTextLayout, Text, TextAttribute, TextLayout, TextLayoutBuilder}, - Color, Data, Event, FontFamily, Lens, LifeCycle, PaintCtx, Rect, RenderContext, Widget, + widget::{Container, Flex, Label, Padding, Svg, SvgData}, + Color, Data, Event, FontDescriptor, FontFamily, FontWeight, Lens, LifeCycle, PaintCtx, Rect, + RenderContext, Target, Widget, WidgetExt, }; use ropey::Rope; @@ -10,14 +14,17 @@ use syntect::easy::HighlightLines; use syntect::highlighting::{Style, ThemeSet}; use syntect::parsing::SyntaxSet; -#[derive(Clone, Data, PartialEq, Eq)] +use crate::{app_header::ToolbarButtonController, Block}; + +const FONT_SIZE: f64 = 16.0; + +#[derive(Clone, Data, PartialEq, Eq, Debug)] pub enum EditMode { Normal, Insert, - Visual, } -#[derive(Data, Lens, Clone)] +#[derive(Data, Lens, Clone, PartialEq, Debug)] pub struct EditorData { #[data(same_fn = "PartialEq::eq")] pub content: Rope, @@ -35,16 +42,27 @@ impl Default for EditorData { cursor_pos: 5, selection_pos: 5, mode: EditMode::Normal, - cursor_opactiy: 0.1, + cursor_opactiy: 255.0, cursor_fade: 1.0, } } } impl EditorData { + pub fn new(content: &str) -> Self { + println!("new block: {}", content); + Self { + content: Rope::from_str(content), + ..Default::default() + } + } + pub fn move_cursor(&mut self, idx: usize) { - self.cursor_pos = idx; - self.deselect(); + if idx <= self.content.len_chars() { + self.cursor_pos = idx; + self.deselect(); + // dbg!(self.content.char(self.cursor_pos)); + } } pub fn select_range(&self) -> Range { @@ -78,6 +96,19 @@ impl EditorData { self.selection_pos = self.cursor_pos; } + pub fn delete_char_forward(&mut self) { + if !self.select_range().is_empty() { + let range = self.select_range(); + self.content.remove(self.select_range()); + self.move_cursor(range.start); + return; + } + + if self.cursor_pos < self.content.len_chars() { + self.content.remove((self.cursor_pos)..self.cursor_pos + 1); + } + } + pub fn delete_char_back(&mut self) { // Delete selection if !self.select_range().is_empty() { @@ -108,7 +139,6 @@ impl EditorData { let line_pos = self.cursor_pos - start_of_current_line; let up_line_start = self.content.line_to_char(line_idx - 1); let up_line = self.content.line(line_idx - 1); - dbg!(line_pos.min(up_line.len_chars())); self.move_cursor(up_line_start + line_pos.min(up_line.len_chars() - 1)); } } @@ -131,6 +161,17 @@ impl EditorData { } } + pub fn select_to_end_of_line(&mut self) { + let line_idx = self.content.char_to_line(self.cursor_pos); + let start_of_line = self.content.line_to_char(line_idx); + let line = self.content.line(line_idx); + if line_idx == self.content.len_lines() - 1 { + self.selection_pos = start_of_line + line.len_chars(); + } else { + self.selection_pos = start_of_line + line.len_chars() - 1; + } + } + pub fn cursor_to_end_of_line(&mut self) { let line_idx = self.content.char_to_line(self.cursor_pos); let start_of_line = self.content.line_to_char(line_idx); @@ -142,6 +183,14 @@ impl EditorData { } } + pub fn select_to_start_of_line(&mut self) { + let start_of_line = self + .content + .line_to_char(self.content.char_to_line(self.cursor_pos)); + + self.selection_pos = start_of_line; + } + pub fn cursor_to_start_of_line(&mut self) { let start_of_line = self .content @@ -157,23 +206,52 @@ impl EditorData { pub fn select_left(&mut self) { if self.cursor_pos > 0 { - self.selection_pos = self.cursor_pos - 1; + self.selection_pos -= 1; } } pub fn select_right(&mut self) { if self.cursor_pos < self.content.len_chars() { - self.selection_pos = self.cursor_pos + 1; + self.selection_pos += 1; } } } -pub struct AbacusEditor; +pub struct AbacusEditor { + syntax_set: SyntaxSet, + theme_set: ThemeSet, +} + +impl Default for AbacusEditor { + fn default() -> Self { + let syntax_set = SyntaxSet::load_defaults_newlines(); + let theme_set = ThemeSet::load_defaults(); + + Self { + syntax_set, + theme_set, + } + } +} impl AbacusEditor { fn paint_cursor(&self, ctx: &mut PaintCtx, data: &EditorData, layout: &CairoTextLayout) { + dbg!(data.cursor_pos); if data.mode == EditMode::Insert { - if let Some(char_rect) = layout + if data.cursor_pos == 0 { + let rects = layout.rects_for_range(0..1); + let rect = rects.first().unwrap(); + let rect = Rect::new( + rect.min_x() + 1.0, + rect.min_y() + 2.0, + rect.min_x() + 3.0, + rect.max_y() - 2.0, + ); + ctx.fill( + rect, + &Color::rgba8(255, 255, 255, data.cursor_opactiy.floor() as u8), + ); + } else if let Some(char_rect) = layout .rects_for_range((data.cursor_pos.max(1) - 1)..data.cursor_pos) .last() { @@ -192,10 +270,8 @@ impl AbacusEditor { } if data.mode == EditMode::Normal { - if let Some(char_rect) = layout - .rects_for_range(data.cursor_pos.max(1)..data.cursor_pos + 1) - .last() - { + let c_pos = data.cursor_pos.min(data.content.len_chars() - 1); + if let Some(char_rect) = layout.rects_for_range(c_pos..(c_pos + 1)).last() { if char_rect.width() == 0. { let rect = Rect::new( char_rect.min_x(), @@ -222,20 +298,18 @@ impl AbacusEditor { ctx: &mut PaintCtx, data: &EditorData, ) -> CairoTextLayout { - let ps = SyntaxSet::load_defaults_newlines(); - let ts = ThemeSet::load_defaults(); - let syntax = ps.find_syntax_by_extension("rs").unwrap(); - let mut h = HighlightLines::new(syntax, &ts.themes["base16-mocha.dark"]); + let syntax = self.syntax_set.find_syntax_by_extension("rs").unwrap(); + let mut h = HighlightLines::new(syntax, &self.theme_set.themes["base16-mocha.dark"]); let mut layout = ctx .text() .new_text_layout(data.content.to_string()) - .font(FontFamily::MONOSPACE, 22.0); + .font(FontFamily::MONOSPACE, FONT_SIZE); let mut pos = 0; for line in data.content.lines() { let s = line.to_string(); - let ranges: Vec<(Style, &str)> = h.highlight_line(&s, &ps).unwrap(); + let ranges: Vec<(Style, &str)> = h.highlight_line(&s, &self.syntax_set).unwrap(); for (style, txt) in ranges { layout = layout.range_attribute( @@ -257,21 +331,24 @@ impl AbacusEditor { impl Widget for AbacusEditor { fn paint(&mut self, ctx: &mut druid::PaintCtx, data: &EditorData, _env: &druid::Env) { - let size = ctx.size(); - let rect = size.to_rect(); + println!("test"); + // let size = ctx.size(); + // let rect = size.to_rect(); - if ctx.is_focused() { - ctx.fill(rect, &Color::rgb8(10, 10, 10)); - } else { - ctx.fill(rect, &Color::rgb8(20, 20, 20)); - } + // if ctx.is_focused() { + // ctx.fill(rect, &Color::rgb8(30, 30, 30)); + // } else { + // ctx.fill(rect, &Color::rgb8(20, 20, 20)); + // } if data.content.len_chars() == 0 { return; } let layout = self.build_highlighted_layout(ctx, data); - self.paint_cursor(ctx, data, &layout); + if ctx.has_focus() { + self.paint_cursor(ctx, data, &layout); + } if data.selection_pos != data.cursor_pos { let rects = layout.rects_for_range(data.select_range()); @@ -291,139 +368,189 @@ impl Widget for AbacusEditor { _env: &druid::Env, ) { match event { - Event::KeyDown(e) => match &e.key { - druid::keyboard_types::Key::Character(ch) - if data.mode == EditMode::Insert && e.mods.ctrl() => - { - match ch.as_ref() { - "a" => { - if e.mods.ctrl() { - data.select_all(); + Event::KeyDown(e) => { + match &e.key { + druid::keyboard_types::Key::Character(ch) if e.mods.ctrl() => { + match ch.as_ref() { + "a" => { + if e.mods.ctrl() { + data.select_all(); + } } - } - "u" => {} - _ => {} - } - } - druid::keyboard_types::Key::Character(c) if data.mode == EditMode::Insert => { - data.push_str(c); - ctx.request_paint(); - } - druid::keyboard_types::Key::Character(ch) if data.mode == EditMode::Normal => { - match ch.as_ref() { - "i" => { - data.mode = EditMode::Insert; - } - "A" => { - data.mode = EditMode::Insert; - data.cursor_to_end_of_line(); - } - "a" => { - if e.mods.ctrl() { - data.select_all(); + "c" => { + if data.cursor_pos != data.selection_pos { + let mut cb: clipboard::ClipboardContext = + clipboard::ClipboardProvider::new().unwrap(); + if let Some(slice) = data.content.get_slice(data.select_range()) + { + let _ = cb.set_contents(slice.to_string()); + } + } } + "v" => { + let mut cb: clipboard::ClipboardContext = + clipboard::ClipboardProvider::new().unwrap(); + data.push_str(&cb.get_contents().unwrap_or_default()); + ctx.request_paint(); + ctx.request_layout(); + } + _ => {} } - "h" => { - data.cursor_left(); - } - "j" => { - data.cursor_down(); - } - "k" => { - data.cursor_up(); - } - "l" => { - data.cursor_right(); - } - _ => {} } - } - druid::keyboard_types::Key::Enter => { - data.push('\n'); - ctx.request_paint(); - ctx.request_layout(); - } - druid::keyboard_types::Key::Backspace => { - data.delete_char_back(); - ctx.request_paint(); - } - druid::keyboard_types::Key::Escape => { - data.mode = EditMode::Normal; - } + druid::keyboard_types::Key::Character(c) if data.mode == EditMode::Insert => { + data.push_str(c); + ctx.request_paint(); + } + druid::keyboard_types::Key::Character(ch) if data.mode == EditMode::Normal => { + match ch.as_ref() { + "i" => { + data.mode = EditMode::Insert; + } + "O" => { + data.mode = EditMode::Insert; + data.cursor_to_start_of_line(); + data.content.insert(data.cursor_pos, "\n"); + } + "A" => { + data.mode = EditMode::Insert; + data.cursor_to_end_of_line(); + } + "a" => { + if e.mods.ctrl() { + data.select_all(); + } + } + "h" => { + data.cursor_left(); + } + "j" => { + data.cursor_down(); + } + "k" => { + data.cursor_up(); + } + "l" => { + data.cursor_right(); + } + "x" => { + data.delete_char_forward(); + } + _ => {} + } + } + druid::keyboard_types::Key::Enter if e.mods.ctrl() => { + ctx.submit_command(crate::commands::PROCESS_WORKBOOK.to(Target::Global)); + } + druid::keyboard_types::Key::Enter => { + data.push('\n'); + ctx.request_layout(); + ctx.request_paint(); + } + druid::keyboard_types::Key::Backspace => { + data.delete_char_back(); + ctx.request_layout(); + } + druid::keyboard_types::Key::Delete => { + data.delete_char_forward(); + ctx.request_layout(); + } + druid::keyboard_types::Key::Escape => { + data.mode = EditMode::Normal; + } - druid::keyboard_types::Key::ArrowLeft if e.mods.shift() => { - data.select_left(); + druid::keyboard_types::Key::ArrowLeft if e.mods.shift() => { + data.select_left(); + } + druid::keyboard_types::Key::ArrowRight if e.mods.shift() => { + data.select_right(); + } + druid::keyboard_types::Key::ArrowLeft if !e.mods.shift() => { + data.cursor_left(); + } + druid::keyboard_types::Key::ArrowRight if !e.mods.shift() => { + data.cursor_right(); + } + druid::keyboard_types::Key::ArrowUp if !e.mods.shift() => { + data.cursor_up(); + } + druid::keyboard_types::Key::ArrowDown if !e.mods.shift() => { + data.cursor_down(); + } + druid::keyboard_types::Key::Tab => { + data.push_str(" "); + } + druid::keyboard_types::Key::End if e.mods.shift() => { + data.select_to_end_of_line(); + } + druid::keyboard_types::Key::Home if e.mods.shift() => { + data.select_to_start_of_line(); + } + druid::keyboard_types::Key::End => { + data.cursor_to_end_of_line(); + data.deselect(); + } + druid::keyboard_types::Key::Home => { + data.cursor_to_start_of_line(); + data.deselect(); + } + e => { + dbg!(e); + } } - druid::keyboard_types::Key::ArrowLeft if !e.mods.shift() => { - data.cursor_left(); - } - druid::keyboard_types::Key::ArrowRight if !e.mods.shift() => { - data.cursor_right(); - } - druid::keyboard_types::Key::ArrowUp if !e.mods.shift() => { - data.cursor_up(); - } - druid::keyboard_types::Key::ArrowDown if !e.mods.shift() => { - data.cursor_down(); - } - druid::keyboard_types::Key::Tab => { - data.push_str(" "); - ctx.request_paint(); - } - druid::keyboard_types::Key::End => { - data.cursor_to_end_of_line(); - data.deselect(); - } - druid::keyboard_types::Key::Home => { - data.cursor_to_start_of_line(); - data.deselect(); - } - e => { - dbg!(e); - } - }, + ctx.request_paint(); + } Event::MouseDown(e) => { if !ctx.is_focused() { ctx.request_focus(); } - let layout = ctx .text() .new_text_layout(data.content.to_string()) - .font(FontFamily::MONOSPACE, 24.0) - .text_color(Color::rgb8(255, 255, 255)) + .font(FontFamily::MONOSPACE, FONT_SIZE) .build() .unwrap(); - let pos = layout.hit_test_point(e.pos); - data.cursor_pos = pos.idx; - data.selection_pos = pos.idx; + if pos.idx != data.cursor_pos { + data.cursor_pos = pos.idx; + data.deselect(); + ctx.request_paint(); + } } Event::MouseMove(e) => { let layout = ctx .text() .new_text_layout(data.content.to_string()) - .font(FontFamily::MONOSPACE, 24.0) + .font(FontFamily::MONOSPACE, FONT_SIZE) .text_color(Color::rgb8(255, 255, 255)) .build() .unwrap(); let pos = layout.hit_test_point(e.pos); if e.buttons.has_left() { - data.selection_pos = pos.idx; + let new_pos = (pos.idx + 1).min(data.content.len_chars()); + if new_pos != data.selection_pos { + if new_pos > data.cursor_pos { + data.selection_pos = (pos.idx + 1).min(data.content.len_chars()); + } else { + data.selection_pos = pos.idx.max(1) - 1; + } + ctx.request_paint(); + } } } - Event::AnimFrame(e) => { - data.cursor_opactiy += ((*e as f64) * 0.00000065) * data.cursor_fade; - if data.cursor_opactiy >= 255.0 { - data.cursor_opactiy = 255.0; - data.cursor_fade *= -1.0; - } else if data.cursor_opactiy <= 0.0 { - data.cursor_opactiy = 0.1; - data.cursor_fade *= -1.0; - } - ctx.request_paint(); - ctx.request_anim_frame(); - } + // Event::AnimFrame(e) => { + // data.cursor_opactiy += ((*e as f64) * 0.00000065) * data.cursor_fade; + // if data.cursor_opactiy >= 255.0 { + // data.cursor_opactiy = 255.0; + // data.cursor_fade *= -1.0; + // } else if data.cursor_opactiy <= 0.0 { + // data.cursor_opactiy = 0.1; + // data.cursor_fade *= -1.0; + // } + // if ctx.has_focus() { + // ctx.request_paint(); + // ctx.request_anim_frame(); + // } + // } _ => {} } } @@ -438,7 +565,6 @@ impl Widget for AbacusEditor { match event { LifeCycle::FocusChanged(_) => { ctx.request_paint(); - ctx.request_anim_frame(); } LifeCycle::WidgetAdded => { // ctx.register_text_input(document) @@ -452,11 +578,15 @@ impl Widget for AbacusEditor { fn update( &mut self, - _ctx: &mut druid::UpdateCtx, - _old_data: &EditorData, - _data: &EditorData, + ctx: &mut druid::UpdateCtx, + old_data: &EditorData, + data: &EditorData, _env: &druid::Env, ) { + if old_data != data { + ctx.request_paint(); + ctx.request_layout(); + } } fn layout( @@ -469,11 +599,69 @@ impl Widget for AbacusEditor { let layout = ctx .text() .new_text_layout(data.content.to_string()) - .font(FontFamily::MONOSPACE, 22.0) + .font(FontFamily::MONOSPACE, FONT_SIZE) .build() .unwrap(); - - bc.shrink_max_height_to(layout.size().height); - bc.max() + (bc.max().width, layout.size().height).into() } } + +pub fn editor_header() -> impl Widget { + let ban_svg = include_str!("../assets/ban.svg") + .parse::() + .unwrap_or_default(); + let trash_svg = include_str!("../assets/trash.svg") + .parse::() + .unwrap_or_default(); + let run_svg = include_str!("../assets/play.svg") + .parse::() + .unwrap_or_default(); + + Container::new( + Flex::row() + .must_fill_main_axis(true) + .with_spacer(20.0) + .with_child( + Label::dynamic(|data: &Block, _| data.name.clone()) + .with_font( + FontDescriptor::new(FontFamily::SANS_SERIF) + .with_weight(FontWeight::BOLD) + .with_size(14.0), + ) + .padding(5.0), + ) + .with_flex_spacer(1.0) + .with_child( + Container::new(Padding::new(10.0, Svg::new(run_svg).fix_width(10.0))) + .controller(ToolbarButtonController::new(Color::rgb8(50, 50, 50))) + .on_click(|ctx, data: &mut Block, _env| { + ctx.submit_command( + crate::commands::PROCESS_BLOCK + .with(data.index) + .to(Target::Global), + ); + }), + ) + .with_spacer(10.0) + .with_child( + Container::new(Padding::new(10.0, Svg::new(ban_svg).fix_width(10.0))) + .controller(ToolbarButtonController::new(Color::rgb8(50, 50, 50))) + .on_click(|_ctx, data: &mut Block, _env| data.output = Output::None), + ) + .with_spacer(10.0) + .with_child( + Container::new(Padding::new(10.0, Svg::new(trash_svg).fix_width(10.0))) + .controller(ToolbarButtonController::new(Color::rgb8(50, 50, 50))) + .on_click(|ctx, data: &mut Block, _env| { + ctx.submit_command( + crate::commands::DELETE_BLOCK + .with(data.index) + .to(Target::Global), + ); + }), + ) + .with_spacer(20.0), + ) + .background(Color::rgb8(35, 35, 35)) + .fix_height(30.0) +} diff --git a/abacus-ui/src/main.rs b/abacus-ui/src/main.rs index 75af26e..8d78d41 100644 --- a/abacus-ui/src/main.rs +++ b/abacus-ui/src/main.rs @@ -1,23 +1,43 @@ +mod app_delegate; +mod app_header; +mod commands; +mod data; mod editor; +mod output_block; -use druid::widget::{Flex, Padding}; -use druid::{AppLauncher, Data, PlatformError, Widget, WindowDesc}; +use data::{AppData, Block}; +use druid::widget::{Container, Flex, List, Padding, Scroll}; +use druid::{AppLauncher, PlatformError, Widget, WidgetExt, WindowDesc}; -fn build_ui() -> impl Widget { - Padding::new( - 10.0, - Flex::column() - .with_flex_child(editor::AbacusEditor, 2.0) - .with_flex_child(editor::AbacusEditor, 1.0), - ) +fn build_ui() -> impl Widget { + Flex::column() + .with_child(app_header::app_header_ui()) + .with_child(app_header::header_separater()) + .with_flex_child( + Scroll::new( + List::new(|| { + Flex::column() + .with_child(editor::editor_header()) + .with_child(Container::new(Padding::new( + 10.0, + editor::AbacusEditor::default().lens(Block::editor_data), + ))) + .with_child(output_block::output_block()) + }) + .lens(AppData::blocks), + ) + .vertical(), + 1.0, + ) } fn main() -> Result<(), PlatformError> { - AppLauncher::with_window(WindowDesc::new(build_ui())).launch(editor::EditorData::default())?; + AppLauncher::with_window( + WindowDesc::new(build_ui()) + .resizable(true) + .window_size((600.0, 800.0)), + ) + .delegate(app_delegate::Delegate) + .launch(AppData::default())?; Ok(()) } - -#[derive(Clone, Data)] -struct CodeEditor { - code: String, -} diff --git a/abacus-ui/src/output_block.rs b/abacus-ui/src/output_block.rs new file mode 100644 index 0000000..2865d77 --- /dev/null +++ b/abacus-ui/src/output_block.rs @@ -0,0 +1,76 @@ +use abacus_core::Output; +use druid::{ + widget::{Flex, Label, Padding, ViewSwitcher}, + Color, FontDescriptor, FontFamily, FontWeight, Widget, WidgetExt, +}; + +use crate::data::Block; + +const OUTPUT_FONT_SIZE: f64 = 16.0; + +pub fn output_block() -> impl Widget { + ViewSwitcher::new( + |data: &Block, _env| data.clone(), + |selector: &Block, _data, _env| match &selector.output { + Output::Scalar(v) => Box::new(Padding::new( + 25.0, + Label::new(v.to_string()) + .with_font( + FontDescriptor::new(FontFamily::MONOSPACE).with_weight(FontWeight::BOLD), + ) + .with_text_size(OUTPUT_FONT_SIZE) + .padding(10.0) + .background(Color::rgb8(30, 30, 30)) + .rounded(4.0) + .expand_width(), + )), + Output::Error(e) => Box::new(Padding::new( + 25.0, + Label::new(e.to_string()) + .with_font( + FontDescriptor::new(FontFamily::MONOSPACE).with_weight(FontWeight::BOLD), + ) + .with_text_alignment(druid::TextAlignment::Center) + .with_line_break_mode(druid::widget::LineBreaking::WordWrap) + .with_text_size(OUTPUT_FONT_SIZE) + .padding(10.0) + .background(Color::rgb8(30, 10, 10)) + .expand_width(), + )), + Output::Series(series) => { + let mut flex = Flex::row(); + + for v in series.iter() { + flex.add_child( + Label::new(v.to_string()) + .with_font( + FontDescriptor::new(FontFamily::MONOSPACE) + .with_weight(FontWeight::BOLD), + ) + .with_text_size(OUTPUT_FONT_SIZE) + .padding(3.0) + .border(Color::rgb8(60, 60, 60), 1.0), + ); + } + + Box::new(Padding::new( + 25.0, + flex.padding(10.0) + .background(Color::rgb8(30, 30, 30)) + .rounded(4.0) + .expand_width(), + )) + } + _ => Box::new(Padding::new( + 0.0, + Label::new("") + .with_font( + FontDescriptor::new(FontFamily::MONOSPACE).with_weight(FontWeight::BOLD), + ) + .with_text_size(0.0) + .padding(0.0) + .background(Color::TRANSPARENT), + )), + }, + ) +} diff --git a/abacus-ui/src/toolbar_button.rs b/abacus-ui/src/toolbar_button.rs new file mode 100644 index 0000000..faec7da --- /dev/null +++ b/abacus-ui/src/toolbar_button.rs @@ -0,0 +1,68 @@ +use druid::{ + kurbo::Line, + widget::{FillStrat, SvgData}, + Color, Data, Rect, RenderContext, Widget, +}; + +pub struct ToolbarItem { + icon: SvgData, +} + +impl Widget for ToolbarItem { + fn event( + &mut self, + _ctx: &mut druid::EventCtx, + _event: &druid::Event, + _data: &mut T, + _env: &druid::Env, + ) { + } + + fn lifecycle( + &mut self, + ctx: &mut druid::LifeCycleCtx, + event: &druid::LifeCycle, + _data: &T, + _env: &druid::Env, + ) { + if let druid::LifeCycle::HotChanged(true) = event { + ctx.request_paint(); + } + } + + fn update(&mut self, _ctx: &mut druid::UpdateCtx, _old_data: &T, _data: &T, _env: &druid::Env) { + } + + fn layout( + &mut self, + _ctx: &mut druid::LayoutCtx, + bc: &druid::BoxConstraints, + _data: &T, + _env: &druid::Env, + ) -> druid::Size { + druid::Size::new(bc.max().height * 1.6, bc.max().height) + } + + fn paint(&mut self, ctx: &mut druid::PaintCtx, _data: &T, _env: &druid::Env) { + let bg_rect = Rect::ZERO.with_size(ctx.size()); + let icon_rect = Rect::from_center_size(bg_rect.center(), (15.0, 15.0)); + let offset_matrix = FillStrat::default().affine_to_fill(icon_rect.size(), self.icon.size()); + + if ctx.is_hot() { + ctx.fill(bg_rect, &Color::rgba8(50, 50, 50, 255)); + ctx.stroke( + Line::new( + (0.0, bg_rect.height() - 2.0), + (bg_rect.width(), bg_rect.height() - 2.0), + ), + &Color::rgb8(100, 100, 100), + 2.0, + ); + } + + ctx.with_child_ctx(icon_rect, |ctx| { + ctx.transform(druid::Affine::translate((icon_rect.x0, icon_rect.y0))); + self.icon.to_piet(offset_matrix, ctx); + }); + } +}