Architecture
notr (GPUI app) ──► notr-store (SQLite) ──► notr-core
└────────────────────────────────► notr-core
Notr is three crates. notr-core is the notebook model and all editing, with no UI or
storage dependencies. notr-store persists a workspace in SQLite. notr is the macOS
app built with GPUI. For how this maps onto NoteSpace, see
Porting notes.
Source map
| File | Responsibility |
|---|---|
notr-core/src/model.rs |
Workspace, Notebook, SectionGroup, NoteSection, NotePage, NoteBlock, TextFormat, TextMark, InkStroke, DeletedPage, PageVersion, Rect; .notespace serialization details (dates, base64) |
notr-core/src/validate.rs |
Every limit and consistency rule, for a page and for a workspace |
notr-core/src/session.rs |
Session: transactions, undo/redo, change reports, and every organizing operation |
notr-core/src/rich_text.rs |
Formatting runs over UTF-16 ranges: resolve, apply, minimal-diff replace, Replace All |
notr-core/src/search.rs |
Literal search with case folding and whole words, scopes, filters, snippets |
notr-core/src/table.rs |
Table geometry, row and column operations, TSV copy and paste |
notr-core/src/outline.rs |
The page outline: levels, subtrees, visible pages |
notr-core/src/groups.rs |
Section-group validation, paths, subtrees and the navigation projection |
notr-core/src/ink.rs |
Stroke bounds, eraser hit testing, stroke simplification |
notr-core/src/interchange.rs |
.notespace load and save, Markdown and HTML export, text import, safe links and file names |
notr-core/src/sample.rs |
The first-launch notebook, and block helpers used by templates |
notr-store/src/lib.rs |
Store: schema migrations, load, incremental save, conflict detection; see Storage |
notr/src/main.rs |
Startup, key bindings, menus, the window |
notr/src/actions.rs |
GPUI actions for shortcuts and menus |
notr/src/workbench.rs |
Workbench: the session, saving, navigation, layout of the panes |
notr/src/commands.rs |
Cmd and Workbench::run: every ribbon, menu and context-menu command |
notr/src/ribbon.rs |
The ribbon's tabs and groups, and rendering them |
notr/src/panes.rs |
Title bar, notebook pane, page list (with drag and drop), section tabs, notices, status bar, File view, context menus |
notr/src/surface.rs |
The page: painting, pointer tools and gestures, editing notes, titles and table cells, note commands |
notr/src/layout.rs |
Rich-text layout shared by the page, the editor and PNG export |
notr/src/editor.rs |
Editor: the text editor for notes, titles, cells and fields |
notr/src/search_pane.rs |
The search pane and the title bar's search field |
notr/src/dialogs.rs |
Prompts, lists, color, font, table editor and Replace dialogs |
notr/src/files.rs |
Import, export, pictures and attachments |
notr/src/png.rs |
PNG export through SVG and resvg |
notr/src/theme.rs |
Palettes, fonts, colors, Button, icons, tooltips |
notr/src/assets.rs |
Icons compiled into the binary |
The model
A Workspace holds notebooks, the recycle bin (trash) and view settings. A notebook
holds a flat list of sections and a flat list of section groups; a section's
group_id and a group's parent_id make the tree, so page enumeration never depends on
grouping. A section's pages are a flat list too, with a level of 0 to 2; the outline
(outline::build) reads it as a tree where each page's subtree is a contiguous range.
A page's blocks are its note containers, each with a kind (text, heading, to-do,
table, picture, attachment, divider), a position and size in page points, and content.
Text formatting is a base format plus marks: ranges in UTF-16 code units, the
unit the .notespace format uses. rich_text resolves marks into non-overlapping runs
(later marks win, as in NoteSpace) and always writes canonical runs back.
rich_text::byte_runs converts them to byte ranges for drawing and editing.
Attachment bytes are Arc<Vec<u8>>, so snapshots for undo share them instead of
copying.
The editing session
Session owns the workspace. Nothing else mutates it.
execute(label, structure, |workspace| …)runs a change on the whole workspace. It keeps a clone as the undo snapshot, validates the result, and restores the clone if the change fails or the result is invalid. A change that leaves the workspace equal to before records nothing.edit_page(page_id, label, structure, |page| …)runs a change on a copy of one page and commits it only if it is valid and different. Its undo snapshot is just the page. This is the path for typing, formatting, drawing and moving notes.- Undo swaps the current state with the snapshot and keeps the swapped-out state for redo. View settings are carried over, so undo never changes the theme or zoom. The history holds 100 entries and an estimated 64 MiB.
- Every commit, undo and redo pushes a
Changenaming the page it touched, or none for a workspace change. The app takes these withtake_changes()to decide what to save and what to redraw.
All organizing operations (add, rename, delete, restore, duplicate, move pages and
subtrees, indent, promote, groups, sections, versions, Replace All) are methods on
Session, each one transaction.
The app
GPUI renders the whole window from Workbench on every notify, so there is no separate
view model: panes read the session directly each frame.
Commands. Ribbon buttons, menu items, context menus and shortcuts all end in
Workbench::run(cmd, entity). It first commits any text being edited (except for
formatting commands, which apply to the editor's selection), runs the command, shows
errors in a native alert, and calls after_change.
Saving. after_change takes the session's changes into a Dirty set (pages, or
all) and schedules a save 650 ms later; view setting changes mark settings. Saving
runs on the main thread: the store writes only what changed, so a save after typing is
a handful of rows. The workbench also saves when the window closes and when the app
quits.
The page. render_surface is a stack: a canvas that paints everything, and, when
text is being edited, the editor placed over the note, title or cell. The canvas's
prepaint reads the workbench and builds a display list of Ops (quads, paths, text
layouts, shaped lines, images), culled to the visible area; paint executes it. Page
coordinates map to the window with the zoom and the scroll offset. Pictures are painted
with Image::use_render_image, so they keep their place under the ink.
Pointer input starts on the surface and continues at the window's root while a gesture
(ink, erase, move, resize, pan) is running, so drags that leave the page still finish.
A gesture draws a preview and commits one edit_page when the pointer is released.
Text layout. layout::layout flows a note's text the way NoteSpace does: words and
spaces are separate pieces, each shaped in its own style with
WindowTextSystem::shape_line, so a line can mix font sizes; pieces wrap at the
container width and share a baseline. Bullets and numbers take a 24-point indent. The
same function lays out notes on the page, in the editor and in PNG export, so a note
wraps identically everywhere.
The editor. Editor is one GPUI entity implementing EntityInputHandler (input
methods, dead keys, the character palette). It works in five modes: a note, a page
title, a table cell, a one-line field and a multi-line field. For notes it carries the
note's style runs and shifts them as you type, so formatting shows while editing. The
workbench commits its text after a 450 ms pause, on blur, and before any other command;
for notes the commit goes through rich_text::replace, which finds the smallest changed
range and keeps the formatting around it.
Dialogs. Messages and confirmations are native alerts (Window::prompt). Prompts,
lists, colors, fonts, the table editor and Replace are GPUI overlays built from
Editor fields; each holds its continuation, run when the dialog is accepted.
Icons. The icons are NoteSpace's own 24-point line paths, as SVG files in
assets/icons/, compiled in by assets.rs and tinted by GPUI.