Foundational UI (#1)

ore Foundational Commit

Implmeneted basic application functionality:

    Editor
    Output
    Engine integration
    Scripting
    Save/Open Dialogs
    Multiple Block Management
    Keybinds
    Modal Editing
    Block Renaming
    File Serialization/Deserialization
This commit is contained in:
Joe Bellus 2022-10-16 18:14:55 +00:00
parent 8a9f3d82a7
commit 5957726421
32 changed files with 3126 additions and 161 deletions

321
Cargo.lock generated
View File

@ -9,6 +9,7 @@ dependencies = [
"polars",
"rhai",
"serde",
"serde_json",
"syntect",
"tracing-subscriber",
]
@ -18,7 +19,10 @@ name = "abacus-ui"
version = "0.1.0"
dependencies = [
"abacus-core",
"clipboard",
"druid",
"ropey",
"syntect",
]
[[package]]
@ -158,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"
@ -250,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"
@ -442,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"
@ -474,12 +518,14 @@ dependencies = [
"fluent-langneg",
"fluent-syntax",
"fnv",
"im",
"instant",
"tracing",
"tracing-subscriber",
"tracing-wasm",
"unic-langid",
"unicode-segmentation",
"usvg",
"xi-unicode",
]
@ -576,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"
@ -616,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"
@ -951,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"
@ -1181,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"
@ -1355,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"
@ -1474,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"
@ -1657,7 +1769,7 @@ dependencies = [
"lexical",
"lexical-core",
"memchr",
"memmap2",
"memmap2 0.5.7",
"num",
"once_cell",
"polars-arrow",
@ -1818,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"
@ -1842,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"
@ -1905,6 +2032,25 @@ dependencies = [
"syn",
]
[[package]]
name = "ropey"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd22239fafefc42138ca5da064f3c17726a80d2379d817a3521240e78dd0064"
dependencies = [
"smallvec",
"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"
@ -1926,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"
@ -1999,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",
@ -2053,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"
@ -2085,6 +2272,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "str_indices"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d9199fa80c817e074620be84374a520062ebac833f358d74b37060ce4a0f2c0"
[[package]]
name = "strength_reduce"
version = "0.2.3"
@ -2128,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"
@ -2315,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"
@ -2324,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"
@ -2399,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"
@ -2611,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"
@ -2623,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"

View File

@ -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"

View File

@ -6,7 +6,7 @@ use std::{
str::FromStr,
};
use rhai::EvalAltResult;
use rhai::{Dynamic, EvalAltResult, EvalContext, Expression, Position};
pub fn setup_engine(engine: &mut rhai::Engine) {
//polar data frame
@ -14,20 +14,36 @@ pub fn setup_engine(engine: &mut rhai::Engine) {
engine.register_fn("dataframe", script_functions::dataframe);
engine.register_fn("select", DataFrame::s_select);
engine.register_fn("load_csv", DataFrame::load_csv);
engine.register_fn("column", DataFrame::s_column);
engine.register_indexer_get(DataFrame::s_column);
engine.register_indexer_set(DataFrame::append_column);
// polar series
engine.register_type::<Series>();
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);
engine.register_fn("sum", Series::s_sum);
engine.register_fn("add", Series::s_op_add);
engine.register_fn("column", DataFrame::s_column);
engine.register_fn("+", Series::add);
engine.register_fn("+", script_functions::add_series_i64);
engine.register_fn("+", script_functions::add_series_f64);
engine.register_fn("-", script_functions::subtract_series_series);
engine.register_fn("-", script_functions::subtract_series_i64);
engine.register_fn("-", script_functions::subtract_series_f64);
engine.register_fn("*", script_functions::multiply_series_series);
engine.register_fn("*", script_functions::multiply_series_i64);
engine.register_fn("*", script_functions::multiply_series_f64);
engine.register_fn("/", script_functions::div_series_series);
engine.register_fn("/", script_functions::div_series_i64);
engine.register_fn("/", script_functions::div_series_f64);
// polar expressions
engine.register_type::<DataFrameExpression>();
engine.register_fn("sum", DataFrameExpression::sum);
@ -49,9 +65,22 @@ pub fn setup_engine(engine: &mut rhai::Engine) {
engine.register_fn("min", script_functions::min);
engine.register_fn("max", script_functions::max);
engine.register_fn("first", script_functions::first);
let _ = engine.register_custom_operator("gt", 200);
let _ = engine.register_custom_operator("gte", 200);
let _ = engine.register_custom_operator("<<", 200);
engine.register_fn("gt", script_functions::gt_op);
engine.register_fn("gte", script_functions::gte_op);
engine
.register_custom_syntax(
["from", "$ident$", "$expr$", ":", "$expr$"], // the custom syntax
true, // variables declared within this custom syntax
implementation_df_select,
)
.unwrap();
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq)]
pub struct DataFrame(polars::frame::DataFrame);
impl Deref for DataFrame {
@ -72,6 +101,8 @@ impl DataFrame {
pub fn load_csv(path: &str) -> ScriptResult<DataFrame> {
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())?;
@ -93,6 +124,12 @@ impl DataFrame {
.map_err(|e| e.to_string())?,
))
}
pub fn append_column(&mut self, idx: &str, mut b: Series) -> ScriptResult<()> {
b.0.rename(idx);
self.0.with_column(b.0).map_err(|e| e.to_string())?;
Ok(())
}
}
#[derive(Clone, Debug)]
@ -158,7 +195,7 @@ impl From<rhai::Dynamic> for DataFrameExpression {
}
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq)]
pub struct Series(polars::series::Series);
impl Deref for Series {
@ -242,6 +279,34 @@ mod script_functions {
Series(a.0 - b)
}
pub fn subtract_series_series(a: Series, b: Series) -> Series {
Series(a.0 - b.0)
}
pub fn multiply_series_i64(a: Series, b: i64) -> Series {
Series(a.0 * b)
}
pub fn multiply_series_f64(a: Series, b: f64) -> Series {
Series(a.0 * b)
}
pub fn multiply_series_series(a: Series, b: Series) -> Series {
Series(a.0 * b.0)
}
pub fn div_series_i64(a: Series, b: i64) -> Series {
Series(a.0 / b)
}
pub fn div_series_f64(a: Series, b: f64) -> Series {
Series(a.0 / b)
}
pub fn div_series_series(a: Series, b: Series) -> Series {
Series(a.0 / b.0)
}
pub fn column(name: &str) -> DataFrameExpression {
DataFrameExpression(polars::prelude::col(name))
}
@ -288,27 +353,42 @@ mod script_functions {
}
}
pub fn series_unnamed(arr: rhai::Array) -> std::result::Result<Series, Box<EvalAltResult>> {
series("Unnamed", arr)
}
pub fn to_series_unnamed(arr: rhai::Array) -> std::result::Result<Series, Box<EvalAltResult>> {
series("Unnamed", arr)
}
pub fn to_series(
arr: rhai::Array,
name: &str,
) -> std::result::Result<Series, Box<EvalAltResult>> {
series(name, arr)
}
pub fn series(name: &str, arr: rhai::Array) -> std::result::Result<Series, Box<EvalAltResult>> {
if let Some(i) = arr.first() {
let series = if i.type_id() == TypeId::of::<i64>() {
polars::series::Series::new(
name,
arr.into_iter()
.map(|i| i.cast::<i64>())
.filter_map(|i| i.try_cast::<i64>())
.collect::<Vec<i64>>(),
)
} else if i.type_id() == TypeId::of::<f64>() {
polars::series::Series::new(
name,
arr.into_iter()
.map(|i| i.cast::<f64>())
.filter_map(|i| i.try_cast::<f64>())
.collect::<Vec<f64>>(),
)
} else {
polars::series::Series::new(
name,
arr.into_iter()
.map(|i| i.cast::<String>())
.filter_map(|i| i.try_cast::<String>())
.collect::<Vec<String>>(),
)
};
@ -319,6 +399,90 @@ mod script_functions {
Ok(Series(s))
}
}
pub fn gt_op(a: &str, b: rhai::Dynamic) -> DataFrameExpression {
DataFrameExpression(polars::prelude::col(a).gt(DataFrameExpression::from(b)))
}
pub fn gte_op(a: &str, b: rhai::Dynamic) -> DataFrameExpression {
DataFrameExpression(polars::prelude::col(a).gt_eq(DataFrameExpression::from(b)))
}
}
fn implementation_df_select(
context: &mut EvalContext,
inputs: &[Expression],
) -> Result<Dynamic, Box<EvalAltResult>> {
let df_name = inputs[0].get_string_value().ok_or_else(|| {
Box::new(EvalAltResult::ErrorVariableNotFound(
"variable not found".to_string(),
Position::default(),
))
})?;
let df = context
.scope()
.get(df_name)
.ok_or_else(|| {
Box::new(EvalAltResult::ErrorVariableNotFound(
format!("{} not found", df_name),
Position::default(),
))
})?
.clone()
.try_cast::<DataFrame>()
.ok_or_else(|| {
Box::new(EvalAltResult::ErrorVariableNotFound(
format!("{} not found", df_name),
Position::default(),
))
})?;
let raw_filter_array = context.eval_expression_tree(&inputs[2])?;
let filter_array = raw_filter_array
.into_array()
.map_err(|e| {
Box::new(EvalAltResult::ErrorVariableNotFound(
format!("{} value not an array", e),
Position::default(),
))
})?
.into_iter()
.map(|i| i.cast::<DataFrameExpression>())
.collect::<Vec<_>>();
let raw_select_array = context.eval_expression_tree(&inputs[1])?;
let select_array = raw_select_array.into_array().map_err(|e| {
Box::new(EvalAltResult::ErrorVariableNotFound(
format!("{} value not an array", e),
Position::default(),
))
})?;
let select_expressions = select_array
.iter()
.filter_map(|s| {
let select_expr = if s.is::<String>() {
polars::prelude::col(&s.to_string())
} else if s.is::<DataFrameExpression>() {
s.clone().cast::<DataFrameExpression>().0
} else {
return None;
};
Some(
filter_array
.iter()
.fold(select_expr, |acc, i| acc.filter(i.0.clone())),
)
})
.collect::<Vec<_>>();
Ok(Dynamic::from(DataFrame(
df.0.lazy()
.select(&select_expressions)
.collect()
.map_err(|e| e.to_string())?,
)))
}
#[cfg(test)]
@ -474,4 +638,26 @@ s1 + s2
.collect::<Vec<_>>();
assert_eq!(s, vec![Some(18)]);
}
#[test]
pub fn test_dataframe_select_syntax() {
let res = process(
r#"
let data = load_csv("test/data.csv");
from data ["age"] : ["age" gt 18];
"#,
);
dbg!(&res);
let s = res
.into_frame()
.column("age")
.unwrap()
.i64()
.unwrap()
.into_iter()
.collect::<Vec<_>>();
assert_eq!(s, vec![Some(22), Some(32)]);
}
}

