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.

StatusCurrent. This page is the reference for how the code is organized
StabilityThe rule is settled. The crate split is the next change

The Three Repositories

Runebender is one library, one editor, and one website.

What each repository is for
runebender-coreEvery operation on a font, the document model, and the runebender command line. No window.
runebender-gpuiThe editor. The window, the input, the drawing, and nothing that changes a font.
runebender-dot-orgThis 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.

What CI checks
cargo fmt —checkFormatting is not a review topic.
cargo clippy —all-targetsWith warnings denied. Both crates are clean today.
cargo doc —no-depsWith warnings denied, so a broken doc link fails the build.
cargo testCore: 299 tests. Editor: 38, all of them about the shell itself.
Minimum RustCore 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.

Outline editing
glyph_opsPoint moves, deletion with segment surgery, smooth constraints, pen primitives, shapes, decompose, remove overlap, metrics, kerning.
point_ops, segment_opsHit testing and insertion on points and segments.
knife, shapeSlicing, and the rectangle and ellipse primitives.
cleanupOperations that keep the shape and fix its points: tidy, direction, rounding, extremes, handle fitting.
effectsOperations that make a new shape: offset, extrude, roughen, expand stroke, corner components, bolden.
convertQuadratic to cubic and back.
emboldenLearn a per-point offset from reference pairs, and apply it.
Reading a font
measureStems, sidebearings, joining bands.
optical, spacingSampled ink density, and sidebearings checked against the family’s grid.
curveCurvature: continuity, kinks, extrema.
category, searchUnicode categories, and the glyph search query language.
Lib keys and formats
lib_keysEvery com.runebender.* key: masks, annotations, saved filters, HOI intermediates. One reader and one writer per key.
metrics_keys, mark_color, color_fontThe Glyphs and ufo2ft keys the editor shares with other tools.
svg, binary_import, glyphs_import, image_traceOther formats in and out: SVG, compiled fonts, .glyphs, images.
Families, text, and the document
projectThe open document: Master and Project. See the next section.
var_model, compositesInterpolation across a designspace, and components with anchors.
shaping, textOpenType shaping, and the text-context editing model.
editing/, font_memory, sidebarSelection, undo, viewport, in-memory fonts, and the glyph grid’s filter data.
theme, theme_oklchThe token file every editor resolves its colours from. See Themes.
bin/runebenderThe 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.

src/
main.rsThe Workspace state, the actions, the menus, the render tree, and main().
commands.rsOne method per user-facing command. The menu item, the shortcut, and the context menu all land here.
canvas.rsThe glyph grid and the editing view: everything painted with paths.
panels.rsThe regions either side of the canvas.
input.rsPointer and keyboard on the canvas.
theme.rsTheme accessors. Call these instead of naming a colour or a radius.
config.rs, journal.rsThe 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.rsWhat the browser build needs and the native build does not.
tests.rsTests 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

What a change is expected to do
One file, one concernA module is named for what it does to a font, and its header comment says so in a sentence.
norad in, norad outCore functions take norad::Glyph or norad::Font and kurbo geometry, and return the same. No private model.
Report what changedAn operation that edits in place returns whether it changed anything, or how many things it changed.
Document every public itemSay 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 placeA UFO lib key has one constant, one reader, and one writer, in lib_keys or the module for its format.
Commit messages say whyThe diff shows what changed. The message records what was wrong and what the change decided.
Builds anywhereNo 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.

Known, and in order of cost
impl WorkspaceAbout 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.
textThe largest module in core, and the least documented. It began as a port of the web editor’s text buffer.
model/workspaceThe old Xilem-era glyph types. They survive only as the intermediate form for hyperbezier conversion in path/.
Two versions of kurboThe hyperbezier solver is on kurbo 0.9, the rest on 0.13, and path/hyper.rs converts between them.
Missing docs are not enforcedThe missing_docs lint is off. Turning it on is the next lint change, after text is documented.
No wasm job in CIThe 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.

The intended split
pathContours and points, hyperbeziers, quadratic and cubic conversion. Kurbo only.
fontThe document model and every lib key.
ufoReading and writing sources. The only crate that names norad.
opsEvery edit: point operations, knife, cleanup, effects, booleans.
analysisMeasurement, optical weight, spacing, curvature. Read only.
textShaping and the text buffer.
themeThe token file and its contrast tests.
cliThe 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.