Repository navigation
window.fullscreen: an app's own window in presentation full screen, from a tap - #520
Conversation
…rom a tap
A script app asks the shell to put its own window in presentation full
screen from the person's tap or click:
host.request("window.fullscreen", {on: true}, fn(r) { … })
The shell's `window` host service (crates/shell/src/app_fullscreen.rs)
finds the window whose Card runner runs the isolate that asked, for that
app, and refuses anything else (`not_a_window:`). The window must be in
front (`foreground_required:`), and the person must have pressed in it in
the last five seconds (`gesture_required:`): makepad cannot tell which
handler made a host request, so the shell keeps a browser-like transient
activation from the presses it routes into the window.
It builds on the window full screen of #516 (`WmLayout::set_fullscreen`
names the window): the window covers the desk, and while the app presents
(`WmState::presentation`) no edge brings the bar, the title bar or the dock
out. The shell's own window enters the host's full screen unless it is
already there, and leaves it only if the presentation entered it
(`HostFullscreen`; makepad's macOS backend ignores `Window::fullscreen`, so
this goes through maximize and restore, which toggle native full screen
there). A three-second hint says "Press Esc to exit full screen". Esc,
`{on: false}`, ⌘F, the window closing or losing the focus, a style switch
or the person leaving the host's full screen ends it, and the app's
optional `on_app_fullscreen(on)` hook hears both ends. Phones answer
without changing anything. Hidden runs never ask the host window.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- No bounce after Esc: a presentation that ends any other way than the
app's own {on: false} drops the window's press (Presenter::finish), so
an app that asks again from on_app_fullscreen(false) needs a new press.
- The request's authority, not a capability: admission no longer refuses
a request without may_prompt as foreground_required. The shell's turn
refuses a window not in front as foreground_required (focus, workspace,
minimized) and a request without its surface's authority, an agent's
tool call or a callback of one, as agent_call; {on: false} needs
neither. App Hub gives every app's window that authority whatever it
declares, and a test holds it to that.
- Only presses on the app's own content count: MpRunViewAction::Pressed,
which MpModuleView sends for a press on the live root with no host
sheet up. Group tabs, a stopped app's face and process tiles send only
Clicked, which focuses.
- A failed app's presentation ends (Ending::Failed), and none carries to
another instance in the window.
- A late host entry is taken back out: once a presentation ends while the
host window is still entering, the window's next report decides, with
no fixed grace.
- Requests that changed nothing log one line per app every 10 s, and the
shell's own window's geometry changes are told from other windows'.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the fixed grace gone, a leave that waited for an enter could wait forever: a presentation that began while the window was still leaving the last full screen had its enter ignored by macOS, the window's report of that leave came during the presentation, and once it ended the leave stayed pending until the person next entered full screen themselves, which the shell then took out once. A pending leave now expires after PENDING_LEAVE_SECONDS (20 s): what the window reports after that is the person's, and a new presentation asks to enter again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
macOS checks on this head (18c1164)
The gate comes in queue order after #519 and #513, with main merged in first. |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Local CI passed on 3023c13 ( GitHub would run for this PR: desktop.yml, phone.yml, apps.yml. Merged on this local pass (macOS runner queue); GitHub CI runs on the merge commit on main.
Skipped (these did NOT pass; (!) marks a skip that blocks tools/ci-local-merge.sh):
Notes:
|
Apps can now ask for presentation full screen from the person's tap. This is for Quick Deck's Present and PDF Tools' Read mode, which until now could only fill their own window. It is built on #516's window full screen and adds what #516 doesn't have: an app-facing request, the host window's own full screen, Esc and a hint, and no edge reveal while an app presents.
Before: ⌘F and
SetFullscreenin the desktop stylesBefore #516, read from the code at c5e2eb3:
WmAction::Fullscreen(FullscreenMode::Fullscreen)) only maximized the focused window, the same as ⌘⌥F.WmLayout::toggle_fullscreen_modetoggledDesktopWindow::maximized, so the window kept its title bar and stopped above the dock (Maximize and snaps end above a floating dock; honour a native window's requested size #487), and the bar and dock stayed.WmLayout::rects_ofnever read the workspace'sfullscreenon a floating desk, andsync_bar_for_fullscreenread only the tiled state.WmRequest::SetFullscreen { fullscreen }focused the client, then comparedfullscreenwith the active workspace's tiledfullscreen_mode(), which is alwaysNoneon a floating desk. So eachtruetoggled the maximize (asking twice maximized, then restored), andfalsenever applied.#516 replaced both on a floating desk: a real full screen with edge reveal, and
SetFullscreenchecked against the window's own state. This PR doesn't touch either.What this adds
window.fullscreen {on}is a host service in the shell (crates/shell/src/app_fullscreen.rs) for in-process card apps:The app's own window only.
App::window_offinds the window whose Card runner runs the requesting isolate (Replier::isolate_key), for the requesting app. Anything else is refusednot_a_window:: a glance card, a host sheet, another app's window.From a tap, by the app itself.
foreground_required:.ServiceCall::may_promptfalse: an agent's tool call, or a callback of one) is refusedagent_call:. No capability decides this; see "Thepromptcapability" below.gesture_required:). A press on a group tab, a stopped app's face, a host sheet over the app or a process tile doesn't count. A request that applies uses the press up, and so does any ending except the app's own{on: false}.{on: false}needs none of these: an app can always give the screen back.On:
WmLayout::set_fullscreenputs that window in full screen. It is a new per-window setter: A window's full screen, as macOS's green button makes it #516's SUPER+F only toggles the focused window.WmState::presentationkeeps every edge from bringing out the bar, the title bar or the dock, as in a slideshow's play mode. That covers A window's full screen, as macOS's green button makes it #516's top and bottom reveals (App::desktop_pointer) and every style's dock or taskbar (desktop::dock_shows). The flag lives and ends with the presentation, and ⌘F full screen keeps A window's full screen, as macOS's green button makes it #516's edges.true.Off. These end it:
{on: false}.Everything goes back as it was. A window that was already full screen (⌘F) stays full screen. The host window leaves full screen only if this presentation entered it (
HostFullscreen). If the host window is still entering when the presentation ends, its next report decides, up to 20 s later (PENDING_LEAVE_SECONDS): in, and it leaves; not in (a failed enter), and there is nothing to give back. After 20 s, whatever the window reports is the person's.Host full screen goes through maximize and restore. makepad's macOS backend ignores
FullscreenWindowandNormalizeWindow, which is whatWindow::fullscreenandnormalsend.maximizeandrestoreboth sendtoggleFullScreen:there, so the shell asks the window to leave only while it reports full screen. A TODO marks where to switch once makepad makesfullscreenandnormalreal on macOS; that makepad fix is a follow-up. Elsewhere it is makepad's maximize, which is unverified.Hidden runs. With
MAKEPAD_HIDE_WINDOWSset (launch_input::hidden_windows), the shell never asks the host window to go full screen: on macOS that could bring the hidden window onto the screen in a Space of its own.Phones (mobile styles) answer
{applied: false, fullscreen: false, fills_screen: true}and change nothing. Every app already fills the screen, and the shell has no immersive mode.The gesture finding
makepad's runtime cannot tell whether a
host.requestcame from a person's tap:SplashHostRequestcarries onlymay_prompt, which says the surface is in the foreground and rules out agent tool calls, home tiles and their callbacks. A foreground app's own timer keepsmay_prompt.trusted_user_input) covers only macOS HID pointers and Android touches. It holds during the native event's synchronous handler, but a Splashon_clickruns later, from the widget-to-script queue (handle_script_tasks), outside it.So, as agreed, the shell keeps a browser-like transient activation: a press on the app's own content in that window (
MpRunViewAction::Pressed) within the last 5 s, plus focus and front, with Esc and the hint as the rest of the protection. Any press counts, remote and synthetic ones included, because a script app cannot create input. A makepad patch could make it exact by carryingtrusted_user_input()from the native event intoWidgetToScriptCallRequestand thenSplashHostRequest. It would still be false on Windows and Linux.The
promptcapabilityThe review asked whether the API quietly needs the app's
promptcapability. At App Hub 38635915 it doesn't, and now the shell no longer leans on a value that could:ServiceCall::may_promptis the surface's authority, not the declaration.IsolateSettings::for_appgives every admitted app's windowhost_prompts: true, and the Card runner (cardapp.rs) lowers it only while the app is suspended (phones only) or one of its tool calls is pending:admitted_prompts && !app_suspended && !script_tools::pending_for(..). A callback keeps the authority of the request it answers. The declaration isAppPolicy::may_prompt, and nothing on a request's path reads it.storage, and its requests applied. PDF Tools and Quick Deck don't declareprompteither.may_prompt. The shell's turn says which reason applies:foreground_required:for a window not in front,agent_call:for a request without its surface's authority, and neither for{on: false}.no_capability_decides_a_requests_authorityresolves manifests with no capabilities, PDF Tools' and Quick Deck's, andprompt, and checks that each window gets the authority. An App Hub pin that tied it to a capability fails there first.Not listed by
runtime.list(App Hub follow-up)api_methods()is empty: App Hub'sHostApiMethod::validaterequires a capability fromKNOWN_CAPABILITIES, and none fits a window method. Soruntime.listdoesn't showwindow.fullscreen, and an app can't put it inhost_api, but calling it needs no declaration. Making it discoverable needs awindowcapability in App Hub's contract, then a repin.Review follow-ups (95214d9 and 18c1164)
The independent review of f1c98d5 found no critical or major issues. Its minor points and nits, fixed as a new commit:
{on: false}now drops the window's press (Presenter::finish). So a click that turned a slide, then Esc, then the app asking again fromon_app_fullscreen(false)is refusedgesture_required:. The app's own toggle keeps the press it was given, as a browser's exit does.promptcapability (minor). See "Thepromptcapability" above: the new refusal isagent_call:, andforeground_required:now says what "in front" means.MpModuleViewsends the newMpRunViewAction::Pressedfor a press on the module's live root with no host sheet up. It asks before the app sees the press, which may close the sheet. Group tabs, a stopped app's face and process tiles send onlyClicked, which focuses.Ending::Failedwhen the module fails. It records its instance (SplashVmId), so another instance in the window ends it too. A failed instance's window is no request's window.PENDING_LEAVE_SECONDS), and a new presentation after that asks to enter again (a_leave_waiting_for_an_enter_that_never_came_expires).QuietLog).host_window_reportedignores geometry changes of windows other than the shell's own.Also,
begin_presentationnow ends another window's presentation that this event hasn't ended yet before it starts one.Tests
From
phone/,cargo test --locked --features mobile-apps -p octosense-shell, withOCTOSENSE_HOME,RINX_DATA_DIRandMAKEPAD_HIDE_WINDOWS=1:MAKEPAD_SPLASH_BUDGET_MS=10000asphone.ymlsets it.python3 tools/setup.py --check --cargopassed.module_resize_testswithscript time budget exceeded, the known wall-clock flake; both pass when run alone.cargo check --locked -p octosense, Linux build host, 95214d9: passed. This Mac built-p octosense --no-default-features --features app-hub,craft-enginesfrom 95214d9 plus the stacked PDF Tools commit, with no warnings from the shell.New tests:
app_fullscreen(19):a_press_made_while_presenting_does_not_bring_it_back: the presses through a presentation as the shell wires them (Presenter). A press made while presenting, then each ending, then the app asking again is refusedgesture_required; the app's own toggle keeps its press;no_capability_decides_a_requests_authority(see "Thepromptcapability");module_input_tests: every press assertion now also checks whose press counts (Asked::Press). A click or touch on the window in front never counts for the app behind it, and a stopped app's face counts for nothing. The newa_press_on_a_host_sheet_over_a_card_app_is_not_the_appsuses App Hub's real Card runner: a press on its host sheet raises the window and counts for nothing, and once the sheet is gone presses count again.layout:set_fullscreen_sets_a_named_window_and_a_closed_window_ends_it,clear_fullscreen_clears_both_desks.desktop:an_apps_presentation_keeps_the_dock_away.shell::osd:the_full_screen_hint_names_esc_and_goes_away.Hidden-shell check on this Mac (13 Oct 2026, first commit)
These runs are on f1c98d5. The review follow-ups are covered by the tests above and haven't been re-run in a hidden shell yet.
What was run. Hidden desktop shells (
MAKEPAD_HIDE_WINDOWS=1), started from a Terminal tab under the shared lock. Each used a scratchOCTOSENSE_HOME,OCTOS_APP_CORE_DIR,OCTOSENSE_APP_DATA,RINX_DATA_DIRandOCTOSENSE_LLM_VAULT=file, in the OctoSense style at 1400×899. They were driven over the remote and ended with/quit.OCTOSENSE_SYSTEM_APPS, never committed. It has Present, Leave and "Ask in 7 s" buttons, and asks once at startup with no tap./snap. The checks on bar, title bar and dock read pixels of/ggrabs, which come from the app's drawable; no OS screenshots were taken.Before, c5e2eb3 (6/6 checks):
[54,76,990,603].[12,76,1376,733]above the dock, keeping its title bar, the bar[0,0,1400,32]and the dock. ⌘F again restored[54,76,990,603].no service answers "window" on this device.This PR at f1c98d5, on main with #516 (25/25 checks):
gesture_required.[0,0,1400,899]with no bar, title bar or dock. The top edge brought the bar back, as the control for A window's full screen, as macOS's green button makes it #516's edges. ⌘F again restored[54,76,990,603]and the bar.{applied: true, fullscreen: true, fills_screen: true}, and the hook heardon_app_fullscreen(true).[0,0,1400,899]with no bar, title bar or dock, and "Press Esc to exit full screen" showed in full.[54,76,990,603], the bar came back, and the hook heardfalse.{on: false}) answered{applied: true, fullscreen: false}and restored the window.gesture_required.Uncovered).Closed), and the bar came back.What a hidden run cannot show:
Unverified
What App Flow's
docs/SCRIPT-API.mdneeds (separate repo, not edited here)window.fullscreen {on}:{applied, fullscreen, fills_screen};gesture_required:,foreground_required:,agent_call:,not_a_window:orinvalid_arguments:;{on: false}) asking again needs a new press;card-hostanswersno service answers "window" on this device;runtime.describereports it as not implemented until App Hub knows awindowcapability.on_app_fullscreen(on)besideon_app_resize(width, height), which SCRIPT-API doesn't list either.window(the desktop shell; Home answers without changing anything).🤖 Generated with Claude Code