diff --git a/ROADMAP.md b/ROADMAP.md index cea74544..df7f6e65 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -17,6 +17,7 @@ > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (progress log: Lucide icon gallery; previous: minor, hours to ~95% per audience added; ready 56% → 55%; stage stays alpha) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (Size and Position dialog; breadth stays 79%, ready stays ~55%; previous: minor, hours to ~95% per audience) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (progress log: previous and next comment; breadth stays 79%; previous: minor, hours to ~95%) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) +> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** trivial (View › New Window; previous: minor, hours to ~95% per audience added) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) DeckCraft aims at full PowerPoint parity, and to be better: faster, open (a documented zip+JSON format plus PPTX), scriptable by agents (CLI, JSON control channel, MCP), and available everywhere @@ -167,6 +168,7 @@ app's JSON control channel (`deckcraft --control PORT`). | 2026-10-14 | Insert › Icons: a gallery of Lucide stroke icons (release 1.56.0) inserted as custom shapes, no fill, round stroke scaled from the 24-unit design | | 2026-10-14 | Size and Position dialog: height, width, rotation, scale and lock aspect ratio in one undo step, from Shape Format › Size and the shape menu. Breadth stays 79% | | 2026-10-14 | Review: Previous and Next comment walk every comment in the deck; the Comments pane lists every slide and highlights the one you landed on | +| 2026-10-13 | View › New Window opens a second window on the same presentation. The window has its own slide and shows edits made in the first window. Arrange All is not included | | 2026-10-13 | Slide transitions like PowerPoint: every transition and option measured frame by frame against PowerPoint's video export and retuned (curves, directions, soft edges, 3-D perspective, contents moving over backgrounds); PowerPoint's own XML elements and defaults read and written (diagonal Wipe, Zoom, Shape In/Out, Glitter Hexagons, Shred In/Out, Page Curl Single); `deckcraft-cli render-transition` renders frames headless | | 2026-10-13 | Wild text cases measured against PowerPoint (102 synthetic slides): bidi by run language, East Asian vertical text upright, mongolianVert/wordArtVert/wordArtVertRtl, underline styles and strikethrough at the font's metrics, centre/right/decimal tabs, East Asian, Thai, Hindi, Arabic and Hebrew numbering, small caps, all-caps "ß", transparent text, soft hyphens, kinsoku, rtlCol and just/dist anchoring, emoji presentation, fallback faces that no longer move the baseline | | 2026-10-13 | Render matches PowerPoint more closely on wild shape, picture and table cases: even-odd custom paths, dashes and caps, arrowheads, radial and path gradients, tile rects, rotWithShape, gradient lines, shadows cast by what is drawn (group and perspective too), group fill, picture borders, tiles, reflection, recolour (biLevel, alphaInv, brightness/contrast), table borders and grid default, 20+ preset shapes by their ECMA formulas, default miter joins, and approximate 3-D bevels, cameras and face lighting | @@ -208,6 +210,7 @@ app's JSON control channel (`deckcraft --control PORT`). | 2026-10-14 | trivial | Progress log: Lucide icon gallery | | 2026-10-14 | trivial | Progress log: Size and Position dialog. Breadth stays 79%, ready stays ~55% | | 2026-10-14 | trivial | Progress log: previous and next comment | +| 2026-10-13 | trivial | Progress log: View › New Window | | 2026-10-13 | trivial | Progress log: wild text cases | | 2026-10-13 | trivial | Progress log: slide show for interactive decks | | 2026-10-12 | trivial | Progress log: File tab (Backstage, #76) | diff --git a/crates/ui-egui/src/doc_window.rs b/crates/ui-egui/src/doc_window.rs new file mode 100644 index 00000000..4445550d --- /dev/null +++ b/crates/ui-egui/src/doc_window.rs @@ -0,0 +1,191 @@ +//! View › New Window: one more window on the same presentation. +//! +//! The window shows that document live, on its own slide, and does not move the editor's +//! selection. It does not edit, and Arrange All is not here. Closing the window leaves the +//! presentation open. + +use deckcraft_render::RenderOpts; +use egui::{Color32, CornerRadius, Rect, RichText, pos2, vec2}; +use serde_json::{Value, json}; + +use crate::SlideApp; + +/// The extra window. `uid` is the presentation it was opened on. +pub struct DocWindow { + pub uid: u64, + pub slide: usize, + key: Option<(u64, u64, usize, u32, u32, bool)>, + tex: Option, +} + +/// `view.newWindow`. `{on: false}` closes it. `{slide}` or `{delta}` moves only this window. +pub fn command(app: &mut SlideApp, p: &Value) -> Result { + if p.get("on").and_then(Value::as_bool) == Some(false) { + app.doc_window = None; + return Ok(json!({"open": false})); + } + let (uid, n, title, editor_slide) = { + let active = app.session.active().ok_or("New Window needs an open presentation")?; + let n = active.doc.slides.len(); + if n == 0 { + return Err("New Window needs a slide".into()); + } + (active.uid, n, window_title(&active.title()), active.selection.slide.min(n - 1)) + }; + let mut slide = app.doc_window.as_ref().filter(|w| w.uid == uid).map(|w| w.slide).unwrap_or(editor_slide); + if let Some(i) = p.get("slide").and_then(Value::as_u64) { + slide = i as usize; + } + if let Some(delta) = p.get("delta").and_then(Value::as_i64) { + slide = shift(slide, delta, n); + } + if slide >= n { + slide = n - 1; + } + match app.doc_window.as_mut().filter(|w| w.uid == uid) { + Some(w) => w.slide = slide, + None => { + app.doc_window = Some(DocWindow { uid, slide, key: None, tex: None }); + } + } + Ok(json!({"open": true, "window": 2, "slide": slide, "slides": n, "title": title})) +} + +pub fn window_title(name: &str) -> String { + format!("{name}:2") +} + +fn shift(slide: usize, delta: i64, n: usize) -> usize { + if n == 0 { + return 0; + } + let last = (n as i64) - 1; + (slide as i64).saturating_add(delta).clamp(0, last) as usize +} + +/// Draw the extra window, or drop it when its presentation has been closed. +pub fn show(app: &mut SlideApp, ctx: &egui::Context) { + let Some(uid) = app.doc_window.as_ref().map(|w| w.uid) else { return }; + let found = + app.session.documents().iter().find(|d| d.uid == uid).map(|d| (d.doc.slides.len(), window_title(&d.title()), d.revision, d.doc.clone())); + let Some((n, title, revision, doc)) = found else { + app.doc_window = None; + return; + }; + if n == 0 { + app.doc_window = None; + return; + } + let slide = app.doc_window.as_ref().map(|w| w.slide.min(n - 1)).unwrap_or(0); + if let Some(w) = app.doc_window.as_mut() { + w.slide = slide; + } + let grayscale = app.ui.grayscale; + ctx.show_viewport_immediate( + egui::ViewportId::from_hash_of("deckcraft_doc_window"), + egui::ViewportBuilder::default().with_title(title.clone()).with_inner_size([960.0, 640.0]).with_min_inner_size([480.0, 360.0]), + |ui, _| { + let t = crate::theme::Tokens::get(ui); + egui::CentralPanel::default().frame(egui::Frame::NONE.fill(t.canvas)).show(ui, |ui| { + egui::Panel::top("doc_window_bar") + .exact_size(40.0) + .frame(egui::Frame::NONE.fill(t.chrome).inner_margin(egui::Margin::symmetric(12, 8))) + .show(ui, |ui| { + ui.horizontal_centered(|ui| { + ui.label(RichText::new(title.clone()).font(crate::theme::bold(13.0)).color(t.text)); + ui.add_space(16.0); + ui.label(RichText::new(format!("Slide {} of {n}", slide + 1)).color(t.text_dim).font(crate::theme::font(13.0))); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if ui.add_enabled(slide + 1 < n, egui::Button::new("Next")).clicked() { + let _ = app.run("view.newWindow", json!({"delta": 1})); + } + if ui.add_enabled(slide > 0, egui::Button::new("Previous")).clicked() { + let _ = app.run("view.newWindow", json!({"delta": -1})); + } + }); + }); + }); + egui::CentralPanel::default().frame(egui::Frame::NONE.fill(t.canvas).inner_margin(egui::Margin::same(16))).show(ui, |ui| { + let avail = ui.available_rect_before_wrap(); + let (w, h) = pixel_size(&doc, avail.width()); + let tex = ensure_texture(app, ui.ctx(), &doc, uid, revision, slide, w, h, grayscale); + if let Some(tex) = tex { + let fitted = fit_rect(avail, w, h); + ui.painter().rect_filled(fitted, CornerRadius::ZERO, Color32::WHITE); + ui.painter().image(tex.id(), fitted, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE); + } + }); + }); + if ui.input(|i| i.viewport().close_requested()) { + app.doc_window = None; + } + }, + ); +} + +fn pixel_size(doc: &deckcraft_model::Presentation, width_pts: f32) -> (u32, u32) { + let w = (width_pts.round() as u32).clamp(64, 1400); + let aspect = doc.slide_size.height / doc.slide_size.width.max(1.0); + let h = ((w as f64) * aspect).round().clamp(1.0, 1400.0) as u32; + (w, h) +} + +fn fit_rect(avail: Rect, w: u32, h: u32) -> Rect { + let aspect = h as f32 / (w as f32).max(1.0); + let mut width = avail.width(); + let mut height = width * aspect; + if height > avail.height() { + height = avail.height(); + width = height / aspect.max(0.01); + } + Rect::from_center_size(avail.center(), vec2(width.max(1.0), height.max(1.0))) +} + +fn ensure_texture( + app: &mut SlideApp, + ctx: &egui::Context, + doc: &deckcraft_model::Presentation, + uid: u64, + revision: u64, + slide: usize, + w: u32, + h: u32, + grayscale: bool, +) -> Option { + let key = (uid, revision, slide, w, h, grayscale); + if let Some(win) = app.doc_window.as_ref() + && win.key == Some(key) + && let Some(tex) = win.tex.clone() + { + return Some(tex); + } + let img = deckcraft_render::render_slide( + doc, + slide, + &RenderOpts { scale: w as f64 / doc.slide_size.width.max(1.0), size: Some((w, h)), edit: true, threads: 0, ..Default::default() }, + ); + let tex = ctx.load_texture("doc-window-slide", crate::textures::to_color_image(&img, grayscale), egui::TextureOptions::LINEAR); + if let Some(win) = app.doc_window.as_mut() { + win.key = Some(key); + win.tex = Some(tex.clone()); + } + Some(tex) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn the_window_title_marks_the_second_window() { + assert_eq!(window_title("Quarterly review"), "Quarterly review:2"); + } + + #[test] + fn shifting_stops_at_the_ends() { + assert_eq!(shift(0, -1, 3), 0); + assert_eq!(shift(1, 1, 3), 2); + assert_eq!(shift(2, 5, 3), 2); + assert_eq!(shift(0, 1, 0), 0); + } +} diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 482df7f4..3a67ac00 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -11,6 +11,7 @@ pub mod canvas; pub mod control; pub mod credits; pub mod dialogs; +pub mod doc_window; pub mod eyedropper; pub mod fillui; pub mod icons; @@ -167,6 +168,7 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[ ("view.collapseRibbon", "Collapse the Ribbon", Some("Cmd+F1"), "{}"), ("view.dark", "Dark Mode", None, "{on?: bool}"), ("view.grayscale", "Grayscale", None, "{on?: bool}"), + ("view.newWindow", "New Window", None, "{on?: bool, slide?: index, delta?: int}"), ("show.start", "Slide Show", Some("F5"), "{from?: index}"), ("show.presenter", "Presenter View", None, "{on?: bool}"), ("show.end", "End Show", None, "{}"), @@ -253,6 +255,8 @@ pub struct SlideApp { pub(crate) thumb_drag: Option<(usize, usize)>, /// Selection pane: folded groups, renaming, drag and drop. pub selpane: selpane::State, + /// View › New Window: a second window on one presentation. `None` when it is closed. + pub doc_window: Option, } impl SlideApp { @@ -295,6 +299,7 @@ impl SlideApp { ui_fonts: uifonts::UiFonts::default(), thumb_drag: None, selpane: selpane::State::default(), + doc_window: None, } } @@ -535,6 +540,7 @@ impl SlideApp { self.restyle = true; } "view.grayscale" => self.ui.grayscale = self.flag(p, self.ui.grayscale), + "view.newWindow" => return doc_window::command(self, p), "show.start" => { let from = p.get("from").and_then(Value::as_u64).map(|v| v as usize).unwrap_or(0); self.start_show(from, false); @@ -998,6 +1004,7 @@ impl SlideApp { eyedropper::tick(self, &ctx); if self.show.is_some() { show::ui(self, ui); + doc_window::show(self, &ctx); self.perf.frame_ms = now_ms() - t0; return; } @@ -1050,6 +1057,7 @@ impl SlideApp { ctx.copy_text(t); } self.drain_requests(); + doc_window::show(self, &ctx); self.perf.frame_ms = now_ms() - t0; } @@ -1395,6 +1403,42 @@ mod tests { app.run("app.home", json!({})).unwrap(); assert!(!app.backstage_open(), "with nothing open the start screen shows anyway"); } + + /// View › New Window is a second window on the open presentation. Its slide is its own; + /// the editor's selection stays where it was. Closing the window leaves the document open. + #[test] + fn new_window_shows_the_same_document_on_its_own_slide() { + let mut app = SlideApp::new(Session::with_new(), Services::default()); + app.session.execute("slide.new", &json!({})).unwrap(); + app.session.execute("slide.go", &json!({"index": 1})).unwrap(); + let err = { + let mut empty = SlideApp::new(Session::new(), Services::default()); + empty.run("view.newWindow", json!({})).unwrap_err() + }; + assert!(err.contains("presentation"), "{err}"); + + let name = app.session.active().unwrap().title(); + let opened = app.run("view.newWindow", json!({})).unwrap(); + assert_eq!(opened["open"], true); + assert_eq!(opened["window"], 2); + assert_eq!(opened["slide"], 1); + assert_eq!(opened["title"], format!("{name}:2")); + assert_eq!(app.session.active().unwrap().selection.slide, 1); + + let moved = app.run("view.newWindow", json!({"delta": -1})).unwrap(); + assert_eq!(moved["slide"], 0); + assert_eq!(app.doc_window.as_ref().unwrap().slide, 0); + assert_eq!(app.session.active().unwrap().selection.slide, 1, "the editor stays on its slide"); + + let again = app.run("view.newWindow", json!({})).unwrap(); + assert_eq!(again["window"], 2); + assert_eq!(again["slide"], 0, "opening again keeps the window's slide"); + + app.session.execute("file.close", &json!({})).unwrap(); + doc_window::show(&mut app, &egui::Context::default()); + assert!(app.doc_window.is_none(), "closing the presentation closes the extra window"); + assert!(app.session.active().is_none()); + } fn canvas_harness() -> egui_kittest::Harness<'static, SlideApp> { let mut harness = egui_kittest::Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_ui_state( |ui, app: &mut SlideApp| { diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 184b3025..276b4f4b 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -209,6 +209,8 @@ pub fn menu_tree() -> Vec<(&'static str, Vec<(&'static str, &'static str)>)> { ("Zoom In", "view.zoomIn"), ("Zoom Out", "view.zoomOut"), ("Fit to Window", "view.fit"), + ("-", ""), + ("New Window", "view.newWindow"), ], ), ( diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index bed031c0..c08b38dc 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -1993,6 +1993,14 @@ fn view(app: &mut SlideApp, ui: &mut Ui) { app.ui.zoom = None; } }); + group(ui, |ui| { + let has = app.session.active().is_some(); + let on = app.doc_window.is_some(); + let resp = big_button_ex(ui, Icon::NewWindow, "New\nWindow", has, false, on).0; + if resp.on_hover_text("Open another window on this presentation").clicked() { + let _ = app.run("view.newWindow", json!({})); + } + }); group(ui, |ui| { let gs = app.ui.grayscale; if big_button_ex(ui, Icon::Grayscale, "Grayscale", true, false, gs).0.clicked() { diff --git a/docs/parity-checklist.md b/docs/parity-checklist.md index 58e4f775..787852ee 100644 --- a/docs/parity-checklist.md +++ b/docs/parity-checklist.md @@ -10,13 +10,15 @@ The summary between ` | P0 | 72 | 61 | 11 | 0 | 92% | | P1 | 65 | 39 | 20 | 6 | 75% | | P2 | 54 | 13 | 15 | 26 | 38% | +| P1 | 65 | 38 | 21 | 6 | 75% | +| P2 | 54 | 13 | 16 | 25 | 39% | | Area | Parity | |---|---| | Application shell | 77% | | Files | 71% | | Slides | 90% | -| Views | 90% | +| Views | 93% | | Masters & themes | 85% | | Shapes | 88% | | Format | 83% | @@ -89,7 +91,7 @@ Open P0 items: Contextual tabs (Shape Format, Picture Format, Table Design/Layou | Views | Rulers, gridlines, guides | P1 | P | | Views | Smart guides | P1 | D | | Views | Grayscale / black and white view | P2 | D | -| Views | New window / arrange windows | P2 | M | +| Views | New window / arrange windows | P2 | P | | Masters & themes | Slide Master view (edit master and layouts) | P0 | D | | Masters & themes | Insert/rename/delete layouts, insert placeholders | P1 | D | | Masters & themes | Handout and Notes masters | P2 | M | diff --git a/docs/roadmap.md b/docs/roadmap.md index 21f13d6b..e30aa0be 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -104,6 +104,8 @@ From the local execution plan (`plan/execution-plan.md`, gitignored). Status as | M8 | Tables and charts | partly | chart data grid, elements, chartex types, draw table | | M9 | SmartArt, equations, WordArt | partly | SmartArt text pane and layouts, equations (PR #55), warp adjust handles | | M10 | Views | done | grid options, New Window | +| M9 | SmartArt, equations, WordArt | partly | SmartArt text pane and layouts, equations (PR #55), text warps | +| M10 | Views | done | grid options, Arrange All | | M11 | Output | partly | vector PDF, SVG, GIF, MP4, print | | M12 | Review | partly | dictionaries, AutoCorrect, find/replace dialog, compare | | M13 | Automation and CLI parity | done | | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 10622c6c..0c248ea0 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -185,7 +185,7 @@ it is the Features row of the dimension table. Automation and Platforms are scor | Application shell | 5 | 71% | ~55% | 8–14 | No native macOS menu bar, thin start screen (templates, recent), one window per presentation missing | [ui-parity.md](ui-parity.md) | | Files (commands) | 4 | 69% | ~50% | see file formats | Print, reuse slides, passwords, legacy and macro formats | [file-format-parity.md](file-format-parity.md) | | Slides and sections | 6 | 90% | ~85% | 3–5 | Zoom (summary/section/slide), copying slides between decks keeps formatting only partly | [parity-checklist.md](parity-checklist.md) | -| Views | 4 | 90% | ~80% | 3–6 | Snap to grid and grid options, New Window | [ui-parity.md](ui-parity.md) | +| Views | 4 | 90% | ~80% | 3–6 | Snap to grid and grid options, Arrange All | [ui-parity.md](ui-parity.md) | | Masters and themes | 7 | 85% | ~70% | 8–14 | Notes and Handout masters, theme variants, `.thmx`, effect schemes | [parity-checklist.md](parity-checklist.md) | | Shapes | 7 | 86% | ~75% | 8–14 | Edit points, warp adjust handles, WordArt depth | [ui-parity.md](ui-parity.md) | | Format (fill, line, effects) | 6 | 81% | ~65% | 8–14 | Format Shape pane depth, bevel and 3-D, picture/texture fill options | [ui-parity.md](ui-parity.md) | diff --git a/docs/ui-parity.md b/docs/ui-parity.md index d0d629f1..50ce2114 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -7,6 +7,7 @@ > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (Insert › Icons is a Lucide stroke gallery; ribbon icons stay original drawings; previous: File tab / Backstage, #76) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (Size and Position dialog; previous: text editing keys, #268) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (Comments pane lists every slide; Review Previous/Next; previous: File tab / Backstage, #76) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) +> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (View › New Window opens a second window on the same presentation; previous: File tab / Backstage, #76) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) > **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (text editing keys observed in PowerPoint for Windows, #268) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) @@ -30,7 +31,7 @@ AGENTS.md §2). Dimension score: **~60% ready** (estimated), **30–50 h** to pa | Status bar: slide counter, language, notes, comments, views, zoom slider | yes | yes | | | Command search (Tell Me) | yes | command palette | #79 default entry fails | | Light / dark appearance | follows the system | light or dark; doesn't follow the system (#41) | 1–2 h | -| One window per presentation, New Window, Arrange All | yes | tabs in one window; no New Window | 3–5 h | +| One window per presentation, New Window, Arrange All | yes | New Window opens one more window on the same presentation (its own slide, live with edits); tabs stay in the first window; no Arrange All | Arrange All | | Quick Access Toolbar customization | yes | no | P1 partial | ## File tab (Backstage) @@ -151,7 +152,7 @@ on the slide as shapes. No pixel comparison against PowerPoint screenshots has b | Missing dialogs (Font, Bullets and Numbering, Find/Replace, Replace Fonts, Action Settings, Print, Insert Object) | 8–14 | | Window chrome, system appearance, palette fixes (#41, #71, #79) | 3–5 | | Grid Options, edit points UI, crop depth | 4–7 | -| Multiple windows | 3–5 | +| Arrange All (New Window is one extra window) | 2–3 | | A screenshot comparison pass on the most-used tabs | 2–4 | | **Total** | **~30–52** | @@ -166,6 +167,7 @@ on the slide as shapes. No pixel comparison against PowerPoint screenshots has b | 2026-10-14 | minor | Insert › Icons is a searchable gallery of Lucide stroke icons inserted as shapes. Ribbon chrome stays original drawings | | 2026-10-14 | minor | Size and Position dialog on Shape Format › Size and the shape context menu. Breadth stays 79%; UI ready stays ~60% | | 2026-10-14 | trivial | Comments pane lists every slide; Review ▸ Previous and Next walk the deck | +| 2026-10-13 | minor | View › New Window: a second window on the same presentation, with its own slide; Arrange All is still missing | | 2026-10-12 | minor | File tab (Backstage) observed in PowerPoint for Windows and matched (#76); the start screen is File › Home | | 2026-10-12 | minor | Off-slide objects drawn and editable around the slide in Normal view (#270) | | 2026-10-12 | minor | Selection Pane matched to PowerPoint (#133): drag and drop, arrows, groups, eye, Lock |