From 66dd31ad9fb5b600959b32aa364e05552a4307ca Mon Sep 17 00:00:00 2001 From: AganzeFelicite Date: Fri, 16 Oct 2026 11:07:33 +0200 Subject: [PATCH 01/14] M7: find fonts activated through Adobe Fonts on macOS Fonts activated through Adobe Fonts rendered in the fallback font without a warning: Creative Cloud keeps them in its own folder as hidden files named by number, which the font scan never read. The macOS scan now includes ~/Library/Application Support/Adobe/CoreSync/plugins/livetype, and files without an extension are indexed when they start with a font signature. Fonts are read in place. Closes #298 --- ROADMAP.md | 4 +++- crates/text/src/fontdb.rs | 16 ++++++++++++++-- crates/text/src/fontdb_tests.rs | 29 +++++++++++++++++++++++++++++ docs/cli.md | 4 ++++ docs/roadmap.md | 3 ++- 5 files changed, 52 insertions(+), 4 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index c3c12085e..d6159904e 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~26 points and ~162–292 h away (one Opus 5.5 agent; ~33–67 h wall-clock with 5 agents) -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-16 · **Change:** minor (bounded canvas pan overscan; community fixes from #478 up and a control-channel fix (#589); bounded canvas pan overscan; Typography spacing icons support scrubbing (#373); font scans skip named pipes; system theme on first run; recent files as cards with thumbnails on the home screen; community fixes: lost images reported on loading (#207), SVG import budgets (#190), Leptos code (#223), full screen (#227), server snapshots readable again (#188), owner-only preferences (#200), ruzstd 0.9.1 (#182), layer cycle repair (#205), batch rollback, Direct3D 12 on Windows, plugin metering (#192); vector edit mode regions: hover hatch, selection and drag; SVG/PDF exports refuse over-depth documents (#208); bounded MCP/control framing, connections and queues (#201); plugin log limits (#191); live-session revert guard (#198); attached MCP no longer replays after a lost reply (#194); repeat command recursion fix; the Layout panel's constrain-proportions link scales the other side, #220; core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; starter templates, a Templates tab and Getting started tips on the home screen; your online files; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages; sharing with a join code; text box sizes measured against documents re-saved by other software; live sessions per document; .fig export checked by opening written files in other software; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; Windows QA pass with eight fixes; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%) · **Target:** Figma Design (desktop app 126.10.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-16 · **Change:** minor (Adobe Fonts found on macOS (#298); bounded canvas pan overscan; community fixes from #478 up and a control-channel fix (#589); bounded canvas pan overscan; Typography spacing icons support scrubbing (#373); font scans skip named pipes; system theme on first run; recent files as cards with thumbnails on the home screen; community fixes: lost images reported on loading (#207), SVG import budgets (#190), Leptos code (#223), full screen (#227), server snapshots readable again (#188), owner-only preferences (#200), ruzstd 0.9.1 (#182), layer cycle repair (#205), batch rollback, Direct3D 12 on Windows, plugin metering (#192); vector edit mode regions: hover hatch, selection and drag; SVG/PDF exports refuse over-depth documents (#208); bounded MCP/control framing, connections and queues (#201); plugin log limits (#191); live-session revert guard (#198); attached MCP no longer replays after a lost reply (#194); repeat command recursion fix; the Layout panel's constrain-proportions link scales the other side, #220; core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; starter templates, a Templates tab and Getting started tips on the home screen; your online files; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages; sharing with a join code; text box sizes measured against documents re-saved by other software; live sessions per document; .fig export checked by opening written files in other software; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; Windows QA pass with eight fixes; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%) · **Target:** Figma Design (desktop app 126.10.7) FigCraft is a clean-room, open-source, pure-Rust reimplementation of the Figma design workflow, native on macOS, Windows, Linux and FreeBSD and in the browser, fully drivable by agents through @@ -131,6 +131,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md). ## Progress log +- **2026-10-16** · Adobe Fonts found on macOS (M7, #298): fonts activated through Adobe Fonts rendered in the fallback font without a warning, because Creative Cloud keeps them in its own folder as hidden files named by number, which the font scan never read. The macOS scan now includes `~/Library/Application Support/Adobe/CoreSync/plugins/livetype`, and files without an extension are indexed when they start with a font signature. Fonts are read in place. Regression test in the font database tests; docs/cli.md lists the folder. Numbers unchanged. - **2026-10-14** · Corner handles win over the corner-radius handles (M1, #292): a press on the inner half of a corner handle's square picked the radius handle 10 px in, so the drag neither resized nor (dragging outward) rounded anything. Inside the drawn square the corner now resizes both sides (in ratio with Shift or the link); further in the radius handle still answers. Regression test in the transform behaviour suite. Numbers unchanged. - **2026-10-14** · Home screen recent files as cards (M7): each card shows the picture its file carries on its first page's colour, the file's icon, name and "Edited ... ago"; sort by name, creation or last change either way; a table view; a click selects, a double-click or Enter opens, arrow keys move; Open, Show in folder and Remove from recents on right-click. Numbers unchanged. - **2026-10-13** · Vector edit mode regions (M4): with the Move tool, the filled area under the pointer is hatched with "/" lines in the selection colour, clipped by the area's own fill rule so holes stay clear; the hatch goes near points (3.5 px) and segments (2 px), and its spacing follows octave folding (8 px at every power-of-two zoom), fixed to the shape. A click selects the area with its points (they move with it but keep the unselected look), Shift adds or removes one, a press on a selected area drags the whole selection as one undo step, and a click on an empty spot clears the areas and stays in edit mode. `vector.edit.select {regions}` and `vector.edit.state` (`regions`, `regionVertices`, `regionHover`); `vector_regions_tests` and `regions_tests`. Numbers unchanged. @@ -313,6 +314,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md). | Date | Change | Summary | |---|---|---| +| 2026-10-16 | minor | Adobe Fonts found on macOS (M7, #298): fonts activated through Adobe Fonts rendered in the fallback font without a warning, because Creative Cloud keeps them in its own folder as hidden files named by number, which the font scan never read. The macOS scan now includes `~/Library/Application Support/Adobe/CoreSync/plugins/livetype`, and files without an extension are indexed when they start with a font signature. Fonts are read in place. Regression test in the font database tests; docs/cli.md lists the folder. Numbers unchanged | | 2026-10-16 | minor | Community fixes from #478 up (batches #591, #598, #602; 69 PRs) and a control-channel fix: a line that isn't JSON now ends a `--control` connection, so a web page can't drive the app through a browser request (#589). Highlights: page copies keep navigation, flows, prototype settings and guides; Find and Replace keeps line endings and case-insensitive matches; text hyperlinks survive import and export; recent colours record only what was picked; MCP screenshots respect `maxSize`; Swedish catalog (UI strings), Czech plurals; bounded canvas overscan while panning; Korean fallback at the run's weight; many vector-edit, guide, page and .fig round-trip fixes. Numbers unchanged | | 2026-10-15 | minor | Drop files anywhere to open them (M7): dragging files over the window shows a "Drop to open" (home) or "Drop to import" (canvas) overlay. On the home screen each supported file opens as a document of its own and other files are turned away with a toast; on the canvas the files import at the drop point. Both wait behind a small card that is on screen before a large file blocks the window. A canvas import stays with the document and page that received the drop even if the tab or page changes meanwhile, and is cancelled with a message if that document or page is gone. Numbers unchanged. | | 2026-10-15 | minor | M7: system font discovery skips entries observed as non-regular files before opening them, avoiding the reproduced named-pipe stall. Regular-file symlinks remain supported; directory depth, font limits and priorities are unchanged. Numbers unchanged. | diff --git a/crates/text/src/fontdb.rs b/crates/text/src/fontdb.rs index d5ff41907..34d4775ea 100644 --- a/crates/text/src/fontdb.rs +++ b/crates/text/src/fontdb.rs @@ -694,7 +694,11 @@ fn system_font_dirs() -> Vec { v.push("/System/Library/Fonts".into()); v.push("/Library/Fonts".into()); if let Some(h) = std::env::var_os("HOME") { - v.push(std::path::PathBuf::from(h).join("Library/Fonts")); + let h = std::path::PathBuf::from(h); + v.push(h.join("Library/Fonts")); + // Adobe Fonts activates its fonts from here (hidden files named by number), without + // installing them into a font folder. + v.push(h.join("Library/Application Support/Adobe/CoreSync/plugins/livetype")); } } #[cfg(target_os = "windows")] @@ -753,7 +757,7 @@ fn scan_dir(dir: &std::path::Path, depth: usize, out: &mut BTreeMap bool { + use std::io::Read; + let mut magic = [0u8; 4]; + std::fs::File::open(path).and_then(|mut f| f.read_exact(&mut magic)).is_ok() && matches!(&magic, &[0, 1, 0, 0] | b"OTTO" | b"true" | b"ttcf") +} + /// The parts of a font file the scan reads: headers, table directories and `name` tables, at their /// offsets in a zeroed buffer of the file's length (reading whole files was most of the scan). fn index_bytes(path: &std::path::Path, len: usize) -> Option> { diff --git a/crates/text/src/fontdb_tests.rs b/crates/text/src/fontdb_tests.rs index ea762be6f..636309d30 100644 --- a/crates/text/src/fontdb_tests.rs +++ b/crates/text/src/fontdb_tests.rs @@ -179,3 +179,32 @@ fn user_font_dirs_preserve_non_utf8_paths() { assert_eq!(dirs[0].as_os_str().as_bytes(), b"/home/\xff/.fonts"); assert_eq!(dirs[1].as_os_str().as_bytes(), b"/data/\xfe/fonts"); } + +#[test] +fn the_scan_reads_font_files_without_an_extension() { + // #298: Adobe Fonts keeps the fonts it activates in hidden files named by number. Any installed + // font stands in for one (none on CI); a file that isn't a font is left alone. + let Some(font) = system_font_dirs().into_iter().find_map(|d| { + std::fs::read_dir(d) + .ok()? + .flatten() + .map(|e| e.path()) + .find(|p| p.extension().is_some_and(|x| x.eq_ignore_ascii_case("ttf") || x.eq_ignore_ascii_case("otf"))) + }) else { + eprintln!("skipping: no installed font to stand in for an activated one"); + return; + }; + let dir = std::env::temp_dir().join(format!("figcraft-font-bare-{}", std::process::id())); + std::fs::create_dir_all(dir.join(".r")).unwrap(); + std::fs::copy(&font, dir.join(".r/.12345")).unwrap(); + std::fs::write(dir.join(".r/entitlements"), b"").unwrap(); + let mut found = BTreeMap::new(); + scan_dir(&dir, 0, &mut found); + let _ = std::fs::remove_dir_all(&dir); + let families: Vec<&String> = found.values().map(|(f, _)| f).collect(); + assert_eq!(found.values().map(|(_, v)| v.len()).sum::(), faces_in(&std::fs::read(&font).unwrap()).len(), "{families:?}"); + assert!(!families.is_empty(), "{} is read as a font", font.display()); + assert!(found.values().flat_map(|(_, v)| v).all(|p| p.path.ends_with(".r/.12345"))); + #[cfg(target_os = "macos")] + assert!(system_font_dirs().iter().any(|d| d.ends_with("Adobe/CoreSync/plugins/livetype")), "Adobe Fonts' folder is scanned"); +} diff --git a/docs/cli.md b/docs/cli.md index 9f5df40ef..ccd2bb772 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -19,6 +19,10 @@ On Linux and other Unix systems except macOS, user fonts are read from `$XDG_DATA_HOME/fonts`, or `$HOME/.local/share/fonts` when `XDG_DATA_HOME` is unset, empty or relative. The legacy `$HOME/.fonts` directory is also scanned. +On macOS, fonts activated through Adobe Fonts are read where Creative Cloud keeps them +(`~/Library/Application Support/Adobe/CoreSync/plugins/livetype`), so they're available while +they're active there. Files without an extension are read when they start like a font file. + The collaboration server is a binary of its own, `figcraft-server` (`cargo build --release -p figcraft-server`): it keeps files with share links and serves their live sessions, and its `key`, `files`, `links`, `reports`, `block-ip` and `check` subcommands manage them (a server can diff --git a/docs/roadmap.md b/docs/roadmap.md index 9e9eaec9f..de8c7ff1f 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # FigCraft roadmap: milestones and what's next -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-16 · **Change:** minor (community fixes from #478 up and a control-channel fix (#589); recent files as cards with thumbnails on the home screen; community fixes: lost images reported on loading, SVG import budgets, Leptos code, full screen, server snapshots, owner-only preferences, ruzstd 0.9.1, layer cycle repair, batch rollback, Direct3D 12 on Windows, plugin metering; vector edit mode regions: hover hatch, selection and drag; repeat command recursion fix; the Layout panel's constrain-proportions link scales the other side, #220; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; `.fig` import, best effort; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~79%) · **Target:** Figma Design (desktop app 126.10.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-16 · **Change:** minor (Adobe Fonts found on macOS (#298); community fixes from #478 up and a control-channel fix (#589); recent files as cards with thumbnails on the home screen; community fixes: lost images reported on loading, SVG import budgets, Leptos code, full screen, server snapshots, owner-only preferences, ruzstd 0.9.1, layer cycle repair, batch rollback, Direct3D 12 on Windows, plugin metering; vector edit mode regions: hover hatch, selection and drag; repeat command recursion fix; the Layout panel's constrain-proportions link scales the other side, #220; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; `.fig` import, best effort; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~79%) · **Target:** Figma Design (desktop app 126.10.7) Forward-looking plan. Where FigCraft stands today: [`ROADMAP.md`](../ROADMAP.md) and [`target-app-parity.md`](target-app-parity.md); the full ranked list: [`gaps.md`](gaps.md). Hours @@ -73,6 +73,7 @@ blockers. | Date | Change | Summary | |---|---|---| +| 2026-10-16 | minor | Adobe Fonts found on macOS (M7, #298): fonts activated through Adobe Fonts rendered in the fallback font without a warning, because Creative Cloud keeps them in its own folder as hidden files named by number, which the font scan never read. The macOS scan now includes `~/Library/Application Support/Adobe/CoreSync/plugins/livetype`, and files without an extension are indexed when they start with a font signature. Fonts are read in place. Regression test in the font database tests; docs/cli.md lists the folder. Numbers unchanged | | 2026-10-16 | minor | Community fixes from #478 up (batches #591, #598, #602; 69 PRs) and a control-channel fix: a line that isn't JSON now ends a `--control` connection, so a web page can't drive the app through a browser request (#589). Highlights: page copies keep navigation, flows, prototype settings and guides; Find and Replace keeps line endings and case-insensitive matches; text hyperlinks survive import and export; recent colours record only what was picked; MCP screenshots respect `maxSize`; Swedish catalog (UI strings), Czech plurals; bounded canvas overscan while panning; Korean fallback at the run's weight; many vector-edit, guide, page and .fig round-trip fixes. Numbers unchanged | | 2026-10-14 | minor | M7: the Small caps button in Type settings showed a box where the UI font has no small-capital `ɢ` (builds without Inter use egui's fonts); its sample is now a capital and a smaller capital, drawn from ordinary letters every UI font has. Numbers unchanged | | 2026-10-14 | minor | M7: Home screen recent files as cards: each card shows the picture its file carries on its first page's colour, the file's icon, name and "Edited ... ago"; sort by name, creation or last change either way; a table view; a click selects, a double-click or Enter opens, arrow keys move; Open, Show in folder and Remove from recents on right-click. Numbers unchanged. | From 6c0ffe66770fc46e45a91e19de89263a08500f79 Mon Sep 17 00:00:00 2001 From: F3R9 Date: Thu, 22 Oct 2026 00:42:48 +0300 Subject: [PATCH 02/14] M9: make graphics-startup assertions independent of UI language --- ROADMAP.md | 3 ++- apps/figcraft/src/startup_tests.rs | 15 +++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index f43cd015b..f19cc014a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~26 points and ~162–292 h away (one Opus 5.5 agent; ~33–67 h wall-clock with 5 agents) -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-17 · **Change:** minor (community batch #753: Selection colors, smart-selection spacing, a Present tab; language table re-measured from the catalogs; community fixes from #524 up (55 PRs: `.fig` import fidelity, paste from other apps, the Actions palette, editor fixes, server limits) and a boolean-op crash guard; number variables in the text content picker (#629); frames and sections convert into each other (#719); bounded canvas pan overscan; community fixes from #478 up and a control-channel fix (#589); bounded canvas pan overscan; Typography spacing icons support scrubbing (#373); font scans skip named pipes; system theme on first run; recent files as cards with thumbnails on the home screen; community fixes: lost images reported on loading (#207), SVG import budgets (#190), Leptos code (#223), full screen (#227), server snapshots readable again (#188), owner-only preferences (#200), ruzstd 0.9.1 (#182), layer cycle repair (#205), batch rollback, Direct3D 12 on Windows, plugin metering (#192); vector edit mode regions: hover hatch, selection and drag; SVG/PDF exports refuse over-depth documents (#208); bounded MCP/control framing, connections and queues (#201); plugin log limits (#191); live-session revert guard (#198); attached MCP no longer replays after a lost reply (#194); repeat command recursion fix; the Layout panel's constrain-proportions link scales the other side, #220; core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; starter templates, a Templates tab and Getting started tips on the home screen; your online files; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages; sharing with a join code; text box sizes measured against documents re-saved by other software; live sessions per document; .fig export checked by opening written files in other software; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; Windows QA pass with eight fixes; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%) · **Target:** Figma Design (desktop app 126.10.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-22 · **Change:** minor (community batch #753: Selection colors, smart-selection spacing, a Present tab; language table re-measured from the catalogs; community fixes from #524 up (55 PRs: `.fig` import fidelity, paste from other apps, the Actions palette, editor fixes, server limits) and a boolean-op crash guard; number variables in the text content picker (#629); frames and sections convert into each other (#719); bounded canvas pan overscan; community fixes from #478 up and a control-channel fix (#589); bounded canvas pan overscan; Typography spacing icons support scrubbing (#373); font scans skip named pipes; system theme on first run; recent files as cards with thumbnails on the home screen; community fixes: lost images reported on loading (#207), SVG import budgets (#190), Leptos code (#223), full screen (#227), server snapshots readable again (#188), owner-only preferences (#200), ruzstd 0.9.1 (#182), layer cycle repair (#205), batch rollback, Direct3D 12 on Windows, plugin metering (#192); vector edit mode regions: hover hatch, selection and drag; SVG/PDF exports refuse over-depth documents (#208); bounded MCP/control framing, connections and queues (#201); plugin log limits (#191); live-session revert guard (#198); attached MCP no longer replays after a lost reply (#194); repeat command recursion fix; the Layout panel's constrain-proportions link scales the other side, #220; core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; starter templates, a Templates tab and Getting started tips on the home screen; your online files; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages; sharing with a join code; text box sizes measured against documents re-saved by other software; live sessions per document; .fig export checked by opening written files in other software; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; Windows QA pass with eight fixes; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%) · **Target:** Figma Design (desktop app 126.10.7) FigCraft is a clean-room, open-source, pure-Rust reimplementation of the Figma design workflow, native on macOS, Windows, Linux and FreeBSD and in the browser, fully drivable by agents through @@ -316,6 +316,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md). | Date | Change | Summary | |---|---|---| +| 2026-10-22 | minor | Startup graphics-error tests assert translated hints rather than English substrings, so desktop recovery tests selecting a different UI language do not make them fail. Runtime behavior and readiness estimates unchanged. | | 2026-10-20 | minor | A document dropped on the canvas opens (#916): a `.figcraft`, `.fig` or file JSON dropped on the canvas opens as a file of its own, as on the home screen, in the browser too; it used to add its pages to the file in front. Images and SVG still import at the drop point, File ▸ Import still adds pages, and dragging only documents over the canvas says "Drop to open" (not in the browser, which names no file until the drop). Numbers unchanged | | 2026-10-20 | minor | Duplicating a main component makes an instance (#892): Duplicate and Alt-drag copied a main component as a second main; within its file the copy is now an instance of it, as Copy then Paste already was (a variant duplicated in its set stays a variant). Numbers unchanged. | | 2026-10-20 | minor | Readable disabled and selected text (#890): the dim text colour (disabled buttons, placeholders, hints) is at least 3:1 against panels, fields and selected rows in both themes (it was 2.1:1 in the light theme), a disabled button is grey with that text instead of fading to 40%, and selected text (renaming a page or layer, any text box) takes the theme's readable green on the highlight (1.4:1 in the dark theme before, now 4.2:1 or more); checked with the colour contrast helper (`theme::tests`). Numbers unchanged | diff --git a/apps/figcraft/src/startup_tests.rs b/apps/figcraft/src/startup_tests.rs index 0c92bd19b..209103099 100644 --- a/apps/figcraft/src/startup_tests.rs +++ b/apps/figcraft/src/startup_tests.rs @@ -2,8 +2,15 @@ //! report it, instead of the app quitting silently (#261). use figcraft_ui_egui::dialogs::window_failed as message; +use figcraft_ui_egui::i18n::tr; const ERROR: &str = "WGPU error: Failed to create surface for any enabled backend: {}"; +const BACKEND_HINT: &str = "WGPU_BACKEND is set and names the graphics API to use; unsetting it lets FigCraft choose."; +const DRIVER_HINT: &str = if cfg!(windows) { + "Updating the graphics driver often helps. To use OpenGL only, start FigCraft from a Command Prompt with:" +} else { + "Updating the graphics driver often helps. WGPU_BACKEND (vulkan, gl, metal) names another graphics API to try." +}; #[test] fn the_message_carries_the_error_and_where_to_report_it() { @@ -15,9 +22,9 @@ fn the_message_carries_the_error_and_where_to_report_it() { #[test] fn without_wgpu_backend_it_suggests_the_driver_and_another_graphics_api() { let text = message(ERROR, false); - assert!(text.contains("graphics driver"), "{text}"); + assert!(text.contains(tr(DRIVER_HINT)), "{text}"); assert!(text.contains("WGPU_BACKEND"), "another graphics API to try: {text}"); - assert!(!text.contains("unsetting"), "{text}"); + assert!(!text.contains(tr(BACKEND_HINT)), "{text}"); } #[cfg(windows)] @@ -30,6 +37,6 @@ fn on_windows_it_shows_how_to_start_with_opengl() { #[test] fn with_wgpu_backend_set_it_suggests_unsetting_it() { let text = message(ERROR, true); - assert!(text.contains("unsetting"), "{text}"); - assert!(!text.contains("graphics driver"), "the chosen backend is the likelier cause: {text}"); + assert!(text.contains(tr(BACKEND_HINT)), "{text}"); + assert!(!text.contains(tr(DRIVER_HINT)), "the chosen backend is the likelier cause: {text}"); } From b13e5c4ff3dee91fcff37607ed9348d5024f1a74 Mon Sep 17 00:00:00 2001 From: AganzeFelicite Date: Thu, 22 Oct 2026 09:00:37 +0200 Subject: [PATCH 03/14] M7: doc comments on the font scan's folders and scan_dir (#298) --- crates/text/src/fontdb.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/text/src/fontdb.rs b/crates/text/src/fontdb.rs index edfe5b604..32d34679a 100644 --- a/crates/text/src/fontdb.rs +++ b/crates/text/src/fontdb.rs @@ -731,6 +731,7 @@ impl FontDb { } } +/// The folders this platform keeps installed and activated fonts in, walked by the font scan. fn system_font_dirs() -> Vec { #[allow(unused_mut)] let mut v: Vec = Vec::new(); @@ -794,6 +795,9 @@ fn unix_user_font_dirs(home: Option, data_home: Option)>) { if depth > 6 { return; From c6dd115e78067ab3e22ff6dc6a103fc752ef034e Mon Sep 17 00:00:00 2001 From: Susiee The Linux User <96988330+SusieeTheLinuxUser@users.noreply.github.com> Date: Sat, 24 Oct 2026 02:09:42 +0200 Subject: [PATCH 04/14] UI: Route text clipboard writes through egui (#1239) Co-Authored-By: GPT-6 Luna --- apps/figcraft/src/main.rs | 5 +++-- apps/figcraft/src/services.rs | 10 +++++++--- crates/ui-egui/src/lib.rs | 7 ++++--- crates/ui-egui/src/text_enter_tests.rs | 24 ++++++++++++++++++++++++ 4 files changed, 38 insertions(+), 8 deletions(-) diff --git a/apps/figcraft/src/main.rs b/apps/figcraft/src/main.rs index 0b71149a9..64d34e48b 100644 --- a/apps/figcraft/src/main.rs +++ b/apps/figcraft/src/main.rs @@ -244,14 +244,15 @@ fn main() -> eframe::Result { let result = eframe::run_native( "FigCraft", options, - Box::new(move |_cc| { + Box::new(move |cc| { + with_app(|fig| fig.services.set_egui_context(&cc.egui_ctx)); Ok(Box::new(App { queue, ctx_slot, title: String::new(), recovery, #[cfg(target_os = "macos")] - apple_events: apple_events.connect(&_cc.egui_ctx), + apple_events: apple_events.connect(&cc.egui_ctx), })) }), ); diff --git a/apps/figcraft/src/services.rs b/apps/figcraft/src/services.rs index 7ee41ada0..4eb636116 100644 --- a/apps/figcraft/src/services.rs +++ b/apps/figcraft/src/services.rs @@ -11,6 +11,7 @@ use serde_json::{Value, json}; pub struct Desktop { clipboard: Option, + egui_ctx: Option, /// Where the preferences live ([`config_dir`]). dir: Option, pub prefs: Option, @@ -54,7 +55,7 @@ impl Desktop { let reduce_motion = Arc::new(AtomicBool::new(false)); let flag = reduce_motion.clone(); std::thread::spawn(move || flag.store(system_reduce_motion(), Ordering::Relaxed)); - Self { clipboard: arboard::Clipboard::new().ok(), dir, prefs, reduce_motion, language: system_language(), recovery } + Self { clipboard: arboard::Clipboard::new().ok(), egui_ctx: None, dir, prefs, reduce_motion, language: system_language(), recovery } } } @@ -277,10 +278,13 @@ impl Services for Desktop { picked(dialog(title).pick_folder()) } fn clipboard_set_text(&mut self, s: &str) { - if let Some(c) = self.clipboard.as_mut() { - let _ = c.set_text(s.to_string()); + if let Some(ctx) = &self.egui_ctx { + ctx.copy_text(s.to_string()); } } + fn set_egui_context(&mut self, ctx: &egui::Context) { + self.egui_ctx = Some(ctx.clone()); + } fn clipboard_get_text(&mut self) -> Option { read_clipboard(self.clipboard.as_mut()?, |c| c.get_text().ok()) } diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 11fc307a6..59faf5aaa 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -272,6 +272,8 @@ pub trait Services { Box::pin(std::future::ready(None)) } fn clipboard_set_text(&mut self, _s: &str) {} + /// Give native services access to the window context for platform clipboard output. + fn set_egui_context(&mut self, _ctx: &egui::Context) {} fn clipboard_get_text(&mut self) -> Option { None } @@ -387,9 +389,8 @@ impl Wake for RepaintWaker { pub struct NoServices; impl Services for NoServices {} -/// Put `text` on the system clipboard through egui's output, which the browser shell writes while -/// it handles the copy event (the only time a page may write it). For shells without a clipboard -/// of their own. +/// Put `text` on the system clipboard through egui's platform output. Native integrations handle +/// it directly; the browser shell writes it while handling the copy event. pub fn egui_copy_text(ctx: &egui::Context, text: &str) { ctx.copy_text(text.to_string()); } diff --git a/crates/ui-egui/src/text_enter_tests.rs b/crates/ui-egui/src/text_enter_tests.rs index 5de0522df..89129a7e9 100644 --- a/crates/ui-egui/src/text_enter_tests.rs +++ b/crates/ui-egui/src/text_enter_tests.rs @@ -59,6 +59,30 @@ fn enter_edits_a_selected_text_with_all_of_it_selected_and_escape_keeps_the_laye assert!(u.app.session.selection().is_empty()); } +#[test] +fn copying_and_cutting_selected_text_use_clipboard_service() { + use std::{cell::RefCell, rc::Rc}; + + struct Clipboard(Rc>>); + impl crate::Services for Clipboard { + fn clipboard_set_text(&mut self, text: &str) { + self.0.borrow_mut().push(text.to_string()); + } + } + + let mut s = Session::new(); + let t = NodeId(s.execute("text.create", &json!({"text": "Hello world", "x": 20, "y": 20})).unwrap()["id"].as_u64().unwrap()); + s.execute("select.set", &json!({"ids": [t]})).unwrap(); + let mut u = Ui::new(s); + let copied = Rc::new(RefCell::new(Vec::new())); + u.app.services = Box::new(Clipboard(copied.clone())); + press(&mut u, Key::Enter); + u.frame_output(vec![Event::Copy]); + u.frame_output(vec![Event::Cut]); + assert_eq!(&*copied.borrow(), &["Hello world", "Hello world"]); + assert_eq!(u.app.session.doc().unwrap().node(t).unwrap().kind.text().unwrap().characters, ""); +} + #[test] fn a_new_file_takes_the_page_colour_of_the_theme_and_keeps_it() { let mut s = Session::new(); From 4da6a11bad93a575c94e30058c4769212254059e Mon Sep 17 00:00:00 2001 From: Ridzzz0Alam Date: Sat, 24 Oct 2026 11:26:27 +0200 Subject: [PATCH 05/14] M1: render bounds stop at the depth the renderer draws (#1062) world_render_bounds and children_render_bounds recursed once per level with no limit, so a crafted chain of ~20,000 nested groups overflowed the stack and aborted the app on PNG/JPG/WebP export, rasterize, copy as PNG and thumbnails. A stack overflow is an abort, not a panic, so the guard can't catch it. The walk now carries a depth and stops descending at 256, the depth draw_layer draws to (MAX_EXPORT_DEPTH): bounds match what is drawn and shallower trees are unchanged. Test: render_bounds_stop_where_drawing_stops_on_very_deep_chains (aborts on the old code). --- crates/doc/src/lib.rs | 18 +++++++++++++----- crates/doc/src/tests.rs | 18 ++++++++++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/crates/doc/src/lib.rs b/crates/doc/src/lib.rs index 70797dd89..5cda05e5a 100644 --- a/crates/doc/src/lib.rs +++ b/crates/doc/src/lib.rs @@ -44,6 +44,10 @@ use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use std::sync::Arc; +/// Deepest descendant [`Document::world_render_bounds`] follows: as deep as the renderer draws +/// (`figcraft_render::MAX_EXPORT_DEPTH`), so a crafted chain can't overflow the stack (#1062). +const MAX_RENDER_DEPTH: usize = 256; + /// Stable node identifier, unique within a document. #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[serde(transparent)] @@ -703,12 +707,16 @@ impl Document { /// a clipping frame's own area, else that and what its visible children show (masked /// children only within their mask). A group is what its children show, grown by its effects. pub fn world_render_bounds(&self, id: NodeId) -> Option { + self.render_bounds_at(id, 0) + } + + fn render_bounds_at(&self, id: NodeId, depth: usize) -> Option { let n = self.node(id)?; let own = n.own_paint_bounds(self.world_transform(id)); - if n.clip && n.kind.is_frame_like() && !n.kind.bounds_follow_children() { + if (n.clip && n.kind.is_frame_like() && !n.kind.bounds_follow_children()) || depth >= MAX_RENDER_DEPTH { return Some(own); } - Some(match (self.children_render_bounds(n), &n.kind) { + Some(match (self.children_render_bounds(n, depth + 1), &n.kind) { (Some(kids), NodeKind::Group) => n.grow_by_effects(kids), (Some(kids), _) => own.union(kids), (None, _) => own, @@ -716,15 +724,15 @@ impl Document { } /// What `n`'s visible children show, together: each mask's run within the mask's bounds. - fn children_render_bounds(&self, n: &Node) -> Option { + fn children_render_bounds(&self, n: &Node, depth: usize) -> Option { let shown = |i: &NodeId| self.node(*i).is_some_and(|c| c.visible && !c.is_mask); let mut out: Option = None; for run in self.paint_runs(n) { let ids = n.children.get(run).unwrap_or(&[]); let mask = ids.first().copied().filter(|m| self.node(*m).is_some_and(|c| c.is_mask && c.visible)); - let r = ids.iter().filter(|i| shown(i)).filter_map(|i| self.world_render_bounds(*i)).reduce(|a, b| a.union(b)); + let r = ids.iter().filter(|i| shown(i)).filter_map(|i| self.render_bounds_at(*i, depth)).reduce(|a, b| a.union(b)); let r = match mask { - Some(m) => r.zip(self.world_render_bounds(m)).map(|(a, b)| a.intersect(b)).filter(|x| x.width() > 0.0 && x.height() > 0.0), + Some(m) => r.zip(self.render_bounds_at(m, depth)).map(|(a, b)| a.intersect(b)).filter(|x| x.width() > 0.0 && x.height() > 0.0), None => r, }; out = match (out, r) { diff --git a/crates/doc/src/tests.rs b/crates/doc/src/tests.rs index 1a006c262..7f64ee740 100644 --- a/crates/doc/src/tests.rs +++ b/crates/doc/src/tests.rs @@ -559,3 +559,21 @@ fn is_ancestor_holds_for_very_deep_chains() { assert!(d.is_ancestor(root, leaf)); assert!(matches!(d.reparent(root, leaf, None), Err(DocError::Cycle(..)))); } + +#[test] +fn render_bounds_stop_where_drawing_stops_on_very_deep_chains() { + // A crafted file nests groups far deeper than anything draws (#1062). The leaf counts while + // it's within the renderer's depth and is left out past it, as the renderer leaves it out. + let chain = |depth: usize| { + let mut d = Document::new("x"); + let mut parent = page(&d); + for _ in 0..depth { + parent = d.create(parent, NodeKind::Group, "G", 0.0, 0.0, 10.0, 10.0).unwrap(); + } + d.create(parent, NodeKind::Rectangle, "R", 100.0, 100.0, 10.0, 10.0).unwrap(); + let top = d.node(page(&d)).unwrap().children[0]; + d.world_render_bounds(top).unwrap() + }; + assert_eq!(chain(MAX_RENDER_DEPTH).x1, 110.0); + assert_eq!(chain(20_000).x1, 10.0); +} From 2a08ad32443498bbee480044c19022eeceb329bc Mon Sep 17 00:00:00 2001 From: Mojtaba Jafarzadeh Date: Sat, 24 Oct 2026 22:35:05 +0330 Subject: [PATCH 06/14] collab-net: wait longer for one-shot server replies Publishing uploads a whole file, and a large project on a slow link takes the server several minutes to receive and answer. The one-shot request connections (hosting, the publish upload, a library, a plugin directory) waited the session idle timeout (45s), so a large publish timed out with "no answer from the server" before the server finished. Add a dedicated request_reply_timeout (5m) for these connections, keeping the session idle timeout unchanged. Co-Authored-By: Claude --- crates/collab-net/src/limits.rs | 20 ++++++++++++++++++++ crates/collab-net/src/manage.rs | 8 +++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/crates/collab-net/src/limits.rs b/crates/collab-net/src/limits.rs index a0647fa6a..0321b36c1 100644 --- a/crates/collab-net/src/limits.rs +++ b/crates/collab-net/src/limits.rs @@ -24,6 +24,12 @@ pub struct NetLimits { pub hello_timeout: Duration, /// A connection with no traffic for this long is dead. pub idle_timeout: Duration, + /// How long a one-shot request connection (hosting, a library, a plugin directory — and, most + /// importantly, publishing a file) waits for the server's reply before giving up. Publishing + /// uploads a whole file and a large one on a slow link takes the server a while to receive and + /// answer, so this is longer than `idle_timeout` (a large project on a slow connection can + /// easily need several minutes). + pub request_reply_timeout: Duration, /// A quiet client pings this often (keeps it under `idle_timeout`). pub keepalive: Duration, /// Messages queued for one client before it counts as stuck and is disconnected. @@ -68,6 +74,7 @@ impl Default for NetLimits { burst: 400.0, hello_timeout: Duration::from_secs(10), idle_timeout: Duration::from_secs(45), + request_reply_timeout: Duration::from_secs(300), keepalive: Duration::from_secs(15), queue: 1024, write_timeout: Duration::from_secs(10), @@ -85,3 +92,16 @@ impl Default for NetLimits { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn request_reply_wait_is_longer_than_idle() { + let d = NetLimits::default(); + // Publishing uploads a whole file; a slow link must not cut it off at the session idle + // timeout. Keep the reply wait comfortably above it. + assert!(d.request_reply_timeout > d.idle_timeout); + } +} diff --git a/crates/collab-net/src/manage.rs b/crates/collab-net/src/manage.rs index 6e995bdb8..843f91cf4 100644 --- a/crates/collab-net/src/manage.rs +++ b/crates/collab-net/src/manage.rs @@ -196,13 +196,15 @@ pub fn plugins(server: &Endpoint, key: &str, req: PluginReq, limits: &NetLimits, Ok(r) } -/// A WebSocket to `server` that waits for answers up to the idle timeout (a large upload takes the -/// server a while to keep). +/// A WebSocket to `server` that waits for answers up to the request-reply timeout (a large upload +/// takes the server a while to keep). fn connect(server: &Endpoint, limits: &NetLimits, timeout: Duration) -> Result { let (stream, _, _) = open(server, timeout, limits)?; let (ws, _) = tungstenite::client::client_with_config(server.url(), stream, Some(config(limits.max_server_message))) .map_err(|e| format!("{} doesn't answer as a FigCraft server: {e}", server.display()))?; - ws.get_ref().set_read_timeout(Some(limits.idle_timeout)).map_err(|e| e.to_string())?; + // A publish uploads a whole file; a large project on a slow link can take several minutes to + // receive and answer. The wait uses `request_reply_timeout`, not the session `idle_timeout`. + ws.get_ref().set_read_timeout(Some(limits.request_reply_timeout)).map_err(|e| e.to_string())?; Ok(ws) } From 9a006175c81712020c0be2a8c6851c18c9b43739 Mon Sep 17 00:00:00 2001 From: Mojtaba Jafarzadeh Date: Sat, 24 Oct 2026 22:35:07 +0330 Subject: [PATCH 07/14] ui: server key field in the Share popover A server with a key rejects publishes without one, but Preferences' server address never holds a key. Add a "Server key" field to the Share popover's Advanced section and pass it to collab.share so a keyed server can be used from the popover without the Publish dialog. Co-Authored-By: Claude --- crates/engine/src/live_share_tests.rs | 17 +++++++++++++++++ crates/ui-egui/locales/en.toml | 1 + crates/ui-egui/locales/es.toml | 1 + crates/ui-egui/src/live.rs | 14 ++++++++++++++ 4 files changed, 33 insertions(+) diff --git a/crates/engine/src/live_share_tests.rs b/crates/engine/src/live_share_tests.rs index c52e1ccb0..f486250f0 100644 --- a/crates/engine/src/live_share_tests.rs +++ b/crates/engine/src/live_share_tests.rs @@ -272,6 +272,23 @@ fn sharing_online_publishes_to_the_server_in_preferences_and_shows_its_code() { assert_eq!((&r["status"], &r["code"], &r["share"]), (&json!("off"), &Value::Null, &Value::Null)); } +#[test] +fn sharing_online_uses_a_separate_key_param() { + // The Share popover sends the server key as its own field when the address (from Preferences, + // which never holds a key) needs one. It must reach `publish` and not leak into the summary. + let (mut s, script) = session(); + ex(&mut s, "pref.publishServer", json!({"url": "wss://share.example/"})); + let r = ex(&mut s, "collab.share", json!({"key": "KeyKeyKeyKeyKeyKeyKey1"})); + assert_eq!(r["status"], "connecting"); + { + let sc = script.lock().unwrap(); + let [p] = sc.published.as_slice() else { panic!("published {}", sc.published.len()) }; + assert_eq!(p.server, "wss://share.example/"); + assert_eq!(p.key.as_deref(), Some("KeyKeyKeyKeyKeyKeyKey1")); + } + assert!(!r.to_string().contains("KeyKey"), "the server's key is nowhere in the summary"); +} + #[test] fn a_server_without_join_codes_leaves_the_links() { let (mut s, script) = session(); diff --git a/crates/ui-egui/locales/en.toml b/crates/ui-egui/locales/en.toml index ae566fc81..b7b06be24 100644 --- a/crates/ui-egui/locales/en.toml +++ b/crates/ui-egui/locales/en.toml @@ -1220,6 +1220,7 @@ name = "English" "Server address (ws://host:47900/)" = "Server address (ws://host:47900/)" "Server address" = "Server address" "Server for online sharing" = "Server for online sharing" +"Server key" = "Server key" "Set as default" = "Set as default" "Set variable" = "Set variable" "Set variable mode" = "Set variable mode" diff --git a/crates/ui-egui/locales/es.toml b/crates/ui-egui/locales/es.toml index 596feda6e..678ebed73 100644 --- a/crates/ui-egui/locales/es.toml +++ b/crates/ui-egui/locales/es.toml @@ -1221,6 +1221,7 @@ status = "draft — needs native review" "Server address (ws://host:47900/)" = "Dirección del servidor (ws://host:47900/)" "Server address" = "Dirección del servidor" "Server for online sharing" = "Servidor para compartir en línea" +"Server key" = "Clave del servidor" "Set as default" = "Establecer como predeterminado" "Set variable" = "Establecer variable" "Set variable mode" = "Establecer modo de variable" diff --git a/crates/ui-egui/src/live.rs b/crates/ui-egui/src/live.rs index a4baa3034..9d896a294 100644 --- a/crates/ui-egui/src/live.rs +++ b/crates/ui-egui/src/live.rs @@ -98,6 +98,10 @@ pub struct LiveUi { pub advanced: bool, pub this_computer: bool, pub port: String, + /// The server key for online sharing (a server that needs one; empty: none). Passed to + /// `collab.share` so a keyed server can be used from the Share popover without the Publish + /// dialog. + pub share_key: String, /// What is typed in the join field of the Share popover and the home screen. pub code: String, /// The join started from the home screen: what it is doing shows under the field, and its @@ -746,6 +750,9 @@ fn start_ui(app: &mut FigApp, ui: &mut Ui, t: &Tokens) { if let (true, Ok(port)) = (local, app.ui.live.port.trim().parse::()) { params["port"] = json!(port); } + if !local && !app.ui.live.share_key.trim().is_empty() { + params["key"] = json!(app.ui.live.share_key.trim()); + } app.run("collab.share", params); } join_and_advanced(app, ui, t, |app, ui, t| { @@ -762,6 +769,13 @@ fn start_ui(app: &mut FigApp, ui: &mut Ui, t: &Tokens) { }); } else { widgets::label(ui, tr("Another server, or one that needs its key. The usual server is set in Preferences."), t.text_secondary); + // The server key (a server that needs one; the address in Preferences is without it). + ui.horizontal(|ui| { + widgets::label(ui, tr("Server key"), t.text); + if let Some(key) = widgets::text_field_named(ui, egui::Id::new("live_share_key"), &app.ui.live.share_key, 200.0, tr("Server key")) { + app.ui.live.share_key = key.trim().chars().filter(|c| !c.is_whitespace() && !c.is_control()).take(128).collect(); + } + }); if widgets::button(ui, tr("Publish to another server…"), false, true).clicked() { open_publish(app); ui.close(); From 726700aa2a1177eb676d76e6f578ad55c5a055ff Mon Sep 17 00:00:00 2001 From: Mojtaba Jafarzadeh Date: Sat, 24 Oct 2026 22:35:07 +0330 Subject: [PATCH 08/14] contributors: add Mojtaba Jafarzadeh's real name Co-Authored-By: Claude --- contributors/people.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contributors/people.toml b/contributors/people.toml index 273079350..e59755224 100644 --- a/contributors/people.toml +++ b/contributors/people.toml @@ -71,3 +71,6 @@ sync_github_name = true [people.AganzeFelicite] sync_github_name = true + +[people.mojtabajafarzadeh] +real_name = "Mojtaba Jafarzadeh" From 408b77e9ec19dbada4aebc9c23eb19c1628df6fc Mon Sep 17 00:00:00 2001 From: Patrik Lermon Date: Wed, 21 Oct 2026 17:21:08 +0200 Subject: [PATCH 09/14] i18n(sv): translate 38 new strings, use the agreed terms in 93 Brings the Swedish catalog up to date with upstream main (35da7b7c). Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/locales/sv.toml | 224 +++++++++++++++++++-------------- 1 file changed, 131 insertions(+), 93 deletions(-) diff --git a/crates/ui-egui/locales/sv.toml b/crates/ui-egui/locales/sv.toml index e4fff1a7c..c944b3550 100644 --- a/crates/ui-egui/locales/sv.toml +++ b/crates/ui-egui/locales/sv.toml @@ -14,10 +14,10 @@ name = "Svenska" "+ Create variable" = "+ Skapa variabel" ".fig (experimental)" = ".fig (experimentell)" "1…9, 0" = "1…9, 0" -"@{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{bin_added} / −{bin_deleted} binary assets, {first} – {last}" = "@{login}: {prs} PR:er, {commits} commits, +{added} / −{deleted} rader (Δ {delta}), +{bin_added} / −{bin_deleted} binära tillgångar, {first} – {last}" -"A collection of this file with these modes and variables: override their values per mode and add variables of its own" = "En samling i den här filen med dessa lägen och variabler: åsidosätt deras värden per läge och lägg till egna variabler" +"@{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{bin_added} / −{bin_deleted} binary assets, {first} – {last}" = "@{login}: {prs} PR:er, {commits} commits, +{added} / −{deleted} rader (Δ {delta}), +{bin_added} / −{bin_deleted} binära resurser, {first} – {last}" +"A collection of this file with these modes and variables: override their values per mode and add variables of its own" = "En samling i den här filen med dessa lägen och variabler: ange overrides för deras värden per läge och lägg till egna variabler" "A .figcraft file with this name already exists. Choose its full name to confirm replacement." = "Det finns redan en .figcraft-fil med det här namnet. Välj dess fullständiga namn för att bekräfta att den ersätts." -"A layer on its own aligns to its frame: select more layers, or one in a frame" = "Ett ensamt lager justeras mot sin ram: markera fler lager, eller ett lager i en ram" +"A layer on its own aligns to its frame: select more layers, or one in a frame" = "Ett ensamt lager justeras mot sin frame: markera fler lager, eller ett lager i en frame" "A responsive product page: hero, features, figures, a quote and a footer" = "En responsiv produktsida: toppsektion, funktioner, figurer, ett citat och en sidfot" "A spring plays until it settles: its mass, stiffness and damping set how long" = "En fjäder spelas tills den stannar: dess massa, styvhet och dämpning avgör hur länge" "A title, an agenda, a big figure, a chart and a closing slide" = "En titel, en agenda, en stor figur, ett diagram och en avslutande bild" @@ -34,14 +34,14 @@ name = "Svenska" "Add a category" = "Lägg till en kategori" "Add a comment" = "Lägg till en kommentar" "Add a description" = "Lägg till en beskrivning" -"Add a frame (F) or a shape (R) to get started" = "Lägg till en ram (F) eller en form (R) för att komma igång" +"Add a frame (F) or a shape (R) to get started" = "Lägg till en frame (F) eller en form (R) för att komma igång" "Add a layer or component to tile first" = "Lägg först till ett lager eller en komponent att lägga sida vid sida" "Add a note for developers" = "Lägg till en anteckning för utvecklare" "Add action" = "Lägg till åtgärd" "Add actions for when no condition holds" = "Lägg till åtgärder för när inget villkor uppfylls" "Add an annotation for developers" = "Lägg till en annotering för utvecklare" "Add another condition" = "Lägg till ett villkor till" -"Add auto layout" = "Lägg till automatisk layout" +"Add auto layout" = "Lägg till auto layout" "Add background behind overlay" = "Lägg till bakgrund bakom överlägg" "Add content to this slot" = "Lägg till innehåll i facket" "Add dev resource" = "Lägg till utvecklarresurs" @@ -92,6 +92,7 @@ name = "Svenska" "All components" = "Alla komponenter" "All fonts" = "Alla typsnitt" "All supported properties" = "Alla egenskaper som stöds" +"All the fonts this file uses are installed" = "Alla typsnitt som filen använder är installerade" "All variables" = "Alla variabler" "Along the bottom" = "Längs nederkanten" "Along the left" = "Längs vänsterkanten" @@ -132,7 +133,7 @@ name = "Svenska" "Arrow at the end" = "Pil i slutet" "As typed" = "Som inskrivet" "Asking the server…" = "Frågar servern…" -"Assets" = "Tillgångar" +"Assets" = "Resurser" "Aug" = "aug" "Auto" = "Auto" "Auto (space around)" = "Auto (utrymme runt om)" @@ -142,15 +143,15 @@ name = "Svenska" "Auto columns" = "Automatiska kolumner" "Auto gap" = "Automatiskt mellanrum" "Auto height" = "Automatisk höjd" -"Auto layout" = "Automatisk layout" +"Auto layout" = "Auto layout" "Auto rows" = "Automatiska rader" "Auto width" = "Automatisk bredd" "Back to the FigCraft server" = "Tillbaka till FigCraft-servern" -"Back to the font's value" = "Tillbaka till teckensnittets värde" +"Back to the font's value" = "Tillbaka till typsnittets värde" "Back" = "Tillbaka" "Back to Ready for dev" = "Tillbaka till Redo för utveckling" "Back to Ready for dev: the next change compares with how it is now" = "Tillbaka till Redo för utveckling: nästa ändring jämförs med hur det ser ut nu" -"Back to assets" = "Tillbaka till tillgångar" +"Back to assets" = "Tillbaka till resurser" "Back to current" = "Tillbaka till aktuell" "Back to styles" = "Tillbaka till formatmallar" "Back to the canvas from a focused region" = "Tillbaka till arbetsytan från en fokuserad region" @@ -178,6 +179,7 @@ name = "Svenska" "Both directions" = "Båda riktningarna" "Bottom" = "Nederkant" "Bottom left radius" = "Radie nere till vänster" +"Bottom padding" = "Nedre padding" "Bottom right radius" = "Radie nere till höger" "Branch" = "Gren" "Branch archived" = "Grenen har arkiverats" @@ -205,6 +207,7 @@ name = "Svenska" "CMYK colour" = "CMYK-färg" "Can view" = "Kan visa" "Check" = "Bock" +"Checking the fonts installed on this computer…" = "Kontrollerar vilka typsnitt som är installerade på den här datorn…" "Checking the server…" = "Kontrollerar servern…" "Chevron" = "Vinkel" "Clear from this list" = "Ta bort från listan" @@ -296,7 +299,7 @@ name = "Svenska" "Connection lost, reconnecting (attempt {attempt})…" = "Anslutningen bröts, återansluter (försök {attempt})…" "Connector" = "Koppling" "Constrain proportions" = "Behåll proportioner" -"Constraints" = "Begränsningar" +"Constraints" = "Constraints" "Content" = "Innehåll" "Contents hidden" = "Innehållet är dolt" "Contextual alternates" = "Kontextuella alternativ" @@ -304,6 +307,8 @@ name = "Svenska" "Contributors" = "Bidragsgivare" "Contributors: {people} · Commits: {commits}" = "Bidragsgivare: {people} · Commits: {commits}" "Controller" = "Handkontroll" +"Convert to frame" = "Konvertera till frame" +"Convert to section" = "Konvertera till avsnitt" "Cool" = "Kall" "Copied as CSS" = "Kopierat som CSS" "Copied as PNG" = "Kopierat som PNG" @@ -420,12 +425,12 @@ name = "Svenska" "Drafts" = "Utkast" "Drag the stroke to add a width point" = "Dra i konturen för att lägga till en breddpunkt" "Draw" = "Rita" -"Draw a frame" = "Rita en ram" +"Draw a frame" = "Rita en frame" "Drawing tools" = "Ritverktyg" "Drop a .figcraft, .fig, .json, .svg or image file." = "Släpp en .figcraft-, .fig-, .json- eller .svg-fil eller en bildfil." "Drop to import" = "Släpp för att importera" "Drop to open" = "Släpp för att öppna" -"Drop shadow" = "Släppskugga" +"Drop shadow" = "Slagskugga" "Dual-licensed MIT OR Apache-2.0. Made by the ArtCraft team and community." = "Dubbellicensierad under MIT OR Apache-2.0. Skapad av ArtCraft-teamet och gemenskapen." "Duo" = "Duo" "Duplicate" = "Duplicera" @@ -457,13 +462,13 @@ name = "Svenska" "End point" = "Slutpunkt" "Ends the session; everyone keeps their copy" = "Avslutar sessionen; alla behåller sin kopia" "Enter" = "Enter" -"Enter the address of a figcraft-server and press Browse to see the plugins it offers." = "Ange adressen till en figcraft-server och tryck på Bläddra för att se vilka plugin-program den erbjuder." +"Enter the address of a figcraft-server and press Browse to see the plugins it offers." = "Ange adressen till en figcraft-server och tryck på Bläddra för att se vilka plugins den erbjuder." "Enter to save · Esc to cancel" = "Enter för att spara · Esc för att avbryta" "Eraser" = "Suddgummi" "Esc" = "Esc" "Essential" = "Grundläggande" "Every conflict has a choice." = "Alla konflikter har ett val." -"Every top-level frame already starts a flow" = "Varje ram på översta nivån startar redan ett flöde" +"Every top-level frame already starts a flow" = "Varje frame på översta nivån startar redan ett flöde" "Exact opacity (4 then 5: 45%)" = "Exakt opacitet (4 och sedan 5: 45 %)" "Exclude" = "Uteslut" "Excluded" = "Utesluten" @@ -474,7 +479,7 @@ name = "Svenska" "Expand" = "Expandera" "Export" = "Exportera" "Export all" = "Exportera alla" -"Export frames and layers as PNG, JPG, SVG or PDF" = "Exportera ramar och lager som PNG, JPG, SVG eller PDF" +"Export frames and layers as PNG, JPG, SVG or PDF" = "Exportera frames och lager som PNG, JPG, SVG eller PDF" "Export page as PNG" = "Exportera sidan som PNG" "Export settings" = "Exportinställningar" "Export variables" = "Exportera variabler" @@ -542,17 +547,17 @@ name = "Svenska" "Follow {name}" = "Följ {name}" "Following" = "Följer" "Following {name} · {key} to stop" = "Följer {name} · {key} för att sluta" -"Font" = "Teckensnitt" -"Font not installed" = "Teckensnittet är inte installerat" +"Font" = "Typsnitt" +"Font not installed" = "Typsnittet är inte installerat" "Font size" = "Teckenstorlek" "Font style" = "Teckenstil" "Forget on this computer" = "Glöm på den här datorn" "Four screens of a trail app with components and a prototype flow" = "Fyra skärmar i en vandringsapp med komponenter och ett prototypflöde" "Fractions" = "Bråk" -"Frame" = "Ram" +"Frame" = "Frame" "Frame selection" = "Rama in markering" -"Frame tools" = "Ramverktyg" -"Frames" = "Ramar" +"Frame tools" = "Frame-verktyg" +"Frames" = "Frames" "From the library \"{name}\": read-only" = "Från biblioteket \"{name}\": skrivskyddad" "From {name}" = "Från {name}" "From {server}" = "Från {server}" @@ -609,7 +614,7 @@ name = "Svenska" "Horizontal" = "Horisontell" "Horizontal align" = "Horisontell justering" "Horizontal hex" = "Horisontell hex" -"Horizontal padding" = "Horisontell utfyllnad" +"Horizontal padding" = "Horisontell padding" "Horizontal spacing" = "Horisontellt avstånd" "Hosted by {operator}" = "Värd: {operator}" "Hover a layer to see its properties" = "Hovra över ett lager för att se dess egenskaper" @@ -622,7 +627,7 @@ name = "Svenska" "If it keeps happening, please report it at {link} with this message." = "Om det fortsätter att hända kan du rapportera det på {link} och bifoga det här meddelandet." "If" = "Om" "If {condition}" = "Om {condition}" -"Ignore auto layout (absolute position)" = "Ignorera automatisk layout (absolut position)" +"Ignore auto layout (absolute position)" = "Ignorera auto layout (absolut position)" "Ignore overlapping layers" = "Ignorera överlappande lager" "Image" = "Bild" "Images" = "Bilder" @@ -643,6 +648,7 @@ name = "Svenska" "Included" = "Inkluderad" "Included in layout" = "Inkluderad i layouten" "Individual corners" = "Enskilda hörn" +"Individual padding" = "Individuell padding" "Individual strokes" = "Enskilda konturer" "Ink" = "Bläck" "Inner shadow" = "Inre skugga" @@ -664,6 +670,7 @@ name = "Svenska" "Interaction" = "Interaktion" "Interactions" = "Interaktioner" "Interface design and prototyping. A free, open-source, clean-room design tool written from scratch in Rust, part of the ArtCraft Crafting Apps." = "Gränssnittsdesign och prototyper. Ett fritt clean-room-designverktyg med öppen källkod, skrivet från grunden i Rust och en del av ArtCraft Crafting Apps." +"Interface size" = "Gränssnittsstorlek" "Intersect" = "Skärning" "Intersects" = "Skär" "Invalid color" = "Ogiltig färg" @@ -714,6 +721,7 @@ name = "Svenska" "Leaves" = "Löv" "Left" = "Vänster" "Left & right" = "Vänster och höger" +"Left padding" = "Vänster padding" "Left side" = "Vänster sida" "Left side width" = "Bredd på vänster sida" "Letter case" = "Skiftläge" @@ -739,7 +747,7 @@ name = "Svenska" "Link" = "Länk" "Link copied" = "Länken har kopierats" "Link docs, code or tickets for developers." = "Länka dokumentation, kod eller ärenden för utvecklare." -"Link frames on the Prototype tab, then present the flow" = "Länka ramar på fliken Prototyp och presentera sedan flödet" +"Link frames on the Prototype tab, then present the flow" = "Länka frames på fliken Prototyp och presentera sedan flödet" "Link this component to its code component: instances then show your real snippet with their values." = "Länka den här komponenten till dess kodkomponent: instanserna visar då ditt riktiga kodavsnitt med sina värden." "List spacing" = "Listavstånd" "List style" = "Liststil" @@ -788,6 +796,7 @@ name = "Svenska" "Mirror angle and length" = "Spegla vinkel och längd" "Mirroring" = "Spegling" "Missing" = "Saknas" +"Missing fonts" = "Saknade typsnitt" "Missing layer" = "Lager saknas" "Missing style" = "Stil saknas" "Missing variable" = "Variabel saknas" @@ -828,7 +837,7 @@ name = "Svenska" "New board" = "Ny whiteboard" "New design file" = "Ny designfil" "Newest first" = "Nyaste först" -"Next frame" = "Nästa ram" +"Next frame" = "Nästa frame" "Next or previous control in the focused region" = "Nästa eller föregående kontroll i den fokuserade regionen" "Next ready for dev" = "Nästa redo för utveckling" "Next size option" = "Nästa storleksalternativ" @@ -847,6 +856,7 @@ name = "Svenska" "No conflicts." = "Inga konflikter." "No contributor data was built into this copy." = "Inga uppgifter om bidragsgivare finns inbyggda i den här kopian." "No device" = "Ingen enhet" +"No files or templates match your search" = "Inga filer eller mallar matchar sökningen" "No fonts match" = "Inga typsnitt matchar" "No join code here (the server is older than join codes, or this address can't be put in one): share the link instead." = "Ingen anslutningskod här (servern är äldre än anslutningskoder, eller så kan den här adressen inte göras om till en): dela länken i stället." "No layer changes to show." = "Inga lagerändringar att visa." @@ -861,9 +871,10 @@ name = "Svenska" "No model credits were built into this copy." = "Inga modellkrediteringar finns inbyggda i den här kopian." "No number variables fit here." = "Inga nummervariabler passar här." "No number variables yet. Create them in Local variables." = "Inga nummervariabler än. Skapa dem i Lokala variabler." +"No other pages" = "Inga andra sidor" "No paints" = "Inga färger" -"No plugins on {server}." = "Inga plugin-program på {server}." -"No plugins yet. Import a development plugin from its figcraft-plugin.json." = "Inga plugin-program än. Importera ett plugin-program under utveckling från dess figcraft-plugin.json." +"No plugins on {server}." = "Inga plugins på {server}." +"No plugins yet. Import a development plugin from its figcraft-plugin.json." = "Inga plugins än. Importera ett plugin under utveckling från dess figcraft-plugin.json." "No preferred components" = "Inga föredragna komponenter" "No preset" = "Ingen förinställning" "No saved versions yet. Save a version to mark a point in your work you can come back to." = "Inga sparade versioner än. Spara en version för att markera en punkt i arbetet som du kan återvända till." @@ -885,19 +896,19 @@ name = "Svenska" "Normal" = "Normal" "Not bound" = "Inte kopplad" "Not in a live session" = "Inte i en livesession" -"Not in this font" = "Finns inte i det här teckensnittet" +"Not in this font" = "Finns inte i det här typsnittet" "Not properties, kept as written: {names}" = "Inte egenskaper, behölls som de skrevs: {names}" "Not read yet" = "Inte läst än" "Not shared" = "Inte delad" "Note {n}" = "Anteckning {n}" "Nothing changed since this was marked ready for dev." = "Inget har ändrats sedan detta markerades som klart för utveckling." -"Nothing here yet. Set a frame's status to list it." = "Inget här än. Ange en rams status för att lista den." +"Nothing here yet. Set a frame's status to list it." = "Inget här än. Ange en frames status för att lista den." "Nothing online yet. Use Share on a file to put it online and it is listed here." = "Inget online än. Använd Dela på en fil för att lägga ut den online, så visas den här." "Nothing to export" = "Inget att exportera" "Nov" = "nov" "Now" = "Nu" -"Nudge (Shift: big nudge)" = "Knuffa (Shift: stor knuff)" -"Nudge amount" = "Knuffavstånd" +"Nudge (Shift: big nudge)" = "Flytta stegvis (Shift: stort steg)" +"Nudge amount" = "Steglängd" "Number" = "Nummer" "Number ↑" = "Nummer ↑" "Number ↓" = "Nummer ↓" @@ -944,6 +955,7 @@ name = "Svenska" "Options" = "Alternativ" "Orange" = "Orange" "Order" = "Ordning" +"Other" = "Annat" "Other apps may not open every .fig file exported here." = "Andra appar kanske inte kan öppna alla .fig-filer som exporteras här." "Others lose access and keep their copy; the file stays on the server for you" = "Andra förlorar åtkomst och behåller sin kopia; filen ligger kvar på servern åt dig" "Others type this under Share ▸ Join with a code" = "Andra skriver in den under Dela ▸ Anslut med en kod" @@ -955,12 +967,12 @@ name = "Svenska" "Overlay" = "Överlägg" "Overlay position" = "Överläggsposition" "Overridden here (the collection it extends has another value)" = "Åsidosatt här (samlingen som den utökar har ett annat värde)" -"Overrides" = "Åsidosättningar" +"Overrides" = "Overrides" "Owner token" = "Ägartoken" "Owner token copied" = "Ägartoken kopierad" "PDF print options" = "PDF-utskriftsalternativ" "PDF print settings" = "PDF-utskriftsinställningar" -"Padding" = "Utfyllnad" +"Padding" = "Padding" "Page" = "Sida" "Page deleted" = "Sidan har raderats" "Pages" = "Sidor" @@ -994,18 +1006,20 @@ name = "Svenska" "Phone" = "Telefon" "Pick color" = "Hämta färg" "Pick color (I)" = "Hämta färg (I)" -"Pick one font family first" = "Välj en teckensnittsfamilj först" -"Pick the Frame tool, then drag on the canvas or choose a device size" = "Välj ramverktyget och dra sedan på arbetsytan eller välj en enhetsstorlek" +"Pick one font family first" = "Välj en typsnittsfamilj först" +"Pick the Frame tool, then drag on the canvas or choose a device size" = "Välj verktyget Frame och dra sedan på arbetsytan eller välj en enhetsstorlek" "Pin a property" = "Fäst en egenskap" "Pinch" = "Nyp" "Pixel grid" = "Pixelrutnät" "Pixel preview 1x" = "Pixelförhandsgranskning 1x" "Pixel preview 2x" = "Pixelförhandsgranskning 2x" -"Place a shape or frame (its tool chosen)" = "Placera en form eller ram (när dess verktyg är valt)" +"Place a shape or frame (its tool chosen)" = "Placera en form eller frame (när dess verktyg är valt)" "Place automatically" = "Placera automatiskt" "Place image" = "Placera bild" -"Plugins run in FigCraft's sandbox, without files, network or clock. Install only plugins you trust." = "Plugin-program körs i FigCrafts sandlåda, utan filer, nätverk eller klocka. Installera bara plugin-program du litar på." -"Plugins" = "Plugin-program" +"Plugin" = "Plugin" +"Plugins & widgets" = "Plugins och widgetar" +"Plugins run in FigCraft's sandbox, without files, network or clock. Install only plugins you trust." = "Plugins körs i FigCrafts sandlåda, utan filer, nätverk eller klocka. Installera bara plugins du litar på." +"Plugins" = "Plugins" "Plus darker" = "Plus mörkare" "Plus lighter" = "Plus ljusare" "Plus one" = "Plus ett" @@ -1026,8 +1040,8 @@ name = "Svenska" "Presenting: comments" = "Presentation: kommentarer" "Presenting: cycle the size options" = "Presentation: växla mellan storleksalternativen" "Presenting: full screen" = "Presentation: helskärm" -"Presenting: next frame" = "Presentation: nästa ram" -"Presenting: previous frame" = "Presentation: föregående ram" +"Presenting: next frame" = "Presentation: nästa frame" +"Presenting: previous frame" = "Presentation: föregående frame" "Presenting: restart" = "Presentation: starta om" "Preserve scroll position" = "Bevara rullningsposition" "Presets" = "Förinställningar" @@ -1035,8 +1049,8 @@ name = "Svenska" "Playground" = "Testyta" "Preview" = "Förhandsgranskning" "Preview this flow" = "Förhandsgranska det här flödet" -"Preview this frame" = "Förhandsgranska den här ramen" -"Previous frame" = "Föregående ram" +"Preview this frame" = "Förhandsgranska den här framen" +"Previous frame" = "Föregående frame" "Previous ready for dev" = "Föregående redo för utveckling" "Progressive" = "Progressiv" "Properties" = "Egenskaper" @@ -1075,10 +1089,11 @@ name = "Svenska" "Reduce motion" = "Minska rörelse" "Refraction" = "Brytning" "Regular expression" = "Reguljärt uttryck" +"Relative to your system's display scaling (100% is the system's own size). Document zoom is separate." = "Relativt systemets skalning av skärmen (100 % är systemets egen storlek). Dokumentets zoom är separat." "Remove" = "Ta bort" "Remove all" = "Ta bort alla" "Remove annotation" = "Ta bort anteckning" -"Remove auto layout" = "Ta bort automatisk layout" +"Remove auto layout" = "Ta bort auto layout" "Remove dev resource" = "Ta bort utvecklarresurs" "Remove export setting" = "Ta bort exportinställning" "Remove from recents" = "Ta bort från Senaste" @@ -1114,6 +1129,7 @@ name = "Svenska" "Rename on the server…" = "Byt namn på servern…" "Rename page" = "Byt namn på sida" "Rename to" = "Nytt namn" +"Reorder selection" = "Ändra ordning på markeringen" "Replace all" = "Ersätt alla" "Replace mixed export settings" = "Ersätt blandade exportinställningar" "Replace with" = "Ersätt med" @@ -1124,14 +1140,14 @@ name = "Svenska" "Reset" = "Återställ" "Reset adjustments" = "Återställ justeringar" "Reset all changes" = "Återställ alla ändringar" -"Reset all overrides on this layer" = "Återställ alla åsidosättningar på det här lagret" -"Reset overrides" = "Återställ åsidosättningar" +"Reset all overrides on this layer" = "Återställ alla overrides på det här lagret" +"Reset overrides" = "Återställ overrides" "Reset slot" = "Återställ plats" -"Reset this override" = "Återställ den här åsidosättningen" +"Reset this override" = "Återställ den här overriden" "Reset to style" = "Återställ till formatmall" "Reset to the inherited value" = "Återställ till ärvt värde" -"Resize by the nudge amount" = "Ändra storlek med knuffavståndet" -"Resize frames to the device's screen (or the window without a device): their constraints and auto layout respond" = "Ändra storlek på ramar efter enhetens skärm (eller fönstret utan enhet): deras begränsningar och automatiska layout anpassas" +"Resize by the nudge amount" = "Ändra storlek med steglängden" +"Resize frames to the device's screen (or the window without a device): their constraints and auto layout respond" = "Ändra storlek på frames efter enhetens skärm (eller fönstret utan enhet): deras constraints och auto layout anpassas" "Resize to fit" = "Anpassa storlek" "Resizing" = "Storleksändring" "Resolve" = "Lös" @@ -1149,6 +1165,7 @@ name = "Svenska" "Review and merge" = "Granska och sammanfoga" "Ribbon" = "Verktygspanel" "Right" = "Höger" +"Right padding" = "Höger padding" "Right side" = "Höger sida" "Right side width" = "Bredd på höger sida" "Rotate 90°" = "Rotera 90°" @@ -1170,6 +1187,9 @@ name = "Svenska" "Run" = "Kör" "Run {label}" = "Kör {label}" "Save files automatically" = "Spara filer automatiskt" +"Search files and templates" = "Sök efter filer och mallar" +"Search pages" = "Sök sidor" +"Search results" = "Sökresultat" "Search templates" = "Sök mallar" "See all templates" = "Se alla mallar" "Select layers and they space themselves as their content changes" = "Markera lager så fördelar de avståndet själva när innehållet ändras" @@ -1191,10 +1211,10 @@ name = "Svenska" "Scroll to" = "Rulla till" "Scroll to layer" = "Rulla till lager" "Scroll with parent" = "Rulla med överordnat element" -"Search plugins" = "Sök plugin-program" +"Search plugins" = "Sök plugins" "Search" = "Sök" "Search actions, layers and tools" = "Sök åtgärder, lager och verktyg" -"Search assets" = "Sök tillgångar" +"Search assets" = "Sök resurser" "Search comments" = "Sök i kommentarer" "Search fonts" = "Sök typsnitt" "Search layers" = "Sök lager" @@ -1202,8 +1222,8 @@ name = "Svenska" "Section" = "Avsnitt" "Section color" = "Avsnittsfärg" "Sections" = "Avsnitt" -"Select a frame and add a starting point to present it as a flow." = "Markera en ram och lägg till en startpunkt för att presentera den som ett flöde." -"Select a frame to preview it" = "Markera en ram för att förhandsgranska den" +"Select a frame and add a starting point to present it as a flow." = "Markera en frame och lägg till en startpunkt för att presentera den som ett flöde." +"Select a frame to preview it" = "Markera en frame för att förhandsgranska den" "Select a layer to see its properties and code" = "Markera ett lager för att se dess egenskaper och kod" "Select all" = "Markera alla" "Select all with same" = "Markera alla med samma" @@ -1350,16 +1370,16 @@ name = "Svenska" "Subtract" = "Subtrahera" "Subtracts" = "Subtraktioner" "Suffix" = "Suffix" -"Suspend snapping while dragging" = "Stäng av fästning medan du drar" +"Suspend snapping while dragging" = "Stäng av fästfunktionen medan du drar" "Swap instance" = "Byt instans" -"Swap library: use another library's assets of the same names" = "Byt bibliotek: använd tillgångar med samma namn från ett annat bibliotek" +"Swap library: use another library's assets of the same names" = "Byt bibliotek: använd resurser med samma namn från ett annat bibliotek" "Swap orientation" = "Växla orientering" "Swap overlay" = "Byt överlägg" "Swap to {name}" = "Byt till {name}" "Swap width and height" = "Byt plats på bredd och höjd" "Swapped instance" = "Utbytt instans" -"Swapped {n} assets" = { one = "Bytte {n} tillgång", other = "Bytte {n} tillgångar" } -"Swapped {n} assets; {missed} had no match and still use the old library" = { one = "Bytte {n} tillgång; {missed} saknade motsvarighet och använder fortfarande det gamla biblioteket", other = "Bytte {n} tillgångar; {missed} saknade motsvarighet och använder fortfarande det gamla biblioteket" } +"Swapped {n} assets" = { one = "Bytte {n} resurs", other = "Bytte {n} resurser" } +"Swapped {n} assets; {missed} had no match and still use the old library" = { one = "Bytte {n} resurs; {missed} saknade motsvarighet och använder fortfarande det gamla biblioteket", other = "Bytte {n} resurser; {missed} saknade motsvarighet och använder fortfarande det gamla biblioteket" } "Sweep" = "Svep" "System" = "Enligt systemet" "Tab / Shift+Tab" = "Tab / Shift+Tab" @@ -1395,6 +1415,7 @@ name = "Svenska" "The selection uses several text styles: click to apply one" = "Markeringen använder flera textstilar: klicka för att använda en" "The server deletes this file if nobody opens it before {date}." = "Servern raderar filen om ingen öppnar den före {date}." "The whole file" = "Hela filen" +"The window is too small for {size}%, so the interface is drawn at {drawn}%. Make the window larger to use {size}%." = "Fönstret är för litet för {size} %, så gränssnittet ritas i {drawn} %. Gör fönstret större för att använda {size} %." "Theme" = "Tema" "There's no destination for that action" = "Det finns inget mål för den åtgärden" "Thickness" = "Tjocklek" @@ -1406,8 +1427,10 @@ name = "Svenska" "This file" = "Den här filen" "This file has unsaved changes" = "Filen har osparade ändringar" "This file is on {server}, open only to you. Share it to let others in with a new code." = "Filen ligger på {server} och är bara öppen för dig. Dela den för att släppa in andra med en ny kod." -"This frame is new on the branch." = "Den här ramen är ny på grenen." -"This frame isn't on the branch." = "Den här ramen finns inte på grenen." +"This file is too large to join here (up to {mb} MB)" = "Den här filen är för stor för att läggas till här (högst {mb} MB)" +"This file uses fonts that aren't installed on this computer. Their text is drawn in a fallback font until you install them." = "Den här filen använder typsnitt som inte är installerade på den här datorn. Deras text ritas med ett reservtypsnitt tills du installerar dem." +"This frame is new on the branch." = "Den här framen är ny på grenen." +"This frame isn't on the branch." = "Den här framen finns inte på grenen." "This server is open to the public: no key needed." = "Servern är öppen för alla: ingen nyckel behövs." "This server needs its key: add ?key=… to the address (`figcraft-server key` prints it on the server's machine)." = "Servern kräver sin nyckel: lägg till ?key=… i adressen (`figcraft-server key` skriver ut den på serverns dator)." "This server takes files only with its key: see Advanced." = "Servern tar bara emot filer med sin nyckel: se Avancerat." @@ -1427,6 +1450,7 @@ name = "Svenska" "Top" = "Överkant" "Top & bottom" = "Över- och nederkant" "Top left radius" = "Radie uppe till vänster" +"Top padding" = "Övre padding" "Top right radius" = "Radie uppe till höger" "Transform" = "Transformera" "Triangle" = "Triangel" @@ -1437,6 +1461,7 @@ name = "Svenska" "Truncate with an ellipsis" = "Korta av med ellips" "Try again" = "Försök igen" "Try another search or filter." = "Prova en annan sökning eller ett annat filter." +"Try another search, or browse templates." = "Prova en annan sökning eller bläddra bland mallar." "Try another word, or look at every kind." = "Prova ett annat ord, eller visa alla typer." "Turn a layer into a component, reuse it, and change every copy at once" = "Gör om ett lager till en komponent, återanvänd den och ändra alla kopior på en gång" "Two digits" = "Två siffror" @@ -1471,10 +1496,11 @@ name = "Svenska" "Use a fixed value" = "Använd ett fast värde" "Use an expression, e.g. count + 1" = "Använd ett uttryck, t.ex. count + 1" "Use as mask" = "Använd som mask" -"Use auto layout" = "Använd automatisk layout" +"Use auto layout" = "Använd auto layout" "Use number keys for opacity" = "Använd siffertangenterna för opacitet" "Use the .figcraft or .fig extension." = "Använd filändelsen .figcraft eller .fig." "Use the layer's fill" = "Använd lagrets fyllning" +"Use your browser's zoom to change the interface size." = "Använd webbläsarens zoom för att ändra gränssnittets storlek." "Used in {files} · {instances}" = "Används i {files} · {instances}" "Username" = "Användarnamn" "Value" = "Värde" @@ -1496,7 +1522,7 @@ name = "Svenska" "Vertical" = "Vertikal" "Vertical align" = "Vertikal justering" "Vertical hex" = "Vertikal hex" -"Vertical padding" = "Vertikal utfyllnad" +"Vertical padding" = "Vertikal padding" "Vertical spacing" = "Vertikalt avstånd" "Vertical trim" = "Vertikal trimning" "View" = "Visa" @@ -1523,12 +1549,13 @@ name = "Svenska" "What changed since it was marked ready for dev" = "Vad som har ändrats sedan den markerades som klar för utveckling" "What is wrong with this file? The server's operator gets your report." = "Vad är fel med den här filen? Serverns operatör får din rapport." "What it's for" = "Vad den är till för" -"What this changed from isn't known: it changed before FigCraft kept a copy of ready-for-dev frames. Resolve the change (or mark it ready again) to compare from now on." = "Vad detta ändrades från är okänt: ändringen gjordes innan FigCraft började spara en kopia av ramar som är klara för utveckling. Lös ändringen (eller markera den som klar igen) för att kunna jämföra från och med nu." +"What this changed from isn't known: it changed before FigCraft kept a copy of ready-for-dev frames. Resolve the change (or mark it ready again) to compare from now on." = "Vad detta ändrades från är okänt: ändringen gjordes innan FigCraft började spara en kopia av frames som är klara för utveckling. Lös ändringen (eller markera den som klar igen) för att kunna jämföra från och med nu." "While hovering" = "Vid hovring" "While pressing" = "Vid nedtryckning" "White" = "Vit" "Who runs it, its terms and how long it keeps files" = "Vem som driver den, dess villkor och hur länge den sparar filer" "Why it changed" = "Varför det ändrades" +"Widget" = "Widget" "Widgets" = "Widgetar" "Width" = "Bredd" "Width point" = "Breddpunkt" @@ -1579,7 +1606,7 @@ name = "Svenska" "connector label was skipped: text style data is missing" = "etiketten på en kopplingslinje hoppades över: textens stildata saknas" "connector label was skipped: text transform is invalid" = "etiketten på en kopplingslinje hoppades över: textens transformering är ogiltig" "connector labels were skipped" = "etiketter på kopplingslinjer hoppades över" -"connector layers import as static frame/vector snapshots with labels when present; live endpoint bindings are not retained" = "kopplingslinjelager importeras som statiska ögonblicksbilder av ramar eller vektorer, med etiketter om de finns; dynamiska bindningar av ändpunkterna behålls inte" +"connector layers import as static frame/vector snapshots with labels when present; live endpoint bindings are not retained" = "kopplingslinjelager importeras som statiska ögonblicksbilder av frames eller vektorer, med etiketter om de finns; dynamiska bindningar av ändpunkterna behålls inte" "connector snapshot vector data is missing; connector was left empty" = "vektordata för kopplingslinjens ögonblicksbild saknas; kopplingslinjen lämnades tom" "connectors whose ends can't be placed are empty" = "kopplingslinjer vars ändar inte kan placeras är tomma" "crop marks" = "skärmärken" @@ -1604,11 +1631,11 @@ name = "Svenska" "image filters are ambiguous because both filter containers are present; filter conversion was skipped" = "bildfiltren är tvetydiga eftersom båda filterbehållarna finns; filterkonverteringen hoppades över" "image filters use FigCraft's approximate renderer" = "bildfilter använder FigCrafts ungefärliga rendering" "instance swap properties whose default component isn't in the file were skipped" = "egenskaper för instansbyte vars standardkomponent inte finns i filen hoppades över" -"instances inside their own main component were imported as frames" = "instanser inuti sin egen huvudkomponent importerades som ramar" +"instances inside their own main component were imported as frames" = "instanser inuti sin egen huvudkomponent importerades som frames" "instances of components that aren't in the file show no layers" = "instanser av komponenter som inte finns i filen visar inga lager" -"instances of components that aren't in the file were imported as frames" = "instanser av komponenter som inte finns i filen importerades som ramar" -"instances that would nest a component inside itself were imported as frames" = "instanser som skulle nästla en komponent i sig själv importerades som ramar" -"instances without a main component were imported as empty frames" = "instanser utan huvudkomponent importerades som tomma ramar" +"instances of components that aren't in the file were imported as frames" = "instanser av komponenter som inte finns i filen importerades som frames" +"instances that would nest a component inside itself were imported as frames" = "instanser som skulle nästla en komponent i sig själv importerades som frames" +"instances without a main component were imported as empty frames" = "instanser utan huvudkomponent importerades som tomma frames" "invalid internal .fig text path data was ignored" = "ogiltiga interna .fig-data för text på bana ignorerades" "invalid or oversized vector networks import as outlines" = "ogiltiga eller för stora vektornätverk importeras som former" "invalid or oversized vector path data was skipped" = "ogiltiga eller för stora vektorbanedata hoppades över" @@ -1626,6 +1653,7 @@ name = "Svenska" "nobody else here yet" = "ingen annan här än" "non-default slot settings are not represented and were skipped" = "fackinställningar som avviker från standard återges inte och hoppades över" "now" = "nu" +"only in a text style or variable" = "endast i en textstil eller variabel" "or" = "eller" "point stroke joins were preserved; dashed or variable-width strokes render with the layer join" = "punkternas hörntyper behölls; streckade konturer och konturer med variabel bredd ritas med lagrets hörntyp" "preferred slot component keys cannot be resolved to file nodes and were skipped" = "nycklar för föredragna komponenter i fack kan inte kopplas till lager i filen och hoppades över" @@ -1648,13 +1676,14 @@ name = "Svenska" "set-variable-mode actions on collections or modes that aren't in the variables JSON were skipped" = "åtgärder för att ange variabelläge på samlingar eller lägen som inte finns i variabel-JSON:en hoppades över" "slot content assignment target is missing or is not slot content" = "målet för tilldelningen av fackinnehåll saknas eller är inte fackinnehåll" "slot content assignment uses the empty GUID sentinel and was skipped" = "tilldelningen av fackinnehåll använder det tomma GUID:t som markörvärde och hoppades över" +"slot layers without a matching slot property were imported as frames" = "facklager utan en motsvarande fackegenskap importerades som frames" "some vector points refer to point settings missing from the file; they use the layer's joins and plain corners" = "vissa vektorpunkter hänvisar till punktinställningar som saknas i filen; de använder lagrets hörntyper och vanliga hörn" "some vector segments refer to style settings missing from the file; they use the layer's stroke" = "vissa vektorsegment hänvisar till stilinställningar som saknas i filen; de använder lagrets kontur" "stale or invalid grid track sizing entries were ignored" = "inaktuella eller ogiltiga storleksposter för rutnätsspår ignorerades" "stars and polygons without point counts or geometry use the default point count" = "stjärnor och polygoner utan antal punkter eller geometri använder standardantalet punkter" "style settings of some vector segments aren't supported; those segments use the layer's stroke" = "stilinställningar för vissa vektorsegment stöds inte; de segmenten använder lagrets kontur" "text path underline or strikethrough decoration is not rendered" = "understrykning eller genomstrykning av text på bana ritas inte" -"text style overrides refer to missing table entries" = "åsidosättningar av textstilar hänvisar till tabellposter som saknas" +"text style overrides refer to missing table entries" = "overrides av textstilar hänvisar till tabellposter som saknas" "the file has no pages" = "filen har inga sidor" "the plugin" = "pluginet" "the variables JSON has no `variableCollections` (expected the local variables response) and was ignored" = "variabel-JSON:en har inga `variableCollections` (svaret med lokala variabler väntades) och ignorerades" @@ -1734,6 +1763,7 @@ name = "Svenska" "{n} replies" = { one = "{n} svar", other = "{n} svar" } "{n} rows" = { one = "{n} rad", other = "{n} rader" } "{n} styles" = { one = "{n} formatmall", other = "{n} formatmallar" } +"{n} text layers" = { one = "{n} textlager", other = "{n} textlager" } "{n} variables" = { one = "{n} variabel", other = "{n} variabler" } "{n} weeks ago" = { one = "för {n} vecka sedan", other = "för {n} veckor sedan" } "{n} years ago" = { one = "för {n} år sedan", other = "för {n} år sedan" } @@ -1742,6 +1772,7 @@ name = "Svenska" "{n}m" = "{n} min" "{n}w" = "{n} v" "{page}, nothing selected" = "{page}, inget markerat" +"{percent}%" = "{percent} %" "{profile} (current)" = "{profile} (aktuell)" "{property} is its value; {property ? \"a\" : \"b\"} picks a string. Other {…} stays as written." = "{property} är dess värde; {property ? \"a\" : \"b\"} väljer en sträng. Andra {…} lämnas som de är skrivna." "{server} can't say what became of your files (it is older than this app)." = "{server} kan inte säga vad som hände med dina filer (den är äldre än den här appen)." @@ -1785,6 +1816,7 @@ name = "Svenska" "arrange.front" = "Flytta längst fram" "arrange.packH" = "Packa horisontellt" "arrange.packV" = "Packa vertikalt" +"arrange.reorder" = "Ändra ordning på markeringen" "arrange.roundToPixels" = "Avrunda till pixlar" "arrange.spacing" = "Avstånd" "arrange.tidy" = "Snygga till" @@ -1868,7 +1900,7 @@ name = "Svenska" "component.pushChanges" = "Skicka ändringar till huvudkomponenten" "component.removeSlot" = "Ta bort plats" "component.resetInstance" = "Återställ alla ändringar" -"component.resetOverride" = "Återställ åsidosättning" +"component.resetOverride" = "Återställ override" "component.restoreMain" = "Återställ huvudkomponent" "dev.compare" = "Jämför ändringar" "dev.focus" = "Fokusvy" @@ -1920,7 +1952,7 @@ name = "Svenska" "file.colorProfile" = "Färgprofil" "file.duplicate" = "Duplicera fil" "file.export" = "Exportera" -"file.exportFramesPdf" = "Exportera ramar som PDF" +"file.exportFramesPdf" = "Exportera frames som PDF" "file.exportLayers" = "Exportera lager" "file.exportSvg" = "Exportera som SVG" "file.import" = "Importera" @@ -1943,8 +1975,8 @@ name = "Svenska" "fill.pattern" = "Mönster" "fill.remove" = "Ta bort fyllning" "fill.set" = "Ange fyllning" -"frame.create" = "Skapa ram" -"frame.presets" = "Ramförinställningar" +"frame.create" = "Skapa frame" +"frame.presets" = "Förinställningar för frames" "frame.swapOrientation" = "Växla orientering" "grid.add" = "Lägg till rutnät" "grid.move" = "Flytta rutnät" @@ -1963,7 +1995,7 @@ name = "Svenska" "image.place" = "Placera bild" "image.presets" = "Bildförinställningar" "image.savePreset" = "Spara förinställning" -"inspect.assets" = "Tillgångar" +"inspect.assets" = "Resurser" "inspect.code" = "Kod" "inspect.componentCode" = "Komponentkod" "inspect.properties" = "Egenskaper" @@ -1977,8 +2009,8 @@ name = "Svenska" "instance.swap" = "Byt instans" "instance.swapAt" = "Byt instans vid position" "layers.collapseAll" = "Fäll ihop alla" -"layout.add" = "Lägg till automatisk layout" -"layout.remove" = "Ta bort automatisk layout" +"layout.add" = "Lägg till auto layout" +"layout.remove" = "Ta bort auto layout" "layout.setTrack" = "Ange spår" "layout.suggest" = "Föreslå layout" "libraries.open" = "Öppna bibliotek" @@ -2002,15 +2034,16 @@ name = "Svenska" "library.updates" = "Biblioteksuppdateringar" "library.usage" = "Biblioteksanvändning" "measure.distances" = "Mät avstånd" -"measure.padding" = "Mät utfyllnad" +"measure.padding" = "Mät padding" "node.create" = "Skapa nod" "node.get" = "Hämta nod" "node.reorder" = "Ändra ordning" "node.reparent" = "Byt överordnad nod" "node.set" = "Ange nodegenskaper" "object.batchRename" = "Byt namn på flera" -"object.convertToFrame" = "Konvertera till ram" +"object.convertToFrame" = "Konvertera till frame" "object.convertToGroup" = "Konvertera till grupp" +"object.convertToSection" = "Konvertera till avsnitt" "object.flipH" = "Vänd vågrätt" "object.flipV" = "Vänd lodrätt" "object.frameSelection" = "Rama in markering" @@ -2022,7 +2055,7 @@ name = "Svenska" "object.rasterize" = "Rastrera" "object.releaseMask" = "Frigör mask" "object.removeFill" = "Ta bort fyllning" -"object.removeFrame" = "Ta bort ram" +"object.removeFrame" = "Ta bort frame" "object.removeStroke" = "Ta bort kontur" "object.rename" = "Byt namn" "object.resizeToFit" = "Anpassa storlek" @@ -2051,23 +2084,24 @@ name = "Svenska" "plugin.directory.unpublish" = "Avpublicera plugin från katalogen" "plugin.event" = "Använd kontroll i plugin-panelen" "plugin.import" = "Importera plugin från manifest…" -"plugin.list" = "Lista plugin-program" +"plugin.list" = "Lista plugins" "plugin.remove" = "Ta bort plugin" "plugin.run" = "Kör plugin" "plugin.ui" = "Plugin-panel" -"plugins.manage" = "Hantera plugin-program…" +"plugins.manage" = "Hantera plugins…" "plugins.runLast" = "Kör senaste plugin" "pref.autoSave" = "Spara filer automatiskt" "pref.highlightHover" = "Framhäv lager vid hovring" "pref.keepTool" = "Behåll verktyget markerat efter användning" "pref.language" = "Gränssnittsspråk" -"pref.nudge" = "Knuffavstånd" +"pref.nudge" = "Steglängd" "pref.numberKeysOpacity" = "Använd siffertangenterna för opacitet" "pref.publishServer" = "Server för delning online" "pref.reduceMotion" = "Minska rörelse" "pref.renameDuplicates" = "Byt namn på duplicerade lager" "pref.showDimensions" = "Visa dimensioner på objekt" "pref.startOnHome" = "Börja på Start" +"pref.uiScale" = "Gränssnittsstorlek" "pref.userName" = "Ditt namn i kommentarer" "proto.action.add" = "Lägg till prototypåtgärd" "proto.action.move" = "Ändra ordning på prototypåtgärd" @@ -2105,7 +2139,7 @@ name = "Svenska" "select.remove" = "Ta bort från markering" "select.sameEffect" = "Markera samma effekt" "select.sameFill" = "Markera samma fyllning" -"select.sameFont" = "Markera samma teckensnitt" +"select.sameFont" = "Markera samma typsnitt" "select.sameInstance" = "Markera samma instans" "select.sameProperties" = "Markera samma egenskaper" "select.sameStroke" = "Markera samma kontur" @@ -2168,7 +2202,7 @@ name = "Svenska" "text.edit.selectWord" = "Markera ord" "text.edit.selection" = "Textmarkering" "text.endPreview" = "Avsluta förhandsgranskning av text" -"text.fonts" = "Lista teckensnitt" +"text.fonts" = "Lista typsnitt" "text.italic" = "Kursiv" "text.lineHeightDown" = "Minska radhöjden" "text.lineHeightUp" = "Öka radhöjden" @@ -2176,17 +2210,21 @@ name = "Svenska" "text.listIndent" = "Öka listindrag" "text.listOutdent" = "Minska listindrag" "text.listStyle" = "Liststil" +"text.loadFont" = "Läs in typsnittsfil" "text.maxLines" = "Max antal rader" +"text.missingFonts" = "Saknade typsnitt" "text.numberList" = "Numrerad lista" "text.onPath" = "Text på bana" "text.paragraphIndent" = "Styckeindrag" "text.paragraphSpacing" = "Styckeavstånd" "text.pathOptions" = "Alternativ för textbana" "text.preview" = "Förhandsgranska textändring" +"text.refit" = "Anpassa text till typsnitt" "text.removeFromPath" = "Ta bort text från bana" +"text.rescanFonts" = "Sök igenom installerade typsnitt igen" "text.set" = "Ange text" "text.setFeatures" = "OpenType-funktioner" -"text.setVariations" = "Axlar för variabla teckensnitt" +"text.setVariations" = "Axlar för variabla typsnitt" "text.sizeDown" = "Minska teckenstorleken" "text.sizeUp" = "Öka teckenstorleken" "text.strikethrough" = "Genomstruken" @@ -2201,7 +2239,7 @@ name = "Svenska" "tool.connector" = "Koppling" "tool.ellipse" = "Ellips" "tool.eraser" = "Suddgummi" -"tool.frame" = "Ram" +"tool.frame" = "Frame" "tool.hand" = "Hand" "tool.highlighter" = "Överstrykningspenna" "tool.line" = "Linje" @@ -2226,8 +2264,8 @@ name = "Svenska" "tool.text" = "Text" "transform.move" = "Flytta" "transform.moveTo" = "Flytta till" -"transform.nudge" = "Knuffa" -"transform.nudgeSize" = "Ändra storlek genom att knuffa" +"transform.nudge" = "Flytta stegvis" +"transform.nudgeSize" = "Ändra storlek stegvis" "transform.resize" = "Ändra storlek" "transform.rotate" = "Rotera" "transform.rotationOrigin" = "Flytta rotationspunkt" @@ -2252,7 +2290,7 @@ name = "Svenska" "variable.setMode" = "Ange variabelläge" "variable.update" = "Redigera variabel" "vector.create" = "Vektor" -"vector.deleteHeal" = "Radera och läk markering" +"vector.deleteHeal" = "Radera och retuschera markering" "vector.edit.addVertex" = "Lägg till punkt" "vector.edit.begin" = "Redigera vektor" "vector.edit.bend" = "Böj segment" @@ -2267,7 +2305,7 @@ name = "Svenska" "vector.edit.mirroring" = "Handtagsspegling" "vector.edit.moveHandles" = "Flytta handtag" "vector.edit.moveVertices" = "Flytta punkter" -"vector.edit.nudge" = "Knuffa punkter" +"vector.edit.nudge" = "Flytta punkter stegvis" "vector.edit.paint" = "Färghink" "vector.edit.pen" = "Pennpunkt" "vector.edit.penEnd" = "Avsluta pennbana" @@ -2301,13 +2339,13 @@ name = "Svenska" "version.preview" = "Visa version" "version.rename" = "Byt namn på version" "version.restore" = "Återställ version" -"view.assets" = "Panelen Tillgångar" +"view.assets" = "Panelen Resurser" "view.comments" = "Kommentarer" "view.contextMenu" = "Visa snabbmeny" "view.design" = "Panelen Design" "view.designPrototype" = "Växla mellan Design och Prototyp" "view.devMode" = "Utvecklarläge" -"view.frameOutlines" = "Konturlinjer för ramar" +"view.frameOutlines" = "Konturlinjer för frames" "view.fullscreen" = "Helskärm" "view.get" = "Hämta vy" "view.layers" = "Panelen Lager" @@ -2315,7 +2353,7 @@ name = "Svenska" "view.leftSidebar" = "Visa/dölj vänster sidofält" "view.maskOutlines" = "Konturlinjer för masker" "view.multiplayerCursors" = "Samarbetsmarkörer" -"view.nextFrame" = "Zooma till nästa ram" +"view.nextFrame" = "Zooma till nästa frame" "view.nextPage" = "Nästa sida" "view.nextRegion" = "Fokusera nästa region" "view.outlines" = "Konturlinjer" @@ -2326,7 +2364,7 @@ name = "Svenska" "view.panelWidths" = "Återställ panelbredder" "view.pixelGrid" = "Pixelrutnät" "view.pixelPreview" = "Pixelförhandsgranskning" -"view.prevFrame" = "Zooma till föregående ram" +"view.prevFrame" = "Zooma till föregående frame" "view.prevPage" = "Föregående sida" "view.previousRegion" = "Fokusera föregående region" "view.proofSrgb" = "Skärmkorrektur i sRGB" @@ -2368,7 +2406,7 @@ name = "Svenska" "libraries.open" = "Öppna bibliotek" "select.sameEffect" = "Effekt" "select.sameFill" = "Fyllning" -"select.sameFont" = "Teckensnitt" +"select.sameFont" = "Typsnitt" "select.sameInstance" = "Instans" "select.sameProperties" = "Egenskaper" "select.sameStroke" = "Kontur" @@ -2377,7 +2415,7 @@ name = "Svenska" "text.alignJustify" = "Marginaljustera text" "text.alignLeft" = "Vänsterjustera text" "text.alignRight" = "Högerjustera text" -"view.assets" = "Tillgångar" +"view.assets" = "Resurser" "view.design" = "Design" "view.layers" = "Lager" "view.outlines" = "Visa konturlinjer" From 0a4e70fb972ab127ce918c37bd3c14b8a40478b4 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Sun, 25 Oct 2026 18:23:37 +0000 Subject: [PATCH 10/14] Name the failing step in batch command errors Fixes #1331 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/engine/src/batch_step_ref_tests.rs | 23 +++++++++++++++++++++++ crates/engine/src/lib.rs | 10 +++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/batch_step_ref_tests.rs b/crates/engine/src/batch_step_ref_tests.rs index ac69118e8..e84985c6e 100644 --- a/crates/engine/src/batch_step_ref_tests.rs +++ b/crates/engine/src/batch_step_ref_tests.rs @@ -110,3 +110,26 @@ fn a_nested_batch_refers_to_its_own_steps() { let rect = s.doc().unwrap().node(id(&inner[1])).unwrap().clone(); assert_eq!(rect.parent, Some(id(&inner[0])), "in the nested batch's own first frame, not the outer one"); } + +#[test] +fn a_failing_command_names_its_step_and_rolls_back() { + let mut s = Session::new(); + let frame = id(&s.execute("node.create", &json!({"type": "frame", "width": 50, "height": 50})).unwrap()); + let text = id(&s.execute("node.create", &json!({"type": "text", "text": "orig"})).unwrap()); + let undo = s.ds().unwrap().history.undo.len(); + let name = s.doc().unwrap().node(s.page().unwrap()).unwrap().name.clone(); + let r = s.execute( + "command.batch", + &json!({"commands": [ + {"command": "text.set", "params": {"ids": [text.0], "text": "novo"}}, + {"command": "text.set", "params": {"ids": [frame.0], "text": "erro"}}, + {"command": "page.rename", "params": {"name": "Depois do erro"}} + ]}), + ); + let msg = r.unwrap_err().to_string(); + assert!(msg.contains("step 1: invalid parameters for"), "{msg}"); + assert!(msg.contains("is not text"), "{msg}"); + assert!(matches!(&s.doc().unwrap().node(text).unwrap().kind, figcraft_doc::NodeKind::Text(t) if t.characters == "orig")); + assert_eq!(s.doc().unwrap().node(s.page().unwrap()).unwrap().name, name); + assert_eq!(s.ds().unwrap().history.undo.len(), undo); +} diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 99b0f6f2e..2d53f1099 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -1280,7 +1280,15 @@ impl Session { /// resolve `$step` the same way, inside its own [`Session::grouped`]. pub fn batch_step(&mut self, results: &[Value], step: usize, cmd: &str, params: &Value) -> Result { let p = if cmd == "command.batch" { params.clone() } else { resolve_step_refs(params, results, step, 0)? }; - self.execute(cmd, &p) + self.execute(cmd, &p).map_err(|e| match e { + // Say which step failed; a nested batch's error already does. + EngineError::UnknownCommand(_) | EngineError::BadParams { .. } | EngineError::NoNode(_) | EngineError::Other(_) + if !e.to_string().starts_with("step ") => + { + EngineError::Other(format!("step {step}: {e}")) + } + e => e, + }) } /// Run `f` as one undo step called `label`: the commands it executes push no steps of their From 6f1999c24836ab13aa188a139adbec274fee497c Mon Sep 17 00:00:00 2001 From: bflatastic Date: Mon, 26 Oct 2026 09:14:25 +0700 Subject: [PATCH 11/14] #1340: a failed step keeps its kind of error; only its message names the step --- crates/engine/src/batch_step_ref_tests.rs | 13 ++++++------ crates/engine/src/lib.rs | 24 +++++++++++++---------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/crates/engine/src/batch_step_ref_tests.rs b/crates/engine/src/batch_step_ref_tests.rs index 644672559..47d6e8766 100644 --- a/crates/engine/src/batch_step_ref_tests.rs +++ b/crates/engine/src/batch_step_ref_tests.rs @@ -266,17 +266,16 @@ fn a_failing_command_names_its_step_and_rolls_back() { let text = id(&s.execute("node.create", &json!({"type": "text", "text": "orig"})).unwrap()); let undo = s.ds().unwrap().history.undo.len(); let name = s.doc().unwrap().node(s.page().unwrap()).unwrap().name.clone(); - let r = s.execute( - "command.batch", - &json!({"commands": [ + let msg = batch_error( + &mut s, + json!([ {"command": "text.set", "params": {"ids": [text.0], "text": "novo"}}, {"command": "text.set", "params": {"ids": [frame.0], "text": "erro"}}, {"command": "page.rename", "params": {"name": "Depois do erro"}} - ]}), + ]), ); - let msg = r.unwrap_err().to_string(); - assert!(msg.contains("step 1: invalid parameters for"), "{msg}"); - assert!(msg.contains("is not text"), "{msg}"); + // Still the command's own kind of error, its message saying which step. + assert!(msg.starts_with("step 1: ") && msg.contains("is not text"), "{msg}"); assert!(matches!(&s.doc().unwrap().node(text).unwrap().kind, figcraft_doc::NodeKind::Text(t) if t.characters == "orig")); assert_eq!(s.doc().unwrap().node(s.page().unwrap()).unwrap().name, name); assert_eq!(s.ds().unwrap().history.undo.len(), undo); diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 710dd779f..f47e2ea23 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -117,6 +117,18 @@ impl From for EngineError { } } +impl EngineError { + /// This error from step `step` of a batch, saying so in its message; the kind stays (errors + /// without a message stay as they are). + fn at_step(self, step: usize) -> Self { + match self { + EngineError::BadParams { cmd, msg } => EngineError::BadParams { cmd, msg: format!("step {step}: {msg}") }, + EngineError::Other(msg) => EngineError::Other(format!("step {step}: {msg}")), + e => e, + } + } +} + pub type Result = std::result::Result; pub fn bad(cmd: &str, msg: impl Into) -> EngineError { @@ -1258,16 +1270,8 @@ impl Session { /// so its steps resolve references the same way, inside its own [`Session::grouped`]. pub fn batch_step(&mut self, scope: &mut BatchScope, cmd: &str, params: &Value) -> Result<()> { let p = if cmd == "command.batch" { params.clone() } else { scope.resolve(params)? }; - let step = scope.next_step(); - let v = self.execute(cmd, &p).map_err(|e| match e { - // Say which step failed; a nested batch's error already does. - EngineError::UnknownCommand(_) | EngineError::BadParams { .. } | EngineError::NoNode(_) | EngineError::Other(_) - if !e.to_string().starts_with("step ") => - { - EngineError::Other(format!("step {step}: {e}")) - } - e => e, - })?; + // Say which step failed; a nested batch names its own steps. + let v = self.execute(cmd, &p).map_err(|e| if cmd == "command.batch" { e } else { e.at_step(scope.next_step()) })?; scope.push(v); Ok(()) } From 8d52baa4dedf7e1acd6b220ce62f442086a2dff5 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Mon, 26 Oct 2026 09:01:33 +0700 Subject: [PATCH 12/14] #984: Swedish joining a live session reads ansluta till --- crates/ui-egui/locales/sv.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ui-egui/locales/sv.toml b/crates/ui-egui/locales/sv.toml index c944b3550..d4eb6bb37 100644 --- a/crates/ui-egui/locales/sv.toml +++ b/crates/ui-egui/locales/sv.toml @@ -1427,7 +1427,7 @@ name = "Svenska" "This file" = "Den här filen" "This file has unsaved changes" = "Filen har osparade ändringar" "This file is on {server}, open only to you. Share it to let others in with a new code." = "Filen ligger på {server} och är bara öppen för dig. Dela den för att släppa in andra med en ny kod." -"This file is too large to join here (up to {mb} MB)" = "Den här filen är för stor för att läggas till här (högst {mb} MB)" +"This file is too large to join here (up to {mb} MB)" = "Den här filen är för stor för att ansluta till här (högst {mb} MB)" "This file uses fonts that aren't installed on this computer. Their text is drawn in a fallback font until you install them." = "Den här filen använder typsnitt som inte är installerade på den här datorn. Deras text ritas med ett reservtypsnitt tills du installerar dem." "This frame is new on the branch." = "Den här framen är ny på grenen." "This frame isn't on the branch." = "Den här framen finns inte på grenen." From 994ddfc47e9d589bbcbc5f8b599e095a14e740ad Mon Sep 17 00:00:00 2001 From: bflatastic Date: Mon, 26 Oct 2026 09:11:23 +0700 Subject: [PATCH 13/14] #1299: the server key field shows dots, as a password field does --- crates/ui-egui/src/live.rs | 2 +- crates/ui-egui/src/live_share_tests.rs | 14 +++++++++++++ crates/ui-egui/src/widgets.rs | 29 +++++++++++++++++--------- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/crates/ui-egui/src/live.rs b/crates/ui-egui/src/live.rs index 199a4e33f..9e7ce3526 100644 --- a/crates/ui-egui/src/live.rs +++ b/crates/ui-egui/src/live.rs @@ -772,7 +772,7 @@ fn start_ui(app: &mut FigApp, ui: &mut Ui, t: &Tokens) { // The server key (a server that needs one; the address in Preferences is without it). ui.horizontal(|ui| { widgets::label(ui, tr("Server key"), t.text); - if let Some(key) = widgets::text_field_named(ui, egui::Id::new("live_share_key"), &app.ui.live.share_key, 200.0, tr("Server key")) { + if let Some(key) = widgets::secret_field_named(ui, egui::Id::new("live_share_key"), &app.ui.live.share_key, 200.0, tr("Server key")) { app.ui.live.share_key = key.trim().chars().filter(|c| !c.is_whitespace() && !c.is_control()).take(128).collect(); } }); diff --git a/crates/ui-egui/src/live_share_tests.rs b/crates/ui-egui/src/live_share_tests.rs index d7fc61c89..6c5bdb84d 100644 --- a/crates/ui-egui/src/live_share_tests.rs +++ b/crates/ui-egui/src/live_share_tests.rs @@ -232,6 +232,20 @@ fn not_sharing_the_popover_offers_one_way_to_start_and_one_to_join() { assert_eq!(h.app.session.live().status, Status::Off); } +#[test] +fn the_server_key_shows_as_dots_while_shown_and_typed() { + let (mut h, _) = harness(); + let shapes = open(&mut h); + click(&mut h, &shapes, "Advanced"); + h.app.ui.live.share_key = "s3cret-key".into(); + let shapes = h.frames(2); + let dots = "•".repeat(10); + assert!(has_text(&shapes, &dots) && !has_text(&shapes, "s3cret-key")); + // Editing it keeps it hidden. + let shapes = click(&mut h, &shapes, &dots); + assert!(!has_text(&shapes, "s3cret-key")); +} + #[test] fn sharing_online_shows_the_code_copies_it_and_stops() { let (mut h, script) = harness(); diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index a5dd2c7b9..cb58e1d16 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -486,13 +486,20 @@ pub fn bound_field(ui: &mut Ui, id: Id, lead: Lead, rect: Rect, name: &str) -> O /// A text field that commits on Enter / focus loss. Returns the new text when committed. pub fn text_field(ui: &mut Ui, id: Id, value: &str, width: f32, placeholder: &str) -> Option { let (rect, _) = ui.allocate_exact_size(vec2(width, FIELD_H), Sense::hover()); - text_field_in(ui, id, value, rect, placeholder, placeholder) + text_field_in(ui, id, value, rect, placeholder, placeholder, false) } /// [`text_field`] without a placeholder, named `name` for screen readers (its label is beside it). pub fn text_field_named(ui: &mut Ui, id: Id, value: &str, width: f32, name: &str) -> Option { let (rect, _) = ui.allocate_exact_size(vec2(width, FIELD_H), Sense::hover()); - text_field_in(ui, id, value, rect, "", name) + text_field_in(ui, id, value, rect, "", name, false) +} + +/// [`text_field_named`] for a secret (a server key): it shows dots, typing included, and screen +/// readers hear dots too. +pub fn secret_field_named(ui: &mut Ui, id: Id, value: &str, width: f32, name: &str) -> Option { + let (rect, _) = ui.allocate_exact_size(vec2(width, FIELD_H), Sense::hover()); + text_field_in(ui, id, value, rect, "", name, true) } /// A multi-line text field across the panel (descriptions, notes) that commits when it loses @@ -536,7 +543,7 @@ pub fn multiline_field(ui: &mut Ui, id: Id, value: &str, hint: &str, rows: usize pub fn combo_field(ui: &mut Ui, id: Id, value: &str, width: f32, options: &[String]) -> Option { let t = Tokens::get(ui.ctx()); let (rect, _) = ui.allocate_exact_size(vec2(width, FIELD_H), Sense::hover()); - let mut out = text_field_in(ui, id, value, rect, "", ""); + let mut out = text_field_in(ui, id, value, rect, "", "", false); // The chevron sits on top of the field's right end. let chev = Rect::from_min_max(pos2(rect.max.x - 18.0, rect.min.y), rect.max); let resp = ui.interact(chev, id.with("menu"), Sense::click()); @@ -557,13 +564,14 @@ pub fn combo_field(ui: &mut Ui, id: Id, value: &str, width: f32, options: &[Stri } /// [`text_field`] drawn into `rect`; `name` names it for screen readers. -fn text_field_in(ui: &mut Ui, id: Id, value: &str, rect: Rect, placeholder: &str, name: &str) -> Option { +fn text_field_in(ui: &mut Ui, id: Id, value: &str, rect: Rect, placeholder: &str, name: &str, secret: bool) -> Option { cancel_retargeted_field_save(ui, id); let t = Tokens::get(ui.ctx()); let editing_id = id.with("editing"); let mut editing: Option = ui.data(|d| d.get_temp(editing_id)); let resp = ui.interact(rect, id.with("click"), Sense::click()); - a11y::text(&resp, name, value); + let shown = if secret { std::borrow::Cow::Owned("•".repeat(value.chars().count())) } else { std::borrow::Cow::Borrowed(value) }; + a11y::text(&resp, name, &shown); ui.painter().rect_filled(rect, CornerRadius::same(5), t.field); if editing.is_some() { ui.painter().rect_stroke(rect, CornerRadius::same(5), Stroke::new(1.0, crate::theme::ACCENT), StrokeKind::Inside); @@ -579,7 +587,7 @@ fn text_field_in(ui: &mut Ui, id: Id, value: &str, rect: Rect, placeholder: &str let mut out = None; match editing { Some(mut text) => { - let r = field_editor(ui, rect, id.with("te"), &mut text, ""); + let r = field_editor(ui, rect, id.with("te"), &mut text, "", secret); a11y::own_focus(&r); keep_focus(&r, started); crate::shortcuts::release_field_for_save(&r); @@ -595,7 +603,7 @@ fn text_field_in(ui: &mut Ui, id: Id, value: &str, rect: Rect, placeholder: &str } } None => { - let (s, col) = if value.is_empty() { (placeholder, t.text_tertiary) } else { (value, t.text) }; + let (s, col) = if value.is_empty() { (placeholder, t.text_tertiary) } else { (&*shown, t.text) }; // Long text stays inside the field. let painter = ui.painter().with_clip_rect(rect.shrink2(vec2(8.0, 0.0))); painter.text(pos2(rect.min.x + 8.0, rect.center().y), Align2::LEFT_CENTER, s, regular(11.0), col); @@ -605,14 +613,15 @@ fn text_field_in(ui: &mut Ui, id: Id, value: &str, rect: Rect, placeholder: &str } /// The editor inside a field's `rect`: one line of text in the field's font, without a frame of -/// its own (the field draws it), showing `hint` while empty. -fn field_editor(ui: &mut Ui, rect: Rect, id: Id, text: &mut String, hint: &str) -> Response { +/// its own (the field draws it), showing `hint` while empty; `password` shows dots. +fn field_editor(ui: &mut Ui, rect: Rect, id: Id, text: &mut String, hint: &str, password: bool) -> Response { let t = Tokens::get(ui.ctx()); let mut child = ui.new_child(egui::UiBuilder::new().max_rect(rect.shrink2(vec2(8.0, 4.0)))); let r = child.add( egui::TextEdit::singleline(text) .id(id) .frame(egui::Frame::NONE) + .password(password) .font(regular(11.0)) .hint_text(egui::RichText::new(hint).font(regular(11.0)).color(t.text_tertiary)) .desired_width(rect.width() - 16.0), @@ -628,7 +637,7 @@ pub fn text_input(ui: &mut Ui, id: Id, text: &mut String, width: f32, hint: &str let t = Tokens::get(ui.ctx()); let (rect, _) = ui.allocate_exact_size(vec2(width, FIELD_H), Sense::hover()); ui.painter().rect_filled(rect, CornerRadius::same(5), t.field); - let r = field_editor(ui, rect, id, text, hint); + let r = field_editor(ui, rect, id, text, hint, false); if r.has_focus() { ui.painter().rect_stroke(rect, CornerRadius::same(5), Stroke::new(1.0, crate::theme::ACCENT), StrokeKind::Inside); } else if ui.rect_contains_pointer(rect) { From 454ec26c11c6c8c569f38c06680ada20d36609d3 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Mon, 26 Oct 2026 09:11:24 +0700 Subject: [PATCH 14/14] #1299: the server key field's id uses egui 0.37's Id::unique --- crates/ui-egui/src/live.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/live.rs b/crates/ui-egui/src/live.rs index 9e7ce3526..bcc0bc77f 100644 --- a/crates/ui-egui/src/live.rs +++ b/crates/ui-egui/src/live.rs @@ -772,7 +772,9 @@ fn start_ui(app: &mut FigApp, ui: &mut Ui, t: &Tokens) { // The server key (a server that needs one; the address in Preferences is without it). ui.horizontal(|ui| { widgets::label(ui, tr("Server key"), t.text); - if let Some(key) = widgets::secret_field_named(ui, egui::Id::new("live_share_key"), &app.ui.live.share_key, 200.0, tr("Server key")) { + if let Some(key) = + widgets::secret_field_named(ui, egui::Id::unique("live_share_key"), &app.ui.live.share_key, 200.0, tr("Server key")) + { app.ui.live.share_key = key.trim().chars().filter(|c| !c.is_whitespace() && !c.is_control()).take(128).collect(); } });