mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-24 10:16:59 -06:00
Merge main: fix(macos): make Dock Quit cancelable; test(recovery): make failed republishing fixtures deterministic
Conflict: Cargo.toml: next's winit patch (rust-windowing/winit#4694, Windows mixed-DPI drags) kept; main's skylartaylor/winit patch held back (Cargo takes one patch per crate; both are single commits on v0.30.13 for different platforms, no published commit has both) Conflict: Cargo.lock: next's winit source; main's ui-egui tempfile dev-dependency taken Conflict: apps/designcraft/src/main.rs: next's startup; main's macOS with_close_requested_on_terminate block left out (needs main's winit patch); apps/designcraft/Cargo.toml keeps next's (no macOS winit dependency) Conflict: ROADMAP.md: headers combined, logs both Conflict: docs/target-app-parity.md: headers combined
This commit is contained in:
Generated
+1
@@ -1178,6 +1178,7 @@ dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"toml",
|
||||
"unicode-bidi",
|
||||
"unicode-bidi-mirroring",
|
||||
|
||||
+2
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
**Stage: pre-alpha** · next: alpha, ~5–7 h away; beta ~28 points and ~420–750 h beyond that (the core-workflow gate, not the percentage: ready for real work is already ~47%)
|
||||
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-12 · **Change:** minor (text threads by hand; the alpha gate waits on Primary Text Frame page fixes; Japanese print, import and table controls localized; Live corners (#186); Optical kerning from outlines (#181); window and menu quit confirmation; failed writes preserve live save and collection state; spring-loaded Zoom navigation; orderly native exit recovery; Pages panel parents) · **Target:** Adobe InDesign 2026
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-12 · **Change:** minor (text threads by hand; the alpha gate waits on Primary Text Frame page fixes; Japanese print, import and table controls localized; Live corners (#186); Optical kerning from outlines (#181); window and menu quit confirmation; failed writes preserve live save and collection state; spring-loaded Zoom navigation; orderly native exit recovery; Pages panel parents; cancelable macOS Dock Quit) · **Target:** Adobe InDesign 2026
|
||||
|
||||
DesignCraft aims at full Adobe InDesign parity, and to be better: faster, open (documented JSON
|
||||
format + IDML), scriptable by agents (MCP), and available on the web.
|
||||
@@ -155,6 +155,7 @@ Newest first.
|
||||
- **2026-10-12** · Hyphenation Zone applies to the Single-line Composer only, as in InDesign. With the default Paragraph Composer, ragged text hyphenates where InDesign does (aper-natem), giving the same line breaks. No readiness percentage change.
|
||||
- **2026-10-12** · Text frame auto-size: Height Only, Width Only, Height and Width (also keeping proportions) frames grow and shrink to fit their text after edits and IDML import, around their reference point, with minimum sizes and No Line Breaks, in the same undo step (#180). No readiness percentage change.
|
||||
- **2026-10-12** · Text Frame Options › Balance Columns (`object.textFrameOptions {balanceColumns}`, IDML `VerticalBalanceColumns`) evens out the columns of the frame a story ends in, from its last column break on; frames the text runs on from fill normally (#609). No readiness percentage change.
|
||||
- **2026-10-12** · macOS Dock Quit uses the same unsaved-document prompts as window and menu Quit. Repeated requests keep the current answers; Cancel retains every tab and unsaved recovery checkpoint, and completed Save/Don't Save answers exit through the ordinary event loop. No readiness percentage change.
|
||||
- **2026-10-12** · Orderly native exit writes the latest recovery state after finalizing current quit approvals; incomplete or stale plans keep their documents, and Undo to the saved state clears stale recovery. Unavailable or failed recovery is reported. Cancelable macOS Dock Quit remains open (#544). No readiness percentage change.
|
||||
- **2026-10-12** · Windows: dragging the window across monitors with different display scales keeps its size instead of growing until it crashes (winit pinned to its 0.30.x backport of the fix). The GPU surface may be as large as the adapter's largest texture (16384 px on DirectX 12, Vulkan and Metal hardware) instead of 8192, and a wgpu error is logged instead of closing the app. No readiness percentage change.
|
||||
- **2026-10-12** · Recovered documents keep a checkpoint until saved, closed or reverted, including a second crash before autosave. Restarted document IDs and explicit recovery folders preserve other snapshots; failed republishing retains the original. No readiness percentage change.
|
||||
|
||||
@@ -656,6 +656,10 @@ pub fn list(dir: &Path) -> Vec<(u64, Value)> {
|
||||
/// Entries belonging to documents already open in this session's recovery folder are left alone.
|
||||
/// Returns the opened documents' indices.
|
||||
pub fn open(s: &mut Session, dir: &Path) -> Result<Vec<usize>> {
|
||||
open_with_publisher(s, dir, |path, bytes| publish_recovery_file(path, bytes, false))
|
||||
}
|
||||
|
||||
fn open_with_publisher(s: &mut Session, dir: &Path, mut publish: impl FnMut(&Path, &[u8]) -> Result<()>) -> Result<Vec<usize>> {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
validate_recovery_dir(dir)?;
|
||||
@@ -712,14 +716,14 @@ pub fn open(s: &mut Session, dir: &Path) -> Result<Vec<usize>> {
|
||||
// clean up every snapshot this document actually owns rather than an unrelated same uid.
|
||||
if let Some(state) = s.docs.get_mut(index) {
|
||||
let (document, metadata) = files(&destination, state.uid);
|
||||
let publish = designcraft_format::save(state.doc.as_ref())
|
||||
let published = designcraft_format::save(state.doc.as_ref())
|
||||
.map_err(|e| EngineError::Other(e.to_string()))
|
||||
.and_then(|bytes| publish_recovery_file(&document, &bytes, false));
|
||||
match publish {
|
||||
.and_then(|bytes| publish(&document, &bytes));
|
||||
match published {
|
||||
Ok(()) => {
|
||||
state.recovery_entries.push((destination.clone(), state.uid));
|
||||
let meta = json!({"path": state.path, "title": state.title(), "saved": designcraft_doc::vars::now()});
|
||||
match publish_recovery_file(&metadata, meta.to_string().as_bytes(), false) {
|
||||
match publish(&metadata, meta.to_string().as_bytes()) {
|
||||
Ok(()) => {
|
||||
discard(dir, uid);
|
||||
state.recovery_entries = vec![(destination.clone(), state.uid)];
|
||||
@@ -809,20 +813,32 @@ mod tests {
|
||||
assert_eq!(list(dir.path()).len(), 1);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
/// Directory collisions fail even for privileged runners. Creating the blocker in the
|
||||
/// publisher occurs after UID reservation, which would otherwise skip an occupied path.
|
||||
fn open_with_failed_republication(s: &mut Session, dir: &Path) -> Result<Vec<usize>> {
|
||||
let mut attempts = 0;
|
||||
let opened = open_with_publisher(s, dir, |path, bytes| {
|
||||
assert_eq!(path.extension().and_then(|extension| extension.to_str()), Some("designcraft"));
|
||||
std::fs::create_dir(path).unwrap();
|
||||
let result = publish_recovery_file(path, bytes, false);
|
||||
std::fs::remove_dir(path).unwrap();
|
||||
assert!(result.is_err(), "a directory must obstruct actual recovery publication");
|
||||
attempts += 1;
|
||||
result
|
||||
});
|
||||
assert_eq!(attempts, 1, "the fixture must exercise the actual failed publication");
|
||||
opened
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_failed_recovery_rewrite_preserves_the_original() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
assert_eq!(save(&unsaved("keep the original"), dir.path()).unwrap(), 1);
|
||||
let entries = list(dir.path());
|
||||
let original = files(dir.path(), entries[0].0).0;
|
||||
let bytes = std::fs::read(&original).unwrap();
|
||||
std::fs::set_permissions(dir.path(), std::fs::Permissions::from_mode(0o500)).unwrap();
|
||||
let mut reopened = Session::new();
|
||||
let result = open(&mut reopened, dir.path());
|
||||
// Restore before assertions so a failing test does not leave an unremovable fixture.
|
||||
std::fs::set_permissions(dir.path(), std::fs::Permissions::from_mode(0o700)).unwrap();
|
||||
let result = open_with_failed_republication(&mut reopened, dir.path());
|
||||
assert_eq!(result.unwrap().len(), 1);
|
||||
assert_eq!(texts(&reopened), ["keep the original"]);
|
||||
assert_eq!(std::fs::read(original).unwrap(), bytes);
|
||||
@@ -926,9 +942,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn failed_republication_session() -> (Session, tempfile::TempDir, u64) {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let mut session = Session::new();
|
||||
session.execute("file.new", &json!({})).unwrap();
|
||||
@@ -937,15 +951,12 @@ mod tests {
|
||||
original.docs[0].uid = live_uid;
|
||||
save(&original, root.path()).unwrap();
|
||||
session.recovery_dir = Some(root.path().to_path_buf());
|
||||
std::fs::set_permissions(root.path(), std::fs::Permissions::from_mode(0o500)).unwrap();
|
||||
let recovered = open(&mut session, root.path());
|
||||
std::fs::set_permissions(root.path(), std::fs::Permissions::from_mode(0o700)).unwrap();
|
||||
let recovered = open_with_failed_republication(&mut session, root.path());
|
||||
assert_eq!(recovered.unwrap().len(), 1);
|
||||
assert_ne!(session.docs[1].uid, live_uid);
|
||||
(session, root, live_uid)
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn retained_source_survives_a_coincident_live_uid_checkpoint_failure() {
|
||||
let (mut session, root, live_uid) = failed_republication_session();
|
||||
@@ -958,7 +969,6 @@ mod tests {
|
||||
assert_eq!(std::fs::read(files(root.path(), live_uid).0).unwrap(), original);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn clean_autosave_preserves_another_documents_retained_source() {
|
||||
let (mut session, root, live_uid) = failed_republication_session();
|
||||
@@ -970,7 +980,6 @@ mod tests {
|
||||
assert_eq!(std::fs::read(checkpoint).unwrap(), original);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn closing_a_coincident_document_preserves_another_documents_retained_source() {
|
||||
let (mut session, root, live_uid) = failed_republication_session();
|
||||
@@ -980,7 +989,6 @@ mod tests {
|
||||
assert_eq!(std::fs::read(checkpoint).unwrap(), original);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn saving_a_coincident_document_preserves_another_documents_retained_source() {
|
||||
let (mut session, root, live_uid) = failed_republication_session();
|
||||
@@ -991,7 +999,6 @@ mod tests {
|
||||
assert_eq!(std::fs::read(checkpoint).unwrap(), original);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn reverting_a_coincident_document_preserves_another_documents_retained_source() {
|
||||
let (mut session, root, live_uid) = failed_republication_session();
|
||||
@@ -1006,7 +1013,6 @@ mod tests {
|
||||
assert_eq!(std::fs::read(checkpoint).unwrap(), original);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn retained_source_provenance_is_consumed_before_its_uid_is_reused() {
|
||||
let (mut session, root, live_uid) = failed_republication_session();
|
||||
|
||||
@@ -28,6 +28,7 @@ serde_json = { workspace = true }
|
||||
log = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
designcraft-idml = { workspace = true }
|
||||
zip = { workspace = true }
|
||||
# Headless (offscreen wgpu) rendering of the whole window for UI screenshots: `examples/ui_shot.rs`.
|
||||
|
||||
@@ -1097,6 +1097,11 @@ impl DesignApp {
|
||||
return;
|
||||
}
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::CancelClose);
|
||||
// Dock Quit can arrive while the app is hidden or minimized. Make its question
|
||||
// visible through the normal safe window commands before asking about documents.
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Visible(true));
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Minimized(false));
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Focus);
|
||||
if self.quitting.is_none() {
|
||||
self.quitting = Some(QuitState { original_active: self.session.active().map(|d| d.uid), accepted: HashMap::new(), pending: None });
|
||||
}
|
||||
@@ -1793,6 +1798,105 @@ mod tests {
|
||||
dialogs::confirm(app).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_quit_prompts_restore_window_visibility_without_focusing_completed_quits() {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = app_with_unsaved_document();
|
||||
let commands = quit_frame(&mut app, &ctx, true);
|
||||
assert!(commands.contains(&egui::ViewportCommand::CancelClose));
|
||||
for command in [egui::ViewportCommand::Visible(true), egui::ViewportCommand::Minimized(false), egui::ViewportCommand::Focus] {
|
||||
assert!(commands.contains(&command), "unsaved prompt must restore the window: {commands:?}");
|
||||
}
|
||||
discard_quit_dialog(&mut app);
|
||||
let approved = quit_frame(&mut app, &ctx, false);
|
||||
assert!(approved.contains(&egui::ViewportCommand::Close));
|
||||
let completed = quit_frame(&mut app, &ctx, true);
|
||||
assert!(app.session.documents().is_empty());
|
||||
|
||||
let mut clean = DesignApp::new(designcraft_engine::Session::new(), Services::default());
|
||||
clean.run("file.new", json!({})).unwrap();
|
||||
let clean_close = quit_frame(&mut clean, &ctx, true);
|
||||
let mut control = app_with_unsaved_document();
|
||||
control.quit_without_asking = true;
|
||||
let control_close = quit_frame(&mut control, &ctx, true);
|
||||
for commands in [&approved, &completed, &clean_close, &control_close] {
|
||||
assert!(
|
||||
!commands.iter().any(|command| matches!(
|
||||
command,
|
||||
egui::ViewportCommand::Visible(true) | egui::ViewportCommand::Minimized(false) | egui::ViewportCommand::Focus
|
||||
)),
|
||||
"completed and immediate quits must not activate the window: {commands:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Dock Quit may arrive again while the first request's dialogs are still open.
|
||||
#[test]
|
||||
fn repeated_native_quit_requests_preserve_partial_approvals_and_cancel() {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = app_with_unsaved_document();
|
||||
second_unsaved_document(&mut app);
|
||||
let original_active = app.session.active().unwrap().uid;
|
||||
let uids: Vec<_> = app.session.documents().iter().map(|d| d.uid).collect();
|
||||
let docs: Vec<_> = app.session.documents().iter().map(|d| d.doc.clone()).collect();
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
app.session.recovery_dir = Some(directory.path().to_path_buf());
|
||||
app.run("file.recovery.save", json!({})).unwrap();
|
||||
assert!(quit_frame(&mut app, &ctx, true).contains(&egui::ViewportCommand::CancelClose));
|
||||
assert_eq!(dialog_uid(&app), Some(uids[0]));
|
||||
discard_quit_dialog(&mut app);
|
||||
quit_frame(&mut app, &ctx, false);
|
||||
assert_eq!(dialog_uid(&app), Some(uids[1]));
|
||||
for _ in 0..3 {
|
||||
assert!(quit_frame(&mut app, &ctx, true).contains(&egui::ViewportCommand::CancelClose));
|
||||
assert_eq!(dialog_uid(&app), Some(uids[1]));
|
||||
}
|
||||
app.ui.dialog = None; // Cancel the second document, retaining the first Don't Save answer.
|
||||
assert!(!quit_frame(&mut app, &ctx, false).contains(&egui::ViewportCommand::Close));
|
||||
assert_eq!(app.session.active().unwrap().uid, original_active);
|
||||
assert_eq!(app.session.documents().iter().map(|d| d.uid).collect::<Vec<_>>(), uids);
|
||||
for (document, original) in app.session.documents().iter().zip(docs) {
|
||||
assert!(std::sync::Arc::ptr_eq(&document.doc, &original));
|
||||
assert!(document.is_dirty());
|
||||
assert!(!document.history.undo.is_empty());
|
||||
assert!(directory.path().join(format!("{}.designcraft", document.uid)).exists());
|
||||
}
|
||||
// A fresh Dock Quit must ask about the first document again after cancellation.
|
||||
assert!(quit_frame(&mut app, &ctx, true).contains(&egui::ViewportCommand::CancelClose));
|
||||
assert_eq!(dialog_uid(&app), Some(uids[0]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_native_quit_requests_save_and_discard_only_after_all_answers() {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = app_with_unsaved_document();
|
||||
second_unsaved_document(&mut app);
|
||||
let uids: Vec<_> = app.session.documents().iter().map(|d| d.uid).collect();
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let path = directory.path().join("saved.designcraft");
|
||||
let chosen = path.to_string_lossy().to_string();
|
||||
app.services.pick_save = Some(Box::new(move |_| Some(chosen.clone())));
|
||||
app.session.recovery_dir = Some(directory.path().join("recovery"));
|
||||
app.run("file.recovery.save", json!({})).unwrap();
|
||||
quit_frame(&mut app, &ctx, true);
|
||||
assert_eq!(dialog_uid(&app), Some(uids[0]));
|
||||
dialogs::confirm(&mut app).unwrap(); // Save the first document.
|
||||
assert!(!app.session.documents()[0].is_dirty());
|
||||
quit_frame(&mut app, &ctx, false);
|
||||
assert_eq!(dialog_uid(&app), Some(uids[1]));
|
||||
assert!(quit_frame(&mut app, &ctx, true).contains(&egui::ViewportCommand::CancelClose));
|
||||
assert_eq!(dialog_uid(&app), Some(uids[1]));
|
||||
discard_quit_dialog(&mut app);
|
||||
assert!(quit_frame(&mut app, &ctx, false).contains(&egui::ViewportCommand::Close));
|
||||
assert_eq!(app.session.documents().len(), 2);
|
||||
assert!(app.session.recovery_dir.as_ref().unwrap().join(format!("{}.designcraft", uids[1])).exists());
|
||||
assert!(quit_frame(&mut app, &ctx, true).is_empty());
|
||||
assert!(app.session.documents().is_empty());
|
||||
assert!(path.exists());
|
||||
assert_eq!(designcraft_engine::Session::new().execute("file.open", &json!({"path": path})).unwrap()["index"], 0);
|
||||
assert_eq!(std::fs::read_dir(app.session.recovery_dir.as_ref().unwrap()).unwrap().count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quitting_first_cancel_restores_original_clean_active_document() {
|
||||
let ctx = egui::Context::default();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# DesignCraft architecture
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (orderly native exit recovery) · **Target:** Adobe InDesign 2026
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (cancelable macOS Dock Quit) · **Target:** Adobe InDesign 2026
|
||||
|
||||
How DesignCraft is built today. The design notes behind it are in `plan/architecture.md`
|
||||
(local, gitignored); this file describes what the code on `main` does. Agents: AGENTS.md has the
|
||||
@@ -60,8 +60,12 @@ command is replayable as a script. Escaped panics during a command or an import/
|
||||
error (`engine/src/guard.rs`), never a lost document; native recovery is written periodically
|
||||
into each running app's locked folder. Orderly native shutdown finalizes complete, current quit
|
||||
approvals and writes the remaining documents' latest recovery state, including clean-document
|
||||
cleanup after Undo. Unavailable or failed recovery is reported. Cancelable macOS Dock Quit
|
||||
remains open (#544).
|
||||
cleanup after Undo. Unavailable or failed recovery is reported. The desktop opts into a pinned
|
||||
winit 0.30 patch that cancels AppKit termination and queues ordinary window close requests,
|
||||
including Quit from the macOS Dock. The existing unsaved-document transaction can cancel
|
||||
those requests or exit after current approvals are complete. A pending prompt restores and
|
||||
focuses a hidden or minimized window; completed and immediate quits do not activate it. The
|
||||
AppKit callback never runs the confirmation dialogs inside a modal termination loop.
|
||||
|
||||
## Composition and rendering
|
||||
|
||||
@@ -100,5 +104,6 @@ builds (on manifest changes), packaging lint and the release workflow; `xtask ci
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-12 | minor | Documented cancelable Dock Quit through the native event-loop boundary |
|
||||
| 2026-10-12 | minor | Documented orderly native exit recovery and the remaining cancelable Dock Quit gap |
|
||||
| 2026-10-10 | major | Created from the code on main (92649ff) and `plan/architecture.md` |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# DesignCraft parity with Adobe InDesign
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (text threads by hand; image clipping paths from IDML; anchored objects wrap text; Tabs panel listed as present (#343); spring-loaded Zoom navigation; recovery checkpoint retention) · **Target:** Adobe InDesign 2026 (21.6.0.57, macOS)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (text threads by hand; image clipping paths from IDML; anchored objects wrap text; Tabs panel listed as present (#343); spring-loaded Zoom navigation; recovery checkpoint retention; cancelable macOS Dock Quit) · **Target:** Adobe InDesign 2026 (21.6.0.57, macOS)
|
||||
|
||||
The authoritative assessment of how close DesignCraft is to Adobe InDesign. [ROADMAP.md](../ROADMAP.md)
|
||||
summarizes it; [gaps.md](gaps.md) lists every known shortfall one at a time; the feature-by-feature
|
||||
@@ -291,7 +291,7 @@ It is evidence of breadth: each item exists. It is not a claim of InDesign-level
|
||||
- Native engine saves, Save a Copy, snippets, books, libraries, swatches and engine/MCP exports use same-folder atomic replacement, as do desktop Services writes and CLI raster exports. Failed writes before replacement retain the original; existing Unix permissions apply before staging document bytes, and already-open readers retain the old bytes. Parent-folder flushing is best effort, so this is not a power-loss durability guarantee.
|
||||
- Save As prepares destination-relative data-source paths before writing and installs them only after a successful save. Failed Save As retains the document, saved state and history; Save a Copy rebases only the saved copy. Library and book collection edits likewise update live values only after persistence succeeds.
|
||||
- Crash recovery: unsaved documents are written every 30 s into a locked folder owned by that running app and reopened (unsaved, remembering their files) after a crash. Startup takes entries from ended runs and leaves live runs alone; writes and cleanup validate current folder and lock ownership. Failed isolation disables recovery with a warning. Recovered checkpoints remain available through another crash before autosave; restarting IDs and explicit recovery from another folder preserve unopened snapshots. Successful Save, Close or Revert clears the document's checkpoints, including manual recovery while autosave is disabled; failed republishing keeps the original. Concurrent older versions still use unlocked root entries; mixed-version isolation remains open. File › Revert and Save a Copy…; `file.recovery.*`, `file.revert`, `file.saveACopy`.
|
||||
- Window close and application/menu Quit ask about every unsaved document. Cancel or a cancelled/failed save keeps all tabs open; Don't Save approvals discard nothing until the complete plan is current. Later edits and new documents are checked before closing. Control-channel quit remains immediate; a cancelable macOS Dock Quit prompt is still missing (#544).
|
||||
- Window close, application/menu Quit and macOS Dock Quit ask about every unsaved document. Cancel or a cancelled/failed save keeps all tabs open; Don't Save approvals discard nothing until the complete plan is current. Later edits and new documents are checked before closing. Repeated native requests preserve an active dialog and partial approvals. Control-channel quit remains immediate.
|
||||
- Graphics backend chosen before the window exists (DirectX 12 on Windows, Vulkan on Linux, Metal on macOS, OpenGL as the fallback; `WGPU_BACKEND` overrides) with a start-up fallback: a backend whose start never showed a frame is skipped at the next start and the status bar says so (`gpu.json`). Found on an AMD hybrid-graphics laptop whose Vulkan driver faulted at the first present.
|
||||
- Graphics adapter chosen by the app: the GPU that drives the primary display (Windows `EnumDisplayDevices`, Linux sysfs), then the power preference (`WGPU_POWER_PREF`, `WGPU_ADAPTER_NAME` override). An adapter that fails or hangs while the window starts is left out and the app starts again on the next one, then on the next backend, with software renderers last; wgpu's indirect-call check is off (it lost the Metal device on an Intel Mac, #334); why the app stopped is logged, also when the window never opened (#273). Ported from VectorCraft.
|
||||
- Edit › Paste Into (copied objects become a frame's content, clipped by it; rendered, PDF and IDML both ways — verified in InDesign 2026); Polygon Settings (sides, star inset; double-click the Polygon tool).
|
||||
|
||||
Reference in New Issue
Block a user