Code Layout
Where each piece of code lives, why it lives there, and the checks that keep it that way. Read this before changing the repositories.
| Status | Current. This page is the reference for how the code is organized |
|---|---|
| Stability | The rule is settled. The crate split is the next change |
The Three Repositories
Runebender is one library, one editor, and one website.
| runebender-core | Every operation on a font, the document model, and the runebender command line. No window. |
|---|---|
| runebender-gpui | The editor. The window, the input, the drawing, and nothing that changes a font. |
| runebender-dot-org | This site: the documentation, and the browser build of the editor. |
The library depends on norad and kurbo. The editor depends on the library and on GPUI. The site depends on neither, and copies the browser build in as a static asset.
One Rule, and How It Is Kept
If an operation changes a font, or reads one to answer a question, it lives in core.
The test is whether another front-end needs it. Where a click lands is the editor’s business. What that click does to the outline is core’s. An operation in core can be tested in milliseconds without a window. It can also run from the command line, from a script, or from an agent.
Continuous integration keeps the rule honest. Every push to either crate runs the same gate on Linux and macOS.
cargo fmt —check | Formatting is not a review topic. |
|---|---|
cargo clippy —all-targets | With warnings denied. Both crates are clean today. |
cargo doc —no-deps | With warnings denied, so a broken doc link fails the build. |
cargo test | Core: 299 tests. Editor: 38, all of them about the shell itself. |
| Minimum Rust | Core also builds at the rust-version in its manifest. |
Core forbids unsafe in its manifest. The editor denies it, and the
two tests that set an environment variable allow it explicitly.
A change that moves font logic into the editor will compile. Nothing stops it mechanically. The rule is kept by review, and by this page saying what the rule is.
runebender-core, by Concern
Modules are named for what they do to a font. The crate has no
folders below src/ except path/, editing/, and model/. This
is the map in src/lib.rs, expanded.
glyph_ops | Point moves, deletion with segment surgery, smooth constraints, pen primitives, shapes, decompose, remove overlap, metrics, kerning. |
|---|---|
point_ops, segment_ops | Hit testing and insertion on points and segments. |
knife, shape | Slicing, and the rectangle and ellipse primitives. |
cleanup | Operations that keep the shape and fix its points: tidy, direction, rounding, extremes, handle fitting. |
effects | Operations that make a new shape: offset, extrude, roughen, expand stroke, corner components, bolden. |
convert | Quadratic to cubic and back. |
embolden | Learn a per-point offset from reference pairs, and apply it. |
measure | Stems, sidebearings, joining bands. |
|---|---|
optical, spacing | Sampled ink density, and sidebearings checked against the family’s grid. |
curve | Curvature: continuity, kinks, extrema. |
category, search | Unicode categories, and the glyph search query language. |
lib_keys | Every com.runebender.* key: masks, annotations, saved filters, HOI intermediates. One reader and one writer per key. |
|---|---|
metrics_keys, mark_color, color_font | The Glyphs and ufo2ft keys the editor shares with other tools. |
svg, binary_import, glyphs_import, image_trace | Other formats in and out: SVG, compiled fonts, .glyphs, images. |
project | The open document: Master and Project. See the next section. |
|---|---|
var_model, composites | Interpolation across a designspace, and components with anchors. |
shaping, text | OpenType shaping, and the text-context editing model. |
editing/, font_memory, sidebar | Selection, undo, viewport, in-memory fonts, and the glyph grid’s filter data. |
theme, theme_oklch | The token file every editor resolves its colours from. See Themes. |
bin/runebender | The command line over all of the above. |
The Document Layer
src/project.rs holds what the editor used to hold on its own.
A Master is one UFO with its bookkeeping and a paint-ready cache.
The bookkeeping is which glyphs changed since the last save, and
whether kerning did. The cache is a GlyphEntry per glyph, in the
order the grid shows. An entry holds the outline as a kurbo path, its
points, its anchors, and its ink box. A Project holds the masters, the axes and
their locations, the variation model, the named instances, and the
sparse brace sources.
Nothing in this layer knows how a glyph is drawn. Names are
Arc<str>, not the toolkit’s string type. The front-end reads the
cache and paints it, and every front-end gets the same cache.
Two constructors stay in the editor: the ones that build a project
from fetched or embedded files in the browser. They call
Project::from_designspace like everything else.
runebender-gpui
The editor is one Workspace struct, split across files by concern.
main.rs | The Workspace state, the actions, the menus, the render tree, and main(). |
|---|---|
commands.rs | One method per user-facing command. The menu item, the shortcut, and the context menu all land here. |
canvas.rs | The glyph grid and the editing view: everything painted with paths. |
panels.rs | The regions either side of the canvas. |
input.rs | Pointer and keyboard on the canvas. |
theme.rs | Theme accessors. Call these instead of naming a colour or a radius. |
config.rs, journal.rs | The config file, and the operation log. |
widgets/ | The controls the editor owns: text input, slider, resizable split, and the in-window menu bar for platforms without a native one. |
web_host.rs | What the browser build needs and the native build does not. |
tests.rs | Tests of the shell itself. Anything checkable without a window belongs in core instead. |
The editor imports what it needs from core by name, at the top of
main.rs. That list is a fair summary of the seam between the two
crates.
Tests and Fixtures
Tests live next to the code they test, in a tests module at the
bottom of the file.
Core tests that need a real font load Virtua Grotesk from the
runebender-web
repository, so the crate stays small. src/test_fonts.rs is the one
place that knows where. It reads RUNEBENDER_TEST_FONTS, or looks
for ../runebender-web/assets/test-fonts beside the checkout, and
fails with the expected path when neither exists.
git clone https://github.com/eliheuer/runebender-webcd runebender-core && cargo test CI checks that repository out beside core, sparse to the fonts.
Conventions
| One file, one concern | A module is named for what it does to a font, and its header comment says so in a sentence. |
|---|---|
| norad in, norad out | Core functions take norad::Glyph or norad::Font and kurbo geometry, and return the same. No private model. |
| Report what changed | An operation that edits in place returns whether it changed anything, or how many things it changed. |
| Document every public item | Say what it does and what the caller is responsible for. A doc comment that names a type wraps it in backticks, because rustdoc treats an angle bracket as HTML. |
| Keys in one place | A UFO lib key has one constant, one reader, and one writer, in lib_keys or the module for its format. |
| Commit messages say why | The diff shows what changed. The message records what was wrong and what the change decided. |
| Builds anywhere | No path to a sibling checkout in a committed file. Local overrides live in a .cargo/config.toml above the repositories. |
What Is Still Out of Place
The rule holds. The shape inside the editor does not yet.
impl Workspace | About 5,300 lines in one block in main.rs. It is a document, an edit session, a view state, tool state, and a render, all with methods on one struct. |
|---|---|
main() | About 1,300 lines. Window setup, key bindings, and file watching share one function. |
text | The largest module in core, and the least documented. It began as a port of the web editor’s text buffer. |
model/workspace | The old Xilem-era glyph types. They survive only as the intermediate form for hyperbezier conversion in path/. |
| Two versions of kurbo | The hyperbezier solver is on kurbo 0.9, the rest on 0.13, and path/hyper.rs converts between them. |
| Missing docs are not enforced | The missing_docs lint is off. Turning it on is the next lint change, after text is documented. |
| No wasm job in CI | The browser build needs a nightly toolchain and a rebuilt standard library, and is checked by hand. |
Where It Is Going
Core will become a Cargo workspace of small crates, layered the way the Linebender crates are. Each crate is one concept, and depends only on the layer below.
| path | Contours and points, hyperbeziers, quadratic and cubic conversion. Kurbo only. |
|---|---|
| font | The document model and every lib key. |
| ufo | Reading and writing sources. The only crate that names norad. |
| ops | Every edit: point operations, knife, cleanup, effects, booleans. |
| analysis | Measurement, optical weight, spacing, curvature. Read only. |
| text | Shaping and the text buffer. |
| theme | The token file and its contrast tests. |
| cli | The runebender binary. |
The two moves that made this possible are done: the font operations and the document layer left the editor. The crate boundaries now fall out of what is already separated. What is left is the names, and the order to do it in.