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
|
@ -9,6 +9,7 @@ dependencies = [
|
||||||
"polars",
|
"polars",
|
||||||
"rhai",
|
"rhai",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"syntect",
|
"syntect",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
]
|
]
|
||||||
|
@ -18,7 +19,10 @@ name = "abacus-ui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"abacus-core",
|
"abacus-core",
|
||||||
|
"clipboard",
|
||||||
"druid",
|
"druid",
|
||||||
|
"ropey",
|
||||||
|
"syntect",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -158,6 +162,15 @@ version = "1.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitmaps"
|
||||||
|
version = "2.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2"
|
||||||
|
dependencies = [
|
||||||
|
"typenum",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block"
|
name = "block"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
|
@ -250,6 +263,28 @@ dependencies = [
|
||||||
"winapi",
|
"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]]
|
[[package]]
|
||||||
name = "cocoa"
|
name = "cocoa"
|
||||||
version = "0.24.0"
|
version = "0.24.0"
|
||||||
|
@ -442,6 +477,15 @@ dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "data-url"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3a30bfce702bcfa94e906ef82421f2c0e61c076ad76030c16ee5d2e9a32fe193"
|
||||||
|
dependencies = [
|
||||||
|
"matches",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dirs"
|
name = "dirs"
|
||||||
version = "4.0.0"
|
version = "4.0.0"
|
||||||
|
@ -474,12 +518,14 @@ dependencies = [
|
||||||
"fluent-langneg",
|
"fluent-langneg",
|
||||||
"fluent-syntax",
|
"fluent-syntax",
|
||||||
"fnv",
|
"fnv",
|
||||||
|
"im",
|
||||||
"instant",
|
"instant",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"tracing-wasm",
|
"tracing-wasm",
|
||||||
"unic-langid",
|
"unic-langid",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
|
"usvg",
|
||||||
"xi-unicode",
|
"xi-unicode",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -576,6 +622,12 @@ dependencies = [
|
||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "float-cmp"
|
||||||
|
version = "0.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75224bec9bfe1a65e2d34132933f2de7fe79900c96a0174307554244ece8150e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fluent-bundle"
|
name = "fluent-bundle"
|
||||||
version = "0.15.2"
|
version = "0.15.2"
|
||||||
|
@ -616,6 +668,17 @@ version = "1.0.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
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]]
|
[[package]]
|
||||||
name = "foreign-types"
|
name = "foreign-types"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
|
@ -951,6 +1014,20 @@ dependencies = [
|
||||||
"winapi",
|
"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]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "1.9.1"
|
version = "1.9.1"
|
||||||
|
@ -1181,6 +1258,15 @@ version = "2.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
|
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memmap2"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "723e3ebdcdc5c023db1df315364573789f8857c11b631a2fdfad7c00f5c046b4"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memmap2"
|
name = "memmap2"
|
||||||
version = "0.5.7"
|
version = "0.5.7"
|
||||||
|
@ -1355,6 +1441,26 @@ dependencies = [
|
||||||
"malloc_buf",
|
"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]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.15.0"
|
version = "1.15.0"
|
||||||
|
@ -1474,6 +1580,12 @@ dependencies = [
|
||||||
"ucd-trie",
|
"ucd-trie",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pico-args"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "piet"
|
name = "piet"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
|
@ -1657,7 +1769,7 @@ dependencies = [
|
||||||
"lexical",
|
"lexical",
|
||||||
"lexical-core",
|
"lexical-core",
|
||||||
"memchr",
|
"memchr",
|
||||||
"memmap2",
|
"memmap2 0.5.7",
|
||||||
"num",
|
"num",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"polars-arrow",
|
"polars-arrow",
|
||||||
|
@ -1818,6 +1930,15 @@ dependencies = [
|
||||||
"rand",
|
"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]]
|
[[package]]
|
||||||
name = "rayon"
|
name = "rayon"
|
||||||
version = "1.5.3"
|
version = "1.5.3"
|
||||||
|
@ -1842,6 +1963,12 @@ dependencies = [
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rctree"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "be9e29cb19c8fe84169fcb07f8f11e66bc9e6e0280efd4715c54818296f8a4a8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.2.16"
|
version = "0.2.16"
|
||||||
|
@ -1905,6 +2032,25 @@ dependencies = [
|
||||||
"syn",
|
"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]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
@ -1926,6 +2072,22 @@ version = "1.0.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8"
|
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]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.11"
|
version = "1.0.11"
|
||||||
|
@ -1999,9 +2161,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.85"
|
version = "1.0.86"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44"
|
checksum = "41feea4228a6f1cd09ec7a3593a682276702cd67b5273544757dae23c096f074"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"ryu",
|
"ryu",
|
||||||
|
@ -2053,6 +2215,31 @@ version = "0.1.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a"
|
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]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.7"
|
version = "0.4.7"
|
||||||
|
@ -2085,6 +2272,12 @@ version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "str_indices"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9d9199fa80c817e074620be84374a520062ebac833f358d74b37060ce4a0f2c0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strength_reduce"
|
name = "strength_reduce"
|
||||||
version = "0.2.3"
|
version = "0.2.3"
|
||||||
|
@ -2128,6 +2321,16 @@ dependencies = [
|
||||||
"syn",
|
"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]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "1.0.102"
|
version = "1.0.102"
|
||||||
|
@ -2315,6 +2518,18 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"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]]
|
[[package]]
|
||||||
name = "type-map"
|
name = "type-map"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
@ -2324,6 +2539,12 @@ dependencies = [
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typenum"
|
||||||
|
version = "1.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ucd-trie"
|
name = "ucd-trie"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
|
@ -2399,24 +2620,87 @@ dependencies = [
|
||||||
"unic-common",
|
"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]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd"
|
checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-script"
|
||||||
|
version = "0.5.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7d817255e1bed6dfd4ca47258685d14d2bdcfbc64fdc9e3819bd5848057b8ecc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-segmentation"
|
name = "unicode-segmentation"
|
||||||
version = "1.10.0"
|
version = "1.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a"
|
checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-vo"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-width"
|
name = "unicode-width"
|
||||||
version = "0.1.10"
|
version = "0.1.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
|
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]]
|
[[package]]
|
||||||
name = "utf16_lit"
|
name = "utf16_lit"
|
||||||
version = "2.0.2"
|
version = "2.0.2"
|
||||||
|
@ -2611,6 +2895,25 @@ dependencies = [
|
||||||
"winapi",
|
"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]]
|
[[package]]
|
||||||
name = "xi-unicode"
|
name = "xi-unicode"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
|
@ -2623,6 +2926,18 @@ version = "0.8.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
|
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]]
|
[[package]]
|
||||||
name = "yaml-rust"
|
name = "yaml-rust"
|
||||||
version = "0.4.5"
|
version = "0.4.5"
|
||||||
|
|
|
@ -11,5 +11,6 @@ syntect = "5.0.0"
|
||||||
rhai = "1.10.1"
|
rhai = "1.10.1"
|
||||||
polars = { version = "0.24.3", features = ["lazy", "rows"] }
|
polars = { version = "0.24.3", features = ["lazy", "rows"] }
|
||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = "0.3"
|
||||||
|
serde_json = "1.0.86"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ use std::{
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
};
|
};
|
||||||
|
|
||||||
use rhai::EvalAltResult;
|
use rhai::{Dynamic, EvalAltResult, EvalContext, Expression, Position};
|
||||||
|
|
||||||
pub fn setup_engine(engine: &mut rhai::Engine) {
|
pub fn setup_engine(engine: &mut rhai::Engine) {
|
||||||
//polar data frame
|
//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("dataframe", script_functions::dataframe);
|
||||||
engine.register_fn("select", DataFrame::s_select);
|
engine.register_fn("select", DataFrame::s_select);
|
||||||
engine.register_fn("load_csv", DataFrame::load_csv);
|
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
|
// polar series
|
||||||
engine.register_type::<Series>();
|
engine.register_type::<Series>();
|
||||||
engine.register_indexer_get(Series::s_get);
|
engine.register_indexer_get(Series::s_get);
|
||||||
engine.register_fn("series", script_functions::series);
|
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("head", Series::s_head);
|
||||||
engine.register_fn("+", Series::add);
|
|
||||||
engine.register_fn("sort", Series::s_sort);
|
engine.register_fn("sort", Series::s_sort);
|
||||||
engine.register_fn("sum", Series::s_sum);
|
engine.register_fn("sum", Series::s_sum);
|
||||||
engine.register_fn("add", Series::s_op_add);
|
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_i64);
|
||||||
engine.register_fn("+", script_functions::add_series_f64);
|
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_i64);
|
||||||
engine.register_fn("-", script_functions::subtract_series_f64);
|
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
|
// polar expressions
|
||||||
engine.register_type::<DataFrameExpression>();
|
engine.register_type::<DataFrameExpression>();
|
||||||
engine.register_fn("sum", DataFrameExpression::sum);
|
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("min", script_functions::min);
|
||||||
engine.register_fn("max", script_functions::max);
|
engine.register_fn("max", script_functions::max);
|
||||||
engine.register_fn("first", script_functions::first);
|
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);
|
pub struct DataFrame(polars::frame::DataFrame);
|
||||||
|
|
||||||
impl Deref for DataFrame {
|
impl Deref for DataFrame {
|
||||||
|
@ -72,6 +101,8 @@ impl DataFrame {
|
||||||
pub fn load_csv(path: &str) -> ScriptResult<DataFrame> {
|
pub fn load_csv(path: &str) -> ScriptResult<DataFrame> {
|
||||||
let df = polars::io::csv::CsvReader::from_path(path)
|
let df = polars::io::csv::CsvReader::from_path(path)
|
||||||
.map_err(|e| e.to_string())?
|
.map_err(|e| e.to_string())?
|
||||||
|
.infer_schema(Some(1))
|
||||||
|
.with_ignore_parser_errors(true)
|
||||||
.has_header(true)
|
.has_header(true)
|
||||||
.finish()
|
.finish()
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
@ -93,6 +124,12 @@ impl DataFrame {
|
||||||
.map_err(|e| e.to_string())?,
|
.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)]
|
#[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);
|
pub struct Series(polars::series::Series);
|
||||||
|
|
||||||
impl Deref for Series {
|
impl Deref for Series {
|
||||||
|
@ -242,6 +279,34 @@ mod script_functions {
|
||||||
Series(a.0 - b)
|
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 {
|
pub fn column(name: &str) -> DataFrameExpression {
|
||||||
DataFrameExpression(polars::prelude::col(name))
|
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>> {
|
pub fn series(name: &str, arr: rhai::Array) -> std::result::Result<Series, Box<EvalAltResult>> {
|
||||||
if let Some(i) = arr.first() {
|
if let Some(i) = arr.first() {
|
||||||
let series = if i.type_id() == TypeId::of::<i64>() {
|
let series = if i.type_id() == TypeId::of::<i64>() {
|
||||||
polars::series::Series::new(
|
polars::series::Series::new(
|
||||||
name,
|
name,
|
||||||
arr.into_iter()
|
arr.into_iter()
|
||||||
.map(|i| i.cast::<i64>())
|
.filter_map(|i| i.try_cast::<i64>())
|
||||||
.collect::<Vec<i64>>(),
|
.collect::<Vec<i64>>(),
|
||||||
)
|
)
|
||||||
} else if i.type_id() == TypeId::of::<f64>() {
|
} else if i.type_id() == TypeId::of::<f64>() {
|
||||||
polars::series::Series::new(
|
polars::series::Series::new(
|
||||||
name,
|
name,
|
||||||
arr.into_iter()
|
arr.into_iter()
|
||||||
.map(|i| i.cast::<f64>())
|
.filter_map(|i| i.try_cast::<f64>())
|
||||||
.collect::<Vec<f64>>(),
|
.collect::<Vec<f64>>(),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
polars::series::Series::new(
|
polars::series::Series::new(
|
||||||
name,
|
name,
|
||||||
arr.into_iter()
|
arr.into_iter()
|
||||||
.map(|i| i.cast::<String>())
|
.filter_map(|i| i.try_cast::<String>())
|
||||||
.collect::<Vec<String>>(),
|
.collect::<Vec<String>>(),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
@ -319,6 +399,90 @@ mod script_functions {
|
||||||
Ok(Series(s))
|
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)]
|
#[cfg(test)]
|
||||||
|
@ -474,4 +638,26 @@ s1 + s2
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
assert_eq!(s, vec![Some(18)]);
|
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)]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
use crate::dataframe;
|
use crate::dataframe;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Engine {
|
pub struct Engine<'a> {
|
||||||
pub blocks: Vec<Block>,
|
|
||||||
engine: rhai::Engine,
|
engine: rhai::Engine,
|
||||||
|
scope: rhai::Scope<'a>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Engine {
|
impl<'a> Default for Engine<'a> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let mut engine = rhai::Engine::new();
|
let mut engine = rhai::Engine::new();
|
||||||
engine.set_fast_operators(false);
|
engine.set_fast_operators(false);
|
||||||
|
@ -14,30 +14,28 @@ impl Default for Engine {
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
engine,
|
engine,
|
||||||
blocks: vec![Block::default()],
|
scope: rhai::Scope::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Engine {
|
impl<'a> Engine<'a> {
|
||||||
pub fn process(&mut self) {
|
pub fn process_script(&mut self, script: &str) -> Output {
|
||||||
for block in self.blocks.iter_mut() {
|
match self
|
||||||
match self.engine.eval::<rhai::Dynamic>(&block.script) {
|
.engine
|
||||||
Ok(res) if res.is::<dataframe::DataFrame>() => {
|
.eval_with_scope::<rhai::Dynamic>(&mut self.scope, script)
|
||||||
let frame = rhai::Dynamic::cast::<dataframe::DataFrame>(res);
|
{
|
||||||
block.output = Output::DataFrame(frame);
|
Ok(res) if res.is::<dataframe::DataFrame>() => {
|
||||||
}
|
let frame = rhai::Dynamic::cast::<dataframe::DataFrame>(res);
|
||||||
Ok(res) if res.is::<dataframe::Series>() => {
|
Output::DataFrame(frame)
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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),
|
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 {
|
impl Output {
|
||||||
pub fn into_frame(self) -> dataframe::DataFrame {
|
pub fn into_frame(self) -> dataframe::DataFrame {
|
||||||
if let Self::DataFrame(v) = self {
|
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)]
|
#[cfg(test)]
|
||||||
pub mod tests {
|
pub mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
pub fn process(script: &str) -> Output {
|
pub fn process(script: &str) -> Output {
|
||||||
let mut engine = Engine::default();
|
let mut engine = Engine::default();
|
||||||
engine.blocks[0].script = script.into();
|
engine.process_script(script)
|
||||||
engine.process();
|
|
||||||
engine.blocks[0].output.clone()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
mod dataframe;
|
mod dataframe;
|
||||||
mod engine;
|
mod engine;
|
||||||
|
mod save_file;
|
||||||
|
|
||||||
pub use engine::Engine;
|
pub use engine::Engine;
|
||||||
|
pub use engine::Output;
|
||||||
|
pub use polars::prelude::AnyValue;
|
||||||
|
pub use save_file::{SaveBlock, SaveFile};
|
||||||
|
|
||||||
use rhai::EvalAltResult;
|
use rhai::EvalAltResult;
|
||||||
type ScriptResult<T> = std::result::Result<T, Box<EvalAltResult>>;
|
type ScriptResult<T> = std::result::Result<T, Box<EvalAltResult>>;
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
name,age,
|
||||||
|
alice,18
|
||||||
|
sasha,22
|
||||||
|
lacey,32
|
|
|
@ -6,5 +6,8 @@ edition = "2021"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
druid = { git = "https://github.com/linebender/druid.git" }
|
druid = { git = "https://github.com/linebender/druid.git", features=["im", "svg"] }
|
||||||
abacus-core = { path = "../abacus-core" }
|
abacus-core = { path = "../abacus-core" }
|
||||||
|
syntect = "5.0.0"
|
||||||
|
ropey = "1.5.0"
|
||||||
|
clipboard = "0.5.0"
|
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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");
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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::*;
|
|
@ -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,
|
||||||
|
}
|
|
@ -1,84 +1,258 @@
|
||||||
|
use abacus_core::Output;
|
||||||
|
use clipboard::ClipboardProvider;
|
||||||
use druid::{
|
use druid::{
|
||||||
piet::{Text, TextLayout, TextLayoutBuilder},
|
piet::{CairoTextLayout, Text, TextAttribute, TextLayout, TextLayoutBuilder},
|
||||||
Color, Data, Event, FontFamily, Lens, LifeCycle, RenderContext, Widget,
|
widget::{Container, Flex, Label, Padding, Svg, SvgData},
|
||||||
|
Color, Event, FontDescriptor, FontFamily, FontWeight, LifeCycle, PaintCtx, Rect, RenderContext,
|
||||||
|
Target, Widget, WidgetExt,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Data, PartialEq, Eq)]
|
use syntect::easy::HighlightLines;
|
||||||
pub enum EditMode {
|
use syntect::highlighting::{Style, ThemeSet};
|
||||||
Normal,
|
use syntect::parsing::SyntaxSet;
|
||||||
Insert,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct AbacusEditor;
|
use crate::{
|
||||||
|
app_header::ToolbarButtonController,
|
||||||
|
data::{EditMode, EditorData},
|
||||||
|
Block,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Data, Lens, Clone)]
|
const FONT_SIZE: f64 = 16.0;
|
||||||
pub struct EditorData {
|
|
||||||
pub raw_content: String,
|
|
||||||
pub cursor_pos: usize,
|
|
||||||
pub mode: EditMode,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for EditorData {
|
mod keymap {
|
||||||
fn default() -> Self {
|
pub const LINE_ABOVE: &str = "O";
|
||||||
Self {
|
pub const LINE_BELOW: &str = "o";
|
||||||
raw_content: "let x = 1;\nx+1".to_string(),
|
pub const DELETE_LINE: &str = "dd";
|
||||||
cursor_pos: 5,
|
pub const EDIT_EOL: &str = "A";
|
||||||
mode: EditMode::Normal,
|
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 struct AbacusEditor {
|
||||||
pub fn push_str(&mut self, s: &str) {
|
syntax_set: SyntaxSet,
|
||||||
self.raw_content.push_str(s);
|
theme_set: ThemeSet,
|
||||||
self.cursor_to_end();
|
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) {
|
pub fn build_highlighted_layout(
|
||||||
self.raw_content.push(c);
|
&self,
|
||||||
self.cursor_to_end();
|
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) {
|
let mut layout = ctx
|
||||||
self.cursor_pos = self.raw_content.len();
|
.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) {
|
let mut pos = 0;
|
||||||
self.raw_content.pop();
|
for line in data.content.lines() {
|
||||||
self.cursor_to_end();
|
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 {
|
impl Widget<EditorData> for AbacusEditor {
|
||||||
fn paint(&mut self, ctx: &mut druid::PaintCtx, data: &EditorData, env: &druid::Env) {
|
fn paint(&mut self, ctx: &mut druid::PaintCtx, data: &EditorData, _env: &druid::Env) {
|
||||||
let size = ctx.size();
|
let layout = self.build_highlighted_layout(ctx, data);
|
||||||
let rect = size.to_rect();
|
|
||||||
|
|
||||||
if ctx.is_focused() {
|
if data.selection_pos.is_some() {
|
||||||
ctx.fill(rect, &Color::rgb8(10, 10, 10));
|
let rects = layout.rects_for_range(data.select_range());
|
||||||
} else {
|
for rect in rects.iter() {
|
||||||
ctx.fill(rect, &Color::rgb8(20, 20, 20));
|
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
|
if ctx.has_focus() {
|
||||||
.text()
|
self.paint_cursor(ctx, data, &layout);
|
||||||
.new_text_layout(data.raw_content.clone())
|
}
|
||||||
.font(FontFamily::MONOSPACE, 24.0)
|
|
||||||
.text_color(Color::rgb8(255, 255, 255))
|
|
||||||
.build()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
dbg!(layout.rects_for_range((data.cursor_pos - 1)..data.cursor_pos));
|
ctx.draw_text(&layout, (0.0, 0.0));
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn event(
|
fn event(
|
||||||
|
@ -86,33 +260,226 @@ impl Widget<EditorData> for AbacusEditor {
|
||||||
ctx: &mut druid::EventCtx,
|
ctx: &mut druid::EventCtx,
|
||||||
event: &druid::Event,
|
event: &druid::Event,
|
||||||
data: &mut EditorData,
|
data: &mut EditorData,
|
||||||
env: &druid::Env,
|
_env: &druid::Env,
|
||||||
) {
|
) {
|
||||||
match event {
|
match event {
|
||||||
Event::KeyUp(e) => match &e.key {
|
Event::KeyDown(e) => {
|
||||||
druid::keyboard_types::Key::Character(c) => {
|
match &e.key {
|
||||||
data.push_str(c);
|
druid::keyboard_types::Key::Character(ch) if e.mods.ctrl() => {
|
||||||
ctx.request_paint();
|
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 => {
|
ctx.request_paint();
|
||||||
data.push('\n');
|
}
|
||||||
ctx.request_paint();
|
Event::MouseDown(e) => {
|
||||||
}
|
|
||||||
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(_) => {
|
|
||||||
if !ctx.is_focused() {
|
if !ctx.is_focused() {
|
||||||
ctx.request_focus();
|
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,
|
&mut self,
|
||||||
ctx: &mut druid::LifeCycleCtx,
|
ctx: &mut druid::LifeCycleCtx,
|
||||||
event: &druid::LifeCycle,
|
event: &druid::LifeCycle,
|
||||||
data: &EditorData,
|
_data: &EditorData,
|
||||||
env: &druid::Env,
|
_env: &druid::Env,
|
||||||
) {
|
) {
|
||||||
match event {
|
match event {
|
||||||
LifeCycle::FocusChanged(_) => {
|
LifeCycle::FocusChanged(_) => {
|
||||||
|
@ -144,8 +511,12 @@ impl Widget<EditorData> for AbacusEditor {
|
||||||
ctx: &mut druid::UpdateCtx,
|
ctx: &mut druid::UpdateCtx,
|
||||||
old_data: &EditorData,
|
old_data: &EditorData,
|
||||||
data: &EditorData,
|
data: &EditorData,
|
||||||
env: &druid::Env,
|
_env: &druid::Env,
|
||||||
) {
|
) {
|
||||||
|
if old_data != data {
|
||||||
|
ctx.request_paint();
|
||||||
|
ctx.request_layout();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn layout(
|
fn layout(
|
||||||
|
@ -153,8 +524,105 @@ impl Widget<EditorData> for AbacusEditor {
|
||||||
ctx: &mut druid::LayoutCtx,
|
ctx: &mut druid::LayoutCtx,
|
||||||
bc: &druid::BoxConstraints,
|
bc: &druid::BoxConstraints,
|
||||||
data: &EditorData,
|
data: &EditorData,
|
||||||
env: &druid::Env,
|
_env: &druid::Env,
|
||||||
) -> druid::Size {
|
) -> 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)
|
||||||
|
}
|
||||||
|
|
|
@ -1,35 +1,47 @@
|
||||||
|
mod app_delegate;
|
||||||
|
mod app_header;
|
||||||
|
mod commands;
|
||||||
|
mod data;
|
||||||
mod editor;
|
mod editor;
|
||||||
|
mod modal_container;
|
||||||
|
mod output_block;
|
||||||
|
|
||||||
use druid::widget::{Align, Flex, Label, Padding, RawLabel};
|
use data::{AppData, Block};
|
||||||
use druid::{AppLauncher, Color, Data, PlatformError, Widget, WidgetExt, WindowDesc};
|
use druid::widget::{Container, Flex, List, Padding, Scroll};
|
||||||
use editor::EditorData;
|
use druid::{AppLauncher, PlatformError, Widget, WidgetExt, WindowDesc};
|
||||||
|
use modal_container::ModalContainer;
|
||||||
|
|
||||||
fn build_ui() -> impl Widget<editor::EditorData> {
|
fn build_ui() -> impl Widget<AppData> {
|
||||||
Padding::new(
|
ModalContainer::new(
|
||||||
10.0,
|
Flex::column()
|
||||||
Flex::row()
|
.with_child(app_header::app_header_ui())
|
||||||
|
.with_child(app_header::header_separater())
|
||||||
.with_flex_child(
|
.with_flex_child(
|
||||||
Flex::column().with_flex_child(editor::AbacusEditor, 1.0),
|
Scroll::new(
|
||||||
1.0,
|
List::new(|| {
|
||||||
)
|
Flex::column()
|
||||||
.with_flex_child(
|
.with_child(editor::editor_header())
|
||||||
Flex::column()
|
.with_child(Container::new(Padding::new(
|
||||||
.with_flex_child(
|
10.0,
|
||||||
Label::new("top right").background(Color::rgb8(0, 0, 255)),
|
editor::AbacusEditor::default().lens(Block::editor_data),
|
||||||
1.0,
|
)))
|
||||||
)
|
.with_child(output_block::output_block())
|
||||||
.with_flex_child(Align::centered(Label::new("bottom right")), 1.0),
|
})
|
||||||
|
.lens(AppData::blocks),
|
||||||
|
)
|
||||||
|
.vertical(),
|
||||||
1.0,
|
1.0,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<(), PlatformError> {
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Data)]
|
|
||||||
struct CodeEditor {
|
|
||||||
code: String,
|
|
||||||
}
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
|
|
@ -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(),
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 }),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|