View File

@ -1,12 +1,12 @@
use crate::dataframe;
#[derive(Debug)]
pub struct Engine {
pub blocks: Vec<Block>,
pub struct Engine<'a> {
engine: rhai::Engine,
scope: rhai::Scope<'a>,
}
impl Default for Engine {
impl<'a> Default for Engine<'a> {
fn default() -> Self {
let mut engine = rhai::Engine::new();
engine.set_fast_operators(false);
@ -14,30 +14,28 @@ impl Default for Engine {
Self {
engine,
blocks: vec![Block::default()],
scope: rhai::Scope::new(),
}
}
}
impl Engine {
pub fn process(&mut self) {
for block in self.blocks.iter_mut() {
match self.engine.eval::<rhai::Dynamic>(&block.script) {
Ok(res) if res.is::<dataframe::DataFrame>() => {
let frame = rhai::Dynamic::cast::<dataframe::DataFrame>(res);
block.output = Output::DataFrame(frame);
}
Ok(res) if res.is::<dataframe::Series>() => {
let frame = rhai::Dynamic::cast::<dataframe::Series>(res);
block.output = Output::Series(frame);
}
Ok(res) => {
block.output = Output::Scalar(res);
}
Err(e) => {
block.output = Output::Error(e.to_string());
}
impl<'a> Engine<'a> {
pub fn process_script(&mut self, script: &str) -> Output {
match self
.engine
.eval_with_scope::<rhai::Dynamic>(&mut self.scope, script)
{
Ok(res) if res.is::<dataframe::DataFrame>() => {
let frame = rhai::Dynamic::cast::<dataframe::DataFrame>(res);
Output::DataFrame(frame)
}
Ok(res) if res.is::<()>() => Output::None,
Ok(res) if res.is::<dataframe::Series>() => {
let series = rhai::Dynamic::cast::<dataframe::Series>(res);
Output::Series(series)
}
Ok(res) => Output::Scalar(res),
Err(e) => Output::Error(e.to_string()),
}
}
}
@ -51,6 +49,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 {
@ -72,29 +88,12 @@ impl Output {
}
}
#[derive(Debug)]
pub struct Block {
pub script: String,
pub output: Output,
}
impl Default for Block {
fn default() -> Self {
Self {
script: Default::default(),
output: Output::None,
}
}
}
#[cfg(test)]
pub mod tests {
use super::*;
pub fn process(script: &str) -> Output {
let mut engine = Engine::default();
engine.blocks[0].script = script.into();
engine.process();
engine.blocks[0].output.clone()
engine.process_script(script)
}
}

View File

@ -1,7 +1,11 @@
mod dataframe;
mod engine;
mod save_file;
pub use engine::Engine;
pub use engine::Output;
pub use polars::prelude::AnyValue;
pub use save_file::{SaveBlock, SaveFile};
use rhai::EvalAltResult;
type ScriptResult<T> = std::result::Result<T, Box<EvalAltResult>>;

View File

@ -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<S: Display>(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<SaveBlock>,
}
impl SaveFile {
pub fn new(filepath: String, blocks: Vec<SaveBlock>) -> Self {
Self { filepath, blocks }
}
pub fn open(filepath: &str) -> Result<Self, std::io::Error> {
let data = std::fs::read_to_string(filepath)?;
let mut slf = serde_json::from_str::<Self>(&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)
}
}

View File

@ -0,0 +1,4 @@
name,age,
alice,18
sasha,22
lacey,32
1 name,age,
2 alice,18
3 sasha,22
4 lacey,32

View File

@ -6,5 +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" }
abacus-core = { path = "../abacus-core" }
druid = { git = "https://github.com/linebender/druid.git", features=["im", "svg"] }
abacus-core = { path = "../abacus-core" }
syntect = "5.0.0"
ropey = "1.5.0"
clipboard = "0.5.0"

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 576 512"
version="1.1"
id="svg4"
sodipodi:docname="abacus.svg"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8" />
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="2.0058594"
inkscape:cx="288.15579"
inkscape:cy="255.75073"
inkscape:window-width="1942"
inkscape:window-height="1980"
inkscape:window-x="6470"
inkscape:window-y="80"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path
d="M512 32H64c-35.35 0-64 28.65-64 64v320c0 35.35 28.65 64 64 64h448c35.35 0 64-28.65 64-64v-320C576 60.65 547.3 32 512 32zM512 64c17.64 0 32 14.36 32 32v96h-96l0-31.1L464 160C472.8 160 480 152.8 480 144s-7.156-15.1-16-15.1L448 128V64H512zM416 64v64l-16 .0002c-8.844 0-16 7.156-16 15.1S391.2 160 400 160l15.1 .0004L416 192h-128l0-31.1L304 160C312.8 160 320 152.8 320 144s-7.156-15.1-16-15.1L288 128V64H416zM256 64v64L240 128C231.2 128 224 135.2 224 144S231.2 160 240 160l15.1 .0004L256 192H160l0-31.1L176 160C184.8 160 192 152.8 192 144S184.8 128 176 128L160 128V64H256zM32 96c0-17.64 14.36-32 32-32h64v64L112 128C103.2 128 96 135.2 96 144S103.2 160 112 160l15.1 .0004L128 192H32V96zM64 448c-17.64 0-32-14.36-32-32V224h96v64L112 288C103.2 288 96 295.2 96 304S103.2 320 112 320l15.1 .0004v32L112 352C103.2 352 96 359.2 96 368s7.156 16 16 16L128 384v64H64zM160 448v-64l16 .0002c8.844 0 16-7.156 16-16S184.8 352 176 352l-15.1 .0004v-32L176 320C184.8 320 192 312.8 192 304S184.8 288 176 288L160 288V224h96v64L240 288C231.2 288 224 295.2 224 304S231.2 320 240 320l15.1 .0004v32L240 352C231.2 352 224 359.2 224 368s7.156 16 16 16L256 384v64H160zM288 448v-64l16 .0002c8.844 0 16-7.156 16-16S312.8 352 304 352l-15.1 .0004v-32L304 320c8.844 0 16-7.156 16-16s-7.156-15.1-16-15.1L288 288V224h128l0 128L400 352c-8.844 0-16 7.156-16 16s7.156 16 16 16L416 384v64H288zM544 416c0 17.64-14.36 32-32 32h-64v-64l16 .0002c8.844 0 16-7.156 16-16S472.8 352 464 352l-15.1 .0004L448 224h96V416z"
id="path2"
style="fill:#ffffff" />
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

38
abacus-ui/assets/ban.svg Normal file
View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 512 512"
version="1.1"
id="svg4"
sodipodi:docname="ban.svg"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8" />
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="2.0058594"
inkscape:cx="255.75073"
inkscape:cy="255.75073"
inkscape:window-width="1830"
inkscape:window-height="1980"
inkscape:window-x="6470"
inkscape:window-y="80"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path
d="M367.2 412.5L99.5 144.8C77.1 176.1 64 214.5 64 256c0 106 86 192 192 192c41.5 0 79.9-13.1 111.2-35.5zm45.3-45.3C434.9 335.9 448 297.5 448 256c0-106-86-192-192-192c-41.5 0-79.9 13.1-111.2 35.5L412.5 367.2zM512 256c0 141.4-114.6 256-256 256S0 397.4 0 256S114.6 0 256 0S512 114.6 512 256z"
id="path2"
style="fill:#ffffff" />
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 448 512"
version="1.1"
id="svg4"
sodipodi:docname="floppy-disk.svg"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8" />
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="2.0058594"
inkscape:cx="223.84421"
inkscape:cy="256.24927"
inkscape:window-width="1830"
inkscape:window-height="970"
inkscape:window-x="8340"
inkscape:window-y="80"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path
d="M64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H384c35.3 0 64-28.7 64-64V173.3c0-17-6.7-33.3-18.7-45.3L352 50.7C340 38.7 323.7 32 306.7 32H64zm0 96c0-17.7 14.3-32 32-32H288c17.7 0 32 14.3 32 32v64c0 17.7-14.3 32-32 32H96c-17.7 0-32-14.3-32-32V128zM224 416c-35.3 0-64-28.7-64-64s28.7-64 64-64s64 28.7 64 64s-28.7 64-64 64z"
id="path2"
style="fill:#ffffff" />
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 576 512"
version="1.1"
id="svg4"
sodipodi:docname="folder-open.svg"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8" />
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="2.0058594"
inkscape:cx="288.15579"
inkscape:cy="256.24927"
inkscape:window-width="1830"
inkscape:window-height="970"
inkscape:window-x="8340"
inkscape:window-y="80"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path
d="M88.7 223.8L0 375.8V96C0 60.7 28.7 32 64 32H181.5c17 0 33.3 6.7 45.3 18.7l26.5 26.5c12 12 28.3 18.7 45.3 18.7H416c35.3 0 64 28.7 64 64v32H144c-22.8 0-43.8 12.1-55.3 31.8zm27.6 16.1C122.1 230 132.6 224 144 224H544c11.5 0 22 6.1 27.7 16.1s5.7 22.2-.1 32.1l-112 192C453.9 474 443.4 480 432 480H32c-11.5 0-22-6.1-27.7-16.1s-5.7-22.2 .1-32.1l112-192z"
id="path2"
style="fill:#ffffff" />
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

38
abacus-ui/assets/play.svg Normal file
View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 384 512"
version="1.1"
id="svg4"
sodipodi:docname="play.svg"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8" />
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="2.0058594"
inkscape:cx="191.93768"
inkscape:cy="256.24927"
inkscape:window-width="1830"
inkscape:window-height="1980"
inkscape:window-x="6470"
inkscape:window-y="80"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path
d="M73 39c-14.8-9.1-33.4-9.4-48.5-.9S0 62.6 0 80V432c0 17.4 9.4 33.4 24.5 41.9s33.7 8.1 48.5-.9L361 297c14.3-8.7 23-24.2 23-41s-8.7-32.2-23-41L73 39z"
id="path2"
style="fill:#ffffff" />
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

38
abacus-ui/assets/plus.svg Normal file
View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 448 512"
version="1.1"
id="svg4"
sodipodi:docname="plus.svg"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8" />
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="2.0058594"
inkscape:cx="223.84421"
inkscape:cy="256.49854"
inkscape:window-width="1830"
inkscape:window-height="633"
inkscape:window-x="8340"
inkscape:window-y="753"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path
d="M256 80c0-17.7-14.3-32-32-32s-32 14.3-32 32V224H48c-17.7 0-32 14.3-32 32s14.3 32 32 32H192V432c0 17.7 14.3 32 32 32s32-14.3 32-32V288H400c17.7 0 32-14.3 32-32s-14.3-32-32-32H256V80z"
id="path2"
style="fill:#ffffff" />
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 448 512"
version="1.1"
id="svg4"
sodipodi:docname="trash.svg"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8" />
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="2.0058594"
inkscape:cx="223.84421"
inkscape:cy="255.75073"
inkscape:window-width="1830"
inkscape:window-height="1980"
inkscape:window-x="6470"
inkscape:window-y="80"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path
d="M135.2 17.7L128 32H32C14.3 32 0 46.3 0 64S14.3 96 32 96H416c17.7 0 32-14.3 32-32s-14.3-32-32-32H320l-7.2-14.3C307.4 6.8 296.3 0 284.2 0H163.8c-12.1 0-23.2 6.8-28.6 17.7zM416 128H32L53.2 467c1.6 25.3 22.6 45 47.9 45H346.9c25.3 0 46.3-19.7 47.9-45L416 128z"
id="path2"
style="fill:#ffffff" />
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,86 @@
use druid::AppDelegate;
use crate::{
commands,
data::{AppData, Block},
};
pub struct Delegate;
impl AppDelegate<AppData> 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();
data.filename = Some(filepath);
data.save();
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.remove_block(*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<druid::Event> {
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)
}
}

188
abacus-ui/src/app_header.rs Normal file
View File

@ -0,0 +1,188 @@
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<AppData> {
let open_svg = include_str!("../assets/folder-open.svg")
.parse::<SvgData>()
.unwrap_or_default();
let disk_svg = include_str!("../assets/floppy-disk.svg")
.parse::<SvgData>()
.unwrap_or_default();
let run_svg = include_str!("../assets/play.svg")
.parse::<SvgData>()
.unwrap_or_default();
let plus_svg = include_str!("../assets/plus.svg")
.parse::<SvgData>()
.unwrap_or_default();
let abacus_svg = include_str!("../assets/abacus.svg")
.parse::<SvgData>()
.unwrap_or_default();
Container::new(
Flex::row()
.must_fill_main_axis(true)
.with_spacer(10.0)
.with_child(
Container::new(Padding::new(5.0, Svg::new(abacus_svg).fix_width(20.0)))
.controller(ToolbarButtonController::new(Color::rgb8(50, 50, 50)))
.on_click(|_ctx, _, _| {}),
)
.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, data: &mut AppData, _| {
let abacus = druid::FileSpec::new("Abacus File", &["abacus"]);
let json = druid::FileSpec::new("JSON File", &["json"]);
if data.filename.is_some() {
data.save();
} else {
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<AppData> {
Painter::new(|ctx, _data, _env| {
let rect = ctx.size().to_rect();
ctx.fill(rect, &Color::rgb8(0, 0, 0))
})
.fix_height(1.0)
}
pub struct ToolbarButtonController {
color: Color,
}
impl ToolbarButtonController {
pub fn new(color: Color) -> Self {
Self { color }
}
}
impl<T: Data> Controller<T, Container<T>> for ToolbarButtonController {
fn event(
&mut self,
child: &mut Container<T>,
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<T>,
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<T>,
ctx: &mut druid::UpdateCtx,
old_data: &T,
data: &T,
env: &druid::Env,
) {
child.update(ctx, old_data, data, env)
}
}

View File

@ -0,0 +1,9 @@
use druid::Selector;
pub const PROCESS_WORKBOOK: Selector<()> = Selector::new("process-workbook");
pub const PROCESS_BLOCK: Selector<usize> = Selector::new("process-block");
pub const DELETE_BLOCK: Selector<usize> = Selector::new("delete-block");
pub const RENAME_BLOCK: Selector<usize> = Selector::new("rename-block");
pub const CLOSE_MODAL: Selector<()> = Selector::new("close-modal");

View File

@ -0,0 +1,96 @@
use abacus_core::Output;
use druid::{
im::{vector, Vector},
Data, Lens,
};
use super::{EditorData, Modals};
#[derive(Clone, Data, Lens, Debug)]
pub struct AppData {
#[data(same_fn = "PartialEq::eq")]
pub filename: Option<String>,
pub blocks: Vector<Block>,
pub modals: Modals,
}
impl AppData {
pub fn save(&self) {
if let Some(ref filepath) = self.filename {
let _ = abacus_core::SaveFile::new(
filepath.clone(),
self.blocks
.iter()
.map(|d| abacus_core::SaveBlock::new(&d.name, &d.editor_data.content))
.collect(),
)
.save();
}
}
pub fn remove_block(&mut self, idx: usize) {
self.blocks.remove(idx);
for (idx, block) in self.blocks.iter_mut().enumerate() {
block.index = idx;
}
}
}
impl Default for AppData {
fn default() -> Self {
Self {
filename: None,
blocks: vector![Block::new("Block #1", 0)],
modals: Modals::default(),
}
}
}
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: 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: super::EditorData::new(content),
output: Default::default(),
index,
}
}
}

View File

@ -0,0 +1,377 @@
use std::ops::Range;
use druid::{Data, Lens};
use ropey::Rope;
#[derive(Clone, Data, PartialEq, Eq, Debug)]
pub enum EditMode {
Normal,
Insert,
}
#[derive(Data, Lens, Clone, PartialEq, Debug)]
pub struct EditorData {
#[data(same_fn = "PartialEq::eq")]
pub content: Rope,
pub cursor_pos: usize,
pub mode: EditMode,
pub cursor_opactiy: f64,
pub cursor_fade: f64,
pub selection_pos: Option<usize>,
}
impl Default for EditorData {
fn default() -> Self {
Self {
content: Rope::from_str(""),
cursor_pos: 0,
selection_pos: None,
mode: EditMode::Normal,
cursor_opactiy: 255.0,
cursor_fade: 1.0,
}
}
}
impl EditorData {
pub fn new(content: &str) -> Self {
Self {
content: Rope::from_str(content),
..Default::default()
}
}
pub fn move_cursor(&mut self, idx: usize) {
self.cursor_pos = idx;
}
pub fn select_range(&self) -> Range<usize> {
if let Some(selection_pos) = self.selection_pos {
if self.cursor_pos > selection_pos {
selection_pos..self.cursor_pos
} else {
self.cursor_pos..selection_pos
}
} else {
self.cursor_pos..self.cursor_pos
}
}
pub fn push_str(&mut self, s: &str) {
if self.selection_pos.is_some() {
self.content.remove(self.select_range());
self.content.insert(self.select_range().start, s);
} else {
self.content.insert(self.cursor_pos, s);
}
self.move_cursor(self.select_range().start + s.len());
}
pub fn push(&mut self, c: char) {
self.content.insert_char(self.cursor_pos, c);
self.cursor_right();
}
// pub fn cursor_to_end(&mut self) {
// self.move_cursor(self.content.len_chars());
// }
pub fn deselect(&mut self) {
self.selection_pos = None;
}
pub fn delete_current_line(&mut self) {
if self.current_line_index() < self.content.len_lines() {
self.content.remove(
self.current_line_start()
..(self.current_line_start()
+ self
.current_line()
.len_chars()
.min(self.content.len_chars())),
);
} else {
self.cursor_up();
self.content.remove(
self.current_line_start()..(self.cursor_pos + self.current_line().len_chars()),
);
}
if self.cursor_pos > self.content.len_chars() && self.content.len_chars() > 0 {
self.cursor_pos = self.content.len_chars() - 1;
} else if self.cursor_pos > self.content.len_chars() {
self.cursor_pos = 0;
}
self.deselect();
}
pub fn word_scan_forward(&mut self) {
while self.cursor_pos < self.content.len_chars() {
self.cursor_pos += 1;
if !self
.current_char()
.map(|c| c.is_alphanumeric())
.unwrap_or(false)
{
break;
}
self.deselect();
}
}
pub fn word_scan_backward(&mut self) {
if self.cursor_pos > 0 {
self.cursor_pos -= 1;
}
while self.cursor_pos > 0 {
if !self
.content
.get_char(self.cursor_pos - 1)
.map(|c| c.is_alphanumeric())
.unwrap_or(false)
{
break;
} else {
self.cursor_pos -= 1;
}
self.deselect();
}
}
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);
self.deselect();
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() {
let range = self.select_range();
self.content.remove(self.select_range());
self.move_cursor(range.start);
self.deselect();
return;
}
// Cant delete character sif we are at the start of the buffer
if self.cursor_pos > 0 {
self.content.remove((self.cursor_pos - 1)..self.cursor_pos);
self.cursor_left();
}
}
pub fn delete_to_eol(&mut self) {
self.content.remove(
self.cursor_pos
..(self.current_line_start() + self.current_line().to_string().len() - 1),
)
}
pub fn cursor_left(&mut self) {
match self.mode {
EditMode::Insert => {
if self.cursor_pos > 0 {
self.move_cursor(self.cursor_pos - 1);
}
}
EditMode::Normal => {
if self.cursor_pos > self.current_line_start() {
self.move_cursor(self.cursor_pos - 1);
}
}
}
}
pub fn cursor_up(&mut self) {
let line_idx = self.content.char_to_line(self.cursor_pos);
if line_idx > 0 {
let up_line_start = self.content.line_to_char(line_idx - 1);
let up_line = self.content.line(line_idx - 1);
self.move_cursor(up_line_start + self.current_column().min(up_line.len_chars() - 1));
}
}
pub fn select_up(&mut self) {
let line_idx = self.content.char_to_line(self.cursor_pos);
if line_idx > 0 {
let start_of_current_line = self.content.line_to_char(line_idx);
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);
self.cursor_pos = up_line_start + line_pos.min(up_line.len_chars() - 1);
}
}
pub fn cursor_down(&mut self) {
let line_idx = self.content.char_to_line(self.cursor_pos);
if line_idx < self.content.len_lines() - 1 {
let start_of_next_line = self.content.line_to_char(self.current_line_index() + 1);
let next_line_len = self.content.line(line_idx + 1).len_chars().max(1) - 1;
let new_pos = start_of_next_line + self.current_column().min(next_line_len);
self.move_cursor(new_pos);
}
}
pub fn select_down(&mut self) {
//12 -> 11
let line_idx = self.content.char_to_line(self.cursor_pos);
if line_idx < self.content.len_lines() - 1 {
let start_of_current_line = self.content.line_to_char(line_idx);
let line_pos = self.cursor_pos - start_of_current_line;
let start_of_next_line = self.content.line_to_char(line_idx + 1) + 1;
let next_line_len = self.content.line(line_idx + 1).len_chars();
self.cursor_pos =
(start_of_next_line + line_pos.min(next_line_len)).min(start_of_next_line);
}
}
pub fn cursor_right(&mut self) {
if self.cursor_pos < self.content.len_chars() {
self.move_cursor(self.cursor_pos + 1);
}
}
pub fn select_to_end_of_line(&mut self) {
self.selection_pos = Some(self.cursor_pos);
self.cursor_to_end_of_line();
}
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);
let line = self.content.line(line_idx);
if line_idx == self.content.len_lines() - 1 {
self.move_cursor(start_of_line + line.len_chars());
} else {
self.move_cursor(start_of_line + line.len_chars() - 1);
}
}
pub fn select_to_start_of_line(&mut self) {
self.selection_pos = Some(self.cursor_pos);
self.cursor_to_start_of_line();
}
pub fn cursor_to_start_of_line(&mut self) {
let start_of_line = self
.content
.line_to_char(self.content.char_to_line(self.cursor_pos));
self.move_cursor(start_of_line);
}
pub fn normal_mode(&mut self) {
self.mode = EditMode::Normal;
if self.cursor_pos == self.content.len_chars() && self.content.len_chars() != 0 {
self.cursor_pos -= 1;
}
self.deselect();
}
pub fn select_all(&mut self) {
self.selection_pos = Some(0);
self.cursor_pos = self.content.len_chars();
}
pub fn select_left(&mut self) {
if self.cursor_pos > 0 {
self.cursor_pos -= 1
}
}
pub fn select_right(&mut self) {
if self.cursor_pos < self.content.len_chars() {
self.cursor_pos += 1;
}
}
pub fn current_line(&self) -> ropey::RopeSlice {
self.content
.get_slice(
self.current_line_start()
..self
.content
.line_to_char(self.current_line_index() + 1)
.min(self.content.len_chars()),
)
.unwrap()
}
pub fn current_line_index(&self) -> usize {
self.content.char_to_line(self.cursor_pos)
}
pub fn current_line_start(&self) -> usize {
self.content
.line_to_char(self.content.char_to_line(self.cursor_pos))
}
pub fn current_column(&self) -> usize {
self.cursor_pos - self.current_line_start()
}
pub fn current_char(&self) -> Option<char> {
self.content.get_char(self.cursor_pos)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cursor_left_normal_empty_line() {
let mut data = EditorData {
mode: EditMode::Normal,
..Default::default()
};
data.push_str("0123456789\n\n1234");
data.cursor_pos = 12;
data.cursor_left();
assert_eq!(data.cursor_pos, 11);
data.cursor_left();
assert_eq!(data.cursor_pos, 10);
}
#[test]
fn cursor_left_normal_double_empty_line() {
let mut data = EditorData {
mode: EditMode::Normal,
..Default::default()
};
data.push_str("0123456789\n\n\n1234");
data.cursor_pos = 14;
data.cursor_left();
assert_eq!(data.cursor_pos, 12);
data.cursor_left();
assert_eq!(data.cursor_pos, 11);
data.cursor_left();
assert_eq!(data.cursor_pos, 10);
}
#[test]
fn cursor_left_normal_end_of_line() {
let mut data = EditorData {
mode: EditMode::Normal,
..Default::default()
};
data.push_str("0123456789\n1234");
data.cursor_pos = 12;
assert_eq!(data.current_char().unwrap(), '1');
data.cursor_left();
assert_eq!(data.cursor_pos, 10);
}
}

View File

@ -0,0 +1,7 @@
mod app_data;
mod editor_data;
mod modals;
pub use app_data::{AppData, Block};
pub use editor_data::{EditMode, EditorData};
pub use modals::*;

View File

@ -0,0 +1,13 @@
use druid::{Data, Lens};
#[derive(Clone, Data, Lens, Default, Debug)]
pub struct RenameBlock {
pub name: String,
pub input: String,
pub block_index: usize,
}
#[derive(Clone, Data, Lens, Default, Debug)]
pub struct Modals {
pub rename_block: RenameBlock,
}

View File

@ -1,84 +1,258 @@
use abacus_core::Output;
use clipboard::ClipboardProvider;
use druid::{
piet::{Text, TextLayout, TextLayoutBuilder},
Color, Data, Event, FontFamily, Lens, LifeCycle, RenderContext, Widget,
piet::{CairoTextLayout, Text, TextAttribute, TextLayout, TextLayoutBuilder},
widget::{Container, Flex, Label, Padding, Svg, SvgData},
Color, Event, FontDescriptor, FontFamily, FontWeight, LifeCycle, PaintCtx, Rect, RenderContext,
Target, Widget, WidgetExt,
};
#[derive(Clone, Data, PartialEq, Eq)]
pub enum EditMode {
Normal,
Insert,
}
use syntect::easy::HighlightLines;
use syntect::highlighting::{Style, ThemeSet};
use syntect::parsing::SyntaxSet;
pub struct AbacusEditor;
use crate::{
app_header::ToolbarButtonController,
data::{EditMode, EditorData},
Block,
};
#[derive(Data, Lens, Clone)]
pub struct EditorData {
pub raw_content: String,
pub cursor_pos: usize,
pub mode: EditMode,
}
const FONT_SIZE: f64 = 16.0;
impl Default for EditorData {
fn default() -> Self {
Self {
raw_content: "let x = 1;\nx+1".to_string(),
cursor_pos: 5,
mode: EditMode::Normal,
mod keymap {
pub const LINE_ABOVE: &str = "O";
pub const LINE_BELOW: &str = "o";
pub const DELETE_LINE: &str = "dd";
pub const EDIT_EOL: &str = "A";
pub const EDIT: &str = "i";
pub const EDIT_BOL: &str = "I";
pub const CURSOR_LEFT: &str = "h";
pub const CURSOR_RIGHT: &str = "l";
pub const CURSOR_UP: &str = "k";
pub const CURSOR_DOWN: &str = "j";
pub const DELETE_CHAR: &str = "x";
pub const DELETE_TO_EOL: &str = "D";
pub const WORD_FORWARD: &str = "w";
pub const WORD_BACK: &str = "b";
pub const SELECT_MODE: &str = "v";
#[derive(Debug, Default)]
pub struct KeyList {
input_value: String,
commands: Vec<&'static str>,
}
impl KeyList {
pub fn new() -> Self {
Self {
input_value: String::new(),
commands: vec![
LINE_ABOVE,
LINE_BELOW,
DELETE_LINE,
EDIT_EOL,
EDIT,
EDIT_BOL,
CURSOR_LEFT,
CURSOR_RIGHT,
CURSOR_UP,
CURSOR_DOWN,
DELETE_CHAR,
DELETE_TO_EOL,
WORD_FORWARD,
WORD_BACK,
SELECT_MODE,
],
}
}
pub fn push(&mut self, s: &str) {
self.input_value.push_str(s);
if !self.valid() {
self.clear();
}
}
fn valid(&self) -> bool {
self.commands
.iter()
.any(|i| i.starts_with(&self.input_value))
}
pub fn clear(&mut self) {
self.input_value.clear();
}
pub fn get(&mut self) -> Option<&str> {
let v = self
.commands
.iter()
.find(|i| **i == self.input_value)
.cloned();
if v.is_some() {
self.clear();
}
v
}
}
}
impl EditorData {
pub fn push_str(&mut self, s: &str) {
self.raw_content.push_str(s);
self.cursor_to_end();
pub struct AbacusEditor {
syntax_set: SyntaxSet,
theme_set: ThemeSet,
key_list: keymap::KeyList,
}
impl Default for AbacusEditor {
fn default() -> Self {
Self {
syntax_set: SyntaxSet::load_defaults_newlines(),
theme_set: ThemeSet::load_defaults(),
key_list: keymap::KeyList::new(),
}
}
}
impl AbacusEditor {
fn paint_cursor(&self, ctx: &mut PaintCtx, data: &EditorData, layout: &CairoTextLayout) {
if data.mode == EditMode::Insert {
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(),
rect.min_x() + 1.0,
rect.max_y(),
);
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()
{
let cursor_rect = Rect::new(
char_rect.max_x() - 1.0,
char_rect.min_y(),
char_rect.max_x() + 1.0,
char_rect.max_y(),
);
ctx.fill(
cursor_rect,
&Color::rgba8(255, 255, 255, data.cursor_opactiy.floor() as u8),
);
}
}
if data.mode == EditMode::Normal {
let char_rect = if data.content.len_chars() == 0 {
let rects = layout.rects_for_range(0..1);
rects.first().cloned()
} else if data.current_char() == Some('\n')
|| data.cursor_pos == data.content.len_chars()
{
let range = (data.cursor_pos.max(1) - 1)..data.cursor_pos;
layout.rects_for_range(range).last().cloned().map(|rect| {
Rect::new(rect.max_x(), rect.min_y(), rect.max_x() + 10., rect.max_y())
})
} else {
let range = data.cursor_pos..(data.cursor_pos + 1);
layout.rects_for_range(range).last().cloned()
};
if let Some(char_rect) = char_rect {
if char_rect.width() == 0. {
let rect = Rect::new(
char_rect.min_x(),
char_rect.min_y(),
char_rect.min_x() + 10.,
char_rect.max_y(),
);
ctx.fill(
rect,
&Color::rgba8(255, 255, 255, data.cursor_opactiy.floor() as u8),
);
} else {
ctx.fill(
char_rect,
&Color::rgba8(255, 255, 255, data.cursor_opactiy.floor() as u8),
);
};
}
}
}
pub fn push(&mut self, c: char) {
self.raw_content.push(c);
self.cursor_to_end();
}
pub fn build_highlighted_layout(
&self,
ctx: &mut PaintCtx,
data: &EditorData,
) -> CairoTextLayout {
let syntax = self.syntax_set.find_syntax_by_extension("rs").unwrap();
let mut h = HighlightLines::new(syntax, &self.theme_set.themes["base16-mocha.dark"]);
pub fn cursor_to_end(&mut self) {
self.cursor_pos = self.raw_content.len();
}
let mut layout = ctx
.text()
.new_text_layout(if data.content.len_chars() == 0 {
String::from(" ")
} else {
data.content.to_string()
})
.font(FontFamily::MONOSPACE, FONT_SIZE);
pub fn delete_char_back(&mut self) {
self.raw_content.pop();
self.cursor_to_end();
let mut pos = 0;
for line in data.content.lines() {
let s = line.to_string();
let ranges: Vec<(Style, &str)> = h.highlight_line(&s, &self.syntax_set).unwrap();
for (style, txt) in ranges {
layout = layout.range_attribute(
pos..(pos + txt.len()),
TextAttribute::TextColor(Color::rgba8(
style.foreground.r,
style.foreground.g,
style.foreground.b,
style.foreground.a,
)),
);
pos += txt.len();
}
}
layout.build().unwrap()
}
}
impl Widget<EditorData> for AbacusEditor {
fn paint(&mut self, ctx: &mut druid::PaintCtx, data: &EditorData, env: &druid::Env) {
let size = ctx.size();
let rect = size.to_rect();
fn paint(&mut self, ctx: &mut druid::PaintCtx, data: &EditorData, _env: &druid::Env) {
let layout = self.build_highlighted_layout(ctx, data);
if ctx.is_focused() {
ctx.fill(rect, &Color::rgb8(10, 10, 10));
} else {
ctx.fill(rect, &Color::rgb8(20, 20, 20));
if data.selection_pos.is_some() {
let rects = layout.rects_for_range(data.select_range());
for rect in rects.iter() {
if rect.width() == 0.0 {
ctx.fill(
Rect::new(
rect.min_x(),
rect.min_y(),
rect.max_x() + 10.0,
rect.max_y(),
),
&Color::rgb8(90, 90, 90),
);
} else {
ctx.fill(rect, &Color::rgb8(90, 90, 90));
}
}
}
let layout = ctx
.text()
.new_text_layout(data.raw_content.clone())
.font(FontFamily::MONOSPACE, 24.0)
.text_color(Color::rgb8(255, 255, 255))
.build()
.unwrap();
if ctx.has_focus() {
self.paint_cursor(ctx, data, &layout);
}
dbg!(layout.rects_for_range((data.cursor_pos - 1)..data.cursor_pos));
ctx.fill(
layout
.rects_for_range((data.cursor_pos - 1)..data.cursor_pos)
.last()
.unwrap(),
&Color::rgb8(50, 50, 50),
);
ctx.draw_text(&layout, (1.0, 1.0));
dbg!(ctx.is_focused());
ctx.draw_text(&layout, (0.0, 0.0));
}
fn event(
@ -86,33 +260,226 @@ impl Widget<EditorData> for AbacusEditor {
ctx: &mut druid::EventCtx,
event: &druid::Event,
data: &mut EditorData,
env: &druid::Env,
_env: &druid::Env,
) {
match event {
Event::KeyUp(e) => match &e.key {
druid::keyboard_types::Key::Character(c) => {
data.push_str(c);
ctx.request_paint();
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();
}
}
"c" => {
if data.selection_pos.is_some() {
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();
}
_ => {}
}
}
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 => {
self.key_list.push(ch);
match self.key_list.get() {
Some(keymap::DELETE_LINE) => {
data.delete_current_line();
data.deselect();
}
Some(keymap::DELETE_CHAR) => {
data.delete_char_forward();
data.deselect();
}
Some(keymap::EDIT) => {
data.mode = EditMode::Insert;
data.deselect();
}
Some(keymap::LINE_ABOVE) => {
data.mode = EditMode::Insert;
data.cursor_to_start_of_line();
data.content.insert(data.cursor_pos, "\n");
data.deselect();
}
Some(keymap::LINE_BELOW) => {
data.mode = EditMode::Insert;
data.cursor_to_end_of_line();
data.push_str("\n");
data.deselect();
}
Some(keymap::EDIT_EOL) => {
data.mode = EditMode::Insert;
data.cursor_to_end_of_line();
data.deselect();
}
Some(keymap::EDIT_BOL) => {
data.cursor_to_start_of_line();
data.mode = EditMode::Insert;
data.deselect();
}
Some(keymap::CURSOR_LEFT) => {
data.cursor_left();
}
Some(keymap::CURSOR_DOWN) => {
data.cursor_down();
}
Some(keymap::CURSOR_UP) => {
data.cursor_up();
}
Some(keymap::CURSOR_RIGHT) => {
data.cursor_right();
}
Some(keymap::DELETE_TO_EOL) => {
data.delete_to_eol();
data.deselect();
}
Some(keymap::WORD_FORWARD) => {
data.word_scan_forward();
}
Some(keymap::WORD_BACK) => {
data.word_scan_backward();
}
Some(keymap::SELECT_MODE) => {
data.selection_pos = Some(data.cursor_pos);
}
_ => {}
}
}
druid::keyboard_types::Key::Enter if e.mods.ctrl() => {
ctx.submit_command(crate::commands::PROCESS_WORKBOOK.to(Target::Global));
}
druid::keyboard_types::Key::Enter if data.mode == EditMode::Insert => {
data.push('\n');
ctx.request_layout();
data.deselect();
}
druid::keyboard_types::Key::Enter if data.mode == EditMode::Normal => {
data.cursor_down();
data.deselect();
}
druid::keyboard_types::Key::Backspace => {
data.delete_char_back();
ctx.request_layout();
data.deselect();
}
druid::keyboard_types::Key::Delete => {
data.delete_char_forward();
ctx.request_layout();
data.deselect();
}
druid::keyboard_types::Key::Escape => {
data.normal_mode();
data.deselect();
}
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();
data.deselect();
}
druid::keyboard_types::Key::ArrowUp if !e.mods.shift() => {
data.cursor_up();
data.deselect();
}
druid::keyboard_types::Key::ArrowDown if !e.mods.shift() => {
data.cursor_down();
data.deselect();
}
druid::keyboard_types::Key::ArrowUp if e.mods.shift() => {
data.select_up();
data.deselect();
}
druid::keyboard_types::Key::ArrowDown if e.mods.shift() => {
data.select_down();
}
druid::keyboard_types::Key::Tab => {
data.push_str(" ");
data.deselect();
}
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::Enter => {
data.push('\n');
ctx.request_paint();
}
druid::keyboard_types::Key::Backspace => {
data.delete_char_back();
ctx.request_paint();
}
druid::keyboard_types::Key::Escape => {
data.mode == EditMode::Normal;
}
e => {
dbg!(e);
}
},
Event::MouseDown(_) => {
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, FONT_SIZE)
.build()
.unwrap();
let pos = layout.hit_test_point(e.pos);
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, FONT_SIZE)
.text_color(Color::rgb8(255, 255, 255))
.build()
.unwrap();
let pos = layout.hit_test_point(e.pos);
if e.buttons.has_left() {
if data.selection_pos.is_none() {
data.selection_pos = Some(data.cursor_pos);
}
let new_pos = (pos.idx + 1).min(data.content.len_chars());
if new_pos > data.cursor_pos {
data.cursor_pos = pos.idx.min(data.content.len_chars());
} else {
data.cursor_pos = pos.idx.max(1) - 1;
}
ctx.request_paint();
}
}
_ => {}
}
@ -122,8 +489,8 @@ impl Widget<EditorData> for AbacusEditor {
&mut self,
ctx: &mut druid::LifeCycleCtx,
event: &druid::LifeCycle,
data: &EditorData,
env: &druid::Env,
_data: &EditorData,
_env: &druid::Env,
) {
match event {
LifeCycle::FocusChanged(_) => {
@ -144,8 +511,12 @@ impl Widget<EditorData> for AbacusEditor {
ctx: &mut druid::UpdateCtx,
old_data: &EditorData,
data: &EditorData,
env: &druid::Env,
_env: &druid::Env,
) {
if old_data != data {
ctx.request_paint();
ctx.request_layout();
}
}
fn layout(
@ -153,8 +524,105 @@ impl Widget<EditorData> for AbacusEditor {
ctx: &mut druid::LayoutCtx,
bc: &druid::BoxConstraints,
data: &EditorData,
env: &druid::Env,
_env: &druid::Env,
) -> druid::Size {
bc.max()
let layout = ctx
.text()
.new_text_layout(data.content.to_string())
.font(FontFamily::MONOSPACE, FONT_SIZE)
.build()
.unwrap();
(bc.max().width, layout.size().height).into()
}
}
pub fn editor_header() -> impl Widget<Block> {
let ban_svg = include_str!("../assets/ban.svg")
.parse::<SvgData>()
.unwrap_or_default();
let trash_svg = include_str!("../assets/trash.svg")
.parse::<SvgData>()
.unwrap_or_default();
let run_svg = include_str!("../assets/play.svg")
.parse::<SvgData>()
.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)
.on_click(|ctx, data, _| {
ctx.submit_command(crate::commands::RENAME_BLOCK.with(data.index));
}),
)
.with_flex_spacer(1.0)
.with_child(
Label::dynamic(|data: &Block, _| {
match data.editor_data.mode {
EditMode::Insert => "EDIT",
EditMode::Normal => "",
}
.to_string()
})
.with_font(
FontDescriptor::new(FontFamily::SANS_SERIF)
.with_size(14.0)
.with_weight(FontWeight::BOLD),
)
.padding(5.0),
)
.with_spacer(10.0)
.with_child(
Label::dynamic(|data: &Block, _| {
format!(
"{}:{}",
data.editor_data.current_line_index() + 1,
data.editor_data.current_column() + 1,
)
})
.with_font(FontDescriptor::new(FontFamily::SANS_SERIF).with_size(14.0))
.padding(5.0),
)
.with_spacer(20.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)
}

View File

@ -1,35 +1,47 @@
mod app_delegate;
mod app_header;
mod commands;
mod data;
mod editor;
mod modal_container;
mod output_block;
use druid::widget::{Align, Flex, Label, Padding, RawLabel};
use druid::{AppLauncher, Color, Data, PlatformError, Widget, WidgetExt, WindowDesc};
use editor::EditorData;
use data::{AppData, Block};
use druid::widget::{Container, Flex, List, Padding, Scroll};
use druid::{AppLauncher, PlatformError, Widget, WidgetExt, WindowDesc};
use modal_container::ModalContainer;
fn build_ui() -> impl Widget<editor::EditorData> {
Padding::new(
10.0,
Flex::row()
fn build_ui() -> impl Widget<AppData> {
ModalContainer::new(
Flex::column()
.with_child(app_header::app_header_ui())
.with_child(app_header::header_separater())
.with_flex_child(
Flex::column().with_flex_child(editor::AbacusEditor, 1.0),
1.0,
)
.with_flex_child(
Flex::column()
.with_flex_child(
Label::new("top right").background(Color::rgb8(0, 0, 255)),
1.0,
)
.with_flex_child(Align::centered(Label::new("bottom right")), 1.0),
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,
}

View File

@ -0,0 +1,256 @@
use crate::data;
use druid::widget::{Controller, Padding};
use druid::{
widget::{Container, Flex, Label, LensWrap, TextBox},
Color, FontDescriptor, FontFamily, FontWeight, RenderContext, Widget, WidgetExt, WidgetPod,
};
use druid::{Data, LifeCycle, TextAlignment};
pub struct ModalContainer {
child: WidgetPod<data::AppData, Box<dyn Widget<data::AppData>>>,
modal: Option<WidgetPod<data::AppData, Box<dyn Widget<data::AppData>>>>,
}
impl ModalContainer {
pub fn new(child: impl Widget<data::AppData> + 'static) -> Self {
Self {
child: WidgetPod::new(child).boxed(),
modal: None,
}
}
}
impl Widget<data::AppData> for ModalContainer {
fn event(
&mut self,
ctx: &mut druid::EventCtx,
event: &druid::Event,
data: &mut data::AppData,
env: &druid::Env,
) {
if let druid::Event::Notification(n) = event {
if n.is(crate::commands::CLOSE_MODAL) {
self.modal = None;
data.modals.rename_block = data::RenameBlock::default();
ctx.children_changed();
return;
}
}
if let druid::Event::Command(c) = event {
if let Some(idx) = c.get(crate::commands::RENAME_BLOCK) {
data.modals.rename_block = data::RenameBlock {
name: data
.blocks
.get(*idx)
.map(|i| i.name.clone())
.unwrap_or_default(),
input: data
.blocks
.get(*idx)
.map(|i| i.name.clone())
.unwrap_or_default(),
block_index: *idx,
};
self.modal = Some(WidgetPod::new(rename_block()).boxed());
ctx.children_changed();
return;
}
}
if let Some(m) = self.modal.as_mut() {
m.event(ctx, event, data, env);
} else {
self.child.event(ctx, event, data, env);
}
}
fn lifecycle(
&mut self,
ctx: &mut druid::LifeCycleCtx,
event: &druid::LifeCycle,
data: &data::AppData,
env: &druid::Env,
) {
if let Some(modal) = self.modal.as_mut() {
modal.lifecycle(ctx, event, data, env);
}
self.child.lifecycle(ctx, event, data, env);
}
fn update(
&mut self,
ctx: &mut druid::UpdateCtx,
_old_data: &data::AppData,
data: &data::AppData,
env: &druid::Env,
) {
if let Some(modal) = self.modal.as_mut() {
modal.update(ctx, data, env);
}
self.child.update(ctx, data, env);
}
fn layout(
&mut self,
ctx: &mut druid::LayoutCtx,
bc: &druid::BoxConstraints,
data: &data::AppData,
env: &druid::Env,
) -> druid::Size {
if let Some(modal) = self.modal.as_mut() {
modal.layout(ctx, bc, data, env);
}
self.child.layout(ctx, bc, data, env)
}
fn paint(&mut self, ctx: &mut druid::PaintCtx, data: &data::AppData, env: &druid::Env) {
self.child.paint(ctx, data, env);
let full_rect = ctx.size().to_rect();
if let Some(m) = self.modal.as_mut() {
ctx.fill(full_rect, &druid::Color::rgba8(0, 0, 0, 100));
m.paint(ctx, data, env)
}
}
}
fn rename_block() -> impl Widget<data::AppData> {
modal_container(
"Rename Block",
(300.0, 150.0),
Flex::column()
.with_child(
Label::new("Block name")
.with_text_color(druid::Color::WHITE)
.with_text_alignment(TextAlignment::Start)
.with_font(
FontDescriptor::new(FontFamily::SYSTEM_UI)
.with_weight(FontWeight::BOLD)
.with_size(14.0),
)
.fix_width(300.0),
)
.with_spacer(5.0)
.with_child(LensWrap::new(
LensWrap::new(
TextBox::default()
.lens(data::RenameBlock::input)
.fix_width(300.0),
data::Modals::rename_block,
),
data::AppData::modals,
))
.with_spacer(20.0)
.with_child(
Flex::row()
.must_fill_main_axis(true)
.with_child(modal_action("Cancel").on_click(|ctx, _, _| {
ctx.submit_notification(crate::commands::CLOSE_MODAL)
}))
.with_flex_spacer(1.0)
.with_child(modal_action("Rename").on_click(
|ctx, data: &mut data::AppData, _| {
let input = data.modals.rename_block.input.clone();
if let Some(blk) =
data.blocks.get_mut(data.modals.rename_block.block_index)
{
blk.name = input;
data.modals.rename_block = data::RenameBlock::default();
ctx.submit_notification(crate::commands::CLOSE_MODAL)
}
},
))
.fix_width(300.0),
),
)
}
fn modal_container<S: Into<druid::Size>>(
title: &str,
size: S,
child: impl Widget<data::AppData> + 'static,
) -> impl Widget<data::AppData> {
let size = size.into();
Flex::column()
.with_child(modal_title(title, size.width))
.with_child(Padding::new(15.0, child))
.background(Color::rgb8(20, 20, 20))
.center()
.fix_size(size.width, size.height)
}
fn modal_title(title: &str, width: f64) -> impl Widget<data::AppData> {
Container::new(Padding::new(
8.0,
Flex::row()
.with_spacer(10.0)
.with_child(
Label::new(title)
.with_text_color(Color::rgb8(150, 150, 150))
.with_text_alignment(TextAlignment::Start)
.with_font(
FontDescriptor::new(FontFamily::SYSTEM_UI)
.with_weight(FontWeight::BOLD)
.with_size(14.0),
)
.fix_width(width),
)
.with_spacer(10.0),
))
.background(Color::rgb8(10, 10, 10))
}
fn modal_action<T: Data>(text: &str) -> impl Widget<T> {
Container::new(Padding::new(5.0, Label::new(text)))
.rounded(4.0)
.controller(ModalActionController)
}
pub struct ModalActionController;
impl<T: Data> Controller<T, Container<T>> for ModalActionController {
fn event(
&mut self,
child: &mut Container<T>,
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<T>,
ctx: &mut druid::LifeCycleCtx,
event: &druid::LifeCycle,
data: &T,
env: &druid::Env,
) {
match event {
LifeCycle::HotChanged(true) => {
child.set_background(Color::rgb8(100, 100, 100));
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<T>,
ctx: &mut druid::UpdateCtx,
old_data: &T,
data: &T,
env: &druid::Env,
) {
child.update(ctx, old_data, data, env)
}
}

1
abacus-ui/src/output.rs Normal file
View File

@ -0,0 +1 @@

View File

@ -0,0 +1,130 @@
use abacus_core::Output;
use druid::{
widget::{Container, Flex, Label, Padding, Scroll, ViewSwitcher},
Color, FontDescriptor, FontFamily, FontWeight, Widget, WidgetExt,
};
use crate::data::Block;
const OUTPUT_FONT_SIZE: f64 = 16.0;
pub fn output_block() -> impl Widget<Block> {
ViewSwitcher::new(
|data: &Block, _env| data.clone(),
|selector: &Block, _data, _env| match &selector.output {
Output::Scalar(v) => {
let str = match v {
_ if v.is::<String>() => v.clone().cast::<String>(),
_ if v.is::<&str>() => v.clone().cast::<&str>().to_string(),
v => v.to_string(),
};
Box::new(Padding::new(
25.0,
Label::new(str)
.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(),
))
}
Output::DataFrame(frame) => {
let mut flex = Flex::row();
for series in frame.iter() {
let mut col = Flex::column()
.cross_axis_alignment(druid::widget::CrossAxisAlignment::Fill);
col.add_child(
Label::new(series.name())
.with_font(
FontDescriptor::new(FontFamily::MONOSPACE)
.with_weight(FontWeight::BLACK),
)
.with_text_size(OUTPUT_FONT_SIZE)
.with_text_color(Color::rgb8(150, 150, 150))
.padding(3.0)
.border(Color::rgb8(50, 50, 50), 1.0)
.background(Color::rgb8(40, 40, 40)),
);
for v in series.iter() {
col.add_child(
Label::new(format_dataframe_value(v))
.with_font(
FontDescriptor::new(FontFamily::MONOSPACE)
.with_weight(FontWeight::MEDIUM),
)
.with_text_size(OUTPUT_FONT_SIZE)
.padding(3.0)
.border(Color::rgb8(50, 50, 50), 1.0),
);
}
flex.add_child(col);
}
Box::new(Padding::new(
25.0,
Container::new(
Scroll::new(flex.background(Color::rgb8(30, 30, 30)).expand_width())
.horizontal()
.expand_width(),
)
.rounded(4.0)
.border(Color::rgb8(30, 30, 30), 5.0)
.background(Color::rgb8(30, 30, 30))
.expand_width(),
))
}
Output::None => Box::new(Container::new(Label::new(""))),
},
)
}
fn format_dataframe_value(value: abacus_core::AnyValue) -> String {
match value {
abacus_core::AnyValue::Utf8(v) => v.to_string(),
abacus_core::AnyValue::Utf8Owned(v) => v,
_ => value.to_string(),
}
}

View File

@ -0,0 +1,336 @@
use piet_common::{kurbo, Color, LineCap, Piet, RenderContext, StrokeDash, StrokeStyle};
use plotters_backend::{BackendColor, BackendCoord, DrawingBackend, DrawingErrorKind};
#[derive(Debug, PartialEq, Eq)]
pub struct Error {}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "plotters-piet error")
}
}
impl std::error::Error for Error {}
/// The piet backend.
///
/// Note that the size of the piet context has to be specified here.
pub struct PietBackend<'a, 'b> {
pub size: (u32, u32),
pub render_ctx: &'a mut Piet<'b>,
}
impl<'a, 'b> std::fmt::Debug for PietBackend<'a, 'b> {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
fmt.debug_struct("PietBackend")
.field("size", &self.size)
.field("render_ctx", &"(not printable)")
.finish()
}
}
impl<'a, 'b> DrawingBackend for PietBackend<'a, 'b> {
type ErrorType = Error;
fn get_size(&self) -> (u32, u32) {
self.size
}
fn ensure_prepared(&mut self) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
Ok(())
}
fn present(&mut self) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
self.render_ctx
.finish()
.map_err(|_| DrawingErrorKind::DrawingError(Error {}))
}
fn draw_pixel(
&mut self,
point: BackendCoord,
color: BackendColor,
) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
let x = point.0 as f64;
let y = point.1 as f64;
self.render_ctx.fill(
kurbo::Rect::new(x, y, x + 1., y + 1.),
&plotters_color_to_piet(&color),
);
Ok(())
}
fn draw_line<S: plotters_backend::BackendStyle>(
&mut self,
from: BackendCoord,
to: BackendCoord,
style: &S,
) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
let from = plotters_point_to_kurbo_mid(from);
let to = plotters_point_to_kurbo_mid(to);
self.render_ctx.stroke_styled(
kurbo::Line::new(from, to),
&plotters_color_to_piet(&style.color()),
style.stroke_width() as f64,
&STROKE_STYLE_SQUARE_CAP,
);
Ok(())
}
fn draw_rect<S: plotters_backend::BackendStyle>(
&mut self,
upper_left: BackendCoord,
bottom_right: BackendCoord,
style: &S,
fill: bool,
) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
let color = plotters_color_to_piet(&style.color());
if fill {
let upper_left = plotters_point_to_kurbo_corner(upper_left);
let mut bottom_right = plotters_point_to_kurbo_corner(bottom_right);
bottom_right.x += 1.;
bottom_right.y += 1.;
let rect = kurbo::Rect::new(upper_left.x, upper_left.y, bottom_right.x, bottom_right.y);
self.render_ctx.fill(rect, &color);
} else {
let upper_left = plotters_point_to_kurbo_mid(upper_left);
let bottom_right = plotters_point_to_kurbo_mid(bottom_right);
let rect = kurbo::Rect::new(upper_left.x, upper_left.y, bottom_right.x, bottom_right.y);
self.render_ctx
.stroke(rect, &color, style.stroke_width() as f64);
}
Ok(())
}
fn draw_path<S: plotters_backend::BackendStyle, I: IntoIterator<Item = BackendCoord>>(
&mut self,
path: I,
style: &S,
) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
if style.color().alpha == 0.0 {
return Ok(());
}
let path: Vec<kurbo::PathEl> = plotters_path_to_kurbo(path).collect();
self.render_ctx.stroke_styled(
&*path,
&plotters_color_to_piet(&style.color()),
style.stroke_width() as f64,
&STROKE_STYLE_SQUARE_CAP,
);
Ok(())
}
fn draw_circle<S: plotters_backend::BackendStyle>(
&mut self,
center: BackendCoord,
radius: u32,
style: &S,
fill: bool,
) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
let center = plotters_point_to_kurbo_mid(center);
let color = plotters_color_to_piet(&style.color());
let circle = kurbo::Circle::new(center, radius as f64);
if fill {
self.render_ctx.fill(circle, &color);
} else {
self.render_ctx
.stroke(circle, &color, style.stroke_width() as f64);
}
Ok(())
}
fn fill_polygon<S: plotters_backend::BackendStyle, I: IntoIterator<Item = BackendCoord>>(
&mut self,
vert: I,
style: &S,
) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
if style.color().alpha == 0.0 {
return Ok(());
}
let path: Vec<kurbo::PathEl> = plotters_path_to_kurbo(vert)
.chain(std::iter::once(kurbo::PathEl::ClosePath))
.collect();
self.render_ctx
.fill(&*path, &plotters_color_to_piet(&style.color()));
Ok(())
}
// For now we use the default text drawing provided by plotters. This is definitely slower,
// but at least we don't have to worry about matching the font size and offset which turns
// out to be trickier than expected.
// fn draw_text<TStyle: plotters_backend::BackendTextStyle>(
// &mut self,
// text: &str,
// style: &TStyle,
// pos: BackendCoord,
// ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
// let pos = plotters_point_to_kurbo(pos);
// let color = plotters_color_to_piet(&style.color());
// let text_api = self.render_ctx.text();
// let font_family = match style.family() {
// plotters_backend::FontFamily::Serif => Ok(FontFamily::SERIF),
// plotters_backend::FontFamily::SansSerif => Ok(FontFamily::SANS_SERIF),
// plotters_backend::FontFamily::Monospace => Ok(FontFamily::MONOSPACE),
// plotters_backend::FontFamily::Name(name) => text_api
// .font_family(name)
// .ok_or(piet_common::Error::MissingFont),
// };
// let (font_style, weight) = match style.style() {
// plotters_backend::FontStyle::Normal => (FontStyle::Regular, FontWeight::REGULAR),
// plotters_backend::FontStyle::Oblique => (FontStyle::Italic, FontWeight::REGULAR),
// plotters_backend::FontStyle::Italic => (FontStyle::Italic, FontWeight::REGULAR),
// plotters_backend::FontStyle::Bold => (FontStyle::Regular, FontWeight::BOLD),
// };
// let alignment = match style.anchor().h_pos {
// plotters_backend::text_anchor::HPos::Left => TextAlignment::Start,
// plotters_backend::text_anchor::HPos::Right => TextAlignment::End,
// plotters_backend::text_anchor::HPos::Center => TextAlignment::Center,
// };
// let layout = text_api
// .new_text_layout(String::from(text))
// .font(font_family.unwrap(), style.size())
// .text_color(color)
// .alignment(alignment)
// .default_attribute(TextAttribute::Style(font_style))
// .default_attribute(TextAttribute::Weight(weight))
// .build()
// .unwrap();
// // todo: style.anchor().v_pos
// // todo: style.transform()
// self.render_ctx.draw_text(&layout, pos);
// Ok(())
// }
}
fn plotters_color_to_piet(col: &BackendColor) -> piet_common::Color {
Color::rgba8(col.rgb.0, col.rgb.1, col.rgb.2, (col.alpha * 256.) as u8)
}
fn plotters_point_to_kurbo_mid((x, y): BackendCoord) -> kurbo::Point {
kurbo::Point {
x: x as f64 + 0.5,
y: y as f64 + 0.5,
}
}
fn plotters_point_to_kurbo_corner((x, y): BackendCoord) -> kurbo::Point {
kurbo::Point {
x: x as f64,
y: y as f64,
}
}
/// This is basically just an iterator map that applies a different function on
/// the first item as on the later items.
/// We need this because the piet direct2d backend doesn't like it if a path
/// consists entirely of `LineTo` entries, it requires the first entry to be
/// a `MoveTo` entry.
struct PlottersPathToKurbo<I> {
iter: I,
first: bool,
}
impl<I> PlottersPathToKurbo<I> {
fn new(path: I) -> PlottersPathToKurbo<I> {
PlottersPathToKurbo {
iter: path,
first: true,
}
}
}
impl<I> Iterator for PlottersPathToKurbo<I>
where
I: Iterator<Item = BackendCoord>,
{
type Item = kurbo::PathEl;
fn next(&mut self) -> Option<Self::Item> {
self.iter.next().map(|point| {
let point = plotters_point_to_kurbo_mid(point);
if self.first {
self.first = false;
kurbo::PathEl::MoveTo(point)
} else {
kurbo::PathEl::LineTo(point)
}
})
}
}
fn plotters_path_to_kurbo(
path: impl IntoIterator<Item = BackendCoord>,
) -> impl Iterator<Item = kurbo::PathEl> {
PlottersPathToKurbo::new(path.into_iter())
}
const STROKE_STYLE_SQUARE_CAP: StrokeStyle = StrokeStyle::new().line_cap(LineCap::Square);
#[cfg(test)]
mod tests {
use super::*;
use piet_common::RenderContext;
use plotters::prelude::*;
#[test]
fn fill_root_white() {
let width = 3;
let height = 2;
let mut device = piet_common::Device::new().unwrap();
let mut bitmap = device.bitmap_target(width, height, 1.0).unwrap();
{
let mut render_ctx = bitmap.render_context();
let piet_backend = PietBackend {
size: (width as u32, height as u32),
render_ctx: &mut render_ctx,
};
let root = piet_backend.into_drawing_area();
root.fill(&WHITE).unwrap();
render_ctx.finish().unwrap();
}
let mut buf = [0; 6 * 4];
bitmap
.copy_raw_pixels(piet_common::ImageFormat::RgbaPremul, &mut buf)
.unwrap();
assert_eq!(buf, [255; 6 * 4]);
}
#[test]
fn test_plotters_path_to_kurbo() {
let path = vec![(1, 2), (3, 4), (5, 6)];
let kurbo_path: Vec<kurbo::PathEl> = plotters_path_to_kurbo(path).collect();
assert_eq!(
kurbo_path,
vec![
kurbo::PathEl::MoveTo(kurbo::Point { x: 1.5, y: 2.5 }),
kurbo::PathEl::LineTo(kurbo::Point { x: 3.5, y: 4.5 }),
kurbo::PathEl::LineTo(kurbo::Point { x: 5.5, y: 6.5 }),
]
);
}
}

View File

@ -0,0 +1,60 @@
use crate::piet_plotters::PietBackend;
use druid::{Data, Widget};
use plotters::{
coord::Shift,
prelude::{DrawingArea, IntoDrawingArea},
};
pub struct Plot<T: Data> {
#[allow(clippy::type_complexity)]
plot: Box<dyn Fn((u32, u32), &T, &DrawingArea<PietBackend, Shift>)>,
}
impl<T: Data> Plot<T> {
pub fn new(f: impl Fn((u32, u32), &T, &DrawingArea<PietBackend, Shift>) + 'static) -> Plot<T> {
Plot { plot: Box::new(f) }
}
}
impl<T> Widget<T> for Plot<T>
where
T: Data,
{
fn event(&mut self, _: &mut druid::EventCtx, _: &druid::Event, _: &mut T, _: &druid::Env) {}
fn lifecycle(
&mut self,
_: &mut druid::LifeCycleCtx,
_: &druid::LifeCycle,
_: &T,
_: &druid::Env,
) {
}
fn update(&mut self, ctx: &mut druid::UpdateCtx, old_data: &T, data: &T, _env: &druid::Env) {
if !old_data.same(data) {
ctx.request_paint();
}
}
fn layout(
&mut self,
_: &mut druid::LayoutCtx,
bc: &druid::BoxConstraints,
_: &T,
_: &druid::Env,
) -> druid::Size {
bc.max()
}
fn paint(&mut self, ctx: &mut druid::PaintCtx, data: &T, _: &druid::Env) {
let druid::Size { width, height } = ctx.size();
let size = (width as u32, height as u32);
let backend = PietBackend {
size,
render_ctx: ctx.render_ctx,
};
(self.plot)(size, data, &backend.into_drawing_area());
}
}

View File

@ -0,0 +1,68 @@
use druid::{
kurbo::Line,
widget::{FillStrat, SvgData},
Color, Data, Rect, RenderContext, Widget,
};
pub struct ToolbarItem {
icon: SvgData,
}
impl<T: Data> Widget<T> 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);
});
}
}

27
drone.yml Normal file
View File

@ -0,0 +1,27 @@
kind: pipeline
name: default
steps:
- name: test
image: rust:latest
commands:
- rustup component add clippy
- cargo clippy
- cargo test --all
- name: deploy
image: rust:latest
commands:
- cargo build --release
- tar cvzf conductor.tar.gz -C target/release conductor
- wget https://dl.min.io/client/mc/release/linux-amd64/mc
- chmod +x mc
- ./mc alias set fivesigma https://objects.5sigma.io $MINIOID $MINIOSECRET
- ./mc cp conductor.tar.gz fivesigma/public/conductor.tar.gz
# when:
# event:
# - promote
# target:
# - staging
# - production

8
frame Normal file
View File

@ -0,0 +1,8 @@
{
"blocks": [
{
"name": "Block #1",
"content": "dataframe(#{ names: [\"Alice\", \"Bob\", \"Charles\"], ages: [18,21,35] })"
}
]
}