From 5bd5d241a0825a4af8551cc28c6c97b9b31f2d66 Mon Sep 17 00:00:00 2001 From: aiongg Date: Thu, 22 Oct 2026 16:21:59 +0000 Subject: [PATCH 1/5] G3: a native window per workbook window Each engine window is drawn in its own native window through craft-window's immediate-viewport driver (desktop only; the web app keeps one window). The root viewport shows one window and adopts another when its window closes. - WindowUi: the cell draft, text-box draft, dialog, Backstage, message, toast, notice, grid state, name box, key tips, selected chart, Tab run and edit mode are per window. SheetApp's fields hold the active window's; the others are parked and swapped in when a window is drawn or activated, and after any command that switched windows. - Session::show_window makes a window current for drawing without counting as an activation. - The ribbon in each window acts on it; menu bar commands and scripts go to the window focused last. Leaving a window enters its draft as Enter would. - A window's close button runs window.close: one of several windows on a workbook closes without asking, a workbook's last asks when unsaved. SheetApp::request_quit (macOS Quit item, Dock quit) asks about each unsaved workbook in its window; Cancel stops. Save Changes answers moved to SheetApp::answer_save_changes. - The last window closed quits on Windows and Linux; macOS hides the root and keeps running until New or Open. - Fonts are bound for every open workbook, since each may be showing. craft-window is pinned to the aiongg/craft-libs window-list branch until it merges into storytold/craft-libs. --- Cargo.lock | 10 + Cargo.toml | 4 + ROADMAP.md | 4 +- apps/gridcraft/src/apple_events.rs | 3 +- apps/gridcraft/src/main.rs | 20 +- apps/gridcraft/src/native_menu.rs | 14 +- crates/engine/src/windows.rs | 20 +- crates/ui-egui/Cargo.toml | 1 + crates/ui-egui/src/dialogs.rs | 25 +-- crates/ui-egui/src/doc_windows.rs | 227 ++++++++++++++++++++++ crates/ui-egui/src/lib.rs | 148 +++++++++++++-- crates/ui-egui/src/window_ui.rs | 133 +++++++++++++ crates/ui-egui/tests/doc_windows.rs | 284 ++++++++++++++++++++++++++++ docs/hardware-parity.md | 5 +- docs/ui-parity.md | 7 +- 15 files changed, 840 insertions(+), 65 deletions(-) create mode 100644 crates/ui-egui/src/doc_windows.rs create mode 100644 crates/ui-egui/src/window_ui.rs create mode 100644 crates/ui-egui/tests/doc_windows.rs diff --git a/Cargo.lock b/Cargo.lock index e041aafbc..77b3b801a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -758,6 +758,15 @@ dependencies = [ "egui", ] +[[package]] +name = "craft-window" +version = "0.1.0" +source = "git+https://github.com/aiongg/craft-libs?rev=4fbdea5fcf86ba893d9bc03685d4531cfdd4efc2#4fbdea5fcf86ba893d9bc03685d4531cfdd4efc2" +dependencies = [ + "egui", + "wayland-client", +] + [[package]] name = "crc32fast" version = "1.5.2" @@ -1636,6 +1645,7 @@ name = "gridcraft-ui-egui" version = "0.10.0" dependencies = [ "craft-ui", + "craft-window", "egui", "egui_kittest", "epaint_default_fonts", diff --git a/Cargo.toml b/Cargo.toml index e8a6dc67e..b28d7eb33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,10 @@ sys-locale = "0.3" skrifa = { version = "0.44", default-features = false, features = ["std"] } # Real font bytes for tests (Ubuntu Light, compiled into epaint). epaint_default_fonts = "0.36" +# One native window per workbook: the window list, Window menu titles and the immediate-viewport +# driver (craft-libs). Pinned to the aiongg fork's `window-list` branch; repins to +# storytold/craft-libs once that branch merges there. +craft-window = { version = "0.1.0", git = "https://github.com/aiongg/craft-libs", rev = "4fbdea5fcf86ba893d9bc03685d4531cfdd4efc2", default-features = false, features = ["egui"] } [workspace.lints.rust] unsafe_code = "forbid" diff --git a/ROADMAP.md b/ROADMAP.md index bf25cc3a9..38c394fb0 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~25 points of "ready for real work" and ~160–240 h away -> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-22 · **Change:** minor (no lost workbook after New: drafts stay in their workbook, Switch Windows, macOS Quit asks to save; New Window shares the workbook) · **Target:** Microsoft Excel (Microsoft 365) +> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-22 · **Change:** minor (a native window per workbook window; no lost workbook after New: drafts stay in their workbook, Switch Windows, macOS Quit asks to save; New Window shares the workbook) · **Target:** Microsoft Excel (Microsoft 365) GridCraft aims at full Microsoft Excel parity, and at being better in places: faster, open (XLSX is the native format), fully scriptable by agents (MCP, control channel, CLI), and on the web. @@ -135,6 +135,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in | Date | What landed | |---|---| +| 2026-10-22 | On the desktop every workbook window is a native window of its own: New, Open and New Window open one, each keeps its own cell draft, dialog and ribbon, and the macOS menu bar acts on the window focused last. Closing one of several windows on a workbook doesn't ask; closing a workbook's last window, or quitting, asks about unsaved changes in that window. The last window closed quits on Windows and Linux; macOS keeps running with the menu bar. First craft-libs `craft-window` use (window list and viewport driver) | | 2026-10-22 | View › New Window opens a second window on the same workbook (it used to open a copy whose Save overwrote the original): each window has its own selection, active sheet and scroll, titles read "Book1:1" / "Book1:2", and Switch Windows, Hide, Unhide and Close act per window. The control channel, CLI `send` and MCP tools take an optional `window` or `doc` id and default to the active window; `window.list` (MCP `list_windows`) lists the windows | | 2026-10-22 | Typing in a cell and then choosing File › New no longer strands the first workbook: the entry is committed into its own workbook before any File or window command (a refused entry stays open and stops the command), View › Switch Windows and the macOS Window menu list the open workbooks with unsaved markers, `view.switchWindows {doc}` switches by a stable workbook id, and the macOS Quit item asks about unsaved workbooks | | 2026-10-21 | Page Setup paper sizes match Japanese Excel: `B4`/`B5`/`B6` are the JIS sizes (ISO B4/B5/B6 stay reachable by name), so a Japanese B5 worksheet prints at 182×257 mm; Japanese Postcard (100×148 mm), Double Postcard (148×200 mm), Envelope #3/#4 (120×235/90×205 mm), Kaku #2/#3 (240×332/216×277 mm) and A6 are added, both sizes reach the PDF page (`file.exportPdf`/`file.print` MediaBox) and the size survives an XLSX round-trip; the Page Layout Size menu lists them. `B4`/`B5` used to be the ISO sizes, which is changed here | @@ -222,6 +223,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in | Date | Change | Summary | |---|---|---| +| 2026-10-22 | minor | Progress log: a native window per workbook window | | 2026-10-22 | minor | Progress log: New Window shares the workbook; window ids for scripts and MCP | | 2026-10-22 | minor | Progress log: drafts stay in their workbook, Switch Windows, macOS Quit asks to save | | 2026-10-21 | minor | Page Setup paper sizes: JIS B4/B5/B6 (ISO by name), Japanese postcards and envelopes, A6; PDF page size follows, XLSX round-trips | diff --git a/apps/gridcraft/src/apple_events.rs b/apps/gridcraft/src/apple_events.rs index 405ee89a0..4d596f957 100644 --- a/apps/gridcraft/src/apple_events.rs +++ b/apps/gridcraft/src/apple_events.rs @@ -43,7 +43,8 @@ pub fn poll(inbox: &Inbox, app: &mut SheetApp, ctx: &egui::Context) { app.open_path(p); } } - Event::Quit => ctx.send_viewport_cmd(egui::ViewportCommand::Close), + // Quit from the Dock or the system: each unsaved workbook is asked about in its window. + Event::Quit => app.request_quit(ctx), } } } diff --git a/apps/gridcraft/src/main.rs b/apps/gridcraft/src/main.rs index 8aad9aeb8..c0b50038d 100644 --- a/apps/gridcraft/src/main.rs +++ b/apps/gridcraft/src/main.rs @@ -22,24 +22,27 @@ use std::cell::RefCell; use std::rc::Rc; use gridcraft_engine::Session; +use gridcraft_ui_egui::doc_windows::DocWindows; use gridcraft_ui_egui::{Services, SheetApp}; use serde_json::json; -struct App(SheetApp, #[cfg(target_os = "macos")] Option, #[cfg(target_os = "macos")] fmv_macos_events::Inbox); +/// The app, its native window per workbook window, and (macOS) the menu bar and Finder events. +struct App(SheetApp, DocWindows, #[cfg(target_os = "macos")] Option, #[cfg(target_os = "macos")] fmv_macos_events::Inbox); impl eframe::App for App { fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { #[cfg(target_os = "macos")] { - if self.1.is_none() && std::env::var_os("GRIDCRAFT_NO_NATIVE_MENU").is_none() { - self.1 = Some(native_menu::NativeMenu::install(ctx)); + if self.2.is_none() && std::env::var_os("GRIDCRAFT_NO_NATIVE_MENU").is_none() { + self.2 = Some(native_menu::NativeMenu::install(ctx)); } - if let Some(m) = &mut self.1 { - m.poll(&mut self.0, ctx); + if let Some(m) = &mut self.2 { + m.poll(&mut self.0, ctx, self.1.focused_viewport()); } - apple_events::poll(&self.2, &mut self.0, ctx); + apple_events::poll(&self.3, &mut self.0, ctx); } self.0.logic(ctx); + self.1.logic(&mut self.0, ctx); self.0.poll_printing(ctx); if ctx.input(|input| input.viewport().close_requested()) && self.0.printing.busy() { ctx.send_viewport_cmd(egui::ViewportCommand::CancelClose); @@ -55,7 +58,7 @@ impl eframe::App for App { } } fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) { - self.0.ui(ui); + self.1.show(&mut self.0, ui); } fn on_exit(&mut self) { print::finish_pending_jobs(); @@ -320,6 +323,7 @@ fn main() -> eframe::Result<()> { if let Some(i) = icon() { viewport = viewport.with_icon(i); } + let window_builder = viewport.clone(); let options = eframe::NativeOptions { viewport, ..Default::default() }; // Before eframe creates the wgpu instance: Windows starts on DirectX 12, or OpenGL when // DirectX 12 has no GPU driver, and never touches Vulkan unless asked (see graphics.rs). @@ -357,8 +361,10 @@ fn main() -> eframe::Result<()> { Err(e) => log::error!("control channel on port {port}: {e}"), } } + let windows = DocWindows::new(&mut app, window_builder.clone(), DocWindows::platform_quits_with_last_window()); Ok(Box::new(App( app, + windows, #[cfg(target_os = "macos")] None, #[cfg(target_os = "macos")] diff --git a/apps/gridcraft/src/native_menu.rs b/apps/gridcraft/src/native_menu.rs index 826a12b31..9559c47d7 100644 --- a/apps/gridcraft/src/native_menu.rs +++ b/apps/gridcraft/src/native_menu.rs @@ -203,8 +203,8 @@ pub struct NativeMenu { /// Menu id of the Window menu item that brings workbook `doc` forward. const WINDOW_ITEM: &str = "window:id:"; -/// The Quit item: closing the window asks about unsaved workbooks first, which AppKit's own -/// Quit (terminate:) would skip. +/// The Quit item: asks about each unsaved workbook in its window first +/// (`SheetApp::request_quit`), which AppKit's own Quit (terminate:) would skip. const QUIT: &str = "app.quit"; fn select_theme(app: &mut SheetApp, id: &str) -> bool { @@ -311,11 +311,13 @@ impl NativeMenu { } } - pub fn poll(&mut self, app: &mut SheetApp, ctx: &egui::Context) { + /// Runs the chosen menu items. Commands go to the active window, the most recently focused; + /// `viewport` is its egui viewport, for the items that act on its input. + pub fn poll(&mut self, app: &mut SheetApp, ctx: &egui::Context, viewport: egui::ViewportId) { while let Ok(ev) = self.events.try_recv() { let id = ev.id.as_ref().to_string(); if id == QUIT { - ctx.send_viewport_cmd(egui::ViewportCommand::Close); + app.request_quit(ctx); continue; } if let Some(window) = id.strip_prefix(WINDOW_ITEM).and_then(|w| w.parse::().ok()) { @@ -331,11 +333,11 @@ impl NativeMenu { _ => None, }; if let Some(command) = clipboard { - ctx.send_viewport_cmd(command); + ctx.send_viewport_cmd_to(viewport, command); continue; } if id == "edit.undo" || id == "edit.redo" { - ctx.input_mut(|i| { + ctx.input_mut_for(viewport, |i| { i.events.push(egui::Event::Key { key: egui::Key::Z, physical_key: None, diff --git a/crates/engine/src/windows.rs b/crates/engine/src/windows.rs index 8dcb92011..4441166db 100644 --- a/crates/engine/src/windows.rs +++ b/crates/engine/src/windows.rs @@ -166,6 +166,16 @@ impl Session { /// Makes a window active, swapping its view into its workbook. Unknown ids are ignored. pub fn activate_window(&mut self, id: u64) { + self.enter_window(id, true); + } + + /// Makes a window the one commands act on while the interface draws it, without counting as + /// an activation: the most recently active window of each workbook stays the same. + pub fn show_window(&mut self, id: u64) { + self.enter_window(id, false); + } + + fn enter_window(&mut self, id: u64, recent: bool) { let Some(wi) = self.windows.iter().position(|w| w.id == id) else { return }; let Some(w) = self.windows.get(wi) else { return }; let doc = w.doc_uid; @@ -185,10 +195,12 @@ impl Session { } } } - self.activation = self.activation.saturating_add(1); - let stamp = self.activation; - if let Some(w) = self.windows.get_mut(wi) { - w.last_active = stamp; + if recent { + self.activation = self.activation.saturating_add(1); + let stamp = self.activation; + if let Some(w) = self.windows.get_mut(wi) { + w.last_active = stamp; + } } self.active_window = Some(id); if let Some(di) = self.document_index(doc) { diff --git a/crates/ui-egui/Cargo.toml b/crates/ui-egui/Cargo.toml index 85a24dda9..626d5e7fa 100644 --- a/crates/ui-egui/Cargo.toml +++ b/crates/ui-egui/Cargo.toml @@ -33,6 +33,7 @@ gridcraft-xlsx = { workspace = true, features = ["test-support"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] # The desktop's preferred language for the first-run interface language. sys-locale = { workspace = true } +craft-window = { workspace = true } [target.'cfg(target_arch = "wasm32")'.dependencies] # Read the browser's preferred language for the first-run web interface language. diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index be474d37f..55c0819df 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -1323,29 +1323,14 @@ pub fn show(app: &mut SheetApp, ctx: &egui::Context) { } "saveChanges" => { ui.label(format!("Do you want to save the changes you made to {}?", d.values.get("title").and_then(Json::as_str).unwrap_or("this workbook"))); - // Asked while quitting: once answered, the next unsaved workbook is asked about. let quitting = d.values.get("quit").and_then(Json::as_bool) == Some(true); ui.horizontal(|ui| { - if ui.button("Don't Save").clicked() { - app.run_or_alert("file.close", json!({"force": true, "document": true})); - if quitting { - app.continue_quit(ui.ctx()); + for (label, answer) in + [("Don't Save", crate::SaveAnswer::DontSave), ("Cancel", crate::SaveAnswer::Cancel), ("Save", crate::SaveAnswer::Save)] + { + if ui.button(label).clicked() && app.answer_save_changes(answer, quitting, ui.ctx()) { + open = false; } - open = false; - } - if ui.button("Cancel").clicked() { - open = false; - } - // File operations must include the cell the user is still editing (as the File > Save menu item already does). - if ui.button("Save").clicked() && app.commit_edit(0, 0, false, false) { - app.run_or_alert("file.save", json!({})); - if app.session.active().is_some_and(|x| !x.is_dirty()) { - app.run_or_alert("file.close", json!({"force": true, "document": true})); - if quitting { - app.continue_quit(ui.ctx()); - } - } - open = false; } }); } diff --git a/crates/ui-egui/src/doc_windows.rs b/crates/ui-egui/src/doc_windows.rs new file mode 100644 index 000000000..88ba3967a --- /dev/null +++ b/crates/ui-egui/src/doc_windows.rs @@ -0,0 +1,227 @@ +//! Desktop: each workbook window is a native window of its own. +//! +//! The engine owns the windows ([`Session::windows`]); [`DocWindows`] mirrors the visible ones in +//! a [`WindowList`] and draws each through [`Viewports`]: the root viewport shows one window and +//! every other window is an immediate viewport. Before a window draws, it becomes the engine's +//! window and its interface state is swapped in ([`SheetApp::show_window`]); after the frame the +//! focused window is active again, so menu commands, scripts and shortcuts outside a window's own +//! input go to the window the user worked in last. +//! +//! Close and quit: +//! - A window's close button closes that window (`window.close`): one of several windows on a +//! workbook closes without asking; a workbook's last window asks Save / Don't Save / Cancel when +//! it has unsaved changes. +//! - Quit ([`SheetApp::request_quit`]) asks about each unsaved workbook in its window; Cancel stops. +//! - When the root's window closes while others are open, another window moves into the root. +//! - With no window left, the app quits, or (macOS) keeps running with its root hidden and the +//! menu bar showing until a workbook is created or opened. +//! +//! [`Session::windows`]: gridcraft_engine::Session::windows + +use craft_window::{Frame, Viewports, WindowEvent, WindowId, WindowList}; +use serde_json::json; + +use crate::SheetApp; + +/// The native windows (see the module docs). +pub struct DocWindows { + /// Engine window id per native window. + list: WindowList, + viewports: Viewports, + /// The builder new windows start from: the root window's size, icon and frame settings. + base: egui::ViewportBuilder, + /// Quit when the last window closes (Windows, Linux); macOS keeps running without a window. + quit_with_last_window: bool, + /// The root viewport is hidden because no window is open. + root_hidden: bool, +} + +impl DocWindows { + /// Takes over `app`'s windows. `base` is the root window's builder, used for new windows. + pub fn new(app: &mut SheetApp, base: egui::ViewportBuilder, quit_with_last_window: bool) -> DocWindows { + app.native_windows = true; + // The root window keeps the desktop's frame, so every window does. + DocWindows { list: WindowList::new(), viewports: Viewports::new(Frame::Desktop), base, quit_with_last_window, root_hidden: false } + } + + /// The platform default for the last window closing: macOS keeps running, others quit. + pub fn platform_quits_with_last_window() -> bool { + !cfg!(target_os = "macos") + } + + /// The native window showing engine window `window`. + fn find(&self, window: u64) -> Option { + self.list.find(|w| *w == window) + } + + /// The engine window drawn in the root viewport. + pub fn root_window(&self) -> Option { + self.viewports.root().and_then(|w| self.list.get(w).copied()) + } + + /// The engine windows that have a native window, in opening order. + pub fn shown_windows(&self) -> Vec { + self.list.iter().map(|(_, w)| *w).collect() + } + + /// The egui viewport showing engine window `window`. + pub fn viewport_of(&self, window: u64) -> Option { + self.find(window).map(|id| self.viewports.viewport_id(id)) + } + + /// The egui viewport of the focused window, where menu commands that act on input (copy, + /// paste, undo in a text box) go. + pub fn focused_viewport(&self) -> egui::ViewportId { + self.list.active().map(|w| self.viewports.viewport_id(w)).unwrap_or(egui::ViewportId::ROOT) + } + + /// Call each frame after [`SheetApp::logic`], also while the root is hidden (eframe runs only + /// the logic then): shows windows created or opened by menu commands, files and scripts. + pub fn logic(&mut self, app: &mut SheetApp, ctx: &egui::Context) { + self.sync(app, ctx); + } + + /// Mirrors the engine's visible windows: closed or hidden windows lose their native window + /// (the root adopting another), new ones get one, and a window a command activated comes to + /// the front. With no window left, quits or hides the root. + fn sync(&mut self, app: &mut SheetApp, ctx: &egui::Context) { + let visible = visible_windows(app); + let gone: Vec = self.list.iter().filter(|(_, w)| !visible.contains(w)).map(|(id, _)| id).collect(); + let mut root_closed = false; + for id in gone { + root_closed |= self.viewports.root() == Some(id); + self.list.close(id); + } + if root_closed { + // The window the engine brought forward, else the most recently focused. + let next = app.session.active_window().and_then(|w| self.find(w)).or_else(|| self.list.active()); + match next { + Some(id) => self.viewports.adopt(ctx, &self.list, id), + None => self.viewports.set_root(None), + } + } + for w in visible { + if self.find(w).is_none() { + self.list.open(w); + } + } + if self.viewports.root().is_none() { + let first = app.session.active_window().and_then(|w| self.find(w)).or_else(|| self.list.active()); + self.viewports.set_root(first); + } + if self.list.is_empty() { + if self.quit_with_last_window { + if !app.allow_close { + app.allow_close = true; + ctx.send_viewport_cmd_to(egui::ViewportId::ROOT, egui::ViewportCommand::Close); + } + } else if !self.root_hidden { + self.root_hidden = true; + ctx.send_viewport_cmd_to(egui::ViewportId::ROOT, egui::ViewportCommand::Visible(false)); + } + return; + } + if self.root_hidden { + self.root_hidden = false; + ctx.send_viewport_cmd_to(egui::ViewportId::ROOT, egui::ViewportCommand::Visible(true)); + ctx.send_viewport_cmd_to(egui::ViewportId::ROOT, egui::ViewportCommand::Focus); + } + // A command or script activated another window: bring its native window forward. + if let Some(id) = app.session.active_window().and_then(|w| self.find(w)) + && self.list.active() != Some(id) + { + self.list.focus(id); + self.viewports.focus(ctx, id); + } + } + + /// Draws every window. Call from the root viewport's UI each frame, in place of + /// [`SheetApp::ui`]. + pub fn show(&mut self, app: &mut SheetApp, ui: &mut egui::Ui) { + let ctx = ui.ctx().clone(); + self.sync(app, &ctx); + let focused = self.list.active().and_then(|id| self.list.get(id).copied()); + app.focus_window = focused; + let titles: std::collections::HashMap = self.list.iter().map(|(_, w)| (*w, window_title(app, *w))).collect(); + let base = &self.base; + // The window a command run in some window's UI activated (File › New, Switch Windows, a close). + let mut requested: Option = None; + let events = self.viewports.show( + ui, + &mut self.list, + |_, w| base.clone().with_title(titles.get(w).cloned().unwrap_or_default()), + |ui, _, w| { + app.show_window(*w); + // Files dropped on a window open as workbooks (the root's arrive through the app's + // raw input hook; a child window's input reaches only its own pass). + let dropped: Vec = ui + .ctx() + .input(|i| i.raw.dropped_files.iter().filter_map(|f| f.path().to_str().map(str::to_string)).filter(|p| !p.is_empty()).collect()); + for path in dropped { + app.open_path(&path); + } + app.ui(ui); + if app.session.active_window() != Some(*w) { + requested = app.session.active_window(); + } + }, + ); + for event in events { + match event { + WindowEvent::Focused(_) => {} + WindowEvent::CloseRequested(id) => { + if let Some(w) = self.close_requested(app, &ctx, id) { + requested = Some(w); + } + } + } + } + let target = requested.or_else(|| self.list.active().and_then(|id| self.list.get(id).copied())); + if let Some(target) = target.filter(|w| app.session.window(*w).is_some()) { + if requested.is_none() + && let Some(previous) = focused.filter(|p| *p != target && app.session.window(*p).is_some()) + { + // The user moved to another window: the previous window's draft is entered there, + // as Enter would. One its workbook refuses stays open in that window. + app.enter_window(previous); + let _ = app.settle_drafts(); + } + app.enter_window(target); + } + self.sync(app, &ctx); + app.focus_window = self.list.active().and_then(|id| self.list.get(id).copied()); + } + + /// A window's close button. Returns the window to bring forward when the close needs an + /// answer (or moved focus); `None` when nothing changed. + fn close_requested(&mut self, app: &mut SheetApp, ctx: &egui::Context, id: WindowId) -> Option { + let is_root = self.viewports.root() == Some(id); + if app.allow_close { + // Quitting: every workbook has been answered for; the root's close ends the app. + return None; + } + if is_root { + // eframe would end the app with the root window; closing it is the app's decision. + ctx.send_viewport_cmd_to(egui::ViewportId::ROOT, egui::ViewportCommand::CancelClose); + } + let window = *self.list.get(id)?; + app.enter_window(window); + app.run_or_alert("window.close", json!({"window": window})); + app.session.active_window() + } +} + +/// The windows that get a native window: every window not hidden (View › Hide), or the active one +/// when all are hidden, so the app still has a window to unhide them from. +fn visible_windows(app: &SheetApp) -> Vec { + let shown: Vec = app.session.windows().iter().filter(|w| !w.hidden).map(|w| w.id).collect(); + if shown.is_empty() { app.session.active_window().into_iter().collect() } else { shown } +} + +/// A native window's title: the window's ("Book1", "Book1:2"), marked while its workbook has +/// unsaved changes. +fn window_title(app: &SheetApp, window: u64) -> String { + let title = app.session.window_title(window).unwrap_or_default(); + let dirty = app.session.window(window).and_then(|w| app.session.documents().iter().find(|d| d.uid == w.doc_uid)).is_some_and(|d| d.is_dirty()); + if dirty { format!("{title} — Edited") } else { title } +} diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index d330d1fcf..f405a3d18 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -13,6 +13,8 @@ pub mod chartview; pub mod control; pub mod credits; pub mod dialogs; +#[cfg(not(target_arch = "wasm32"))] +pub mod doc_windows; pub mod editor; mod floating_pictures; pub mod fonts; @@ -34,6 +36,9 @@ pub mod tabs; pub mod text_box; pub mod theme; pub mod widgets; +mod window_ui; + +pub use window_ui::WindowUi; use std::collections::HashMap; @@ -166,6 +171,14 @@ pub struct Notice { pub action: Option<(String, String)>, } +/// An answer to the Save Changes question. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SaveAnswer { + Save, + DontSave, + Cancel, +} + /// One open window, as View › Switch Windows and the macOS Window menu list it. #[derive(Clone, Debug, PartialEq, Eq)] pub struct OpenWindow { @@ -282,8 +295,8 @@ pub struct SheetApp { fonts_set: bool, /// Workbook fonts bound into the current font set; a change rebinds (installed fonts only). fonts_bound: Vec, - /// The (doc uid, revision) `fonts_bound` was computed for, so the style table is walked only - /// when the workbook changes. + /// The open workbooks' (uid, revision), folded, that `fonts_bound` was computed for, so the + /// style tables are walked only when a workbook changes. fonts_seen: (u64, u64), /// Set while a font-set change is pending activation: skip this frame's paint, like the /// startup `fonts_ready` gate (egui applies new definitions at the next pass). @@ -320,6 +333,17 @@ pub struct SheetApp { /// View › Switch Windows opens its list on the next frame the View tab draws (asked for by a /// key tip, the command search or a menu, possibly while another tab was showing). pub(crate) open_switch_windows: bool, + /// The window whose interface state the per-window fields above hold (see [`WindowUi`]). + ui_window: Option, + /// The other windows' interface state, by window id. + parked: HashMap, + /// Each window is a native window of its own ([`doc_windows`] on the desktop): a window's + /// close button closes that window, and quitting is asked for separately. Without it (web, + /// tests, embedding hosts) one window shows the active workbook window, and its close button + /// quits. + pub native_windows: bool, + /// The native window that has focus, which menu commands and screenshots go to. + pub(crate) focus_window: Option, } impl SheetApp { @@ -367,6 +391,10 @@ impl SheetApp { started: now_ms(), allow_close: false, open_switch_windows: false, + ui_window: None, + parked: HashMap::new(), + native_windows: false, + focus_window: None, } } @@ -385,11 +413,12 @@ impl SheetApp { theme::apply(ctx, dark); } - /// The font names the active workbook uses: every style's font plus the theme's heading/body - /// fonts. These are the families [`fonts`] binds an installed file for. + /// The font names the open workbooks use: every style's font plus the theme's heading/body + /// fonts. These are the families [`fonts`] binds an installed file for. Every workbook's, as + /// each may be showing in a window of its own. pub fn used_fonts(&self) -> Vec { let mut names: Vec = Vec::new(); - if let Some(d) = self.session.active() { + for d in self.session.documents() { names.extend(d.wb.styles.iter().map(|(_, s)| s.font.name.clone())); names.push(d.wb.theme.major_font.clone()); names.push(d.wb.theme.minor_font.clone()); @@ -441,7 +470,14 @@ impl SheetApp { if let Some(r) = self.run_ui_command(id, ¶ms) { return r; } + let before = self.session.active_window(); let r = self.session.run(id, params); + if id.starts_with("file.") && self.session.active_window() != before { + // File › New or Open from the Backstage view: the window it was in goes back to its sheet. + self.backstage = None; + } + // Requests the command made (a dialog, a draft) belong to the window it left active. + self.follow_active_window(); self.after_engine(); // The new chart is selected, as after inserting any object, so Delete removes it, not its data. if id == "insert.chart" @@ -1065,7 +1101,7 @@ impl SheetApp { /// Commits the cell and text-box drafts, as Enter would, before a command that can show /// another workbook or write this one to a file, so a draft never lands in another workbook. /// A draft its workbook refuses stays open with the reason shown, and the command doesn't run. - fn settle_drafts(&mut self) -> Result<(), String> { + pub(crate) fn settle_drafts(&mut self) -> Result<(), String> { if !self.commit_edit(0, 0, false, false) { return Err(self.message.as_ref().map(|(_, m)| m.clone()).unwrap_or_else(|| "The cell being edited can't be entered.".into())); } @@ -1216,7 +1252,7 @@ impl SheetApp { /// Holds back a window close (the close button, Alt+F4, the OS quitting the app) while a /// workbook has unsaved changes, and asks about them first. fn guard_window_close(&mut self, ctx: &egui::Context) { - if self.allow_close || !ctx.input(|i| i.viewport().close_requested()) { + if self.native_windows || self.allow_close || !ctx.input(|i| i.viewport().close_requested()) { return; } // A cell still being edited is part of the workbook, as for File › Close. @@ -1237,21 +1273,75 @@ impl SheetApp { } } - /// Asks about the next unsaved workbook before quitting, or closes the window once none is left. + /// Asks about the next unsaved workbook before quitting, in its window, or closes the app + /// once none is left. pub(crate) fn continue_quit(&mut self, ctx: &egui::Context) { - match self.session.documents().iter().position(|d| d.is_dirty()) { - Some(i) => { - self.session.set_active(i); + let dirty = self.session.documents().iter().find(|d| d.is_dirty()).map(|d| d.uid); + match dirty.and_then(|uid| self.session.doc_window(uid)) { + Some(window) => { + // Its window comes forward (the native window too: see `doc_windows`). + self.enter_window(window); let title = self.session.active().map(|d| d.display_title()).unwrap_or_default(); self.open_dialog("saveChanges", json!({"title": title, "quit": true})); } None => { self.allow_close = true; - ctx.send_viewport_cmd(egui::ViewportCommand::Close); + ctx.send_viewport_cmd_to(egui::ViewportId::ROOT, egui::ViewportCommand::Close); } } } + /// Answers the Save Changes question about the active workbook (its buttons). While quitting, + /// the next unsaved workbook is asked about once this one is answered; Cancel stops the quit. + /// Returns whether the question is settled: Save keeps it open when the cell being edited + /// can't be entered. + pub fn answer_save_changes(&mut self, answer: SaveAnswer, quitting: bool, ctx: &egui::Context) -> bool { + match answer { + SaveAnswer::Cancel => {} + SaveAnswer::DontSave => { + self.run_or_alert("file.close", json!({"force": true, "document": true})); + if quitting { + self.continue_quit(ctx); + } + } + SaveAnswer::Save => { + // File operations include the cell still being edited, as File › Save does. + if !self.commit_edit(0, 0, false, false) { + return false; + } + self.run_or_alert("file.save", json!({})); + if self.session.active().is_some_and(|x| !x.is_dirty()) { + self.run_or_alert("file.close", json!({"force": true, "document": true})); + if quitting { + self.continue_quit(ctx); + } + } + } + } + true + } + + /// Quit (the macOS app menu, ⌘Q): commits every window's cell draft as Enter would, then asks + /// about each unsaved workbook in its window; Cancel on any of them stops the quit. A draft its + /// workbook refuses stops the quit too, with its window in front and the reason shown. + pub fn request_quit(&mut self, ctx: &egui::Context) { + while let Some(window) = self.parked_window_with_draft() { + self.enter_window(window); + if self.settle_drafts().is_err() { + return; + } + } + if self.settle_drafts().is_err() { + return; + } + if let Some(d) = self.dialog.as_mut().filter(|d| d.name() == "saveChanges") { + // Already asking about a workbook (File › Close): answering it goes on to quit. + d.values.insert("quit".into(), json!(true)); + return; + } + self.continue_quit(ctx); + } + /// Per-frame logic (control channel, screenshots). Call before `ui`. pub fn logic(&mut self, ctx: &egui::Context) { if self.session.text_width.is_none() { @@ -1288,7 +1378,11 @@ impl SheetApp { // Bind the workbook's installed fonts once the background scan has finished. Adding // fonts re-applies the font set, which egui activates at the next pass, so skip this // frame's paint (same one-frame cost as startup). Only when the workbook changed. - let seen = self.session.active().map(|d| (d.uid, d.revision)).unwrap_or((0, 0)); + let seen = self + .session + .documents() + .iter() + .fold((0u64, 0u64), |(uid, rev), d| (uid.wrapping_mul(31).wrapping_add(d.uid), rev.wrapping_mul(31).wrapping_add(d.revision))); if seen != self.fonts_seen { let used = self.used_fonts(); self.fonts_seen = seen; @@ -1322,9 +1416,10 @@ impl SheetApp { ctx.options_mut(|o| o.zoom_with_keyboard = false); } // egui closes the window on Ctrl+Q (and with Shift or Alt held too) without asking to save - // (#1443). Excel for Windows has no such key (Ctrl+Q is Quick Analysis); macOS keeps Cmd+Q, - // the app's Quit there. - if ctx.os() != egui::os::OperatingSystem::Mac && ctx.options(|o| !o.quit_shortcuts.is_empty()) { + // (#1443). Excel for Windows has no such key (Ctrl+Q is Quick Analysis). With a native + // window per workbook egui's Cmd+Q would close only the window it was typed in: the menu + // bar's Quit item has the key instead (it asks about unsaved workbooks first). + if (ctx.os() != egui::os::OperatingSystem::Mac || self.native_windows) && ctx.options(|o| !o.quit_shortcuts.is_empty()) { ctx.options_mut(|o| o.quit_shortcuts.clear()); } let zoom = self.ui.zoom_factor(); @@ -1357,9 +1452,12 @@ impl SheetApp { } self.after_engine(); } - if self.session.active().is_none() { + // One window always shows a workbook, unless each workbook has a native window of its own + // (`doc_windows` decides what having none left means). + if self.session.active().is_none() && !self.native_windows { self.session.new_workbook(); } + self.follow_active_window(); } pub fn raw_input_hook(&mut self, raw: &mut egui::RawInput) { @@ -1383,6 +1481,10 @@ impl SheetApp { return; } let t0 = now_ms(); + // A script may have switched windows since the last frame. + self.follow_active_window(); + // Native windows: menu commands and screenshots go to the focused one. + let focused = !self.native_windows || self.focus_window == self.ui_window; text_box::before_ui(self, &ctx); // Another window came forward without going through a command (which settles drafts // first): the draft goes into its own window instead of floating over this one. If its @@ -1428,8 +1530,10 @@ impl SheetApp { dialogs::show(self, &ctx); widgets::message_box(self, &ctx); widgets::toast(self, &ctx); - control::issue_screenshots(self, &ctx); - control::collect_screenshots(self, &ctx); + if focused { + control::issue_screenshots(self, &ctx); + control::collect_screenshots(self, &ctx); + } if !ctx.input(|i| i.focused) { // Nothing else to do. } @@ -1453,8 +1557,10 @@ impl SheetApp { self.store_view_state(); let _ = self.session.run("file.save", json!({})); } - // Window title. - if let Some(d) = self.session.active() { + // Window title (native windows get theirs from `doc_windows`). + if !self.native_windows + && let Some(d) = self.session.active() + { let title = format!("{}{}", d.display_title(), if d.is_dirty() { " — Edited" } else { "" }); if self.grid.last_title.as_deref() != Some(title.as_str()) { ctx.send_viewport_cmd(egui::ViewportCommand::Title(title.clone())); diff --git a/crates/ui-egui/src/window_ui.rs b/crates/ui-egui/src/window_ui.rs new file mode 100644 index 000000000..96aa83c79 --- /dev/null +++ b/crates/ui-egui/src/window_ui.rs @@ -0,0 +1,133 @@ +//! The interface state each window keeps for itself. +//! +//! [`SheetApp`]'s per-window fields (the cell draft, dialog, Backstage view, grid gestures…) hold +//! the state of one window: the engine's active window. Every other window's state is parked in a +//! [`WindowUi`] and swapped in when that window is activated or drawn, the way the engine swaps a +//! window's selection and sheet into its workbook. So a draft, a dialog or a message stays in the +//! window it belongs to. + +use gridcraft_engine::core::CellRef; + +use crate::{Notice, SheetApp, backstage, dialogs, editor, grid, keytips, text_box}; + +/// One window's parked interface state (see the module docs). +#[derive(Default)] +pub struct WindowUi { + editor: Option, + text_box_editor: Option, + dialog: Option, + backstage: Option, + message: Option<(String, String)>, + toast: Option<(String, f64)>, + notice: Option, + grid: grid::GridState, + name_box: Option, + keytips: keytips::KeyTips, + selected_chart: Option, + tab_run: Option<(u64, usize, u32, CellRef)>, + open_switch_windows: bool, + /// The status bar's Ready/Enter/Edit, which follows the window's cell draft. + mode: gridcraft_engine::Mode, +} + +impl WindowUi { + /// A new window's state. The last border, fill and font colour picked carry over, as they are + /// the user's recent choices rather than the window's. + fn fresh(like: &grid::GridState) -> WindowUi { + WindowUi { + grid: grid::GridState { + last_border: like.last_border.clone(), + last_fill: like.last_fill.clone(), + last_font_color: like.last_font_color.clone(), + ..Default::default() + }, + ..Default::default() + } + } +} + +impl SheetApp { + /// Exchanges the per-window fields with `w`. Destructured in full, so a field added to + /// [`WindowUi`] can't be left out of the swap. + fn swap_window_ui(&mut self, w: &mut WindowUi) { + let WindowUi { + editor, + text_box_editor, + dialog, + backstage, + message, + toast, + notice, + grid, + name_box, + keytips, + selected_chart, + tab_run, + open_switch_windows, + mode, + } = w; + std::mem::swap(&mut self.editor, editor); + std::mem::swap(&mut self.text_box_editor, text_box_editor); + std::mem::swap(&mut self.dialog, dialog); + std::mem::swap(&mut self.backstage, backstage); + std::mem::swap(&mut self.message, message); + std::mem::swap(&mut self.toast, toast); + std::mem::swap(&mut self.notice, notice); + std::mem::swap(&mut self.grid, grid); + std::mem::swap(&mut self.name_box, name_box); + std::mem::swap(&mut self.keytips, keytips); + std::mem::swap(&mut self.selected_chart, selected_chart); + std::mem::swap(&mut self.tab_run, tab_run); + std::mem::swap(&mut self.open_switch_windows, open_switch_windows); + std::mem::swap(&mut self.session.mode, mode); + } + + /// The window whose interface state is in the per-window fields. + pub fn ui_window(&self) -> Option { + self.ui_window + } + + /// Brings the per-window fields in line with the engine's active window, after a command (or + /// a script) switched windows: the previous window's state is parked, the active one's swapped + /// in (a window never seen before starts fresh). A closed window's state is dropped. + pub fn follow_active_window(&mut self) { + let active = self.session.active_window(); + if active == self.ui_window { + return; + } + let Some(previous) = self.ui_window else { + // The first window: the fields are already its state. + self.ui_window = active; + return; + }; + let mut outgoing = WindowUi::fresh(&self.grid); + self.swap_window_ui(&mut outgoing); + if self.session.window(previous).is_some() { + self.parked.insert(previous, outgoing); + } + if let Some(mut incoming) = active.and_then(|w| self.parked.remove(&w)) { + self.swap_window_ui(&mut incoming); + } + self.ui_window = active; + let session = &self.session; + self.parked.retain(|w, _| session.window(*w).is_some()); + } + + /// Activates window `id` (it becomes the most recently active) with its interface state. + pub fn enter_window(&mut self, id: u64) { + self.session.activate_window(id); + self.follow_active_window(); + } + + /// Makes window `id` and its interface state current while it is drawn, without changing + /// which window counts as most recently active. + pub fn show_window(&mut self, id: u64) { + self.session.show_window(id); + self.follow_active_window(); + } + + /// Whether a window other than the current one has a cell or text-box draft open. + pub fn parked_window_with_draft(&self) -> Option { + self.parked.iter().filter(|(_, w)| w.editor.is_some() || w.text_box_editor.is_some()).map(|(id, _)| *id).min() + } +} diff --git a/crates/ui-egui/tests/doc_windows.rs b/crates/ui-egui/tests/doc_windows.rs new file mode 100644 index 000000000..ff8e2d631 --- /dev/null +++ b/crates/ui-egui/tests/doc_windows.rs @@ -0,0 +1,284 @@ +//! One native window per workbook window: each window keeps its own interface state, and closing +//! or quitting asks about unsaved workbooks only when a workbook would close. +//! +//! The windows are drawn through a test backend that, like eframe, runs each immediate viewport +//! as a nested pass with its own input, so a close button can be pressed on one window. +#![cfg(not(target_arch = "wasm32"))] + +use std::cell::RefCell; + +use egui::{Context, RawInput, ViewportCommand, ViewportEvent, ViewportId, ViewportInfo}; +use gridcraft_engine::{Session, core::CellRef, core::Value}; +use gridcraft_ui_egui::doc_windows::DocWindows; +use gridcraft_ui_egui::{SaveAnswer, SheetApp}; +use serde_json::json; + +thread_local! { + /// Each viewport's input for the next frame (focus, close button). + static INFOS: RefCell> = RefCell::new(egui::ViewportIdMap::default()); + /// The viewport commands the app sent this frame. + static SENT: RefCell> = const { RefCell::new(Vec::new()) }; +} + +fn record(mut out: egui::FullOutput) { + out.textures_delta.clear(); // not painted + SENT.with(|s| { + let mut s = s.borrow_mut(); + for (id, v) in &out.viewport_output { + s.extend(v.commands.iter().map(|c| (*id, c.clone()))); + } + }); +} + +struct Desk { + ctx: Context, + app: SheetApp, + windows: DocWindows, +} + +impl Desk { + fn new(quit_with_last_window: bool) -> Desk { + INFOS.with(|i| i.borrow_mut().clear()); + let ctx = Context::default(); + ctx.set_embed_viewports(false); + Context::set_immediate_viewport_renderer(|ctx, viewport| { + let id = viewport.ids.this; + let mut input = RawInput { viewport_id: id, ..RawInput::default() }; + input.viewports = INFOS.with(|i| i.borrow().clone()); + input.viewports.entry(id).or_default().parent = Some(viewport.ids.parent); + let mut cb = viewport.viewport_ui_cb; + record(ctx.run_ui(input, |ui| cb(ui))); + }); + let mut session = Session::new(); + session.new_workbook(); + let mut app = SheetApp::new(session, Default::default()); + let windows = DocWindows::new(&mut app, egui::ViewportBuilder::default(), quit_with_last_window); + let mut desk = Desk { ctx, app, windows }; + desk.frames(3); + desk + } + + /// One frame as eframe runs it; `act` runs first, inside the frame. Returns the viewport + /// commands sent. + fn frame_with(&mut self, act: impl FnOnce(&mut SheetApp, &Context)) -> Vec<(ViewportId, ViewportCommand)> { + SENT.with(|s| s.borrow_mut().clear()); + let mut input = RawInput { viewport_id: ViewportId::ROOT, ..RawInput::default() }; + input.viewports = INFOS.with(|i| i.borrow().clone()); + input.viewports.entry(ViewportId::ROOT).or_default(); + let Desk { ctx, app, windows } = self; + let mut act = Some(act); + let out = ctx.clone().run_ui(input, |ui| { + let ctx = ui.ctx().clone(); + if let Some(act) = act.take() { + act(app, &ctx); + } + app.logic(&ctx); + windows.logic(app, &ctx); + windows.show(app, ui); + }); + record(out); + // Close buttons are one-shot events. + INFOS.with(|i| i.borrow_mut().values_mut().for_each(|v| v.events.clear())); + SENT.with(|s| s.borrow().clone()) + } + + fn frames(&mut self, n: usize) -> Vec<(ViewportId, ViewportCommand)> { + (0..n).flat_map(|_| self.frame_with(|_, _| {})).collect() + } + + fn viewport(&self, window: u64) -> ViewportId { + self.windows.viewport_of(window).unwrap_or_else(|| panic!("window {window} has no native window")) + } + + /// The user presses window `window`'s close button. + fn close(&mut self, window: u64) -> Vec<(ViewportId, ViewportCommand)> { + let vp = self.viewport(window); + INFOS.with(|i| i.borrow_mut().entry(vp).or_default().events.push(ViewportEvent::Close)); + let mut sent = self.frame_with(|_, _| {}); + sent.extend(self.frames(2)); + sent + } + + /// Answers the Save Changes question showing in the active window, as its button does. + fn answer(&mut self, answer: SaveAnswer) -> Vec<(ViewportId, ViewportCommand)> { + let mut sent = self.frame_with(|app, ctx| { + let d = app.dialog.take().unwrap_or_else(|| panic!("no question is showing")); + assert_eq!(d.name(), "saveChanges"); + let quitting = d.values.get("quit").and_then(serde_json::Value::as_bool) == Some(true); + app.answer_save_changes(answer, quitting, ctx); + }); + sent.extend(self.frames(2)); + sent + } + + fn window_of(&self, doc: usize) -> u64 { + let uid = self.app.session.documents()[doc].uid; + self.app.session.doc_window(uid).unwrap_or_default() + } +} + +fn sent_to(sent: &[(ViewportId, ViewportCommand)], vp: ViewportId, cmd: &ViewportCommand) -> bool { + sent.iter().any(|(id, c)| *id == vp && c == cmd) +} + +#[test] +fn each_window_keeps_its_own_selection_and_draft() { + let mut desk = Desk::new(true); + desk.app.run("file.new", json!({})).unwrap(); + desk.frames(3); + let (w1, w2) = (desk.window_of(0), desk.window_of(1)); + assert_eq!(desk.windows.shown_windows(), [w1, w2], "a native window per workbook"); + assert_ne!(desk.viewport(w1), desk.viewport(w2)); + + desk.app.enter_window(w1); + desk.app.run("selection.set", json!({"range": "B2"})).unwrap(); + desk.app.begin_edit(Some("one".into()), false); + desk.frames(2); + desk.app.enter_window(w2); + desk.app.run("selection.set", json!({"range": "D4"})).unwrap(); + desk.app.begin_edit(Some("two".into()), false); + desk.frames(3); + + for (w, cell, text) in [(w1, "B2", "one"), (w2, "D4", "two")] { + desk.app.enter_window(w); + assert_eq!(desk.app.editor.as_ref().map(|ed| ed.text.as_str()), Some(text), "window {w} keeps its draft"); + assert!(desk.app.grid.rect.is_some(), "window {w} drew its own grid"); + assert_eq!(desk.app.session.doc().unwrap().selection.active, CellRef::parse(cell).unwrap(), "window {w} keeps its selection"); + } + assert!(desk.app.session.documents().iter().all(|d| !d.is_dirty()), "drawing the other window commits no draft"); +} + +#[test] +fn closing_one_of_two_windows_on_a_workbook_does_not_ask() { + let mut desk = Desk::new(true); + desk.app.run("cell.set", json!({"cell": "A1", "input": "42"})).unwrap(); + desk.app.run("view.newWindow", json!({})).unwrap(); + desk.frames(3); + let windows = desk.windows.shown_windows(); + assert_eq!(windows.len(), 2, "New Window opens a native window"); + let child = windows.iter().copied().find(|w| Some(*w) != desk.windows.root_window()).unwrap(); + + let sent = desk.close(child); + assert!(desk.app.dialog.is_none(), "the workbook stays open in its other window: nothing to ask"); + assert_eq!(desk.app.session.documents().len(), 1); + assert_eq!(desk.app.session.windows().len(), 1); + assert!(desk.app.session.documents()[0].is_dirty(), "its changes are kept"); + assert_eq!(desk.windows.shown_windows().len(), 1); + assert!(!sent_to(&sent, ViewportId::ROOT, &ViewportCommand::Close), "{sent:?}"); +} + +#[test] +fn closing_the_last_window_of_an_unsaved_workbook_asks_and_the_app_quits_after() { + let mut desk = Desk::new(true); + desk.app.run("cell.set", json!({"cell": "A1", "input": "42"})).unwrap(); + desk.frames(2); + let w = desk.window_of(0); + let sent = desk.close(w); + assert!(sent_to(&sent, ViewportId::ROOT, &ViewportCommand::CancelClose), "the window stays open to ask: {sent:?}"); + assert_eq!(desk.app.dialog.as_ref().map(|d| d.name()), Some("saveChanges")); + assert_eq!(desk.app.session.documents().len(), 1); + + let sent = desk.answer(SaveAnswer::Cancel); + assert!(!sent_to(&sent, ViewportId::ROOT, &ViewportCommand::Close), "Cancel keeps it open"); + assert_eq!(desk.app.session.documents().len(), 1); + + desk.close(w); + let sent = desk.answer(SaveAnswer::DontSave); + assert!(desk.app.session.documents().is_empty()); + assert!(sent_to(&sent, ViewportId::ROOT, &ViewportCommand::Close), "the last window closed: the app quits: {sent:?}"); + assert!(desk.app.allow_close); +} + +#[test] +fn without_quitting_on_the_last_window_the_app_hides_and_new_shows_it_again() { + let mut desk = Desk::new(false); + let w = desk.window_of(0); + let sent = desk.close(w); + assert!(desk.app.session.documents().is_empty(), "a clean workbook closes without asking"); + assert!(sent_to(&sent, ViewportId::ROOT, &ViewportCommand::Visible(false)), "{sent:?}"); + assert!(!sent_to(&sent, ViewportId::ROOT, &ViewportCommand::Close), "the app keeps running: {sent:?}"); + assert!(desk.app.session.documents().is_empty(), "and doesn't make a workbook of its own"); + + let sent = desk.frame_with(|app, _| app.run_or_alert("file.new", json!({}))); + assert!(sent_to(&sent, ViewportId::ROOT, &ViewportCommand::Visible(true)), "{sent:?}"); + assert_eq!(desk.windows.root_window(), Some(desk.window_of(0))); +} + +#[test] +fn quit_asks_about_each_unsaved_workbook_in_its_window_and_cancel_stops() { + let mut desk = Desk::new(true); + desk.app.run("cell.set", json!({"cell": "A1", "input": "1"})).unwrap(); + desk.app.run("file.new", json!({})).unwrap(); + desk.app.run("cell.set", json!({"cell": "A1", "input": "2"})).unwrap(); + desk.frames(3); + let (w1, w2) = (desk.window_of(0), desk.window_of(1)); + assert_eq!(desk.app.session.active_window(), Some(w2)); + + let sent = desk.frame_with(|app, ctx| app.request_quit(ctx)); + assert_eq!(desk.app.session.active_window(), Some(w1), "Book1's window comes forward to ask"); + assert!(sent_to(&sent, desk.viewport(w1), &ViewportCommand::Focus), "{sent:?}"); + assert_eq!(desk.app.dialog.as_ref().map(|d| d.name()), Some("saveChanges")); + let sent = desk.answer(SaveAnswer::Cancel); + assert!(!sent_to(&sent, ViewportId::ROOT, &ViewportCommand::Close), "Cancel stops the quit: {sent:?}"); + assert_eq!(desk.app.session.documents().len(), 2); + assert!(desk.app.dialog.is_none()); + + desk.frame_with(|app, ctx| app.request_quit(ctx)); + let sent = desk.answer(SaveAnswer::DontSave); + assert!(!sent_to(&sent, ViewportId::ROOT, &ViewportCommand::Close), "Book2 is still unsaved: {sent:?}"); + assert_eq!(desk.app.session.active_window(), Some(w2), "then Book2's window asks"); + assert_eq!(desk.app.dialog.as_ref().map(|d| d.name()), Some("saveChanges")); + let sent = desk.answer(SaveAnswer::DontSave); + assert!(sent_to(&sent, ViewportId::ROOT, &ViewportCommand::Close), "both answered: the app quits: {sent:?}"); +} + +#[test] +fn the_root_adopts_the_remaining_window_with_its_workbook_and_state() { + let mut desk = Desk::new(true); + desk.app.run("file.new", json!({})).unwrap(); + desk.frames(3); + let (w1, w2) = (desk.window_of(0), desk.window_of(1)); + assert_eq!(desk.windows.root_window(), Some(w1)); + let book2 = desk.app.session.documents()[1].uid; + desk.app.enter_window(w2); + desk.app.run("selection.set", json!({"range": "C3"})).unwrap(); + desk.app.begin_edit(Some("kept".into()), false); + desk.frames(2); + + let sent = desk.close(w1); + assert!(sent_to(&sent, ViewportId::ROOT, &ViewportCommand::CancelClose), "closing the root's window doesn't end the app: {sent:?}"); + assert!(!sent_to(&sent, ViewportId::ROOT, &ViewportCommand::Close), "{sent:?}"); + assert_eq!(desk.windows.root_window(), Some(w2), "Book2's window moves into the root"); + assert_eq!(desk.windows.shown_windows(), [w2]); + assert_eq!(desk.app.session.documents().len(), 1); + assert_eq!(desk.app.session.doc().unwrap().uid, book2); + assert_eq!(desk.app.editor.as_ref().map(|ed| ed.text.as_str()), Some("kept"), "its draft is still open"); + assert_eq!(desk.app.session.doc().unwrap().selection.active, CellRef::parse("C3").unwrap()); + assert_eq!(desk.app.session.doc().unwrap().wb.active().unwrap().value(CellRef::parse("C3").unwrap()), Value::Empty); +} + +#[test] +fn menu_commands_follow_the_focused_window_and_leaving_a_window_enters_its_draft() { + let mut desk = Desk::new(true); + desk.app.run("file.new", json!({})).unwrap(); + desk.frames(3); + let (w1, w2) = (desk.window_of(0), desk.window_of(1)); + desk.app.begin_edit(Some("typed".into()), false); + desk.frames(2); + assert_eq!(desk.app.session.active_window(), Some(w2)); + + // The user clicks into Book1's window. + let (v1, v2) = (desk.viewport(w1), desk.viewport(w2)); + INFOS.with(|i| { + let mut i = i.borrow_mut(); + i.entry(v1).or_default().focused = Some(true); + i.entry(v2).or_default().focused = Some(false); + }); + desk.frames(2); + assert_eq!(desk.app.session.active_window(), Some(w1), "commands from the menu bar go to the window focused last"); + assert!(desk.app.editor.is_none(), "Book1's window has no draft"); + let book2 = &desk.app.session.documents()[1]; + assert_eq!(book2.wb.active().unwrap().value(CellRef::new(0, 0)), Value::text("typed"), "the draft was entered in its own window"); + desk.app.run("cell.set", json!({"cell": "B1", "input": "menu"})).unwrap(); + assert_eq!(desk.app.session.documents()[0].wb.active().unwrap().value(CellRef::new(0, 1)), Value::text("menu")); +} diff --git a/docs/hardware-parity.md b/docs/hardware-parity.md index 3f9135c49..f19b4a49d 100644 --- a/docs/hardware-parity.md +++ b/docs/hardware-parity.md @@ -1,6 +1,6 @@ # Hardware parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (direct desktop printing through craft-print) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-22 · **Change:** minor (a native window per workbook window) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4 The hardware Excel uses, per platform, against GridCraft. Part of [target-app-parity.md](target-app-parity.md). **Hardware: ~40% ready, 25–40 h** (estimated). @@ -11,7 +11,7 @@ The hardware Excel uses, per platform, against GridCraft. Part of | GPU rendering of the grid and charts | DirectX / Metal accelerated drawing | wgpu: Metal · DirectX 12 (default since 10-08; OpenGL when DirectX 12 has no GPU driver or fails to start; Vulkan only on request, so the Intel UHD `igvk64.dll` crash (#120) can't happen; shaders compiled with FXC, so no DXC runtime is needed, #339) · Vulkan/GL · Vulkan/GL · WebGPU or WebGL2 | At parity in kind. Driver problems: screen flashing on Windows 11 (#76): 2–4 h | | Pen and touch (Draw tab) | Pen pressure and tilt, eraser end, touch mode, Ink to Shape, ink replay | Pen, highlighter, eraser, Ink to Shape with mouse/pen input; **no pressure or tilt**, no touch mode | Pressure-sensitive strokes where winit reports force; touch-friendly ribbon spacing: 4–6 h | | Trackpad and mouse | Smooth scrolling, pinch zoom, horizontal scroll, Magic Mouse | Smooth scrolling and zoom via egui | Verify pinch zoom on each platform: 1–2 h | -| Multiple windows and monitors | One window per workbook, New Window for the same workbook, per-monitor DPI | One window; multiple workbooks switch inside it (#162; PR #171) | Native window per workbook, New Window, Arrange All across monitors: 6–8 h | +| Multiple windows and monitors | One window per workbook, New Window for the same workbook, per-monitor DPI | A native window per workbook window on the desktop, New Window for the same workbook; new windows cascade from the last one on its monitor (web: one window) | Arrange All across monitors; verify per-monitor DPI with windows on two monitors: 2–3 h | | High-DPI and display scaling | yes | yes (display scale matched to desktop density; View › Interface size 75–200 % on top, #230) | — | | Accessibility hardware (screen readers, braille displays, switch control) | Narrator, JAWS, NVDA, VoiceOver | AccessKit enabled in eframe; grid cells and ribbon not yet exposed as an accessible tree | Counted under UI ([ui-parity.md](ui-parity.md)) | | Printers | System print with driver options | Desktop submits through craft-print with printer choice, copies, duplex and grayscale; web browser printing | Counted under print | @@ -20,6 +20,7 @@ The hardware Excel uses, per platform, against GridCraft. Part of | Date | Change | Summary | |---|---|---| +| 2026-10-22 | minor | Multiple windows: each workbook window is a native window of its own (desktop). | | 2026-10-12 | minor | Desktop File > Print now discovers printers and submits PDF jobs through pinned craft-print with printer choice, copies, duplex and grayscale; browser printing and PDF export remain available. Remaining print parity includes page ranges and driver-specific tray/options UI; physical printer fidelity needs hardware validation. | | 2026-10-11 | trivial | Windows: DirectX 12 shaders compiled with FXC, no DXC runtime needed (#339, #354) | | 2026-10-11 | trivial | Windows: OpenGL fallback when DirectX 12 has no GPU or fails to start; Vulkan never by default (#120) | diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 839984959..817b5b915 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-22 · **Change:** minor (New Window opens a second window on the same workbook, titled Name:1 / Name:2; Switch Windows lists the open windows; a cell being typed stays in its own window through New, Open and window switches; macOS Quit asks about unsaved workbooks) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-22 · **Change:** minor (every workbook window is a native window of its own on the desktop, with its own draft, dialog and ribbon; closing one of several windows on a workbook doesn't ask; Quit asks about each unsaved workbook in its window; the last window closed quits on Windows and Linux, macOS keeps running) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4 Interaction, controls and feel, against Excel. Part of [target-app-parity.md](target-app-parity.md). **UI/UX fidelity: ~55% ready, 50–80 h** (estimated). Ribbon command coverage is measured @@ -33,8 +33,8 @@ Sources: `crates/ui-egui/src` (ribbon, grid, editor, formula bar, dialogs, panes | Backstage (File) view: Info, New with templates, Open recent, Export, Options | yes | yes (#174, 10-11): the File tab opens a full-window view (Esc or the back arrow return) with Home, New (blank + the three samples), Open (recent + Browse; the recent list tells when each workbook was last changed, 10-12), Info (location, properties, Protect Workbook), Save, Save As, Print (page preview, active sheet or workbook, fit to page, Page Setup), Export (PDF, CSV, web page), Close and Options (`file.options`: theme, language, formula bar, AutoSave, calculation) | no Account, Share or template gallery beyond the samples; Options applies at once (no OK/Cancel dialog) and holds only the settings GridCraft has | | Dark mode, system appearance | yes | yes | done (10-09) | | Page Layout view (paper view with rulers and header editing) | yes | yes (#329, 10-11): per-sheet view saved in XLSX, pages with margins and gaps on the print pagination, "Click to add data" pages, rulers (inches or centimetres), headers and footers drawn and edited in place with a Header & Footer tab | margins drag on the rulers (#792, with header and footer margins); header/footer pictures, Different First Page / Odd & Even missing (1–3 h) | -| Multiple windows per workbook | yes | no (#162) | see [hardware-parity.md](hardware-parity.md) | -| Switch Windows, New Window, unsaved workbooks on quit | yes | yes (10-22): View › New Window opens another window on the same workbook (no copy: saving from either saves it), each with its own selection, active sheet and scroll; a workbook with several windows titles them "Book1:1", "Book1:2"; File › Close closes one of them without asking and asks only for the last; View › Switch Windows and the macOS Window menu list the open windows (active one checked, unsaved ones marked •) and bring the chosen one forward (`view.switchWindows {window}`, or `{doc}` for a workbook's most recent window); a cell being typed is entered into its own window and workbook before New, Open, Close, Save, Print and window switches (also between two windows on one workbook), and a refused entry stays open and stops the command; quitting (window close, Cmd+Q, the macOS Quit item) asks Save / Don't Save / Cancel for each unsaved workbook | one OS window for all workbooks (#162) | +| Multiple windows per workbook | yes | yes (10-22, desktop): each workbook window is a native window of its own (New, Open and New Window open one; titles "Book1", or "Book1:1" / "Book1:2" for a workbook's second window, with " — Edited" while unsaved). Each window keeps its own cell draft, dialog, Backstage view, message and grid state, and its ribbon acts on it; on macOS the menu bar acts on the window focused last. Leaving a window enters its draft as Enter would. A window's close button closes that window: one of several on a workbook closes without asking, a workbook's last asks Save / Don't Save / Cancel when unsaved. Quit asks about each unsaved workbook in its window, and Cancel stops it. The last window closed quits on Windows and Linux; macOS keeps running with the menu bar until New or Open. Web: one window | no Arrange All; a hidden window (View › Hide) has no native window; every window repaints when one does; see [hardware-parity.md](hardware-parity.md) | +| Switch Windows, New Window, unsaved workbooks on quit | yes | yes (10-22): View › New Window opens another window on the same workbook (no copy: saving from either saves it), each with its own selection, active sheet and scroll; a workbook with several windows titles them "Book1:1", "Book1:2"; File › Close closes one of them without asking and asks only for the last; View › Switch Windows and the macOS Window menu list the open windows (active one checked, unsaved ones marked •) and bring the chosen one forward (`view.switchWindows {window}`, or `{doc}` for a workbook's most recent window); a cell being typed is entered into its own window and workbook before New, Open, Close, Save, Print and window switches (also between two windows on one workbook), and a refused entry stays open and stops the command; quitting (Cmd+Q, the macOS Quit item, the Dock) asks Save / Don't Save / Cancel for each unsaved workbook | | ## Grid interaction @@ -177,6 +177,7 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12. | Date | Change | Summary | |---|---|---| +| 2026-10-22 | minor | One native window per workbook window on the desktop: per-window drafts, dialogs and ribbons; menu bar commands go to the focused window; closing one of several windows on a workbook doesn't ask; Quit asks in each unsaved workbook's window; the last window closed quits (Windows, Linux) or leaves macOS running | | 2026-10-22 | minor | New Window opens a second window on the same workbook instead of a copy that saved over the original; windows keep their own selection, sheet and scroll; titles Name:1 / Name:2; Switch Windows, Hide and Unhide work by window; File › Close closes one window of several without asking | | 2026-10-22 | minor | New workbook no longer loses the first one: the cell being typed is entered into its own workbook before any File or window command (an entry the workbook refuses stays open and the command doesn't run; an untouched F2 draft changes nothing), the editor is tied to its workbook, View › Switch Windows and the macOS Window menu list the open workbooks, and the macOS Quit item asks about unsaved workbooks like the close button | | 2026-10-21 | minor | Background error checking, first part (#1994): a constant whose text would be a number if typed (signs, thousands, currency, percent, exponent, parenthesised negative) gets Excel's green corner triangle; the active cell then shows a warning button whose menu names the problem and offers Convert to Number for the selection (`formulas.convertToNumber`). Dates, times and fractions kept as text and formula results are not flagged, as in Excel | From d4db58cde467f7a412293e55861d390a7f77b4ad Mon Sep 17 00:00:00 2001 From: aiongg Date: Thu, 22 Oct 2026 16:29:24 +0000 Subject: [PATCH 2/5] G3: feedback from a command that switched windows shows in the window it left active after_engine, show_open_warnings and alert bring the per-window state in line with the engine's active window first, so a notice, message or toast from opening a workbook (also by a script on the session) lands in the opened workbook's window instead of being parked with the previous one. --- crates/ui-egui/src/lib.rs | 7 +++++++ crates/ui-egui/tests/doc_windows.rs | 25 +++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index f405a3d18..19f69a90f 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -648,6 +648,8 @@ impl SheetApp { /// Shows a command error: a toast for an unavailable command, else a message box. fn alert(&mut self, e: String) { + // Shown in the window the user ends up in, also after a command that switched windows. + self.follow_active_window(); if e.contains("is not available right now") { self.toast = Some((e, now_ms())); } else { @@ -799,6 +801,8 @@ impl SheetApp { /// Picks up requests commands made (dialogs, edit mode…). pub fn after_engine(&mut self) { + // The requests belong to the window the command left active. + self.follow_active_window(); for req in self.session.take_ui_requests() { match req { UiRequest::Dialog(name, params) => self.open_dialog(&name, params), @@ -924,6 +928,9 @@ impl SheetApp { /// most important one (a preserved VBA project outranks, say, a skipped optional part). /// Kept deliberately generic: any open-time warning can reach the user this way. pub fn show_open_warnings(&mut self, r: &Json) { + // The opened workbook's window shows them, however it was opened (a script may have run + // `file.open` on the session directly). + self.follow_active_window(); let Some(list) = r.get("warnings").and_then(Json::as_array) else { self.notice = None; return; diff --git a/crates/ui-egui/tests/doc_windows.rs b/crates/ui-egui/tests/doc_windows.rs index ff8e2d631..b6d518ce2 100644 --- a/crates/ui-egui/tests/doc_windows.rs +++ b/crates/ui-egui/tests/doc_windows.rs @@ -282,3 +282,28 @@ fn menu_commands_follow_the_focused_window_and_leaving_a_window_enters_its_draft desk.app.run("cell.set", json!({"cell": "B1", "input": "menu"})).unwrap(); assert_eq!(desk.app.session.documents()[0].wb.active().unwrap().value(CellRef::new(0, 1)), Value::text("menu")); } + +#[test] +fn feedback_from_opening_a_workbook_shows_in_its_new_window() { + let mut desk = Desk::new(true); + desk.app.run("cell.set", json!({"cell": "A1", "input": "kept"})).unwrap(); + desk.frames(2); + let book1 = desk.window_of(0); + let mut wb = gridcraft_engine::model::Workbook::new(); + wb.vba_project = Some(b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1 fake OLE2 vbaProject".to_vec().into()); + let b64 = gridcraft_engine::io::base64_encode(&gridcraft_engine::io::save_bytes(&wb, "macros.xlsm").unwrap()); + desk.frame_with(|app, _| { + // A script opening the file on the session, then the interface reporting on it. + let r = app.session.run("file.open", json!({"name": "macros.xlsm", "base64": b64})).unwrap(); + app.show_open_warnings(&r); + app.run_or_alert("no.such.command", json!({})); + }); + desk.frames(2); + let opened = desk.window_of(1); + assert_eq!(desk.windows.shown_windows(), [book1, opened]); + assert_eq!(desk.app.session.active_window(), Some(opened)); + assert_eq!(desk.app.notice.as_ref().map(|n| n.kind), Some(gridcraft_ui_egui::NoticeKind::Warning), "the macro notice shows in the opened window"); + assert!(desk.app.message.is_some(), "and so does the alert"); + desk.app.enter_window(book1); + assert!(desk.app.notice.is_none() && desk.app.message.is_none(), "Book1's window shows neither"); +} From 94fcbf8b4c50196e2c8b0c8eb0792c5042ba087b Mon Sep 17 00:00:00 2001 From: aiongg Date: Mon, 26 Oct 2026 02:37:42 +0000 Subject: [PATCH 3/5] G3: craft-libs at e8b4dde5 (craft-libs#340) craft-update, craft-window, craft-ui and craft-print all come from one craft-libs revision, the head of storytold/craft-libs#340 on the aiongg fork, until #340 merges. --- Cargo.lock | 249 ++++++++++++++++++++++++++++++++++---- Cargo.toml | 10 +- apps/gridcraft/Cargo.toml | 4 +- crates/ui-egui/Cargo.toml | 7 +- 4 files changed, 241 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4e7cf4a26..21b2f04dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -220,7 +220,7 @@ dependencies = [ "objc2-foundation 0.3.2", "parking_lot", "percent-encoding", - "windows-sys 0.52.0", + "windows-sys 0.61.2", "wl-clipboard-rs", "x11rb", ] @@ -742,6 +742,15 @@ dependencies = [ "x509-cert", ] +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.21", +] + [[package]] name = "codespan-reporting" version = "0.13.1" @@ -877,13 +886,41 @@ dependencies = [ "libc", ] +[[package]] +name = "craft-fonts-build" +version = "0.1.0" +source = "git+https://github.com/aiongg/craft-libs?rev=e8b4dde5137fe41823326b55018c31302218d3a4#e8b4dde5137fe41823326b55018c31302218d3a4" + +[[package]] +name = "craft-i18n" +version = "0.1.0" +source = "git+https://github.com/aiongg/craft-libs?rev=e8b4dde5137fe41823326b55018c31302218d3a4#e8b4dde5137fe41823326b55018c31302218d3a4" +dependencies = [ + "fixed_decimal", + "icu_decimal", + "icu_locale_core", + "icu_locale_fallback", + "icu_normalizer", + "icu_plurals", + "icu_properties", + "icu_provider", + "icu_provider_adapters", + "icu_provider_blob", + "log", + "objc2-foundation 0.3.2", + "sys-locale", + "unicode-bidi", + "windows-registry", + "writeable", +] + [[package]] name = "craft-print" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=25da1314d443171b4adeaad2cd9454b273f35c6f#25da1314d443171b4adeaad2cd9454b273f35c6f" +source = "git+https://github.com/aiongg/craft-libs?rev=e8b4dde5137fe41823326b55018c31302218d3a4#e8b4dde5137fe41823326b55018c31302218d3a4" dependencies = [ "craft-winprint-gdi", - "miniz_oxide 0.8.9", + "miniz_oxide 0.9.1", "roxmltree", "tempfile", "winprint", @@ -893,15 +930,27 @@ dependencies = [ [[package]] name = "craft-ui" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=25da1314d443171b4adeaad2cd9454b273f35c6f#25da1314d443171b4adeaad2cd9454b273f35c6f" +source = "git+https://github.com/aiongg/craft-libs?rev=e8b4dde5137fe41823326b55018c31302218d3a4#e8b4dde5137fe41823326b55018c31302218d3a4" dependencies = [ + "craft-fonts-build", + "craft-i18n", "egui", + "epaint", + "js-sys", + "log", + "objc2 0.6.5", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "skrifa", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", ] [[package]] name = "craft-update" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=495e5971bc024aa96e6ed318cb6e7cd3a0103184#495e5971bc024aa96e6ed318cb6e7cd3a0103184" +source = "git+https://github.com/aiongg/craft-libs?rev=e8b4dde5137fe41823326b55018c31302218d3a4#e8b4dde5137fe41823326b55018c31302218d3a4" dependencies = [ "chrono", "dunce", @@ -923,6 +972,17 @@ dependencies = [ "zip 9.1.0", ] +[[package]] +name = "craft-window" +version = "0.1.0" +source = "git+https://github.com/aiongg/craft-libs?rev=e8b4dde5137fe41823326b55018c31302218d3a4#e8b4dde5137fe41823326b55018c31302218d3a4" +dependencies = [ + "egui", + "objc2 0.6.5", + "objc2-app-kit 0.3.2", + "wayland-client", +] + [[package]] name = "craft-winprint-gdi" version = "0.1.0" @@ -1395,7 +1455,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1476,6 +1536,17 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" +[[package]] +name = "fixed_decimal" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2214fb00f669ee96bee5bb29dfb1ddabe058b2bc426817efc79afa03c9bc55ae" +dependencies = [ + "displaydoc", + "smallvec", + "writeable", +] + [[package]] name = "fixedbitset" version = "0.5.7" @@ -2011,6 +2082,7 @@ version = "0.12.1" dependencies = [ "craft-ui", "craft-update", + "craft-window", "egui", "egui_kittest", "epaint_default_fonts", @@ -2307,6 +2379,23 @@ dependencies = [ "zerovec", ] +[[package]] +name = "icu_decimal" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eb8f655bba2d0c0459e43a5b0e6cd3cd388109898fb3d85d048165e8b0abd08" +dependencies = [ + "displaydoc", + "fixed_decimal", + "icu_locale_core", + "icu_pattern", + "icu_plurals", + "icu_provider", + "serde", + "writeable", + "zerovec", +] + [[package]] name = "icu_locale_core" version = "2.3.0" @@ -2315,11 +2404,32 @@ checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" dependencies = [ "displaydoc", "litemap", + "serde", "tinystr", "writeable", "zerovec", ] +[[package]] +name = "icu_locale_fallback" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "251af8e57c9400e3eb58242fe5b8b1152b2a64fdf4cf632f923c38ccee6f2fa9" +dependencies = [ + "icu_locale_core", + "icu_locale_fallback_data", + "icu_provider", + "potential_utf", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locale_fallback_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "decf2a22ec8fa68f1a0c1129a3f8583f8f8bc24e8b9ccbe98ead99f62a4dc3a8" + [[package]] name = "icu_normalizer" version = "2.3.0" @@ -2331,6 +2441,9 @@ dependencies = [ "icu_properties", "icu_provider", "smallvec", + "utf16_iter", + "utf8_iter", + "write16", "zerovec", ] @@ -2340,6 +2453,32 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" +[[package]] +name = "icu_pattern" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc43ce0e76ee8cb0482dc6e2dbb9b76278e8038f45daabf2a2877577e2ceb18f" +dependencies = [ + "displaydoc", + "either", + "serde", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_plurals" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e475e6766ef87b1d3c1f97be6363995b0bfaeddbc789671615df598a97ff0593" +dependencies = [ + "displaydoc", + "fixed_decimal", + "icu_provider", + "serde", + "zerovec", +] + [[package]] name = "icu_properties" version = "2.3.0" @@ -2369,6 +2508,9 @@ checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" dependencies = [ "displaydoc", "icu_locale_core", + "postcard", + "serde", + "stable_deref_trait", "writeable", "yoke", "zerofrom", @@ -2376,6 +2518,30 @@ dependencies = [ "zerovec", ] +[[package]] +name = "icu_provider_adapters" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df887bc4f55dbc773c7c45a0c7dfa75be886f7b7812830a71ab7271cc1801820" +dependencies = [ + "icu_locale_fallback", + "icu_provider", +] + +[[package]] +name = "icu_provider_blob" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5916b44fac319d3f58e4d8d805a627df19e27937b582cd99fcde3bb6b7ce1b0e" +dependencies = [ + "icu_provider", + "postcard", + "serde", + "writeable", + "zerotrie", + "zerovec", +] + [[package]] name = "idna" version = "1.1.0" @@ -2663,6 +2829,9 @@ name = "litemap" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" +dependencies = [ + "serde_core", +] [[package]] name = "litrs" @@ -3288,7 +3457,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3537,12 +3706,24 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "serde", +] + [[package]] name = "potential_utf" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" dependencies = [ + "serde_core", + "writeable", "zerovec", ] @@ -3690,7 +3871,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3949,9 +4130,12 @@ dependencies = [ [[package]] name = "roxmltree" -version = "0.20.0" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" +checksum = "9da7f59328b09405b181fee1018925fad50f157afb43c0c0f5c522c256dd5907" +dependencies = [ + "memchr", +] [[package]] name = "rustc-hash" @@ -3997,7 +4181,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4056,7 +4240,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4742,7 +4926,10 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" dependencies = [ + "js-sys", "libc", + "wasm-bindgen", + "web-sys", ] [[package]] @@ -4784,10 +4971,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.3", "once_cell", "rustix 1.1.5", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4890,6 +5077,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" dependencies = [ "displaydoc", + "serde_core", "zerovec", ] @@ -5224,6 +5412,12 @@ dependencies = [ "serde", ] +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + [[package]] name = "utf8-zero" version = "0.8.1" @@ -5739,7 +5933,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -6084,11 +6278,20 @@ dependencies = [ "wayland-protocols-wlr", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + [[package]] name = "writeable" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" +dependencies = [ + "either", +] [[package]] name = "x11-dl" @@ -6204,9 +6407,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +checksum = "417ef3fcf6c300b7dbd25c85531a746931c02ad536f3cf4786ed1e88223340b3" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -6399,16 +6602,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" dependencies = [ "displaydoc", + "litemap", + "serde_core", "yoke", "zerofrom", + "zerovec", ] [[package]] name = "zerovec" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +checksum = "f58a2799cfe9a1670c7ea8edbd52ef8edb93cb37a7f83bf31ba9e35d9dae6a40" dependencies = [ + "serde", "yoke", "zerofrom", "zerovec-derive", @@ -6416,9 +6623,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.6" +version = "0.11.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +checksum = "c3743ddbf47738d929cd1ae2e199d40d0162979dfe31aef8ae646d2bf00bf97a" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 5ceb7e9f0..451106ae8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,10 @@ gridcraft-engine = { path = "crates/engine" } gridcraft-ui-egui = { path = "crates/ui-egui" } gridcraft-mcp = { path = "crates/mcp" } gridcraft-pdf = { path = "crates/pdf" } -craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "495e5971bc024aa96e6ed318cb6e7cd3a0103184", features = ["egui"] } +# Every craft-libs crate (craft-update and craft-window here, craft-ui in ui-egui, craft-print in +# the app) comes from one craft-libs revision: the head of storytold/craft-libs#340 on the aiongg +# fork. They repin to storytold/craft-libs main once #340 merges. +craft-update = { git = "https://github.com/aiongg/craft-libs", rev = "e8b4dde5137fe41823326b55018c31302218d3a4", version = "=0.1.0", features = ["egui"] } # foundation thiserror = "2" @@ -58,9 +61,8 @@ skrifa = { version = "0.44", default-features = false, features = ["std"] } # Real font bytes for tests (Ubuntu Light, compiled into epaint). epaint_default_fonts = "0.37" # One native window per workbook: the window list, Window menu titles and the immediate-viewport -# driver (craft-libs). Pinned to the aiongg fork's `window-list` branch; repins to -# storytold/craft-libs once that branch merges there. -craft-window = { version = "0.1.0", git = "https://github.com/aiongg/craft-libs", rev = "4fbdea5fcf86ba893d9bc03685d4531cfdd4efc2", default-features = false, features = ["egui"] } +# driver. Same craft-libs revision as craft-update above. +craft-window = { version = "0.1.0", git = "https://github.com/aiongg/craft-libs", rev = "e8b4dde5137fe41823326b55018c31302218d3a4", default-features = false, features = ["egui"] } [workspace.lints.rust] unsafe_code = "forbid" diff --git a/apps/gridcraft/Cargo.toml b/apps/gridcraft/Cargo.toml index 52b577cc5..61d197d7d 100644 --- a/apps/gridcraft/Cargo.toml +++ b/apps/gridcraft/Cargo.toml @@ -28,7 +28,9 @@ winresource = "0.1" winit = { version = "0.30", default-features = false, features = ["x11"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -craft-print = { git = "https://github.com/storytold/craft-libs", rev = "25da1314d443171b4adeaad2cd9454b273f35c6f" } +# Same craft-libs revision as the workspace's craft-update and craft-window (see the root +# Cargo.toml): the head of storytold/craft-libs#340 on the aiongg fork. +craft-print = { git = "https://github.com/aiongg/craft-libs", rev = "e8b4dde5137fe41823326b55018c31302218d3a4" } craft-update = { workspace = true } rfd = { workspace = true } arboard = { workspace = true } diff --git a/crates/ui-egui/Cargo.toml b/crates/ui-egui/Cargo.toml index 1c3531db3..33c967b84 100644 --- a/crates/ui-egui/Cargo.toml +++ b/crates/ui-egui/Cargo.toml @@ -9,9 +9,10 @@ description = "GridCraft egui frontend (Excel-style UI); swappable — nothing b workspace = true [dependencies] -# The shared capture slot (`craft_ui::capture`) comes from the same reviewed immutable -# craft-libs revision as the desktop's craft-print dependency. -craft-ui = { git = "https://github.com/storytold/craft-libs", rev = "25da1314d443171b4adeaad2cd9454b273f35c6f" } +# The shared capture slot (`craft_ui::capture`) comes from the one craft-libs revision every +# craft-libs dependency uses (see the root Cargo.toml): the head of storytold/craft-libs#340 on +# the aiongg fork, repinned to storytold/craft-libs main once #340 merges. +craft-ui = { git = "https://github.com/aiongg/craft-libs", rev = "e8b4dde5137fe41823326b55018c31302218d3a4" } gridcraft-engine = { workspace = true } gridcraft-chart = { workspace = true } # `monochrome_emoji_fonts` restores the Noto Emoji and emoji-icon faces that 0.37 moved behind From 7085aa1c472ab287c7da43f84e4053ca14888cc0 Mon Sep 17 00:00:00 2001 From: aiongg Date: Mon, 26 Oct 2026 02:45:24 +0000 Subject: [PATCH 4/5] G3: build on current main Screenshots are admitted in logic for the root viewport and issued from its pass, so nothing reads the focused native window any more; the field goes. --- crates/ui-egui/src/doc_windows.rs | 2 -- crates/ui-egui/src/lib.rs | 3 --- 2 files changed, 5 deletions(-) diff --git a/crates/ui-egui/src/doc_windows.rs b/crates/ui-egui/src/doc_windows.rs index 88ba3967a..bc4ad064e 100644 --- a/crates/ui-egui/src/doc_windows.rs +++ b/crates/ui-egui/src/doc_windows.rs @@ -141,7 +141,6 @@ impl DocWindows { let ctx = ui.ctx().clone(); self.sync(app, &ctx); let focused = self.list.active().and_then(|id| self.list.get(id).copied()); - app.focus_window = focused; let titles: std::collections::HashMap = self.list.iter().map(|(_, w)| (*w, window_title(app, *w))).collect(); let base = &self.base; // The window a command run in some window's UI activated (File › New, Switch Windows, a close). @@ -189,7 +188,6 @@ impl DocWindows { app.enter_window(target); } self.sync(app, &ctx); - app.focus_window = self.list.active().and_then(|id| self.list.get(id).copied()); } /// A window's close button. Returns the window to bring forward when the close needs an diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 53bb7a95d..9f163d052 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -374,8 +374,6 @@ pub struct SheetApp { /// tests, embedding hosts) one window shows the active workbook window, and its close button /// quits. pub native_windows: bool, - /// The native window that has focus, which menu commands and screenshots go to. - pub(crate) focus_window: Option, /// The update check, the startup prompt and a running self-update (desktop only). #[cfg(not(target_arch = "wasm32"))] updates_rt: updates_ui::Runtime, @@ -436,7 +434,6 @@ impl SheetApp { ui_window: None, parked: HashMap::new(), native_windows: false, - focus_window: None, #[cfg(not(target_arch = "wasm32"))] updates_rt: Default::default(), } From c7911c836a650c60afa5af6a2b5f7275195fb9a2 Mon Sep 17 00:00:00 2001 From: aiongg Date: Mon, 26 Oct 2026 03:28:46 +0000 Subject: [PATCH 5/5] G3: craft-libs at storytold main b926b5c0 --- Cargo.lock | 12 ++++++------ Cargo.toml | 7 +++---- apps/gridcraft/Cargo.toml | 4 ++-- crates/ui-egui/Cargo.toml | 5 ++--- 4 files changed, 13 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 21b2f04dc..53b2326da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -889,12 +889,12 @@ dependencies = [ [[package]] name = "craft-fonts-build" version = "0.1.0" -source = "git+https://github.com/aiongg/craft-libs?rev=e8b4dde5137fe41823326b55018c31302218d3a4#e8b4dde5137fe41823326b55018c31302218d3a4" +source = "git+https://github.com/storytold/craft-libs?rev=b926b5c03e256c86ca10934f58d03f628810b5b7#b926b5c03e256c86ca10934f58d03f628810b5b7" [[package]] name = "craft-i18n" version = "0.1.0" -source = "git+https://github.com/aiongg/craft-libs?rev=e8b4dde5137fe41823326b55018c31302218d3a4#e8b4dde5137fe41823326b55018c31302218d3a4" +source = "git+https://github.com/storytold/craft-libs?rev=b926b5c03e256c86ca10934f58d03f628810b5b7#b926b5c03e256c86ca10934f58d03f628810b5b7" dependencies = [ "fixed_decimal", "icu_decimal", @@ -917,7 +917,7 @@ dependencies = [ [[package]] name = "craft-print" version = "0.1.0" -source = "git+https://github.com/aiongg/craft-libs?rev=e8b4dde5137fe41823326b55018c31302218d3a4#e8b4dde5137fe41823326b55018c31302218d3a4" +source = "git+https://github.com/storytold/craft-libs?rev=b926b5c03e256c86ca10934f58d03f628810b5b7#b926b5c03e256c86ca10934f58d03f628810b5b7" dependencies = [ "craft-winprint-gdi", "miniz_oxide 0.9.1", @@ -930,7 +930,7 @@ dependencies = [ [[package]] name = "craft-ui" version = "0.1.0" -source = "git+https://github.com/aiongg/craft-libs?rev=e8b4dde5137fe41823326b55018c31302218d3a4#e8b4dde5137fe41823326b55018c31302218d3a4" +source = "git+https://github.com/storytold/craft-libs?rev=b926b5c03e256c86ca10934f58d03f628810b5b7#b926b5c03e256c86ca10934f58d03f628810b5b7" dependencies = [ "craft-fonts-build", "craft-i18n", @@ -950,7 +950,7 @@ dependencies = [ [[package]] name = "craft-update" version = "0.1.0" -source = "git+https://github.com/aiongg/craft-libs?rev=e8b4dde5137fe41823326b55018c31302218d3a4#e8b4dde5137fe41823326b55018c31302218d3a4" +source = "git+https://github.com/storytold/craft-libs?rev=b926b5c03e256c86ca10934f58d03f628810b5b7#b926b5c03e256c86ca10934f58d03f628810b5b7" dependencies = [ "chrono", "dunce", @@ -975,7 +975,7 @@ dependencies = [ [[package]] name = "craft-window" version = "0.1.0" -source = "git+https://github.com/aiongg/craft-libs?rev=e8b4dde5137fe41823326b55018c31302218d3a4#e8b4dde5137fe41823326b55018c31302218d3a4" +source = "git+https://github.com/storytold/craft-libs?rev=b926b5c03e256c86ca10934f58d03f628810b5b7#b926b5c03e256c86ca10934f58d03f628810b5b7" dependencies = [ "egui", "objc2 0.6.5", diff --git a/Cargo.toml b/Cargo.toml index 451106ae8..e23c106e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,9 +25,8 @@ gridcraft-ui-egui = { path = "crates/ui-egui" } gridcraft-mcp = { path = "crates/mcp" } gridcraft-pdf = { path = "crates/pdf" } # Every craft-libs crate (craft-update and craft-window here, craft-ui in ui-egui, craft-print in -# the app) comes from one craft-libs revision: the head of storytold/craft-libs#340 on the aiongg -# fork. They repin to storytold/craft-libs main once #340 merges. -craft-update = { git = "https://github.com/aiongg/craft-libs", rev = "e8b4dde5137fe41823326b55018c31302218d3a4", version = "=0.1.0", features = ["egui"] } +# the app) comes from one storytold/craft-libs revision. +craft-update = { git = "https://github.com/storytold/craft-libs", rev = "b926b5c03e256c86ca10934f58d03f628810b5b7", version = "=0.1.0", features = ["egui"] } # foundation thiserror = "2" @@ -62,7 +61,7 @@ skrifa = { version = "0.44", default-features = false, features = ["std"] } epaint_default_fonts = "0.37" # One native window per workbook: the window list, Window menu titles and the immediate-viewport # driver. Same craft-libs revision as craft-update above. -craft-window = { version = "0.1.0", git = "https://github.com/aiongg/craft-libs", rev = "e8b4dde5137fe41823326b55018c31302218d3a4", default-features = false, features = ["egui"] } +craft-window = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "b926b5c03e256c86ca10934f58d03f628810b5b7", default-features = false, features = ["egui"] } [workspace.lints.rust] unsafe_code = "forbid" diff --git a/apps/gridcraft/Cargo.toml b/apps/gridcraft/Cargo.toml index 61d197d7d..8429070b7 100644 --- a/apps/gridcraft/Cargo.toml +++ b/apps/gridcraft/Cargo.toml @@ -29,8 +29,8 @@ winit = { version = "0.30", default-features = false, features = ["x11"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] # Same craft-libs revision as the workspace's craft-update and craft-window (see the root -# Cargo.toml): the head of storytold/craft-libs#340 on the aiongg fork. -craft-print = { git = "https://github.com/aiongg/craft-libs", rev = "e8b4dde5137fe41823326b55018c31302218d3a4" } +# Cargo.toml). +craft-print = { git = "https://github.com/storytold/craft-libs", rev = "b926b5c03e256c86ca10934f58d03f628810b5b7" } craft-update = { workspace = true } rfd = { workspace = true } arboard = { workspace = true } diff --git a/crates/ui-egui/Cargo.toml b/crates/ui-egui/Cargo.toml index 33c967b84..ac1a67c33 100644 --- a/crates/ui-egui/Cargo.toml +++ b/crates/ui-egui/Cargo.toml @@ -10,9 +10,8 @@ workspace = true [dependencies] # The shared capture slot (`craft_ui::capture`) comes from the one craft-libs revision every -# craft-libs dependency uses (see the root Cargo.toml): the head of storytold/craft-libs#340 on -# the aiongg fork, repinned to storytold/craft-libs main once #340 merges. -craft-ui = { git = "https://github.com/aiongg/craft-libs", rev = "e8b4dde5137fe41823326b55018c31302218d3a4" } +# craft-libs dependency uses (see the root Cargo.toml). +craft-ui = { git = "https://github.com/storytold/craft-libs", rev = "b926b5c03e256c86ca10934f58d03f628810b5b7" } gridcraft-engine = { workspace = true } gridcraft-chart = { workspace = true } # `monochrome_emoji_fonts` restores the Noto Emoji and emoji-icon faces that 0.37 moved behind