diff --git a/crates/engine/src/catalog.rs b/crates/engine/src/catalog.rs index 66439ecb..c8f34fc1 100644 --- a/crates/engine/src/catalog.rs +++ b/crates/engine/src/catalog.rs @@ -104,6 +104,8 @@ pub static MENU: &[MenuItem] = &[ m!("View", "Soft-proof sRGB", "view.proofSrgb"), m!("View", SEP, ""), m!("View/Panels", "Show/Hide UI", "view.toggleUi"), + // FigCraft's own: the window fills the screen (F11), for setups that hide the taskbar then. + m!("View", "Full screen", "view.fullscreen"), m!("View/Panels", "Reset panel widths", "view.panelWidths"), m!("View/Panels", "Layers", "view.layers"), m!("View/Panels", "Assets", "view.assets"), diff --git a/crates/engine/src/cmd/view.rs b/crates/engine/src/cmd/view.rs index b068544d..6135e5df 100644 --- a/crates/engine/src/cmd/view.rs +++ b/crates/engine/src/cmd/view.rs @@ -268,6 +268,10 @@ pub fn specs() -> Vec { spec!("view.snapObjects", "Snap to objects", "{on?}", false, |s, p| flag(s, p, |v| &mut v.snap_objects)), spec!("view.snapPixel", "Snap to pixel grid", "{on?}", false, |s, p| flag(s, p, |v| &mut v.snap_pixel)), spec!("view.toggleUi", "Show/Hide UI", "{on?}", false, |s, p| flag(s, p, |v| &mut v.ui_hidden)), + spec!("view.fullscreen", "Full screen", "{on?} the window fills the screen (the desktop app; a no-op headless)", false, |s, p| { + s.request_ui("window.fullscreen", json!({"on": boolean(p, "on")})); + ok() + }), spec!("view.layers", "Layers panel", "{}", false, |s, _| ui(s, "panel.layers")), spec!("view.assets", "Assets panel", "{}", false, |s, _| ui(s, "panel.assets")), spec!("view.design", "Design panel", "{}", false, |s, _| ui(s, "panel.design")), diff --git a/crates/engine/src/keymap.rs b/crates/engine/src/keymap.rs index 80f0c99b..c5e3c334 100644 --- a/crates/engine/src/keymap.rs +++ b/crates/engine/src/keymap.rs @@ -162,6 +162,7 @@ pub static KEYS: &[Binding] = &[ k(Essential, "app.actions", &["Cmd+K", "Cmd+/"]).label("Actions…"), k(Essential, "help.shortcuts", &["Ctrl+Shift+?"]).both(), k(Essential, "view.toggleUi", &["Cmd+\\"]), + k(Essential, "view.fullscreen", &["Ctrl+Cmd+F"]).win(&["F11"]), k(Essential, "edit.pickColor", &["I", "Ctrl+C"]).win(&["I"]), k(Essential, "app.preferences", &["Cmd+,"]), k(Essential, "file.new", &["Cmd+N"]), diff --git a/crates/engine/src/shortcut_parity_tests.rs b/crates/engine/src/shortcut_parity_tests.rs index 0435446f..cf7d5d47 100644 --- a/crates/engine/src/shortcut_parity_tests.rs +++ b/crates/engine/src/shortcut_parity_tests.rs @@ -91,6 +91,7 @@ static ROWS: &[Row] = &[ doc(Essential, "Save to version history", &["Alt+Cmd+S"], &["Ctrl+Alt+S"], Bound("file.saveVersion"), ""), doc(Essential, "Run the last plugin", &["Alt+Cmd+P"], &["Ctrl+Alt+P"], Moved("plugins.runLast", &["Alt+Cmd+P"], &["Ctrl+Alt+Shift+P"]), "conflict: the documentation also gives Ctrl+Alt+P to pixel preview on Windows; pixel preview keeps it, the plugin takes Ctrl+Alt+Shift+P"), + pin(Essential, "view.fullscreen", &["Ctrl+Cmd+F"], &["F11"]), pin(Essential, "app.preferences", &["Cmd+,"], &[]), pin(Essential, "file.new", &["Cmd+N"], &[]).reserved(BROWSER_TAB), pin(Essential, "file.open", &["Cmd+O"], &[]), @@ -397,7 +398,7 @@ fn counts_match_the_docs() { let reserved = documented.iter().filter(|r| !r.reserved.is_empty()).count(); let pinned = ROWS.len() - documented.len(); // docs/ui-parity.md quotes these. - assert_eq!((documented.len(), bound, moved, missing, reserved, pinned), (129, 128, 1, 0, 2, 48)); + assert_eq!((documented.len(), bound, moved, missing, reserved, pinned), (129, 128, 1, 0, 2, 49)); } #[test] diff --git a/crates/ui-egui/locales/de.toml b/crates/ui-egui/locales/de.toml index 3a10dac8..9ee8d8e3 100644 --- a/crates/ui-egui/locales/de.toml +++ b/crates/ui-egui/locales/de.toml @@ -2034,6 +2034,7 @@ status = "draft — needs native review" "view.prevPage" = "Vorherige Seite" "view.previousRegion" = "Vorherigen Bereich fokussieren" "view.proofSrgb" = "Softproof sRGB" +"view.fullscreen" = "Vollbild" "view.prototype" = "Bereich Prototyp" "view.rulers" = "Lineale" "view.set" = "Ansicht festlegen" diff --git a/crates/ui-egui/locales/es.toml b/crates/ui-egui/locales/es.toml index 99b91497..d919c5f8 100644 --- a/crates/ui-egui/locales/es.toml +++ b/crates/ui-egui/locales/es.toml @@ -2087,6 +2087,7 @@ status = "draft — needs native review" "view.prevPage" = "Página anterior" "view.previousRegion" = "Enfocar la región anterior" "view.proofSrgb" = "Prueba en pantalla sRGB" +"view.fullscreen" = "Pantalla completa" "view.prototype" = "Panel de prototipo" "view.rulers" = "Reglas" "view.set" = "Establecer vista" diff --git a/crates/ui-egui/locales/fr.toml b/crates/ui-egui/locales/fr.toml index 06b4c104..9bb55aac 100644 --- a/crates/ui-egui/locales/fr.toml +++ b/crates/ui-egui/locales/fr.toml @@ -2034,6 +2034,7 @@ status = "draft — needs native review" "view.prevPage" = "Page précédente" "view.previousRegion" = "Activer la région précédente" "view.proofSrgb" = "Épreuve écran sRGB" +"view.fullscreen" = "Plein écran" "view.prototype" = "Panneau Prototype" "view.rulers" = "Règles" "view.set" = "Définir l’affichage" diff --git a/crates/ui-egui/locales/ja.toml b/crates/ui-egui/locales/ja.toml index 65c2994c..e2827b01 100644 --- a/crates/ui-egui/locales/ja.toml +++ b/crates/ui-egui/locales/ja.toml @@ -2036,6 +2036,7 @@ status = "draft — needs native review" "view.prevPage" = "前のページ" "view.previousRegion" = "前の領域にフォーカス" "view.proofSrgb" = "sRGB でソフトプルーフ" +"view.fullscreen" = "全画面表示" "view.prototype" = "プロトタイプパネル" "view.rulers" = "定規" "view.set" = "ビューを設定" diff --git a/crates/ui-egui/locales/ko.toml b/crates/ui-egui/locales/ko.toml index b1c401fb..1df265eb 100644 --- a/crates/ui-egui/locales/ko.toml +++ b/crates/ui-egui/locales/ko.toml @@ -2035,6 +2035,7 @@ status = "draft — needs native review" "view.prevPage" = "이전 페이지" "view.previousRegion" = "이전 영역에 포커스" "view.proofSrgb" = "sRGB 교정 미리 보기" +"view.fullscreen" = "전체 화면" "view.prototype" = "프로토타입 패널" "view.rulers" = "눈금자" "view.set" = "보기 설정" diff --git a/crates/ui-egui/locales/pt-BR.toml b/crates/ui-egui/locales/pt-BR.toml index 3f8d5c38..ecbbce28 100644 --- a/crates/ui-egui/locales/pt-BR.toml +++ b/crates/ui-egui/locales/pt-BR.toml @@ -2033,6 +2033,7 @@ status = "draft — needs native review" "view.prevPage" = "Página anterior" "view.previousRegion" = "Focar a região anterior" "view.proofSrgb" = "Prova em tela sRGB" +"view.fullscreen" = "Tela cheia" "view.prototype" = "Painel de protótipo" "view.rulers" = "Réguas" "view.set" = "Definir visualização" diff --git a/crates/ui-egui/locales/zh-Hans.toml b/crates/ui-egui/locales/zh-Hans.toml index 1c0d5596..8ad74b15 100644 --- a/crates/ui-egui/locales/zh-Hans.toml +++ b/crates/ui-egui/locales/zh-Hans.toml @@ -2036,6 +2036,7 @@ status = "draft — needs native review" "view.prevPage" = "上一页" "view.previousRegion" = "聚焦上一个区域" "view.proofSrgb" = "sRGB 软打样" +"view.fullscreen" = "全屏" "view.prototype" = "原型面板" "view.rulers" = "标尺" "view.set" = "设置视图" diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 9600dcd3..3b275055 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -735,7 +735,7 @@ impl FigApp { "dialog.unsaved" => self.toast(i18n::tr("This file has unsaved changes")), "dialog.plugins" => self.ui.plugins_window = true, "dialog.cursorChat" => live::open_chat(self), - "window.new" | "window.focus" => self.window_requests.push(r), + "window.new" | "window.focus" | "window.fullscreen" => self.window_requests.push(r), _ => {} } } diff --git a/crates/ui-egui/src/shortcut_keys_tests.rs b/crates/ui-egui/src/shortcut_keys_tests.rs index 03e5687c..4d1e5e85 100644 --- a/crates/ui-egui/src/shortcut_keys_tests.rs +++ b/crates/ui-egui/src/shortcut_keys_tests.rs @@ -189,3 +189,25 @@ fn enter_with_a_shape_tool_places_one_in_the_middle_of_the_view() { press(&mut u, Key::Enter, Modifiers::NONE); assert!(u.app.session.ds().unwrap().vector_edit.is_some()); } + +#[test] +fn full_screen_key_and_command_toggle_the_window() { + let mut u = Ui::new(Session::new()); + let fullscreen = |out: &egui::FullOutput| -> Vec { + out.viewport_output.get(&ViewportId::ROOT).map(|v| v.commands.clone()).unwrap_or_default() + }; + // F11 (Windows and Linux) or Ctrl+Cmd+F (macOS) asks the window to fill the screen. + let out = if crate::shortcuts::platform() == Platform::Mac { + press(&mut u, Key::F, Modifiers::MAC_CMD | Modifiers::COMMAND | Modifiers::CTRL) + } else { + press(&mut u, Key::F11, Modifiers::NONE) + }; + assert!(fullscreen(&out).contains(&ViewportCommand::Fullscreen(true)), "full screen key, {:?}", fullscreen(&out)); + // `on: false` leaves it, whatever the window reports. + u.app.run("view.fullscreen", json!({"on": false})); + let out = u.frame_output(vec![]); + assert!(fullscreen(&out).contains(&ViewportCommand::Fullscreen(false)), "{:?}", fullscreen(&out)); + // Junk parameters are ignored, not fatal. + u.app.run("view.fullscreen", json!({"on": "yes"})); + u.frame(vec![]); +} diff --git a/crates/ui-egui/src/windows.rs b/crates/ui-egui/src/windows.rs index 2c233fc2..82da7555 100644 --- a/crates/ui-egui/src/windows.rs +++ b/crates/ui-egui/src/windows.rs @@ -242,6 +242,12 @@ pub fn focus(app: &mut FigApp, ctx: &Context, id: u64) -> Result pub(crate) fn requests(app: &mut FigApp, ctx: &Context) { for r in std::mem::take(&mut app.window_requests) { let n = |k: &str| r.data.get(k).and_then(Value::as_u64); + if r.kind == "window.fullscreen" { + let full = ctx.input(|i| i.viewport().fullscreen.unwrap_or(false)); + let on = r.data.get("on").and_then(Value::as_bool).unwrap_or(!full); + ctx.send_viewport_cmd(ViewportCommand::Fullscreen(on)); + continue; + } let res = if r.kind == "window.new" { open(app, ctx, n("index").map(|i| i as usize), r.data.get("path").and_then(Value::as_str), None).map(|_| ()) } else { diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 184750b0..3b34e6fe 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -124,7 +124,7 @@ in the table. `crates/ui-egui/src/shortcut_keys_tests.rs` drives the keys a look | Bound on other keys (documented conflict) | 1 | Run last plugin: the documentation also gives Ctrl+Alt+P to pixel preview on Windows; the plugin takes Ctrl+Alt+Shift+P there | | Missing | 0 | moving the rotation origin (Alt+R, `transform.rotationOrigin`) and placing an object with Enter after choosing its tool (`tool.place`) were the last two | | Documented keys a platform keeps | 2 | Ctrl+Space (Windows/Linux keyboard box selection) switches some CJK input methods; F6 goes to a browser's address bar in the web build | -| Undocumented bindings pinned | 48 | of which Cmd/Ctrl+W, Cmd/Ctrl+N and Ctrl+Tab are taken by browsers (web build) and Ctrl+Alt+T / Ctrl+Alt+L by some Linux desktops | +| Undocumented bindings pinned | 49 | of which Cmd/Ctrl+W, Cmd/Ctrl+N and Ctrl+Tab are taken by browsers (web build) and Ctrl+Alt+T / Ctrl+Alt+L by some Linux desktops | - **Newly bound:** A for the Frame tool, Shift+' for the pixel grid (Cmd+' kept: the articles disagree), Alt+Cmd+\ / Ctrl+Alt+\ for others' cursors (`view.multiplayerCursors`, new), Ctrl+F6 @@ -165,6 +165,7 @@ in the table. `crates/ui-egui/src/shortcut_keys_tests.rs` drives the keys a look | Date | Change | Summary | |---|---|---| | 2026-10-13 | minor | Home screen: a Templates tab (eight starter templates as cards with thumbnails, search and a kind filter; click or Enter opens a new document) replaces the Community placeholder, the Drafts placeholder is removed, Recents offers four templates while there are no recent files and shows a Getting started card (closable; Help ▸ Getting started brings it back), File ▸ New from template…, and a first run opens on the home screen | +| 2026-10-13 | minor | Full screen (#227): View ▸ Full screen (`view.fullscreen {on?}`, F11 on Windows and Linux, Ctrl+Cmd+F on macOS) makes the window fill the screen, taskbar and title bar included, and leaves it again; FigCraft's own item (not in the incumbent's menu). Percentages and hours unchanged | | 2026-10-12 | minor | Everyday editing partials: number fields (scrub a step a point, ×10 with Shift, ×0.1 with Alt, on past the screen edge where the platform reports it; arrow keys by the nudge preferences, opacity and rotation by 1 and 10; brackets and units in arithmetic; `50%` relative outside percentage fields; Tab commits and moves on; an untouched field commits nothing; relative edits and arrow keys on Mixed go to each layer in one undo step) done; outline mode's Include hidden layers and Include object bounds, drawn and clickable, done; Hide when publishing from the Assets panel done (the name's leading `.`); cursors per tool and modifier done (a table in the engine, drawn rotate, pen, eyedropper and comment cursors, not-allowed on refused edits; guides and handles keep their cursors); resizable panels done (limits, double-click reset, kept per window and in the preferences, the canvas keeps its room in a small window); a file or page made in the dark theme has a #1E1E1E page (the light theme and sessions without an interface: #F5F5F5) and text drawn on a dark page is white; Enter on a text selects all of it (test added). Menu items: View ▸ Outlines ▸ Include hidden layers / Include object bounds, View ▸ Panels ▸ Reset panel widths, Libraries ▸ Hide when publishing | | 2026-10-12 | minor | Drag threshold and double-clicks: the second press of a double-click began a move or resize that was never finished, so double-clicking text inside a group often didn't start editing (from the middle of the text) or dragged and resized it when the hand moved a pixel or two; a move or resize now starts only after 3 px of travel on screen and a double-click drops the press's gesture first | | 2026-10-12 | minor | Rotation origin and placing with Enter: Alt+R shows a rotation origin handle to drag (snapping to the box's corners, edge middles and centre; double-click resets; Escape hides; kept while the selection lasts, not saved), which the corner handles, the rotation field, Rotate 90°/180° and `transform.rotate` turn about (`transform.rotationOrigin`, `transform.setRotationOrigin`); Enter with a shape or frame tool chosen, on the canvas or the toolbar's tool, places a click-sized one in the middle of the view (`tool.place`). Documented shortcuts: 128 bound, 0 missing. The keyboard box selection's toast and screen-reader text name Cmd/Ctrl+arrows too; the shortcuts panel cuts a label that doesn't fit before its keycaps with "…" (measured in the font it's drawn in; long labels ran into the keycaps) and shows it in full on hover |