184 lines
8.5 KiB
Markdown
184 lines
8.5 KiB
Markdown
# JSON Canvas Editor
|
|
|
|
A native desktop editor for the [JSON Canvas](https://jsoncanvas.org) 1.0 file
|
|
format, written in Rust. It runs on Windows and Linux from a single binary and
|
|
reads and writes plain `.canvas` files on the local disk — there is no server,
|
|
no account and no hidden state.
|
|
|
|

|
|
|
|
## What it does
|
|
|
|
**The whole spec.** All four node types (`text`, `file`, `link`, `group`) and
|
|
every edge attribute (`fromSide`/`toSide`, `fromEnd`/`toEnd`, `label`, `color`),
|
|
plus both `canvasColor` forms — the six presets and `#rrggbb` hex.
|
|
The format as this editor reads and writes it is written down in
|
|
[`spec/spec.md`](spec/spec.md), the handful of additions to JSON Canvas 1.0 are
|
|
listed on their own in [`spec/extensions.md`](spec/extensions.md), and the
|
|
original 1.0 spec sits next to them in [`spec/1.0.md`](spec/1.0.md) to diff
|
|
against.
|
|
|
|
**It does not damage files it does not fully understand.** Unknown top-level
|
|
keys, unknown node/edge attributes and even unknown node `type`s are carried
|
|
through a load/save cycle untouched, and attributes are written back in the
|
|
order the spec lists them, so a save produces a small diff rather than a
|
|
reshuffle.
|
|
|
|
**Direct manipulation.**
|
|
|
|
| Gesture | Result |
|
|
| --- | --- |
|
|
| Double-click empty canvas | New text note, ready to type in |
|
|
| Double-click a note | Edit its Markdown in an editor window |
|
|
| Double-click a file/link node | Edit a plain-text file, or open anything else with the system default application |
|
|
| Right-click empty canvas | Menu of every node type, added where you clicked |
|
|
| Right-click a node | Edit, fit the card to its text, colour, bring to front / send to back, delete |
|
|
| Drag a dot on a node's edge | Draw a connection; drop it on empty canvas to create the node too |
|
|
| Drag a corner or side handle | Resize |
|
|
| Drag a group | Moves the group and everything inside it |
|
|
| Drag on empty canvas | Rubber-band selection (hold <kbd>Shift</kbd> to add) |
|
|
| Middle- or right-drag | Pan |
|
|
| Drop files on the window | A file node each; a `.canvas` file opens instead |
|
|
| Wheel | Zoom around the pointer, anywhere on the canvas |
|
|
|
|
**Markdown in text nodes**: headings, bold/italic/strikethrough, inline and
|
|
fenced code, quotes, bullet, numbered and task lists, rules, images and links.
|
|
Links are clickable; a link to another `.canvas` file opens it in the editor.
|
|
|
|
**File node previews**: images are rendered inline, text files are previewed as
|
|
Markdown, and a `subpath` such as `#Design` shows just that section of the file.
|
|
Group `background` images are drawn with the fitting their `backgroundStyle`
|
|
asks for (`cover`, `ratio` or `repeat`). Paths are stored relative to the
|
|
canvas, so a canvas plus its files stays portable.
|
|
|
|
**File nodes follow their files**: the files behind file nodes are watched
|
|
while the canvas is open, so a note written in another program, an image
|
|
exported again, or a file that only now appears shows up on the card by itself.
|
|
A file edited here is the one exception: it keeps the edit, and its card is
|
|
marked to say the file changed underneath it, so nothing is written over
|
|
without you saying so.
|
|
|
|
**A display text for link nodes**: name a link in the inspector and the canvas
|
|
shows the name instead of the address, with the full URL in its tooltip. It is
|
|
stored as a `label` attribute — the name groups and edges already use — so a
|
|
tool that does not know about it carries it through untouched. This is an
|
|
addition to the format; see [`spec/extensions.md`](spec/extensions.md).
|
|
|
|
**Dropping and pasting things in**: drop files on the window and each becomes a
|
|
file node, with its path stored relative to the canvas when it lives beside it.
|
|
Paste text and it becomes a note, or a link node when the text is a URL. A copy
|
|
made in the editor goes onto the system clipboard as JSON Canvas, so it pastes
|
|
back into another window as nodes rather than as text.
|
|
|
|
Dropping *text* on the window does nothing, and no drop lands under the pointer:
|
|
the windowing layer this editor is built on accepts file drags only, and discards
|
|
the position that comes with them. Paste is the way in for text.
|
|
|
|
**The usual editor comforts**: undo/redo of every change, copy/paste/duplicate,
|
|
grouping, alignment, z-order, a properties inspector, snap-to-grid, light and
|
|
dark themes, and a warning before you lose unsaved work.
|
|
|
|
## Building and running
|
|
|
|
```sh
|
|
cargo run --release # start with an empty canvas
|
|
cargo run --release -- board.canvas
|
|
```
|
|
|
|
The release binary is self-contained:
|
|
|
|
```sh
|
|
cargo build --release # target/release/jsoncanvas-desktop[.exe]
|
|
```
|
|
|
|
* **Windows** — nothing beyond a Rust toolchain (MSVC or GNU).
|
|
* **Linux** — the usual desktop development packages for windowing and
|
|
rendering, e.g. on Debian/Ubuntu:
|
|
|
|
```sh
|
|
sudo apt install build-essential pkg-config libx11-dev libxcursor-dev \
|
|
libxrandr-dev libxi-dev libgl1-mesa-dev libwayland-dev libxkbcommon-dev
|
|
```
|
|
|
|
File dialogs use the XDG desktop portal, so no GTK development packages are
|
|
needed; install `xdg-desktop-portal` and a backend for your desktop if it is
|
|
not already present.
|
|
|
|
## Tests
|
|
|
|
`cargo test` runs everything, headless and without a display. Each module's
|
|
tests are a child module of it in a file of its own — `src/view/tests.rs` for
|
|
`src/view.rs` — so they still reach private state without the module itself
|
|
carrying a thousand lines of test code. The GUI is tested the same way as the
|
|
rest:
|
|
|
|
* **Widgets** are driven through
|
|
[`egui_kittest`](https://crates.io/crates/egui_kittest), which builds the real
|
|
`App` on a real `egui::Context` and finds widgets by the label on them:
|
|
|
|
```rust
|
|
harness.get_by_label("Close & Fit").click();
|
|
harness.run();
|
|
assert_eq!(harness.state().view.editing(), None);
|
|
```
|
|
|
|
* **Canvas gestures** — clicks, drags and the wheel on cards, which are painted
|
|
rather than built from widgets — go through the `pass` helper in `view.rs`,
|
|
which runs one frame of `CanvasView::show` against synthetic input at chosen
|
|
coordinates.
|
|
|
|
* **How it looks** is covered by snapshot tests, which render a frame with
|
|
`wgpu` and compare it with the images in `tests/snapshots`. A failure writes
|
|
`<name>.diff.png` and `<name>.new.png` beside the expected image; after an
|
|
intentional change, re-record with `UPDATE_SNAPSHOTS=1 cargo test`. Rendering
|
|
falls back to Mesa's software Vulkan driver, so no GPU is needed.
|
|
|
|
Driving the built binary with synthetic input (`xdotool` and friends) is *not*
|
|
the way to test this: under a Wayland compositor the X pointer warp is scaled,
|
|
`--sync` can block until it is killed, and window captures come back stale, so
|
|
the results are slow and misleading.
|
|
|
|
## Keyboard shortcuts
|
|
|
|
| | |
|
|
| --- | --- |
|
|
| <kbd>Ctrl</kbd>+<kbd>N</kbd> / <kbd>O</kbd> / <kbd>S</kbd> | New, open, save |
|
|
| <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>S</kbd> | Save as |
|
|
| <kbd>Ctrl</kbd>+<kbd>Z</kbd> / <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>Z</kbd> | Undo, redo |
|
|
| <kbd>Ctrl</kbd>+<kbd>C</kbd> / <kbd>V</kbd> / <kbd>D</kbd> | Copy, paste, duplicate |
|
|
| <kbd>Ctrl</kbd>+<kbd>A</kbd> | Select everything |
|
|
| <kbd>Ctrl</kbd>+<kbd>G</kbd> | Put the selection in a group |
|
|
| <kbd>Ctrl</kbd>+<kbd>I</kbd> | Show or hide the inspector |
|
|
| <kbd>Delete</kbd> | Delete the selection |
|
|
| <kbd>F2</kbd> | Edit the selected node |
|
|
| <kbd>F</kbd> | Zoom to the selection |
|
|
| <kbd>Escape</kbd> | Stop editing, clear the selection |
|
|
| Arrow keys | Nudge by 1 (<kbd>Shift</kbd>: 10) |
|
|
| <kbd>Ctrl</kbd>+<kbd>0</kbd> / <kbd>1</kbd> | Zoom to fit, actual size |
|
|
|
|
## How it is put together
|
|
|
|
The UI toolkit is [egui](https://github.com/emilk/egui) via `eframe`. An
|
|
infinite canvas is essentially one large custom-painted widget, which is what
|
|
immediate-mode GUI is good at, and it builds to a single binary on both targets
|
|
without a GTK or Qt runtime. Pan and zoom are a transform on a dedicated egui
|
|
layer, so real widgets (file previews, links) keep working while the canvas is
|
|
scaled. Cards crop what they cannot show and editing happens in a window of its
|
|
own, so nothing on the canvas is hidden behind a scroll bar.
|
|
|
|
| Module | Responsibility |
|
|
| --- | --- |
|
|
| `model.rs` | The file format: parsing, writing, round-trip fidelity |
|
|
| `document.rs` | Open document: path, dirty state, undo history |
|
|
| `geometry.rs` | Node boxes, edge anchors, Bézier routing, hit testing |
|
|
| `view.rs` | The canvas: painting, selection, and every gesture |
|
|
| `nodes.rs` | Node bodies: Markdown, file previews, links |
|
|
| `markdown.rs` | The Markdown subset used by text nodes |
|
|
| `inspector.rs` | The properties panel |
|
|
| `theme.rs` | Palettes and the six preset colours |
|
|
| `app.rs` | Menus, toolbar, dialogs, file handling, shortcuts |
|
|
|
|
## Licence
|
|
|
|
MIT.
|