Compare commits
12 Commits
all-fonts
...
f2556f7125
| Author | SHA1 | Date | |
|---|---|---|---|
|
f2556f7125
|
|||
| 7494dc6b75 | |||
| 43afb9dfd3 | |||
| 6b5bbfbc54 | |||
|
b39419888b
|
|||
|
4e9eacc7b0
|
|||
|
8251937be9
|
|||
|
5ca9dfabb8
|
|||
|
1df81509df
|
|||
|
7d234641cb
|
|||
| 1ed278cc55 | |||
|
2a830f0539
|
318
Cargo.lock
generated
318
Cargo.lock
generated
@ -163,6 +163,12 @@ dependencies = [
|
|||||||
"x11rb",
|
"x11rb",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arrayref"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arrayvec"
|
name = "arrayvec"
|
||||||
version = "0.7.6"
|
version = "0.7.6"
|
||||||
@ -523,6 +529,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 = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991"
|
checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "data-url"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dispatch"
|
name = "dispatch"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@ -696,6 +708,21 @@ dependencies = [
|
|||||||
"winit",
|
"winit",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "egui_extras"
|
||||||
|
version = "0.31.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "624659a2e972a46f4d5f646557906c55f1cd5a0836eddbe610fdf1afba1b4226"
|
||||||
|
dependencies = [
|
||||||
|
"ahash",
|
||||||
|
"egui",
|
||||||
|
"enum-map",
|
||||||
|
"log",
|
||||||
|
"mime_guess2",
|
||||||
|
"profiling",
|
||||||
|
"resvg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "egui_glow"
|
name = "egui_glow"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
@ -745,6 +772,27 @@ version = "1.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
|
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "enum-map"
|
||||||
|
version = "2.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9"
|
||||||
|
dependencies = [
|
||||||
|
"enum-map-derive",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "enum-map-derive"
|
||||||
|
version = "0.17.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "enumn"
|
name = "enumn"
|
||||||
version = "0.1.14"
|
version = "0.1.14"
|
||||||
@ -853,6 +901,12 @@ dependencies = [
|
|||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "float-cmp"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "foldhash"
|
name = "foldhash"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@ -1326,6 +1380,12 @@ dependencies = [
|
|||||||
"tiff",
|
"tiff",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "imagesize"
|
||||||
|
version = "0.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indenter"
|
name = "indenter"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
@ -1344,11 +1404,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "inkr"
|
name = "inkr"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"eframe",
|
"eframe",
|
||||||
"egui",
|
"egui",
|
||||||
|
"egui_extras",
|
||||||
"egui_glow 0.31.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"egui_glow 0.31.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"eyre",
|
"eyre",
|
||||||
@ -1356,7 +1417,7 @@ dependencies = [
|
|||||||
"half",
|
"half",
|
||||||
"insta",
|
"insta",
|
||||||
"log",
|
"log",
|
||||||
"rand",
|
"rand 0.9.1",
|
||||||
"rfd",
|
"rfd",
|
||||||
"serde",
|
"serde",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
@ -1471,6 +1532,15 @@ version = "3.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
|
checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "kurbo"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bd85a5776cd9500c2e2059c8c76c3b01528566b7fcbaf8098b55a33fc298849b"
|
||||||
|
dependencies = [
|
||||||
|
"arrayvec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.172"
|
version = "0.2.172"
|
||||||
@ -1580,6 +1650,24 @@ dependencies = [
|
|||||||
"paste",
|
"paste",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mime"
|
||||||
|
version = "0.3.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mime_guess2"
|
||||||
|
version = "2.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1706dc14a2e140dec0a7a07109d9a3d5890b81e85bd6c60b906b249a77adf0ca"
|
||||||
|
dependencies = [
|
||||||
|
"mime",
|
||||||
|
"phf",
|
||||||
|
"phf_shared",
|
||||||
|
"unicase",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.8"
|
version = "0.8.8"
|
||||||
@ -2047,6 +2135,56 @@ version = "2.3.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf"
|
||||||
|
version = "0.11.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
|
||||||
|
dependencies = [
|
||||||
|
"phf_macros",
|
||||||
|
"phf_shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_generator"
|
||||||
|
version = "0.11.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
|
||||||
|
dependencies = [
|
||||||
|
"phf_shared",
|
||||||
|
"rand 0.8.5",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_macros"
|
||||||
|
version = "0.11.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
|
||||||
|
dependencies = [
|
||||||
|
"phf_generator",
|
||||||
|
"phf_shared",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"unicase",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_shared"
|
||||||
|
version = "0.11.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
|
||||||
|
dependencies = [
|
||||||
|
"siphasher 1.0.1",
|
||||||
|
"unicase",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pico-args"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project"
|
name = "pin-project"
|
||||||
version = "1.1.10"
|
version = "1.1.10"
|
||||||
@ -2179,6 +2317,15 @@ version = "5.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
|
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.8.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||||
|
dependencies = [
|
||||||
|
"rand_core 0.6.4",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.9.1"
|
version = "0.9.1"
|
||||||
@ -2186,7 +2333,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
|
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rand_chacha",
|
"rand_chacha",
|
||||||
"rand_core",
|
"rand_core 0.9.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2196,9 +2343,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ppv-lite86",
|
"ppv-lite86",
|
||||||
"rand_core",
|
"rand_core 0.9.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.6.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_core"
|
name = "rand_core"
|
||||||
version = "0.9.3"
|
version = "0.9.3"
|
||||||
@ -2214,6 +2367,12 @@ version = "0.6.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rctree"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3b42e27ef78c35d3998403c1d26f3efd9e135d3e5121b0a4845cc5cc27547f4f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@ -2267,6 +2426,20 @@ 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 = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
|
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "resvg"
|
||||||
|
version = "0.37.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cadccb3d99a9efb8e5e00c16fbb732cbe400db2ec7fc004697ee7d97d86cf1f4"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"pico-args",
|
||||||
|
"rgb",
|
||||||
|
"svgtypes",
|
||||||
|
"tiny-skia",
|
||||||
|
"usvg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rfd"
|
name = "rfd"
|
||||||
version = "0.15.3"
|
version = "0.15.3"
|
||||||
@ -2291,6 +2464,15 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rgb"
|
||||||
|
version = "0.8.50"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a"
|
||||||
|
dependencies = [
|
||||||
|
"bytemuck",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ron"
|
name = "ron"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
@ -2303,6 +2485,12 @@ dependencies = [
|
|||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "roxmltree"
|
||||||
|
version = "0.19.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@ -2396,6 +2584,27 @@ version = "2.7.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
|
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simplecss"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "siphasher"
|
||||||
|
version = "0.3.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "siphasher"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.9"
|
version = "0.4.9"
|
||||||
@ -2486,6 +2695,15 @@ 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 = "strict-num"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
|
||||||
|
dependencies = [
|
||||||
|
"float-cmp",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strum"
|
name = "strum"
|
||||||
version = "0.26.3"
|
version = "0.26.3"
|
||||||
@ -2508,6 +2726,16 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "svgtypes"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6e44e288cd960318917cbd540340968b90becc8bc81f171345d706e7a89d9d70"
|
||||||
|
dependencies = [
|
||||||
|
"kurbo",
|
||||||
|
"siphasher 0.3.11",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.100"
|
version = "2.0.100"
|
||||||
@ -2609,6 +2837,32 @@ dependencies = [
|
|||||||
"weezl",
|
"weezl",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tiny-skia"
|
||||||
|
version = "0.11.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab"
|
||||||
|
dependencies = [
|
||||||
|
"arrayref",
|
||||||
|
"arrayvec",
|
||||||
|
"bytemuck",
|
||||||
|
"cfg-if",
|
||||||
|
"log",
|
||||||
|
"png",
|
||||||
|
"tiny-skia-path",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tiny-skia-path"
|
||||||
|
version = "0.11.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93"
|
||||||
|
dependencies = [
|
||||||
|
"arrayref",
|
||||||
|
"bytemuck",
|
||||||
|
"strict-num",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinystr"
|
name = "tinystr"
|
||||||
version = "0.7.6"
|
version = "0.7.6"
|
||||||
@ -2684,6 +2938,12 @@ dependencies = [
|
|||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicase"
|
||||||
|
version = "2.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.18"
|
version = "1.0.18"
|
||||||
@ -2719,6 +2979,50 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "usvg"
|
||||||
|
version = "0.37.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "38b0a51b72ab80ca511d126b77feeeb4fb1e972764653e61feac30adc161a756"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.21.7",
|
||||||
|
"log",
|
||||||
|
"pico-args",
|
||||||
|
"usvg-parser",
|
||||||
|
"usvg-tree",
|
||||||
|
"xmlwriter",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "usvg-parser"
|
||||||
|
version = "0.37.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9bd4e3c291f45d152929a31f0f6c819245e2921bfd01e7bd91201a9af39a2bdc"
|
||||||
|
dependencies = [
|
||||||
|
"data-url",
|
||||||
|
"flate2",
|
||||||
|
"imagesize",
|
||||||
|
"kurbo",
|
||||||
|
"log",
|
||||||
|
"roxmltree",
|
||||||
|
"simplecss",
|
||||||
|
"siphasher 0.3.11",
|
||||||
|
"svgtypes",
|
||||||
|
"usvg-tree",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "usvg-tree"
|
||||||
|
version = "0.37.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ee3d202ebdb97a6215604b8f5b4d6ef9024efd623cf2e373a6416ba976ec7d3"
|
||||||
|
dependencies = [
|
||||||
|
"rctree",
|
||||||
|
"strict-num",
|
||||||
|
"svgtypes",
|
||||||
|
"tiny-skia-path",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf16_iter"
|
name = "utf16_iter"
|
||||||
version = "1.0.5"
|
version = "1.0.5"
|
||||||
@ -3525,6 +3829,12 @@ version = "0.8.26"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda"
|
checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xmlwriter"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yoke"
|
name = "yoke"
|
||||||
version = "0.7.5"
|
version = "0.7.5"
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "inkr"
|
name = "inkr"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
authors = []
|
authors = []
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
@ -13,6 +13,7 @@ pinenote = []
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
egui = "0.31"
|
egui = "0.31"
|
||||||
|
egui_extras = { version = "0.31", features = ["svg"] }
|
||||||
eframe = { version = "0.31", default-features = false, features = [
|
eframe = { version = "0.31", default-features = false, features = [
|
||||||
"glow", # alt: "wgpu".
|
"glow", # alt: "wgpu".
|
||||||
"persistence",
|
"persistence",
|
||||||
|
|||||||
27
PKGBUILD
Normal file
27
PKGBUILD
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
pkgname=inkr
|
||||||
|
pkgver=1.0.0
|
||||||
|
pkgrel=1
|
||||||
|
pkgdesc="A note-taking and handwriting tool"
|
||||||
|
arch=('x86_64' 'aarch64')
|
||||||
|
url="https://git.nubo.sh/hulthe/inkr"
|
||||||
|
#license=('GPL')
|
||||||
|
groups=('base-devel')
|
||||||
|
depends=('glibc')
|
||||||
|
makedepends=('cargo')
|
||||||
|
#optdepends=('ed: for "patch -e" functionality')
|
||||||
|
#source=(" ftp://ftp.gnu.org/gnu/$pkgname/$pkgname-$pkgver.tar.xz"{,.sig})
|
||||||
|
#sha256sums=('SKIP')
|
||||||
|
prepare() {
|
||||||
|
export RUSTUP_TOOLCHAIN=stable
|
||||||
|
cargo fetch --locked --target "$(rustc -vV | sed -n 's/host: //p')"
|
||||||
|
}
|
||||||
|
build() {
|
||||||
|
export RUSTUP_TOOLCHAIN=stable
|
||||||
|
cargo build --frozen --release
|
||||||
|
}
|
||||||
|
package() {
|
||||||
|
cd ..
|
||||||
|
install -Dm0755 -t "$pkgdir/usr/bin/" "${CARGO_TARGET_DIR:-target}/release/$pkgname"
|
||||||
|
install -Dm0755 -t "$pkgdir/usr/share/applications/" "assets/$pkgname.desktop"
|
||||||
|
install -Dm0755 "assets/icon.svg" "$pkgdir/usr/share/pixmaps/$pkgname.svg"
|
||||||
|
}
|
||||||
73
assets/collapse-icon.svg
Normal file
73
assets/collapse-icon.svg
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="256"
|
||||||
|
height="256"
|
||||||
|
viewBox="0 0 67.733332 67.733332"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.4.2 (ebf0e940, 2025-05-08)"
|
||||||
|
sodipodi:docname="collapse-icon.svg"
|
||||||
|
inkscape:export-filename="collapse-icon.png"
|
||||||
|
inkscape:export-xdpi="96"
|
||||||
|
inkscape:export-ydpi="96"
|
||||||
|
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">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#505050"
|
||||||
|
bordercolor="#eeeeee"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:showpageshadow="0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#505050"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:zoom="2.0010679"
|
||||||
|
inkscape:cx="97.198102"
|
||||||
|
inkscape:cy="140.42502"
|
||||||
|
inkscape:window-width="1472"
|
||||||
|
inkscape:window-height="815"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="38"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Lager 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<rect
|
||||||
|
style="fill:#000000;fill-opacity:0;stroke:#ffffff;stroke-width:4.92907;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect1"
|
||||||
|
width="62.804268"
|
||||||
|
height="52.220932"
|
||||||
|
x="2.4645352"
|
||||||
|
y="7.7562032"
|
||||||
|
ry="6.9627905" />
|
||||||
|
<path
|
||||||
|
style="fill:#000000;fill-opacity:0;stroke:#ffffff;stroke-width:4.92628;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 28.921473,6.35 V 61.383333"
|
||||||
|
id="path1" />
|
||||||
|
<path
|
||||||
|
style="fill:#000000;fill-opacity:0;stroke:#ffffff;stroke-width:3.12398;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 22.250504,15.411171 9.4994923,15.320325"
|
||||||
|
id="path1-7" />
|
||||||
|
<path
|
||||||
|
style="fill:#000000;fill-opacity:0;stroke:#ffffff;stroke-width:3.12398;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 22.250506,21.743139 9.4994938,21.652293"
|
||||||
|
id="path1-7-4" />
|
||||||
|
<path
|
||||||
|
style="fill:#000000;fill-opacity:0;stroke:#ffffff;stroke-width:3.12398;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 22.250506,28.075107 9.4994938,27.984261"
|
||||||
|
id="path1-7-5" />
|
||||||
|
<path
|
||||||
|
style="fill:#000000;fill-opacity:0;stroke:#ffffff;stroke-width:3.12398;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 22.250508,34.407075 9.4994958,34.316229"
|
||||||
|
id="path1-7-4-4" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.8 KiB |
9
assets/inkr.desktop
Executable file
9
assets/inkr.desktop
Executable file
@ -0,0 +1,9 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Name=inkr
|
||||||
|
Exec=inkr
|
||||||
|
Terminal=false
|
||||||
|
Type=Application
|
||||||
|
Icon=inkr
|
||||||
|
StartupWMClass=inkr
|
||||||
|
MimeType=x-scheme-handler/inkr;
|
||||||
|
Categories=Office;
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
244
src/app.rs
244
src/app.rs
@ -2,11 +2,14 @@ use std::{
|
|||||||
fs,
|
fs,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::{Arc, mpsc},
|
sync::{Arc, mpsc},
|
||||||
|
thread::JoinHandle,
|
||||||
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{file_editor::FileEditor, preferences::Preferences, util::GuiSender};
|
use crate::{file_editor::FileEditor, folder::Folder, preferences::Preferences, util::GuiSender};
|
||||||
use egui::{
|
use egui::{
|
||||||
Align, Button, Color32, FontData, FontDefinitions, PointerButton, RichText, ScrollArea, Stroke,
|
Align, Button, Color32, Context, FontData, FontDefinitions, Image, Key, Modifiers,
|
||||||
|
PointerButton, RichText, ScrollArea, Stroke, Widget, include_image,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(serde::Deserialize, serde::Serialize)]
|
#[derive(serde::Deserialize, serde::Serialize)]
|
||||||
@ -18,13 +21,46 @@ pub struct App {
|
|||||||
actions_tx: mpsc::Sender<Action>,
|
actions_tx: mpsc::Sender<Action>,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
actions_rx: mpsc::Receiver<Action>,
|
actions_rx: mpsc::Receiver<Action>,
|
||||||
|
#[serde(skip)]
|
||||||
|
jobs: Jobs,
|
||||||
|
|
||||||
tabs: Vec<(TabId, Tab)>,
|
tabs: Vec<(TabId, Tab)>,
|
||||||
|
|
||||||
|
show_folders: bool,
|
||||||
|
folders: Vec<Folder>,
|
||||||
|
|
||||||
open_tab_index: Option<usize>,
|
open_tab_index: Option<usize>,
|
||||||
|
|
||||||
next_tab_id: TabId,
|
next_tab_id: TabId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct Jobs {
|
||||||
|
handles: Vec<JoinHandle<()>>,
|
||||||
|
actions_tx: mpsc::Sender<Action>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Jobs {
|
||||||
|
fn start(&mut self, ctx: &Context, job: impl FnOnce() -> Option<Action> + Send + 'static) {
|
||||||
|
let ctx = ctx.clone();
|
||||||
|
let actions_tx = self.actions_tx.clone();
|
||||||
|
self.handles.push(std::thread::spawn(move || {
|
||||||
|
// start rendering the spinner thingy
|
||||||
|
ctx.request_repaint();
|
||||||
|
|
||||||
|
let start = Instant::now();
|
||||||
|
|
||||||
|
if let Some(action) = job() {
|
||||||
|
let _ = actions_tx.send(action);
|
||||||
|
ctx.request_repaint();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make sure that task takes at least 250ms to run, so that the spinner won't blink
|
||||||
|
let sleep_for = Duration::from_millis(250).saturating_sub(start.elapsed());
|
||||||
|
std::thread::sleep(sleep_for);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize, serde::Serialize)]
|
#[derive(serde::Deserialize, serde::Serialize)]
|
||||||
enum Tab {
|
enum Tab {
|
||||||
File(FileEditor),
|
File(FileEditor),
|
||||||
@ -36,12 +72,19 @@ impl Tab {
|
|||||||
Tab::File(file_editor) => file_editor.title(),
|
Tab::File(file_editor) => file_editor.title(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_dirty(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Tab::File(file_editor) => file_editor.is_dirty,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type TabId = usize;
|
pub type TabId = usize;
|
||||||
|
|
||||||
pub enum Action {
|
pub enum Action {
|
||||||
OpenFile(FileEditor),
|
OpenFile(FileEditor),
|
||||||
|
OpenFolder(Folder),
|
||||||
MoveFile(TabId, PathBuf),
|
MoveFile(TabId, PathBuf),
|
||||||
CloseTab(TabId),
|
CloseTab(TabId),
|
||||||
// TODO
|
// TODO
|
||||||
@ -55,11 +98,17 @@ impl Default for App {
|
|||||||
let (actions_tx, actions_rx) = mpsc::channel();
|
let (actions_tx, actions_rx) = mpsc::channel();
|
||||||
Self {
|
Self {
|
||||||
preferences: Preferences::default(),
|
preferences: Preferences::default(),
|
||||||
actions_tx,
|
actions_tx: actions_tx.clone(/* this is silly, i know */),
|
||||||
actions_rx,
|
actions_rx,
|
||||||
|
jobs: Jobs {
|
||||||
|
handles: Default::default(),
|
||||||
|
actions_tx,
|
||||||
|
},
|
||||||
tabs: vec![(1, Tab::File(FileEditor::new("note.md")))],
|
tabs: vec![(1, Tab::File(FileEditor::new("note.md")))],
|
||||||
open_tab_index: None,
|
open_tab_index: None,
|
||||||
next_tab_id: 2,
|
next_tab_id: 2,
|
||||||
|
show_folders: false,
|
||||||
|
folders: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -139,6 +188,9 @@ impl App {
|
|||||||
Stroke::new(1.0, Color32::from_rgb(200, 200, 200));
|
Stroke::new(1.0, Color32::from_rgb(200, 200, 200));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// enable features on egui_extras to add more image types
|
||||||
|
egui_extras::install_image_loaders(&cc.egui_ctx);
|
||||||
|
|
||||||
if let Some(storage) = cc.storage {
|
if let Some(storage) = cc.storage {
|
||||||
return eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default();
|
return eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default();
|
||||||
}
|
}
|
||||||
@ -146,12 +198,24 @@ impl App {
|
|||||||
Default::default()
|
Default::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn actions_tx(&self, ctx: &egui::Context) -> GuiSender<Action> {
|
fn actions_tx(&self, ctx: &Context) -> GuiSender<Action> {
|
||||||
GuiSender::new(self.actions_tx.clone(), ctx)
|
GuiSender::new(self.actions_tx.clone(), ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_action(&mut self, action: Action) {
|
fn handle_action(&mut self, action: Action) {
|
||||||
match action {
|
match action {
|
||||||
|
Action::OpenFolder(new_folder) => {
|
||||||
|
if let Some(folder) = self
|
||||||
|
.folders
|
||||||
|
.iter_mut()
|
||||||
|
.find(|folder| folder.path() == new_folder.path())
|
||||||
|
{
|
||||||
|
*folder = new_folder;
|
||||||
|
} else {
|
||||||
|
self.folders.push(new_folder);
|
||||||
|
self.folders.sort_by(|a, b| a.name().cmp(b.name()));
|
||||||
|
}
|
||||||
|
}
|
||||||
Action::OpenFile(file_editor) => {
|
Action::OpenFile(file_editor) => {
|
||||||
self.open_tab(Tab::File(file_editor));
|
self.open_tab(Tab::File(file_editor));
|
||||||
}
|
}
|
||||||
@ -175,9 +239,11 @@ impl eframe::App for App {
|
|||||||
eframe::set_value(storage, eframe::APP_KEY, self);
|
eframe::set_value(storage, eframe::APP_KEY, self);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) {
|
||||||
self.preferences.apply(ctx);
|
self.preferences.apply(ctx);
|
||||||
|
|
||||||
|
self.jobs.handles.retain(|job| !job.is_finished());
|
||||||
|
|
||||||
while let Ok(action) = self.actions_rx.try_recv() {
|
while let Ok(action) = self.actions_rx.try_recv() {
|
||||||
self.handle_action(action);
|
self.handle_action(action);
|
||||||
}
|
}
|
||||||
@ -186,11 +252,11 @@ impl eframe::App for App {
|
|||||||
self.open_tab_index = Some(self.tabs.len().saturating_sub(1));
|
self.open_tab_index = Some(self.tabs.len().saturating_sub(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
//ctx.input_mut(|input| {
|
ctx.input_mut(|input| {
|
||||||
// if input.consume_key(Modifiers::CTRL, Key::H) {
|
if input.consume_key(Modifiers::CTRL, Key::S) {
|
||||||
// self.buffer.push(BufferItem::Painting(Default::default()));
|
self.save_active_tab(ctx);
|
||||||
// }
|
}
|
||||||
//});
|
});
|
||||||
|
|
||||||
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
|
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
|
||||||
egui::containers::menu::Bar::new().ui(ui, |ui| {
|
egui::containers::menu::Bar::new().ui(ui, |ui| {
|
||||||
@ -205,27 +271,25 @@ impl eframe::App for App {
|
|||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
if ui.button("Open File").clicked() {
|
if ui.button("Open File").clicked() {
|
||||||
let actions_tx = self.actions_tx(ui.ctx());
|
self.jobs.start(ui.ctx(), move || {
|
||||||
std::thread::spawn(move || {
|
let file_path = rfd::FileDialog::new().pick_file()?;
|
||||||
let file = rfd::FileDialog::new().pick_file();
|
|
||||||
|
|
||||||
let Some(file_path) = file else { return };
|
let text = fs::read_to_string(&file_path)
|
||||||
|
.inspect_err(|e| log::error!("Failed to read {file_path:?}: {e}"))
|
||||||
let text = match fs::read_to_string(&file_path) {
|
.ok()?;
|
||||||
Ok(text) => text,
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("Failed to read {file_path:?}: {e}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let editor = FileEditor::from_file(file_path, &text);
|
let editor = FileEditor::from_file(file_path, &text);
|
||||||
let _ = actions_tx.send(Action::OpenFile(editor));
|
Some(Action::OpenFile(editor))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if ui.button("Open Folder").clicked() {
|
if ui.button("Open Folder").clicked() {
|
||||||
log::error!("Open Folder not implemented");
|
self.jobs.start(ui.ctx(), move || {
|
||||||
|
let path = rfd::FileDialog::new().pick_folder()?;
|
||||||
|
let name = path.file_name()?.to_string_lossy().to_string();
|
||||||
|
let folder = Folder::NotLoaded { name, path };
|
||||||
|
Some(Action::OpenFolder(folder))
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if ui
|
if ui
|
||||||
@ -237,52 +301,41 @@ impl eframe::App for App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let open_file =
|
let can_save_file = self
|
||||||
self.open_tab_index
|
.open_tab_index
|
||||||
.and_then(|i| self.tabs.get(i))
|
.and_then(|i| self.tabs.get(i))
|
||||||
.and_then(|(id, tab)| match tab {
|
.map(|(id, tab)| match tab {
|
||||||
Tab::File(file_editor) => Some((*id, file_editor)),
|
Tab::File(file_editor) => (*id, file_editor),
|
||||||
});
|
})
|
||||||
|
.and_then(|(_, file_editor)| file_editor.path().zip(Some(file_editor)))
|
||||||
|
.is_some();
|
||||||
|
|
||||||
let open_file_with_path = open_file
|
if ui.add_enabled(can_save_file, Button::new("Save")).clicked() {
|
||||||
.clone()
|
self.save_active_tab(ui.ctx());
|
||||||
.and_then(|(_, file_editor)| file_editor.path().zip(Some(file_editor)));
|
|
||||||
|
|
||||||
if ui
|
|
||||||
.add_enabled(open_file_with_path.is_some(), Button::new("Save"))
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
if let Some((file_path, file_editor)) = open_file_with_path {
|
|
||||||
let text = file_editor.to_string();
|
|
||||||
let file_path = file_path.to_owned();
|
|
||||||
std::thread::spawn(move || {
|
|
||||||
if let Err(e) = fs::write(file_path, text.as_bytes()) {
|
|
||||||
log::error!("{e}");
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let open_file = self.open_tab_index.and_then(|i| self.tabs.get(i)).map(
|
||||||
|
|(id, tab)| match tab {
|
||||||
|
Tab::File(file_editor) => (*id, file_editor),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
if ui
|
if ui
|
||||||
.add_enabled(open_file.is_some(), Button::new("Save As"))
|
.add_enabled(open_file.is_some(), Button::new("Save As"))
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
let actions_tx = self.actions_tx(ui.ctx());
|
|
||||||
let (tab_id, editor) =
|
let (tab_id, editor) =
|
||||||
open_file.expect("We checked that open_file is_some");
|
open_file.expect("We checked that open_file is_some");
|
||||||
let text = editor.to_string();
|
let text = editor.to_string();
|
||||||
std::thread::spawn(move || {
|
self.jobs.start(ui.ctx(), move || {
|
||||||
let Some(file_path) = rfd::FileDialog::new().save_file() else {
|
let file_path = rfd::FileDialog::new().save_file()?;
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(e) = fs::write(&file_path, text.as_bytes()) {
|
fs::write(&file_path, text.as_bytes())
|
||||||
log::error!("{e}");
|
.inspect_err(|e| log::error!("{e}"))
|
||||||
return;
|
.ok()?;
|
||||||
};
|
|
||||||
|
|
||||||
let _ = actions_tx.send(Action::MoveFile(tab_id, file_path));
|
Some(Action::MoveFile(tab_id, file_path))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -297,7 +350,18 @@ impl eframe::App for App {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.add_space(16.0);
|
ui.add_space(8.0);
|
||||||
|
|
||||||
|
let image = Image::new(include_image!("../assets/collapse-icon.svg"));
|
||||||
|
let image = image.tint(ui.style().visuals.text_color());
|
||||||
|
if Button::image(image).ui(ui).clicked() {
|
||||||
|
self.show_folders = !self.show_folders;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.jobs.handles.is_empty() {
|
||||||
|
ui.add_space(8.0);
|
||||||
|
ui.spinner();
|
||||||
|
}
|
||||||
|
|
||||||
ui.add_space(16.0);
|
ui.add_space(16.0);
|
||||||
|
|
||||||
@ -306,8 +370,7 @@ impl eframe::App for App {
|
|||||||
let selected = self.open_tab_index == Some(i);
|
let selected = self.open_tab_index == Some(i);
|
||||||
let mut button = Button::new(tab.title()).selected(selected);
|
let mut button = Button::new(tab.title()).selected(selected);
|
||||||
|
|
||||||
let dirty = i == 0; // TODO: mark as dirty when contents hasn't been saved
|
if tab.is_dirty() {
|
||||||
if dirty {
|
|
||||||
button = button.right_text(RichText::new("*").strong())
|
button = button.right_text(RichText::new("*").strong())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -323,6 +386,39 @@ impl eframe::App for App {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
egui::SidePanel::left("file browser")
|
||||||
|
.resizable(true)
|
||||||
|
.show_animated(ctx, self.show_folders, |ui| {
|
||||||
|
if ui.button("refresh").clicked() {
|
||||||
|
for folder in &mut self.folders {
|
||||||
|
folder.unload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollArea::both().auto_shrink(false).show(ui, |ui| {
|
||||||
|
self.folders.retain_mut(|folder| {
|
||||||
|
let response = folder.show(ui);
|
||||||
|
|
||||||
|
if let Some(file_path) = response.open_file {
|
||||||
|
let file_path = file_path.to_owned();
|
||||||
|
self.jobs.start(ui.ctx(), move || {
|
||||||
|
let text = fs::read_to_string(&file_path)
|
||||||
|
.inspect_err(|e| {
|
||||||
|
log::error!("Failed to read {file_path:?}: {e}")
|
||||||
|
})
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
let editor = FileEditor::from_file(file_path, &text);
|
||||||
|
Some(Action::OpenFile(editor))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete on right-click
|
||||||
|
!response.clicked_by(PointerButton::Secondary)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
if let Some(Tab::File(file_editor)) = self
|
if let Some(Tab::File(file_editor)) = self
|
||||||
.open_tab_index
|
.open_tab_index
|
||||||
@ -354,5 +450,33 @@ impl App {
|
|||||||
let id = self.next_tab_id;
|
let id = self.next_tab_id;
|
||||||
self.next_tab_id += 1;
|
self.next_tab_id += 1;
|
||||||
self.tabs.insert(i, (id, tab));
|
self.tabs.insert(i, (id, tab));
|
||||||
|
self.open_tab_index = Some(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_active_tab(&mut self, ctx: &Context) {
|
||||||
|
let open_file = self
|
||||||
|
.open_tab_index
|
||||||
|
.and_then(|i| self.tabs.get_mut(i))
|
||||||
|
.map(|(id, tab)| match tab {
|
||||||
|
Tab::File(file_editor) => (*id, file_editor),
|
||||||
|
})
|
||||||
|
.and_then(|(_, file_editor)| {
|
||||||
|
file_editor
|
||||||
|
.path()
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
.zip(Some(file_editor))
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some((file_path, file_editor)) = open_file {
|
||||||
|
file_editor.is_dirty = false;
|
||||||
|
let text = file_editor.to_string();
|
||||||
|
let file_path = file_path.to_owned();
|
||||||
|
self.jobs.start(ctx, move || {
|
||||||
|
if let Err(e) = fs::write(file_path, text.as_bytes()) {
|
||||||
|
log::error!("{e}");
|
||||||
|
};
|
||||||
|
None
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ use egui::{
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
custom_code_block::{MdItem, iter_lines_and_code_blocks},
|
custom_code_block::{MdItem, iter_lines_and_code_blocks},
|
||||||
painting::{self, Handwriting, HandwritingStyle},
|
handwriting::{self, Handwriting, HandwritingStyle},
|
||||||
preferences::Preferences,
|
preferences::Preferences,
|
||||||
text_editor::MdTextEdit,
|
text_editor::MdTextEdit,
|
||||||
};
|
};
|
||||||
@ -22,12 +22,15 @@ pub struct FileEditor {
|
|||||||
title: String,
|
title: String,
|
||||||
pub path: Option<PathBuf>,
|
pub path: Option<PathBuf>,
|
||||||
pub buffer: Vec<BufferItem>,
|
pub buffer: Vec<BufferItem>,
|
||||||
|
|
||||||
|
/// Whether the file has been edited since it was laste saved to disk.
|
||||||
|
pub is_dirty: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize, serde::Serialize)]
|
#[derive(serde::Deserialize, serde::Serialize)]
|
||||||
pub enum BufferItem {
|
pub enum BufferItem {
|
||||||
Text(MdTextEdit),
|
Text(MdTextEdit),
|
||||||
Handwriting(Handwriting),
|
Handwriting(Box<Handwriting>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileEditor {
|
impl FileEditor {
|
||||||
@ -37,6 +40,7 @@ impl FileEditor {
|
|||||||
title: title.into(),
|
title: title.into(),
|
||||||
path: None,
|
path: None,
|
||||||
buffer,
|
buffer,
|
||||||
|
is_dirty: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,9 +73,11 @@ impl FileEditor {
|
|||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label("new");
|
ui.label("new");
|
||||||
if ui.button("text").clicked() {
|
if ui.button("text").clicked() {
|
||||||
|
self.is_dirty = true;
|
||||||
self.buffer.push(BufferItem::Text(Default::default()));
|
self.buffer.push(BufferItem::Text(Default::default()));
|
||||||
}
|
}
|
||||||
if ui.button("writing").clicked() {
|
if ui.button("writing").clicked() {
|
||||||
|
self.is_dirty = true;
|
||||||
self.buffer
|
self.buffer
|
||||||
.push(BufferItem::Handwriting(Default::default()));
|
.push(BufferItem::Handwriting(Default::default()));
|
||||||
}
|
}
|
||||||
@ -138,14 +144,18 @@ impl FileEditor {
|
|||||||
|
|
||||||
let item_response = ui.allocate_ui(item_size, |ui| match item {
|
let item_response = ui.allocate_ui(item_size, |ui| match item {
|
||||||
BufferItem::Text(text_edit) => {
|
BufferItem::Text(text_edit) => {
|
||||||
text_edit.ui(ui);
|
if text_edit.ui(ui).changed {
|
||||||
|
self.is_dirty = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
BufferItem::Handwriting(painting) => {
|
BufferItem::Handwriting(handwriting) => {
|
||||||
let style = HandwritingStyle {
|
let style = HandwritingStyle {
|
||||||
animate: preferences.animations,
|
animate: preferences.animations,
|
||||||
..HandwritingStyle::from_theme(ui.ctx().theme())
|
..HandwritingStyle::from_theme(ui.ctx().theme())
|
||||||
};
|
};
|
||||||
painting.ui(&style, ui);
|
if handwriting.ui(&style, ui).changed {
|
||||||
|
self.is_dirty = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -211,10 +221,12 @@ impl FileEditor {
|
|||||||
Ordering::Greater => {
|
Ordering::Greater => {
|
||||||
let item = self.buffer.remove(from);
|
let item = self.buffer.remove(from);
|
||||||
self.buffer.insert(to, item);
|
self.buffer.insert(to, item);
|
||||||
|
self.is_dirty = true;
|
||||||
}
|
}
|
||||||
Ordering::Less => {
|
Ordering::Less => {
|
||||||
let item = self.buffer.remove(from);
|
let item = self.buffer.remove(from);
|
||||||
self.buffer.insert(to - 1, item);
|
self.buffer.insert(to - 1, item);
|
||||||
|
self.is_dirty = true;
|
||||||
}
|
}
|
||||||
Ordering::Equal => {}
|
Ordering::Equal => {}
|
||||||
}
|
}
|
||||||
@ -275,7 +287,7 @@ impl From<&str> for FileEditor {
|
|||||||
match item {
|
match item {
|
||||||
MdItem::Line(line) => push_text(buffer, line),
|
MdItem::Line(line) => push_text(buffer, line),
|
||||||
MdItem::CodeBlock { key, content, span } => match key {
|
MdItem::CodeBlock { key, content, span } => match key {
|
||||||
painting::CODE_BLOCK_KEY => match Handwriting::from_str(span) {
|
handwriting::CODE_BLOCK_KEY => match Handwriting::from_str(span) {
|
||||||
Ok(handwriting) => {
|
Ok(handwriting) => {
|
||||||
if let Some(BufferItem::Text(text_edit)) = buffer.last_mut() {
|
if let Some(BufferItem::Text(text_edit)) = buffer.last_mut() {
|
||||||
if text_edit.text.ends_with('\n') {
|
if text_edit.text.ends_with('\n') {
|
||||||
@ -285,7 +297,7 @@ impl From<&str> for FileEditor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
buffer.push(BufferItem::Handwriting(handwriting))
|
buffer.push(BufferItem::Handwriting(Box::new(handwriting)))
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to decode handwriting {content:?}: {e}");
|
log::error!("Failed to decode handwriting {content:?}: {e}");
|
||||||
|
|||||||
218
src/folder.rs
Normal file
218
src/folder.rs
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
use std::{
|
||||||
|
fs::read_dir,
|
||||||
|
mem,
|
||||||
|
ops::Deref,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
sync::mpsc,
|
||||||
|
thread,
|
||||||
|
};
|
||||||
|
|
||||||
|
use egui::{Response, Ui};
|
||||||
|
use eyre::{Context, OptionExt, eyre};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub enum Folder {
|
||||||
|
NotLoaded {
|
||||||
|
name: String,
|
||||||
|
path: PathBuf,
|
||||||
|
},
|
||||||
|
Loading {
|
||||||
|
name: String,
|
||||||
|
path: PathBuf,
|
||||||
|
recv: mpsc::Receiver<LoadedFolder>,
|
||||||
|
},
|
||||||
|
Loaded(LoadedFolder),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LoadedFolder {
|
||||||
|
pub name: String,
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub child_folders: Vec<Folder>,
|
||||||
|
pub child_files: Vec<File>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct File {
|
||||||
|
pub name: String,
|
||||||
|
pub path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FolderResponse<'a> {
|
||||||
|
inner: Response,
|
||||||
|
pub open_file: Option<&'a Path>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for FolderResponse<'_> {
|
||||||
|
type Target = Response;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoadedFolder {
|
||||||
|
pub fn show<'a>(&'a mut self, ui: &mut Ui) -> FolderResponse<'a> {
|
||||||
|
let mut open_file = None;
|
||||||
|
let inner = ui
|
||||||
|
.collapsing(&self.name, |ui| {
|
||||||
|
for folder in &mut self.child_folders {
|
||||||
|
open_file = open_file.or(folder.show(ui).open_file);
|
||||||
|
}
|
||||||
|
|
||||||
|
for file in &mut self.child_files {
|
||||||
|
if ui.button(&file.name).clicked() {
|
||||||
|
open_file = Some(file.path.as_path())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.header_response;
|
||||||
|
|
||||||
|
FolderResponse { inner, open_file }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load(path: PathBuf) -> eyre::Result<Self> {
|
||||||
|
let name = path
|
||||||
|
.file_name()
|
||||||
|
.ok_or_eyre("Path is missing a file-name")?
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let mut child_folders = vec![];
|
||||||
|
let mut child_files = vec![];
|
||||||
|
|
||||||
|
for entry in read_dir(&path).with_context(|| eyre!("Couldn't read dir {path:?}"))? {
|
||||||
|
let entry = entry.with_context(|| eyre!("Couldn't read dir {path:?}"))?;
|
||||||
|
let path = entry.path();
|
||||||
|
let name = path
|
||||||
|
.file_name()
|
||||||
|
.ok_or_eyre("Path is missing a file-name")?
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let file_type = entry.file_type()?;
|
||||||
|
|
||||||
|
if file_type.is_symlink() {
|
||||||
|
log::error!("Symlinks not yet supported, skipping {path:?}");
|
||||||
|
continue;
|
||||||
|
} else if file_type.is_file() {
|
||||||
|
child_files.push(File { name, path });
|
||||||
|
} else if file_type.is_dir() {
|
||||||
|
child_folders.push(Folder::NotLoaded { name, path });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let folder = LoadedFolder {
|
||||||
|
name,
|
||||||
|
path,
|
||||||
|
child_folders,
|
||||||
|
child_files,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(folder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Folder {
|
||||||
|
fn load(&mut self, ui: &mut Ui) -> Option<&mut LoadedFolder> {
|
||||||
|
if let Folder::NotLoaded { name, path } = self {
|
||||||
|
let (tx, rx) = mpsc::channel();
|
||||||
|
|
||||||
|
{
|
||||||
|
let path = path.clone();
|
||||||
|
let ctx = ui.ctx().clone();
|
||||||
|
thread::spawn(move || match LoadedFolder::load(path) {
|
||||||
|
Err(e) => log::error!("Failed to load folder: {e}"),
|
||||||
|
Ok(folder) => {
|
||||||
|
let _ = tx.send(folder);
|
||||||
|
ctx.request_repaint();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
*self = Folder::Loading {
|
||||||
|
name: mem::take(name),
|
||||||
|
path: mem::take(path),
|
||||||
|
recv: rx,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Folder::Loading { recv, .. } = self {
|
||||||
|
match recv.try_recv() {
|
||||||
|
Ok(folder) => *self = Folder::Loaded(folder),
|
||||||
|
Err(_) => return None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let Folder::Loaded(folder) = self else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(folder)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show<'a>(&'a mut self, ui: &mut Ui) -> FolderResponse<'a> {
|
||||||
|
self.load(ui);
|
||||||
|
|
||||||
|
if let Folder::Loaded(folder) = self {
|
||||||
|
return folder.show(ui);
|
||||||
|
}
|
||||||
|
|
||||||
|
FolderResponse {
|
||||||
|
inner: ui.label(self.name()),
|
||||||
|
open_file: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path(&self) -> &Path {
|
||||||
|
match self {
|
||||||
|
Folder::NotLoaded { path, .. } => path,
|
||||||
|
Folder::Loading { path, .. } => path,
|
||||||
|
Folder::Loaded(folder) => &folder.path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Folder::NotLoaded { name, .. } => name,
|
||||||
|
Folder::Loading { name, .. } => name,
|
||||||
|
Folder::Loaded(folder) => &folder.name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unload(&mut self) {
|
||||||
|
let (name, path) = match self {
|
||||||
|
Folder::NotLoaded { .. } => return,
|
||||||
|
Folder::Loading { name, path, .. } => (name, path),
|
||||||
|
Folder::Loaded(folder) => (&mut folder.name, &mut folder.path),
|
||||||
|
};
|
||||||
|
|
||||||
|
*self = Folder::NotLoaded {
|
||||||
|
name: mem::take(name),
|
||||||
|
path: mem::take(path),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for Folder {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
self.path().serialize(serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for Folder {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
use serde::de::Error;
|
||||||
|
let path = PathBuf::deserialize(deserializer)?;
|
||||||
|
let name = path
|
||||||
|
.file_name()
|
||||||
|
.ok_or(D::Error::custom("Path is missing a file-name"))?
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
|
Ok(Folder::NotLoaded { name, path })
|
||||||
|
}
|
||||||
|
}
|
||||||
81
src/handwriting/canvas_rasterizer.rs
Normal file
81
src/handwriting/canvas_rasterizer.rs
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use egui::{
|
||||||
|
Color32, ColorImage, CornerRadius, Painter, Pos2, Rect, Stroke, StrokeKind, TextureHandle,
|
||||||
|
emath::TSTransform,
|
||||||
|
epaint::{Brush, RectShape, Vertex},
|
||||||
|
load::SizedTexture,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::rasterizer::rasterize_onto;
|
||||||
|
|
||||||
|
use super::StrokeBlendMode;
|
||||||
|
|
||||||
|
/// Rasterize onto a resizeable canvas.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct CanvasRasterizer {
|
||||||
|
image: ColorImage,
|
||||||
|
texture: Option<TextureHandle>,
|
||||||
|
texture_is_dirty: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CanvasRasterizer {
|
||||||
|
//pub fn clear(&mut self) {
|
||||||
|
// self.image = ColorImage::new(self.image.size, Color32::TRANSPARENT);
|
||||||
|
// self.texture_is_dirty = true;
|
||||||
|
//}
|
||||||
|
|
||||||
|
//pub fn set_size(&mut self, width: usize, height: usize) {
|
||||||
|
// if self.image.size != [width, height] {
|
||||||
|
// self.image = ColorImage::new([width, height], Color32::TRANSPARENT);
|
||||||
|
// self.texture_is_dirty = true;
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
pub fn clear_and_set_size(&mut self, width: usize, height: usize) {
|
||||||
|
self.image = ColorImage::new([width, height], Color32::TRANSPARENT);
|
||||||
|
self.texture_is_dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rasterize<'a>(
|
||||||
|
&mut self,
|
||||||
|
point_to_pixel: TSTransform,
|
||||||
|
triangles: impl Iterator<Item = [&'a Vertex; 3]>,
|
||||||
|
) {
|
||||||
|
rasterize_onto::<StrokeBlendMode>(&mut self.image, point_to_pixel, triangles);
|
||||||
|
self.texture_is_dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show(&mut self, ctx: &egui::Context, painter: &Painter, at: Rect) {
|
||||||
|
if self.texture_is_dirty {
|
||||||
|
self.texture_is_dirty = false;
|
||||||
|
let new_image = || {
|
||||||
|
let image = ColorImage::new(self.image.size, Color32::TRANSPARENT);
|
||||||
|
ctx.load_texture("handwriting", image, Default::default())
|
||||||
|
};
|
||||||
|
let texture = self.texture.get_or_insert_with(new_image);
|
||||||
|
texture.set(self.image.clone(), Default::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(texture) = &mut self.texture {
|
||||||
|
let texture = SizedTexture::new(texture.id(), texture.size_vec2());
|
||||||
|
let shape = RectShape {
|
||||||
|
rect: at,
|
||||||
|
corner_radius: CornerRadius::ZERO,
|
||||||
|
fill: Color32::WHITE,
|
||||||
|
stroke: Stroke::NONE,
|
||||||
|
stroke_kind: StrokeKind::Inside,
|
||||||
|
round_to_pixels: None,
|
||||||
|
blur_width: 0.0,
|
||||||
|
brush: Some(Arc::new(Brush {
|
||||||
|
fill_texture_id: texture.id,
|
||||||
|
uv: Rect {
|
||||||
|
min: Pos2::ZERO,
|
||||||
|
max: Pos2::new(1.0, 1.0),
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
painter.add(shape);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
97
src/handwriting/disk_format.rs
Normal file
97
src/handwriting/disk_format.rs
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
//! see [Packet]
|
||||||
|
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use half::f16;
|
||||||
|
use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
|
||||||
|
|
||||||
|
/// A `u16` encoded in little-endian.
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, PartialEq, Eq)]
|
||||||
|
#[repr(C, packed)]
|
||||||
|
pub struct u16_le([u8; 2]);
|
||||||
|
|
||||||
|
/// An `f16` encoded in little-endian.
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable)]
|
||||||
|
#[repr(C, packed)]
|
||||||
|
pub struct f16_le(u16_le);
|
||||||
|
|
||||||
|
/// Top-level type describing the handwriting disk-format.
|
||||||
|
#[derive(FromBytes, KnownLayout, Immutable)]
|
||||||
|
#[repr(C, packed)]
|
||||||
|
pub struct DiskFormat {
|
||||||
|
pub header: Header,
|
||||||
|
|
||||||
|
/// A packed array of [Stroke]s.
|
||||||
|
pub strokes: [u8],
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const V1: u16_le = u16_le::new(1);
|
||||||
|
|
||||||
|
#[derive(FromBytes, IntoBytes, KnownLayout, Immutable)]
|
||||||
|
#[repr(C, packed)]
|
||||||
|
pub struct Header {
|
||||||
|
/// Version of the disk format
|
||||||
|
pub version: u16_le,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(FromBytes, IntoBytes, KnownLayout, Immutable)]
|
||||||
|
#[repr(C, packed)]
|
||||||
|
pub struct RawStrokeHeader {
|
||||||
|
/// Number of points in the stroke.
|
||||||
|
pub len: u16_le,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(FromBytes, KnownLayout, Immutable)]
|
||||||
|
#[repr(C, packed)]
|
||||||
|
pub struct RawStroke {
|
||||||
|
pub header: RawStrokeHeader,
|
||||||
|
pub positions: [f16_le],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RawStroke {
|
||||||
|
pub const MIN_LEN: usize = size_of::<RawStrokeHeader>();
|
||||||
|
}
|
||||||
|
|
||||||
|
impl u16_le {
|
||||||
|
pub const fn new(init: u16) -> Self {
|
||||||
|
u16_le(init.to_le_bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl f16_le {
|
||||||
|
pub const fn new(init: f16) -> Self {
|
||||||
|
f16_le(u16_le::new(init.to_bits()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for u16_le {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
u16::from(*self).fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u16_le> for u16 {
|
||||||
|
fn from(value: u16_le) -> Self {
|
||||||
|
u16::from_le_bytes(value.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<f16_le> for f16 {
|
||||||
|
fn from(value: f16_le) -> Self {
|
||||||
|
f16::from_bits(u16::from(value.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u16> for u16_le {
|
||||||
|
fn from(value: u16) -> Self {
|
||||||
|
u16_le::new(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<f16> for f16_le {
|
||||||
|
fn from(value: f16) -> Self {
|
||||||
|
f16_le::new(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +1,6 @@
|
|||||||
|
mod canvas_rasterizer;
|
||||||
|
mod disk_format;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
fmt::{self, Display},
|
fmt::{self, Display},
|
||||||
iter, mem,
|
iter, mem,
|
||||||
@ -6,22 +9,20 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use base64::{Engine, prelude::BASE64_STANDARD};
|
use base64::{Engine, prelude::BASE64_STANDARD};
|
||||||
|
use canvas_rasterizer::CanvasRasterizer;
|
||||||
|
use disk_format::{DiskFormat, RawStroke, RawStrokeHeader, f16_le};
|
||||||
use egui::{
|
use egui::{
|
||||||
Color32, ColorImage, CornerRadius, Event, Frame, Id, Mesh, PointerButton, Pos2, Rect, Sense,
|
Color32, Event, Frame, Id, Mesh, PointerButton, Pos2, Rect, Sense, Shape, Stroke, Theme, Ui,
|
||||||
Shape, Stroke, TextureHandle, Theme, Ui, Vec2,
|
Vec2,
|
||||||
emath::{self, TSTransform},
|
emath::{self, TSTransform},
|
||||||
epaint::{Brush, RectShape, TessellationOptions, Tessellator, Vertex},
|
epaint::{TessellationOptions, Tessellator, Vertex},
|
||||||
load::SizedTexture,
|
|
||||||
};
|
};
|
||||||
use eyre::{Context, bail};
|
use eyre::{Context, bail};
|
||||||
use eyre::{OptionExt, eyre};
|
use eyre::{OptionExt, eyre};
|
||||||
use half::f16;
|
use half::f16;
|
||||||
use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
|
use zerocopy::{FromBytes, IntoBytes};
|
||||||
|
|
||||||
use crate::{
|
use crate::{custom_code_block::try_from_custom_code_block, rasterizer};
|
||||||
custom_code_block::try_from_custom_code_block,
|
|
||||||
rasterizer::{self, rasterize, rasterize_onto},
|
|
||||||
};
|
|
||||||
use crate::{custom_code_block::write_custom_code_block, util::random_id};
|
use crate::{custom_code_block::write_custom_code_block, util::random_id};
|
||||||
|
|
||||||
const HANDWRITING_MIN_HEIGHT: f32 = 100.0;
|
const HANDWRITING_MIN_HEIGHT: f32 = 100.0;
|
||||||
@ -33,7 +34,7 @@ pub const CODE_BLOCK_KEY: &str = "handwriting";
|
|||||||
|
|
||||||
type StrokeBlendMode = rasterizer::blend::Normal;
|
type StrokeBlendMode = rasterizer::blend::Normal;
|
||||||
|
|
||||||
const TESSELATION_OPTIONS: TessellationOptions = TessellationOptions {
|
const TESSELLATION_OPTIONS: TessellationOptions = TessellationOptions {
|
||||||
feathering: true,
|
feathering: true,
|
||||||
feathering_size_in_pixels: 1.0,
|
feathering_size_in_pixels: 1.0,
|
||||||
coarse_tessellation_culling: true,
|
coarse_tessellation_culling: true,
|
||||||
@ -60,40 +61,42 @@ pub struct HandwritingStyle {
|
|||||||
#[derive(serde::Deserialize, serde::Serialize)]
|
#[derive(serde::Deserialize, serde::Serialize)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct Handwriting {
|
pub struct Handwriting {
|
||||||
#[serde(skip, default = "random_id")]
|
|
||||||
id: Id,
|
|
||||||
|
|
||||||
strokes: Vec<Vec<Pos2>>,
|
strokes: Vec<Vec<Pos2>>,
|
||||||
|
|
||||||
/// The stroke that is currently being drawed.
|
|
||||||
#[serde(skip)]
|
|
||||||
current_stroke: Vec<Pos2>,
|
|
||||||
|
|
||||||
/// The lines that have not been blitted to `texture` yet.
|
|
||||||
#[serde(skip)]
|
|
||||||
unblitted_lines: Vec<[Pos2; 2]>,
|
|
||||||
|
|
||||||
height: f32,
|
height: f32,
|
||||||
desired_height: f32,
|
desired_height: f32,
|
||||||
|
|
||||||
/// Tesselated mesh of all strokes
|
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
|
e: Ephemeral,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handwriting data that isn't persisted across restarts.
|
||||||
|
struct Ephemeral {
|
||||||
|
id: Id,
|
||||||
|
|
||||||
|
canvas_rasterizer: CanvasRasterizer,
|
||||||
|
|
||||||
|
/// The stroke that is currently being drawed.
|
||||||
|
current_stroke: Vec<Pos2>,
|
||||||
|
|
||||||
|
/// The lines that have not been blitted to `texture` yet.
|
||||||
|
unblitted_lines: Vec<[Pos2; 2]>,
|
||||||
|
|
||||||
|
tessellator: Option<Tessellator>,
|
||||||
|
|
||||||
|
/// Tessellated mesh of all strokes
|
||||||
mesh: Arc<Mesh>,
|
mesh: Arc<Mesh>,
|
||||||
|
|
||||||
#[serde(skip)]
|
|
||||||
texture: Option<TextureHandle>,
|
|
||||||
|
|
||||||
#[serde(skip)]
|
|
||||||
image: ColorImage,
|
|
||||||
|
|
||||||
#[serde(skip)]
|
|
||||||
refresh_texture: bool,
|
refresh_texture: bool,
|
||||||
|
|
||||||
/// Context of the last mesh render.
|
/// Context of the last mesh render.
|
||||||
#[serde(skip)]
|
|
||||||
last_mesh_ctx: Option<MeshContext>,
|
last_mesh_ctx: Option<MeshContext>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct HandwritingResponse {
|
||||||
|
pub changed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
/// Context of a mesh render.
|
/// Context of a mesh render.
|
||||||
#[derive(Clone, Copy, PartialEq)]
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
struct MeshContext {
|
struct MeshContext {
|
||||||
@ -108,31 +111,6 @@ struct MeshContext {
|
|||||||
pub stroke: Stroke,
|
pub stroke: Stroke,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get [Painting::texture], initializing it if necessary.
|
|
||||||
macro_rules! texture {
|
|
||||||
($self_:expr, $ui:expr, $mesh_context:expr) => {{
|
|
||||||
let ui: &Ui = $ui;
|
|
||||||
let mesh_context: &MeshContext = $mesh_context;
|
|
||||||
let image_size = mesh_context.pixel_size();
|
|
||||||
|
|
||||||
let new_image = || {
|
|
||||||
let image = ColorImage::new(image_size, Color32::TRANSPARENT);
|
|
||||||
ui.ctx()
|
|
||||||
.load_texture("handwriting", image, Default::default())
|
|
||||||
};
|
|
||||||
|
|
||||||
let texture = $self_.texture.get_or_insert_with(new_image);
|
|
||||||
|
|
||||||
if texture.size() != image_size {
|
|
||||||
$self_.refresh_texture = true;
|
|
||||||
// TODO: don't redraw the entire mesh, just blit the old texture onto the new one
|
|
||||||
*texture = new_image()
|
|
||||||
};
|
|
||||||
|
|
||||||
texture
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MeshContext {
|
impl MeshContext {
|
||||||
/// Calculate canvas size in pixels
|
/// Calculate canvas size in pixels
|
||||||
pub fn pixel_size(&self) -> [usize; 2] {
|
pub fn pixel_size(&self) -> [usize; 2] {
|
||||||
@ -144,14 +122,22 @@ impl MeshContext {
|
|||||||
impl Default for Handwriting {
|
impl Default for Handwriting {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: random_id(),
|
|
||||||
strokes: Default::default(),
|
strokes: Default::default(),
|
||||||
current_stroke: Default::default(),
|
|
||||||
height: HANDWRITING_MIN_HEIGHT,
|
height: HANDWRITING_MIN_HEIGHT,
|
||||||
desired_height: HANDWRITING_MIN_HEIGHT,
|
desired_height: HANDWRITING_MIN_HEIGHT,
|
||||||
|
e: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Ephemeral {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
id: random_id(),
|
||||||
|
canvas_rasterizer: Default::default(),
|
||||||
|
current_stroke: Default::default(),
|
||||||
|
tessellator: None,
|
||||||
mesh: Default::default(),
|
mesh: Default::default(),
|
||||||
texture: None,
|
|
||||||
image: ColorImage::new([0, 0], Color32::WHITE),
|
|
||||||
refresh_texture: true,
|
refresh_texture: true,
|
||||||
last_mesh_ctx: None,
|
last_mesh_ctx: None,
|
||||||
unblitted_lines: Default::default(),
|
unblitted_lines: Default::default(),
|
||||||
@ -164,6 +150,7 @@ impl Handwriting {
|
|||||||
&mut self,
|
&mut self,
|
||||||
style: Option<&mut HandwritingStyle>,
|
style: Option<&mut HandwritingStyle>,
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
|
response: &mut HandwritingResponse,
|
||||||
) -> egui::Response {
|
) -> egui::Response {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
if let Some(style) = style {
|
if let Some(style) = style {
|
||||||
@ -172,33 +159,35 @@ impl Handwriting {
|
|||||||
ui.separator();
|
ui.separator();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ui.button("Clear Painting").clicked() {
|
if ui.button("clear").clicked() {
|
||||||
self.strokes.clear();
|
self.strokes.clear();
|
||||||
self.refresh_texture = true;
|
self.e.refresh_texture = true;
|
||||||
|
response.changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.add_enabled_ui(!self.strokes.is_empty(), |ui| {
|
ui.add_enabled_ui(!self.strokes.is_empty(), |ui| {
|
||||||
if ui.button("Undo").clicked() {
|
if ui.button("undo").clicked() {
|
||||||
self.strokes.pop();
|
self.strokes.pop();
|
||||||
self.refresh_texture = true;
|
self.e.refresh_texture = true;
|
||||||
|
response.changed = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let vertex_count: usize = self.mesh.indices.len() / 3;
|
let vertex_count: usize = self.e.mesh.indices.len() / 3;
|
||||||
ui.label(format!("vertices: {vertex_count}"));
|
ui.label(format!("vertices: {vertex_count}"));
|
||||||
})
|
})
|
||||||
.response
|
.response
|
||||||
}
|
}
|
||||||
|
|
||||||
fn commit_current_line(&mut self) {
|
pub fn ui_content(
|
||||||
debug_assert!(!self.current_stroke.is_empty());
|
&mut self,
|
||||||
self.strokes.push(mem::take(&mut self.current_stroke));
|
style: &HandwritingStyle,
|
||||||
}
|
ui: &mut Ui,
|
||||||
|
hw_response: &mut HandwritingResponse,
|
||||||
pub fn ui_content(&mut self, style: &HandwritingStyle, ui: &mut Ui) -> egui::Response {
|
) -> egui::Response {
|
||||||
if style.animate {
|
if style.animate {
|
||||||
self.height = ui.ctx().animate_value_with_time(
|
self.height = ui.ctx().animate_value_with_time(
|
||||||
self.id.with("height animation"),
|
self.e.id.with("height animation"),
|
||||||
self.desired_height,
|
self.desired_height,
|
||||||
0.4,
|
0.4,
|
||||||
);
|
);
|
||||||
@ -206,8 +195,8 @@ impl Handwriting {
|
|||||||
self.height = self.desired_height;
|
self.height = self.desired_height;
|
||||||
}
|
}
|
||||||
|
|
||||||
let size = Vec2::new(ui.available_width(), self.height);
|
let desired_size = Vec2::new(ui.available_width(), self.height);
|
||||||
let (response, painter) = ui.allocate_painter(size, Sense::drag());
|
let (response, painter) = ui.allocate_painter(desired_size, Sense::drag());
|
||||||
|
|
||||||
let mut response = response
|
let mut response = response
|
||||||
//.on_hover_cursor(CursorIcon::Crosshair)
|
//.on_hover_cursor(CursorIcon::Crosshair)
|
||||||
@ -216,20 +205,24 @@ impl Handwriting {
|
|||||||
|
|
||||||
let size = response.rect.size();
|
let size = response.rect.size();
|
||||||
|
|
||||||
let to_screen = emath::RectTransform::from_to(
|
// Calculate matrices that convert between screen-space and image-space.
|
||||||
//Rect::from_min_size(Pos2::ZERO, response.rect.square_proportions()),
|
// - image-space: 0,0 is the top-left of the texture.
|
||||||
Rect::from_min_size(Pos2::ZERO, size),
|
// - screen-space: 0,0 is the top-left of the window.
|
||||||
response.rect,
|
// Both spaces use the same logical points, not pixels.
|
||||||
);
|
let to_screen =
|
||||||
|
emath::RectTransform::from_to(Rect::from_min_size(Pos2::ZERO, size), response.rect);
|
||||||
let from_screen = to_screen.inverse();
|
let from_screen = to_screen.inverse();
|
||||||
|
|
||||||
|
// Was the user in the process of drawing a stroke last frame?
|
||||||
|
let was_drawing = !self.e.current_stroke.is_empty();
|
||||||
|
|
||||||
|
// Is the user in the process of drawing a stroke now?
|
||||||
let is_drawing = response.interact_pointer_pos().is_some();
|
let is_drawing = response.interact_pointer_pos().is_some();
|
||||||
let was_drawing = !self.current_stroke.is_empty();
|
|
||||||
|
|
||||||
if !is_drawing {
|
if !is_drawing {
|
||||||
// commit current line
|
|
||||||
if was_drawing {
|
if was_drawing {
|
||||||
self.commit_current_line();
|
// commit current line
|
||||||
|
self.commit_current_line(hw_response);
|
||||||
response.mark_changed();
|
response.mark_changed();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,6 +234,8 @@ impl Handwriting {
|
|||||||
.map(|p| p.y + HANDWRITING_BOTTOM_PADDING)
|
.map(|p| p.y + HANDWRITING_BOTTOM_PADDING)
|
||||||
.fold(HANDWRITING_MIN_HEIGHT, |max, y| max.max(y));
|
.fold(HANDWRITING_MIN_HEIGHT, |max, y| max.max(y));
|
||||||
|
|
||||||
|
// Change the height of the handwriting item.
|
||||||
|
// We don't do this mid-stroke, only when the user e.g. lifts the pen.
|
||||||
if self.desired_height != lines_max_y {
|
if self.desired_height != lines_max_y {
|
||||||
self.desired_height = lines_max_y;
|
self.desired_height = lines_max_y;
|
||||||
response.mark_changed();
|
response.mark_changed();
|
||||||
@ -267,16 +262,17 @@ impl Handwriting {
|
|||||||
_ => Some(next),
|
_ => Some(next),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.cloned()
|
|
||||||
.filter(|event| {
|
.filter(|event| {
|
||||||
// FIXME: pinenote: PointerMoved are duplicated after the MouseMoved events
|
// FIXME: pinenote: PointerMoved are duplicated after the MouseMoved events
|
||||||
cfg!(not(feature = "pinenote")) || !matches!(event, Event::PointerMoved(..))
|
cfg!(not(feature = "pinenote")) || !matches!(event, Event::PointerMoved(..))
|
||||||
})
|
})
|
||||||
|
.cloned()
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Process input events and turn them into strokes
|
||||||
for event in events {
|
for event in events {
|
||||||
let last_canvas_pos = self.current_stroke.last();
|
let last_canvas_pos = self.e.current_stroke.last();
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
Event::PointerMoved(new_position) => {
|
Event::PointerMoved(new_position) => {
|
||||||
@ -318,13 +314,13 @@ impl Handwriting {
|
|||||||
} => match (button, pressed) {
|
} => match (button, pressed) {
|
||||||
(PointerButton::Primary, true) => {
|
(PointerButton::Primary, true) => {
|
||||||
if last_canvas_pos.is_none() {
|
if last_canvas_pos.is_none() {
|
||||||
self.current_stroke.push(from_screen * pos);
|
self.e.current_stroke.push(from_screen * pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(PointerButton::Primary, false) => {
|
(PointerButton::Primary, false) => {
|
||||||
if last_canvas_pos.is_some() {
|
if last_canvas_pos.is_some() {
|
||||||
self.push_to_stroke(from_screen * pos);
|
self.push_to_stroke(from_screen * pos);
|
||||||
self.commit_current_line();
|
self.commit_current_line(hw_response);
|
||||||
response.mark_changed();
|
response.mark_changed();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -340,8 +336,8 @@ impl Handwriting {
|
|||||||
// TODO: In theory, we can get multiple press->draw->release series
|
// TODO: In theory, we can get multiple press->draw->release series
|
||||||
// in the same frame. Should handle this.
|
// in the same frame. Should handle this.
|
||||||
Event::PointerGone | Event::WindowFocused(false) => {
|
Event::PointerGone | Event::WindowFocused(false) => {
|
||||||
if !self.current_stroke.is_empty() {
|
if !self.e.current_stroke.is_empty() {
|
||||||
self.commit_current_line();
|
self.commit_current_line(hw_response);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -361,6 +357,7 @@ impl Handwriting {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw the horizontal ruled lines
|
||||||
(1..)
|
(1..)
|
||||||
.map(|n| n as f32 * HANDWRITING_LINE_SPACING)
|
.map(|n| n as f32 * HANDWRITING_LINE_SPACING)
|
||||||
.take_while(|&y| y < size.y)
|
.take_while(|&y| y < size.y)
|
||||||
@ -373,9 +370,12 @@ impl Handwriting {
|
|||||||
painter.add(shape);
|
painter.add(shape);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Get the position and dimensions of the image
|
||||||
let mesh_rect = response
|
let mesh_rect = response
|
||||||
.rect
|
.rect
|
||||||
.with_max_y(response.rect.min.y + self.desired_height);
|
.with_max_y(response.rect.min.y + self.desired_height);
|
||||||
|
|
||||||
|
// These are the values that, if changed, would require the mesh to be re-rendered.
|
||||||
let new_context = MeshContext {
|
let new_context = MeshContext {
|
||||||
ui_theme: ui.ctx().theme(),
|
ui_theme: ui.ctx().theme(),
|
||||||
pixels_per_point: ui.pixels_per_point(),
|
pixels_per_point: ui.pixels_per_point(),
|
||||||
@ -383,101 +383,101 @@ impl Handwriting {
|
|||||||
stroke: style.stroke,
|
stroke: style.stroke,
|
||||||
};
|
};
|
||||||
|
|
||||||
if Some(&new_context) != self.last_mesh_ctx.as_ref() {
|
// Figure out if we need to re-rasterize the mesh.
|
||||||
self.refresh_texture = true;
|
if Some(&new_context) != self.e.last_mesh_ctx.as_ref() {
|
||||||
|
self.e.refresh_texture = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.refresh_texture {
|
if self.e.refresh_texture {
|
||||||
// rasterize the entire texture from scratch
|
// ...if we do, rasterize the entire texture from scratch
|
||||||
self.refresh_texture(style, new_context, ui);
|
self.refresh_texture(style, new_context);
|
||||||
self.unblitted_lines.clear();
|
self.e.unblitted_lines.clear();
|
||||||
} else if !self.unblitted_lines.is_empty() {
|
} else if !self.e.unblitted_lines.is_empty() {
|
||||||
// only rasterize the new lines onto the existing texture
|
// ...if we don't, we can get away with only rasterizing the *new* lines onto the
|
||||||
for [from, to] in std::mem::take(&mut self.unblitted_lines) {
|
// existing texture.
|
||||||
self.draw_line_to_texture(from, to, &new_context, ui);
|
for [from, to] in std::mem::take(&mut self.e.unblitted_lines) {
|
||||||
|
self.draw_line_to_texture(from, to, &new_context);
|
||||||
}
|
}
|
||||||
self.unblitted_lines.clear();
|
self.e.unblitted_lines.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
//painter.add(self.mesh.clone());
|
// Draw the texture
|
||||||
|
self.e.canvas_rasterizer.show(ui.ctx(), &painter, mesh_rect);
|
||||||
if let Some(texture) = &self.texture {
|
|
||||||
let texture = SizedTexture::new(texture.id(), texture.size_vec2());
|
|
||||||
let shape = RectShape {
|
|
||||||
rect: mesh_rect,
|
|
||||||
corner_radius: CornerRadius::ZERO,
|
|
||||||
fill: Color32::WHITE,
|
|
||||||
stroke: Stroke::NONE,
|
|
||||||
stroke_kind: egui::StrokeKind::Inside,
|
|
||||||
round_to_pixels: None,
|
|
||||||
blur_width: 0.0,
|
|
||||||
brush: Some(Arc::new(Brush {
|
|
||||||
fill_texture_id: texture.id,
|
|
||||||
uv: Rect {
|
|
||||||
min: Pos2::ZERO,
|
|
||||||
max: Pos2::new(1.0, 1.0),
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
painter.add(shape);
|
|
||||||
}
|
|
||||||
|
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
|
|
||||||
fn refresh_texture(
|
fn commit_current_line(&mut self, response: &mut HandwritingResponse) {
|
||||||
&mut self,
|
debug_assert!(!self.e.current_stroke.is_empty());
|
||||||
style: &HandwritingStyle,
|
self.strokes.push(mem::take(&mut self.e.current_stroke));
|
||||||
mesh_context: MeshContext,
|
response.changed = true;
|
||||||
ui: &mut Ui,
|
}
|
||||||
) {
|
|
||||||
self.last_mesh_ctx = Some(mesh_context);
|
|
||||||
|
|
||||||
self.refresh_texture = false;
|
/// Tessellate and rasterize the strokes into a new texture.
|
||||||
|
fn refresh_texture(&mut self, style: &HandwritingStyle, mesh_context: MeshContext) {
|
||||||
|
let Ephemeral {
|
||||||
|
current_stroke,
|
||||||
|
tessellator,
|
||||||
|
mesh,
|
||||||
|
refresh_texture,
|
||||||
|
last_mesh_ctx,
|
||||||
|
..
|
||||||
|
} = &mut self.e;
|
||||||
|
// TODO: don't tessellate and rasterize on the GUI thread
|
||||||
|
|
||||||
|
*last_mesh_ctx = Some(mesh_context);
|
||||||
|
|
||||||
|
*refresh_texture = false;
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
let start_time = std::time::Instant::now();
|
let start_time = std::time::Instant::now();
|
||||||
|
|
||||||
let mut tesselator = Tessellator::new(
|
let mesh = Arc::make_mut(mesh);
|
||||||
mesh_context.pixels_per_point,
|
|
||||||
TESSELATION_OPTIONS,
|
|
||||||
Default::default(), // we don't tesselate fonts
|
|
||||||
vec![],
|
|
||||||
);
|
|
||||||
|
|
||||||
let mesh = Arc::make_mut(&mut self.mesh);
|
|
||||||
mesh.clear();
|
mesh.clear();
|
||||||
|
|
||||||
|
// TODO: re-use tessellator if pixels_per_point hasn't changed
|
||||||
|
let tessellator = tessellator.insert(new_tessellator(mesh_context.pixels_per_point));
|
||||||
|
|
||||||
self.strokes
|
self.strokes
|
||||||
.iter()
|
.iter()
|
||||||
.chain([&self.current_stroke])
|
.chain([&*current_stroke])
|
||||||
.filter(|stroke| stroke.len() >= 2)
|
.filter(|stroke| stroke.len() >= 2)
|
||||||
.map(|stroke| {
|
.map(|stroke| {
|
||||||
//let points: Vec<Pos2> = stroke.iter().map(|&p| to_screen * p).collect();
|
//let points: Vec<Pos2> = stroke.iter().map(|&p| to_screen * p).collect();
|
||||||
egui::Shape::line(stroke.clone(), style.stroke)
|
egui::Shape::line(stroke.clone(), style.stroke)
|
||||||
})
|
})
|
||||||
.for_each(|shape| {
|
.for_each(|shape| {
|
||||||
tesselator.tessellate_shape(shape, mesh);
|
tessellator.tessellate_shape(shape, mesh);
|
||||||
});
|
});
|
||||||
|
|
||||||
let texture = texture!(self, ui, &mesh_context);
|
// sanity-check that tessellation did not produce any NaNs.
|
||||||
let triangles = mesh_triangles(&self.mesh);
|
// this can happen if the line contains duplicated consecutive positions
|
||||||
|
//for vertex in &mesh.vertices {
|
||||||
|
// debug_assert!(vertex.pos.x.is_finite(), "{} must be finite", vertex.pos.x);
|
||||||
|
// debug_assert!(vertex.pos.y.is_finite(), "{} must be finite", vertex.pos.y);
|
||||||
|
//}
|
||||||
|
|
||||||
let [px_x, px_y] = mesh_context.pixel_size();
|
let [px_x, px_y] = mesh_context.pixel_size();
|
||||||
let point_to_pixel = TSTransform::from_scaling(mesh_context.pixels_per_point);
|
let point_to_pixel = TSTransform::from_scaling(mesh_context.pixels_per_point);
|
||||||
self.image = rasterize::<StrokeBlendMode>(px_x, px_y, point_to_pixel, triangles);
|
let triangles = mesh_triangles(&self.e.mesh);
|
||||||
texture.set(self.image.clone(), Default::default());
|
|
||||||
|
self.e.canvas_rasterizer.clear_and_set_size(px_x, px_y);
|
||||||
|
self.e
|
||||||
|
.canvas_rasterizer
|
||||||
|
.rasterize(point_to_pixel, triangles);
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
{
|
{
|
||||||
let elapsed = start_time.elapsed();
|
let elapsed = start_time.elapsed();
|
||||||
println!("refreshed mesh in {:.3}s", elapsed.as_secs_f32());
|
log::debug!("refreshed mesh in {:.3}s", elapsed.as_secs_f32());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ui(&mut self, style: &HandwritingStyle, ui: &mut Ui) {
|
pub fn ui(&mut self, style: &HandwritingStyle, ui: &mut Ui) -> HandwritingResponse {
|
||||||
|
let mut response = HandwritingResponse { changed: false };
|
||||||
|
|
||||||
ui.vertical_centered_justified(|ui| {
|
ui.vertical_centered_justified(|ui| {
|
||||||
self.ui_control(None, ui);
|
self.ui_control(None, ui, &mut response);
|
||||||
|
|
||||||
//ui.label("Paint with your mouse/touch!");
|
//ui.label("Paint with your mouse/touch!");
|
||||||
Frame::canvas(ui.style())
|
Frame::canvas(ui.style())
|
||||||
@ -485,51 +485,53 @@ impl Handwriting {
|
|||||||
.stroke(Stroke::new(5.0, Color32::from_black_alpha(40)))
|
.stroke(Stroke::new(5.0, Color32::from_black_alpha(40)))
|
||||||
.fill(style.bg_color)
|
.fill(style.bg_color)
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
self.ui_content(style, ui);
|
self.ui_content(style, ui, &mut response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Append a new [Pos2] to [Self::current_stroke].
|
||||||
|
///
|
||||||
|
/// Queue a new line to be drawn onto [Self::texture].
|
||||||
fn push_to_stroke(&mut self, new_canvas_pos: Pos2) {
|
fn push_to_stroke(&mut self, new_canvas_pos: Pos2) {
|
||||||
if let Some(&last_canvas_pos) = self.current_stroke.last() {
|
if let Some(&last_canvas_pos) = self.e.current_stroke.last() {
|
||||||
if last_canvas_pos == new_canvas_pos {
|
if last_canvas_pos == new_canvas_pos {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.unblitted_lines.push([last_canvas_pos, new_canvas_pos]);
|
self.e
|
||||||
|
.unblitted_lines
|
||||||
|
.push([last_canvas_pos, new_canvas_pos]);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.current_stroke.push(new_canvas_pos);
|
self.e.current_stroke.push(new_canvas_pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw a single line onto the existing texture.
|
/// Draw a single line onto the existing texture.
|
||||||
fn draw_line_to_texture(
|
fn draw_line_to_texture(&mut self, from: Pos2, to: Pos2, mesh_context: &MeshContext) {
|
||||||
&mut self,
|
// INVARIANT: if this function was called, then pixels_per_point is the same as last frame,
|
||||||
from: Pos2,
|
// so there's no need to create a new tessellator.
|
||||||
to: Pos2,
|
let tessellator = self
|
||||||
mesh_context: &MeshContext,
|
.e
|
||||||
ui: &mut Ui,
|
.tessellator
|
||||||
) {
|
.get_or_insert_with(|| new_tessellator(mesh_context.pixels_per_point));
|
||||||
let mut tesselator = Tessellator::new(
|
|
||||||
mesh_context.pixels_per_point,
|
|
||||||
TESSELATION_OPTIONS,
|
|
||||||
Default::default(), // we don't tesselate fonts
|
|
||||||
vec![],
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut mesh = Mesh::default();
|
let mut mesh = Mesh::default();
|
||||||
let line = egui::Shape::line_segment([from, to], mesh_context.stroke);
|
let line = egui::Shape::line_segment([from, to], mesh_context.stroke);
|
||||||
tesselator.tessellate_shape(line, &mut mesh);
|
tessellator.tessellate_shape(line, &mut mesh);
|
||||||
|
|
||||||
self.draw_mesh_to_texture(&mesh, mesh_context, ui);
|
self.draw_mesh_to_texture(&mesh, mesh_context);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw a single mesh onto the existing texture.
|
/// Draw a single mesh onto the existing texture.
|
||||||
fn draw_mesh_to_texture(&mut self, mesh: &Mesh, mesh_context: &MeshContext, ui: &mut Ui) {
|
fn draw_mesh_to_texture(&mut self, mesh: &Mesh, mesh_context: &MeshContext) {
|
||||||
let triangles = mesh_triangles(mesh);
|
let triangles = mesh_triangles(mesh);
|
||||||
let point_to_pixel = TSTransform::from_scaling(mesh_context.pixels_per_point);
|
let point_to_pixel = TSTransform::from_scaling(mesh_context.pixels_per_point);
|
||||||
rasterize_onto::<StrokeBlendMode>(&mut self.image, point_to_pixel, triangles);
|
self.e
|
||||||
texture!(self, ui, mesh_context).set(self.image.clone(), Default::default());
|
.canvas_rasterizer
|
||||||
|
.rasterize(point_to_pixel, triangles);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn strokes(&self) -> &[Vec<Pos2>] {
|
pub fn strokes(&self) -> &[Vec<Pos2>] {
|
||||||
@ -557,24 +559,49 @@ impl Handwriting {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn encode_as_disk_format(&self) -> Box<[u8]> {
|
||||||
|
let mut bytes = vec![];
|
||||||
|
let header = disk_format::Header {
|
||||||
|
version: disk_format::V1,
|
||||||
|
};
|
||||||
|
|
||||||
|
bytes.extend_from_slice(header.as_bytes());
|
||||||
|
|
||||||
|
for stroke in &self.strokes {
|
||||||
|
let Ok(len) = u16::try_from(stroke.len()) else {
|
||||||
|
log::error!("More than u16::MAX points in a stroke!");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let header = RawStrokeHeader { len: len.into() };
|
||||||
|
bytes.extend_from_slice(header.as_bytes());
|
||||||
|
|
||||||
|
for position in stroke {
|
||||||
|
for v in [position.x, position.y] {
|
||||||
|
let v = f16::from_f32(v);
|
||||||
|
let v = f16_le::from(v);
|
||||||
|
bytes.extend_from_slice(v.as_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes.into_boxed_slice()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_tessellator(pixels_per_point: f32) -> Tessellator {
|
||||||
|
Tessellator::new(
|
||||||
|
pixels_per_point,
|
||||||
|
TESSELLATION_OPTIONS,
|
||||||
|
Default::default(), // we don't tessellate fonts
|
||||||
|
vec![],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Handwriting {
|
impl Display for Handwriting {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
let mut raw = vec![];
|
let raw = self.encode_as_disk_format();
|
||||||
|
|
||||||
for stroke in &self.strokes {
|
|
||||||
raw.push((stroke.len() as u16).to_le_bytes());
|
|
||||||
for position in stroke {
|
|
||||||
let x = half::f16::from_f32(position.x);
|
|
||||||
let y = half::f16::from_f32(position.y);
|
|
||||||
raw.push(x.to_bits().to_le_bytes());
|
|
||||||
raw.push(y.to_bits().to_le_bytes());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let raw = raw.as_slice().as_bytes();
|
|
||||||
|
|
||||||
write_custom_code_block(f, CODE_BLOCK_KEY, BASE64_STANDARD.encode(raw))
|
write_custom_code_block(f, CODE_BLOCK_KEY, BASE64_STANDARD.encode(raw))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -590,53 +617,72 @@ impl FromStr for Handwriting {
|
|||||||
.decode(s)
|
.decode(s)
|
||||||
.wrap_err("Failed to decode painting data from base64")?;
|
.wrap_err("Failed to decode painting data from base64")?;
|
||||||
|
|
||||||
#[allow(non_camel_case_types)]
|
// HACK: first iteration of disk format did not have version header
|
||||||
type u16_le = [u8; 2];
|
//let mut bytes = bytes;
|
||||||
|
//bytes.insert(0, 0);
|
||||||
|
//bytes.insert(0, 1);
|
||||||
|
|
||||||
#[allow(non_camel_case_types)]
|
let disk_format = DiskFormat::ref_from_bytes(&bytes[..]).map_err(|_| eyre!("Too short"))?;
|
||||||
type f16_le = [u8; 2];
|
|
||||||
|
|
||||||
#[derive(FromBytes, KnownLayout, Immutable)]
|
if disk_format.header.version != disk_format::V1 {
|
||||||
#[repr(C, packed)]
|
bail!(
|
||||||
struct Stroke {
|
"Unknown disk_format version: {}",
|
||||||
pub len: u16_le,
|
disk_format.header.version
|
||||||
pub positions: [f16_le],
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut bytes = &bytes[..];
|
let mut raw_strokes = &disk_format.strokes[..];
|
||||||
let mut strokes = vec![];
|
let mut strokes = vec![];
|
||||||
|
|
||||||
while !bytes.is_empty() {
|
while !raw_strokes.is_empty() {
|
||||||
let header_len = size_of::<u16_le>();
|
if raw_strokes.len() < RawStroke::MIN_LEN {
|
||||||
if bytes.len() < header_len {
|
bail!("Invalid remaining length: {}", raw_strokes.len());
|
||||||
bail!("Invalid remaining length: {}", bytes.len());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let stroke = Stroke::ref_from_bytes(&bytes[..header_len]).expect("length is correct");
|
let stroke = RawStroke::ref_from_bytes(&raw_strokes[..RawStroke::MIN_LEN])
|
||||||
let len = usize::from(u16::from_le_bytes(stroke.len));
|
.expect("length is correct");
|
||||||
let len = len * size_of::<f16_le>() * 2;
|
|
||||||
|
|
||||||
if bytes.len() < len {
|
// get length as number of points
|
||||||
bail!("Invalid remaining length: {}", bytes.len());
|
let len = usize::from(u16::from(stroke.header.len));
|
||||||
|
|
||||||
|
// convert to length in bytes
|
||||||
|
let byte_len = 2 * size_of::<f16_le>() * len;
|
||||||
|
|
||||||
|
if raw_strokes.len() < byte_len {
|
||||||
|
bail!("Invalid remaining length: {}", raw_strokes.len());
|
||||||
}
|
}
|
||||||
|
|
||||||
let (stroke, rest) = bytes.split_at(header_len + len);
|
let (stroke, rest) = raw_strokes.split_at(RawStroke::MIN_LEN + byte_len);
|
||||||
bytes = rest;
|
raw_strokes = rest;
|
||||||
let stroke = Stroke::ref_from_bytes(stroke)
|
|
||||||
.map_err(|e| eyre!("Failed to decode stroke bytes: {e}"))?;
|
|
||||||
|
|
||||||
let mut positions = stroke
|
let stroke = RawStroke::ref_from_bytes(stroke).expect("length is correct");
|
||||||
|
|
||||||
|
debug_assert_eq!(
|
||||||
|
stroke.positions.len().rem_euclid(2),
|
||||||
|
0,
|
||||||
|
"{} must be divisible by 2",
|
||||||
|
stroke.positions.len()
|
||||||
|
);
|
||||||
|
debug_assert_eq!(stroke.positions.len(), len * 2);
|
||||||
|
|
||||||
|
let mut last_pos = Pos2::new(f32::NEG_INFINITY, f32::INFINITY);
|
||||||
|
|
||||||
|
// positions are encoded as an array of f16s [x, y, x, y, x, y, ..]
|
||||||
|
let stroke: Vec<Pos2> = stroke
|
||||||
.positions
|
.positions
|
||||||
.iter()
|
.chunks_exact(2)
|
||||||
.map(|&position| f16::from_bits(u16::from_le_bytes(position)));
|
.map(|chunk| [chunk[0], chunk[1]])
|
||||||
|
.map(|pos| pos.map(f16::from)) // interpret bytes as f16
|
||||||
|
.map(|pos| pos.map(f32::from)) // widen to f32
|
||||||
|
.filter(|pos| pos.iter().all(|f| f.is_finite())) // filter out NaNs and Infs
|
||||||
|
.map(|[x, y]| Pos2::new(x, y))
|
||||||
|
.filter(|pos| {
|
||||||
|
let is_duplicate = pos == &last_pos;
|
||||||
|
last_pos = *pos;
|
||||||
|
!is_duplicate // skip duplicates
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
let mut stroke = vec![];
|
|
||||||
while let Some(x) = positions.next() {
|
|
||||||
let Some(y) = positions.next() else {
|
|
||||||
unreachable!("len is a multiple of two");
|
|
||||||
};
|
|
||||||
stroke.push(Pos2::new(x.into(), y.into()));
|
|
||||||
}
|
|
||||||
strokes.push(stroke);
|
strokes.push(stroke);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
source: src/handwriting/mod.rs
|
||||||
|
expression: deserialized.strokes
|
||||||
|
---
|
||||||
|
[
|
||||||
|
[
|
||||||
|
[-1.0 1.0],
|
||||||
|
[3.0 1.0],
|
||||||
|
[3.0 3.0],
|
||||||
|
[1.5 2.0],
|
||||||
|
[0.0 0.0],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[3.0 3.0],
|
||||||
|
[-1.0 1.0],
|
||||||
|
[0.0 0.0],
|
||||||
|
[3.0 1.0],
|
||||||
|
],
|
||||||
|
]
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
source: src/handwriting/mod.rs
|
||||||
|
expression: handwriting.strokes
|
||||||
|
---
|
||||||
|
[
|
||||||
|
[
|
||||||
|
[-1.0 1.0],
|
||||||
|
[3.0 1.0],
|
||||||
|
[3.0 3.0],
|
||||||
|
[1.5 2.0],
|
||||||
|
[0.0 0.0],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[3.0 3.0],
|
||||||
|
[-1.0 1.0],
|
||||||
|
[0.0 0.0],
|
||||||
|
[3.0 1.0],
|
||||||
|
],
|
||||||
|
]
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
source: src/handwriting/mod.rs
|
||||||
|
expression: serialized
|
||||||
|
---
|
||||||
|
```handwriting
|
||||||
|
AQAFAAC8ADwAQgA8AEIAQgA+AEAAAAAABAAAQgBCALwAPAAAAAAAQgA8
|
||||||
|
```
|
||||||
@ -5,7 +5,8 @@ pub mod constants;
|
|||||||
pub mod custom_code_block;
|
pub mod custom_code_block;
|
||||||
pub mod easy_mark;
|
pub mod easy_mark;
|
||||||
pub mod file_editor;
|
pub mod file_editor;
|
||||||
pub mod painting;
|
pub mod folder;
|
||||||
|
pub mod handwriting;
|
||||||
pub mod preferences;
|
pub mod preferences;
|
||||||
pub mod rasterizer;
|
pub mod rasterizer;
|
||||||
pub mod text_editor;
|
pub mod text_editor;
|
||||||
|
|||||||
@ -72,15 +72,11 @@ pub fn rasterize_onto<'a, Blend: BlendFn>(
|
|||||||
|
|
||||||
// If the pixel is within the triangle, fill it in.
|
// If the pixel is within the triangle, fill it in.
|
||||||
if point_in_triangle.inside {
|
if point_in_triangle.inside {
|
||||||
let c0 = triangle[0]
|
let [c0, c1, c2] = [0, 1, 2].map(|i| {
|
||||||
.color
|
triangle[i]
|
||||||
.linear_multiply(point_in_triangle.weights[0]);
|
.color
|
||||||
let c1 = triangle[1]
|
.linear_multiply(point_in_triangle.weights[i])
|
||||||
.color
|
});
|
||||||
.linear_multiply(point_in_triangle.weights[1]);
|
|
||||||
let c2 = triangle[2]
|
|
||||||
.color
|
|
||||||
.linear_multiply(point_in_triangle.weights[2]);
|
|
||||||
|
|
||||||
let color = c0 + c1 + c2;
|
let color = c0 + c1 + c2;
|
||||||
|
|
||||||
@ -169,6 +165,10 @@ fn point_in_triangle(point: Pos2, triangle: [&Vertex; 3]) -> PointInTriangle {
|
|||||||
// Normalize the weights.
|
// Normalize the weights.
|
||||||
let weights = areas.map(|area| area / triangle_area);
|
let weights = areas.map(|area| area / triangle_area);
|
||||||
|
|
||||||
|
if cfg!(debug_assertions) && weights.into_iter().any(f32::is_nan) {
|
||||||
|
panic!("weights must not be NaN! {weights:?} {triangle_area:?} {areas:?} {sides:?}");
|
||||||
|
}
|
||||||
|
|
||||||
PointInTriangle { inside, weights }
|
PointInTriangle { inside, weights }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
source: src/custom_code_block.rs
|
||||||
|
expression: list
|
||||||
|
---
|
||||||
|
[
|
||||||
|
Line(
|
||||||
|
"\n",
|
||||||
|
),
|
||||||
|
Line(
|
||||||
|
"# Hello world\n",
|
||||||
|
),
|
||||||
|
Line(
|
||||||
|
"## Subheader\n",
|
||||||
|
),
|
||||||
|
Line(
|
||||||
|
"- 1\n",
|
||||||
|
),
|
||||||
|
CodeBlock {
|
||||||
|
key: "foo",
|
||||||
|
content: " whatever\n some code\n Hi mom!",
|
||||||
|
span: "```foo\n whatever\n some code\n Hi mom!\n```",
|
||||||
|
},
|
||||||
|
Line(
|
||||||
|
" \n",
|
||||||
|
),
|
||||||
|
Line(
|
||||||
|
"\n",
|
||||||
|
),
|
||||||
|
CodeBlock {
|
||||||
|
key: "` # wrong number of ticks, but that's ok",
|
||||||
|
content: " ``` # indented ticks",
|
||||||
|
span: "```` # wrong number of ticks, but that's ok\n ``` # indented ticks\n```\n",
|
||||||
|
},
|
||||||
|
Line(
|
||||||
|
"\n",
|
||||||
|
),
|
||||||
|
Line(
|
||||||
|
"``` # no closing ticks\n",
|
||||||
|
),
|
||||||
|
Line(
|
||||||
|
" ",
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -24,6 +24,10 @@ pub struct MdTextEdit {
|
|||||||
cursor: Option<CCursorRange>,
|
cursor: Option<CCursorRange>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct MdTextEditOutput {
|
||||||
|
pub changed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
impl MdTextEdit {
|
impl MdTextEdit {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
MdTextEdit::default()
|
MdTextEdit::default()
|
||||||
@ -36,7 +40,7 @@ impl MdTextEdit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ui(&mut self, ui: &mut Ui) {
|
pub fn ui(&mut self, ui: &mut Ui) -> MdTextEditOutput {
|
||||||
let Self {
|
let Self {
|
||||||
text,
|
text,
|
||||||
highlighter,
|
highlighter,
|
||||||
@ -72,6 +76,10 @@ impl MdTextEdit {
|
|||||||
*cursor = text_edit.cursor_range;
|
*cursor = text_edit.cursor_range;
|
||||||
//ui.ctx().request_repaint();
|
//ui.ctx().request_repaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MdTextEditOutput {
|
||||||
|
changed: text_edit.response.changed(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user