M7: closing a tab with unsaved changes asks Don't Save / Cancel / Save (#2032)

Closing a tab whose file has unsaved changes, by its X, a middle click, File ▸ Close tab or its
shortcut, only showed a toast ("save first, or close again with Alt to discard"; from the menu
"close the tab with Alt-click") and left the tab open, so closing a tab seemed not to work. A
`file.close` refused for unsaved changes asked the UI with `dialog.unsaved`, which toasted too.

Closing such a tab now asks the unsaved-changes question the window asks when it closes
(`windows::unsaved_question`, shared): Don't Save closes the tab and drops the changes, Cancel or
Escape keeps it, Save saves the file next frame and then closes the tab. A file without a path, or
saved as .fig, asks where with Save as first; the tab closes once that saved it and stays open if
it is cancelled or fails. `UiState::close_tab` (`windows::TabClose`: Ask, Save, AfterSaveAs) keeps
where it is; `ask_close_tab` starts it for the tab strip, the menu and `dialog.unsaved`. Alt+close
still drops the changes without asking. The three toasts are gone from the code and the catalogs.

Reordering tabs by dragging, the issue's other half, already works on main (landed 2026-10-19,
`dragging_a_tab_along_the_bar_reorders_it_without_detaching`).

Tests: `titlebar_tests::closing_a_tab_with_unsaved_changes_asks` (the X asks, Escape keeps the tab,
Don't Save closes it, Alt+click closes without asking, File ▸ Close tab asks),
`titlebar_tests::save_in_the_tab_question_saves_then_closes` (Enter saves then closes; no path:
the tab stays). The title bar test harness can hold modifier keys. Docs: ui-parity.md, ROADMAP.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvG6E2LxvJ6MuFmiLA6Zqm
This commit is contained in:
Filip StrajnarandClaude Opus 5.5 committed 2026-11-06 12:39:39 +01:00
1 parent 8eb0b87646
commit 98710acb80
32 files changed
+255 -92

No files matched your search

+2 -1
View File
@@ -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-11-02 · **Change:** minor (glass bends towards its core, and rounded rectangles work it out in closed form; glass's light is a line on its bevel and splay turns the bend towards the centre; glass bends with a thick-lens profile and splits its colours over 8 steps; glass effects take variables and keep their frost within 0–100; web host bridge: `?host=parent` or `data-host="page"` hands a page the app's files, documented in the hosting guide; a connector end moved with Cmd/Ctrl held goes on any part of an object, and stays there as the object moves or resizes; a tap on a plugin panel's button keeps what was typed in its fields; a Dash cap setting (none, round or square) on Dash and Custom strokes, drawn on both ends of every dash; library analytics list how many layers and files use each style and variable; the Controller menu names each button as PlayStation and Switch controllers do; widgets draw lines and SVG pictures (plugin API 6); a connector end dropped along a side of a shape attaches to that side (#1708); regression tests pin a rounded shape's one-sided stroke and SVG ids and font families without XML-forbidden characters (#1351, #1124); a damaged image is refused with a reason instead of showing as an empty box (#1474); an image copied in another app pastes again on the desktop, and in the browser for the first time (#492); SwiftUI and Compose code names a bound variable by its iOS or Android code syntax; the colour picker's Image tab shows a placeholder and Upload from computer, an asset preview, and a file dropped on it replaces the image, keeping the fill mode, crop and position (#1682); the Frame tool's Phone presets have the iPhone 17 family (#1691); Show in folder works in the Flatpak build and selects the file on Linux (#1701); a dashed line starts and ends with a half-length dash, on the canvas and in exports; the Pages, Layers and Assets search fields keep their text in the middle of the field (#1703); a board connector's path can be reshaped by dragging its path handles (#1710); boards have 16 more shapes (arrows, polygons, star, plus and flowchart symbols), in pickers that lay them out in a grid; library analytics count insertions and show insertions and detachments over the past 30 days, 90 days or year; a stroke's Custom style takes a dash pattern of any length as a comma-separated list (#1681); a control request can set its own timeout, up to 10 minutes (`timeoutMs`; `figcraft-cli mcp --attach PORT --timeout SECONDS`) (#1696); gap 6: a widget opens its plugin's panel (plugin API 5), whose callbacks redraw it; the keyboard's Menu key opens the context menu, as Shift+F10 does; an Open link action can open in place of the prototype, with "Open in new tab" off (gap 15); a collection of this file can be extended (Extend collection on its row, `variable.extendCollection`), its extension following its edits at once, and extended collections extend again in the file; the eyedropper with Cmd/Ctrl+Shift makes a colour style or variable of what it picks; a live session's welcome sends the document compressed to clients that read it, about seven times smaller (#1665); a control request that times out says truthfully whether it was dropped or is still running and will take effect, and only screenshots and input blame the window (#1673); a board keeps earlier votes' results, listed newest first in Results and deleted one at a time; the object a stamp will stick to is outlined while the stamp tool is over it or a stamp is dragged over it, and board objects take no drops (gap 24); a server can list the files shared from it now for everyone who reaches it, Home ▸ Online shows them and their links carry no secret (`team_files`, part of #742); team library analytics count detached instances per component and file (gap 10); skipping the Open link notice by command is saved with the preferences at once (gap 15); a selected connector's bar on a board sets its start and end points, solid or dashed, its weight and any colour (#1596); cells copied from a spreadsheet paste onto a board as a table; a long layer name stays on one line in the Export button and the Dev Mode header, ending with an ellipsis, and no longer covers the Design panel (#1650); with hundreds of layers selected, the Design panel and the selection overlays stop going over the whole selection on every frame, so dragging 648 layers averages about 1.1 ms of UI per frame (#1571); plugin panels show pictures: an `image` control, base64 within 256 KB and 4,096 px a side, named by its label (gap 6); `.fig` connector labels without a saved snapshot are kept, centred on their line (gap 1); a drag moving many layers re-runs only what sits around them, not each layer's bindings, sizing, text and instance checks, on every move (#1571); damaged font tracking table offsets are checked instead of wrapping on 32-bit targets, and font indexing makes its existing offset bounds explicit (#1125); BLine truncated
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** patch (closing a tab whose file has unsaved changes (its X, a middle click, File ▸ Close tab) asks Don't Save / Cancel / Save as closing the window does, instead of a toast that left it open (#2032); glass bends towards its core, and rounded rectangles work it out in closed form; glass's light is a line on its bevel and splay turns the bend towards the centre; glass bends with a thick-lens profile and splits its colours over 8 steps; glass effects take variables and keep their frost within 0–100; web host bridge: `?host=parent` or `data-host="page"` hands a page the app's files, documented in the hosting guide; a connector end moved with Cmd/Ctrl held goes on any part of an object, and stays there as the object moves or resizes; a tap on a plugin panel's button keeps what was typed in its fields; a Dash cap setting (none, round or square) on Dash and Custom strokes, drawn on both ends of every dash; library analytics list how many layers and files use each style and variable; the Controller menu names each button as PlayStation and Switch controllers do; widgets draw lines and SVG pictures (plugin API 6); a connector end dropped along a side of a shape attaches to that side (#1708); regression tests pin a rounded shape's one-sided stroke and SVG ids and font families without XML-forbidden characters (#1351, #1124); a damaged image is refused with a reason instead of showing as an empty box (#1474); an image copied in another app pastes again on the desktop, and in the browser for the first time (#492); SwiftUI and Compose code names a bound variable by its iOS or Android code syntax; the colour picker's Image tab shows a placeholder and Upload from computer, an asset preview, and a file dropped on it replaces the image, keeping the fill mode, crop and position (#1682); the Frame tool's Phone presets have the iPhone 17 family (#1691); Show in folder works in the Flatpak build and selects the file on Linux (#1701); a dashed line starts and ends with a half-length dash, on the canvas and in exports; the Pages, Layers and Assets search fields keep their text in the middle of the field (#1703); a board connector's path can be reshaped by dragging its path handles (#1710); boards have 16 more shapes (arrows, polygons, star, plus and flowchart symbols), in pickers that lay them out in a grid; library analytics count insertions and show insertions and detachments over the past 30 days, 90 days or year; a stroke's Custom style takes a dash pattern of any length as a comma-separated list (#1681); a control request can set its own timeout, up to 10 minutes (`timeoutMs`; `figcraft-cli mcp --attach PORT --timeout SECONDS`) (#1696); gap 6: a widget opens its plugin's panel (plugin API 5), whose callbacks redraw it; the keyboard's Menu key opens the context menu, as Shift+F10 does; an Open link action can open in place of the prototype, with "Open in new tab" off (gap 15); a collection of this file can be extended (Extend collection on its row, `variable.extendCollection`), its extension following its edits at once, and extended collections extend again in the file; the eyedropper with Cmd/Ctrl+Shift makes a colour style or variable of what it picks; a live session's welcome sends the document compressed to clients that read it, about seven times smaller (#1665); a control request that times out says truthfully whether it was dropped or is still running and will take effect, and only screenshots and input blame the window (#1673); a board keeps earlier votes' results, listed newest first in Results and deleted one at a time; the object a stamp will stick to is outlined while the stamp tool is over it or a stamp is dragged over it, and board objects take no drops (gap 24); a server can list the files shared from it now for everyone who reaches it, Home ▸ Online shows them and their links carry no secret (`team_files`, part of #742); team library analytics count detached instances per component and file (gap 10); skipping the Open link notice by command is saved with the preferences at once (gap 15); a selected connector's bar on a board sets its start and end points, solid or dashed, its weight and any colour (#1596); cells copied from a spreadsheet paste onto a board as a table; a long layer name stays on one line in the Export button and the Dev Mode header, ending with an ellipsis, and no longer covers the Design panel (#1650); with hundreds of layers selected, the Design panel and the selection overlays stop going over the whole selection on every frame, so dragging 648 layers averages about 1.1 ms of UI per frame (#1571); plugin panels show pictures: an `image` control, base64 within 256 KB and 4,096 px a side, named by its label (gap 6); `.fig` connector labels without a saved snapshot are kept, centred on their line (gap 1); a drag moving many layers re-runs only what sits around them, not each layer's bindings, sizing, text and inLine truncated
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
@@ -358,6 +358,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md).
| Date | Change | Summary |
|---|---|---|
| 2026-11-06 | patch | M7, #2032: closing a tab whose file has unsaved changes asks Don't Save / Cancel / Save, as closing the window does, instead of a toast that left the tab open; Alt+close still discards at once. Tests: `titlebar_tests::closing_a_tab_with_unsaved_changes_asks`, `titlebar_tests::save_in_the_tab_question_saves_then_closes`. Numbers unchanged |
| 2026-11-02 | patch | M8, #1885: a glass whose shape is too big to keep whole at the zoom (past `MAX_SHAPE_PX` with its core's margin) bends along its edge normals instead of working out a core, so its fields never cover more pixels than a kept shape's, and the core's feature transform holds 16 bytes a pixel. The edge normals are box-blurred twice again and the bend directions once, as documented. Test: `glass::tests::a_shape_too_big_to_keep_bends_along_its_normals`. Numbers unchanged |
| 2026-11-02 | minor | M3, #1885: glass bends the backdrop towards its core, the shape shrunk by `depth` and the bevel, as the reference tool's: round a sharp corner the bend turns smoothly instead of meeting along the diagonal, and a part thinner than twice that (a capsule, a letter's stroke) bends across itself towards its own middle (pixels about as deep as any within 2 px). Rectangles, frames, components, instances and sections whose corners aren't smoothed work their glass out in closed form, with nothing to rasterise, transform or keep: a zoom frame draws in 40 ms instead of 54 and a resize frame in 46 instead of 67 (`glass_render_timings`, release, 2×). Other shapes keep their fields, with the bend directions for the last two depths asked for beside them, so dragging the Depth slider works out only those again. Tests: `glass::tests::a_sharp_corner_bends_round_towards_the_core`, `glass::tests::a_thin_part_bends_across_itself`, `glass::tests::neighbouring_strokes_bend_towards_their_own_middles`, `glass::tests::a_rounded_rectangle_in_closed_form_matches_its_distance_transform`, `glass_cut_tests::what_the_shape_depends_on_works_it_out_again_and_nothing_else_does`. Numbers unchanged |
| 2026-11-02 | minor | M3, #1885: glass's light is a line on its 1-unit bevel added to the backdrop, as the reference tool's: fully where the edge faces the light, none below 0.35 of facing it and 0.6 as much on the opposite side, with a faint gradient across the curve, 5% darker facing the light and lighter opposite (it was a glow mixed towards white, as deep as `splay` made it). `splay` turns the bend from along the edge's normal towards the way away from the layer's centre, as the reference tool's does, and no longer widens the light: files with a splay above 0 look different. The direction to the centre is worked out in the shape's own frame, so cut renders match. Tests: `glass::tests::the_light_lies_on_the_bevel_and_the_gradient_across_the_curve`, `backdrop_tests::splay_turns_the_bend_towards_the_centre`, `glass_cut_tests::the_light_is_a_line_on_the_bevel`. Numbers unchanged |
-3
View File
@@ -1520,7 +1520,6 @@ status = "draft — needs native review"
"This page keeps the owner token only while it is open. Copy it to manage the file later: it is a password for this file and won't be shown again." = "تحتفظ هذه الصفحة برمز المالك ما دامت مفتوحة فقط. انسخه لإدارة الملف لاحقًا؛ فهو كلمة مرور لهذا الملف ولن يُعرض مجددًا."
"This condition is edited as an expression" = "يُحرر هذا الشرط كتعبير"
"This file" = "هذا الملف"
"This file has unsaved changes" = "يحتوي هذا الملف على تغييرات غير محفوظة"
"This file is too large to join here (up to {mb} 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." = "يستخدم هذا الملف خطوطًا غير مثبتة على هذا الحاسوب. يُعرض نصها بخط بديل حتى تثبتها."
"This file is on {server}, open only to you. Share it to let others in with a new code." = "هذا الملف على {server} ومتاح لك وحدك. شاركه ليتمكن الآخرون من الانضمام برمز جديد."
@@ -1580,8 +1579,6 @@ status = "draft — needs native review"
"Unresolve" = "إعادة فتح"
"Unsaved changes" = "تغييرات غير محفوظة"
"Unsaved changes in {names}. Save them before closing?" = "تغييرات غير محفوظة في {names}. هل تريد حفظها قبل الإغلاق؟"
"Unsaved changes: save first ({save}), or close the tab with {alt}-click to discard" = "تغييرات غير محفوظة: احفظ أولًا ({save})، أو أغلق علامة التبويب بالنقر مع {alt} لتجاهلها"
"Unsaved changes: save first, or close again with {key} to discard" = "تغييرات غير محفوظة: احفظ أولًا، أو أغلق مجددًا مع {key} لتجاهلها"
"Up" = "لأعلى"
"Update check is not supported in the web browser." = "التحقق من التحديثات غير مدعوم في المتصفح."
"Update plugin" = "تحديث الإضافة"
-3
View File
@@ -1476,7 +1476,6 @@ status = "draft — needs native review"
"This page keeps the owner token only while it is open. Copy it to manage the file later: it is a password for this file and won't be shown again." = "এই পেজটি খোলা থাকা পর্যন্তই ওনার টোকেন রাখে। পরে ফাইলটি পরিচালনা করতে এটি কপি করুন: এটি এই ফাইলের জন্য একটি পাসওয়ার্ড এবং আর কখনো দেখানো হবে না।"
"This condition is edited as an expression" = "এই শর্তটি এক্সপ্রেশন হিসেবে এডিট করা হয়েছে"
"This file" = "এই ফাইল"
"This file has unsaved changes" = "এই ফাইলে সেভ করা হয়নি এমন পরিবর্তন রয়েছে"
"This file uses fonts that aren't installed on this computer. Their text is drawn in a fallback font until you install them." = "এই ফাইলে এমন ফন্ট ব্যবহার করা হয়েছে যা এই কম্পিউটারে ইনস্টল করা নেই। আপনি ইনস্টল না করা পর্যন্ত সেগুলোর টেক্সট একটি ফলব্যাক ফন্টে দেখানো হয়।"
"This file is on {server}, open only to you. Share it to let others in with a new code." = "এই ফাইলটি {server}-এ আছে, শুধুমাত্র আপনার জন্য খোলা। নতুন কোড দিয়ে অন্যদের প্রবেশ করাতে এটি শেয়ার করুন।"
"This frame is new on the branch." = "এই ফ্রেমটি ব্রাঞ্চে নতুন।"
@@ -1531,8 +1530,6 @@ status = "draft — needs native review"
"Unresolve" = "অমীমাংসিত করুন"
"Unsaved changes" = "সংরক্ষিত হয়নি এমন পরিবর্তন"
"Unsaved changes in {names}. Save them before closing?" = "{names}-এ সংরক্ষিত হয়নি এমন পরিবর্তন রয়েছে। বন্ধ করার আগে সেগুলো সংরক্ষণ করবেন?"
"Unsaved changes: save first ({save}), or close the tab with {alt}-click to discard" = "সংরক্ষিত হয়নি এমন পরিবর্তন: প্রথমে সেভ করুন ({save}), অথবা বাতিল করতে {alt}-ক্লিক করে ট্যাব বন্ধ করুন"
"Unsaved changes: save first, or close again with {key} to discard" = "সংরক্ষিত হয়নি এমন পরিবর্তন: প্রথমে সেভ করুন, অথবা বাতিল করতে {key} দিয়ে আবার বন্ধ করুন"
"Up" = "উপরে"
"Update plugin" = "প্লাগইন আপডেট করুন"
"Update" = "আপডেট"
-3
View File
@@ -1533,7 +1533,6 @@ status = "draft — needs native review"
"This page keeps the owner token only while it is open. Copy it to manage the file later: it is a password for this file and won't be shown again." = "Ova stranica čuva token vlasnika samo dok je otvorena. Kopirajte ga da kasnije upravljate datotekom: to je lozinka za ovu datoteku i neće se ponovo prikazati."
"This condition is edited as an expression" = "Ovaj uslov se uređuje kao izraz"
"This file" = "Ova datoteka"
"This file has unsaved changes" = "Ova datoteka ima nesačuvane promjene"
"This file is too large to join here (up to {mb} MB)" = "Ova datoteka je prevelika za pridruživanje ovdje (do {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." = "Ova datoteka koristi fontove koji nisu instalirani na ovom računaru. Njihov tekst se iscrtava rezervnim fontom dok ih ne instalirate."
"This file is on {server}, open only to you. Share it to let others in with a new code." = "Ova datoteka je na serveru {server}, otvorena samo Vama. Podijelite je da drugi uđu s novim kodom."
@@ -1594,8 +1593,6 @@ status = "draft — needs native review"
"Unresolve" = "Poništi rješenje"
"Unsaved changes" = "Nesačuvane promjene"
"Unsaved changes in {names}. Save them before closing?" = "Nesačuvane promjene u: {names}. Sačuvati ih prije zatvaranja?"
"Unsaved changes: save first ({save}), or close the tab with {alt}-click to discard" = "Nesačuvane promjene: prvo sačuvajte ({save}) ili zatvorite karticu uz {alt}-klik da ih odbacite"
"Unsaved changes: save first, or close again with {key} to discard" = "Nesačuvane promjene: prvo sačuvajte ili ponovo zatvorite s {key} da ih odbacite"
"Up" = "Gore"
"Update check is not supported in the web browser." = "Provjera ažuriranja nije podržana u web pregledniku."
"Update plugin" = "Ažuriraj dodatak"
-3
View File
@@ -1448,7 +1448,6 @@ status = "draft — needs native review"
"This page keeps the owner token only while it is open. Copy it to manage the file later: it is a password for this file and won't be shown again." = "Tato stránka si token vlastníka pamatuje, jen dokud je otevřená. Zkopírujte ho pro pozdější správu souboru: je to heslo k tomuto souboru a znovu se nezobrazí."
"This condition is edited as an expression" = "Tato podmínka se upravuje jako výraz"
"This file" = "Tento soubor"
"This file has unsaved changes" = "Tento soubor obsahuje neuložené změny"
"This file is on {server}, open only to you. Share it to let others in with a new code." = "Tento soubor je na serveru {server}, přístupný pouze vám. Sdílením umožníte ostatním připojení pomocí nového kódu."
"This frame is new on the branch." = "Tento rámec je ve větvi nový."
"This frame isn't on the branch." = "Tento rámec není ve větvi."
@@ -1501,8 +1500,6 @@ status = "draft — needs native review"
"Unresolve" = "Znovu otevřít"
"Unsaved changes" = "Neuložené změny"
"Unsaved changes in {names}. Save them before closing?" = "Neuložené změny v {names}. Uložit je před zavřením?"
"Unsaved changes: save first ({save}), or close the tab with {alt}-click to discard" = "Neuložené změny: nejprve uložte ({save}) nebo zavřete kartu kliknutím s {alt} a zahoďte je"
"Unsaved changes: save first, or close again with {key} to discard" = "Neuložené změny: nejprve uložte nebo zavřete znovu s {key} a zahoďte je"
"Up" = "Nahoru"
"Update plugin" = "Aktualizovat plugin"
"Update" = "Aktualizovat"
-3
View File
@@ -1416,7 +1416,6 @@ status = "draft — needs native review"
"This page keeps the owner token only while it is open. Copy it to manage the file later: it is a password for this file and won't be shown again." = "Diese Seite behält das Eigentümer-Token nur, solange sie geöffnet ist. Kopieren Sie es, um die Datei später zu verwalten: Es ist ein Passwort für diese Datei und wird nicht noch einmal angezeigt."
"This condition is edited as an expression" = "Diese Bedingung wird als Ausdruck bearbeitet"
"This file" = "Diese Datei"
"This file has unsaved changes" = "Diese Datei hat nicht gespeicherte Änderungen"
"This file is on {server}, open only to you. Share it to let others in with a new code." = "Diese Datei liegt auf {server} und ist nur für Sie zugänglich. Teilen Sie sie, um andere mit einem neuen Code hereinzulassen."
"This frame is new on the branch." = "Dieser Rahmen ist im Branch neu."
"This frame isn't on the branch." = "Dieser Rahmen ist nicht im Branch."
@@ -1469,8 +1468,6 @@ status = "draft — needs native review"
"Unresolve" = "Wieder öffnen"
"Unsaved changes" = "Nicht gespeicherte Änderungen"
"Unsaved changes in {names}. Save them before closing?" = "Nicht gespeicherte Änderungen in {names}. Vor dem Schließen speichern?"
"Unsaved changes: save first ({save}), or close the tab with {alt}-click to discard" = "Nicht gespeicherte Änderungen: Speichern Sie zuerst ({save}) oder schließen Sie den Tab mit {alt}+Klick, um sie zu verwerfen"
"Unsaved changes: save first, or close again with {key} to discard" = "Nicht gespeicherte Änderungen: Speichern Sie zuerst oder schließen Sie erneut mit {key}, um sie zu verwerfen"
"Up" = "Nach oben"
"Update" = "Aktualisieren"
"Update from main" = "Aus Hauptdatei aktualisieren"
-3
View File
@@ -1573,7 +1573,6 @@ name = "English"
"This page keeps the owner token only while it is open. Copy it to manage the file later: it is a password for this file and won't be shown again." = "This page keeps the owner token only while it is open. Copy it to manage the file later: it is a password for this file and won't be shown again."
"This condition is edited as an expression" = "This condition is edited as an expression"
"This file" = "This file"
"This file has unsaved changes" = "This file has unsaved changes"
"This file is too large to join here (up to {mb} MB)" = "This file is too large to join here (up to {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." = "This file uses fonts that aren't installed on this computer. Their text is drawn in a fallback font until you install them."
"This file is on {server}, open only to you. Share it to let others in with a new code." = "This file is on {server}, open only to you. Share it to let others in with a new code."
@@ -1635,8 +1634,6 @@ name = "English"
"Unresolve" = "Unresolve"
"Unsaved changes" = "Unsaved changes"
"Unsaved changes in {names}. Save them before closing?" = "Unsaved changes in {names}. Save them before closing?"
"Unsaved changes: save first ({save}), or close the tab with {alt}-click to discard" = "Unsaved changes: save first ({save}), or close the tab with {alt}-click to discard"
"Unsaved changes: save first, or close again with {key} to discard" = "Unsaved changes: save first, or close again with {key} to discard"
"Up" = "Up"
"Update check is not supported in the web browser." = "Update check is not supported in the web browser."
"Update plugin" = "Update plugin"
-3
View File
@@ -1515,7 +1515,6 @@ status = "draft — needs native review"
"This page keeps the owner token only while it is open. Copy it to manage the file later: it is a password for this file and won't be shown again." = "Esta página guarda el token de propietario solo mientras está abierta. Cópialo para gestionar el archivo más tarde: es una contraseña para este archivo y no se volverá a mostrar."
"This condition is edited as an expression" = "Esta condición se edita como expresión"
"This file" = "Este archivo"
"This file has unsaved changes" = "Este archivo tiene cambios sin guardar"
"This file is too large to join here (up to {mb} MB)" = "Este archivo es demasiado grande para unirte aquí (hasta {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." = "Este archivo usa fuentes que no están instaladas en este equipo. Su texto se dibuja con una fuente de reserva hasta que las instales."
"This file is on {server}, open only to you. Share it to let others in with a new code." = "Este archivo está en {server}, abierto solo para ti. Compártelo para dejar entrar a otros con un código nuevo."
@@ -1573,8 +1572,6 @@ status = "draft — needs native review"
"Unresolve" = "Reabrir"
"Unsaved changes" = "Cambios sin guardar"
"Unsaved changes in {names}. Save them before closing?" = "Hay cambios sin guardar en {names}. ¿Guardarlos antes de cerrar?"
"Unsaved changes: save first ({save}), or close the tab with {alt}-click to discard" = "Cambios sin guardar: guarda primero ({save}) o cierra la pestaña con {alt}+clic para descartarlos"
"Unsaved changes: save first, or close again with {key} to discard" = "Cambios sin guardar: guarda primero o vuelve a cerrar con {key} para descartarlos"
"Up" = "Arriba"
"Update plugin" = "Actualizar plugin"
"Update" = "Actualizar"
-3
View File
@@ -1546,7 +1546,6 @@ status = "draft — needs native review"
"This page keeps the owner token only while it is open. Copy it to manage the file later: it is a password for this file and won't be shown again." = "Tämä sivu säilyttää omistajan tunnisteen vain niin kauan kuin se on auki. Kopioi tunniste hallitaksesi tiedostoa myöhemmin. Se on tämän tiedoston salasana, eikä sitä näytetä uudelleen."
"This condition is edited as an expression" = "Tätä ehtoa muokataan lausekkeena"
"This file" = "Tämä tiedosto"
"This file has unsaved changes" = "Tässä tiedostossa on tallentamattomia muutoksia"
"This file is too large to join here (up to {mb} MB)" = "Tiedosto on liian suuri liitettäväksi tähän (enintään {mb} Mt)"
"This file is on {server}, open only to you. Share it to let others in with a new code." = "Tämä tiedosto on palvelimella {server} vain sinun käytössäsi. Jaa se, jotta muut voivat liittyä uudella koodilla."
"This file uses fonts that aren't installed on this computer. Their text is drawn in a fallback font until you install them." = "Tämä tiedosto käyttää fontteja, joita ei ole asennettu tälle tietokoneelle. Teksti piirretään korvaavalla fontilla, kunnes asennat puuttuvat fontit."
@@ -1608,8 +1607,6 @@ status = "draft — needs native review"
"Unresolve" = "Merkitse ratkaisemattomaksi"
"Unsaved changes" = "Tallentamattomat muutokset"
"Unsaved changes in {names}. Save them before closing?" = "Tallentamattomia muutoksia kohteissa {names}. Tallennetaanko ne ennen sulkemista?"
"Unsaved changes: save first ({save}), or close the tab with {alt}-click to discard" = "Tallentamattomia muutoksia: tallenna ensin ({save}) tai hylkää sulkemalla välilehti {alt}-napsautuksella"
"Unsaved changes: save first, or close again with {key} to discard" = "Tallentamattomia muutoksia: tallenna ensin tai hylkää sulkemalla uudelleen näppäimellä {key}"
"Up" = "Ylös"
"Update check is not supported in the web browser." = "Päivitysten tarkistusta ei tueta selaimessa."
"Update" = "Päivitä"
-3
View File
@@ -1481,7 +1481,6 @@ status = "draft — needs native review"
"This page keeps the owner token only while it is open. Copy it to manage the file later: it is a password for this file and won't be shown again." = "Cette page ne garde le jeton de propriétaire que tant qu’elle est ouverte. Copiez-le pour gérer le fichier plus tard : c’est un mot de passe pour ce fichier et il ne sera plus affiché."
"This condition is edited as an expression" = "Cette condition se modifie en tant qu’expression"
"This file" = "Ce fichier"
"This file has unsaved changes" = "Ce fichier contient des modifications non enregistrées"
"This file is on {server}, open only to you. Share it to let others in with a new code." = "Ce fichier est sur {server}, accessible à vous seul. Partagez-le pour laisser entrer d’autres personnes avec un nouveau code."
"This frame is new on the branch." = "Ce cadre est nouveau dans la branche."
"This frame isn't on the branch." = "Ce cadre n’est pas dans la branche."
@@ -1536,8 +1535,6 @@ status = "draft — needs native review"
"Unresolve" = "Rouvrir"
"Unsaved changes" = "Modifications non enregistrées"
"Unsaved changes in {names}. Save them before closing?" = "Modifications non enregistrées dans {names}. Les enregistrer avant de fermer ?"
"Unsaved changes: save first ({save}), or close the tab with {alt}-click to discard" = "Modifications non enregistrées : enregistrez d’abord ({save}) ou fermez l’onglet avec {alt}+clic pour les abandonner"
"Unsaved changes: save first, or close again with {key} to discard" = "Modifications non enregistrées : enregistrez d’abord ou fermez à nouveau avec {key} pour les abandonner"
"Up" = "Haut"
"Update" = "Mettre à jour"
"Update from main" = "Mettre à jour depuis le fichier principal"
-3
View File
@@ -1454,7 +1454,6 @@ status = "draft — needs native review"
"This page keeps the owner token only while it is open. Copy it to manage the file later: it is a password for this file and won't be shown again." = "Halaman ini hanya menyimpan token pemilik selama halaman terbuka. Salin token untuk mengelola file nanti: token ini adalah kata sandi untuk file ini dan tidak akan ditampilkan lagi."
"This condition is edited as an expression" = "Kondisi ini diedit sebagai ekspresi"
"This file" = "File ini"
"This file has unsaved changes" = "File ini memiliki perubahan yang belum disimpan"
"This file is on {server}, open only to you. Share it to let others in with a new code." = "File ini ada di {server}, hanya dapat dibuka oleh Anda. Bagikan untuk mengizinkan orang lain masuk dengan kode baru."
"This frame is new on the branch." = "Frame ini baru di branch."
"This frame isn't on the branch." = "Frame ini tidak ada di branch."
@@ -1506,8 +1505,6 @@ status = "draft — needs native review"
"Unresolve" = "Buka kembali"
"Unsaved changes" = "Perubahan belum disimpan"
"Unsaved changes in {names}. Save them before closing?" = "Perubahan belum disimpan di {names}. Simpan sebelum menutup?"
"Unsaved changes: save first ({save}), or close the tab with {alt}-click to discard" = "Perubahan belum disimpan: simpan dulu ({save}), atau tutup tab dengan {alt}-klik untuk membuangnya"
"Unsaved changes: save first, or close again with {key} to discard" = "Perubahan belum disimpan: simpan dulu, atau tutup lagi dengan {key} untuk membuangnya"
"Up" = "Atas"
"Update plugin" = "Perbarui plugin"
"Update" = "Perbarui"
-3
View File
@@ -1413,7 +1413,6 @@ status = "draft — needs native review"
"This page keeps the owner token only while it is open. Copy it to manage the file later: it is a password for this file and won't be shown again." = "Questa pagina conserva il token del proprietario solo finché resta aperta. Copialo per gestire il file in seguito: è una password per questo file e non verrà mostrato di nuovo."
"This condition is edited as an expression" = "Questa condizione viene modificata come espressione"
"This file" = "Questo file"
"This file has unsaved changes" = "Questo file ha modifiche non salvate"
"This file is too large to join here (up to {mb} MB)" = "Questo file è troppo grande per parteciparvi da qui (fino a {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." = "Questo file usa font che non sono installati su questo computer. Il loro testo è disegnato con un font di riserva finché non li installi."
"This file is on {server}, open only to you. Share it to let others in with a new code." = "Questo file è su {server}, aperto solo a te. Condividilo per far entrare altri con un nuovo codice."
@@ -1464,8 +1463,6 @@ status = "draft — needs native review"
"Unread" = "Non letto"
"Unresolve" = "Riapri"
"Unsaved changes" = "Modifiche non salvate"
"Unsaved changes: save first ({save}), or close the tab with {alt}-click to discard" = "Modifiche non salvate: salva prima ({save}), oppure chiudi la scheda con {alt}+clic per scartarle"
"Unsaved changes: save first, or close again with {key} to discard" = "Modifiche non salvate: salva prima, oppure chiudi di nuovo con {key} per scartarle"
"Up" = "Su"
"Update plugin" = "Aggiorna plugin"
"Update" = "Aggiorna"
-3
View File
@@ -1464,7 +1464,6 @@ status = "draft — needs native review"
"This page keeps the owner token only while it is open. Copy it to manage the file later: it is a password for this file and won't be shown again." = "このページは開いている間だけ所有者トークンを保持する。後でファイルを管理するにはコピーしておくこと:このファイルのパスワードにあたり、再表示されない。"
"This condition is edited as an expression" = "この条件は式として編集されている"
"This file" = "このファイル"
"This file has unsaved changes" = "このファイルには未保存の変更がある"
"This file is on {server}, open only to you. Share it to let others in with a new code." = "このファイルは {server} にあり、開けるのはあなただけ。共有すると、新しいコードで他の人が参加可能。"
"This frame is new on the branch." = "このフレームはブランチで新規作成。"
"This frame isn't on the branch." = "このフレームはブランチにない。"
@@ -1519,8 +1518,6 @@ status = "draft — needs native review"
"Unresolve" = "未解決に戻す"
"Unsaved changes" = "未保存の変更"
"Unsaved changes in {names}. Save them before closing?" = "{names} に未保存の変更がある。閉じる前に保存する?"
"Unsaved changes: save first ({save}), or close the tab with {alt}-click to discard" = "未保存の変更:先に保存する({save})か、{alt}-クリックでタブを閉じて破棄"
"Unsaved changes: save first, or close again with {key} to discard" = "未保存の変更:先に保存するか、もう一度 {key} で閉じて破棄"
"Up" = "上"
"Update" = "更新"
"Update from main" = "メインから更新"
-3
View File
@@ -1495,7 +1495,6 @@ status = "draft — reviewed by a native speaker, awaiting a maintainer's approv
"This page keeps the owner token only while it is open. Copy it to manage the file later: it is a password for this file and won't be shown again." = "ეს გვერდი მფლობელის ტოკენს მხოლოდ მანამ ინახავს, სანამ ღიაა. ფაილის მოგვიანებით სამართავად დააკოპირეთ ის: ეს ამ ფაილის პაროლია და აღარ გამოჩნდება."
"This condition is edited as an expression" = "ეს პირობა ექსპრესიად რედაქტირდება"
"This file" = "ეს ფაილი"
"This file has unsaved changes" = "ამ ფაილს შეუნახავი ცვლილებები აქვს"
"This file is too large to join here (up to {mb} 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." = "ეს ფაილი იყენებს შრიფტებს, რომლებიც ამ კომპიუტერზე დაინსტალირებული არ არის. მათი ტექსტი სათადარიგო შრიფტით გამოისახება, სანამ მათ არ დააინსტალირებთ."
"This file is on {server}, open only to you. Share it to let others in with a new code." = "ეს ფაილი {server}-ზეა და მხოლოდ თქვენთვისაა ღია. გააზიარეთ, რომ სხვები ახალი კოდით შემოუშვათ."
@@ -1554,8 +1553,6 @@ status = "draft — reviewed by a native speaker, awaiting a maintainer's approv
"Unresolve" = "ხელახლა გახსნა"
"Unsaved changes" = "შეუნახავი ცვლილებები"
"Unsaved changes in {names}. Save them before closing?" = "შეუნახავი ცვლილებები: {names}. შევინახოთ დახურვამდე?"
"Unsaved changes: save first ({save}), or close the tab with {alt}-click to discard" = "შეუნახავი ცვლილებები: ჯერ შეინახეთ ({save}) ან გასაუქმებლად დახურეთ ჩანართი {alt}+დაწკაპუნებით"
"Unsaved changes: save first, or close again with {key} to discard" = "შეუნახავი ცვლილებები: ჯერ შეინახეთ ან გასაუქმებლად ხელახლა დახურეთ {key}-ით"
"Up" = "ზემოთ"
"Update plugin" = "პლაგინის განახლება"
"Update" = "განახლება"
-3
View File
@@ -1399,7 +1399,6 @@ status = "draft — needs native review"
"This page keeps the owner token only while it is open. Copy it to manage the file later: it is a password for this file and won't be shown again." = "이 페이지는 열려 있는 동안에만 소유자 토큰을 보관함. 나중에 파일을 관리하려면 복사해 두세요: 이 파일의 비밀번호와 같으며 다시 표시되지 않음."
"This condition is edited as an expression" = "이 조건은 수식으로 편집됨"
"This file" = "이 파일"
"This file has unsaved changes" = "이 파일에 저장되지 않은 변경 사항이 있음"
"This file is on {server}, open only to you. Share it to let others in with a new code." = "이 파일은 {server}에 있으며 나만 열 수 있음. 공유하면 다른 사람들이 새 코드로 참여할 수 있음."
"This frame is new on the branch." = "이 프레임은 브랜치에서 새로 만들어졌음."
"This frame isn't on the branch." = "이 프레임은 브랜치에 없음."
@@ -1452,8 +1451,6 @@ status = "draft — needs native review"
"Unresolve" = "미해결로 되돌리기"
"Unsaved changes" = "저장되지 않은 변경 사항"
"Unsaved changes in {names}. Save them before closing?" = "{names}에 저장되지 않은 변경 사항이 있음. 닫기 전에 저장할까요?"
"Unsaved changes: save first ({save}), or close the tab with {alt}-click to discard" = "저장되지 않은 변경 사항: 먼저 저장({save})하거나 {alt}-클릭으로 탭을 닫아 버리세요"
"Unsaved changes: save first, or close again with {key} to discard" = "저장되지 않은 변경 사항: 먼저 저장하거나 {key}로 다시 닫아 버리세요"
"Up" = "위"
"Update" = "업데이트"
"Update from main" = "메인에서 업데이트"
-3
View File
@@ -1481,7 +1481,6 @@ status = "draft — needs native review"
"This page keeps the owner token only while it is open. Copy it to manage the file later: it is a password for this file and won't be shown again." = "Deze pagina bewaart het eigenaartoken alleen zolang ze open is. Kopieer het om het bestand later te beheren: het is het wachtwoord voor dit bestand en wordt niet opnieuw weergegeven."
"This condition is edited as an expression" = "Deze voorwaarde wordt als expressie bewerkt"
"This file" = "Dit bestand"
"This file has unsaved changes" = "Dit bestand heeft niet-opgeslagen wijzigingen"
"This file is on {server}, open only to you. Share it to let others in with a new code." = "Dit bestand staat op {server}, alleen toegankelijk voor jou. Deel het om anderen binnen te laten met een nieuwe code."
"This file uses fonts that aren't installed on this computer. Their text is drawn in a fallback font until you install them." = "Dit bestand gebruikt lettertypen die niet op deze computer zijn geïnstalleerd. Hun tekst wordt in een vervangend lettertype getoond tot je ze installeert."
"This frame is new on the branch." = "Dit frame is nieuw op de branch."
@@ -1536,8 +1535,6 @@ status = "draft — needs native review"
"Unresolve" = "Oplossing ongedaan maken"
"Unsaved changes" = "Niet-opgeslagen wijzigingen"
"Unsaved changes in {names}. Save them before closing?" = "Niet-opgeslagen wijzigingen in {names}. Opslaan voor het sluiten?"
"Unsaved changes: save first ({save}), or close the tab with {alt}-click to discard" = "Niet-opgeslagen wijzigingen: sla eerst op ({save}), of sluit het tabblad met {alt}-klik om ze te verwerpen"
"Unsaved changes: save first, or close again with {key} to discard" = "Niet-opgeslagen wijzigingen: sla eerst op, of sluit nogmaals met {key} om te verwerpen"
"Up" = "Omhoog"
"Update" = "Bijwerken"
"Update from main" = "Bijwerken vanaf main"
-3
View File
@@ -1488,7 +1488,6 @@ name = "Polski"
"This page keeps the owner token only while it is open. Copy it to manage the file later: it is a password for this file and won't be shown again." = "Ta strona przechowuje token właściciela tylko, dopóki jest otwarta. Skopiuj go, aby zarządzać plikiem później: jest to hasło do tego pliku i nie zostanie ponownie wyświetlone."
"This condition is edited as an expression" = "Ten warunek jest edytowany jako wyrażenie"
"This file" = "Ten plik"
"This file has unsaved changes" = "Ten plik ma niezapisane zmiany"
"This file is too large to join here (up to {mb} MB)" = "Ten plik jest zbyt duży, aby do niego dołączyć (maksymalnie {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." = "Ten plik używa czcionek, które nie są zainstalowane na tym komputerze. Do czasu ich zainstalowania tekst jest wyświetlany czcionką zastępczą."
"This file is on {server}, open only to you. Share it to let others in with a new code." = "Ten plik znajduje się na {server}, otwarty tylko dla Ciebie. Udostępnij go, aby wpuścić innych nowym kodem."
@@ -1545,8 +1544,6 @@ name = "Polski"
"Unresolve" = "Cofnij rozwiązanie"
"Unsaved changes" = "Niezapisane zmiany"
"Unsaved changes in {names}. Save them before closing?" = "Niezapisane zmiany w: {names}. Zapisać je przed zamknięciem?"
"Unsaved changes: save first ({save}), or close the tab with {alt}-click to discard" = "Niezapisane zmiany: najpierw zapisz ({save}) lub zamknij kartę z {alt}-kliknięciem, aby odrzucić"
"Unsaved changes: save first, or close again with {key} to discard" = "Niezapisane zmiany: najpierw zapisz lub naciśnij ponownie {key}, aby odrzucić"
"Up" = "W górę"
"Update plugin" = "Zaktualizuj wtyczkę"
"Update" = "Aktualizuj"
-3
View File
@@ -1397,7 +1397,6 @@ status = "draft — needs native review"
"This page keeps the owner token only while it is open. Copy it to manage the file later: it is a password for this file and won't be shown again." = "Esta página guarda o token de proprietário só enquanto estiver aberta. Copie-o para gerenciar o arquivo depois: ele é uma senha para este arquivo e não será mostrado de novo."
"This condition is edited as an expression" = "Esta condição é editada como expressão"
"This file" = "Este arquivo"
"This file has unsaved changes" = "Este arquivo tem alterações não salvas"
"This file is on {server}, open only to you. Share it to let others in with a new code." = "Este arquivo está em {server}, aberto somente para você. Compartilhe-o para deixar outras pessoas entrarem com um novo código."
"This frame is new on the branch." = "Este quadro é novo na branch."
"This frame isn't on the branch." = "Este quadro não está na branch."
@@ -1450,8 +1449,6 @@ status = "draft — needs native review"
"Unresolve" = "Reabrir"
"Unsaved changes" = "Alterações não salvas"
"Unsaved changes in {names}. Save them before closing?" = "Há alterações não salvas em {names}. Salvá-las antes de fechar?"
"Unsaved changes: save first ({save}), or close the tab with {alt}-click to discard" = "Alterações não salvas: salve primeiro ({save}) ou feche a aba com {alt}+clique para descartá-las"
"Unsaved changes: save first, or close again with {key} to discard" = "Alterações não salvas: salve primeiro ou feche de novo com {key} para descartá-las"
"Up" = "Para cima"
"Update" = "Atualizar"
"Update from main" = "Atualizar a partir do principal"
-3
View File
@@ -1488,7 +1488,6 @@ name = "Русский"
"This page keeps the owner token only while it is open. Copy it to manage the file later: it is a password for this file and won't be shown again." = "Эта страница хранит токен владельца, только пока она открыта. Скопируйте токен, чтобы управлять файлом позже: это пароль к файлу, он больше не будет показан."
"This condition is edited as an expression" = "Это условие редактируется как выражение"
"This file" = "Этот файл"
"This file has unsaved changes" = "В файле есть несохранённые изменения"
"This file is on {server}, open only to you. Share it to let others in with a new code." = "Файл находится на {server} и доступен только вам. Включите общий доступ, чтобы пригласить других по новому коду."
"This file is too large to join here (up to {mb} 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." = "В этом файле используются шрифты, не установленные на этом компьютере. Их текст отрисовывается запасным шрифтом, пока вы их не установите."
@@ -1545,8 +1544,6 @@ name = "Русский"
"Unresolve" = "Снова открыть"
"Unsaved changes" = "Несохранённые изменения"
"Unsaved changes in {names}. Save them before closing?" = "Несохранённые изменения в {names}. Сохранить их перед закрытием?"
"Unsaved changes: save first ({save}), or close the tab with {alt}-click to discard" = "Есть несохранённые изменения: сохраните ({save}) или закройте вкладку нажатием с {alt}, чтобы отбросить их"
"Unsaved changes: save first, or close again with {key} to discard" = "Есть несохранённые изменения: сохраните или закройте ещё раз с {key}, чтобы отбросить их"
"Up" = "Вверх"
"Update plugin" = "Обновить плагин"
"Update" = "Обновить"
-3
View File
@@ -1561,7 +1561,6 @@ name = "Svenska"
"This page keeps the owner token only while it is open. Copy it to manage the file later: it is a password for this file and won't be shown again." = "Den här sidan behåller ägartoken bara så länge den är öppen. Kopiera den om du vill hantera filen senare: den fungerar som ett lösenord för filen och visas inte igen."
"This condition is edited as an expression" = "Det här villkoret redigeras som ett uttryck"
"This file" = "Den här filen"
"This file has unsaved changes" = "Filen har osparade ändringar"
"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 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."
@@ -1623,8 +1622,6 @@ name = "Svenska"
"Unresolve" = "Markera som olöst"
"Unsaved changes" = "Osparade ändringar"
"Unsaved changes in {names}. Save them before closing?" = "Osparade ändringar i {names}. Vill du spara dem innan du stänger?"
"Unsaved changes: save first ({save}), or close the tab with {alt}-click to discard" = "Osparade ändringar: spara först ({save}), eller stäng fliken med {alt}-klick för att slänga dem"
"Unsaved changes: save first, or close again with {key} to discard" = "Osparade ändringar: spara först, eller stäng igen med {key} för att slänga dem"
"Up" = "Upp"
"Update check is not supported in the web browser." = "Sökning efter uppdateringar stöds inte i webbläsaren."
"Update plugin" = "Uppdatera plugin"
-3
View File
@@ -1470,7 +1470,6 @@ status = "draft — needs native review"
"This page keeps the owner token only while it is open. Copy it to manage the file later: it is a password for this file and won't be shown again." = "หน้านี้เก็บโทเคนเจ้าของไว้เฉพาะตอนที่เปิดอยู่ คัดลอกไว้เพื่อจัดการไฟล์ภายหลัง: โทเคนนี้เป็นรหัสผ่านของไฟล์นี้และจะไม่แสดงอีก"
"This condition is edited as an expression" = "เงื่อนไขนี้แก้ไขเป็นนิพจน์"
"This file" = "ไฟล์นี้"
"This file has unsaved changes" = "ไฟล์นี้มีการเปลี่ยนแปลงที่ยังไม่ได้บันทึก"
"This file is too large to join here (up to {mb} MB)" = "ไฟล์นี้ใหญ่เกินกว่าจะเข้าร่วมที่นี่ (สูงสุด {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." = "ไฟล์นี้ใช้ฟอนต์ที่ยังไม่ได้ติดตั้งในคอมพิวเตอร์เครื่องนี้ ข้อความจะแสดงด้วยฟอนต์สำรองจนกว่าคุณจะติดตั้ง"
"This file is on {server}, open only to you. Share it to let others in with a new code." = "ไฟล์นี้อยู่บน {server} เปิดได้เฉพาะคุณ แชร์เพื่อให้คนอื่นเข้าได้ด้วยรหัสใหม่"
@@ -1527,8 +1526,6 @@ status = "draft — needs native review"
"Unresolve" = "ยกเลิกสถานะแก้ไขแล้ว"
"Unsaved changes" = "การเปลี่ยนแปลงที่ยังไม่ได้บันทึก"
"Unsaved changes in {names}. Save them before closing?" = "{names} มีการเปลี่ยนแปลงที่ยังไม่ได้บันทึก บันทึกก่อนปิดหรือไม่"
"Unsaved changes: save first ({save}), or close the tab with {alt}-click to discard" = "มีการเปลี่ยนแปลงที่ยังไม่ได้บันทึก: บันทึกก่อน ({save}) หรือปิดแท็บด้วย {alt}+คลิกเพื่อทิ้งการเปลี่ยนแปลง"
"Unsaved changes: save first, or close again with {key} to discard" = "มีการเปลี่ยนแปลงที่ยังไม่ได้บันทึก: บันทึกก่อน หรือปิดอีกครั้งด้วย {key} เพื่อทิ้งการเปลี่ยนแปลง"
"Up" = "ขึ้น"
"Update plugin" = "อัปเดตปลั๊กอิน"
"Update" = "อัปเดต"
-3
View File
@@ -1452,7 +1452,6 @@ name = "Türkçe"
"This page keeps the owner token only while it is open. Copy it to manage the file later: it is a password for this file and won't be shown again." = "Bu sayfa sahip belirtecini yalnızca açık kaldığı sürece tutar. Dosyayı daha sonra yönetmek için kopyalayın: bu, dosyanın parolasıdır ve bir daha gösterilmez."
"This condition is edited as an expression" = "Bu koşul bir ifade olarak düzenleniyor"
"This file" = "Bu dosya"
"This file has unsaved changes" = "Bu dosyada kaydedilmemiş değişiklikler var"
"This file is on {server}, open only to you. Share it to let others in with a new code." = "Bu dosya {server} üzerinde ve yalnızca size açık. Başkalarını yeni bir kodla almak için paylaşın."
"This frame is new on the branch." = "Bu çerçeve dalda yeni."
"This frame isn't on the branch." = "Bu çerçeve dalda yok."
@@ -1505,8 +1504,6 @@ name = "Türkçe"
"Unresolve" = "Çözülmedi yap"
"Unsaved changes" = "Kaydedilmemiş değişiklikler"
"Unsaved changes in {names}. Save them before closing?" = "{names} içinde kaydedilmemiş değişiklikler var. Kapatmadan önce kaydedilsin mi?"
"Unsaved changes: save first ({save}), or close the tab with {alt}-click to discard" = "Kaydedilmemiş değişiklikler: önce kaydedin ({save}) veya atmak için sekmeyi {alt}-tıklamayla kapatın"
"Unsaved changes: save first, or close again with {key} to discard" = "Kaydedilmemiş değişiklikler: önce kaydedin veya atmak için {key} ile yeniden kapatın"
"Up" = "Yukarı"
"Update plugin" = "Eklentiyi güncelle"
"Update" = "Güncelle"
-3
View File
@@ -1414,7 +1414,6 @@ status = "draft — needs native review"
"This page keeps the owner token only while it is open. Copy it to manage the file later: it is a password for this file and won't be shown again." = "Trang này chỉ giữ mã thông báo chủ sở hữu khi còn mở. Hãy sao chép mã này để quản lý tệp sau: đây là mật khẩu của tệp và sẽ không hiển thị lại."
"This condition is edited as an expression" = "Điều kiện này được chỉnh sửa dưới dạng biểu thức"
"This file" = "Tệp này"
"This file has unsaved changes" = "Tệp này có thay đổi chưa lưu"
"This file is on {server}, open only to you. Share it to let others in with a new code." = "Tệp này đang ở trên {server} và chỉ mình bạn mở được. Hãy chia sẻ để người khác tham gia bằng mã mới."
"This file is too large to join here (up to {mb} MB)" = "Tệp này quá lớn để tham gia tại đây (tối đa {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." = "Tệp này sử dụng phông chữ chưa được cài đặt trên máy tính này. Văn bản sẽ hiển thị bằng phông chữ dự phòng cho đến khi bạn cài đặt chúng."
@@ -1465,8 +1464,6 @@ status = "draft — needs native review"
"Unread" = "Chưa đọc"
"Unresolve" = "Mở lại bình luận"
"Unsaved changes" = "Thay đổi chưa lưu"
"Unsaved changes: save first ({save}), or close the tab with {alt}-click to discard" = "Có thay đổi chưa lưu: hãy lưu trước ({save}), hoặc giữ {alt} khi nhấp đóng thẻ để hủy bỏ"
"Unsaved changes: save first, or close again with {key} to discard" = "Có thay đổi chưa lưu: hãy lưu trước, hoặc đóng lại lần nữa bằng {key} để hủy bỏ"
"Up" = "Lên"
"Update" = "Cập nhật"
"Update from main" = "Cập nhật từ nhánh chính"
-3
View File
@@ -1407,7 +1407,6 @@ status = "draft — needs native review"
"This page keeps the owner token only while it is open. Copy it to manage the file later: it is a password for this file and won't be shown again." = "此页面仅在打开期间保留所有者令牌。如需稍后管理此文件,请复制它:它相当于此文件的密码,且不会再次显示。"
"This condition is edited as an expression" = "此条件以表达式编辑"
"This file" = "此文件"
"This file has unsaved changes" = "此文件有未保存的更改"
"This file is on {server}, open only to you. Share it to let others in with a new code." = "此文件在 {server} 上,只有你能打开。分享后,其他人可以用新代码加入。"
"This frame is new on the branch." = "此画框是分支上新增的。"
"This frame isn't on the branch." = "此画框不在分支上。"
@@ -1460,8 +1459,6 @@ status = "draft — needs native review"
"Unresolve" = "取消解决"
"Unsaved changes" = "未保存的更改"
"Unsaved changes in {names}. Save them before closing?" = "{names} 中有未保存的更改。关闭前要保存吗?"
"Unsaved changes: save first ({save}), or close the tab with {alt}-click to discard" = "有未保存的更改:请先保存({save}),或按住 {alt} 点击关闭标签页以丢弃"
"Unsaved changes: save first, or close again with {key} to discard" = "有未保存的更改:请先保存,或再次按 {key} 关闭以丢弃"
"Up" = "向上"
"Update" = "更新"
"Update from main" = "从主文件更新"
+9 -3
View File
@@ -1,6 +1,6 @@
use super::{
Done, FigApp, LeftTab, MobilePanel, NodeId, PANEL_DEFAULT, RightTab, Value, comments, dev_compare, dialogs, home, i18n, import_report, json,
layers, live, menus, missing_fonts, plugins_ui, present, tabs, updates_ui, versions,
Done, FigApp, LeftTab, MobilePanel, NodeId, PANEL_DEFAULT, RightTab, Value, comments, dev_compare, dialogs, home, import_report, json, layers,
live, menus, missing_fonts, plugins_ui, present, tabs, updates_ui, versions,
};
impl FigApp {
@@ -270,7 +270,13 @@ impl FigApp {
let y = r.data.get("y").and_then(|v| v.as_f64()).unwrap_or(0.0);
comments::start_draft(self, x, y);
}
"dialog.unsaved" => self.toast(i18n::tr("This file has unsaved changes")),
"dialog.unsaved" => {
// `file.close` refused a file with unsaved changes: ask about it.
let i = r.data.get("index").and_then(Value::as_u64).and_then(|i| usize::try_from(i).ok()).unwrap_or(self.session.active);
if let Some(d) = self.session.docs.get(i) {
self.ui.close_tab = Some(crate::windows::TabClose::Ask(d.uid));
}
}
"dialog.plugins" => self.ui.plugins_window = true,
"dialog.pluginSave" => plugins_ui::save_file(self, &r.data),
"dialog.cursorChat" => live::open_chat(self),
+6
View File
@@ -178,6 +178,7 @@ pub fn keys(app: &mut FigApp, ctx: &Context) {
|| u.import_report.is_some()
|| u.missing_fonts.is_some()
|| u.confirm_close
|| matches!(u.close_tab, Some(crate::windows::TabClose::Ask(_)))
|| u.export_preview
|| u.shortcuts_panel
|| u.preferences
@@ -194,6 +195,10 @@ pub fn keys(app: &mut FigApp, ctx: &Context) {
{
return;
}
if matches!(u.close_tab, Some(crate::windows::TabClose::Ask(_))) {
u.close_tab = None;
return;
}
for flag in [&mut u.confirm_close, &mut u.export_preview, &mut u.shortcuts_panel, &mut u.preferences, &mut u.about] {
if std::mem::take(flag) {
return;
@@ -405,6 +410,7 @@ pub fn show(app: &mut FigApp, ctx: &Context) {
crate::live::publish_dialog(app, ctx);
crate::online::dialogs(app, ctx);
crate::windows::confirm_close(app, ctx);
crate::windows::confirm_close_tab(app, ctx);
}
/// Preferences ▸ Interface size: a size relative to the system's display scale (100 % is the
+3
View File
@@ -592,6 +592,8 @@ pub struct UiState {
pub online: online::OnlineUi,
/// The window was asked to close with unsaved changes: the question shows.
pub confirm_close: bool,
/// A tab was asked to close with unsaved changes: the question for it, then its save.
pub close_tab: Option<windows::TabClose>,
/// The file a Save as started from the unsaved-changes question is saving: the window closes
/// once it is saved, and stays open if the dialog is cancelled or the save fails.
pub close_after_save_as: Option<u64>,
@@ -715,6 +717,7 @@ impl Default for UiState {
live: live::LiveUi::default(),
online: online::OnlineUi::default(),
confirm_close: false,
close_tab: None,
close_after_save_as: None,
save_on_close: false,
}
+1 -5
View File
@@ -212,11 +212,7 @@ pub fn invoke(app: &mut FigApp, id: &str) {
}
"edit.paste" => paste(app, None, None),
"file.close" if app.session.ds().is_ok_and(|d| d.is_dirty() && d.branch_of.is_none()) => {
let save = pretty_shortcut(crate::shortcuts::shortcut("file.save").unwrap_or(""));
app.toast(trf(
"Unsaved changes: save first ({save}), or close the tab with {alt}-click to discard",
&[("save", &save), ("alt", &pretty_shortcut("Alt"))],
));
crate::windows::ask_close_tab(app, app.session.active);
}
_ => {
app.run(id, json!({}));
+5 -8
View File
@@ -303,18 +303,15 @@ fn drag_free_space(ui: &mut Ui, rect: Rect, from_x: f32, to_x: f32) {
}
}
/// Closes tab `i`; unsaved changes need a second close with Alt held.
/// Closes tab `i`: with unsaved changes it asks first (Don't Save / Cancel / Save), and Alt held
/// drops them without asking.
fn close_tab(app: &mut FigApp, ui: &Ui, i: usize) {
// A branch tab's edits live on in its main file: closing it loses nothing.
let dirty = app.session.docs.get(i).is_some_and(|d| d.is_dirty() && d.branch_of.is_none());
if dirty {
app.ui.find = None;
app.toast(trf("Unsaved changes: save first, or close again with {key} to discard", &[("key", &crate::toolbar::pretty_shortcut("Alt"))]));
if ui.input(|inp| inp.modifiers.alt) {
app.run("file.close", json!({"index": i, "force": true}));
}
if dirty && ui.input(|inp| inp.modifiers.alt) {
app.run("file.close", json!({"index": i, "force": true}));
} else {
app.run("file.close", json!({"index": i}));
crate::windows::ask_close_tab(app, i);
}
}
+109 -2
View File
@@ -16,13 +16,15 @@ struct Ui {
time: f64,
/// What the system reports about the window.
maximized: bool,
/// Modifier keys held during the next frames.
modifiers: egui::Modifiers,
}
impl Ui {
fn new(title_bar: TitleBar) -> Self {
let mut app = FigApp::new(Session::new(), Box::new(NoServices));
app.ui.title_bar = title_bar;
let mut u = Self { ctx: egui::Context::default(), app, time: 0.0, maximized: false };
let mut u = Self { ctx: egui::Context::default(), app, time: 0.0, maximized: false, modifiers: egui::Modifiers::NONE };
for _ in 0..3 {
u.frame(vec![]);
}
@@ -30,8 +32,9 @@ impl Ui {
}
/// Run one frame; returns the window commands it sent.
fn frame(&mut self, events: Vec<Event>) -> Vec<ViewportCommand> {
fn frame(&mut self, mut events: Vec<Event>) -> Vec<ViewportCommand> {
self.time += 1.0 / 60.0;
events.insert(0, Event::ModifiersChanged(self.modifiers));
let mut input = RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, SIZE)), time: Some(self.time), events, ..Default::default() };
input.viewports.entry(ViewportId::ROOT).or_default().maximized = Some(self.maximized);
let app = &mut self.app;
@@ -242,3 +245,107 @@ fn a_system_title_bar_leaves_the_window_alone() {
assert!(u.drag(EMPTY, vec2(40.0, 10.0)).is_empty());
assert!(u.drag(pos2(SIZE.x - 2.0, SIZE.y - 2.0), vec2(-30.0, -30.0)).iter().all(|c| !matches!(c, ViewportCommand::BeginResize(_))));
}
/// The main window's first tab closing: (its file's uid, the close button of the first tab).
fn first_tab(u: &Ui) -> (u64, Pos2) {
(u.app.session.docs[0].uid, pos2(156.0, 20.0))
}
/// Whether file `uid` is still open in a tab.
fn open(u: &Ui, uid: u64) -> bool {
u.app.session.docs.iter().any(|d| d.uid == uid)
}
/// Whether the unsaved-changes question is on screen.
fn asking(u: &Ui) -> bool {
u.ctx.memory(|memory| memory.area_rect(egui::Id::unique(Some(crate::i18n::tr("Unsaved changes")))).is_some())
}
/// #2032: closing a tab whose file has unsaved changes asks Don't Save / Cancel / Save, as
/// closing the window does, instead of a toast that left the tab open. Escape (or Cancel) keeps the
/// tab, Don't Save closes it, Alt+click closes it without asking, and File ▸ Close tab asks too.
#[test]
fn closing_a_tab_with_unsaved_changes_asks() {
use crate::windows::{CloseAnswer, TabClose, answer_close_tab};
let mut u = Ui::new(TitleBar::Custom);
u.app.session.execute("file.new", &json!({})).unwrap();
u.app.session.execute("doc.switch", &json!({"index": 0})).unwrap();
u.app.session.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap();
let (uid, close) = first_tab(&u);
u.frame(vec![Event::PointerMoved(close)]);
u.click(close);
u.frame(vec![]);
assert!(open(&u, uid), "the tab waits for an answer");
assert_eq!(u.app.ui.close_tab, Some(TabClose::Ask(uid)));
assert!(asking(&u), "the question is on screen");
// Escape is Cancel: the tab stays, with its changes.
u.frame(vec![Event::Key { key: egui::Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() }]);
u.frame(vec![]);
assert!(open(&u, uid), "Escape keeps the tab");
assert!(u.app.ui.close_tab.is_none(), "and ends the question");
// Don't Save closes it.
u.click(close);
assert_eq!(u.app.ui.close_tab, Some(TabClose::Ask(uid)));
let ctx = u.ctx.clone();
answer_close_tab(&mut u.app, &ctx, CloseAnswer::DontSave);
assert!(!open(&u, uid), "Don't Save closes the tab");
// Alt+click drops the changes without asking.
u.app.session.execute("doc.switch", &json!({"index": 0})).unwrap();
u.app.session.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap();
u.app.session.execute("file.new", &json!({})).unwrap();
let (uid, close) = first_tab(&u);
u.frame(vec![Event::PointerMoved(close)]);
u.modifiers = egui::Modifiers::ALT;
u.click(close);
u.modifiers = egui::Modifiers::NONE;
assert!(!open(&u, uid), "Alt+click closes without asking");
assert!(u.app.ui.close_tab.is_none());
// File ▸ Close tab (and its shortcut) asks about the file shown.
u.app.session.execute("doc.switch", &json!({"index": 0})).unwrap();
u.app.session.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap();
let uid = u.app.session.docs[0].uid;
crate::menus::invoke(&mut u.app, "file.close");
assert_eq!(u.app.ui.close_tab, Some(TabClose::Ask(uid)));
assert!(open(&u, uid));
}
/// #2032: Save in the question saves the file, then closes its tab; a file without a path asks
/// where with Save as first and the tab stays open until it is saved.
#[test]
fn save_in_the_tab_question_saves_then_closes() {
let dir = std::env::temp_dir().join(format!("figcraft-tab-close-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("kept.figcraft");
let mut u = Ui::new(TitleBar::Custom);
u.app.session.execute("file.new", &json!({})).unwrap();
u.app.session.execute("doc.switch", &json!({"index": 0})).unwrap();
u.app.session.execute("file.save", &json!({"path": path.to_string_lossy()})).unwrap();
let saved = std::fs::metadata(&path).unwrap().len();
u.app.session.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 40, "height": 40})).unwrap();
let (uid, close) = first_tab(&u);
u.frame(vec![Event::PointerMoved(close)]);
u.click(close);
u.frame(vec![]);
assert!(asking(&u));
// Enter answers Save.
u.frame(vec![Event::Key { key: egui::Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() }]);
for _ in 0..3 {
u.frame(vec![]);
}
assert!(!open(&u, uid), "saved, then closed");
assert!(std::fs::metadata(&path).unwrap().len() > saved, "the rectangle was saved");
// No path: Save as starts (headless: no dialog can open), and the tab stays open.
u.app.session.execute("doc.switch", &json!({"index": 0})).unwrap();
u.app.session.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 40, "height": 40})).unwrap();
let (uid, close) = first_tab(&u);
u.frame(vec![Event::PointerMoved(close)]);
u.click(close);
let ctx = u.ctx.clone();
crate::windows::answer_close_tab(&mut u.app, &ctx, crate::windows::CloseAnswer::Save);
for _ in 0..3 {
u.frame(vec![]);
}
assert!(open(&u, uid), "nothing saved it: the tab stays");
assert!(u.app.ui.close_tab.is_none());
let _ = std::fs::remove_dir_all(&dir);
}
+117 -2
View File
@@ -445,6 +445,15 @@ pub(crate) fn confirm_close(app: &mut FigApp, ctx: &Context) {
app.ui.confirm_close = false;
return;
}
if let Some(a) = unsaved_question(ctx, &names) {
answer_close(app, ctx, a);
}
}
/// The unsaved-changes question about files `names` (Don't Save / Cancel / Save, as the other
/// Craft apps ask it; Enter answers Save, the dialog's close button Cancel): `None` while it is
/// unanswered.
fn unsaved_question(ctx: &Context, names: &[String]) -> Option<CloseAnswer> {
let t = crate::theme::Tokens::get(ctx);
let mut open = true;
let mut answer = None;
@@ -471,8 +480,114 @@ pub(crate) fn confirm_close(app: &mut FigApp, ctx: &Context) {
if !open {
answer = Some(CloseAnswer::Cancel);
}
if let Some(a) = answer {
answer_close(app, ctx, a);
answer
}
/// Where closing a tab whose file has unsaved changes is (each step names the file's uid).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TabClose {
/// The unsaved-changes question shows for it.
Ask(u64),
/// Save was answered: it saves next frame, once a panel field the click took the keyboard
/// from has committed what was typed, then the tab closes.
Save(u64),
/// Its Save as dialog (a file without a path, or a .fig) is open: the tab closes once the file
/// is saved, and stays open if the dialog is cancelled or the save fails.
AfterSaveAs(u64),
}
/// Close tab `i` (its X, a middle click, File ▸ Close tab): at once when its file has no unsaved
/// changes (a branch tab's edits are in its main file already), else after the unsaved-changes
/// question ([`confirm_close_tab`]).
pub(crate) fn ask_close_tab(app: &mut FigApp, i: usize) {
let Some(d) = app.session.docs.get(i) else { return };
if d.is_dirty() && d.branch_of.is_none() {
app.ui.find = None;
app.ui.close_tab = Some(TabClose::Ask(d.uid));
} else {
app.run("file.close", json!({"index": i}));
}
}
/// Act on the unsaved-changes question's answer for the tab it asks about: Cancel keeps the tab,
/// Don't Save closes it and drops the changes, Save saves the file next frame, then closes it.
pub(crate) fn answer_close_tab(app: &mut FigApp, ctx: &Context, answer: CloseAnswer) {
let Some(TabClose::Ask(uid)) = app.ui.close_tab else { return };
app.ui.close_tab = None;
match answer {
CloseAnswer::Cancel => {}
CloseAnswer::DontSave => {
if let Some(i) = tab_of(app, uid) {
app.run("file.close", json!({"index": i, "force": true}));
}
}
CloseAnswer::Save => {
app.ui.close_tab = Some(TabClose::Save(uid));
ctx.request_repaint();
}
}
}
/// Tab `i` of the session whose file has uid `uid`, if it is still open.
fn tab_of(app: &FigApp, uid: u64) -> Option<usize> {
app.session.docs.iter().position(|d| d.uid == uid)
}
/// The unsaved-changes question for a closing tab and what its answer starts: Don't Save closes
/// the tab and drops the changes, Cancel keeps it, Save saves the file (Save as for one without a
/// path or saved as .fig) and closes the tab once it is saved.
pub(crate) fn confirm_close_tab(app: &mut FigApp, ctx: &Context) {
let Some(step) = app.ui.close_tab else { return };
let (TabClose::Ask(uid) | TabClose::Save(uid) | TabClose::AfterSaveAs(uid)) = step;
let Some(i) = tab_of(app, uid) else {
app.ui.close_tab = None;
return;
};
let dirty = app.session.docs.get(i).is_some_and(DocState::is_dirty);
match step {
TabClose::Ask(_) if !dirty => {
// Saved meanwhile: nothing is lost by closing.
app.ui.close_tab = None;
app.run("file.close", json!({"index": i}));
}
TabClose::Ask(_) => {
let names: Vec<String> = app.session.docs.get(i).map(|d| d.doc.name.clone()).into_iter().collect();
if let Some(a) = unsaved_question(ctx, &names) {
answer_close_tab(app, ctx, a);
}
}
TabClose::Save(_) => {
app.ui.close_tab = None;
// Saving acts on the file shown.
app.session.active = i;
let native = app.session.docs.get(i).and_then(|d| d.path.as_deref()).is_some_and(|p| {
!std::path::Path::new(p).extension().and_then(std::ffi::OsStr::to_str).is_some_and(|e| e.eq_ignore_ascii_case("fig"))
});
if !native {
app.ui.close_tab = Some(TabClose::AfterSaveAs(uid));
app.save_as_dialog();
if app.pending_dialog.is_none() {
// No dialog could start: the tab stays open.
app.ui.close_tab = None;
}
ctx.request_repaint();
return;
}
if app.run("file.save", json!({})).is_some()
&& let Some(i) = tab_of(app, uid)
{
app.run("file.close", json!({"index": i}));
}
}
TabClose::AfterSaveAs(_) => {
if app.pending_dialog.is_some() {
return;
}
app.ui.close_tab = None;
if !dirty {
app.run("file.close", json!({"index": i}));
}
}
}
}
+3 -2
View File
@@ -1,6 +1,6 @@
# UI and interaction parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-04 · **Change:** patch (a press on the open gradient's handles or stops on the canvas keeps the colour picker open through the drag, as its own control (#1852); a click with the Bend tool on a sharp point curves it, its handles along the line between its neighbours, as Edit vector layers says (#1875); wrap style: Auto, Balance and Pretty on a text layer decide where its lines break, as Explore text properties says (#1827), with the Type settings control, `text.wrapStyle`, `node.set wrapStyle`, inspect and the CSS `text-wrap`; Backspace or Delete at the start of a list item keeps the item's indentation level, as the lists article says; max lines on auto-width text keep it auto width, hugging that many lines, as the text properties article says; superscript and subscript are synthesised where the font has no glyphs for them, from the standard glyphs shrunk and shifted by the font's OS/2 metrics, and a run mixing available and unavailable glyphs is synthesised whole, as Explore text properties says (#1828); Alt+Enter while editing text opens the link at the caret, as the accessibility page documents; Minimize, by its shortcut or the button by the file name, collapses the navigation bar and both sidebars as the help center says, with the right sidebar back while something is selected (#1835); the interface size keys of the help center's accessibility page: Cmd/Ctrl+Shift+Alt with =, - and 0 step the interface through the Preferences sizes and back to the system's size; Cmd/Ctrl+Alt+arrows rotate the selection a degree at a time, 15 with Shift, as the help center's accessibility page says; they resized before; the Layers panel answers Ctrl+1 as well as Alt+1 on Windows and Linux, the two keys the help center gives it; the colour picker's Image tab shows a placeholder and Upload from computer, an asset preview, and a file dropped on it replaces the image, keeping the fill mode, crop and position (#1682); the keyboard's Menu key opens the context menu, as Shift+F10 does; the eyedropper with Cmd/Ctrl+Shift makes a colour style or variable of what it picks; sidebar number and text fields keep their text vertically centered when focused, including typography spacing; with the pen out, Enter or a double-click ends the path and edits the vector with the Move tool instead of drawing on (#1430); the Actions palette offers only commands that run from it, and no second row reading like a menu item: Place image opens the file dialog (#1574); Preferences ▸ Scroll wheel zoom: a mouse wheel zooms the canvas about the pointer and Cmd/Ctrl+wheel scrolls (#1390); boolean operations refuse input past their caps, and a boolean group past them draws without its result (#1104); the selection and hover outlines of rounded rectangles and frames follow their corners (#1506); colour picker review fixes for text undo, fitted sidebars, moving controls and gradient variables; Corner smoothing: an original icon and the shared number-field drag gesture, preserving fractional baselines; guides snap to layers when dragged; Crop image: Object ▸ Crop image, the right-click menu and the image fill row enter crop mode (`image.crop`, #1464); the Design panel's sections on one grid — 16 / 8 margins, 88 + 8 + 88 + 8 + 24 columns, 24 pt rows, 4 pt between a row's buttons (#858); the toolbar on one 32 pt grid, without its Resources button (#1210); colour picker LCH and OKLCH models; an XML (Android Vector Drawable) export format; Context menus: the Menu key reaches egui 0.37 but is unbound (row done → partial); the Text on a path tool; board tool keys pinned; community fixes: the text box by dragging, the font picker keys, the pen path on leaving Design, a still canvas in vector edit mode, a scrolling pages list, colour emoji, readable new text in frames, system theme on first run; the Small caps sample in Type settings drawn from capitals; recent files as cards; vector edit mode: hovering, hatching, selecting and dragging filled regions; home screen: Templates tab, first-run templates and Getting started tips; number fields: scrub rates, arrow steps, arithmetic with brackets, relative edits of mixed values; outline mode's hidden layers and object bounds; Hide when publishing; cursors per tool and modifier; panel widths; dark pages for files made in the dark theme; drag threshold and double-clicks into text in groups; rotation origin on Alt+R and placing with Enter: no documented shortcut missing; keyboard shortcut parity suite and bindings; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and the vector edit tail; canvas navigation, guides and grids behaviour suite and fixes; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; layers panel keyboard: F2 rename, Shift+F10 context menu, F8 box selection; selection and transLine truncated
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** patch (closing a tab whose file has unsaved changes (its X, a middle click, File ▸ Close tab) asks Don't Save / Cancel / Save as closing the window does, instead of a toast that left it open (#2032); a press on the open gradient's handles or stops on the canvas keeps the colour picker open through the drag, as its own control (#1852); a click with the Bend tool on a sharp point curves it, its handles along the line between its neighbours, as Edit vector layers says (#1875); wrap style: Auto, Balance and Pretty on a text layer decide where its lines break, as Explore text properties says (#1827), with the Type settings control, `text.wrapStyle`, `node.set wrapStyle`, inspect and the CSS `text-wrap`; Backspace or Delete at the start of a list item keeps the item's indentation level, as the lists article says; max lines on auto-width text keep it auto width, hugging that many lines, as the text properties article says; superscript and subscript are synthesised where the font has no glyphs for them, from the standard glyphs shrunk and shifted by the font's OS/2 metrics, and a run mixing available and unavailable glyphs is synthesised whole, as Explore text properties says (#1828); Alt+Enter while editing text opens the link at the caret, as the accessibility page documents; Minimize, by its shortcut or the button by the file name, collapses the navigation bar and both sidebars as the help center says, with the right sidebar back while something is selected (#1835); the interface size keys of the help center's accessibility page: Cmd/Ctrl+Shift+Alt with =, - and 0 step the interface through the Preferences sizes and back to the system's size; Cmd/Ctrl+Alt+arrows rotate the selection a degree at a time, 15 with Shift, as the help center's accessibility page says; they resized before; the Layers panel answers Ctrl+1 as well as Alt+1 on Windows and Linux, the two keys the help center gives it; the colour picker's Image tab shows a placeholder and Upload from computer, an asset preview, and a file dropped on it replaces the image, keeping the fill mode, crop and position (#1682); the keyboard's Menu key opens the context menu, as Shift+F10 does; the eyedropper with Cmd/Ctrl+Shift makes a colour style or variable of what it picks; sidebar number and text fields keep their text vertically centered when focused, including typography spacing; with the pen out, Enter or a double-click ends the path and edits the vector with the Move tool instead of drawing on (#1430); the Actions palette offers only commands that run from it, and no second row reading like a menu item: Place image opens the file dialog (#1574); Preferences ▸ Scroll wheel zoom: a mouse wheel zooms the canvas about the pointer and Cmd/Ctrl+wheel scrolls (#1390); boolean operations refuse input past their caps, and a boolean group past them draws without its result (#1104); the selection and hover outlines of rounded rectangles and frames follow their corners (#1506); colour picker review fixes for text undo, fitted sidebars, moving controls and gradient variables; Corner smoothing: an original icon and the shared number-field drag gesture, preserving fractional baselines; guides snap to layers when dragged; Crop image: Object ▸ Crop image, the right-click menu and the image fill row enter crop mode (`image.crop`, #1464); the Design panel's sections on one grid — 16 / 8 margins, 88 + 8 + 88 + 8 + 24 columns, 24 pt rows, 4 pt between a row's buttons (#858); the toolbar on one 32 pt grid, without its Resources button (#1210); colour picker LCH and OKLCH models; an XML (Android Vector Drawable) export format; Context menus: the Menu key reaches egui 0.37 but is unbound (row done → partial); the Text on a path tool; board tool keys pinned; community fixes: the text box by dragging, the font picker keys, the pen path on leaving Design, a still canvas in vector edit mode, a scrolling pages list, colour emoji, readable new text in frames, system theme on first run; the Small caps sample in Type settings drawn from capitals; recent files as cards; vector edit mode: hovering, hatching, selecting and dragging filled regions; home screen: Templates tab, first-run templates and Getting started tips; number fields: scrub rates, arrow steps, arithmetic with brackets, relative edits of mixed values; outline mode's hidden layers and object bounds; Hide when publishing; cursors per tool and modifier; panel widths; dark pages for files made in the dark theme; drag threshold and double-clicks into text in groups; rotation origin on Alt+R and placing with Enter: no documented shortcut missing; keyboard shortcut parity suite and bindings; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and the vector edit tail; canvas navigation, guides and grids behaviour suite and fixes; shapes and vector behaviour suite and fixes; paint aLine truncated
Fine canvas control is what Figma users feel first: handles, snapping, nudging, modifier keys,
drag behaviour, numeric precision. This checklist covers the tools, gestures and panels. Status:
@@ -15,7 +15,7 @@ interaction feel not yet compared.
| Item | Status | Notes |
|---|---|---|
| UI3 layout: tab strip as title bar, icon rail, file header, pages + layers, floating bottom toolbar with flyouts, Design / Prototype / Inspect panels | done | frameless window on Windows/Linux with drawn buttons; on a Wayland compositor that offers no server-side decorations (GNOME) the window gets winit's Adwaita controls instead (#592; checked by its author on GNOME, KDE not run) |
| UI3 layout: tab strip as title bar, icon rail, file header, pages + layers, floating bottom toolbar with flyouts, Design / Prototype / Inspect panels | done | frameless window on Windows/Linux with drawn buttons; tabs reorder by dragging along the strip, and closing one whose file has unsaved changes asks Don't Save / Cancel / Save (Alt+close drops the changes at once, #2032); on a Wayland compositor that offers no server-side decorations (GNOME) the window gets winit's Adwaita controls instead (#592; checked by its author on GNOME, KDE not run) |
| Light and dark themes | done | colours from `theme::Tokens`; a file or a page made while the dark theme shows gets a `#1E1E1E` page (`#F5F5F5` in the light theme), its own from then on: changing the theme repaints no page and opened files keep theirs; the file the app opens with takes the theme once it is known; the CLI, MCP and exports have no theme and keep `#F5F5F5`; text drawn with the Text tool is white on a dark page and black on a light one (so is text a command makes in the app without naming a fill; headless sessions keep black) (`theme_pages_tests`, `dark_page_text_tests`) |
| Minimize UI (Shift+Cmd/Ctrl+\\, the Minimize button by the file name) and Show/Hide UI (Cmd/Ctrl+\\) | done | Minimize collapses the navigation bar and both sidebars and keeps the toolbar and the panel widths; "If you select an object while the panels are minimized, the left sidebar remains minimized while the right sidebar expands [...] Once all objects are deselected, the right panel minimizes again" (Explore the navigation bar and left sidebar); a small Expand button at the top left of the canvas puts them back (FigCraft's own, the article names no control for it). Hide UI takes the toolbar too (`panel_width_tests`, `shortcuts_tests`; #1835) |
| Resizable panels | done | each panel's edge drags its width (left 200–480, right 240–480, 240 by default; a double-click on the edge or View ▸ Panels ▸ Reset panel widths puts the default back); the widths are each window's own, the main window's are kept in the preferences when a drag ends and come back at the next start, a new window opens with those of the window it comes from; Minimize UI hides the panels and brings them back as they were; the view keeps its centre and zoom while a panel is resized; in a window too small for them the panels give way so the canvas keeps 160 px (the left one first), down to the smallest window (800×500) (`panel_width_tests`). Not compared with Figma's widths; no snap points |
@@ -178,6 +178,7 @@ in the table. `crates/ui-egui/src/shortcut_keys_tests.rs` drives the keys a look
| Date | Change | Summary |
|---|---|---|
| 2026-11-06 | patch | M7, #2032: closing a tab whose file has unsaved changes, by its X, a middle click, File ▸ Close tab or its shortcut, only showed a toast ("save first, or close again with Alt to discard") and left the tab open, so closing seemed not to work. It now asks the unsaved-changes question the window asks (`windows::TabClose`): Don't Save closes the tab, Cancel or Escape keeps it, Save saves the file and then closes it (a file without a path, or saved as .fig, asks where with Save as first and stays open if that is cancelled); Alt+close still drops the changes at once, and `file.close`'s `dialog.unsaved` request asks too. The three toasts left the catalogs. Tests: `titlebar_tests::closing_a_tab_with_unsaved_changes_asks`, `titlebar_tests::save_in_the_tab_question_saves_then_closes`. Reordering tabs by dragging, the issue's other half, landed on 2026-10-19 (`dragging_a_tab_along_the_bar_reorders_it_without_detaching`) |
| 2026-11-04 | patch | M1, #1852: a press on one of the open gradient's handles or stops on the canvas is the colour picker's own, not a press outside it (Figma keeps the picker and the handles up while a handle or stop is dragged; the stops are made to be dragged: "You can click and drag a stop to change its placement on the gradient", Use gradients as a fill or stroke). The canvas took the press and began the drag, then the picker, drawn later in the same frame, saw a press outside its card and closed, committing the drag's interaction at once; the handles vanished with it and the drag went on without an undo step of its own. Now the picker stays while a gradient handle or stop drags, and the drag is one undo step; a press elsewhere on the canvas still closes it. A handle drag whose button goes up off the canvas (over a panel or the picker) ends there too, instead of waiting, uncommitted, to move the handle to the next press on the canvas. Tests: `colorpicker_selection_tests::dragging_a_gradient_handle_on_the_canvas_keeps_the_picker_open`, `colorpicker_selection_tests::a_gradient_drag_released_off_the_canvas_ends_there`. |
| 2026-11-02 | patch | M8, #1875: a click with the Bend tool on a sharp point between two segments gives it handles along the line between its neighbours, a third of the way to each ("Click on a point or path where you want to add a curve", Edit vector layers); a click on a curved point still makes it sharp, a drag still sets the handles by hand, an endpoint is left alone. Between a handle's end and a point both within reach of the pointer, the nearer one on the page is hit (the handle on a tie), so a short path's point (its handles a few pixels long) can be clicked again. Before, only a drag curved a point, and a short one left two tiny handles in its direction. Tests: `vectoredit::tests::short_handles_leave_their_point_clickable`, `vectoredit::tests::a_bend_click_curves_a_sharp_point_between_its_neighbours`, `bend_tool_smooths_and_sharpens_points`. Numbers unchanged. |
| 2026-11-02 | minor | M4, #1827: wrap style on a text layer (`WrapStyle`: Auto, Balance, Pretty): Balance re-breaks each paragraph at the narrowest width that keeps its line count (bisection over the greedy breaker, as browsers balance), Pretty narrows one of the last four lines just enough so the last line isn't one word; `text.wrapStyle`, `node.set wrapStyle` (`textWrap` too), inspect `wrapStyle`, Dev Mode CSS `text-wrap` (Tailwind `text-balance` / `text-pretty`), the Type settings control under Vertical trim. Per-paragraph settings, text-style support and the `.fig` import/export mapping stay open. Test: `text_behaviour_tests::balance_and_pretty_wrap_styles_break_lines_as_documented`. |