From d3659655ff0d27cf9ef85952eae0ab81e88dcf64 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Mon, 12 Oct 2026 23:44:57 -0400 Subject: [PATCH] UI: the File tab (Backstage) like PowerPoint's (#76) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit File (first ribbon tab) or Alt+F opens a full-window view in place of the ribbon, slides and status bar: a side menu (Back, Home, New, Open, Info, Save, Save As, Print, Export, Close; Account and Options at the bottom) and the chosen page. Esc or Back returns; Up/Down walk the side menu and show each page as it gets the focus; Enter runs Save/Close/Options. Laid out after PowerPoint for Windows as observed: 160-pt menu a step darker than the page, outline icons, current page in the accent colour. The start screen is now File › Home (as in PowerPoint), with only Home, New and Open at the top and no Back. Pages call existing commands and file helpers: file.new {theme} and the sample deck (Home, New); recent files with search, Browse and Recover Unsaved (Open); file.info and file.properties, Accessibility Checker, Revert (Info); file.save / file.saveAs / file.export by type (Save, Save As); a PDF with the Print page's slides, layout (full page, notes, handouts 1-9), hidden slides and frames, opened in the system viewer or downloaded on the web, with a page preview (Print, Ctrl+P); Create PDF, Create Handouts and Change File Type (Export); appearance and About (Account); Preferences (Options). Share (cloud) is left out. Close and Ctrl+W now ask Save / Don't Save / Cancel about unsaved changes instead of closing at once. The Export dialog shares SlideApp::export_as with the Export page. New UI commands: app.backstage {page}, app.closePresentation, app.print; ui.inspect reports the page. New icons drawn in code: back, save as, account, recent, new file, open folder. Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/ui-egui/src/backstage.rs | 1245 +++++++++++++++++++++++++ crates/ui-egui/src/backstage_tests.rs | 438 +++++++++ crates/ui-egui/src/canvas.rs | 2 +- crates/ui-egui/src/control.rs | 3 +- crates/ui-egui/src/dialogs.rs | 204 +--- crates/ui-egui/src/icons.rs | 34 + crates/ui-egui/src/lib.rs | 238 ++++- crates/ui-egui/src/menus.rs | 22 +- crates/ui-egui/src/ribbon.rs | 19 +- 9 files changed, 2003 insertions(+), 202 deletions(-) create mode 100644 crates/ui-egui/src/backstage.rs create mode 100644 crates/ui-egui/src/backstage_tests.rs diff --git a/crates/ui-egui/src/backstage.rs b/crates/ui-egui/src/backstage.rs new file mode 100644 index 0000000..f1d0fde --- /dev/null +++ b/crates/ui-egui/src/backstage.rs @@ -0,0 +1,1245 @@ +//! The File tab (Backstage, #76): a full-window view that replaces the ribbon, slides and status +//! bar until Esc or the back arrow returns to the presentation. With nothing open it is the start +//! screen: PowerPoint's start screen and File › Home are the same page. +//! +//! A side menu on the left picks a page (Home, New, Open, Info, Save As, Print, Export, Account); +//! Save, Close and Options act at once. Every action runs an existing command through +//! [`SlideApp::run`] or one of the app's file helpers ([`SlideApp::export_as`], +//! [`SlideApp::print_pdf`], [`SlideApp::save_as_dialog`]); the state here is UI-only and never +//! saved. Recent presentations are listed by path: nothing on disk is read until one is picked. +//! +//! Laid out after PowerPoint for Windows (observed black-box): a 160-point side menu darker than +//! the page, 40-point entries with an outline icon, the current page in the accent colour. + +use egui::{Align2, Color32, CornerRadius, Key, Rect, RichText, Sense, Stroke, StrokeKind, Ui, pos2, vec2}; +use serde_json::{Value, json}; + +use crate::SlideApp; +use crate::icons::{self, Icon}; +use crate::theme::{self, Tokens}; + +/// Width of the side menu. +pub const NAV_W: f32 = 160.0; +/// Height of a side-menu entry (the back arrow's row is a little taller). +const ITEM_H: f32 = 40.0; +const BACK_H: f32 = 46.0; +/// The widest a page's content grows. +const CONTENT_W: f32 = 1180.0; + +/// A page of the File tab. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Page { + Home, + New, + Open, + Info, + SaveAs, + Print, + Export, + Account, +} + +impl Page { + pub const ALL: [Page; 8] = [Page::Home, Page::New, Page::Open, Page::Info, Page::SaveAs, Page::Print, Page::Export, Page::Account]; + + /// The id `app.backstage {page}` takes. + pub fn id(self) -> &'static str { + match self { + Page::Home => "home", + Page::New => "new", + Page::Open => "open", + Page::Info => "info", + Page::SaveAs => "saveAs", + Page::Print => "print", + Page::Export => "export", + Page::Account => "account", + } + } + + pub fn parse(id: &str) -> Option { + let id = id.trim(); + Page::ALL.into_iter().find(|p| p.id().eq_ignore_ascii_case(id)) + } + + /// Side-menu label and page heading. + pub fn label(self) -> &'static str { + match self { + Page::Home => "Home", + Page::New => "New", + Page::Open => "Open", + Page::Info => "Info", + Page::SaveAs => "Save As", + Page::Print => "Print", + Page::Export => "Export", + Page::Account => "Account", + } + } + + fn icon(self) -> Icon { + match self { + Page::Home => Icon::Home, + Page::New => Icon::NewFile, + Page::Open => Icon::OpenFolder, + Page::Info => Icon::Info, + Page::SaveAs => Icon::SaveAs, + Page::Print => Icon::Print, + Page::Export => Icon::Export, + Page::Account => Icon::Account, + } + } + + /// Pages about the open presentation (hidden on the start screen). + pub fn needs_doc(self) -> bool { + matches!(self, Page::Info | Page::SaveAs | Page::Print | Page::Export) + } +} + +/// A side-menu entry: a page, or an action that runs at once. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Nav { + Page(Page), + Save, + Close, + Options, +} + +impl Nav { + fn label(self) -> &'static str { + match self { + Nav::Page(p) => p.label(), + Nav::Save => "Save", + Nav::Close => "Close", + Nav::Options => "Options", + } + } + fn icon(self) -> Icon { + match self { + Nav::Page(p) => p.icon(), + Nav::Save => Icon::Save, + Nav::Close => Icon::File, + Nav::Options => Icon::Settings, + } + } +} + +/// The side menu, top group then bottom group, in keyboard order. PowerPoint's Share (cloud) is +/// left out; with nothing open only Home, New and Open stay at the top. +pub fn entries(has_doc: bool) -> (Vec