Compare commits
101 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
6b6af9e826 | ||
|
b97f105408 | ||
|
e4ac219bf9 | ||
|
fbe0a4f554 | ||
|
4fd4b6eb17 | ||
|
e08c673e86 | ||
|
f992a24719 | ||
|
24710414bd | ||
![]() |
4c00a633af | ||
![]() |
6500531270 | ||
![]() |
fdaacaa6f3 | ||
![]() |
ac484fdea8 | ||
![]() |
01a9501f86 | ||
![]() |
1cdb558823 | ||
![]() |
f32b6dc29f | ||
![]() |
d126b88686 | ||
![]() |
04917b5b02 | ||
![]() |
97fbd0d778 | ||
![]() |
f05c489d62 | ||
![]() |
356a59fa22 | ||
![]() |
ae74973b13 | ||
![]() |
bdb37a7357 | ||
![]() |
d7874e728b | ||
![]() |
1d20d3ac56 | ||
![]() |
a99915c8c3 | ||
![]() |
c1b0eb33bf | ||
![]() |
d291e84f61 | ||
![]() |
ad68d2c282 | ||
![]() |
cc6dc79a39 | ||
![]() |
d28dee3d69 | ||
![]() |
9bca370872 | ||
![]() |
39887b3396 | ||
![]() |
a8d2c19ad6 | ||
![]() |
c74ac82b1c | ||
![]() |
f355000abb | ||
![]() |
4f0f129021 | ||
![]() |
617082f68d | ||
![]() |
d33b92e9b7 | ||
![]() |
3aa48b1b78 | ||
![]() |
d1bcb4f9b4 | ||
![]() |
bc0377c499 | ||
![]() |
bd57939ef3 | ||
![]() |
acc1bf8ca3 | ||
![]() |
2962102962 | ||
![]() |
ceaa265c72 | ||
![]() |
6cfcdc6d0b | ||
![]() |
a60f4f9ec4 | ||
![]() |
7015cc9ce6 | ||
![]() |
55f1b42ab0 | ||
![]() |
b32b6e4f87 | ||
![]() |
87e650b3a8 | ||
![]() |
b2055ff22d | ||
![]() |
deef698f8d | ||
![]() |
95450ce52a | ||
![]() |
e01752ecec | ||
![]() |
f0e9a8ca94 | ||
![]() |
3778469de9 | ||
![]() |
ae4c76ed41 | ||
![]() |
2fbb9377f6 | ||
![]() |
714fc0cd00 | ||
![]() |
34a525c38c | ||
![]() |
e10063ac74 | ||
![]() |
e7aa41ce1f | ||
![]() |
54b1d0799e | ||
![]() |
220e6d19bd | ||
![]() |
4a385dbc64 | ||
![]() |
fb64c8b3d1 | ||
![]() |
96a6a68ceb | ||
![]() |
5184eae4f6 | ||
![]() |
4545656c31 | ||
![]() |
fc0dd43464 | ||
![]() |
0cd6dce47c | ||
![]() |
d078f3a470 | ||
![]() |
e59d10d23a | ||
![]() |
5c778b4350 | ||
![]() |
50238c1577 | ||
![]() |
4807b1bde5 | ||
![]() |
a299ae1082 | ||
![]() |
a58ea4fba5 | ||
![]() |
257b83b582 | ||
![]() |
e934ac031d | ||
![]() |
60d4231575 | ||
![]() |
7107f77c82 | ||
![]() |
48dad951a5 | ||
![]() |
1881d62c56 | ||
![]() |
d654714c0d | ||
![]() |
d0188c0ad7 | ||
![]() |
a6da2887c9 | ||
![]() |
fbdf8090a1 | ||
![]() |
3820a64789 | ||
![]() |
4168c645ec | ||
![]() |
bc023d38b8 | ||
![]() |
4906634ffb | ||
![]() |
c2f49901f6 | ||
![]() |
64cc46d4ac | ||
![]() |
c0872b0eba | ||
![]() |
ef74c4d115 | ||
![]() |
3f61f9f3fe | ||
![]() |
d8c1b0abac | ||
![]() |
6e85b7fa35 | ||
![]() |
c0de520811 |
|
@ -1,2 +1,3 @@
|
|||
/*
|
||||
!/src-tauri
|
||||
!/shared
|
||||
|
|
12
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"rust-analyzer.cargo.cfgs": {
|
||||
"FASTWAVE_PLATFORM": "TAURI",
|
||||
// "FASTWAVE_PLATFORM": "BROWSER",
|
||||
},
|
||||
// https://github.com/rustwasm/wasm-bindgen/issues/2339#issuecomment-2147636233
|
||||
"rust-analyzer.cargo.extraEnv": {
|
||||
"RUSTFLAGS": "--cfg=web_sys_unstable_apis"
|
||||
},
|
||||
// to prevent rebuilding from scratch on each change
|
||||
"rust-analyzer.cargo.target": "wasm32-unknown-unknown"
|
||||
}
|
2883
Cargo.lock
generated
|
@ -3,7 +3,9 @@ members = [
|
|||
"frontend",
|
||||
"backend",
|
||||
"shared",
|
||||
"src-tauri",
|
||||
"src-tauri",
|
||||
"test_files/components/rust_decoder",
|
||||
"test_files/components/rust_diagram_connector",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
|
@ -16,11 +18,12 @@ readme = "../README.md"
|
|||
publish = false
|
||||
|
||||
[workspace.dependencies]
|
||||
shared = { path = "./shared" }
|
||||
# wellen = { version = "0.9.9", features = ["serde1"] }
|
||||
# wellen = { path = "../wellen/wellen", features = ["serde1"] }
|
||||
wellen = { git = "https://github.com/MartinKavik/wellen", features = ["serde1"], branch = "new_pub_types" }
|
||||
# moon = { path = "../../crates/moon" }
|
||||
# zoon = { path = "../../crates/zoon" }
|
||||
# moonlight = { path = "../../crates/zoon" }
|
||||
zoon = { git = "https://github.com/MoonZoon/MoonZoon", rev = "fc73b0d90bf39be72e70fdcab4f319ea5b8e6cfc" }
|
||||
moon = { git = "https://github.com/MoonZoon/MoonZoon", rev = "fc73b0d90bf39be72e70fdcab4f319ea5b8e6cfc" }
|
||||
moonlight = { git = "https://github.com/MoonZoon/MoonZoon", rev = "fc73b0d90bf39be72e70fdcab4f319ea5b8e6cfc" }
|
||||
|
|
107
Makefile.toml
|
@ -18,6 +18,7 @@ dependencies = [
|
|||
"install_mzoon",
|
||||
"init_pixi_canvas",
|
||||
"init_tauri_glue",
|
||||
"init_excalidraw_canvas",
|
||||
]
|
||||
|
||||
[tasks.start]
|
||||
|
@ -27,6 +28,7 @@ run_task = { fork = true, parallel = true, name = [
|
|||
"tauri_dev_with_cleanup",
|
||||
"watch_pixi_canvas",
|
||||
"watch_tauri_glue",
|
||||
"watch_excalidraw_canvas",
|
||||
]}
|
||||
|
||||
[tasks.start_browser]
|
||||
|
@ -36,6 +38,17 @@ run_task = { fork = true, parallel = true, name = [
|
|||
"mzoon_start_with_cleanup",
|
||||
"watch_pixi_canvas",
|
||||
"watch_tauri_glue",
|
||||
"watch_excalidraw_canvas",
|
||||
]}
|
||||
|
||||
[tasks.start_browser_release]
|
||||
description = "Run without Tauri in the browser & watch Typescript and Rust in the release mode"
|
||||
dependencies = ["store_current_process_id"]
|
||||
run_task = { fork = true, parallel = true, name = [
|
||||
"mzoon_start_release_with_cleanup",
|
||||
"watch_pixi_canvas",
|
||||
"watch_tauri_glue",
|
||||
"watch_excalidraw_canvas",
|
||||
]}
|
||||
|
||||
[tasks.bundle]
|
||||
|
@ -100,6 +113,11 @@ description = "Run `mzoon start`"
|
|||
extend = "mzoon"
|
||||
args = ["start"]
|
||||
|
||||
[tasks.mzoon_start_release]
|
||||
description = "Run `mzoon start --release`"
|
||||
extend = "mzoon"
|
||||
args = ["start", "--release"]
|
||||
|
||||
[tasks.tauri_dev_with_cleanup]
|
||||
description = "Run forked `tauri dev` with cleanup"
|
||||
run_task = { fork = true, cleanup_task = "kill_watchers", name = ["tauri_dev"] }
|
||||
|
@ -108,6 +126,10 @@ run_task = { fork = true, cleanup_task = "kill_watchers", name = ["tauri_dev"] }
|
|||
description = "Run forked `mzoon start` with cleanup"
|
||||
run_task = { fork = true, cleanup_task = "kill_watchers", name = ["mzoon_start"] }
|
||||
|
||||
[tasks.mzoon_start_release_with_cleanup]
|
||||
description = "Run forked `mzoon start` with cleanup"
|
||||
run_task = { fork = true, cleanup_task = "kill_watchers", name = ["mzoon_start_release"] }
|
||||
|
||||
[tasks.kill_watchers]
|
||||
description = "Kill the cargo-make/makers process and all its children / forked processes"
|
||||
script_runner = "@duckscript"
|
||||
|
@ -143,7 +165,7 @@ description = "Install Tauri CLI (tauri) locally"
|
|||
command = "cargo"
|
||||
args = [
|
||||
"install",
|
||||
"tauri-cli@=2.0.0-beta.17",
|
||||
"tauri-cli@=2.1.0",
|
||||
"--locked",
|
||||
"--root",
|
||||
"tauri",
|
||||
|
@ -186,7 +208,14 @@ run_task = { fork = true, parallel = true, name = [
|
|||
description = "Compile `frontend/typescript/pixi_canvas` on change"
|
||||
cwd = "frontend/typescript/pixi_canvas"
|
||||
command = "node_modules/.bin/esbuild"
|
||||
args = ["pixi_canvas.ts", "--bundle", "--outfile=../bundles/pixi_canvas.js", "--format=esm", "--watch"]
|
||||
args = [
|
||||
"pixi_canvas.ts",
|
||||
"--bundle",
|
||||
"--outfile=../bundles/pixi_canvas.js",
|
||||
"--format=esm",
|
||||
"--minify",
|
||||
"--watch"
|
||||
]
|
||||
|
||||
[tasks.watch_build_pixi_canvas.windows]
|
||||
command = "node_modules/.bin/esbuild.cmd"
|
||||
|
@ -231,7 +260,14 @@ run_task = { fork = true, parallel = true, name = [
|
|||
description = "Compile `frontend/typescript/tauri_glue` on change"
|
||||
cwd = "frontend/typescript/tauri_glue"
|
||||
command = "node_modules/.bin/esbuild"
|
||||
args = ["tauri_glue.ts", "--bundle", "--outfile=../bundles/tauri_glue.js", "--format=esm", "--watch"]
|
||||
args = [
|
||||
"tauri_glue.ts",
|
||||
"--bundle",
|
||||
"--outfile=../bundles/tauri_glue.js",
|
||||
"--format=esm",
|
||||
"--minify",
|
||||
"--watch"
|
||||
]
|
||||
|
||||
[tasks.watch_build_tauri_glue.windows]
|
||||
command = "node_modules/.bin/esbuild.cmd"
|
||||
|
@ -254,3 +290,68 @@ args = [
|
|||
[tasks.watch_typecheck_tauri_glue.windows]
|
||||
command = "node_modules/.bin/tsc.cmd"
|
||||
|
||||
## excalidraw_canvas ##
|
||||
|
||||
[tasks.init_excalidraw_canvas]
|
||||
description = "Initialize `frontend/typescript/excalidraw_canvas`"
|
||||
dependencies = [
|
||||
"init_excalidraw_canvas_npm_install",
|
||||
"init_excalidraw_canvas_en_json",
|
||||
]
|
||||
|
||||
[tasks.init_excalidraw_canvas_npm_install]
|
||||
description = "Partly initialize `frontend/typescript/excalidraw_canvas`"
|
||||
cwd = "frontend/typescript/excalidraw_canvas"
|
||||
command = "npm"
|
||||
args = ["install"]
|
||||
|
||||
[tasks.init_excalidraw_canvas_npm_install.windows]
|
||||
command = "npm.cmd"
|
||||
|
||||
[tasks.init_excalidraw_canvas_en_json]
|
||||
description = "Partly initialize `frontend/typescript/excalidraw_canvas`"
|
||||
cwd = "frontend/typescript/excalidraw_canvas"
|
||||
command = "cp"
|
||||
args = ["-r", "locales", "node_modules/@excalidraw/excalidraw/types/"]
|
||||
|
||||
[tasks.watch_excalidraw_canvas]
|
||||
description = "Build and typescheck Typescript on change"
|
||||
run_task = { fork = true, parallel = true, name = [
|
||||
"watch_build_excalidraw_canvas",
|
||||
"watch_typecheck_excalidraw_canvas",
|
||||
]}
|
||||
|
||||
[tasks.watch_build_excalidraw_canvas]
|
||||
description = "Compile `frontend/typescript/excalidraw_canvas` on change"
|
||||
cwd = "frontend/typescript/excalidraw_canvas"
|
||||
command = "node_modules/.bin/esbuild"
|
||||
args = [
|
||||
"excalidraw_canvas.tsx",
|
||||
"--bundle",
|
||||
"--outfile=../bundles/excalidraw_canvas.js",
|
||||
"--format=esm",
|
||||
"--minify",
|
||||
"--watch",
|
||||
]
|
||||
|
||||
[tasks.watch_build_excalidraw_canvas.windows]
|
||||
command = "node_modules/.bin/esbuild.cmd"
|
||||
|
||||
[tasks.watch_typecheck_excalidraw_canvas]
|
||||
description = "Typecheck `frontend/typescript/excalidraw_canvas` on change"
|
||||
cwd = "frontend/typescript/excalidraw_canvas"
|
||||
command = "node_modules/.bin/tsc"
|
||||
args = [
|
||||
"excalidraw_canvas.tsx",
|
||||
"--jsx", "react",
|
||||
"--watch",
|
||||
"--noEmit",
|
||||
"--preserveWatchOutput",
|
||||
"--strict",
|
||||
"--target", "esnext",
|
||||
"--module", "esnext",
|
||||
"--moduleResolution", "bundler",
|
||||
]
|
||||
|
||||
[tasks.watch_typecheck_excalidraw_canvas.windows]
|
||||
command = "node_modules/.bin/tsc.cmd"
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
port = 8080
|
||||
# port = 8443
|
||||
https = false
|
||||
cache_busting = true
|
||||
# @TODO how to import `pkg/frontend.js` with enabled cache busting?
|
||||
# @TODO add a switch to enable Typescript generator in mzoon?
|
||||
cache_busting = false
|
||||
backend_log_level = "warn" # "error" / "warn" / "info" / "debug" / "trace"
|
||||
|
||||
[redirect]
|
||||
|
@ -15,10 +17,15 @@ origins = ["*"]
|
|||
frontend = [
|
||||
"public",
|
||||
"frontend/Cargo.toml",
|
||||
"frontend/typescript/bundles",
|
||||
"frontend/src",
|
||||
"frontend/typescript/bundles",
|
||||
"shared/Cargo.toml",
|
||||
"shared/src",
|
||||
]
|
||||
backend = [
|
||||
"backend/Cargo.toml",
|
||||
"backend/src",
|
||||
"backend/globals.js",
|
||||
"backend/index.js",
|
||||
"backend/style.css",
|
||||
]
|
||||
|
|
121
README.md
|
@ -3,17 +3,91 @@
|
|||
|
||||
---
|
||||
|
||||
<p align="center">Default state - Dark (new design)</p>
|
||||
<p align="center">
|
||||
<img width="800" src="docs/screenshot_firefox.png" alt="fastwave_screenshot_firefox" />
|
||||
<img width="800" src="docs/home_dark.png" alt="Fastwave - Default state - Dark (not implemented yet)" />
|
||||
</p>
|
||||
|
||||
<p align="center">Loaded files - Dark (new design)</p>
|
||||
<p align="center">
|
||||
<img width="800" src="docs/video_desktop.gif" alt="fastwave_video_desktop" />
|
||||
<img width="800" src="docs/loaded_files_dark.png" alt="Fastwave - Loaded files - Dark (not implemented yet)" />
|
||||
</p>
|
||||
|
||||
<p align="center">Default state - Light (new design)</p>
|
||||
<p align="center">
|
||||
<img width="800" src="docs/home_light.png" alt="Fastwave - Default state - Light (not implemented yet)" />
|
||||
</p>
|
||||
|
||||
<p align="center">Loaded files - Light (new design)</p>
|
||||
<p align="center">
|
||||
<img width="800" src="docs/loaded_files_light.png" alt="Fastwave - Loaded files - Light (not implemented yet)" />
|
||||
</p>
|
||||
|
||||
|
||||
<p align="center">Browser (Firefox)</p>
|
||||
<p align="center">
|
||||
<img width="800" src="docs/screenshot_firefox.png" alt="Fastwave - Browser (Firefox)" />
|
||||
</p>
|
||||
|
||||
<p align="center">Desktop, miller columns and tree</p>
|
||||
<p align="center">
|
||||
<img width="800" src="docs/video_desktop.gif" alt="Fastwave - Desktop, miller columns and tree" />
|
||||
</p>
|
||||
|
||||
<p align="center">Zoom, pan and basic number formats</p>
|
||||
<p align="center">
|
||||
<img width="800" src="docs/video_zoom_formatting_simple.gif" alt="Fastwave - Zoom, pan and basic number formats" />
|
||||
</p>
|
||||
|
||||
<p align="center">Zoom and all formats</p>
|
||||
<p align="center">
|
||||
<img width="800" src="docs/video_zoom_formatting.gif" alt="Fastwave - Zoom and all formats" />
|
||||
</p>
|
||||
|
||||
<p align="center">Javascript commands</p>
|
||||
<p align="center">
|
||||
<img width="800" src="docs/video_javascript_commands.gif" alt="Fastwave - Javascript commands" />
|
||||
</p>
|
||||
|
||||
<p align="center">Load and save selected variables</p>
|
||||
<p align="center">
|
||||
<img width="800" src="docs/video_load_save_selected_vars.gif" alt="Fastwave - Load and save selected variables" />
|
||||
</p>
|
||||
|
||||
<p align="center">Decoders (Plugins) Demo</p>
|
||||
<p align="center">
|
||||
<img width="800" src="docs/video_decoders.gif" alt="Fastwave - Decoders demo" />
|
||||
</p>
|
||||
|
||||
<p align="center">Decoder Interface</p>
|
||||
<p align="center">
|
||||
<img width="500" src="docs/screenshot_world_wit.png" alt="Fastwave - Decoder Interface" />
|
||||
</p>
|
||||
|
||||
<p align="center">Diagrams - open, edit, JS API</p>
|
||||
<p align="center">
|
||||
<img width="800" src="docs/video_diagrams.gif" alt="Fastwave - Diagrams" />
|
||||
</p>
|
||||
|
||||
<p align="center">Diagram Connector Demo</p>
|
||||
<p align="center">
|
||||
<img width="800" src="docs/video_diagram_connector.gif" alt="Fastwave - Diagram Connector demo" />
|
||||
</p>
|
||||
|
||||
<p align="center">Diagram Connector Code snippet</p>
|
||||
<p align="center">
|
||||
<img width="500" src="docs/screenshot_diagram_connector_rs.png" alt="Fastwave - Diagram Connector Code snippet" />
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
### Install requirements:
|
||||
### Installation (desktop version):
|
||||
|
||||
Download and install from [Releases](https://github.com/JoyOfHardware/FastWave2.0/releases/latest)
|
||||
|
||||
---
|
||||
|
||||
### Requirements to build from source:
|
||||
|
||||
1. Install [Rust](https://www.rust-lang.org/tools/install)
|
||||
2. Install [Node.js](https://nodejs.org/)
|
||||
|
@ -22,7 +96,7 @@
|
|||
|
||||
___
|
||||
|
||||
### Start:
|
||||
### Start the desktop version:
|
||||
|
||||
1. `makers start`
|
||||
|
||||
|
@ -39,21 +113,50 @@ Troubleshooting:
|
|||
|
||||
---
|
||||
|
||||
### Start in the browser:
|
||||
### Production build of the desktop version:
|
||||
|
||||
1. `makers bundle`
|
||||
2. Runnable executable is in `target/release`
|
||||
3. Installable bundles specific for the platform are in `target/release/bundle`
|
||||
|
||||
---
|
||||
|
||||
### Start in a browser:
|
||||
|
||||
1. `makers start_browser`
|
||||
2. Ctrl + Click the server URL mentioned in the terminal log
|
||||
|
||||
---
|
||||
|
||||
### Start in a browser in the release mode:
|
||||
|
||||
1. `makers start_browser_release`
|
||||
2. Ctrl + Click the server URL mentioned in the terminal log
|
||||
|
||||
---
|
||||
|
||||
### Steps before pushing:
|
||||
|
||||
1. `makers format`
|
||||
|
||||
---
|
||||
|
||||
### Production build:
|
||||
### Rebuild Decoders:
|
||||
|
||||
1. `makers bundle`
|
||||
2. Runnable executable is in `target/release`
|
||||
3. Installable bundles specific for the platform are in `target/release/bundle`
|
||||
See `test_files/components/[language]_decoder/README.md`
|
||||
|
||||
---
|
||||
|
||||
### Test files
|
||||
|
||||
See the folder `test_files`.
|
||||
|
||||
---
|
||||
|
||||
# Sponsors
|
||||
|
||||
<p align="center">
|
||||
<a href="https://NLnet.nl">
|
||||
<img src="docs/nlnet_logo.png" width="269" alt="Logo NLnet">
|
||||
</a>
|
||||
</p>
|
||||
|
|
8
backend/favicon.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
<link rel="apple-touch-icon" sizes="180x180" href="/_api/public/favicon/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/_api/public/favicon/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/_api/public/favicon/favicon-16x16.png">
|
||||
<link rel="manifest" href="/_api/public/favicon/site.webmanifest">
|
||||
<link rel="shortcut icon" href="/_api/public/favicon/favicon.ico">
|
||||
<meta name="msapplication-TileColor" content="#00a300">
|
||||
<meta name="msapplication-config" content="/_api/public/favicon/browserconfig.xml">
|
||||
<meta name="theme-color" content="#ffffff">
|
7
backend/globals.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
// -- Excalidraw settings --
|
||||
// @TODO replace with "true" once Preact is integrated into ExcalidrawCanvas
|
||||
var process = { env: { IS_PREACT: "false" } };
|
||||
// @TODO probably remove or update once Preact is integrated into ExcalidrawCanvas
|
||||
window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = { isDisabled: true };
|
||||
window.EXCALIDRAW_ASSET_PATH = "/_api/public/excalidraw/";
|
||||
// -- / --
|
3
backend/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import { FW } from '/_api/pkg/frontend.js';
|
||||
window.FW = FW;
|
||||
|
|
@ -1,11 +1,20 @@
|
|||
use moon::*;
|
||||
|
||||
async fn frontend() -> Frontend {
|
||||
Frontend::new().title("FastWave").append_to_head(concat!(
|
||||
"<style>",
|
||||
include_str!("../style.css"),
|
||||
"</style>"
|
||||
))
|
||||
Frontend::new()
|
||||
.title("FastWave")
|
||||
.append_to_head(include_str!("../favicon.html")) // realfavicongenerator.net
|
||||
.append_to_head(concat!("<style>", include_str!("../style.css"), "</style>"))
|
||||
.append_to_head(concat!(
|
||||
"<script>",
|
||||
include_str!("../globals.js"),
|
||||
"</script>"
|
||||
))
|
||||
.append_to_head(concat!(
|
||||
"<script type=\"module\">",
|
||||
include_str!("../index.js"),
|
||||
"</script>"
|
||||
))
|
||||
}
|
||||
|
||||
async fn up_msg_handler(_: UpMsgRequest<()>) {}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
html {
|
||||
background-color: DarkSlateBlue;
|
||||
}
|
||||
/* background-color: DarkSlateBlue; */
|
||||
background-color: oklch(41.43% 0.125 262.26);
|
||||
}
|
||||
|
|
BIN
docs/fastwave_logo.png
Normal file
After Width: | Height: | Size: 546 KiB |
BIN
docs/fastwave_logo_square.png
Normal file
After Width: | Height: | Size: 378 KiB |
BIN
docs/home_dark.png
Normal file
After Width: | Height: | Size: 94 KiB |
BIN
docs/home_light.png
Normal file
After Width: | Height: | Size: 92 KiB |
BIN
docs/loaded_files_dark.png
Normal file
After Width: | Height: | Size: 168 KiB |
BIN
docs/loaded_files_light.png
Normal file
After Width: | Height: | Size: 166 KiB |
BIN
docs/nlnet_logo.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
docs/screenshot_diagram_connector_rs.png
Normal file
After Width: | Height: | Size: 170 KiB |
BIN
docs/screenshot_world_wit.png
Normal file
After Width: | Height: | Size: 53 KiB |
BIN
docs/video_decoders.gif
Normal file
After Width: | Height: | Size: 764 KiB |
BIN
docs/video_diagram_connector.gif
Normal file
After Width: | Height: | Size: 1.9 MiB |
BIN
docs/video_diagrams.gif
Normal file
After Width: | Height: | Size: 764 KiB |
BIN
docs/video_javascript_commands.gif
Normal file
After Width: | Height: | Size: 593 KiB |
BIN
docs/video_konata.gif
Normal file
After Width: | Height: | Size: 1.8 MiB |
BIN
docs/video_load_save_selected_vars.gif
Normal file
After Width: | Height: | Size: 1.5 MiB |
BIN
docs/video_zoom_formatting.gif
Normal file
After Width: | Height: | Size: 612 KiB |
BIN
docs/video_zoom_formatting_simple.gif
Normal file
After Width: | Height: | Size: 170 KiB |
|
@ -10,9 +10,13 @@ publish.workspace = true
|
|||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.19"
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "allow", check-cfg = ['cfg(FASTWAVE_PLATFORM)'] }
|
||||
|
||||
[dependencies]
|
||||
shared.workspace = true
|
||||
unicode-segmentation = "1.10"
|
||||
zoon.workspace = true
|
||||
wellen.workspace = true
|
||||
shared = { path = "../shared", features = ["frontend"] }
|
||||
web-sys = { version = "*", features = ["FileSystemFileHandle"] }
|
||||
gloo-file = { version = "0.3.0", features = ["futures"] }
|
||||
|
|
141
frontend/src/command_panel.rs
Normal file
|
@ -0,0 +1,141 @@
|
|||
use crate::{script_bridge, theme::*};
|
||||
use zoon::*;
|
||||
|
||||
pub struct CommandPanel {}
|
||||
|
||||
impl CommandPanel {
|
||||
pub fn new() -> impl Element {
|
||||
Self {}.root()
|
||||
}
|
||||
|
||||
fn root(&self) -> impl Element {
|
||||
let command_result: Mutable<Option<Result<JsValue, JsValue>>> = <_>::default();
|
||||
Row::new()
|
||||
.s(Align::new().top())
|
||||
.s(Gap::both(30))
|
||||
.s(Width::fill())
|
||||
.s(Padding::new().x(20).bottom(20))
|
||||
.item(self.command_editor_panel(command_result.clone()))
|
||||
.item(self.command_result_panel(command_result.read_only()))
|
||||
}
|
||||
|
||||
fn command_editor_panel(
|
||||
&self,
|
||||
command_result: Mutable<Option<Result<JsValue, JsValue>>>,
|
||||
) -> impl Element {
|
||||
Column::new()
|
||||
.s(Align::new().top())
|
||||
.s(Gap::new().y(10))
|
||||
.s(Width::growable())
|
||||
.item(
|
||||
Row::new()
|
||||
.s(Gap::new().x(15))
|
||||
.s(Padding::new().x(5))
|
||||
.item(El::new().child("Javascript commands"))
|
||||
.item(El::new().s(Align::new().right()).child("Shift + Enter")),
|
||||
)
|
||||
.item(self.command_editor(command_result))
|
||||
}
|
||||
|
||||
fn command_editor(
|
||||
&self,
|
||||
command_result: Mutable<Option<Result<JsValue, JsValue>>>,
|
||||
) -> impl Element {
|
||||
let (script, script_signal) = Mutable::new_and_signal_cloned(String::new());
|
||||
// @TODO perhaps replace with an element with syntax highlighter like https://github.com/WebCoder49/code-input later
|
||||
TextArea::new()
|
||||
.s(Background::new().color(COLOR_SLATE_BLUE))
|
||||
.s(Padding::new().x(10).y(8))
|
||||
.s(RoundedCorners::all(15))
|
||||
.s(Height::default().min(50))
|
||||
.s(Width::fill().min(300))
|
||||
.s(Font::new()
|
||||
.tracking(1)
|
||||
.weight(FontWeight::Medium)
|
||||
.color(COLOR_WHITE)
|
||||
.family([FontFamily::new("Courier New"), FontFamily::Monospace]))
|
||||
.s(Shadows::new([Shadow::new()
|
||||
.inner()
|
||||
.color(COLOR_DARK_SLATE_BLUE)
|
||||
.blur(4)]))
|
||||
// @TODO `spellcheck` and `resize` to MZ API? (together with autocomplete and others?)
|
||||
.update_raw_el(|raw_el| {
|
||||
raw_el
|
||||
.attr("spellcheck", "false")
|
||||
.style("resize", "vertical")
|
||||
})
|
||||
.placeholder(Placeholder::new("FW.say_hello()").s(Font::new().color(COLOR_LIGHT_BLUE)))
|
||||
.label_hidden("command editor panel")
|
||||
.text_signal(script_signal)
|
||||
.on_change(clone!((script, command_result) move |text| {
|
||||
script.set_neq(text);
|
||||
command_result.set_neq(None);
|
||||
}))
|
||||
.on_key_down_event_with_options(EventOptions::new().preventable(), move |event| {
|
||||
if event.key() == &Key::Enter {
|
||||
let RawKeyboardEvent::KeyDown(raw_event) = event.raw_event.clone();
|
||||
if raw_event.shift_key() {
|
||||
// @TODO move `prevent_default` to MZ API (next to the `pass_to_parent` method?)
|
||||
raw_event.prevent_default();
|
||||
Task::start(clone!((script, command_result) async move {
|
||||
let result = script_bridge::strict_eval(&script.lock_ref()).await;
|
||||
command_result.set(Some(result));
|
||||
}));
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn command_result_panel(
|
||||
&self,
|
||||
command_result: ReadOnlyMutable<Option<Result<JsValue, JsValue>>>,
|
||||
) -> impl Element {
|
||||
Column::new()
|
||||
.s(Gap::new().y(10))
|
||||
.s(Align::new().top())
|
||||
.s(Scrollbars::both())
|
||||
.s(Padding::new().x(5))
|
||||
.s(Width::growable().max(750))
|
||||
.item(El::new().child("Command result"))
|
||||
.item(self.command_result_el(command_result))
|
||||
}
|
||||
|
||||
fn command_result_el(
|
||||
&self,
|
||||
command_result: ReadOnlyMutable<Option<Result<JsValue, JsValue>>>,
|
||||
) -> impl Element {
|
||||
El::new()
|
||||
.s(Font::new()
|
||||
.tracking(1)
|
||||
.weight(FontWeight::Medium)
|
||||
.color(COLOR_WHITE)
|
||||
.family([FontFamily::new("Courier New"), FontFamily::Monospace]))
|
||||
.s(Scrollbars::both())
|
||||
.s(Height::default().max(100))
|
||||
.child_signal(command_result.signal_ref(|result| {
|
||||
fn format_complex_js_value(js_value: &JsValue) -> String {
|
||||
let value = format!("{js_value:?}");
|
||||
let value = value.strip_prefix("JsValue(").unwrap_throw();
|
||||
let value = value.strip_suffix(')').unwrap_throw();
|
||||
value.to_owned()
|
||||
}
|
||||
match result {
|
||||
Some(Ok(js_value)) => {
|
||||
if let Some(string_value) = js_value.as_string() {
|
||||
string_value
|
||||
} else if let Some(number_value) = js_value.as_f64() {
|
||||
number_value.to_string()
|
||||
} else if let Some(bool_value) = js_value.as_bool() {
|
||||
bool_value.to_string()
|
||||
} else {
|
||||
format_complex_js_value(js_value)
|
||||
}
|
||||
}
|
||||
Some(Err(js_value)) => {
|
||||
format!("ERROR: {}", format_complex_js_value(js_value))
|
||||
}
|
||||
None => "-".to_owned(),
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
|
@ -1,16 +1,15 @@
|
|||
use crate::{platform, HierarchyAndTimeTable, Layout};
|
||||
use futures_util::join;
|
||||
use crate::{theme::*, Filename, Layout};
|
||||
use std::cell::Cell;
|
||||
use std::mem;
|
||||
use std::ops::Not;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use wellen::GetItem;
|
||||
use zoon::*;
|
||||
|
||||
const SCOPE_VAR_ROW_MAX_WIDTH: u32 = 480;
|
||||
const MILLER_COLUMN_SCOPE_VAR_ROW_MIN_WIDTH: u32 = 480;
|
||||
const MILLER_COLUMN_MAX_HEIGHT: u32 = 500;
|
||||
|
||||
type Filename = String;
|
||||
const TREE_MAX_WIDTH: u32 = 600;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct VarForUI {
|
||||
|
@ -35,7 +34,7 @@ struct ScopeForUI {
|
|||
#[derive(Clone)]
|
||||
pub struct ControlsPanel {
|
||||
selected_scope_ref: Mutable<Option<wellen::ScopeRef>>,
|
||||
hierarchy_and_time_table: Mutable<Option<HierarchyAndTimeTable>>,
|
||||
hierarchy: Mutable<Option<Arc<wellen::Hierarchy>>>,
|
||||
selected_var_refs: MutableVec<wellen::VarRef>,
|
||||
layout: Mutable<Layout>,
|
||||
loaded_filename: Mutable<Option<Filename>>,
|
||||
|
@ -43,16 +42,17 @@ pub struct ControlsPanel {
|
|||
|
||||
impl ControlsPanel {
|
||||
pub fn new(
|
||||
hierarchy_and_time_table: Mutable<Option<HierarchyAndTimeTable>>,
|
||||
hierarchy: Mutable<Option<Arc<wellen::Hierarchy>>>,
|
||||
selected_var_refs: MutableVec<wellen::VarRef>,
|
||||
layout: Mutable<Layout>,
|
||||
loaded_filename: Mutable<Option<Filename>>,
|
||||
) -> impl Element {
|
||||
Self {
|
||||
selected_scope_ref: <_>::default(),
|
||||
hierarchy_and_time_table,
|
||||
hierarchy,
|
||||
selected_var_refs,
|
||||
layout,
|
||||
loaded_filename: <_>::default(),
|
||||
loaded_filename,
|
||||
}
|
||||
.root()
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ impl ControlsPanel {
|
|||
fn triggers(&self) -> Vec<TaskHandle> {
|
||||
vec![Task::start_droppable(clone!((self => s) async move {
|
||||
let was_some = Cell::new(false);
|
||||
s.hierarchy_and_time_table
|
||||
s.hierarchy
|
||||
.signal_ref(Option::is_some)
|
||||
.dedupe()
|
||||
.for_each_sync(clone!((s) move |is_some| {
|
||||
|
@ -81,8 +81,8 @@ impl ControlsPanel {
|
|||
let layout = self.layout.clone();
|
||||
let layout_and_hierarchy_signal = map_ref! {
|
||||
let layout = layout.signal(),
|
||||
let hierarchy_and_time_table = self.hierarchy_and_time_table.signal_cloned() => {
|
||||
(*layout, hierarchy_and_time_table.clone().map(|(hierarchy, _)| hierarchy))
|
||||
let hierarchy = self.hierarchy.signal_cloned() => {
|
||||
(*layout, hierarchy.clone())
|
||||
}
|
||||
};
|
||||
Column::new()
|
||||
|
@ -93,27 +93,25 @@ impl ControlsPanel {
|
|||
.map(|layout| matches!(layout, Layout::Columns))
|
||||
.map_true(|| Width::fill()),
|
||||
))
|
||||
.s(Width::with_signal_self(layout.signal().map(
|
||||
move |layout| match layout {
|
||||
Layout::Tree => Width::growable().max(TREE_MAX_WIDTH),
|
||||
Layout::Columns => Width::fill(),
|
||||
},
|
||||
)))
|
||||
.s(Height::with_signal_self(layout.signal().map(
|
||||
move |layout| match layout {
|
||||
Layout::Tree => Height::fill(),
|
||||
Layout::Columns => Height::fill().max(MILLER_COLUMN_MAX_HEIGHT),
|
||||
},
|
||||
)))
|
||||
.s(Scrollbars::both())
|
||||
.s(Padding::all(20))
|
||||
.s(Gap::new().y(40))
|
||||
.s(Align::new().top())
|
||||
.item(
|
||||
Row::new()
|
||||
.s(Gap::both(15))
|
||||
.s(Align::new().left())
|
||||
.item(self.load_button())
|
||||
.item(self.layout_switcher()),
|
||||
)
|
||||
.item_signal(
|
||||
self.hierarchy_and_time_table
|
||||
self.hierarchy
|
||||
.signal_cloned()
|
||||
.map_some(clone!((self => s) move |(hierarchy, _)| s.scopes_panel(hierarchy))),
|
||||
.map_some(clone!((self => s) move |hierarchy| s.scopes_panel(hierarchy))),
|
||||
)
|
||||
.item_signal(layout_and_hierarchy_signal.map(
|
||||
clone!((self => s) move |(layout, hierarchy)| {
|
||||
|
@ -124,143 +122,9 @@ impl ControlsPanel {
|
|||
))
|
||||
}
|
||||
|
||||
#[cfg(FASTWAVE_PLATFORM = "TAURI")]
|
||||
fn load_button(&self) -> impl Element {
|
||||
let (hovered, hovered_signal) = Mutable::new_and_signal(false);
|
||||
let hierarchy_and_time_table = self.hierarchy_and_time_table.clone();
|
||||
let loaded_filename = self.loaded_filename.clone();
|
||||
Button::new()
|
||||
.s(Padding::new().x(20).y(10))
|
||||
.s(Background::new().color_signal(
|
||||
hovered_signal.map_bool(|| color!("MediumSlateBlue"), || color!("SlateBlue")),
|
||||
))
|
||||
.s(Align::new().left())
|
||||
.s(RoundedCorners::all(15))
|
||||
.label(El::new().s(Font::new().no_wrap()).child_signal(
|
||||
loaded_filename.signal_cloned().map_option(
|
||||
|filename| format!("Unload {filename}"),
|
||||
|| format!("Load file.."),
|
||||
),
|
||||
))
|
||||
.on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered))
|
||||
.on_press(move || {
|
||||
let mut hierarchy_and_time_table_lock = hierarchy_and_time_table.lock_mut();
|
||||
if hierarchy_and_time_table_lock.is_some() {
|
||||
*hierarchy_and_time_table_lock = None;
|
||||
return;
|
||||
}
|
||||
drop(hierarchy_and_time_table_lock);
|
||||
let hierarchy_and_time_table = hierarchy_and_time_table.clone();
|
||||
let loaded_filename = loaded_filename.clone();
|
||||
Task::start(async move {
|
||||
if let Some(filename) = platform::pick_and_load_waveform(None).await {
|
||||
loaded_filename.set_neq(Some(filename));
|
||||
let (hierarchy, time_table) =
|
||||
join!(platform::get_hierarchy(), platform::get_time_table());
|
||||
hierarchy_and_time_table
|
||||
.set(Some((Rc::new(hierarchy), Rc::new(time_table))))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(FASTWAVE_PLATFORM = "BROWSER")]
|
||||
fn load_button(&self) -> impl Element {
|
||||
let (hovered, hovered_signal) = Mutable::new_and_signal(false);
|
||||
let hierarchy_and_time_table = self.hierarchy_and_time_table.clone();
|
||||
let loaded_filename = self.loaded_filename.clone();
|
||||
let file_input_id = "file_input";
|
||||
Row::new()
|
||||
.item(
|
||||
Label::new()
|
||||
.s(Padding::new().x(20).y(10))
|
||||
.s(Background::new().color_signal(
|
||||
hovered_signal.map_bool(|| color!("MediumSlateBlue"), || color!("SlateBlue")),
|
||||
))
|
||||
.s(Align::new().left())
|
||||
.s(RoundedCorners::all(15))
|
||||
.s(Cursor::new(CursorIcon::Pointer))
|
||||
.label(El::new().s(Font::new().no_wrap()).child_signal(
|
||||
loaded_filename.signal_cloned().map_option(
|
||||
|filename| format!("Unload {filename}"),
|
||||
|| format!("Load file.."),
|
||||
),
|
||||
))
|
||||
.on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered))
|
||||
.for_input(file_input_id)
|
||||
.on_click_event_with_options(EventOptions::new().preventable(), clone!((hierarchy_and_time_table) move |event| {
|
||||
let mut hierarchy_and_time_table_lock = hierarchy_and_time_table.lock_mut();
|
||||
if hierarchy_and_time_table_lock.is_some() {
|
||||
*hierarchy_and_time_table_lock = None;
|
||||
if let RawMouseEvent::Click(raw_event) = event.raw_event {
|
||||
// @TODO Move to MoonZoon as a new API
|
||||
raw_event.prevent_default();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}))
|
||||
)
|
||||
.item(
|
||||
// @TODO https://github.com/MoonZoon/MoonZoon/issues/39
|
||||
// + https://developer.mozilla.org/en-US/docs/Web/API/File_API/Using_files_from_web_applications#using_hidden_file_input_elements_using_the_click_method
|
||||
TextInput::new()
|
||||
.id(file_input_id)
|
||||
.update_raw_el(|raw_el| {
|
||||
let dom_element = raw_el.dom_element();
|
||||
raw_el
|
||||
.style("display", "none")
|
||||
.attr("type", "file")
|
||||
.event_handler(move |_: events::Input| {
|
||||
let Some(file_list) = dom_element.files().map(gloo_file::FileList::from) else {
|
||||
zoon::println!("file list is `None`");
|
||||
return;
|
||||
};
|
||||
let Some(file) = file_list.first().cloned() else {
|
||||
zoon::println!("file list is empty");
|
||||
return;
|
||||
};
|
||||
let hierarchy_and_time_table = hierarchy_and_time_table.clone();
|
||||
let loaded_filename = loaded_filename.clone();
|
||||
Task::start(async move {
|
||||
if let Some(filename) = platform::pick_and_load_waveform(Some(file)).await {
|
||||
loaded_filename.set_neq(Some(filename));
|
||||
let (hierarchy, time_table) =
|
||||
join!(platform::get_hierarchy(), platform::get_time_table());
|
||||
hierarchy_and_time_table
|
||||
.set(Some((Rc::new(hierarchy), Rc::new(time_table))))
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
fn layout_switcher(&self) -> impl Element {
|
||||
let layout = self.layout.clone();
|
||||
let (hovered, hovered_signal) = Mutable::new_and_signal(false);
|
||||
Button::new()
|
||||
.s(Padding::new().x(20).y(10))
|
||||
.s(Background::new().color_signal(
|
||||
hovered_signal.map_bool(|| color!("MediumSlateBlue"), || color!("SlateBlue")),
|
||||
))
|
||||
.s(Align::new().left())
|
||||
.s(RoundedCorners::all(15))
|
||||
.label_signal(layout.signal().map(|layout| match layout {
|
||||
Layout::Tree => "Columns",
|
||||
Layout::Columns => "Tree",
|
||||
}))
|
||||
.on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered))
|
||||
.on_press(move || {
|
||||
layout.update(|layout| match layout {
|
||||
Layout::Tree => Layout::Columns,
|
||||
Layout::Columns => Layout::Tree,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn scopes_panel(&self, hierarchy: Rc<wellen::Hierarchy>) -> impl Element {
|
||||
fn scopes_panel(&self, hierarchy: Arc<wellen::Hierarchy>) -> impl Element {
|
||||
Column::new()
|
||||
.s(Height::fill().min(150))
|
||||
.s(Height::fill())
|
||||
.s(Scrollbars::y_and_clip_x())
|
||||
.s(Gap::new().y(20))
|
||||
.s(Width::fill())
|
||||
|
@ -273,7 +137,7 @@ impl ControlsPanel {
|
|||
.item(self.scopes_list(hierarchy))
|
||||
}
|
||||
|
||||
fn scopes_list(&self, hierarchy: Rc<wellen::Hierarchy>) -> impl Element {
|
||||
fn scopes_list(&self, hierarchy: Arc<wellen::Hierarchy>) -> impl Element {
|
||||
let layout = self.layout.clone();
|
||||
let mut scopes_for_ui = Vec::new();
|
||||
let mut max_level_index: usize = 0;
|
||||
|
@ -309,7 +173,7 @@ impl ControlsPanel {
|
|||
let s = self.clone();
|
||||
El::new()
|
||||
.s(Height::fill())
|
||||
.s(Scrollbars::both())
|
||||
.s(Scrollbars::y_and_clip_x())
|
||||
.s(Width::fill())
|
||||
.child_signal(layout.signal().map(move |layout| match layout {
|
||||
Layout::Tree => {
|
||||
|
@ -374,9 +238,9 @@ impl ControlsPanel {
|
|||
let background_color = map_ref! {
|
||||
let is_selected = is_selected,
|
||||
let is_hovered = button_hovered_signal => match (*is_selected, *is_hovered) {
|
||||
(true, _) => color!("BlueViolet"),
|
||||
(false, true) => color!("MediumSlateBlue"),
|
||||
(false, false) => color!("SlateBlue"),
|
||||
(true, _) => COLOR_BLUE_VIOLET,
|
||||
(false, true) => COLOR_MEDIUM_SLATE_BLUE,
|
||||
(false, false) => COLOR_SLATE_BLUE,
|
||||
}
|
||||
};
|
||||
let task_collapse_on_parent_collapse = {
|
||||
|
@ -423,7 +287,6 @@ impl ControlsPanel {
|
|||
Layout::Tree => level * 30,
|
||||
Layout::Columns => 0,
|
||||
})))
|
||||
.s(Width::default().max(SCOPE_VAR_ROW_MAX_WIDTH))
|
||||
.after_remove(move |_| {
|
||||
drop(task_collapse_on_parent_collapse);
|
||||
drop(task_expand_or_collapse_on_selected_scope_in_level_change);
|
||||
|
@ -469,7 +332,7 @@ impl ControlsPanel {
|
|||
.map_true(|| 10),
|
||||
))
|
||||
.s(Height::fill())
|
||||
.s(Font::new().color_signal(hovered_signal.map_true(|| color!("LightBlue"))))
|
||||
.s(Font::new().color_signal(hovered_signal.map_true(|| COLOR_LIGHT_BLUE)))
|
||||
.label(
|
||||
El::new()
|
||||
.s(Transform::with_signal_self(layout_and_expanded.map(
|
||||
|
@ -514,7 +377,6 @@ impl ControlsPanel {
|
|||
) -> impl Element {
|
||||
Button::new()
|
||||
.s(Padding::new().x(15).y(5))
|
||||
.s(Font::new().wrap_anywhere())
|
||||
.on_hovered_change(move |is_hovered| button_hovered.set_neq(is_hovered))
|
||||
.on_press(
|
||||
clone!((self.selected_scope_ref => selected_scope_ref, scope_for_ui) move || {
|
||||
|
@ -525,12 +387,14 @@ impl ControlsPanel {
|
|||
.label(scope_for_ui.name)
|
||||
}
|
||||
|
||||
fn vars_panel(&self, hierarchy: Rc<wellen::Hierarchy>) -> impl Element {
|
||||
fn vars_panel(&self, hierarchy: Arc<wellen::Hierarchy>) -> impl Element {
|
||||
let selected_scope_ref = self.selected_scope_ref.clone();
|
||||
Column::new()
|
||||
.s(Align::new().top())
|
||||
.s(Gap::new().y(20))
|
||||
.s(Height::fill().min(150))
|
||||
.s(Scrollbars::y_and_clip_x())
|
||||
.s(Width::fill())
|
||||
.s(Scrollbars::both())
|
||||
.item_signal(
|
||||
self.layout
|
||||
.signal()
|
||||
|
@ -546,7 +410,7 @@ impl ControlsPanel {
|
|||
fn vars_list(
|
||||
&self,
|
||||
selected_scope_ref: wellen::ScopeRef,
|
||||
hierarchy: Rc<wellen::Hierarchy>,
|
||||
hierarchy: Arc<wellen::Hierarchy>,
|
||||
) -> impl Element {
|
||||
let vars_for_ui = hierarchy
|
||||
.get(selected_scope_ref)
|
||||
|
@ -563,6 +427,7 @@ impl ControlsPanel {
|
|||
});
|
||||
|
||||
// Lazy loading to not freeze the main thread
|
||||
// @TODO replace with grouping and/or virtual scroll (https://dev.to/adamklein/build-your-own-virtual-scroll-part-i-11ib)
|
||||
const CHUNK_SIZE: usize = 50;
|
||||
let mut chunked_vars_for_ui: Vec<Vec<VarForUI>> = <_>::default();
|
||||
let mut chunk = Vec::with_capacity(CHUNK_SIZE);
|
||||
|
@ -584,17 +449,18 @@ impl ControlsPanel {
|
|||
}
|
||||
}));
|
||||
|
||||
let layout = self.layout.clone();
|
||||
Column::new()
|
||||
.s(Width::with_signal_self(
|
||||
self.layout
|
||||
.signal()
|
||||
.map(|layout| matches!(layout, Layout::Columns))
|
||||
.map_true(|| Width::default().min(SCOPE_VAR_ROW_MAX_WIDTH)),
|
||||
))
|
||||
.s(Width::with_signal_self(layout.signal().map(
|
||||
move |layout| match layout {
|
||||
Layout::Tree => Width::fill(),
|
||||
Layout::Columns => Width::default().min(MILLER_COLUMN_SCOPE_VAR_ROW_MIN_WIDTH),
|
||||
},
|
||||
)))
|
||||
.s(Align::new().left())
|
||||
.s(Gap::new().y(10))
|
||||
.s(Height::fill())
|
||||
.s(Scrollbars::y_and_clip_x())
|
||||
.s(Scrollbars::both())
|
||||
.items_signal_vec(
|
||||
vars_for_ui_mutable_vec
|
||||
.signal_vec_cloned()
|
||||
|
@ -606,13 +472,13 @@ impl ControlsPanel {
|
|||
fn var_row(&self, var_for_ui: VarForUI) -> impl Element {
|
||||
Row::new()
|
||||
.s(Gap::new().x(10))
|
||||
.s(Padding::new().right(15))
|
||||
.s(Width::default().max(SCOPE_VAR_ROW_MAX_WIDTH))
|
||||
.item(self.var_button(var_for_ui.clone()))
|
||||
.item(self.var_tag_type(var_for_ui.clone()))
|
||||
.item(self.var_tag_index(var_for_ui.clone()))
|
||||
.item(self.var_tag_bit(var_for_ui.clone()))
|
||||
.item(self.var_tag_direction(var_for_ui))
|
||||
// Note: Padding or 0 height don't work for some reasons here
|
||||
.item(El::new().s(Width::exact(10)).s(Height::exact(1)))
|
||||
}
|
||||
|
||||
fn var_button(&self, var_for_ui: VarForUI) -> impl Element {
|
||||
|
@ -620,10 +486,9 @@ impl ControlsPanel {
|
|||
let selected_var_ref = self.selected_var_refs.clone();
|
||||
El::new().child(
|
||||
Button::new()
|
||||
.s(Font::new().wrap_anywhere())
|
||||
.s(Padding::new().x(15).y(5))
|
||||
.s(Background::new().color_signal(
|
||||
hovered_signal.map_bool(|| color!("MediumSlateBlue"), || color!("SlateBlue")),
|
||||
hovered_signal.map_bool(|| COLOR_MEDIUM_SLATE_BLUE, || COLOR_SLATE_BLUE),
|
||||
))
|
||||
.s(RoundedCorners::all(15))
|
||||
.on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered))
|
||||
|
|
40
frontend/src/diagram_panel.rs
Normal file
|
@ -0,0 +1,40 @@
|
|||
use zoon::*;
|
||||
|
||||
mod excalidraw_canvas;
|
||||
use excalidraw_canvas::ExcalidrawCanvas;
|
||||
pub use excalidraw_canvas::ExcalidrawController;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DiagramPanel {
|
||||
canvas_controller: Mutable<Mutable<Option<SendWrapper<ExcalidrawController>>>>,
|
||||
}
|
||||
|
||||
impl DiagramPanel {
|
||||
pub fn new(
|
||||
canvas_controller: Mutable<Mutable<Option<SendWrapper<ExcalidrawController>>>>,
|
||||
) -> impl Element {
|
||||
Self { canvas_controller }.root()
|
||||
}
|
||||
|
||||
fn root(&self) -> impl Element {
|
||||
Column::new()
|
||||
.s(Padding::all(20))
|
||||
.s(Scrollbars::y_and_clip_x())
|
||||
.s(Width::fill())
|
||||
.s(Height::fill())
|
||||
.s(Gap::new().y(20))
|
||||
.item(self.canvas())
|
||||
}
|
||||
|
||||
fn canvas(&self) -> impl Element {
|
||||
let canvas_controller = self.canvas_controller.clone();
|
||||
ExcalidrawCanvas::new()
|
||||
.s(Align::new().top())
|
||||
.s(Width::fill())
|
||||
.s(Height::fill())
|
||||
.task_with_controller(move |controller| {
|
||||
canvas_controller.set(controller.clone());
|
||||
async {}
|
||||
})
|
||||
}
|
||||
}
|
90
frontend/src/diagram_panel/excalidraw_canvas.rs
Normal file
|
@ -0,0 +1,90 @@
|
|||
pub use js_bridge::ExcalidrawController;
|
||||
use zoon::*;
|
||||
|
||||
pub struct ExcalidrawCanvas {
|
||||
raw_el: RawHtmlEl<web_sys::HtmlElement>,
|
||||
controller: Mutable<Option<SendWrapper<js_bridge::ExcalidrawController>>>,
|
||||
task_with_controller: Mutable<Option<TaskHandle>>,
|
||||
}
|
||||
|
||||
impl Element for ExcalidrawCanvas {}
|
||||
|
||||
impl RawElWrapper for ExcalidrawCanvas {
|
||||
type RawEl = RawHtmlEl<web_sys::HtmlElement>;
|
||||
fn raw_el_mut(&mut self) -> &mut Self::RawEl {
|
||||
&mut self.raw_el
|
||||
}
|
||||
}
|
||||
|
||||
impl Styleable<'_> for ExcalidrawCanvas {}
|
||||
impl KeyboardEventAware for ExcalidrawCanvas {}
|
||||
impl MouseEventAware for ExcalidrawCanvas {}
|
||||
impl PointerEventAware for ExcalidrawCanvas {}
|
||||
impl TouchEventAware for ExcalidrawCanvas {}
|
||||
impl AddNearbyElement<'_> for ExcalidrawCanvas {}
|
||||
impl HasIds for ExcalidrawCanvas {}
|
||||
|
||||
impl ExcalidrawCanvas {
|
||||
pub fn new() -> Self {
|
||||
let controller: Mutable<Option<SendWrapper<js_bridge::ExcalidrawController>>> =
|
||||
Mutable::new(None);
|
||||
let task_with_controller = Mutable::new(None);
|
||||
Self {
|
||||
controller: controller.clone(),
|
||||
task_with_controller: task_with_controller.clone(),
|
||||
raw_el: El::new()
|
||||
.s(RoundedCorners::all(10))
|
||||
.s(Clip::both())
|
||||
.after_insert(clone!((controller) move |element| {
|
||||
Task::start(async move {
|
||||
let excalidraw_controller = SendWrapper::new(js_bridge::ExcalidrawController::new());
|
||||
excalidraw_controller.init(&element).await;
|
||||
controller.set(Some(excalidraw_controller));
|
||||
});
|
||||
}))
|
||||
.after_remove(move |_| {
|
||||
drop(task_with_controller);
|
||||
})
|
||||
.into_raw_el(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn task_with_controller<FUT: Future<Output = ()> + 'static>(
|
||||
self,
|
||||
f: impl FnOnce(Mutable<Option<SendWrapper<js_bridge::ExcalidrawController>>>) -> FUT,
|
||||
) -> Self {
|
||||
self.task_with_controller
|
||||
.set(Some(Task::start_droppable(f(self.controller.clone()))));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
mod js_bridge {
|
||||
use zoon::*;
|
||||
|
||||
// Note: Add all corresponding methods to `frontend/typescript/excalidraw_canvas/excalidraw_canvas.tsx`
|
||||
#[wasm_bindgen(module = "/typescript/bundles/excalidraw_canvas.js")]
|
||||
extern "C" {
|
||||
#[derive(Clone)]
|
||||
pub type ExcalidrawController;
|
||||
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new() -> ExcalidrawController;
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub async fn init(this: &ExcalidrawController, parent_element: &JsValue);
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn draw_diagram_element(this: &ExcalidrawController, excalidraw_element: JsValue);
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn listen_for_component_text_changes(
|
||||
this: &ExcalidrawController,
|
||||
component_id: &str,
|
||||
on_change: &Closure<dyn Fn(String)>,
|
||||
);
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn set_component_text(this: &ExcalidrawController, component_id: &str, text: &str);
|
||||
}
|
||||
}
|
239
frontend/src/header_panel.rs
Normal file
|
@ -0,0 +1,239 @@
|
|||
use crate::{platform, theme::*, Filename, Layout, Mode};
|
||||
use std::sync::Arc;
|
||||
use zoon::*;
|
||||
use crate::term::TERM_OPEN;
|
||||
|
||||
pub struct HeaderPanel {
|
||||
hierarchy: Mutable<Option<Arc<wellen::Hierarchy>>>,
|
||||
layout: Mutable<Layout>,
|
||||
mode: Mutable<Mode>,
|
||||
loaded_filename: Mutable<Option<Filename>>,
|
||||
}
|
||||
|
||||
impl HeaderPanel {
|
||||
pub fn new(
|
||||
hierarchy: Mutable<Option<Arc<wellen::Hierarchy>>>,
|
||||
layout: Mutable<Layout>,
|
||||
mode: Mutable<Mode>,
|
||||
loaded_filename: Mutable<Option<Filename>>,
|
||||
) -> impl Element {
|
||||
Self {
|
||||
hierarchy,
|
||||
layout,
|
||||
loaded_filename,
|
||||
mode,
|
||||
}
|
||||
.root()
|
||||
}
|
||||
|
||||
fn root(&self) -> impl Element {
|
||||
Row::new()
|
||||
.s(Padding::new().x(20).y(15))
|
||||
.s(Gap::both(40))
|
||||
.item(
|
||||
Row::new()
|
||||
.s(Align::new().top())
|
||||
.s(Padding::new().top(5))
|
||||
.s(Gap::both(15))
|
||||
.item(self.load_button())
|
||||
.item(self.layout_switcher())
|
||||
.item(self.mode_switcher())
|
||||
.item(self.open_terminal())
|
||||
.item(self.open_konata_file()),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(FASTWAVE_PLATFORM = "TAURI")]
|
||||
fn load_button(&self) -> impl Element {
|
||||
let (hovered, hovered_signal) = Mutable::new_and_signal(false);
|
||||
let hierarchy = self.hierarchy.clone();
|
||||
let loaded_filename = self.loaded_filename.clone();
|
||||
Button::new()
|
||||
.s(Padding::new().x(20).y(10))
|
||||
.s(Background::new().color_signal(
|
||||
hovered_signal.map_bool(|| COLOR_MEDIUM_SLATE_BLUE, || COLOR_SLATE_BLUE),
|
||||
))
|
||||
.s(Align::new().left())
|
||||
.s(RoundedCorners::all(15))
|
||||
.label(El::new().s(Font::new().no_wrap()).child_signal(
|
||||
loaded_filename.signal_cloned().map_option(
|
||||
|filename| format!("Unload {filename}"),
|
||||
|| format!("Load file.."),
|
||||
),
|
||||
))
|
||||
.on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered))
|
||||
.on_press(move || {
|
||||
let mut hierarchy_lock = hierarchy.lock_mut();
|
||||
if hierarchy_lock.is_some() {
|
||||
*hierarchy_lock = None;
|
||||
return;
|
||||
}
|
||||
drop(hierarchy_lock);
|
||||
let hierarchy = hierarchy.clone();
|
||||
let loaded_filename = loaded_filename.clone();
|
||||
Task::start(async move {
|
||||
if let Some(filename) = platform::pick_and_load_waveform(None).await {
|
||||
loaded_filename.set_neq(Some(filename));
|
||||
hierarchy.set(Some(Arc::new(platform::get_hierarchy().await)))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(FASTWAVE_PLATFORM = "BROWSER")]
|
||||
fn load_button(&self) -> impl Element {
|
||||
let (hovered, hovered_signal) = Mutable::new_and_signal(false);
|
||||
let hierarchy = self.hierarchy.clone();
|
||||
let loaded_filename = self.loaded_filename.clone();
|
||||
let file_input_id = "file_input_for_load_waveform_button";
|
||||
Row::new()
|
||||
.item(
|
||||
Label::new()
|
||||
.s(Padding::new().x(20).y(10))
|
||||
.s(Background::new().color_signal(
|
||||
hovered_signal.map_bool(|| COLOR_MEDIUM_SLATE_BLUE, || COLOR_SLATE_BLUE),
|
||||
))
|
||||
.s(Align::new().left())
|
||||
.s(RoundedCorners::all(15))
|
||||
.s(Cursor::new(CursorIcon::Pointer))
|
||||
.label(El::new().s(Font::new().no_wrap()).child_signal(
|
||||
loaded_filename.signal_cloned().map_option(
|
||||
|filename| format!("Unload {filename}"),
|
||||
|| format!("Load file.."),
|
||||
),
|
||||
))
|
||||
.on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered))
|
||||
.for_input(file_input_id)
|
||||
.on_click_event_with_options(
|
||||
EventOptions::new().preventable(),
|
||||
clone!((hierarchy) move |event| {
|
||||
let mut hierarchy_lock = hierarchy.lock_mut();
|
||||
if hierarchy_lock.is_some() {
|
||||
*hierarchy_lock = None;
|
||||
if let RawMouseEvent::Click(raw_event) = event.raw_event {
|
||||
// @TODO Move to MoonZoon as a new API
|
||||
raw_event.prevent_default();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.item(
|
||||
// @TODO https://github.com/MoonZoon/MoonZoon/issues/39
|
||||
// + https://developer.mozilla.org/en-US/docs/Web/API/File_API/Using_files_from_web_applications#using_hidden_file_input_elements_using_the_click_method
|
||||
TextInput::new().id(file_input_id).update_raw_el(|raw_el| {
|
||||
let dom_element = raw_el.dom_element();
|
||||
raw_el
|
||||
.style("display", "none")
|
||||
.attr("type", "file")
|
||||
.event_handler(move |_: events::Input| {
|
||||
let Some(file_list) =
|
||||
dom_element.files().map(gloo_file::FileList::from)
|
||||
else {
|
||||
zoon::println!("file list is `None`");
|
||||
return;
|
||||
};
|
||||
let Some(file) = file_list.first().cloned() else {
|
||||
zoon::println!("file list is empty");
|
||||
return;
|
||||
};
|
||||
let hierarchy = hierarchy.clone();
|
||||
let loaded_filename = loaded_filename.clone();
|
||||
Task::start(async move {
|
||||
if let Some(filename) =
|
||||
platform::pick_and_load_waveform(Some(file)).await
|
||||
{
|
||||
loaded_filename.set_neq(Some(filename));
|
||||
hierarchy.set(Some(Arc::new(platform::get_hierarchy().await)))
|
||||
}
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn layout_switcher(&self) -> impl Element {
|
||||
let layout = self.layout.clone();
|
||||
let (hovered, hovered_signal) = Mutable::new_and_signal(false);
|
||||
Button::new()
|
||||
.s(Padding::new().x(20).y(10))
|
||||
.s(Background::new().color_signal(
|
||||
hovered_signal.map_bool(|| COLOR_MEDIUM_SLATE_BLUE, || COLOR_SLATE_BLUE),
|
||||
))
|
||||
.s(RoundedCorners::all(15))
|
||||
.label_signal(layout.signal().map(|layout| match layout {
|
||||
Layout::Tree => "Columns",
|
||||
Layout::Columns => "Tree",
|
||||
}))
|
||||
.on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered))
|
||||
.on_press(move || {
|
||||
layout.update(|layout| match layout {
|
||||
Layout::Tree => Layout::Columns,
|
||||
Layout::Columns => Layout::Tree,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn mode_switcher(&self) -> impl Element {
|
||||
let mode = self.mode.clone();
|
||||
let (hovered, hovered_signal) = Mutable::new_and_signal(false);
|
||||
Button::new()
|
||||
.s(Padding::new().x(20).y(10))
|
||||
.s(Background::new().color_signal(
|
||||
hovered_signal.map_bool(|| COLOR_MEDIUM_SLATE_BLUE, || COLOR_SLATE_BLUE),
|
||||
))
|
||||
.s(RoundedCorners::all(15))
|
||||
.label_signal(mode.signal().map(|mode| match mode {
|
||||
Mode::Waves => "Diagrams",
|
||||
Mode::Diagrams => "Waves",
|
||||
}))
|
||||
.on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered))
|
||||
.on_press(move || {
|
||||
mode.update(|mode| match mode {
|
||||
Mode::Waves => Mode::Diagrams,
|
||||
Mode::Diagrams => Mode::Waves,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn open_konata_file(&self) -> impl Element {
|
||||
let (hovered, hovered_signal) = Mutable::new_and_signal(false);
|
||||
Button::new()
|
||||
.s(Padding::new().x(20).y(10))
|
||||
.s(Background::new().color_signal(
|
||||
hovered_signal.map_bool(|| COLOR_MEDIUM_SLATE_BLUE, || COLOR_SLATE_BLUE),
|
||||
))
|
||||
.s(Align::new().left())
|
||||
.s(RoundedCorners::all(15))
|
||||
.label(
|
||||
El::new()
|
||||
.s(Font::new().no_wrap())
|
||||
.child("Open Konata file.."),
|
||||
)
|
||||
.on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered))
|
||||
.on_press(move || Task::start(platform::open_konata_file()))
|
||||
}
|
||||
|
||||
fn open_terminal(&self) -> impl Element {
|
||||
let (hovered, hovered_signal) = Mutable::new_and_signal(false);
|
||||
Button::new()
|
||||
.s(Padding::new().x(20).y(10))
|
||||
.s(Background::new().color_signal(
|
||||
hovered_signal.map_bool(|| COLOR_MEDIUM_SLATE_BLUE, || COLOR_SLATE_BLUE),
|
||||
))
|
||||
.s(Align::new().left())
|
||||
.s(RoundedCorners::all(15))
|
||||
.label(
|
||||
El::new()
|
||||
.s(Font::new().no_wrap())
|
||||
.child("Open Terminal"),
|
||||
)
|
||||
.on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered))
|
||||
.on_press(move || {
|
||||
let term_open = TERM_OPEN.get();
|
||||
TERM_OPEN.set(!term_open);
|
||||
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,15 +1,31 @@
|
|||
use std::rc::Rc;
|
||||
use shared::DiagramConnectorMessage;
|
||||
use term::TERM_OPEN;
|
||||
use std::{mem, sync::Arc};
|
||||
use zoon::*;
|
||||
|
||||
mod platform;
|
||||
mod script_bridge;
|
||||
|
||||
mod controls_panel;
|
||||
use controls_panel::ControlsPanel;
|
||||
|
||||
mod waveform_panel;
|
||||
use waveform_panel::WaveformPanel;
|
||||
mod diagram_panel;
|
||||
use diagram_panel::{DiagramPanel, ExcalidrawController};
|
||||
|
||||
type HierarchyAndTimeTable = (Rc<wellen::Hierarchy>, Rc<wellen::TimeTable>);
|
||||
mod waveform_panel;
|
||||
use waveform_panel::{PixiController, WaveformPanel};
|
||||
|
||||
mod header_panel;
|
||||
use header_panel::HeaderPanel;
|
||||
|
||||
mod command_panel;
|
||||
use command_panel::CommandPanel;
|
||||
|
||||
pub mod theme;
|
||||
use theme::*;
|
||||
|
||||
pub mod term;
|
||||
use shared::term::{TerminalDownMsg, TerminalScreen};
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
enum Layout {
|
||||
|
@ -18,39 +34,173 @@ enum Layout {
|
|||
Columns,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
enum Mode {
|
||||
// @TODO make default
|
||||
// #[default]
|
||||
Waves,
|
||||
#[default]
|
||||
Diagrams,
|
||||
}
|
||||
|
||||
type Filename = String;
|
||||
|
||||
#[derive(Default)]
|
||||
struct Store {
|
||||
selected_var_refs: MutableVec<wellen::VarRef>,
|
||||
hierarchy: Mutable<Option<Arc<wellen::Hierarchy>>>,
|
||||
loaded_filename: Mutable<Option<Filename>>,
|
||||
pixi_canvas_controller: Mutable<Mutable<Option<SendWrapper<PixiController>>>>,
|
||||
excalidraw_canvas_controller: Mutable<Mutable<Option<SendWrapper<ExcalidrawController>>>>,
|
||||
}
|
||||
|
||||
static STORE: Lazy<Store> = lazy::default();
|
||||
|
||||
fn main() {
|
||||
start_app("app", root);
|
||||
|
||||
Task::start(async {
|
||||
// https://github.com/tauri-apps/tauri/issues/5170
|
||||
Timer::sleep(100).await;
|
||||
platform::show_window().await;
|
||||
});
|
||||
|
||||
Task::start(async {
|
||||
platform::listen_diagram_connectors_messages(|message| {
|
||||
match message {
|
||||
DiagramConnectorMessage::ListenForComponentTextChanges {
|
||||
diagram_connector_name,
|
||||
component_id,
|
||||
} => {
|
||||
let closure = Closure::new({
|
||||
// @TODO Rcs/Arcs?
|
||||
let diagram_connector_name = diagram_connector_name.clone();
|
||||
let component_id = component_id.clone();
|
||||
move |text| {
|
||||
Task::start(platform::notify_diagram_connector_text_change(
|
||||
diagram_connector_name.clone(),
|
||||
component_id.clone(),
|
||||
text,
|
||||
));
|
||||
}
|
||||
});
|
||||
STORE
|
||||
.excalidraw_canvas_controller
|
||||
.lock_ref()
|
||||
.lock_ref()
|
||||
.as_ref()
|
||||
.unwrap_throw()
|
||||
.listen_for_component_text_changes(&component_id, &closure);
|
||||
// @TODO don't forget
|
||||
mem::forget(closure);
|
||||
}
|
||||
DiagramConnectorMessage::SetComponentText { component_id, text } => STORE
|
||||
.excalidraw_canvas_controller
|
||||
.lock_ref()
|
||||
.lock_ref()
|
||||
.as_ref()
|
||||
.unwrap_throw()
|
||||
.set_component_text(&component_id, &text),
|
||||
}
|
||||
}).await;
|
||||
platform::listen_term_update(|down_msg| {
|
||||
term::TERMINAL_STATE.set(down_msg);
|
||||
}).await;
|
||||
});
|
||||
}
|
||||
|
||||
fn root() -> impl Element {
|
||||
let hierarchy_and_time_table: Mutable<Option<HierarchyAndTimeTable>> = <_>::default();
|
||||
let selected_var_refs: MutableVec<wellen::VarRef> = <_>::default();
|
||||
let hierarchy = STORE.hierarchy.clone();
|
||||
let selected_var_refs = STORE.selected_var_refs.clone();
|
||||
let layout: Mutable<Layout> = <_>::default();
|
||||
let mode: Mutable<Mode> = <_>::default();
|
||||
let loaded_filename = STORE.loaded_filename.clone();
|
||||
let pixi_canvas_controller = STORE.pixi_canvas_controller.clone();
|
||||
let excalidraw_canvas_controller = STORE.excalidraw_canvas_controller.clone();
|
||||
Column::new()
|
||||
.s(Height::fill())
|
||||
.s(Scrollbars::y_and_clip_x())
|
||||
.s(Font::new().color(color!("Lavender")))
|
||||
.item(
|
||||
Row::new()
|
||||
.s(Height::fill())
|
||||
.s(Gap::new().x(15))
|
||||
.item(ControlsPanel::new(
|
||||
hierarchy_and_time_table.clone(),
|
||||
selected_var_refs.clone(),
|
||||
layout.clone(),
|
||||
))
|
||||
.item_signal(layout.signal().map(|layout| matches!(layout, Layout::Tree)).map_true(clone!((hierarchy_and_time_table, selected_var_refs) move || WaveformPanel::new(
|
||||
hierarchy_and_time_table.clone(),
|
||||
selected_var_refs.clone(),
|
||||
))))
|
||||
.s(Font::new().color(COLOR_LAVENDER))
|
||||
.item(HeaderPanel::new(
|
||||
hierarchy.clone(),
|
||||
layout.clone(),
|
||||
mode.clone(),
|
||||
loaded_filename.clone(),
|
||||
))
|
||||
.item_signal(mode.signal().map(clone!((hierarchy, selected_var_refs, loaded_filename, pixi_canvas_controller) move |mode| match mode {
|
||||
Mode::Waves => {
|
||||
Column::new()
|
||||
.s(Height::fill())
|
||||
.s(Scrollbars::y_and_clip_x())
|
||||
.item(
|
||||
Row::new()
|
||||
.s(Scrollbars::y_and_clip_x())
|
||||
.s(Gap::new().x(15))
|
||||
.s(Height::growable().min(150))
|
||||
.item(ControlsPanel::new(
|
||||
hierarchy.clone(),
|
||||
selected_var_refs.clone(),
|
||||
layout.clone(),
|
||||
loaded_filename.clone(),
|
||||
))
|
||||
.item_signal({
|
||||
let hierarchy = hierarchy.clone();
|
||||
let selected_var_refs = selected_var_refs.clone();
|
||||
let loaded_filename = loaded_filename.clone();
|
||||
let pixi_canvas_controller = pixi_canvas_controller.clone();
|
||||
map_ref!{
|
||||
let layout = layout.signal(),
|
||||
let hierarchy_is_some = hierarchy.signal_ref(Option::is_some) => {
|
||||
(*hierarchy_is_some && matches!(layout, Layout::Tree)).then(clone!((hierarchy, selected_var_refs, loaded_filename, pixi_canvas_controller) move || WaveformPanel::new(
|
||||
hierarchy.clone(),
|
||||
selected_var_refs.clone(),
|
||||
loaded_filename.clone(),
|
||||
pixi_canvas_controller.clone(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.item_signal({
|
||||
let hierarchy = hierarchy.clone();
|
||||
let selected_var_refs = selected_var_refs.clone();
|
||||
let loaded_filename = loaded_filename.clone();
|
||||
let pixi_canvas_controller = pixi_canvas_controller.clone();
|
||||
map_ref!{
|
||||
let layout = layout.signal(),
|
||||
let hierarchy_is_some = hierarchy.signal_ref(Option::is_some) => {
|
||||
(*hierarchy_is_some && matches!(layout, Layout::Columns)).then(clone!((hierarchy, selected_var_refs, loaded_filename, pixi_canvas_controller) move || WaveformPanel::new(
|
||||
hierarchy.clone(),
|
||||
selected_var_refs.clone(),
|
||||
loaded_filename.clone(),
|
||||
pixi_canvas_controller.clone(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
Mode::Diagrams => {
|
||||
Column::new()
|
||||
.s(Height::fill())
|
||||
.s(Scrollbars::y_and_clip_x())
|
||||
.item(DiagramPanel::new(excalidraw_canvas_controller.clone()))
|
||||
}
|
||||
})))
|
||||
.item(CommandPanel::new())
|
||||
.item_signal(
|
||||
TERM_OPEN.signal_cloned().map(
|
||||
|term_open| {
|
||||
match term_open {
|
||||
true =>
|
||||
El::new()
|
||||
.s(Height::fill().max(400).min(400))
|
||||
.s(Padding::all(5))
|
||||
.child(term::root()),
|
||||
false =>
|
||||
El::new()
|
||||
.child("")
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
.item_signal(layout.signal().map(|layout| matches!(layout, Layout::Columns)).map_true(move || WaveformPanel::new(
|
||||
hierarchy_and_time_table.clone(),
|
||||
selected_var_refs.clone(),
|
||||
)))
|
||||
}
|
||||
|
|
|
@ -3,6 +3,9 @@
|
|||
|
||||
// NOTE: `FASTWAVE_PLATFORM` is set in `Makefile.toml` tasks and then in `build.rs`
|
||||
|
||||
use crate::STORE;
|
||||
use shared::DiagramConnectorMessage;
|
||||
|
||||
#[cfg(FASTWAVE_PLATFORM = "TAURI")]
|
||||
mod tauri;
|
||||
#[cfg(FASTWAVE_PLATFORM = "TAURI")]
|
||||
|
@ -14,6 +17,19 @@ mod browser;
|
|||
use browser as platform;
|
||||
|
||||
type Filename = String;
|
||||
type JavascriptCode = String;
|
||||
|
||||
type AddedDecodersCount = usize;
|
||||
type RemovedDecodersCount = usize;
|
||||
type DecoderPath = String;
|
||||
|
||||
type AddedDiagramConnectorsCount = usize;
|
||||
type RemovedDiagramConnectorsCount = usize;
|
||||
type DiagramConnectorPath = String;
|
||||
type DiagramConnectorName = String;
|
||||
type ComponentId = String;
|
||||
|
||||
use shared::term::{TerminalDownMsg, TerminalScreen};
|
||||
|
||||
pub async fn show_window() {
|
||||
platform::show_window().await
|
||||
|
@ -25,18 +41,97 @@ pub async fn pick_and_load_waveform(file: Option<gloo_file::File>) -> Option<Fil
|
|||
platform::pick_and_load_waveform(file).await
|
||||
}
|
||||
|
||||
// @TODO allow only supported file type (*.fw.js)
|
||||
// @TODO remove the `file` parameter once we don't have to use FileInput element
|
||||
pub async fn load_file_with_selected_vars(file: Option<gloo_file::File>) -> Option<JavascriptCode> {
|
||||
platform::load_file_with_selected_vars(file).await
|
||||
}
|
||||
|
||||
pub async fn get_hierarchy() -> wellen::Hierarchy {
|
||||
platform::get_hierarchy().await
|
||||
}
|
||||
|
||||
pub async fn get_time_table() -> wellen::TimeTable {
|
||||
platform::get_time_table().await
|
||||
}
|
||||
|
||||
pub async fn load_and_get_signal(signal_ref: wellen::SignalRef) -> wellen::Signal {
|
||||
platform::load_and_get_signal(signal_ref).await
|
||||
pub async fn load_signal_and_get_timeline(
|
||||
signal_ref: wellen::SignalRef,
|
||||
timeline_zoom: f64,
|
||||
timeline_viewport_width: u32,
|
||||
timeline_viewport_x: i32,
|
||||
block_height: u32,
|
||||
var_format: shared::VarFormat,
|
||||
) -> shared::Timeline {
|
||||
platform::load_signal_and_get_timeline(
|
||||
signal_ref,
|
||||
timeline_zoom,
|
||||
timeline_viewport_width,
|
||||
timeline_viewport_x,
|
||||
block_height,
|
||||
var_format,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn unload_signal(signal_ref: wellen::SignalRef) {
|
||||
platform::unload_signal(signal_ref).await
|
||||
}
|
||||
|
||||
pub async fn send_char(c : String) {
|
||||
platform::send_char(c).await
|
||||
}
|
||||
|
||||
pub async fn add_decoders(decoder_paths: Vec<DecoderPath>) -> AddedDecodersCount {
|
||||
let count = platform::add_decoders(decoder_paths).await;
|
||||
if count > 0 {
|
||||
redraw_all_timeline_rows().await;
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
pub async fn remove_all_decoders() -> RemovedDecodersCount {
|
||||
let count = platform::remove_all_decoders().await;
|
||||
if count > 0 {
|
||||
redraw_all_timeline_rows().await;
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
async fn redraw_all_timeline_rows() {
|
||||
if let Some(controller) = STORE.pixi_canvas_controller.get_cloned().get_cloned() {
|
||||
controller.redraw_all_rows().await
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn add_diagram_connectors(
|
||||
diagram_connector_paths: Vec<DecoderPath>,
|
||||
) -> AddedDecodersCount {
|
||||
let count = platform::add_diagram_connectors(diagram_connector_paths).await;
|
||||
count
|
||||
}
|
||||
|
||||
pub async fn remove_all_diagram_connectors() -> RemovedDecodersCount {
|
||||
let count = platform::remove_all_diagram_connectors().await;
|
||||
count
|
||||
}
|
||||
|
||||
pub async fn listen_diagram_connectors_messages(
|
||||
on_message: impl FnMut(DiagramConnectorMessage) + 'static,
|
||||
) {
|
||||
platform::listen_diagram_connectors_messages(on_message).await;
|
||||
}
|
||||
|
||||
pub async fn listen_term_update(
|
||||
on_message: impl FnMut(TerminalDownMsg) + 'static,
|
||||
) {
|
||||
platform::listen_term_update(on_message).await;
|
||||
}
|
||||
|
||||
pub async fn notify_diagram_connector_text_change(
|
||||
diagram_connector: DiagramConnectorName,
|
||||
component_id: ComponentId,
|
||||
text: String,
|
||||
) {
|
||||
platform::notify_diagram_connector_text_change(diagram_connector, component_id, text).await;
|
||||
}
|
||||
|
||||
pub async fn open_konata_file() {
|
||||
platform::open_konata_file().await;
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
use shared::wellen_helpers;
|
||||
use std::sync::Mutex;
|
||||
use wellen::simple::Waveform;
|
||||
use zoon::*;
|
||||
use zoon::{eprintln, *};
|
||||
|
||||
#[derive(Default)]
|
||||
struct Store {
|
||||
struct BrowserPlatformStore {
|
||||
waveform: Mutex<Option<Waveform>>,
|
||||
}
|
||||
|
||||
static STORE: Lazy<Store> = lazy::default();
|
||||
static BROWSER_PLATFORM_STORE: Lazy<BrowserPlatformStore> = lazy::default();
|
||||
|
||||
pub(super) async fn show_window() {}
|
||||
|
||||
|
@ -25,7 +25,7 @@ pub(super) async fn pick_and_load_waveform(
|
|||
let Ok(waveform) = waveform else {
|
||||
panic!("Waveform file reading failed")
|
||||
};
|
||||
*STORE.waveform.lock().unwrap_throw() = Some(waveform);
|
||||
*BROWSER_PLATFORM_STORE.waveform.lock().unwrap_throw() = Some(waveform);
|
||||
Some(file.name())
|
||||
}
|
||||
|
||||
|
@ -63,36 +63,111 @@ pub(super) async fn pick_and_load_waveform(
|
|||
// let Ok(waveform) = waveform else {
|
||||
// panic!("Waveform file reading failed")
|
||||
// };
|
||||
// *STORE.waveform.lock().unwrap_throw() = Some(waveform);
|
||||
// *BROWSER_PLATFORM_STORE.waveform.lock().unwrap_throw() = Some(waveform);
|
||||
// Some(file.name())
|
||||
// }
|
||||
|
||||
// @TODO allow only supported file type (*.fw.js)
|
||||
// @TODO remove the `file` parameter once we don't have to use FileInput element
|
||||
pub async fn load_file_with_selected_vars(
|
||||
file: Option<gloo_file::File>,
|
||||
) -> Option<super::JavascriptCode> {
|
||||
let file = file.unwrap_throw();
|
||||
|
||||
let javascript_code = gloo_file::futures::read_as_text(&file).await.unwrap_throw();
|
||||
|
||||
Some(javascript_code)
|
||||
}
|
||||
|
||||
// @TODO Use alternative `load_file_with_selected_vars` version once `showOpenFilePicker` is supported by Safari and Firefox
|
||||
// https://caniuse.com/mdn-api_window_showopenfilepicker
|
||||
// (see the `pick_and_load_waveform` method above)
|
||||
|
||||
pub(super) async fn get_hierarchy() -> wellen::Hierarchy {
|
||||
let waveform = STORE.waveform.lock().unwrap_throw();
|
||||
let waveform = BROWSER_PLATFORM_STORE.waveform.lock().unwrap_throw();
|
||||
let hierarchy = waveform.as_ref().unwrap_throw().hierarchy();
|
||||
// @TODO Wrap `hierarchy` in `Waveform` with `Rc/Arc` or add the method `take` / `clone` or refactor?
|
||||
serde_json::from_value(serde_json::to_value(hierarchy).unwrap_throw()).unwrap_throw()
|
||||
}
|
||||
|
||||
pub(super) async fn get_time_table() -> wellen::TimeTable {
|
||||
let waveform = STORE.waveform.lock().unwrap_throw();
|
||||
let time_table = waveform.as_ref().unwrap_throw().time_table();
|
||||
// @TODO Wrap `time_table` in `Waveform` with `Rc/Arc` or add the method `take` / `clone` or refactor?
|
||||
serde_json::from_value(serde_json::to_value(time_table).unwrap_throw()).unwrap_throw()
|
||||
}
|
||||
|
||||
pub(super) async fn load_and_get_signal(signal_ref: wellen::SignalRef) -> wellen::Signal {
|
||||
let mut waveform_lock = STORE.waveform.lock().unwrap_throw();
|
||||
let waveform = waveform_lock.as_mut().unwrap_throw();
|
||||
// @TODO maybe run it in a thread to not block the main one and then
|
||||
waveform.load_signals(&[signal_ref]);
|
||||
let signal = waveform.get_signal(signal_ref).unwrap_throw();
|
||||
// @TODO `clone` / `Rc/Arc` / refactor?
|
||||
serde_json::from_value(serde_json::to_value(signal).unwrap_throw()).unwrap_throw()
|
||||
pub(super) async fn load_signal_and_get_timeline(
|
||||
signal_ref: wellen::SignalRef,
|
||||
timeline_zoom: f64,
|
||||
timeline_viewport_width: u32,
|
||||
timeline_viewport_x: i32,
|
||||
block_height: u32,
|
||||
var_format: shared::VarFormat,
|
||||
) -> shared::Timeline {
|
||||
let mut waveform_lock = BROWSER_PLATFORM_STORE.waveform.lock().unwrap();
|
||||
let waveform = waveform_lock.as_mut().unwrap();
|
||||
waveform.load_signals_multi_threaded(&[signal_ref]);
|
||||
let signal = waveform.get_signal(signal_ref).unwrap();
|
||||
let time_table = waveform.time_table();
|
||||
let timeline = shared::signal_to_timeline(
|
||||
signal,
|
||||
time_table,
|
||||
timeline_zoom,
|
||||
timeline_viewport_width,
|
||||
timeline_viewport_x,
|
||||
block_height,
|
||||
var_format,
|
||||
|value| Box::pin(async { value }),
|
||||
)
|
||||
.await;
|
||||
timeline
|
||||
}
|
||||
|
||||
pub(super) async fn unload_signal(signal_ref: wellen::SignalRef) {
|
||||
let mut waveform_lock = STORE.waveform.lock().unwrap_throw();
|
||||
let mut waveform_lock = BROWSER_PLATFORM_STORE.waveform.lock().unwrap_throw();
|
||||
let waveform = waveform_lock.as_mut().unwrap_throw();
|
||||
waveform.unload_signals(&[signal_ref]);
|
||||
}
|
||||
|
||||
pub(super) async fn add_decoders(
|
||||
_decoder_paths: Vec<super::DecoderPath>,
|
||||
) -> super::AddedDecodersCount {
|
||||
// @TODO error message for user
|
||||
eprintln!("Adding decoders is not supported in the browser.");
|
||||
0
|
||||
}
|
||||
|
||||
pub(super) async fn remove_all_decoders() -> super::RemovedDecodersCount {
|
||||
// @TODO error message for user
|
||||
eprintln!("Removing decoders is not supported in the browser.");
|
||||
0
|
||||
}
|
||||
|
||||
pub(super) async fn add_diagram_connectors(
|
||||
diagram_connector_paths: Vec<super::DecoderPath>,
|
||||
) -> super::AddedDecodersCount {
|
||||
// @TODO error message for user
|
||||
eprintln!("Adding diagram connectors is not supported in the browser.");
|
||||
0
|
||||
}
|
||||
|
||||
pub(super) async fn remove_all_diagram_connectors() -> super::RemovedDiagramConnectorsCount {
|
||||
// @TODO error message for user
|
||||
eprintln!("Removing diagram connectors is not supported in the browser.");
|
||||
0
|
||||
}
|
||||
|
||||
pub async fn listen_diagram_connectors_messages(
|
||||
on_message: impl FnMut(DiagramConnectorMessage) + 'static,
|
||||
) {
|
||||
// @TODO error message for user
|
||||
eprintln!("Removing listen for diagram connectors messages is not supported in the browser.");
|
||||
}
|
||||
|
||||
pub async fn notify_diagram_connector_text_change(
|
||||
diagram_connector: DiagramConnectorName,
|
||||
component_id: ComponentId,
|
||||
text: String,
|
||||
) {
|
||||
// @TODO error message for user
|
||||
eprintln!("Diagram connectors notifications are not supported in the browser.");
|
||||
}
|
||||
|
||||
pub async fn open_konata_file() {
|
||||
// @TODO error message for user
|
||||
eprintln!("Opening Konata files is not supported in the browser.");
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use shared::DiagramConnectorMessage;
|
||||
use shared::term::{TerminalDownMsg, TerminalScreen};
|
||||
use zoon::*;
|
||||
|
||||
pub(super) async fn show_window() {
|
||||
|
@ -13,19 +15,39 @@ pub(super) async fn pick_and_load_waveform(
|
|||
.as_string()
|
||||
}
|
||||
|
||||
pub(super) async fn load_file_with_selected_vars(
|
||||
_file: Option<gloo_file::File>,
|
||||
) -> Option<super::JavascriptCode> {
|
||||
tauri_glue::load_file_with_selected_vars()
|
||||
.await
|
||||
.unwrap_throw()
|
||||
.as_string()
|
||||
}
|
||||
|
||||
pub(super) async fn get_hierarchy() -> wellen::Hierarchy {
|
||||
serde_wasm_bindgen::from_value(tauri_glue::get_hierarchy().await.unwrap_throw()).unwrap_throw()
|
||||
}
|
||||
|
||||
pub(super) async fn get_time_table() -> wellen::TimeTable {
|
||||
serde_wasm_bindgen::from_value(tauri_glue::get_time_table().await.unwrap_throw()).unwrap_throw()
|
||||
}
|
||||
|
||||
pub(super) async fn load_and_get_signal(signal_ref: wellen::SignalRef) -> wellen::Signal {
|
||||
pub(super) async fn load_signal_and_get_timeline(
|
||||
signal_ref: wellen::SignalRef,
|
||||
timeline_zoom: f64,
|
||||
timeline_viewport_width: u32,
|
||||
timeline_viewport_x: i32,
|
||||
block_height: u32,
|
||||
var_format: shared::VarFormat,
|
||||
) -> shared::Timeline {
|
||||
let var_format = serde_wasm_bindgen::to_value(&var_format).unwrap_throw();
|
||||
serde_wasm_bindgen::from_value(
|
||||
tauri_glue::load_and_get_signal(signal_ref.index())
|
||||
.await
|
||||
.unwrap_throw(),
|
||||
tauri_glue::load_signal_and_get_timeline(
|
||||
signal_ref.index(),
|
||||
timeline_zoom,
|
||||
timeline_viewport_width,
|
||||
timeline_viewport_x,
|
||||
block_height,
|
||||
var_format,
|
||||
)
|
||||
.await
|
||||
.unwrap_throw(),
|
||||
)
|
||||
.unwrap_throw()
|
||||
}
|
||||
|
@ -36,6 +58,74 @@ pub(super) async fn unload_signal(signal_ref: wellen::SignalRef) {
|
|||
.unwrap_throw()
|
||||
}
|
||||
|
||||
pub(super) async fn send_char(c : String) {
|
||||
tauri_glue::send_char(c)
|
||||
.await
|
||||
.unwrap_throw()
|
||||
}
|
||||
|
||||
pub(super) async fn add_decoders(
|
||||
decoder_paths: Vec<super::DecoderPath>,
|
||||
) -> super::AddedDecodersCount {
|
||||
serde_wasm_bindgen::from_value(tauri_glue::add_decoders(decoder_paths).await.unwrap_throw())
|
||||
.unwrap_throw()
|
||||
}
|
||||
|
||||
pub(super) async fn remove_all_decoders() -> super::RemovedDecodersCount {
|
||||
serde_wasm_bindgen::from_value(tauri_glue::remove_all_decoders().await.unwrap_throw())
|
||||
.unwrap_throw()
|
||||
}
|
||||
|
||||
pub(super) async fn add_diagram_connectors(
|
||||
diagram_connector_paths: Vec<super::DecoderPath>,
|
||||
) -> super::AddedDiagramConnectorsCount {
|
||||
serde_wasm_bindgen::from_value(
|
||||
tauri_glue::add_diagram_connectors(diagram_connector_paths)
|
||||
.await
|
||||
.unwrap_throw(),
|
||||
)
|
||||
.unwrap_throw()
|
||||
}
|
||||
|
||||
pub(super) async fn remove_all_diagram_connectors() -> super::RemovedDiagramConnectorsCount {
|
||||
serde_wasm_bindgen::from_value(
|
||||
tauri_glue::remove_all_diagram_connectors()
|
||||
.await
|
||||
.unwrap_throw(),
|
||||
)
|
||||
.unwrap_throw()
|
||||
}
|
||||
|
||||
pub(super) async fn listen_diagram_connectors_messages(
|
||||
mut on_message: impl FnMut(DiagramConnectorMessage) + 'static,
|
||||
) {
|
||||
let on_message =
|
||||
move |message: JsValue| on_message(serde_wasm_bindgen::from_value(message).unwrap_throw());
|
||||
tauri_glue::listen_diagram_connectors_messages(Closure::new(on_message).into_js_value()).await
|
||||
}
|
||||
|
||||
pub(super) async fn listen_term_update(
|
||||
mut on_message: impl FnMut(TerminalDownMsg) + 'static,
|
||||
) {
|
||||
let on_message =
|
||||
move |message: JsValue| on_message(serde_wasm_bindgen::from_value(message).unwrap_throw());
|
||||
tauri_glue::listen_term_update(Closure::new(on_message).into_js_value()).await
|
||||
}
|
||||
|
||||
pub(super) async fn notify_diagram_connector_text_change(
|
||||
diagram_connector: super::DiagramConnectorName,
|
||||
component_id: super::ComponentId,
|
||||
text: String,
|
||||
) {
|
||||
tauri_glue::notify_diagram_connector_text_change(diagram_connector, component_id, text)
|
||||
.await
|
||||
.unwrap_throw();
|
||||
}
|
||||
|
||||
pub(super) async fn open_konata_file() {
|
||||
tauri_glue::open_konata_file().await;
|
||||
}
|
||||
|
||||
mod tauri_glue {
|
||||
use zoon::*;
|
||||
|
||||
|
@ -48,16 +138,55 @@ mod tauri_glue {
|
|||
#[wasm_bindgen(catch)]
|
||||
pub async fn pick_and_load_waveform() -> Result<JsValue, JsValue>;
|
||||
|
||||
#[wasm_bindgen(catch)]
|
||||
pub async fn load_file_with_selected_vars() -> Result<JsValue, JsValue>;
|
||||
|
||||
#[wasm_bindgen(catch)]
|
||||
pub async fn get_hierarchy() -> Result<JsValue, JsValue>;
|
||||
|
||||
#[wasm_bindgen(catch)]
|
||||
pub async fn get_time_table() -> Result<JsValue, JsValue>;
|
||||
|
||||
#[wasm_bindgen(catch)]
|
||||
pub async fn load_and_get_signal(signal_ref_index: usize) -> Result<JsValue, JsValue>;
|
||||
pub async fn load_signal_and_get_timeline(
|
||||
signal_ref_index: usize,
|
||||
timeline_zoom: f64,
|
||||
timeline_viewport_width: u32,
|
||||
timeline_viewport_x: i32,
|
||||
block_height: u32,
|
||||
var_format: JsValue,
|
||||
) -> Result<JsValue, JsValue>;
|
||||
|
||||
#[wasm_bindgen(catch)]
|
||||
pub async fn unload_signal(signal_ref_index: usize) -> Result<(), JsValue>;
|
||||
|
||||
#[wasm_bindgen(catch)]
|
||||
pub async fn send_char(c : String) -> Result<(), JsValue>;
|
||||
|
||||
#[wasm_bindgen(catch)]
|
||||
pub async fn add_decoders(
|
||||
decoder_paths: Vec<super::super::DecoderPath>,
|
||||
) -> Result<JsValue, JsValue>;
|
||||
|
||||
#[wasm_bindgen(catch)]
|
||||
pub async fn remove_all_decoders() -> Result<JsValue, JsValue>;
|
||||
|
||||
#[wasm_bindgen(catch)]
|
||||
pub async fn add_diagram_connectors(
|
||||
diagram_connector_paths: Vec<super::super::DiagramConnectorPath>,
|
||||
) -> Result<JsValue, JsValue>;
|
||||
|
||||
#[wasm_bindgen(catch)]
|
||||
pub async fn remove_all_diagram_connectors() -> Result<JsValue, JsValue>;
|
||||
|
||||
pub async fn listen_diagram_connectors_messages(on_event: JsValue);
|
||||
|
||||
pub async fn listen_term_update(on_event: JsValue);
|
||||
|
||||
#[wasm_bindgen(catch)]
|
||||
pub async fn notify_diagram_connector_text_change(
|
||||
diagram_connector: super::super::DiagramConnectorName,
|
||||
component_id: super::super::ComponentId,
|
||||
text: String,
|
||||
) -> Result<(), JsValue>;
|
||||
|
||||
pub async fn open_konata_file();
|
||||
}
|
||||
}
|
||||
|
|
111
frontend/src/script_bridge.rs
Normal file
|
@ -0,0 +1,111 @@
|
|||
use crate::{platform, STORE};
|
||||
use wellen::GetItem;
|
||||
use zoon::*;
|
||||
|
||||
type FullVarName = String;
|
||||
|
||||
type AddedDecodersCount = usize;
|
||||
type RemovedDecodersCount = usize;
|
||||
type DecoderPath = String;
|
||||
|
||||
type AddedDiagramConnectorsCount = usize;
|
||||
type RemovedDiagramConnectorsCount = usize;
|
||||
type DiagramConnectorPath = String;
|
||||
|
||||
#[wasm_bindgen(module = "/typescript/bundles/strict_eval.js")]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(catch)]
|
||||
pub async fn strict_eval(code: &str) -> Result<JsValue, JsValue>;
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct FW;
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl FW {
|
||||
/// JS: `FW.say_hello()` -> `Hello!`
|
||||
pub fn say_hello() -> String {
|
||||
"Hello!".to_owned()
|
||||
}
|
||||
|
||||
/// JS: `FW.clear_selected_vars()` -> `4`
|
||||
pub fn clear_selected_vars() -> usize {
|
||||
let mut vars = STORE.selected_var_refs.lock_mut();
|
||||
let var_count = vars.len();
|
||||
vars.clear();
|
||||
var_count
|
||||
}
|
||||
|
||||
/// JS: `FW.select_vars(["simple_tb.s.A", "simple_tb.s.B"])` -> `2`
|
||||
pub fn select_vars(full_var_names: Vec<FullVarName>) -> usize {
|
||||
if let Some(hierarchy) = STORE.hierarchy.get_cloned() {
|
||||
let mut new_var_refs = Vec::new();
|
||||
for full_var_name in full_var_names {
|
||||
let path_with_name = full_var_name.split_terminator('.').collect::<Vec<_>>();
|
||||
if let Some((name, path)) = path_with_name.split_last() {
|
||||
if let Some(var_ref) = hierarchy.lookup_var(path, name) {
|
||||
new_var_refs.push(var_ref);
|
||||
}
|
||||
}
|
||||
}
|
||||
let var_ref_count = new_var_refs.len();
|
||||
STORE.selected_var_refs.lock_mut().replace(new_var_refs);
|
||||
return var_ref_count;
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
/// JS: `FW.loaded_filename()` -> `simple.vcd`
|
||||
pub fn loaded_filename() -> Option<String> {
|
||||
STORE.loaded_filename.get_cloned()
|
||||
}
|
||||
|
||||
/// JS: `FW.selected_vars()` -> `["simple_tb.s.A", "simple_tb.s.B"]`
|
||||
pub fn selected_vars() -> Vec<FullVarName> {
|
||||
if let Some(hierarchy) = STORE.hierarchy.get_cloned() {
|
||||
let mut full_var_names = Vec::new();
|
||||
for var_ref in STORE.selected_var_refs.lock_ref().as_slice() {
|
||||
let var = hierarchy.get(*var_ref);
|
||||
let var_name = var.full_name(&hierarchy);
|
||||
full_var_names.push(var_name);
|
||||
}
|
||||
return full_var_names;
|
||||
}
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
/// JS: `FW.add_decoders(["../test_files/components/rust_decoder/rust_decoder.wasm"])` -> `1`
|
||||
pub async fn add_decoders(decoder_paths: Vec<DecoderPath>) -> AddedDecodersCount {
|
||||
platform::add_decoders(decoder_paths).await
|
||||
}
|
||||
|
||||
/// JS: `FW.remove_all_decoders()` -> `5`
|
||||
pub async fn remove_all_decoders() -> RemovedDecodersCount {
|
||||
platform::remove_all_decoders().await
|
||||
}
|
||||
|
||||
// @TODO replace argument once Excalidraw's `convertToExcalidrawElements` works: {type: "rectangle", x: 100, y: 250}
|
||||
/// JS: `FW.draw_diagram_element({id: 'my_rectangle', type: 'rectangle', x: 500, y: 250, strokeColor: 'black', backgroundColor: 'lightblue', fillStyle: 'solid', strokeWidth: 5, strokeStyle: 'solid', roundness: null, roughness: 0, opacity: 100, width: 100, height: 50, angle: 0, seed: 0, version: 0, versionNonce: 0, isDeleted: false, groupIds: [], frameId: null, boundElements: null, updated: 0, link: null, locked: false, customData: {}})`
|
||||
pub async fn draw_diagram_element(excalidraw_element: JsValue) {
|
||||
if let Some(controller) = STORE
|
||||
.excalidraw_canvas_controller
|
||||
.lock_ref()
|
||||
.lock_ref()
|
||||
.as_ref()
|
||||
{
|
||||
controller.draw_diagram_element(excalidraw_element)
|
||||
}
|
||||
}
|
||||
|
||||
/// JS: `FW.add_diagram_connectors(["../test_files/components/rust_diagram_connector/rust_diagram_connector.wasm"])` -> `1`
|
||||
pub async fn add_diagram_connectors(
|
||||
connector_paths: Vec<DiagramConnectorPath>,
|
||||
) -> AddedDiagramConnectorsCount {
|
||||
platform::add_diagram_connectors(connector_paths).await
|
||||
}
|
||||
|
||||
/// JS: `FW.remove_all_diagram_connectors()` -> `5`
|
||||
pub async fn remove_all_diagram_connectors() -> RemovedDiagramConnectorsCount {
|
||||
platform::remove_all_diagram_connectors().await
|
||||
}
|
||||
}
|
142
frontend/src/term.rs
Normal file
|
@ -0,0 +1,142 @@
|
|||
use std::ops::Index;
|
||||
|
||||
use chrono::format;
|
||||
use zoon::*;
|
||||
use zoon::{println, eprintln, *};
|
||||
use shared::term::{TerminalDownMsg, TerminalScreen, TerminalUpMsg};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
// use tokio::time::timeout;
|
||||
pub static TERM_OPEN: Lazy<Mutable<bool>> = Lazy::new(|| {false.into()});
|
||||
|
||||
pub const TERMINAL_COLOR: Oklch = color!("oklch(20% 0.125 262.26)");
|
||||
|
||||
pub static TERMINAL_STATE: Lazy<Mutable<TerminalDownMsg>> =
|
||||
Lazy::new(|| {
|
||||
Mutable::new(TerminalDownMsg::TermNotStarted)
|
||||
});
|
||||
|
||||
pub fn root() -> impl Element {
|
||||
let terminal =
|
||||
El::new()
|
||||
.s(Width::fill())
|
||||
.s(Height::fill())
|
||||
.s(Background::new().color(TERMINAL_COLOR))
|
||||
.s(RoundedCorners::all(7))
|
||||
.s(Font::new().family([
|
||||
FontFamily::new("Lucida Console"),
|
||||
FontFamily::new("Courier"),
|
||||
FontFamily::new("monospace")
|
||||
]))
|
||||
.update_raw_el(|raw_el| {
|
||||
raw_el.global_event_handler(|event: events::KeyDown| {
|
||||
send_char(
|
||||
(&event).key().as_str(),
|
||||
(&event).ctrl_key(),
|
||||
);
|
||||
})
|
||||
})
|
||||
.child_signal(TERMINAL_STATE.signal_cloned().map(
|
||||
|down_msg| {
|
||||
match down_msg {
|
||||
TerminalDownMsg::FullTermUpdate(term) => {
|
||||
make_grid_with_newlines(&term)
|
||||
},
|
||||
TerminalDownMsg::TermNotStarted => {
|
||||
"Term not yet started!".to_string()
|
||||
},
|
||||
TerminalDownMsg::BackendTermStartFailure(msg) => {
|
||||
format!("Error: BackendTermStartFailure: {}", msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
;
|
||||
let root = Column::new()
|
||||
.s(Width::fill())
|
||||
.s(Height::fill())
|
||||
.s(Align::new().top())
|
||||
.item(terminal);
|
||||
root
|
||||
}
|
||||
|
||||
fn send_char(
|
||||
s : &str,
|
||||
has_control : bool,
|
||||
) {
|
||||
match process_str(s, has_control) {
|
||||
Some(c) => {
|
||||
let send_c = c.clone();
|
||||
Task::start(async move {
|
||||
crate::platform::send_char(send_c.to_string()).await;
|
||||
});
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
fn make_grid_with_newlines(term: &TerminalScreen) -> String {
|
||||
let mut formatted = String::with_capacity(term.content.len() + (term.content.len() / term.cols as usize));
|
||||
|
||||
term.content.chars().enumerate().for_each(|(i, c)| {
|
||||
formatted.push(c);
|
||||
if (i + 1) as u16 % term.cols == 0 {
|
||||
formatted.push('\n');
|
||||
}
|
||||
});
|
||||
|
||||
formatted
|
||||
}
|
||||
|
||||
|
||||
fn process_str(s: &str, has_ctrl: bool) -> Option<char> {
|
||||
match s {
|
||||
"Enter" => {return Some('\n');}
|
||||
"Escape" => {return Some('\x1B');}
|
||||
"Backspace" => {return Some('\x08');}
|
||||
"ArrowUp" => {return Some('\x10');}
|
||||
"ArrowDown" => {return Some('\x0E');}
|
||||
"ArrowLeft" => {return Some('\x02');}
|
||||
"ArrowRight" => {return Some('\x06');}
|
||||
"Control" => {return None;}
|
||||
"Shift" => {return None;}
|
||||
"Meta" => {return None;}
|
||||
"Alt" => {return None;}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let mut graphemes = s.graphemes(true);
|
||||
let first = graphemes.next();
|
||||
|
||||
if let Some(g) = first {
|
||||
if g.len() == 1 {
|
||||
if let Some(c) = g.chars().next() {
|
||||
let c = process_for_ctrl_char(c, has_ctrl);
|
||||
return Some(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
// Helper function to process control characters
|
||||
|
||||
fn is_lowercase_alpha(c: char) -> bool {
|
||||
char_is_between_inclusive(c, 'a', 'z')
|
||||
}
|
||||
|
||||
fn process_for_ctrl_char(c: char, has_ctrl: bool) -> char {
|
||||
if has_ctrl {
|
||||
(c as u8 & 0x1F) as char
|
||||
} else {
|
||||
c
|
||||
}
|
||||
}
|
||||
|
||||
fn char_is_between_inclusive(c : char, lo_char : char, hi_char : char) -> bool {
|
||||
c >= lo_char && c <= hi_char
|
||||
}
|
27
frontend/src/theme.rs
Normal file
|
@ -0,0 +1,27 @@
|
|||
use zoon::*;
|
||||
|
||||
// https://oklch.com/
|
||||
|
||||
// pub const COLOR_BLUE_VIOLET: Rgba = color!("BlueViolet"); // oklch(53.38% 0.25 301.37)
|
||||
pub const COLOR_BLUE_VIOLET: Oklch = color!("oklch(53.38% 0.25 262.59)");
|
||||
|
||||
// pub const COLOR_MEDIUM_SLATE_BLUE: Rgba = color!("MediumSlateBlue"); // oklch(60.45% 0.194 285.5)
|
||||
pub const COLOR_MEDIUM_SLATE_BLUE: Oklch = color!("oklch(60.45% 0.194 262.26)");
|
||||
|
||||
// pub const COLOR_SLATE_BLUE_WITH_ALPHA: Rgba = color!("SlateBlue", 0.8); // oklch(54.36% 0.171 285.54)
|
||||
pub const COLOR_SLATE_BLUE_WITH_ALPHA: Oklch = color!("oklch(54.36% 0.171 262.26)", 0.8);
|
||||
|
||||
// pub const COLOR_SLATE_BLUE: Rgba = color!("SlateBlue"); // oklch(54.36% 0.171 285.54)
|
||||
pub const COLOR_SLATE_BLUE: Oklch = color!("oklch(54.36% 0.171 262.26)");
|
||||
|
||||
// pub const COLOR_LIGHT_BLUE: Rgba = color!("LightBlue"); // oklch(85.62% 0.049 219.65)
|
||||
pub const COLOR_LIGHT_BLUE: Oklch = color!("oklch(85.62% 0.049 262.26)");
|
||||
|
||||
// pub const COLOR_WHITE: Rgba = color!("White"); // oklch(100% 3.5594404384177905e-8 106.37411429114086)
|
||||
pub const COLOR_WHITE: Oklch = color!("oklch(100% 3.5594404384177905e-8 105.88)");
|
||||
|
||||
// pub const COLOR_DARK_SLATE_BLUE: Rgba = color!("DarkSlateBlue"); // oklch(41.43% 0.125 286.04)
|
||||
pub const COLOR_DARK_SLATE_BLUE: Oklch = color!("oklch(41.43% 0.125 262.26)");
|
||||
|
||||
// pub const COLOR_LAVENDER: Rgba = color!("Lavender"); // oklch(93.09% 0.027 285.86)
|
||||
pub const COLOR_LAVENDER: Oklch = color!("oklch(93.09% 0.027 262.26)");
|
|
@ -1,9 +1,11 @@
|
|||
use crate::{platform, HierarchyAndTimeTable};
|
||||
use crate::{platform, script_bridge, theme::*, Filename};
|
||||
use std::sync::Arc;
|
||||
use wellen::GetItem;
|
||||
use zoon::{eprintln, *};
|
||||
use zoon::*;
|
||||
|
||||
mod pixi_canvas;
|
||||
use pixi_canvas::{PixiCanvas, PixiController};
|
||||
use pixi_canvas::PixiCanvas;
|
||||
pub use pixi_canvas::PixiController;
|
||||
|
||||
const ROW_HEIGHT: u32 = 40;
|
||||
const ROW_GAP: u32 = 4;
|
||||
|
@ -11,25 +13,209 @@ const ROW_GAP: u32 = 4;
|
|||
#[derive(Clone)]
|
||||
pub struct WaveformPanel {
|
||||
selected_var_refs: MutableVec<wellen::VarRef>,
|
||||
hierarchy_and_time_table: Mutable<Option<HierarchyAndTimeTable>>,
|
||||
hierarchy: Mutable<Option<Arc<wellen::Hierarchy>>>,
|
||||
loaded_filename: Mutable<Option<Filename>>,
|
||||
canvas_controller: Mutable<Mutable<Option<SendWrapper<PixiController>>>>,
|
||||
}
|
||||
|
||||
impl WaveformPanel {
|
||||
pub fn new(
|
||||
hierarchy_and_time_table: Mutable<Option<HierarchyAndTimeTable>>,
|
||||
hierarchy: Mutable<Option<Arc<wellen::Hierarchy>>>,
|
||||
selected_var_refs: MutableVec<wellen::VarRef>,
|
||||
loaded_filename: Mutable<Option<Filename>>,
|
||||
canvas_controller: Mutable<Mutable<Option<SendWrapper<PixiController>>>>,
|
||||
) -> impl Element {
|
||||
Self {
|
||||
selected_var_refs,
|
||||
hierarchy_and_time_table,
|
||||
hierarchy,
|
||||
loaded_filename,
|
||||
canvas_controller,
|
||||
}
|
||||
.root()
|
||||
}
|
||||
|
||||
fn root(&self) -> impl Element {
|
||||
Column::new()
|
||||
.s(Padding::all(20))
|
||||
.s(Scrollbars::y_and_clip_x())
|
||||
.s(Width::fill())
|
||||
.s(Height::fill())
|
||||
.s(Gap::new().y(20))
|
||||
.item(self.selected_vars_controls())
|
||||
.item(self.vars_and_timelines_panel())
|
||||
}
|
||||
|
||||
fn selected_vars_controls(&self) -> impl Element {
|
||||
Row::new()
|
||||
.s(Align::center())
|
||||
.s(Gap::new().x(20))
|
||||
.s(Width::fill())
|
||||
.item(Spacer::fill())
|
||||
.item(self.load_save_selected_vars_buttons())
|
||||
.item(self.keys_info())
|
||||
}
|
||||
|
||||
fn keys_info(&self) -> impl Element {
|
||||
El::new().s(Width::fill()).child(
|
||||
Row::new()
|
||||
.s(Align::new().center_x())
|
||||
.s(Gap::new().x(15))
|
||||
.item(El::new().s(Font::new().no_wrap()).child("Zoom: Wheel"))
|
||||
.item(
|
||||
El::new()
|
||||
.s(Font::new().no_wrap())
|
||||
.child("Pan: Shift + Wheel"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn load_save_selected_vars_buttons(&self) -> impl Element {
|
||||
Row::new()
|
||||
.s(Gap::new().x(20))
|
||||
.item(self.load_selected_vars_button())
|
||||
.item(
|
||||
El::new()
|
||||
.s(Font::new().no_wrap())
|
||||
.child("Selected Variables"),
|
||||
)
|
||||
.item(self.save_selected_vars_button())
|
||||
}
|
||||
|
||||
#[cfg(FASTWAVE_PLATFORM = "TAURI")]
|
||||
fn load_selected_vars_button(&self) -> impl Element {
|
||||
let (hovered, hovered_signal) = Mutable::new_and_signal(false);
|
||||
Button::new()
|
||||
.s(Padding::new().x(20).y(10))
|
||||
.s(Background::new().color_signal(
|
||||
hovered_signal.map_bool(|| COLOR_MEDIUM_SLATE_BLUE, || COLOR_SLATE_BLUE),
|
||||
))
|
||||
.s(Align::new().left())
|
||||
.s(RoundedCorners::all(15))
|
||||
.label("Load")
|
||||
.on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered))
|
||||
.on_press(|| {
|
||||
Task::start(async move {
|
||||
if let Some(javascript_code) =
|
||||
platform::load_file_with_selected_vars(None).await
|
||||
{
|
||||
match script_bridge::strict_eval(&javascript_code).await {
|
||||
Ok(js_value) => {
|
||||
zoon::println!("File with selected vars loaded: {js_value:?}")
|
||||
}
|
||||
Err(js_value) => {
|
||||
zoon::eprintln!(
|
||||
"Failed to load file with selected vars: {js_value:?}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(FASTWAVE_PLATFORM = "BROWSER")]
|
||||
fn load_selected_vars_button(&self) -> impl Element {
|
||||
let (hovered, hovered_signal) = Mutable::new_and_signal(false);
|
||||
let file_input_id = "file_input_for_load_selected_vars_button";
|
||||
Row::new()
|
||||
.item(
|
||||
Label::new()
|
||||
.s(Padding::new().x(20).y(10))
|
||||
.s(Background::new().color_signal(
|
||||
hovered_signal.map_bool(|| COLOR_MEDIUM_SLATE_BLUE, || COLOR_SLATE_BLUE),
|
||||
))
|
||||
.s(Align::new().left())
|
||||
.s(RoundedCorners::all(15))
|
||||
.s(Cursor::new(CursorIcon::Pointer))
|
||||
.label("Load")
|
||||
.on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered))
|
||||
.for_input(file_input_id),
|
||||
)
|
||||
.item(
|
||||
// @TODO https://github.com/MoonZoon/MoonZoon/issues/39
|
||||
// + https://developer.mozilla.org/en-US/docs/Web/API/File_API/Using_files_from_web_applications#using_hidden_file_input_elements_using_the_click_method
|
||||
TextInput::new().id(file_input_id).update_raw_el(|raw_el| {
|
||||
let dom_element = raw_el.dom_element();
|
||||
raw_el
|
||||
.style("display", "none")
|
||||
.attr("type", "file")
|
||||
.event_handler(move |_: events::Input| {
|
||||
let Some(file_list) =
|
||||
dom_element.files().map(gloo_file::FileList::from)
|
||||
else {
|
||||
zoon::println!("file list is `None`");
|
||||
return;
|
||||
};
|
||||
let Some(file) = file_list.first().cloned() else {
|
||||
zoon::println!("file list is empty");
|
||||
return;
|
||||
};
|
||||
Task::start(async move {
|
||||
if let Some(javascript_code) =
|
||||
platform::load_file_with_selected_vars(Some(file)).await
|
||||
{
|
||||
match script_bridge::strict_eval(&javascript_code).await {
|
||||
Ok(js_value) => zoon::println!(
|
||||
"File with selected vars loaded: {js_value:?}"
|
||||
),
|
||||
Err(js_value) => zoon::eprintln!(
|
||||
"Failed to load file with selected vars: {js_value:?}"
|
||||
),
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn save_selected_vars_button(&self) -> impl Element {
|
||||
let (hovered, hovered_signal) = Mutable::new_and_signal(false);
|
||||
let loaded_filename = self.loaded_filename.clone();
|
||||
let selected_var_refs = self.selected_var_refs.clone();
|
||||
let hierarchy = self.hierarchy.clone();
|
||||
Button::new()
|
||||
.s(Padding::new().x(20).y(10))
|
||||
.s(Background::new().color_signal(
|
||||
hovered_signal.map_bool(|| COLOR_MEDIUM_SLATE_BLUE, || COLOR_SLATE_BLUE),
|
||||
))
|
||||
.s(RoundedCorners::all(15))
|
||||
.label("Save")
|
||||
.on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered))
|
||||
.on_press(move || {
|
||||
let loaded_filename = loaded_filename.get_cloned().unwrap_throw();
|
||||
let file_name = format!("{}_vars.fw.js", loaded_filename.replace('.', "_"));
|
||||
|
||||
let hierarchy = hierarchy.get_cloned().unwrap_throw();
|
||||
let mut full_var_names = Vec::new();
|
||||
for var_ref in selected_var_refs.lock_ref().as_slice() {
|
||||
let var = hierarchy.get(*var_ref);
|
||||
let var_name = var.full_name(&hierarchy);
|
||||
full_var_names.push(format!("\"{var_name}\""));
|
||||
}
|
||||
let full_var_names_string = full_var_names.join(",\n\t\t");
|
||||
let file_content = include_str!("waveform_panel/template_vars.px.js")
|
||||
.replacen("{LOADED_FILENAME}", &loaded_filename, 1)
|
||||
.replacen("{FULL_VAR_NAMES}", &full_var_names_string, 1);
|
||||
|
||||
// @TODO we need to use ugly code with temp anchor element until (if ever)
|
||||
// `showSaveFilePicker` is supported in Safari and Firefox (https://caniuse.com/?search=showSaveFilePicker)
|
||||
let file = gloo_file::File::new(&file_name, file_content.as_str());
|
||||
let file_object_url = gloo_file::ObjectUrl::from(file);
|
||||
let a = document().create_element("a").unwrap_throw();
|
||||
a.set_attribute("href", &file_object_url).unwrap_throw();
|
||||
a.set_attribute("download", &file_name).unwrap_throw();
|
||||
a.set_attribute("style", "display: none;").unwrap_throw();
|
||||
dom::body().append_child(&a).unwrap_throw();
|
||||
a.unchecked_ref::<web_sys::HtmlElement>().click();
|
||||
a.remove();
|
||||
})
|
||||
}
|
||||
|
||||
// @TODO autoscroll down
|
||||
fn vars_and_timelines_panel(&self) -> impl Element {
|
||||
let selected_vars_panel_height_getter: Mutable<u32> = <_>::default();
|
||||
Row::new()
|
||||
.s(Padding::all(20))
|
||||
.s(Scrollbars::y_and_clip_x())
|
||||
.s(Width::growable())
|
||||
.s(Height::fill())
|
||||
|
@ -51,27 +237,29 @@ impl WaveformPanel {
|
|||
|
||||
fn canvas(&self, selected_vars_panel_height: ReadOnlyMutable<u32>) -> impl Element {
|
||||
let selected_var_refs = self.selected_var_refs.clone();
|
||||
let hierarchy_and_time_table = self.hierarchy_and_time_table.clone();
|
||||
let hierarchy = self.hierarchy.clone();
|
||||
let canvas_controller = self.canvas_controller.clone();
|
||||
PixiCanvas::new(ROW_HEIGHT, ROW_GAP)
|
||||
.s(Align::new().top())
|
||||
.s(Width::fill())
|
||||
.s(Height::exact_signal(selected_vars_panel_height.signal()))
|
||||
.s(RoundedCorners::new().right(15))
|
||||
.task_with_controller(move |controller| {
|
||||
selected_var_refs.signal_vec().delay_remove(clone!((hierarchy_and_time_table) move |var_ref| {
|
||||
clone!((var_ref, hierarchy_and_time_table) async move {
|
||||
if let Some(hierarchy_and_time_table) = hierarchy_and_time_table.get_cloned() {
|
||||
platform::unload_signal(hierarchy_and_time_table.0.get(var_ref).signal_ref()).await;
|
||||
canvas_controller.set(controller.clone());
|
||||
selected_var_refs.signal_vec().delay_remove(clone!((hierarchy) move |var_ref| {
|
||||
clone!((var_ref, hierarchy) async move {
|
||||
if let Some(hierarchy) = hierarchy.get_cloned() {
|
||||
// @TODO unload only when no other selected variable use it?
|
||||
platform::unload_signal(hierarchy.get(var_ref).signal_ref()).await;
|
||||
}
|
||||
})
|
||||
})).for_each(clone!((controller, hierarchy_and_time_table) move |vec_diff| {
|
||||
clone!((controller, hierarchy_and_time_table) async move {
|
||||
})).for_each(clone!((controller, hierarchy) move |vec_diff| {
|
||||
clone!((controller, hierarchy) async move {
|
||||
match vec_diff {
|
||||
VecDiff::Replace { values } => {
|
||||
let controller = controller.wait_for_some_cloned().await;
|
||||
controller.clear_vars();
|
||||
for var_ref in values {
|
||||
Self::push_var(&controller, &hierarchy_and_time_table, var_ref).await;
|
||||
Self::push_var(&controller, &hierarchy, var_ref).await;
|
||||
}
|
||||
},
|
||||
VecDiff::InsertAt { index: _, value: _ } => { todo!("`task_with_controller` + `InsertAt`") }
|
||||
|
@ -84,7 +272,7 @@ impl WaveformPanel {
|
|||
VecDiff::Move { old_index: _, new_index: _ } => { todo!("`task_with_controller` + `Move`") }
|
||||
VecDiff::Push { value: var_ref } => {
|
||||
if let Some(controller) = controller.lock_ref().as_ref() {
|
||||
Self::push_var(controller, &hierarchy_and_time_table, var_ref).await;
|
||||
Self::push_var(controller, &hierarchy, var_ref).await;
|
||||
}
|
||||
}
|
||||
VecDiff::Pop {} => {
|
||||
|
@ -105,38 +293,33 @@ impl WaveformPanel {
|
|||
|
||||
async fn push_var(
|
||||
controller: &PixiController,
|
||||
hierarchy_and_time_table: &Mutable<Option<HierarchyAndTimeTable>>,
|
||||
hierarchy: &Mutable<Option<Arc<wellen::Hierarchy>>>,
|
||||
var_ref: wellen::VarRef,
|
||||
) {
|
||||
let (hierarchy, time_table) = hierarchy_and_time_table.get_cloned().unwrap();
|
||||
if time_table.is_empty() {
|
||||
eprintln!("timetable is empty");
|
||||
return;
|
||||
}
|
||||
let last_time = time_table.last().copied().unwrap_throw();
|
||||
let hierarchy = hierarchy.get_cloned().unwrap();
|
||||
|
||||
let var_format = shared::VarFormat::default();
|
||||
|
||||
let var = hierarchy.get(var_ref);
|
||||
let signal_ref = var.signal_ref();
|
||||
let signal = platform::load_and_get_signal(signal_ref).await;
|
||||
let timeline = platform::load_signal_and_get_timeline(
|
||||
signal_ref,
|
||||
controller.get_timeline_zoom(),
|
||||
controller.get_timeline_viewport_width(),
|
||||
controller.get_timeline_viewport_x(),
|
||||
ROW_HEIGHT,
|
||||
var_format,
|
||||
)
|
||||
.await;
|
||||
|
||||
let timescale = hierarchy.timescale();
|
||||
// @TODO remove
|
||||
zoon::println!("{timescale:?}");
|
||||
|
||||
let mut timeline: Vec<(wellen::Time, String)> = signal
|
||||
.iter_changes()
|
||||
.map(|(time_index, signal_value)| {
|
||||
(time_table[time_index as usize], signal_value.to_string())
|
||||
})
|
||||
.collect();
|
||||
if timeline.is_empty() {
|
||||
eprintln!("timeline is empty");
|
||||
return;
|
||||
}
|
||||
timeline.push((last_time, timeline.last().cloned().unwrap_throw().1));
|
||||
// @TODO render timeline with time units
|
||||
// let timescale = hierarchy.timescale();
|
||||
|
||||
// Note: Sync `timeline`'s type with the `Timeline` in `frontend/typescript/pixi_canvas/pixi_canvas.ts'
|
||||
controller.push_var(serde_wasm_bindgen::to_value(&timeline).unwrap_throw());
|
||||
let timeline = serde_wasm_bindgen::to_value(&timeline).unwrap_throw();
|
||||
let signal_ref_index = signal_ref.index();
|
||||
let var_format = serde_wasm_bindgen::to_value(&var_format).unwrap_throw();
|
||||
controller.push_var(signal_ref_index, timeline, var_format);
|
||||
}
|
||||
|
||||
fn selected_var_panel(
|
||||
|
@ -144,27 +327,84 @@ impl WaveformPanel {
|
|||
index: ReadOnlyMutable<Option<usize>>,
|
||||
var_ref: wellen::VarRef,
|
||||
) -> Option<impl Element> {
|
||||
let Some((hierarchy, _)) = self.hierarchy_and_time_table.get_cloned() else {
|
||||
let Some(hierarchy) = self.hierarchy.get_cloned() else {
|
||||
None?
|
||||
};
|
||||
let var = hierarchy.get(var_ref);
|
||||
let name: &str = var.name(&hierarchy);
|
||||
Row::new()
|
||||
.item(self.selected_var_name_button(var.name(&hierarchy), index.clone()))
|
||||
.item(self.selected_var_format_button(index))
|
||||
.apply(Some)
|
||||
}
|
||||
|
||||
fn selected_var_name_button(
|
||||
&self,
|
||||
name: &str,
|
||||
index: ReadOnlyMutable<Option<usize>>,
|
||||
) -> impl Element {
|
||||
let selected_var_refs = self.selected_var_refs.clone();
|
||||
let (hovered, hovered_signal) = Mutable::new_and_signal(false);
|
||||
Button::new()
|
||||
.s(Height::exact(ROW_HEIGHT))
|
||||
.s(Background::new().color(color!("SlateBlue", 0.8)))
|
||||
.s(RoundedCorners::new().left(15))
|
||||
.s(Width::growable())
|
||||
.s(Background::new().color_signal(
|
||||
hovered_signal.map_bool(|| COLOR_SLATE_BLUE, || COLOR_SLATE_BLUE_WITH_ALPHA),
|
||||
))
|
||||
.s(RoundedCorners::new().left(15).right(5))
|
||||
.label(
|
||||
El::new()
|
||||
.s(Align::center())
|
||||
.update_raw_el(|raw_el| {
|
||||
raw_el
|
||||
// @TODO move `title` to MZ API? (as `native_tooltip`?)
|
||||
.attr("title", name)
|
||||
// Note: `text-overflow` / ellipsis` doesn't work with flex and dynamic sizes
|
||||
.style("text-overflow", "ellipsis")
|
||||
.style("display", "inline-block")
|
||||
})
|
||||
.s(Scrollbars::both().visible(false))
|
||||
.s(Width::default().max(400))
|
||||
.s(Align::new().left())
|
||||
.s(Padding::new().left(20).right(17).y(10))
|
||||
.child(name),
|
||||
)
|
||||
.on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered))
|
||||
.on_press(move || {
|
||||
if let Some(index) = index.get() {
|
||||
selected_var_refs.lock_mut().remove(index);
|
||||
}
|
||||
})
|
||||
.apply(Some)
|
||||
}
|
||||
|
||||
fn selected_var_format_button(&self, index: ReadOnlyMutable<Option<usize>>) -> impl Element {
|
||||
let var_format = Mutable::new(shared::VarFormat::default());
|
||||
let (hovered, hovered_signal) = Mutable::new_and_signal(false);
|
||||
let canvas_controller = self.canvas_controller.clone();
|
||||
Button::new()
|
||||
.s(Height::exact(ROW_HEIGHT))
|
||||
.s(Width::exact(70))
|
||||
.s(Background::new().color_signal(
|
||||
hovered_signal.map_bool(|| COLOR_SLATE_BLUE, || COLOR_SLATE_BLUE_WITH_ALPHA),
|
||||
))
|
||||
.s(RoundedCorners::new().left(5))
|
||||
.label(
|
||||
El::new()
|
||||
.s(Align::center())
|
||||
.s(Padding::new().left(20).right(17).y(10))
|
||||
.child_signal(var_format.signal().map(|format| format.as_static_str())),
|
||||
)
|
||||
.on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered))
|
||||
.on_press(move || {
|
||||
let next_format = var_format.get().next();
|
||||
var_format.set(next_format);
|
||||
if let Some(canvas_controller) = canvas_controller.get_cloned().lock_ref().as_ref()
|
||||
{
|
||||
if let Some(index) = index.get() {
|
||||
canvas_controller.set_var_format(
|
||||
index,
|
||||
serde_wasm_bindgen::to_value(&next_format).unwrap_throw(),
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
use crate::platform;
|
||||
pub use js_bridge::PixiController;
|
||||
use std::rc::Rc;
|
||||
use zoon::*;
|
||||
|
||||
pub struct PixiCanvas {
|
||||
raw_el: RawHtmlEl<web_sys::HtmlElement>,
|
||||
controller: ReadOnlyMutable<Option<js_bridge::PixiController>>,
|
||||
controller: Mutable<Option<SendWrapper<js_bridge::PixiController>>>,
|
||||
#[allow(dead_code)]
|
||||
width: ReadOnlyMutable<u32>,
|
||||
#[allow(dead_code)]
|
||||
|
@ -30,23 +32,53 @@ impl HasIds for PixiCanvas {}
|
|||
|
||||
impl PixiCanvas {
|
||||
pub fn new(row_height: u32, row_gap: u32) -> Self {
|
||||
let controller: Mutable<Option<js_bridge::PixiController>> = Mutable::new(None);
|
||||
let controller: Mutable<Option<SendWrapper<js_bridge::PixiController>>> =
|
||||
Mutable::new(None);
|
||||
let width = Mutable::new(0);
|
||||
let height = Mutable::new(0);
|
||||
let resize_task = Task::start_droppable(
|
||||
map_ref! {
|
||||
let _ = width.signal(),
|
||||
let _ = height.signal() => ()
|
||||
let width = width.signal(),
|
||||
let height = height.signal() => (*width, *height)
|
||||
}
|
||||
.for_each_sync(clone!((controller) move |_| {
|
||||
if let Some(controller) = controller.lock_ref().as_ref() {
|
||||
controller.queue_resize();
|
||||
}
|
||||
})),
|
||||
.dedupe()
|
||||
.throttle(|| Timer::sleep(50))
|
||||
.for_each(
|
||||
clone!((controller) move |(width, height)| clone!((controller) async move {
|
||||
if let Some(controller) = controller.lock_ref().as_ref() {
|
||||
controller.resize(width, height).await
|
||||
}
|
||||
})),
|
||||
),
|
||||
);
|
||||
let task_with_controller = Mutable::new(None);
|
||||
// -- FastWave-specific --
|
||||
let timeline_getter = Rc::new(Closure::new(
|
||||
|signal_ref_index,
|
||||
timeline_zoom,
|
||||
timeline_viewport_width,
|
||||
timeline_viewport_x,
|
||||
row_height,
|
||||
var_format| {
|
||||
future_to_promise(async move {
|
||||
let signal_ref = wellen::SignalRef::from_index(signal_ref_index).unwrap_throw();
|
||||
let timeline = platform::load_signal_and_get_timeline(
|
||||
signal_ref,
|
||||
timeline_zoom,
|
||||
timeline_viewport_width,
|
||||
timeline_viewport_x,
|
||||
row_height,
|
||||
serde_wasm_bindgen::from_value(var_format).unwrap_throw(),
|
||||
)
|
||||
.await;
|
||||
let timeline = serde_wasm_bindgen::to_value(&timeline).unwrap_throw();
|
||||
Ok(timeline)
|
||||
})
|
||||
},
|
||||
));
|
||||
// -- // --
|
||||
Self {
|
||||
controller: controller.read_only(),
|
||||
controller: controller.clone(),
|
||||
width: width.read_only(),
|
||||
height: height.read_only(),
|
||||
task_with_controller: task_with_controller.clone(),
|
||||
|
@ -56,14 +88,38 @@ impl PixiCanvas {
|
|||
width.set_neq(new_width);
|
||||
height.set_neq(new_height);
|
||||
}))
|
||||
.after_insert(clone!((controller) move |element| {
|
||||
.update_raw_el(|raw_el| {
|
||||
// @TODO rewrite to a native Zoon API
|
||||
raw_el.event_handler_with_options(
|
||||
EventOptions::new().preventable(),
|
||||
clone!((controller) move |event: events_extra::WheelEvent| {
|
||||
event.prevent_default();
|
||||
if let Some(controller) = controller.lock_ref().as_ref() {
|
||||
controller.zoom_or_pan(
|
||||
event.delta_y(),
|
||||
event.shift_key(),
|
||||
event.offset_x() as u32,
|
||||
);
|
||||
}
|
||||
}),
|
||||
)
|
||||
})
|
||||
.after_insert(clone!((controller, timeline_getter) move |element| {
|
||||
Task::start(async move {
|
||||
let pixi_controller = js_bridge::PixiController::new(row_height, row_gap);
|
||||
let pixi_controller = SendWrapper::new(js_bridge::PixiController::new(
|
||||
1.,
|
||||
width.get(),
|
||||
0,
|
||||
row_height,
|
||||
row_gap,
|
||||
&timeline_getter
|
||||
));
|
||||
pixi_controller.init(&element).await;
|
||||
controller.set(Some(pixi_controller));
|
||||
});
|
||||
}))
|
||||
.after_remove(move |_| {
|
||||
drop(timeline_getter);
|
||||
drop(resize_task);
|
||||
drop(task_with_controller);
|
||||
if let Some(controller) = controller.take() {
|
||||
|
@ -76,7 +132,7 @@ impl PixiCanvas {
|
|||
|
||||
pub fn task_with_controller<FUT: Future<Output = ()> + 'static>(
|
||||
self,
|
||||
f: impl FnOnce(ReadOnlyMutable<Option<js_bridge::PixiController>>) -> FUT,
|
||||
f: impl FnOnce(Mutable<Option<SendWrapper<js_bridge::PixiController>>>) -> FUT,
|
||||
) -> Self {
|
||||
self.task_with_controller
|
||||
.set(Some(Task::start_droppable(f(self.controller.clone()))));
|
||||
|
@ -87,6 +143,24 @@ impl PixiCanvas {
|
|||
mod js_bridge {
|
||||
use zoon::*;
|
||||
|
||||
type TimelinePromise = js_sys::Promise;
|
||||
type SignalRefIndex = usize;
|
||||
type TimelineZoom = f64;
|
||||
type TimelineViewportWidth = u32;
|
||||
type TimelineViewportX = i32;
|
||||
type RowHeight = u32;
|
||||
type VarFormatJs = JsValue;
|
||||
type TimelineGetter = Closure<
|
||||
dyn FnMut(
|
||||
SignalRefIndex,
|
||||
TimelineZoom,
|
||||
TimelineViewportWidth,
|
||||
TimelineViewportX,
|
||||
RowHeight,
|
||||
VarFormatJs,
|
||||
) -> TimelinePromise,
|
||||
>;
|
||||
|
||||
// Note: Add all corresponding methods to `frontend/typescript/pixi_canvas/pixi_canvas.ts`
|
||||
#[wasm_bindgen(module = "/typescript/bundles/pixi_canvas.js")]
|
||||
extern "C" {
|
||||
|
@ -95,29 +169,64 @@ mod js_bridge {
|
|||
|
||||
// @TODO `row_height` and `row_gap` is FastWave-specific
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(row_height: u32, row_gap: u32) -> PixiController;
|
||||
pub fn new(
|
||||
timeline_zoom: f64,
|
||||
timeline_viewport_width: u32,
|
||||
timeline_viewport_x: i32,
|
||||
row_height: u32,
|
||||
row_gap: u32,
|
||||
timeline_getter: &TimelineGetter,
|
||||
) -> PixiController;
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub async fn init(this: &PixiController, parent_element: &JsValue);
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn destroy(this: &PixiController);
|
||||
pub async fn resize(this: &PixiController, width: u32, height: u32);
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn queue_resize(this: &PixiController);
|
||||
pub fn destroy(this: &PixiController);
|
||||
|
||||
// -- FastWave-specific --
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn get_timeline_zoom(this: &PixiController) -> f64;
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn get_timeline_viewport_width(this: &PixiController) -> u32;
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn get_timeline_viewport_x(this: &PixiController) -> i32;
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn set_var_format(this: &PixiController, index: usize, var_format: JsValue);
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn zoom_or_pan(
|
||||
this: &PixiController,
|
||||
wheel_delta_y: f64,
|
||||
shift_key: bool,
|
||||
offset_x: u32,
|
||||
);
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn remove_var(this: &PixiController, index: usize);
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn push_var(this: &PixiController, timeline: JsValue);
|
||||
pub fn push_var(
|
||||
this: &PixiController,
|
||||
signal_ref_index: usize,
|
||||
timeline: JsValue,
|
||||
var_format: JsValue,
|
||||
);
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn pop_var(this: &PixiController);
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn clear_vars(this: &PixiController);
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub async fn redraw_all_rows(this: &PixiController);
|
||||
}
|
||||
}
|
||||
|
|
5
frontend/src/waveform_panel/template_vars.px.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
if (FW.loaded_filename() === "{LOADED_FILENAME}") {
|
||||
FW.select_vars([
|
||||
{FULL_VAR_NAMES}
|
||||
])
|
||||
}
|
352
frontend/typescript/bundles/excalidraw_canvas.js
Normal file
5
frontend/typescript/bundles/strict_eval.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
"use strict";
|
||||
|
||||
export async function strict_eval(code) {
|
||||
return await eval(code)
|
||||
}
|
25
frontend/typescript/excalidraw_canvas/README.md
Normal file
|
@ -0,0 +1,25 @@
|
|||
Init
|
||||
- `npm install && cp -r locales node_modules/@excalidraw/excalidraw/types/`
|
||||
|
||||
Watch & build (without typechecking)
|
||||
- `node_modules/.bin/esbuild excalidraw_canvas.ts --bundle --minify --outfile=../bundles/excalidraw_canvas.js --format=esm --watch`
|
||||
|
||||
Watch & typecheck (without building)
|
||||
- `node_modules/.bin/tsc excalidraw_canvas.ts --watch -noEmit --preserveWatchOutput --target esnext --module esnext --moduleResolution bundler`
|
||||
|
||||
Created with commands:
|
||||
- `npm i -E react react-dom @excalidraw/excalidraw`
|
||||
- `npm i -E @types/react @types/react-dom`
|
||||
- `npm i -E roughjs @excalidraw/laser-pointer jotai browser-fs-access`
|
||||
- `npm i -D esbuild typescript`
|
||||
- `locales/en.json` downloaded from `https://raw.githubusercontent.com/excalidraw/excalidraw/refs/tags/v0.17.6/src/locales/en.json`
|
||||
- `excalidraw-assets-dev` and `excalidraw-assets` from `FastWave2.0\frontend\typescript\excalidraw_canvas\node_modules\@excalidraw\excalidraw\dist` copied into `FastWave2.0\public\excalidraw`
|
||||
- Lines added to `FastWave2.0\backend\globals.js`:
|
||||
```js
|
||||
// -- Excalidraw settings --
|
||||
// @TODO replace with "true" once Preact is integrated into ExcalidrawCanvas
|
||||
var process = { env: { IS_PREACT: "false" } };
|
||||
// @TODO probably remove or update once Preact is integrated into ExcalidrawCanvas
|
||||
window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = { isDisabled: true };
|
||||
window.EXCALIDRAW_ASSET_PATH = "/_api/public/excalidraw/";
|
||||
```
|
107
frontend/typescript/excalidraw_canvas/excalidraw_canvas.tsx
Normal file
|
@ -0,0 +1,107 @@
|
|||
import { Excalidraw, MainMenu } from '@excalidraw/excalidraw'
|
||||
import { ExcalidrawImperativeAPI } from '@excalidraw/excalidraw/types/types'
|
||||
import { ExcalidrawElement } from '@excalidraw/excalidraw/types/element/types'
|
||||
// @TODO doesn't work with Excalidraw 0.17.6
|
||||
// import { ExcalidrawElementSkeleton, convertToExcalidrawElements } from '@excalidraw/excalidraw/types/data/transform'
|
||||
import * as React from 'react'
|
||||
import * as ReactDOM from 'react-dom/client'
|
||||
|
||||
export class ExcalidrawController {
|
||||
api: Promise<ExcalidrawImperativeAPI>
|
||||
resolve_api: (api: ExcalidrawImperativeAPI) => void
|
||||
|
||||
constructor() {
|
||||
this.resolve_api = (api) => {};
|
||||
this.api = new Promise(resolve => {
|
||||
this.resolve_api = (api) => resolve(api)
|
||||
});
|
||||
}
|
||||
|
||||
draw_diagram_element(excalidraw_element: ExcalidrawElement) {
|
||||
this.api.then(api => {
|
||||
const elements = api.getSceneElements()
|
||||
api.updateScene({
|
||||
elements: elements.concat(excalidraw_element)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
listen_for_component_text_changes(id: string, on_change: (text: string) => void) {
|
||||
this.api.then(api => {
|
||||
let old_text: string | null = null;
|
||||
api.onChange((elements: readonly ExcalidrawElement[]) => {
|
||||
const element = elements.find(element => element.id === id);
|
||||
if (typeof element !== 'undefined') {
|
||||
if (element.type === 'text') {
|
||||
if (old_text === null) {
|
||||
old_text = element.text;
|
||||
on_change(old_text);
|
||||
} else {
|
||||
if (old_text !== element.text) {
|
||||
old_text = element.text;
|
||||
on_change(old_text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
set_component_text(id: string, text: string) {
|
||||
this.api.then(api => {
|
||||
let element_found = false;
|
||||
const elements = api.getSceneElements().map(element => {
|
||||
if (element.id === id) {
|
||||
element_found = true;
|
||||
return { ...element, text: text, originalText: text }
|
||||
} else {
|
||||
return element
|
||||
}
|
||||
});
|
||||
if (element_found) {
|
||||
api.updateScene({
|
||||
elements
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async init(parent_element: HTMLElement) {
|
||||
const App = () => {
|
||||
return (
|
||||
<>
|
||||
<div style={{ height: "100%" }}>
|
||||
<Excalidraw
|
||||
theme='dark'
|
||||
gridModeEnabled={true}
|
||||
UIOptions={{canvasActions: {toggleTheme: true}}}
|
||||
initialData={{appState: {
|
||||
// Canvas background: 3. default, blue
|
||||
viewBackgroundColor: "#f5faff",
|
||||
// Sloppiness: Artist
|
||||
currentItemRoughness: 0,
|
||||
// Font family: Code
|
||||
currentItemFontFamily: 3,
|
||||
}}}
|
||||
excalidrawAPI={(api) => this.resolve_api(api)}
|
||||
>
|
||||
<MainMenu>
|
||||
<MainMenu.DefaultItems.LoadScene />
|
||||
<MainMenu.DefaultItems.SaveToActiveFile />
|
||||
<MainMenu.DefaultItems.Export />
|
||||
<MainMenu.DefaultItems.SaveAsImage />
|
||||
<MainMenu.DefaultItems.ClearCanvas />
|
||||
<MainMenu.DefaultItems.ToggleTheme />
|
||||
<MainMenu.DefaultItems.ChangeCanvasBackground />
|
||||
</MainMenu>
|
||||
</Excalidraw>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
const root = ReactDOM.createRoot(parent_element);
|
||||
root.render(React.createElement(App));
|
||||
}
|
||||
}
|
||||
|
612
frontend/typescript/excalidraw_canvas/locales/en.json
Normal file
|
@ -0,0 +1,612 @@
|
|||
{
|
||||
"labels": {
|
||||
"paste": "Paste",
|
||||
"pasteAsPlaintext": "Paste as plaintext",
|
||||
"pasteCharts": "Paste charts",
|
||||
"selectAll": "Select all",
|
||||
"multiSelect": "Add element to selection",
|
||||
"moveCanvas": "Move canvas",
|
||||
"cut": "Cut",
|
||||
"copy": "Copy",
|
||||
"copyAsPng": "Copy to clipboard as PNG",
|
||||
"copyAsSvg": "Copy to clipboard as SVG",
|
||||
"copyText": "Copy to clipboard as text",
|
||||
"copySource": "Copy source to clipboard",
|
||||
"convertToCode": "Convert to code",
|
||||
"bringForward": "Bring forward",
|
||||
"sendToBack": "Send to back",
|
||||
"bringToFront": "Bring to front",
|
||||
"sendBackward": "Send backward",
|
||||
"delete": "Delete",
|
||||
"copyStyles": "Copy styles",
|
||||
"pasteStyles": "Paste styles",
|
||||
"stroke": "Stroke",
|
||||
"changeStroke": "Change stroke color",
|
||||
"background": "Background",
|
||||
"changeBackground": "Change background color",
|
||||
"fill": "Fill",
|
||||
"strokeWidth": "Stroke width",
|
||||
"strokeStyle": "Stroke style",
|
||||
"strokeStyle_solid": "Solid",
|
||||
"strokeStyle_dashed": "Dashed",
|
||||
"strokeStyle_dotted": "Dotted",
|
||||
"sloppiness": "Sloppiness",
|
||||
"opacity": "Opacity",
|
||||
"textAlign": "Text align",
|
||||
"edges": "Edges",
|
||||
"sharp": "Sharp",
|
||||
"round": "Round",
|
||||
"arrowheads": "Arrowheads",
|
||||
"arrowhead_none": "None",
|
||||
"arrowhead_arrow": "Arrow",
|
||||
"arrowhead_bar": "Bar",
|
||||
"arrowhead_circle": "Circle",
|
||||
"arrowhead_circle_outline": "Circle (outline)",
|
||||
"arrowhead_triangle": "Triangle",
|
||||
"arrowhead_triangle_outline": "Triangle (outline)",
|
||||
"arrowhead_diamond": "Diamond",
|
||||
"arrowhead_diamond_outline": "Diamond (outline)",
|
||||
"arrowtypes": "Arrow type",
|
||||
"arrowtype_sharp": "Sharp arrow",
|
||||
"arrowtype_round": "Curved arrow",
|
||||
"arrowtype_elbowed": "Elbow arrow",
|
||||
"fontSize": "Font size",
|
||||
"fontFamily": "Font family",
|
||||
"addWatermark": "Add \"Made with Excalidraw\"",
|
||||
"handDrawn": "Hand-drawn",
|
||||
"normal": "Normal",
|
||||
"code": "Code",
|
||||
"small": "Small",
|
||||
"medium": "Medium",
|
||||
"large": "Large",
|
||||
"veryLarge": "Very large",
|
||||
"solid": "Solid",
|
||||
"hachure": "Hachure",
|
||||
"zigzag": "Zigzag",
|
||||
"crossHatch": "Cross-hatch",
|
||||
"thin": "Thin",
|
||||
"bold": "Bold",
|
||||
"left": "Left",
|
||||
"center": "Center",
|
||||
"right": "Right",
|
||||
"extraBold": "Extra bold",
|
||||
"architect": "Architect",
|
||||
"artist": "Artist",
|
||||
"cartoonist": "Cartoonist",
|
||||
"fileTitle": "File name",
|
||||
"colorPicker": "Color picker",
|
||||
"canvasColors": "Used on canvas",
|
||||
"canvasBackground": "Canvas background",
|
||||
"drawingCanvas": "Drawing canvas",
|
||||
"clearCanvas": "Clear canvas",
|
||||
"layers": "Layers",
|
||||
"actions": "Actions",
|
||||
"language": "Language",
|
||||
"liveCollaboration": "Live collaboration...",
|
||||
"duplicateSelection": "Duplicate",
|
||||
"untitled": "Untitled",
|
||||
"name": "Name",
|
||||
"yourName": "Your name",
|
||||
"madeWithExcalidraw": "Made with Excalidraw",
|
||||
"group": "Group selection",
|
||||
"ungroup": "Ungroup selection",
|
||||
"collaborators": "Collaborators",
|
||||
"toggleGrid": "Toggle grid",
|
||||
"addToLibrary": "Add to library",
|
||||
"removeFromLibrary": "Remove from library",
|
||||
"libraryLoadingMessage": "Loading library…",
|
||||
"libraries": "Browse libraries",
|
||||
"loadingScene": "Loading scene…",
|
||||
"loadScene": "Load scene from file",
|
||||
"align": "Align",
|
||||
"alignTop": "Align top",
|
||||
"alignBottom": "Align bottom",
|
||||
"alignLeft": "Align left",
|
||||
"alignRight": "Align right",
|
||||
"centerVertically": "Center vertically",
|
||||
"centerHorizontally": "Center horizontally",
|
||||
"distributeHorizontally": "Distribute horizontally",
|
||||
"distributeVertically": "Distribute vertically",
|
||||
"flipHorizontal": "Flip horizontal",
|
||||
"flipVertical": "Flip vertical",
|
||||
"viewMode": "View mode",
|
||||
"share": "Share",
|
||||
"showStroke": "Show stroke color picker",
|
||||
"showBackground": "Show background color picker",
|
||||
"showFonts": "Show font picker",
|
||||
"toggleTheme": "Toggle light/dark theme",
|
||||
"theme": "Theme",
|
||||
"personalLib": "Personal Library",
|
||||
"excalidrawLib": "Excalidraw Library",
|
||||
"decreaseFontSize": "Decrease font size",
|
||||
"increaseFontSize": "Increase font size",
|
||||
"unbindText": "Unbind text",
|
||||
"bindText": "Bind text to the container",
|
||||
"createContainerFromText": "Wrap text in a container",
|
||||
"link": {
|
||||
"edit": "Edit link",
|
||||
"editEmbed": "Edit link & embed",
|
||||
"create": "Create link",
|
||||
"createEmbed": "Create link & embed",
|
||||
"label": "Link",
|
||||
"labelEmbed": "Link & embed",
|
||||
"empty": "No link is set"
|
||||
},
|
||||
"lineEditor": {
|
||||
"edit": "Edit line",
|
||||
"editArrow": "Edit arrow"
|
||||
},
|
||||
"elementLock": {
|
||||
"lock": "Lock",
|
||||
"unlock": "Unlock",
|
||||
"lockAll": "Lock all",
|
||||
"unlockAll": "Unlock all"
|
||||
},
|
||||
"statusPublished": "Published",
|
||||
"sidebarLock": "Keep sidebar open",
|
||||
"selectAllElementsInFrame": "Select all elements in frame",
|
||||
"removeAllElementsFromFrame": "Remove all elements from frame",
|
||||
"eyeDropper": "Pick color from canvas",
|
||||
"textToDiagram": "Text to diagram",
|
||||
"prompt": "Prompt",
|
||||
"followUs": "Follow us",
|
||||
"discordChat": "Discord chat",
|
||||
"zoomToFitViewport": "Zoom to fit in viewport",
|
||||
"zoomToFitSelection": "Zoom to fit selection",
|
||||
"zoomToFit": "Zoom to fit all elements",
|
||||
"installPWA": "Install Excalidraw locally (PWA)",
|
||||
"autoResize": "Enable text auto-resizing"
|
||||
},
|
||||
"library": {
|
||||
"noItems": "No items added yet...",
|
||||
"hint_emptyLibrary": "Select an item on canvas to add it here, or install a library from the public repository, below.",
|
||||
"hint_emptyPrivateLibrary": "Select an item on canvas to add it here."
|
||||
},
|
||||
"search": {
|
||||
"title": "Find on canvas",
|
||||
"noMatch": "No matches found...",
|
||||
"singleResult": "result",
|
||||
"multipleResults": "results",
|
||||
"placeholder": "Find text on canvas..."
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Reset the canvas",
|
||||
"exportJSON": "Export to file",
|
||||
"exportImage": "Export image...",
|
||||
"export": "Save to...",
|
||||
"copyToClipboard": "Copy to clipboard",
|
||||
"copyLink": "Copy link",
|
||||
"save": "Save to current file",
|
||||
"saveAs": "Save as",
|
||||
"load": "Open",
|
||||
"getShareableLink": "Get shareable link",
|
||||
"close": "Close",
|
||||
"selectLanguage": "Select language",
|
||||
"scrollBackToContent": "Scroll back to content",
|
||||
"zoomIn": "Zoom in",
|
||||
"zoomOut": "Zoom out",
|
||||
"resetZoom": "Reset zoom",
|
||||
"menu": "Menu",
|
||||
"done": "Done",
|
||||
"edit": "Edit",
|
||||
"undo": "Undo",
|
||||
"redo": "Redo",
|
||||
"resetLibrary": "Reset library",
|
||||
"createNewRoom": "Create new room",
|
||||
"fullScreen": "Full screen",
|
||||
"darkMode": "Dark mode",
|
||||
"lightMode": "Light mode",
|
||||
"systemMode": "System mode",
|
||||
"zenMode": "Zen mode",
|
||||
"objectsSnapMode": "Snap to objects",
|
||||
"exitZenMode": "Exit zen mode",
|
||||
"cancel": "Cancel",
|
||||
"clear": "Clear",
|
||||
"remove": "Remove",
|
||||
"embed": "Toggle embedding",
|
||||
"publishLibrary": "Publish",
|
||||
"submit": "Submit",
|
||||
"confirm": "Confirm",
|
||||
"embeddableInteractionButton": "Click to interact"
|
||||
},
|
||||
"alerts": {
|
||||
"clearReset": "This will clear the whole canvas. Are you sure?",
|
||||
"couldNotCreateShareableLink": "Couldn't create shareable link.",
|
||||
"couldNotCreateShareableLinkTooBig": "Couldn't create shareable link: the scene is too big",
|
||||
"couldNotLoadInvalidFile": "Couldn't load invalid file",
|
||||
"importBackendFailed": "Importing from backend failed.",
|
||||
"cannotExportEmptyCanvas": "Cannot export empty canvas.",
|
||||
"couldNotCopyToClipboard": "Couldn't copy to clipboard.",
|
||||
"decryptFailed": "Couldn't decrypt data.",
|
||||
"uploadedSecurly": "The upload has been secured with end-to-end encryption, which means that Excalidraw server and third parties can't read the content.",
|
||||
"loadSceneOverridePrompt": "Loading external drawing will replace your existing content. Do you wish to continue?",
|
||||
"collabStopOverridePrompt": "Stopping the session will overwrite your previous, locally stored drawing. Are you sure?\n\n(If you want to keep your local drawing, simply close the browser tab instead.)",
|
||||
"errorAddingToLibrary": "Couldn't add item to the library",
|
||||
"errorRemovingFromLibrary": "Couldn't remove item from the library",
|
||||
"confirmAddLibrary": "This will add {{numShapes}} shape(s) to your library. Are you sure?",
|
||||
"imageDoesNotContainScene": "This image does not seem to contain any scene data. Have you enabled scene embedding during export?",
|
||||
"cannotRestoreFromImage": "Scene couldn't be restored from this image file",
|
||||
"invalidSceneUrl": "Couldn't import scene from the supplied URL. It's either malformed, or doesn't contain valid Excalidraw JSON data.",
|
||||
"resetLibrary": "This will clear your library. Are you sure?",
|
||||
"removeItemsFromsLibrary": "Delete {{count}} item(s) from library?",
|
||||
"invalidEncryptionKey": "Encryption key must be of 22 characters. Live collaboration is disabled.",
|
||||
"collabOfflineWarning": "No internet connection available.\nYour changes will not be saved!"
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Unsupported file type.",
|
||||
"imageInsertError": "Couldn't insert image. Try again later...",
|
||||
"fileTooBig": "File is too big. Maximum allowed size is {{maxSize}}.",
|
||||
"svgImageInsertError": "Couldn't insert SVG image. The SVG markup looks invalid.",
|
||||
"failedToFetchImage": "Failed to fetch image.",
|
||||
"cannotResolveCollabServer": "Couldn't connect to the collab server. Please reload the page and try again.",
|
||||
"importLibraryError": "Couldn't load library",
|
||||
"saveLibraryError": "Couldn't save library to storage. Please save your library to a file locally to make sure you don't lose changes.",
|
||||
"collabSaveFailed": "Couldn't save to the backend database. If problems persist, you should save your file locally to ensure you don't lose your work.",
|
||||
"collabSaveFailed_sizeExceeded": "Couldn't save to the backend database, the canvas seems to be too big. You should save the file locally to ensure you don't lose your work.",
|
||||
"imageToolNotSupported": "Images are disabled.",
|
||||
"brave_measure_text_error": {
|
||||
"line1": "Looks like you are using Brave browser with the <bold>Aggressively Block Fingerprinting</bold> setting enabled.",
|
||||
"line2": "This could result in breaking the <bold>Text Elements</bold> in your drawings.",
|
||||
"line3": "We strongly recommend disabling this setting. You can follow <link>these steps</link> on how to do so.",
|
||||
"line4": "If disabling this setting doesn't fix the display of text elements, please open an <issueLink>issue</issueLink> on our GitHub, or write us on <discordLink>Discord</discordLink>"
|
||||
},
|
||||
"libraryElementTypeError": {
|
||||
"embeddable": "Embeddable elements cannot be added to the library.",
|
||||
"iframe": "IFrame elements cannot be added to the library.",
|
||||
"image": "Support for adding images to the library coming soon!"
|
||||
},
|
||||
"asyncPasteFailedOnRead": "Couldn't paste (couldn't read from system clipboard).",
|
||||
"asyncPasteFailedOnParse": "Couldn't paste.",
|
||||
"copyToSystemClipboardFailed": "Couldn't copy to clipboard."
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "Selection",
|
||||
"image": "Insert image",
|
||||
"rectangle": "Rectangle",
|
||||
"diamond": "Diamond",
|
||||
"ellipse": "Ellipse",
|
||||
"arrow": "Arrow",
|
||||
"line": "Line",
|
||||
"freedraw": "Draw",
|
||||
"text": "Text",
|
||||
"library": "Library",
|
||||
"lock": "Keep selected tool active after drawing",
|
||||
"penMode": "Pen mode - prevent touch",
|
||||
"link": "Add / Update link for a selected shape",
|
||||
"eraser": "Eraser",
|
||||
"frame": "Frame tool",
|
||||
"magicframe": "Wireframe to code",
|
||||
"embeddable": "Web Embed",
|
||||
"laser": "Laser pointer",
|
||||
"hand": "Hand (panning tool)",
|
||||
"extraTools": "More tools",
|
||||
"mermaidToExcalidraw": "Mermaid to Excalidraw"
|
||||
},
|
||||
"element": {
|
||||
"rectangle": "Rectangle",
|
||||
"diamond": "Diamond",
|
||||
"ellipse": "Ellipse",
|
||||
"arrow": "Arrow",
|
||||
"line": "Line",
|
||||
"freedraw": "Freedraw",
|
||||
"text": "Text",
|
||||
"image": "Image",
|
||||
"group": "Group",
|
||||
"frame": "Frame",
|
||||
"magicframe": "Wireframe to code",
|
||||
"embeddable": "Web Embed",
|
||||
"selection": "Selection",
|
||||
"iframe": "IFrame"
|
||||
},
|
||||
"headings": {
|
||||
"canvasActions": "Canvas actions",
|
||||
"selectedShapeActions": "Selected shape actions",
|
||||
"shapes": "Shapes"
|
||||
},
|
||||
"hints": {
|
||||
"dismissSearch": "Escape to dismiss search",
|
||||
"canvasPanning": "To move canvas, hold mouse wheel or spacebar while dragging, or use the hand tool",
|
||||
"linearElement": "Click to start multiple points, drag for single line",
|
||||
"arrowTool": "Click to start multiple points, drag for single line. Press {{arrowShortcut}} again to change arrow type.",
|
||||
"freeDraw": "Click and drag, release when you're finished",
|
||||
"text": "Tip: you can also add text by double-clicking anywhere with the selection tool",
|
||||
"embeddable": "Click-drag to create a website embed",
|
||||
"text_selected": "Double-click or press ENTER to edit text",
|
||||
"text_editing": "Press Escape or CtrlOrCmd+ENTER to finish editing",
|
||||
"linearElementMulti": "Click on last point or press Escape or Enter to finish",
|
||||
"lockAngle": "You can constrain angle by holding SHIFT",
|
||||
"resize": "You can constrain proportions by holding SHIFT while resizing,\nhold ALT to resize from the center",
|
||||
"resizeImage": "You can resize freely by holding SHIFT,\nhold ALT to resize from the center",
|
||||
"rotate": "You can constrain angles by holding SHIFT while rotating",
|
||||
"lineEditor_info": "Hold CtrlOrCmd and Double-click or press CtrlOrCmd + Enter to edit points",
|
||||
"lineEditor_pointSelected": "Press Delete to remove point(s),\nCtrlOrCmd+D to duplicate, or drag to move",
|
||||
"lineEditor_nothingSelected": "Select a point to edit (hold SHIFT to select multiple),\nor hold Alt and click to add new points",
|
||||
"placeImage": "Click to place the image, or click and drag to set its size manually",
|
||||
"publishLibrary": "Publish your own library",
|
||||
"bindTextToElement": "Press enter to add text",
|
||||
"createFlowchart": "Hold CtrlOrCmd and Arrow key to create a flowchart",
|
||||
"deepBoxSelect": "Hold CtrlOrCmd to deep select, and to prevent dragging",
|
||||
"eraserRevert": "Hold Alt to revert the elements marked for deletion",
|
||||
"firefox_clipboard_write": "This feature can likely be enabled by setting the \"dom.events.asyncClipboard.clipboardItem\" flag to \"true\". To change the browser flags in Firefox, visit the \"about:config\" page.",
|
||||
"disableSnapping": "Hold CtrlOrCmd to disable snapping"
|
||||
},
|
||||
"canvasError": {
|
||||
"cannotShowPreview": "Cannot show preview",
|
||||
"canvasTooBig": "The canvas may be too big.",
|
||||
"canvasTooBigTip": "Tip: try moving the farthest elements a bit closer together."
|
||||
},
|
||||
"errorSplash": {
|
||||
"headingMain": "Encountered an error. Try <button>reloading the page</button>.",
|
||||
"clearCanvasMessage": "If reloading doesn't work, try <button>clearing the canvas</button>.",
|
||||
"clearCanvasCaveat": " This will result in loss of work ",
|
||||
"trackedToSentry": "The error with identifier {{eventId}} was tracked on our system.",
|
||||
"openIssueMessage": "We were very cautious not to include your scene information on the error. If your scene is not private, please consider following up on our <button>bug tracker</button>. Please include information below by copying and pasting into the GitHub issue.",
|
||||
"sceneContent": "Scene content:"
|
||||
},
|
||||
"shareDialog": {
|
||||
"or": "Or"
|
||||
},
|
||||
"roomDialog": {
|
||||
"desc_intro": "Invite people to collaborate on your drawing.",
|
||||
"desc_privacy": "Don't worry, the session is end-to-end encrypted, and fully private. Not even our server can see what you draw.",
|
||||
"button_startSession": "Start session",
|
||||
"button_stopSession": "Stop session",
|
||||
"desc_inProgressIntro": "Live-collaboration session is now in progress.",
|
||||
"desc_shareLink": "Share this link with anyone you want to collaborate with:",
|
||||
"desc_exitSession": "Stopping the session will disconnect you from the room, but you'll be able to continue working with the scene, locally. Note that this won't affect other people, and they'll still be able to collaborate on their version.",
|
||||
"shareTitle": "Join a live collaboration session on Excalidraw"
|
||||
},
|
||||
"errorDialog": {
|
||||
"title": "Error"
|
||||
},
|
||||
"exportDialog": {
|
||||
"disk_title": "Save to disk",
|
||||
"disk_details": "Export the scene data to a file from which you can import later.",
|
||||
"disk_button": "Save to file",
|
||||
"link_title": "Shareable link",
|
||||
"link_details": "Export as a read-only link.",
|
||||
"link_button": "Export to Link",
|
||||
"excalidrawplus_description": "Save the scene to your Excalidraw+ workspace.",
|
||||
"excalidrawplus_button": "Export",
|
||||
"excalidrawplus_exportError": "Couldn't export to Excalidraw+ at this moment..."
|
||||
},
|
||||
"helpDialog": {
|
||||
"blog": "Read our blog",
|
||||
"click": "click",
|
||||
"deepSelect": "Deep select",
|
||||
"deepBoxSelect": "Deep select within box, and prevent dragging",
|
||||
"createFlowchart": "Create a flowchart from a generic element",
|
||||
"navigateFlowchart": "Navigate a flowchart",
|
||||
"curvedArrow": "Curved arrow",
|
||||
"curvedLine": "Curved line",
|
||||
"documentation": "Documentation",
|
||||
"doubleClick": "double-click",
|
||||
"drag": "drag",
|
||||
"editor": "Editor",
|
||||
"editLineArrowPoints": "Edit line/arrow points",
|
||||
"editText": "Edit text / add label",
|
||||
"github": "Found an issue? Submit",
|
||||
"howto": "Follow our guides",
|
||||
"or": "or",
|
||||
"preventBinding": "Prevent arrow binding",
|
||||
"tools": "Tools",
|
||||
"shortcuts": "Keyboard shortcuts",
|
||||
"textFinish": "Finish editing (text editor)",
|
||||
"textNewLine": "Add new line (text editor)",
|
||||
"title": "Help",
|
||||
"view": "View",
|
||||
"zoomToFit": "Zoom to fit all elements",
|
||||
"zoomToSelection": "Zoom to selection",
|
||||
"toggleElementLock": "Lock/unlock selection",
|
||||
"movePageUpDown": "Move page up/down",
|
||||
"movePageLeftRight": "Move page left/right"
|
||||
},
|
||||
"clearCanvasDialog": {
|
||||
"title": "Clear canvas"
|
||||
},
|
||||
"publishDialog": {
|
||||
"title": "Publish library",
|
||||
"itemName": "Item name",
|
||||
"authorName": "Author name",
|
||||
"githubUsername": "GitHub username",
|
||||
"twitterUsername": "Twitter username",
|
||||
"libraryName": "Library name",
|
||||
"libraryDesc": "Library description",
|
||||
"website": "Website",
|
||||
"placeholder": {
|
||||
"authorName": "Your name or username",
|
||||
"libraryName": "Name of your library",
|
||||
"libraryDesc": "Description of your library to help people understand its usage",
|
||||
"githubHandle": "GitHub handle (optional), so you can edit the library once submitted for review",
|
||||
"twitterHandle": "Twitter username (optional), so we know who to credit when promoting over Twitter",
|
||||
"website": "Link to your personal website or elsewhere (optional)"
|
||||
},
|
||||
"errors": {
|
||||
"required": "Required",
|
||||
"website": "Enter a valid URL"
|
||||
},
|
||||
"noteDescription": "Submit your library to be included in the <link>public library repository</link> for other people to use in their drawings.",
|
||||
"noteGuidelines": "The library needs to be manually approved first. Please read the <link>guidelines</link> before submitting. You will need a GitHub account to communicate and make changes if requested, but it is not strictly required.",
|
||||
"noteLicense": "By submitting, you agree the library will be published under the <link>MIT License</link>, which in short means anyone can use them without restrictions.",
|
||||
"noteItems": "Each library item must have its own name so it's filterable. The following library items will be included:",
|
||||
"atleastOneLibItem": "Please select at least one library item to get started",
|
||||
"republishWarning": "Note: some of the selected items are marked as already published/submitted. You should only resubmit items when updating an existing library or submission."
|
||||
},
|
||||
"publishSuccessDialog": {
|
||||
"title": "Library submitted",
|
||||
"content": "Thank you {{authorName}}. Your library has been submitted for review. You can track the status <link>here</link>"
|
||||
},
|
||||
"confirmDialog": {
|
||||
"resetLibrary": "Reset library",
|
||||
"removeItemsFromLib": "Remove selected items from library"
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "Export image",
|
||||
"label": {
|
||||
"withBackground": "Background",
|
||||
"onlySelected": "Only selected",
|
||||
"darkMode": "Dark mode",
|
||||
"embedScene": "Embed scene",
|
||||
"scale": "Scale",
|
||||
"padding": "Padding"
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": "Scene data will be saved into the exported PNG/SVG file so that the scene can be restored from it.\nWill increase exported file size."
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "Export to PNG",
|
||||
"exportToSvg": "Export to SVG",
|
||||
"copyPngToClipboard": "Copy PNG to clipboard"
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "PNG",
|
||||
"exportToSvg": "SVG",
|
||||
"copyPngToClipboard": "Copy to clipboard"
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Your drawings are end-to-end encrypted so Excalidraw's servers will never see them.",
|
||||
"link": "Blog post on end-to-end encryption in Excalidraw"
|
||||
},
|
||||
"stats": {
|
||||
"angle": "Angle",
|
||||
"shapes": "Shapes",
|
||||
"height": "Height",
|
||||
"scene": "Scene",
|
||||
"selected": "Selected",
|
||||
"storage": "Storage",
|
||||
"fullTitle": "Canvas & Shape properties",
|
||||
"title": "Properties",
|
||||
"generalStats": "General",
|
||||
"elementProperties": "Shape properties",
|
||||
"total": "Total",
|
||||
"version": "Version",
|
||||
"versionCopy": "Click to copy",
|
||||
"versionNotAvailable": "Version not available",
|
||||
"width": "Width"
|
||||
},
|
||||
"toast": {
|
||||
"addedToLibrary": "Added to library",
|
||||
"copyStyles": "Copied styles.",
|
||||
"copyToClipboard": "Copied to clipboard.",
|
||||
"copyToClipboardAsPng": "Copied {{exportSelection}} to clipboard as PNG\n({{exportColorScheme}})",
|
||||
"fileSaved": "File saved.",
|
||||
"fileSavedToFilename": "Saved to {filename}",
|
||||
"canvas": "canvas",
|
||||
"selection": "selection",
|
||||
"pasteAsSingleElement": "Use {{shortcut}} to paste as a single element,\nor paste into an existing text editor",
|
||||
"unableToEmbed": "Embedding this url is currently not allowed. Raise an issue on GitHub to request the url whitelisted",
|
||||
"unrecognizedLinkFormat": "The link you embedded does not match the expected format. Please try to paste the 'embed' string provided by the source site"
|
||||
},
|
||||
"colors": {
|
||||
"transparent": "Transparent",
|
||||
"black": "Black",
|
||||
"white": "White",
|
||||
"red": "Red",
|
||||
"pink": "Pink",
|
||||
"grape": "Grape",
|
||||
"violet": "Violet",
|
||||
"gray": "Gray",
|
||||
"blue": "Blue",
|
||||
"cyan": "Cyan",
|
||||
"teal": "Teal",
|
||||
"green": "Green",
|
||||
"yellow": "Yellow",
|
||||
"orange": "Orange",
|
||||
"bronze": "Bronze"
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"app": {
|
||||
"center_heading": "All your data is saved locally in your browser.",
|
||||
"center_heading_plus": "Did you want to go to the Excalidraw+ instead?",
|
||||
"menuHint": "Export, preferences, languages, ..."
|
||||
},
|
||||
"defaults": {
|
||||
"menuHint": "Export, preferences, and more...",
|
||||
"center_heading": "Diagrams. Made. Simple.",
|
||||
"toolbarHint": "Pick a tool & Start drawing!",
|
||||
"helpHint": "Shortcuts & help"
|
||||
}
|
||||
},
|
||||
"colorPicker": {
|
||||
"mostUsedCustomColors": "Most used custom colors",
|
||||
"colors": "Colors",
|
||||
"shades": "Shades",
|
||||
"hexCode": "Hex code",
|
||||
"noShades": "No shades available for this color"
|
||||
},
|
||||
"overwriteConfirm": {
|
||||
"action": {
|
||||
"exportToImage": {
|
||||
"title": "Export as image",
|
||||
"button": "Export as image",
|
||||
"description": "Export the scene data as an image from which you can import later."
|
||||
},
|
||||
"saveToDisk": {
|
||||
"title": "Save to disk",
|
||||
"button": "Save to disk",
|
||||
"description": "Export the scene data to a file from which you can import later."
|
||||
},
|
||||
"excalidrawPlus": {
|
||||
"title": "Excalidraw+",
|
||||
"button": "Export to Excalidraw+",
|
||||
"description": "Save the scene to your Excalidraw+ workspace."
|
||||
}
|
||||
},
|
||||
"modal": {
|
||||
"loadFromFile": {
|
||||
"title": "Load from file",
|
||||
"button": "Load from file",
|
||||
"description": "Loading from a file will <bold>replace your existing content</bold>.<br></br>You can back up your drawing first using one of the options below."
|
||||
},
|
||||
"shareableLink": {
|
||||
"title": "Load from link",
|
||||
"button": "Replace my content",
|
||||
"description": "Loading external drawing will <bold>replace your existing content</bold>.<br></br>You can back up your drawing first by using one of the options below."
|
||||
}
|
||||
}
|
||||
},
|
||||
"mermaid": {
|
||||
"title": "Mermaid to Excalidraw",
|
||||
"button": "Insert",
|
||||
"description": "Currently only <flowchartLink>Flowchart</flowchartLink>,<sequenceLink> Sequence, </sequenceLink> and <classLink>Class </classLink>Diagrams are supported. The other types will be rendered as image in Excalidraw.",
|
||||
"syntax": "Mermaid Syntax",
|
||||
"preview": "Preview"
|
||||
},
|
||||
"quickSearch": {
|
||||
"placeholder": "Quick search"
|
||||
},
|
||||
"fontList": {
|
||||
"badge": {
|
||||
"old": "old"
|
||||
},
|
||||
"sceneFonts": "In this scene",
|
||||
"availableFonts": "Available fonts",
|
||||
"empty": "No fonts found"
|
||||
},
|
||||
"userList": {
|
||||
"empty": "No users found",
|
||||
"hint": {
|
||||
"text": "Click on user to follow",
|
||||
"followStatus": "You're currently following this user",
|
||||
"inCall": "User is in a voice call",
|
||||
"micMuted": "User's microphone is muted",
|
||||
"isSpeaking": "User is speaking"
|
||||
}
|
||||
},
|
||||
"commandPalette": {
|
||||
"title": "Command palette",
|
||||
"shortcuts": {
|
||||
"select": "Select",
|
||||
"confirm": "Confirm",
|
||||
"close": "Close"
|
||||
},
|
||||
"recents": "Recently used",
|
||||
"search": {
|
||||
"placeholder": "Search menus, commands, and discover hidden gems",
|
||||
"noMatch": "No matching commands..."
|
||||
},
|
||||
"itemNotAvailable": "Command is not available...",
|
||||
"shortcutHint": "For Command palette, use {{shortcut}}"
|
||||
}
|
||||
}
|
608
frontend/typescript/excalidraw_canvas/package-lock.json
generated
Normal file
|
@ -0,0 +1,608 @@
|
|||
{
|
||||
"name": "excalidraw_canvas",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@excalidraw/excalidraw": "0.17.6",
|
||||
"@excalidraw/laser-pointer": "1.3.1",
|
||||
"@types/react": "18.3.11",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"browser-fs-access": "0.35.0",
|
||||
"jotai": "2.10.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"roughjs": "4.6.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.24.0",
|
||||
"typescript": "^5.6.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz",
|
||||
"integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz",
|
||||
"integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz",
|
||||
"integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz",
|
||||
"integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz",
|
||||
"integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz",
|
||||
"integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz",
|
||||
"integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz",
|
||||
"integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz",
|
||||
"integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz",
|
||||
"integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz",
|
||||
"integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz",
|
||||
"integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz",
|
||||
"integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz",
|
||||
"integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz",
|
||||
"integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz",
|
||||
"integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz",
|
||||
"integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz",
|
||||
"integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz",
|
||||
"integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz",
|
||||
"integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz",
|
||||
"integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz",
|
||||
"integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz",
|
||||
"integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz",
|
||||
"integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@excalidraw/excalidraw": {
|
||||
"version": "0.17.6",
|
||||
"resolved": "https://registry.npmjs.org/@excalidraw/excalidraw/-/excalidraw-0.17.6.tgz",
|
||||
"integrity": "sha512-fyCl+zG/Z5yhHDh5Fq2ZGmphcrALmuOdtITm8gN4d8w4ntnaopTXcTfnAAaU3VleDC6LhTkoLOTG6P5kgREiIg==",
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.2 || ^18.2.0",
|
||||
"react-dom": "^17.0.2 || ^18.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@excalidraw/laser-pointer": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@excalidraw/laser-pointer/-/laser-pointer-1.3.1.tgz",
|
||||
"integrity": "sha512-psA1z1N2qeAfsORdXc9JmD2y4CmDwmuMRxnNdJHZexIcPwaNEyIpNcelw+QkL9rz9tosaN9krXuKaRqYpRAR6g=="
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
|
||||
"integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA=="
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.3.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.11.tgz",
|
||||
"integrity": "sha512-r6QZ069rFTjrEYgFdOck1gK7FLVsgJE7tTz0pQBczlBNUhBNk0MQH4UbnFSwjpQLMkLzgqvBBa+qGpLje16eTQ==",
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "18.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz",
|
||||
"integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==",
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/browser-fs-access": {
|
||||
"version": "0.35.0",
|
||||
"resolved": "https://registry.npmjs.org/browser-fs-access/-/browser-fs-access-0.35.0.tgz",
|
||||
"integrity": "sha512-sLoadumpRfsjprP8XzVjpQc0jK8yqHBx0PtUTGYj2fftT+P/t+uyDAQdMgGAPKD011in/O+YYGh7fIs0oG/viw=="
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz",
|
||||
"integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.24.0",
|
||||
"@esbuild/android-arm": "0.24.0",
|
||||
"@esbuild/android-arm64": "0.24.0",
|
||||
"@esbuild/android-x64": "0.24.0",
|
||||
"@esbuild/darwin-arm64": "0.24.0",
|
||||
"@esbuild/darwin-x64": "0.24.0",
|
||||
"@esbuild/freebsd-arm64": "0.24.0",
|
||||
"@esbuild/freebsd-x64": "0.24.0",
|
||||
"@esbuild/linux-arm": "0.24.0",
|
||||
"@esbuild/linux-arm64": "0.24.0",
|
||||
"@esbuild/linux-ia32": "0.24.0",
|
||||
"@esbuild/linux-loong64": "0.24.0",
|
||||
"@esbuild/linux-mips64el": "0.24.0",
|
||||
"@esbuild/linux-ppc64": "0.24.0",
|
||||
"@esbuild/linux-riscv64": "0.24.0",
|
||||
"@esbuild/linux-s390x": "0.24.0",
|
||||
"@esbuild/linux-x64": "0.24.0",
|
||||
"@esbuild/netbsd-x64": "0.24.0",
|
||||
"@esbuild/openbsd-arm64": "0.24.0",
|
||||
"@esbuild/openbsd-x64": "0.24.0",
|
||||
"@esbuild/sunos-x64": "0.24.0",
|
||||
"@esbuild/win32-arm64": "0.24.0",
|
||||
"@esbuild/win32-ia32": "0.24.0",
|
||||
"@esbuild/win32-x64": "0.24.0"
|
||||
}
|
||||
},
|
||||
"node_modules/hachure-fill": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz",
|
||||
"integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="
|
||||
},
|
||||
"node_modules/jotai": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/jotai/-/jotai-2.10.0.tgz",
|
||||
"integrity": "sha512-8W4u0aRlOIwGlLQ0sqfl/c6+eExl5D8lZgAUolirZLktyaj4WnxO/8a0HEPmtriQAB6X5LMhXzZVmw02X0P0qQ==",
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=17.0.0",
|
||||
"react": ">=17.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"loose-envify": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/path-data-parser": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz",
|
||||
"integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="
|
||||
},
|
||||
"node_modules/points-on-curve": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz",
|
||||
"integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="
|
||||
},
|
||||
"node_modules/points-on-path": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz",
|
||||
"integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==",
|
||||
"dependencies": {
|
||||
"path-data-parser": "0.1.0",
|
||||
"points-on-curve": "0.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/roughjs": {
|
||||
"version": "4.6.6",
|
||||
"resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz",
|
||||
"integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==",
|
||||
"dependencies": {
|
||||
"hachure-fill": "^0.5.2",
|
||||
"path-data-parser": "^0.1.0",
|
||||
"points-on-curve": "^0.2.0",
|
||||
"points-on-path": "^0.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.23.2",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
||||
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.6.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
|
||||
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
17
frontend/typescript/excalidraw_canvas/package.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"@excalidraw/excalidraw": "0.17.6",
|
||||
"@excalidraw/laser-pointer": "1.3.1",
|
||||
"@types/react": "18.3.11",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"browser-fs-access": "0.35.0",
|
||||
"jotai": "2.10.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"roughjs": "4.6.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.24.0",
|
||||
"typescript": "^5.6.3"
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@ Init
|
|||
- `npm install`
|
||||
|
||||
Watch & build (without typechecking)
|
||||
- `node_modules/.bin/esbuild pixi_canvas.ts --bundle --outfile=../bundles/pixi_canvas.js --format=esm --watch`
|
||||
- `node_modules/.bin/esbuild pixi_canvas.ts --bundle --minify --outfile=../bundles/pixi_canvas.js --format=esm --watch`
|
||||
|
||||
Watch & typecheck (without building)
|
||||
- `node_modules/.bin/tsc pixi_canvas.ts --watch -noEmit --preserveWatchOutput --target esnext --module esnext --moduleResolution bundler`
|
||||
|
|
|
@ -1,35 +1,97 @@
|
|||
import { Application, Text, Graphics, Container, TextStyle } from "pixi.js";
|
||||
import { Application, Text, Graphics, Container, TextStyle, Sprite, Texture } from "pixi.js";
|
||||
|
||||
type Time = number;
|
||||
type BitString = string;
|
||||
type Timeline = Array<[Time, BitString]>;
|
||||
// const color_dark_slate_blue = 'DarkSlateBlue'
|
||||
const color_dark_slate_blue = '#24478e' // oklch(41.43% 0.125 262.26)'
|
||||
|
||||
type X = number;
|
||||
type TimelineForUI = Array<[X, string]>;
|
||||
// const color_white = 'White'
|
||||
const color_white = '#ffffff' // oklch(100% 3.5594404384177905e-8 105.88)
|
||||
|
||||
// const color_slate_blue = 'SlateBlue'
|
||||
const color_slate_blue = '#3d7af3' // oklch(60.45% 0.194 262.26)
|
||||
|
||||
// const color_dark_violet_with_x = '0x550099' // oklch(37.6% 0.201 299.56)
|
||||
const color_dark_violet_with_x = '0x002ca9' // oklch(37.6% 0.201 263.53)
|
||||
|
||||
// @TODO sync with Rust and `tauri_glue.ts`
|
||||
type Timeline = {
|
||||
blocks: Array<TimelineBlock>
|
||||
}
|
||||
type TimelineBlock = {
|
||||
x: number,
|
||||
width: number,
|
||||
height: number,
|
||||
label: TimeLineBlockLabel | undefined,
|
||||
}
|
||||
type TimeLineBlockLabel = {
|
||||
text: string,
|
||||
x: number,
|
||||
y: number,
|
||||
}
|
||||
|
||||
// @TODO sync with Rust
|
||||
enum VarFormat {
|
||||
ASCII,
|
||||
Binary,
|
||||
BinaryWithGroups,
|
||||
Hexadecimal,
|
||||
Octal,
|
||||
Signed,
|
||||
Unsigned,
|
||||
}
|
||||
|
||||
type TimelineGetter = (
|
||||
signal_ref_index: number,
|
||||
timeline_zoom: number,
|
||||
timeline_viewport_width: number,
|
||||
timeline_viewport_x: number,
|
||||
row_height: number,
|
||||
var_format: VarFormat
|
||||
) => Promise<Timeline>;
|
||||
|
||||
export class PixiController {
|
||||
app: Application
|
||||
// -- FastWave-specific --
|
||||
var_signal_rows: Array<VarSignalRow> = [];
|
||||
var_signal_rows_container = new Container();
|
||||
// @TODO reset `timeline_*` on file unload?
|
||||
timeline_zoom: number;
|
||||
timeline_viewport_width: number;
|
||||
timeline_viewport_x: number;
|
||||
row_height: number;
|
||||
row_gap: number;
|
||||
timeline_getter: TimelineGetter;
|
||||
|
||||
constructor(row_height: number, row_gap: number) {
|
||||
constructor(
|
||||
timeline_zoom: number,
|
||||
timeline_viewport_width: number,
|
||||
timeline_viewport_x: number,
|
||||
row_height: number,
|
||||
row_gap: number,
|
||||
timeline_getter: TimelineGetter
|
||||
) {
|
||||
this.app = new Application();
|
||||
// -- FastWave-specific --
|
||||
this.timeline_zoom = timeline_zoom;
|
||||
this.timeline_viewport_width = timeline_viewport_width;
|
||||
this.timeline_viewport_x = timeline_viewport_x;
|
||||
this.row_height = row_height;
|
||||
this.row_gap = row_gap;
|
||||
this.app.stage.addChild(this.var_signal_rows_container);
|
||||
this.timeline_getter = timeline_getter;
|
||||
}
|
||||
|
||||
async init(parent_element: HTMLElement) {
|
||||
await this.app.init({ background: 'DarkSlateBlue', antialias: true, resizeTo: parent_element });
|
||||
await this.app.init({ background: color_dark_slate_blue, antialias: true, resizeTo: parent_element });
|
||||
parent_element.appendChild(this.app.canvas);
|
||||
}
|
||||
|
||||
// Default automatic Pixi resizing is not reliable
|
||||
queue_resize() {
|
||||
// Default automatic Pixi resizing according to the parent is not reliable
|
||||
// and the `app.renderer`'s `resize` event is fired on every browser window size change
|
||||
async resize(width: number, _height: number) {
|
||||
// -- FastWave-specific --
|
||||
this.timeline_viewport_width = width;
|
||||
await this.redraw_all_rows();
|
||||
// -- // --
|
||||
this.app.queueResize();
|
||||
}
|
||||
|
||||
|
@ -46,16 +108,93 @@ export class PixiController {
|
|||
this.app.destroy(rendererDestroyOptions, options);
|
||||
}
|
||||
|
||||
get_timeline_zoom() {
|
||||
return this.timeline_zoom;
|
||||
}
|
||||
|
||||
get_timeline_viewport_width() {
|
||||
return this.timeline_viewport_width;
|
||||
}
|
||||
|
||||
get_timeline_viewport_x() {
|
||||
return this.timeline_viewport_x;
|
||||
}
|
||||
|
||||
// -- FastWave-specific --
|
||||
|
||||
async redraw_all_rows() {
|
||||
await Promise.all(this.var_signal_rows.map(async row => {
|
||||
const timeline = await this.timeline_getter(
|
||||
row.signal_ref_index,
|
||||
this.timeline_zoom,
|
||||
this.timeline_viewport_width,
|
||||
this.timeline_viewport_x,
|
||||
this.row_height,
|
||||
row.var_format
|
||||
);
|
||||
row.redraw(timeline);
|
||||
}))
|
||||
}
|
||||
|
||||
async redraw_row(index: number) {
|
||||
const row = this.var_signal_rows[index];
|
||||
if (typeof row !== 'undefined') {
|
||||
const timeline = await this.timeline_getter(
|
||||
row.signal_ref_index,
|
||||
this.timeline_zoom,
|
||||
this.timeline_viewport_width,
|
||||
this.timeline_viewport_x,
|
||||
this.row_height,
|
||||
row.var_format
|
||||
);
|
||||
row.redraw(timeline);
|
||||
}
|
||||
}
|
||||
|
||||
async set_var_format(index: number, var_format: VarFormat) {
|
||||
const row = this.var_signal_rows[index];
|
||||
if (typeof row !== 'undefined') {
|
||||
row.set_var_format(var_format);
|
||||
this.redraw_row(index);
|
||||
}
|
||||
}
|
||||
|
||||
async zoom_or_pan(wheel_delta_y: number, shift_key: boolean, offset_x: number) {
|
||||
if (shift_key) {
|
||||
this.timeline_viewport_x += Math.sign(wheel_delta_y) * 20;
|
||||
} else {
|
||||
const offset_x_ratio = offset_x / this.timeline_viewport_width;
|
||||
const old_timeline_width = this.timeline_viewport_width * this.timeline_zoom;
|
||||
const new_zoom = this.timeline_zoom - Math.sign(wheel_delta_y) * this.timeline_zoom * 0.5;
|
||||
const new_timeline_width = this.timeline_viewport_width * new_zoom;
|
||||
if (new_timeline_width < this.timeline_viewport_width) {
|
||||
this.timeline_zoom = 1;
|
||||
this.timeline_viewport_x = 0;
|
||||
} else {
|
||||
const timeline_width_difference = new_timeline_width - old_timeline_width;
|
||||
this.timeline_viewport_x += timeline_width_difference * offset_x_ratio;
|
||||
this.timeline_zoom = new_zoom;
|
||||
}
|
||||
}
|
||||
const timeline_width = this.timeline_viewport_width * this.timeline_zoom;
|
||||
if (this.timeline_viewport_x < 0) {
|
||||
this.timeline_viewport_x = 0;
|
||||
} else if (this.timeline_viewport_x + this.timeline_viewport_width > timeline_width) {
|
||||
this.timeline_viewport_x = timeline_width - this.timeline_viewport_width;
|
||||
}
|
||||
this.redraw_all_rows();
|
||||
}
|
||||
|
||||
remove_var(index: number) {
|
||||
if (typeof this.var_signal_rows[index] !== 'undefined') {
|
||||
this.var_signal_rows[index].destroy();
|
||||
}
|
||||
}
|
||||
|
||||
push_var(timeline: Timeline) {
|
||||
push_var(signal_ref_index: number, timeline: Timeline, var_format: VarFormat) {
|
||||
new VarSignalRow(
|
||||
signal_ref_index,
|
||||
var_format,
|
||||
timeline,
|
||||
this.app,
|
||||
this.var_signal_rows,
|
||||
|
@ -72,26 +211,36 @@ export class PixiController {
|
|||
clear_vars() {
|
||||
this.var_signal_rows.slice().reverse().forEach(row => row.destroy());
|
||||
}
|
||||
|
||||
request_timeline_redraw() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
class VarSignalRow {
|
||||
app: Application;
|
||||
signal_ref_index: number;
|
||||
var_format: VarFormat;
|
||||
timeline: Timeline;
|
||||
last_time: Time;
|
||||
formatter: (signal_value: BitString) => string;
|
||||
timeline_for_ui: TimelineForUI;
|
||||
app: Application;
|
||||
owner: Array<VarSignalRow>;
|
||||
index_in_owner: number;
|
||||
rows_container: Container;
|
||||
row_height: number;
|
||||
row_gap: number;
|
||||
row_height_with_gap: number;
|
||||
renderer_resize_callback = () => this.redraw_on_canvas_resize();
|
||||
// -- elements --
|
||||
row_container = new Container();
|
||||
row_container_background: Sprite;
|
||||
signal_blocks_container = new Container();
|
||||
label_style = new TextStyle({
|
||||
align: "center",
|
||||
fill: color_white,
|
||||
fontSize: 16,
|
||||
fontFamily: '"Courier New", monospace',
|
||||
});
|
||||
|
||||
constructor(
|
||||
signal_ref_index: number,
|
||||
var_format: VarFormat,
|
||||
timeline: Timeline,
|
||||
app: Application,
|
||||
owner: Array<VarSignalRow>,
|
||||
|
@ -99,16 +248,10 @@ class VarSignalRow {
|
|||
row_height: number,
|
||||
row_gap: number,
|
||||
) {
|
||||
this.app = app;
|
||||
|
||||
this.signal_ref_index = signal_ref_index;
|
||||
this.var_format = var_format;
|
||||
this.timeline = timeline;
|
||||
this.last_time = timeline[timeline.length - 1][0];
|
||||
this.formatter = signal_value => parseInt(signal_value, 2).toString(16);
|
||||
|
||||
this.timeline_for_ui = this.timeline.map(([time, value]) => {
|
||||
const x = time / this.last_time * this.app.screen.width;
|
||||
return [x, this.formatter(value)]
|
||||
});
|
||||
this.app = app;
|
||||
|
||||
this.row_height = row_height;
|
||||
this.row_gap = row_gap;
|
||||
|
@ -120,81 +263,68 @@ class VarSignalRow {
|
|||
|
||||
this.rows_container = rows_container;
|
||||
|
||||
this.draw();
|
||||
this.app.renderer.on("resize", this.renderer_resize_callback);
|
||||
}
|
||||
|
||||
draw() {
|
||||
// row_container
|
||||
this.row_container.y = this.index_in_owner * this.row_height_with_gap;
|
||||
this.rows_container.addChild(this.row_container);
|
||||
|
||||
// signal_block_container
|
||||
// row background
|
||||
this.row_container_background = new Sprite();
|
||||
this.row_container_background.texture = Texture.WHITE;
|
||||
this.row_container_background.tint = color_dark_violet_with_x;
|
||||
this.row_container_background.height = this.row_height;
|
||||
this.row_container.addChild(this.row_container_background);
|
||||
|
||||
// signal_blocks_container
|
||||
this.row_container.addChild(this.signal_blocks_container);
|
||||
|
||||
const label_style = new TextStyle({
|
||||
align: "center",
|
||||
fill: "White",
|
||||
fontSize: 16,
|
||||
fontFamily: 'system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"',
|
||||
});
|
||||
this.draw();
|
||||
}
|
||||
|
||||
this.timeline_for_ui.forEach(([x, value], index) => {
|
||||
if (index == this.timeline_for_ui.length - 1) {
|
||||
return;
|
||||
}
|
||||
const block_width = this.timeline_for_ui[index+1][0] - x;
|
||||
const block_height = this.row_height;
|
||||
set_var_format(var_format: VarFormat) {
|
||||
this.var_format = var_format;
|
||||
}
|
||||
|
||||
redraw(timeline: Timeline) {
|
||||
this.timeline = timeline;
|
||||
this.draw();
|
||||
}
|
||||
|
||||
draw() {
|
||||
// Screen can be null when we are, for instance, switching between miller columns and tree layout
|
||||
// and then the canvas has to be recreated.
|
||||
if (this?.app?.screen?.width === undefined) {
|
||||
return;
|
||||
}
|
||||
// Workaround for "TypeError: Cannot read properties of null (reading 'orig')"
|
||||
if (this?.row_container_background?._texture?.orig?.width !== undefined) {
|
||||
this.row_container_background.width = this.app.screen.width;
|
||||
}
|
||||
|
||||
// @TODO optimize by reusing a pool of blocks instead or removing all children on every redraw?
|
||||
this.signal_blocks_container.removeChildren();
|
||||
this.timeline.blocks.forEach(timeline_block => {
|
||||
// signal_block
|
||||
const signal_block = new Container();
|
||||
signal_block.x = x;
|
||||
signal_block.x = timeline_block.x;
|
||||
this.signal_blocks_container.addChild(signal_block);
|
||||
|
||||
// background
|
||||
const gap_between_blocks = 2;
|
||||
const background = new Graphics()
|
||||
.roundRect(0, 0, block_width, block_height, 15)
|
||||
.fill("SlateBlue");
|
||||
background.label = "background";
|
||||
.rect(gap_between_blocks / 2, 0, timeline_block.width - gap_between_blocks, timeline_block.height)
|
||||
.fill(color_slate_blue);
|
||||
signal_block.addChild(background);
|
||||
|
||||
// label
|
||||
const label = new Text({ text: value, style: label_style });
|
||||
label.x = (block_width - label.width) / 2;
|
||||
label.y = (block_height - label.height) / 2;
|
||||
label.visible = label.width < block_width;
|
||||
label.label = "label";
|
||||
signal_block.addChild(label);
|
||||
})
|
||||
}
|
||||
|
||||
redraw_on_canvas_resize() {
|
||||
for (let index = 0; index < this.timeline_for_ui.length; index++) {
|
||||
const x = this.timeline[index][0] / this.last_time * this.app.screen.width;
|
||||
this.timeline_for_ui[index][0] = x;
|
||||
}
|
||||
this.timeline_for_ui.forEach(([x, _value], index) => {
|
||||
if (index == this.timeline_for_ui.length - 1) {
|
||||
return;
|
||||
if (timeline_block.label !== undefined) {
|
||||
const label = new Text();
|
||||
label.text = timeline_block.label.text;
|
||||
label.style = this.label_style;
|
||||
label.x = timeline_block.label.x;
|
||||
label.y = timeline_block.label.y;
|
||||
signal_block.addChild(label);
|
||||
}
|
||||
|
||||
const block_width = this.timeline_for_ui[index+1][0] - x;
|
||||
const block_height = this.row_height;
|
||||
|
||||
// signal_block
|
||||
const signal_block = this.signal_blocks_container.getChildAt(index);
|
||||
signal_block.x = x;
|
||||
|
||||
// background
|
||||
const background = signal_block.getChildByLabel("background")!;
|
||||
background.width = block_width;
|
||||
|
||||
// label
|
||||
const label = signal_block.getChildByLabel("label")!;
|
||||
label.x = (block_width - label.width) / 2;
|
||||
label.y = (block_height - label.height) / 2;
|
||||
label.visible = label.width < block_width;
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
decrement_index() {
|
||||
|
@ -203,7 +333,6 @@ class VarSignalRow {
|
|||
}
|
||||
|
||||
destroy() {
|
||||
this.app.renderer.off("resize", this.renderer_resize_callback);
|
||||
this.owner.splice(this.index_in_owner, 1);
|
||||
this.rows_container.removeChildAt(this.index_in_owner);
|
||||
this.row_container.destroy(true);
|
||||
|
|
|
@ -2,7 +2,7 @@ Init
|
|||
- `npm install`
|
||||
|
||||
Watch & build (without typechecking)
|
||||
-`- `node_modules/.bin/esbuild pixi_canvas.ts --bundle --outfile=../bundles/tauri_glue.js --format=esm --watch``
|
||||
- `node_modules/.bin/esbuild tauri_glue.ts --bundle --minify --outfile=../bundles/tauri_glue.js --format=esm --watch`
|
||||
|
||||
Watch & typecheck (without building)
|
||||
- `node_modules/.bin/tsc tauri_glue.ts --watch -noEmit --preserveWatchOutput --target esnext --module esnext --moduleResolution bundler`
|
||||
|
|
|
@ -1,13 +1,25 @@
|
|||
// @TODO use TS and Tauri bindgens to make this code properly typed
|
||||
|
||||
import { core } from '@tauri-apps/api'
|
||||
import { core, event } from '@tauri-apps/api'
|
||||
|
||||
const invoke = core.invoke;
|
||||
const listen = event.listen;
|
||||
|
||||
type Filename = string;
|
||||
type JavascriptCode = string;
|
||||
type WellenHierarchy = unknown;
|
||||
type WellenTimeTable = unknown;
|
||||
type WellenSignal = unknown;
|
||||
type Timeline = unknown;
|
||||
type VarFormat = unknown;
|
||||
|
||||
type AddedDecodersCount = number;
|
||||
type RemovedDecodersCount = number;
|
||||
type DecoderPath = string;
|
||||
|
||||
type AddedDiagramConnectorsCount = number;
|
||||
type RemovedDiagramConnectorsCount = number;
|
||||
type DiagramConnectorPath = string;
|
||||
type DiagramConnectorName = string;
|
||||
type ComponentId = string;
|
||||
|
||||
export async function show_window(): Promise<void> {
|
||||
return await invoke("show_window");
|
||||
|
@ -17,18 +29,68 @@ export async function pick_and_load_waveform(): Promise<Filename | undefined> {
|
|||
return await invoke("pick_and_load_waveform");
|
||||
}
|
||||
|
||||
export async function load_file_with_selected_vars(): Promise<JavascriptCode | undefined> {
|
||||
return await invoke("load_file_with_selected_vars");
|
||||
}
|
||||
|
||||
export async function get_hierarchy(): Promise<WellenHierarchy> {
|
||||
return await invoke("get_hierarchy");
|
||||
}
|
||||
|
||||
export async function get_time_table(): Promise<WellenTimeTable> {
|
||||
return await invoke("get_time_table");
|
||||
}
|
||||
|
||||
export async function load_and_get_signal(signal_ref_index: number): Promise<WellenSignal> {
|
||||
return await invoke("load_and_get_signal", { signal_ref_index });
|
||||
export async function load_signal_and_get_timeline(
|
||||
signal_ref_index: number,
|
||||
timeline_zoom: number,
|
||||
timeline_viewport_width: number,
|
||||
timeline_viewport_x: number,
|
||||
block_height: number,
|
||||
var_format: VarFormat,
|
||||
): Promise<Timeline> {
|
||||
return await invoke("load_signal_and_get_timeline", {
|
||||
signal_ref_index,
|
||||
timeline_zoom,
|
||||
timeline_viewport_width,
|
||||
timeline_viewport_x,
|
||||
block_height,
|
||||
var_format
|
||||
});
|
||||
}
|
||||
|
||||
export async function unload_signal(signal_ref_index: number): Promise<void> {
|
||||
return await invoke("unload_signal", { signal_ref_index });
|
||||
}
|
||||
|
||||
export async function send_char(c : string): Promise<void> {
|
||||
return await invoke("send_char", { c });
|
||||
}
|
||||
|
||||
export async function add_decoders(decoder_paths: Array<DecoderPath>): Promise<AddedDecodersCount> {
|
||||
return await invoke("add_decoders", { decoder_paths });
|
||||
}
|
||||
|
||||
export async function remove_all_decoders(): Promise<RemovedDecodersCount> {
|
||||
return await invoke("remove_all_decoders");
|
||||
}
|
||||
|
||||
export async function add_diagram_connectors(diagram_connector_paths: Array<DiagramConnectorPath>): Promise<AddedDiagramConnectorsCount> {
|
||||
return await invoke("add_diagram_connectors", { diagram_connector_paths });
|
||||
}
|
||||
|
||||
export async function remove_all_diagram_connectors(): Promise<RemovedDiagramConnectorsCount> {
|
||||
return await invoke("remove_all_diagram_connectors");
|
||||
}
|
||||
|
||||
export async function listen_diagram_connectors_messages(on_message: (message: any) => void) {
|
||||
return await listen("diagram_connector_message", (message) => on_message(message.payload));
|
||||
}
|
||||
|
||||
export async function listen_term_update(on_message: (message: any) => void) {
|
||||
return await listen("term_content", (message) => on_message(message.payload));
|
||||
}
|
||||
|
||||
export async function notify_diagram_connector_text_change(diagram_connector: DiagramConnectorName, component_id: ComponentId, text: string): Promise<void> {
|
||||
return await invoke("notify_diagram_connector_text_change", { diagram_connector, component_id, text });
|
||||
}
|
||||
|
||||
export async function open_konata_file() {
|
||||
return await invoke("open_konata_file");
|
||||
}
|
||||
|
|