- The last line shown runs on into the word wrapped after it, so the ellipsis
fills the line ("Lorem consec…") instead of following the last whole word.
- An explicit height (node.set, transform.resize, a resize handle, fixed
sizing) on truncating text keeps it truncating (TextData::fix_size).
- Max lines on auto-height text hugs up to that many lines and ends in an
ellipsis; text.maxLines keeps auto height, Type settings show it as
truncating, .fig export writes textTruncation and import keeps auto height.
- node.get reports maxLines; text.fit measures every line.
Co-authored-by: Cursor <cursoragent@cursor.com>
The bindings that move with the path's fills and effects are now found with list_binding, which reads `fills` as the first fill's color, as style_sets in cmd/styles.rs does.
A path linked to an effect style with no effects gave the text an empty effect list and the style link. The text now takes the path's effects only when there are any.
text.onPath, and the Design panel's Put text on path button that runs
it, kept the text layer's own fill and effects when the text went onto
a path. The help center's Guide to text says that the vector object's
fill and effects are transferred to the text layer.
The text now takes the path layer's fills, with their fill style and
variable bindings, when one of them shows, and the path layer's effects,
with their effect style and bindings, when it has any. The text's own
bindings on those fields are dropped. When the path has no visible fill
or no effects, the text keeps its own.
An explicit edit of a bound width, height, opacity, gap, padding, corner
radius, stroke weight, arc or text field reported ok, but normalize wrote
the variable's value straight back. Now the edit sticks and that field's
binding is dropped, as paint edits already do.
- `styles::bound_before` / `detach_edited`: compare each layer-level
bound field's stored value before and after the edit (`bound_field`),
so keys written unchanged keep their bindings and fields moved along
with another (proportions, min/max) lose theirs. A binding the edit
itself made (`boundVariables`) stays. List and component property
bindings are left to their own rules.
- Hooked into node.set, transform.resize, object.opacityKeys, text.style
and `Session::preview` (canvas drags of the selected layers).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- text.rescanFonts offers a loaded family the files it hasn't read (styles
installed since) and refits every family that gained faces; `added` still
lists only the families that were unknown
- text.refit refuses an `ids` that isn't a list instead of refitting the
whole document
- text.loadFont with a file already loaded returns its families without
keeping its bytes or adding its faces again
- doc comments on the functions this change touches
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A text box keeps the size it was measured at, so text laid out while its
family was missing kept the fallback font's box after the font arrived (a
hugging chip clipped its label).
- text.refit {ids?} measures auto-sizing text again (whole document by
default) → {refitted}
- text.rescanFonts and text.loadFont refit the open document's text in the
families they add, as one undo step
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The system fonts are indexed once per process, so a font installed while
FigCraft (or `figcraft-cli mcp`) runs stayed missing until a restart.
- text.rescanFonts walks the font folders again and offers the families
installed since (loaded families keep their faces) → {added}
- text.loadFont {path | data} adds a font file without installing it → {families}
- both clear the layout cache so text lays out again in the new face
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hovering a family in the Typography font dropdown previews it on the
canvas via text.preview; closing without picking restores the original,
clicking applies it as one undo step.
- A shortcut parity table (`crates/engine/src/shortcut_parity_tests.rs`): one row per shortcut in
the public documentation (help-centre feature articles: keyboard layouts, text properties,
alignment, frames, components, prototypes, plugins, keyboard navigation), with the macOS and the
Windows/Linux keys, what handles it here (keymap command, held key/gesture listed in the panel,
vector edit tool key, or missing), and a note in our own words. Rows the documentation is silent
on pin today's keys, so every default binding is in the table.
- 129 documented rows: 126 bound (110 before), 1 moved off a documented conflict (Run last
plugin on Windows: the docs also give Ctrl+Alt+P to pixel preview; the plugin keeps
Ctrl+Alt+Shift+P), 2 missing (moving the rotation origin with Alt+R; placing an object with
Enter after choosing a tool from the toolbar), 2 kept by a platform (Ctrl+Space with some CJK
input methods; F6 in browsers). 48 undocumented bindings pinned.
- Tests: every row fires its command on both platforms and sits in the right shortcuts-panel tab;
every default binding has a row; no binding uses a chord the OS keeps (Cmd+H/M/Q/Tab/Space,
Ctrl+Space, screenshot keys on macOS; Alt+Space/Tab/F4, Ctrl+Esc, Ctrl+Alt+Delete, Linux
workspace and VT keys); macOS keycaps show Cmd/Option glyphs only and Windows/Linux keycaps
Ctrl/Alt words only; vector edit tool keys; counts pinned for the docs.
- Newly bound: A (Frame tool), Shift+' (pixel grid; Cmd+' kept, the articles disagree),
Alt+Cmd+\ / Ctrl+Alt+\ (others' cursors in a live session), Ctrl+F6 (toolbar focus on
Windows/Linux), Shift+Cmd+U (link text; Cmd+K kept), Cmd/Ctrl+] and [ (list levels while
editing), Cmd/Ctrl+Backspace (ungroup), Alt+Space / Ctrl+Space (keyboard box selection; F8 kept,
Cmd/Ctrl+arrows draw the box out), Ctrl+Shift+A / Ctrl+Alt+Shift+A (suggest auto layout),
Ctrl+Alt+Cmd+K / Ctrl+Alt+Shift+K (go to main component).
- New commands: `view.multiplayerCursors {on?}` (View menu item, menu breadth 207/207),
`view.pan {dx, dy, big?}` (arrows pan the view by 20 screen px, 200 with Shift, when nothing is
selected; the canvas and MCP `key` both use it), `text.listIndent` / `text.listOutdent`.
- Behaviour: Control held while dragging suspends snapping on macOS too (Cmd/Ctrl already did);
the presenter takes Space and N for the next frame, C for comments (Shift+C kept) and F for full
screen, after the prototype's own key triggers; the paint bucket's tooltip shows Shift+B (B still
works).
- Keyboard layouts: shortcuts follow the character typed, named as on a US keyboard. A symbol
another layout types with Shift (`/` is Shift+7 on German keyboards) now counts without the
Shift (canvas, text editing and F6 navigation share one lookup). Characters egui has no key for
already arrive as the key at that position.
- Keyboard shortcuts panel: new rows for panning with the arrows, suspending snapping, moving along
the toolbar, and the presentation keys, in the tabs the documentation groups them in (en/es
strings).
- Docs: ui-parity.md gets a Keyboard shortcuts section with the counts; target-app-parity.md,
ROADMAP.md, gaps.md (gap 4 landed list), mcp.md, parity-checklist.md (regenerated).
The tail of the text area, from the public documentation of the target app's text settings, with a table-driven behaviour suite (each case derived in one line; "silent" cases pin today's behaviour).
- **Lists per paragraph** (`TextData::paragraphs`, one `ParaList {list, indent}` per paragraph; empty when the layer has one list style at level 0, so old files load and save unchanged). The list buttons, Shift+Cmd+8 / Shift+Cmd+7, `text.listStyle` and Type settings act on the paragraphs the selection touches while editing (the whole layer otherwise). The per-paragraph list follows edits: a new paragraph copies the one it split from, joined paragraphs keep the first one's.
- **Nesting** up to 8 levels: Tab nests a list item, Shift+Tab moves it out; Enter on an empty item and Backspace at an item's start move it a level out, then out of the list. Typing `- `, `* ` or `1. ` at the start of a plain paragraph starts a list. Numbers count per level (1. a. i., then round again), bullets go • ◦ ▪; a plain paragraph, a shallower item or the other list kind starts the count again. Each level indents by one marker gutter; a nested bullet a font lacks comes from a fallback font.
- **List spacing** (space between two items of a list, in place of paragraph spacing), **hanging lists** (the outer gutter outside the box, item text lined up with plain paragraphs) and **hanging punctuation** (opening quotes and brackets outside the start edge of left-aligned and justified lines, closing punctuation outside the end edge of right-aligned and justified ones; hung punctuation takes no room when lines wrap).
- **Decoration look** per character (`TextStyle::decoration_look`, boxed, None by default): style (solid, dotted, wavy), thickness and underline offset (auto, px or % of the font size), skip ink (underlines break around descenders) and colour (its own style index in the layout, so canvas, PNG and PDF paint it without renderer changes). Decorations are drawn from `Decoration::path()` (bar, dots or wave) on the canvas, in PNG, PDF and outlined SVG.
- `node.set` keys: `listStyle` and `indentation` (range-aware with start/end), `listSpacing`, `hangingPunctuation`, `hangingList`, `textDecorationStyle`, `textDecorationThickness`, `textDecorationOffset`, `textDecorationSkipInk`, `textDecorationColor`; documented in `KEYS` and docs/mcp.md. `text.edit.key` documents Tab.
- Inspect: `listStyle`, or `lineTypes` and `lineIndentations` per paragraph; `listSpacing`, `hangingPunctuation`, `hangingList`, the decoration keys. CSS: `text-decoration-style`, `-thickness`, `-color`, `text-underline-offset`, `text-decoration-skip-ink: none` (browsers skip ink by default). Dev Mode rows: decoration style, list spacing.
- REST import: `lineTypes` with `lineIndentations` per paragraph (they collapsed to one list style for the layer before), `listSpacing`, `hangingPunctuation`, `hangingList`, the decoration style, thickness, offset, skip ink and colour; instance override names for them.
- Type settings ▸ Basics: decoration style, thickness and offset fields (with Auto), Skip ink, decoration colour; List spacing; Hanging punctuation and Hanging list checkboxes; the list control acts on the edited paragraphs. New strings in en/es.
- Components carry the new text fields in the characters and text-style override groups; scaling scales list spacing and px decoration lengths.
- `TextData::path` is boxed (`Option<Box<TextPath>>`) to keep `NodeKind` under clippy's enum size limit after the new fields.
Text layers can follow a path (start offset, flip, alignment; edited in place; canvas, PNG, SVG, PDF; SVG textPath imports as real text on a path). A new pattern paint tiles a source layer in grid or hex rows with scale, spacing and alignment, live as the source changes (SVG pattern, PDF vector tiling pattern).
Right-to-left and mixed-direction names in the panels: layers, pages,
Assets, tab titles, the file header and canvas frame titles show in
visual order (Unicode bidi, joined Arabic letters, mirrored brackets),
elided at their end (the left end of a right-to-left name). egui shapes
each font run itself, so shown names are laid out a cluster at a time.
Rename fields keep egui's editing and draw the shown text over it: the
caret, selection, clicks, drags and arrow keys follow what's on screen.
System fonts are added for scripts the built-in UI fonts lack.
The Typography section's text style browser: list and grid views with
each style in its own font, search, folders, this file's and enabled
libraries' styles, apply, detach, edit the typography, create from the
selected chars, hover to preview on the canvas, and "Mixed" when the
chars use several styles. Text styles now link chars (style_id on text
styles), so a range can take a style; editing what a style sets
unlinks the chars edited; files from before link on open.
Live text previews (text.preview / text.endPreview): a hovered text
style, font style or named instance and a dragged axis slider show on
the canvas without an undo step; release commits one step, Escape
cancels. ui.input sends raw window input over the control channel.
Visual QA: list markers keep the paragraph's font, renames start with
the name selected and centred, Max lines shows while truncating.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- Inline IME composition: egui Preedit events become a composition held in the text edit
session (`TextEdit.preedit`), shown inline at the caret with an underline and the IME cursor,
reflowing the text; the document only gets the committed text (`text.edit.insert`). Composing
over a selection replaces it; moving the caret or ending the edit drops an uncommitted
composition. The IME candidate window follows the cursor inside the composition.
New command `text.edit.preedit {text, cursor?}`; `text.edit.selection` reports it.
- Variable fonts: faces read their fvar axes (tag, name, range, default, hidden) and named
instances. Named instances are offered as styles and resolve in `find`; a style name with no
instance implies weight/italic/slant on the axes and optical size follows the font size.
Per-run custom axis values (`TextStyle.variations`, serde default, omitted when empty) are applied
in HarfRust shaping (ShaperInstance) and skrifa outlines/advances (cached per coordinates).
PDF export outlines non-default instances instead of embedding the default one.
- Commands/props: `fontVariations` / `variationSettings` props (whole layer or start..end),
`text.setVariations {variations, replace?}` (merges; edited range while editing),
`text.fonts {family, style?, axes: true}`; `node.get` and copy/paste properties carry the axes.
Picking a named style (Bold/Italic toggles, weight steps, fontStyle/fontWeight) drops custom
wght/ital/slnt values and keeps the others.
- Type settings ▸ Variable tab for variable fonts: a slider (default tick, double-click to reset)
and a field per visible axis, "Mixed" across differing styles, live drag as one undo step,
"Reset to style". Long style/family names stay on one line with an ellipsis and a tooltip.
- Tests: variable font shaping/outlines/clamping/instances (skipped without a variable font),
serde backward compatibility, preedit lifecycle and undo, variation commands/props/undo/junk;
junk sweeps extended with variation and preedit params.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>