Merge main into batch 15

# Conflicts:
#	docs/ui-parity.md
This commit is contained in:
bflatastic
2026-10-18 01:42:03 +07:00
5 changed files with 104 additions and 7 deletions
+1 -1
View File
@@ -117,7 +117,7 @@ French, German, Brazilian Portuguese, Japanese, Korean, Simplified Chinese and I
Language; Traditional Chinese has no catalog; CJK UI
text uses an installed CJK font, loaded only when needed; adding a language:
[`docs/translating.md`](docs/translating.md)). Document text in any script (bidi, Arabic shaping,
IME) already works. Detail: [`docs/localization-parity.md`](docs/localization-parity.md).
IME) already works (macOS IME composition fixed for #747, not yet checked on a Mac). Detail: [`docs/localization-parity.md`](docs/localization-parity.md).
## Upcoming
+87
View File
@@ -0,0 +1,87 @@
//! IME composition in a text layer, driven through egui headless with the events macOS sends for
//! Pinyin ("nihao", then a candidate picked): the platform keeps the IME on every frame of the
//! edit (an idle frame too) with the candidate window at the caret, the composition shows without
//! entering the document, keys pressed while composing are the IME's, and the commit lands once.
use egui::{Event, ImeEvent, Key, Modifiers, RawInput, Rect};
use figcraft_doc::{NodeId, NodeKind};
use figcraft_engine::Session;
use serde_json::json;
use crate::layers_drag_tests::Ui;
fn key(key: Key, pressed: bool) -> Event {
Event::Key { key, physical_key: None, pressed, repeat: false, modifiers: Modifiers::NONE }
}
fn preedit(text: &str) -> Event {
let n = text.chars().count();
Event::Ime(ImeEvent::Preedit { text: text.into(), active_range_chars: (n > 0).then_some(n..n) })
}
#[expect(deprecated, reason = "egui still delivers these, and the editor must ignore them")]
fn enabled_disabled() -> (Event, Event) {
(Event::Ime(ImeEvent::Enabled), Event::Ime(ImeEvent::Disabled))
}
fn text(u: &Ui, id: NodeId) -> String {
match &u.app.session.doc().unwrap().node(id).unwrap().kind {
NodeKind::Text(t) => t.characters.clone(),
_ => panic!("not text"),
}
}
/// One frame; the IME output it leaves for the platform (`None` turns the IME off).
fn ime(u: &mut Ui, events: Vec<Event>) -> Rect {
let ime = u.frame_raw(RawInput { events, ..Default::default() }).platform_output.ime.expect("the IME stays on while editing");
assert_eq!(ime.rect, ime.cursor_rect, "the desktop places candidates by `rect`: the caret");
ime.cursor_rect
}
fn composing(u: &Ui) -> Option<String> {
u.app.session.ds().unwrap().text_edit.as_ref().expect("still editing").preedit.as_ref().map(|p| p.text.clone())
}
#[test]
fn pinyin_composes_inline_and_commits_once_with_the_ime_on_every_frame() {
// The text is measured with a font: without craft-fonts a system one, scanned here rather than
// by whichever test happens to run first (#333).
figcraft_text::scan_system_fonts();
let mut s = Session::new();
let t = NodeId(s.execute("text.create", &json!({"text": "Hi", "x": 40, "y": 40})).unwrap()["id"].as_u64().unwrap());
// On a machine without any font nothing lays out and every caret sits at the start.
let fonts = figcraft_text::layout_node(s.doc().unwrap().node(t).unwrap()).is_some_and(|(l, _)| !l.glyphs.is_empty());
let mut u = Ui::new(s);
figcraft_engine::textedit::begin(&mut u.app.session, t, Some(2), false).unwrap();
// An idle frame (the caret blinking) keeps the IME on, or macOS drops the composition.
let caret = ime(&mut u, vec![]);
let ds = u.app.session.ds().unwrap();
let b = ds.doc.world_bounds(t).unwrap();
let layer = Rect::from_two_pos(u.app.to_screen(b.x0, b.y0), u.app.to_screen(b.x1, b.y1)).expand(1.0);
assert!(layer.contains(caret.center()), "candidates open at the caret ({caret:?} in {layer:?})");
let (enabled, disabled) = enabled_disabled();
ime(&mut u, vec![enabled, preedit("n")]);
ime(&mut u, vec![]);
let mid = ime(&mut u, vec![preedit("ni")]);
assert!(!fonts || mid.min.x > caret.min.x, "the IME cursor follows the composition");
// A key the platform leaks while composing (Backspace) belongs to the IME, not the text.
ime(&mut u, vec![key(Key::Backspace, true), preedit("nih"), key(Key::Backspace, false)]);
ime(&mut u, vec![preedit("nihao")]);
assert_eq!(composing(&u).as_deref(), Some("nihao"));
assert_eq!(text(&u, t), "Hi", "the composition isn't in the document yet");
// Enter confirms the candidate: no newline, editing goes on.
let after = ime(&mut u, vec![key(Key::Enter, true), preedit(""), Event::Ime(ImeEvent::Commit("你好".into())), disabled, key(Key::Enter, false)]);
assert_eq!(text(&u, t), "Hi你好");
assert_eq!(composing(&u), None);
let te = u.app.session.ds().unwrap().text_edit.clone().unwrap();
assert_eq!((te.caret, te.anchor), (4, 4));
assert!(!fonts || after.min.x > caret.min.x, "the caret moved past the committed text");
ime(&mut u, vec![]);
assert_eq!(text(&u, t), "Hi你好", "committed once");
// Keys are the editor's again once the composition is over.
ime(&mut u, vec![key(Key::Enter, true), key(Key::Enter, false), Event::Text("!".into())]);
assert_eq!(text(&u, t), "Hi你好\n!");
}
+2
View File
@@ -117,6 +117,8 @@ mod home_tests;
#[cfg(test)]
mod idle_tests;
#[cfg(test)]
mod ime_tests;
#[cfg(test)]
mod import_report_tests;
#[cfg(test)]
mod layers_behaviour_tests;
+11 -4
View File
@@ -102,14 +102,16 @@ pub fn handle(app: &mut FigApp, ctx: &Context, typing: bool, swallowed: Swallowe
return;
}
let events: Vec<Event> = ctx.input(|i| i.events.clone());
if events.is_empty() {
return;
}
// Every frame while editing, even one without input: a frame without the IME output turns the
// platform's IME off, which drops the composition (macOS then types plain letters).
let editing = app.session.ds().map(|d| d.text_edit.is_some()).unwrap_or(false);
if editing {
text_keys(app, ctx, &events);
return;
}
if events.is_empty() {
return;
}
// An image-only clipboard brings no paste event: paste from the key then. The browser sends
// the press; the desktop shell swallows it and sends only the release.
let paste_event = events.iter().any(|e| matches!(e, Event::Paste(_)));
@@ -448,6 +450,10 @@ fn text_keys(app: &mut FigApp, ctx: &Context, events: &[Event]) {
}
}
Event::Key { key, physical_key, pressed: true, modifiers: m, .. } => {
// Keys during a composition are the IME's (Enter picks a candidate, Escape cancels).
if app.session.ds().is_ok_and(|d| d.text_edit.as_ref().is_some_and(|te| te.preedit.is_some())) {
continue;
}
if (m.command || m.alt || m.ctrl) && text_shortcut(app, *key, *physical_key, *m) {
skip_text = true;
continue;
@@ -532,7 +538,8 @@ fn ime_output(app: &FigApp, ctx: &Context) {
ctx.output_mut(|o| {
o.ime = Some(egui::output::IMEOutput {
purpose: egui::IMEPurpose::Normal,
rect: app.canvas.rect,
// The desktop places the candidate window by `rect` (the web by `cursor_rect`).
rect: cursor_rect,
cursor_rect,
should_interrupt_composition: false,
})
+3 -2
View File
@@ -1,6 +1,6 @@
# UI and interaction parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-17 · **Change:** minor (Escape or Enter after a pen path stays on the same vector (#760); Selection colors for mixed selections and smart-selection spacing (#753); submenus with a hover delay and a safe triangle; the Actions palette's tabs, ranked search, highlight and recents; menu items that ask for a value; editor fixes from the community batches; fields select their text on focus; Typography spacing icons support scrubbing (#373); CSS colour names in hex fields; the Small caps sample in Type settings drawn from capitals; recent files as cards; vector edit mode: hovering, hatching, selecting and dragging filled regions; home screen: Templates tab, first-run templates and Getting started tips; number fields: scrub rates, arrow steps, arithmetic with brackets, relative edits of mixed values; outline mode's hidden layers and object bounds; Hide when publishing; cursors per tool and modifier; panel widths; dark pages for files made in the dark theme; drag threshold and double-clicks into text in groups; rotation origin on Alt+R and placing with Enter: no documented shortcut missing; keyboard shortcut parity suite and bindings; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and the vector edit tail; canvas navigation, guides and grids behaviour suite and fixes; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; layers panel keyboard: F2 rename, Shift+F10 context menu, F8 box selection; selection and transform behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; boolean hit testing on the result, group drags, frame outlines and slices; outlines for the selection, mask outlines; arc fields, live boolean editing, shape builder on strokes, one-sided widths) · **Target:** Figma Design UI3 (desktop app 126.10.7)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-17 · **Change:** minor (Escape or Enter after a pen path stays on the same vector (#760); IME composition kept on through idle frames of a text edit, candidates at the caret (#747); Selection colors for mixed selections and smart-selection spacing (#753); submenus with a hover delay and a safe triangle; the Actions palette's tabs, ranked search, highlight and recents; menu items that ask for a value; editor fixes from the community batches; fields select their text on focus; Typography spacing icons support scrubbing (#373); CSS colour names in hex fields; the Small caps sample in Type settings drawn from capitals; recent files as cards; vector edit mode: hovering, hatching, selecting and dragging filled regions; home screen: Templates tab, first-run templates and Getting started tips; number fields: scrub rates, arrow steps, arithmetic with brackets, relative edits of mixed values; outline mode's hidden layers and object bounds; Hide when publishing; cursors per tool and modifier; panel widths; dark pages for files made in the dark theme; drag threshold and double-clicks into text in groups; rotation origin on Alt+R and placing with Enter: no documented shortcut missing; keyboard shortcut parity suite and bindings; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and the vector edit tail; canvas navigation, guides and grids behaviour suite and fixes; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; layers panel keyboard: F2 rename, Shift+F10 context menu, F8 box selection; selection and transform behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; boolean hit testing on the result, group drags, frame outlines and slices; outlines for the selection, mask outlines; arc fields, live boolean editing, shape builder on strokes, one-sided widths) · **Target:** Figma Design UI3 (desktop app 126.10.7)
Fine canvas control is what Figma users feel first: handles, snapping, nudging, modifier keys,
drag behaviour, numeric precision. This checklist covers the tools, gestures and panels. Status:
@@ -85,7 +85,7 @@ interaction feel not yet compared.
| Vector edit mode: bend, paint bucket, lasso, cut, eraser, transform box, point snapping, mirroring | done | a double-click a few pixels off a thin stroke enters edit mode (#646); a point dropped on another point merges with it, and dropping one end of an open path on the other closes it (a 2-point line's end just moves, #401); Flatten keeps open paths open (#377); paint bucket drags across areas; cut drags divide a shape into layers; delete and heal follows the curve; rounded points are drawn; Shift-click deselects a point at a corner of the box (`vector_behaviour_tests`); several vectors edited at once, Shift-click handles to drag them together, a cap and join per point, rounded points beside curves (`vector_multi_behaviour_tests`); filled regions with the Move tool: the area under the pointer is hatched ("/" lines in the selection colour, clipped by the area's own fill rule, holes left out, gone 3.5 px from a point or 2 px from a segment at any zoom), the line spacing follows octave folding (fitted to measurements at several zoom levels: 8 px at every power-of-two zoom, growing with the zoom and stepping back at √2 · 2ⁿ) and stays fixed to the shape when panning; a click selects the area and its points (moved with it but drawn unselected), Shift adds or removes an area, a press on a selected area drags the whole selection, no hover hatch during drags, a click on an empty spot (a hole) clears it and stays in edit mode; `vector.edit.select {regions}` (`regions_tests`, `vector_regions_tests`). Not yet: the transform box around points of several layers; areas of other layers edited at once |
| Shape builder (Shift+M) on fills, strokes and open paths; variable width (Shift+W), one side with Alt | done | |
| Boolean groups: clicked where the result is, double-click or Cmd/Ctrl-click into operands, live results while dragging or editing points, per-operand operations | done | |
| Text: point, auto height, fixed; caret, words, lines, IME | done | the font picker previews a family on the canvas while hovered (no undo step; Escape restores, #653); a selection highlight after a line break starts at that line's start and an empty last line shows its mark (#665); carets step over whole emoji and accented letters; CJK words by script run (no dictionary); vertical trim in Type settings; no spell check |
| Text: point, auto height, fixed; caret, words, lines, IME | done | the font picker previews a family on the canvas while hovered (no undo step; Escape restores, #653); a selection highlight after a line break starts at that line's start and an empty last line shows its mark (#665); carets step over whole emoji and accented letters; IME composition replayed in tests as macOS sends it, not yet checked on a Mac (#747); CJK words by script run (no dictionary); vertical trim in Type settings; no spell check |
| Eyedropper (I) | done | picks the colour seen under the pointer (opaque) into the selection's fill and the recent colours, one undo step (`fx_behaviour_tests`) |
| Comment (C), Shift+C to show/hide | done | local only |
| Text on a path (Figma Draw) | done | put on from the Design panel or `text.onPath`, not by clicking a path with the Text tool; the path is edited by resizing only |
@@ -173,6 +173,7 @@ in the table. `crates/ui-egui/src/shortcut_keys_tests.rs` drives the keys a look
| Date | Change | Summary |
|---|---|---|
| 2026-10-17 | minor | Escape or Enter while the pen draws ends the path and keeps its vector in edit mode with the pen, so the next path is drawn into the same layer (unconnected); another Escape leaves edit mode (#760). Entering edit mode on a layer that already is a vector no longer records an empty undo step. Numbers unchanged |
| 2026-10-17 | minor | Text layers keep the platform IME on for every frame of an edit, not only frames with input (an idle frame turned it off and macOS dropped each composition, #747); the candidate window opens at the caret instead of the canvas corner; keys pressed while composing go to the IME |
| 2026-10-17 | minor | The community batches #733, #735, #736, #729 and #753: submenus open after a short hover, keep their place along a safe triangle and reopen closed (main and context menus); the Actions palette's tabs, ranked search, keyboard highlight and recents; menu items that need a value ask for it, File ▸ Rename edits in place; Shift+I and Resources open Assets; a draggable Pages/Layers divider; Shift-drag moves the selection; a double-click near a thin stroke, merging dropped points and Flatten keeping open paths in vector editing; live font preview and the selection highlight after line breaks; the colour picker on a new fill or stroke; Paste over and Paste properties from the keyboard; Selection colors (partial: no library styles or variables there) and smart-selection spacing fields and canvas handles (partial: no reorder rings), both also checked by their author on macOS; the keyboard shortcuts panel closes on the home screen; Adwaita window controls on Wayland compositors without server decorations (checked by its author on GNOME; KDE not run). Checked in the app on Windows (submenus by their author), not side by side with Figma. Summary and hours unchanged |
| 2026-10-15 | minor | Hex fields take CSS colour names (`red` → `FF0000`): fills, strokes, effects, grids, variables, backgrounds and the colour picker |
| 2026-10-15 | minor | Typography: drag the line-height, letter-spacing and paragraph-spacing icons to scrub their values; shared numeric-field rates, resize cursor and one Undo step per gesture (#373). |