diff --git a/Cargo.lock b/Cargo.lock index 34db3590..96ba9993 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11394,8 +11394,8 @@ dependencies = [ [[package]] name = "octosense-app-contract" -version = "1.11.0" -source = "git+https://github.com/OctoSense-org/OctoSense-App-Hub?rev=80a64b4557aaba71f7c826c5dc69721c21c0f0fc#80a64b4557aaba71f7c826c5dc69721c21c0f0fc" +version = "1.12.0" +source = "git+https://github.com/OctoSense-org/OctoSense-App-Hub?rev=d86101d7206162b73ea873e2a31b48364600a110#d86101d7206162b73ea873e2a31b48364600a110" dependencies = [ "blake3", "serde", @@ -11406,7 +11406,7 @@ dependencies = [ [[package]] name = "octosense-app-hub" version = "0.1.0" -source = "git+https://github.com/OctoSense-org/OctoSense-App-Hub?rev=80a64b4557aaba71f7c826c5dc69721c21c0f0fc#80a64b4557aaba71f7c826c5dc69721c21c0f0fc" +source = "git+https://github.com/OctoSense-org/OctoSense-App-Hub?rev=d86101d7206162b73ea873e2a31b48364600a110#d86101d7206162b73ea873e2a31b48364600a110" dependencies = [ "base64 0.22.1", "blake3", @@ -11433,7 +11433,7 @@ dependencies = [ [[package]] name = "octosense-app-hub-app" version = "0.1.0" -source = "git+https://github.com/OctoSense-org/OctoSense-App-Hub?rev=80a64b4557aaba71f7c826c5dc69721c21c0f0fc#80a64b4557aaba71f7c826c5dc69721c21c0f0fc" +source = "git+https://github.com/OctoSense-org/OctoSense-App-Hub?rev=d86101d7206162b73ea873e2a31b48364600a110#d86101d7206162b73ea873e2a31b48364600a110" dependencies = [ "makepad-app-module", "makepad-widgets", @@ -11461,7 +11461,7 @@ dependencies = [ [[package]] name = "octosense-app-policy" version = "0.1.0" -source = "git+https://github.com/OctoSense-org/OctoSense-App-Hub?rev=80a64b4557aaba71f7c826c5dc69721c21c0f0fc#80a64b4557aaba71f7c826c5dc69721c21c0f0fc" +source = "git+https://github.com/OctoSense-org/OctoSense-App-Hub?rev=d86101d7206162b73ea873e2a31b48364600a110#d86101d7206162b73ea873e2a31b48364600a110" dependencies = [ "makepad-widgets", "octosense-app-contract", @@ -11483,7 +11483,7 @@ dependencies = [ [[package]] name = "octosense-appstore" version = "0.1.0" -source = "git+https://github.com/OctoSense-org/OctoSense-App-Hub?rev=80a64b4557aaba71f7c826c5dc69721c21c0f0fc#80a64b4557aaba71f7c826c5dc69721c21c0f0fc" +source = "git+https://github.com/OctoSense-org/OctoSense-App-Hub?rev=d86101d7206162b73ea873e2a31b48364600a110#d86101d7206162b73ea873e2a31b48364600a110" dependencies = [ "makepad-app-module", "makepad-widgets", diff --git a/Cargo.toml b/Cargo.toml index 75aa209e..0b8eaca4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,7 +91,7 @@ octosense-reference = { path = "apps/reference" } makepad-sheets = { git = "https://github.com/OctoSense-org/makepad.git", rev = "d653bc4493619ab979ccff165cf48c0c63b77d19", default-features = false } makepad-terminal = { git = "https://github.com/OctoSense-org/makepad.git", rev = "d653bc4493619ab979ccff165cf48c0c63b77d19" } octosense-appcard = { path = "apps/appcard/module" } -octosense-app-hub-app = { git = "https://github.com/OctoSense-org/OctoSense-App-Hub", rev = "80a64b4557aaba71f7c826c5dc69721c21c0f0fc" } +octosense-app-hub-app = { git = "https://github.com/OctoSense-org/OctoSense-App-Hub", rev = "d86101d7206162b73ea873e2a31b48364600a110" } makepad-calculator = { git = "https://github.com/OctoSense-org/makepad.git", rev = "d653bc4493619ab979ccff165cf48c0c63b77d19", default-features = false } makepad-clock = { git = "https://github.com/OctoSense-org/makepad.git", rev = "d653bc4493619ab979ccff165cf48c0c63b77d19", default-features = false } makepad-notes = { git = "https://github.com/OctoSense-org/makepad.git", rev = "d653bc4493619ab979ccff165cf48c0c63b77d19", default-features = false } @@ -131,17 +131,17 @@ makepad-d3 = { git = "https://github.com/OctoSense-org/Octoscript-Makepad.git", octoscript-node = { git = "https://github.com/OctoSense-org/Octoscript-Makepad.git", rev = "6cf2d90aa33ff4ae603f49c7557f3ecf986207f0" } # --- App Hub's host service registry the Mail and llm services register # into; the same revision as App Hub's app crate (a native app, above). -octosense-appstore = { git = "https://github.com/OctoSense-org/OctoSense-App-Hub", rev = "80a64b4557aaba71f7c826c5dc69721c21c0f0fc" } +octosense-appstore = { git = "https://github.com/OctoSense-org/OctoSense-App-Hub", rev = "d86101d7206162b73ea873e2a31b48364600a110" } # The app policy (resolution, isolate settings, the Splash adapter) and the # Hub client the Card runner resolves an app's policy with; the shell's # glance tiles run cards under it (crates/shell/src/glance_card.rs). -octosense-app-policy = { git = "https://github.com/OctoSense-org/OctoSense-App-Hub", rev = "80a64b4557aaba71f7c826c5dc69721c21c0f0fc" } -octosense-app-hub = { git = "https://github.com/OctoSense-org/OctoSense-App-Hub", rev = "80a64b4557aaba71f7c826c5dc69721c21c0f0fc" } +octosense-app-policy = { git = "https://github.com/OctoSense-org/OctoSense-App-Hub", rev = "d86101d7206162b73ea873e2a31b48364600a110" } +octosense-app-hub = { git = "https://github.com/OctoSense-org/OctoSense-App-Hub", rev = "d86101d7206162b73ea873e2a31b48364600a110" } # The app contract (ADR 0005), from crates.io by version: App Hub, the shell # and Rinx all depend on `octosense-app-contract = "1"`, so Cargo links ONE # 1.x for the whole build (Cargo.lock pins the exact version OctoSense ships) # and App Hub and Rinx no longer need the same App Hub commit. -octosense-app-contract = "1.11" +octosense-app-contract = "1.12" # --- octos: ONE source for every octos crate (reviewed octos#2715 at # b0759a57: bounded ledger session-directory names, serialized peer purge # against session writes, and provider transport diagnostics that omit @@ -439,7 +439,7 @@ ruma = { git = "https://github.com/project-robius/ruma.git", branch = "tsp" } # -i octos-core` still sees one octos source. [patch.crates-io] # Keep the contract identical to the pinned SDK for this release candidate. -octosense-app-contract = { git = "https://github.com/OctoSense-org/OctoSense-App-Hub", rev = "80a64b4557aaba71f7c826c5dc69721c21c0f0fc" } +octosense-app-contract = { git = "https://github.com/OctoSense-org/OctoSense-App-Hub", rev = "d86101d7206162b73ea873e2a31b48364600a110" } nix = { git = "https://github.com/octos-org/octos.git", rev = "18fcd3f16e527d2b601d7ae244f056fb711bb6b8" } # pdfcraft's bootstrap PDF stack: its workspace patches these five diff --git a/apps/README.md b/apps/README.md index 75335cc5..98cb27d3 100644 --- a/apps/README.md +++ b/apps/README.md @@ -346,7 +346,8 @@ appcard/ the native AppCard assistant ``` apps//bundle/ - manifest.json id, version, name, capabilities, network.hosts, integrity + manifest.json id, version, name, capabilities, network.hosts, integrity; + optionally window, the desktop window's size (schema_minor 1) main.splash the program icon.png|svg optional launcher artwork; the shell owns the outer shape thumbs/ ... any other files the app loads, as {{assets}}/ diff --git a/apps/README.zh-CN.md b/apps/README.zh-CN.md index a7a29e51..2417fc15 100644 --- a/apps/README.zh-CN.md +++ b/apps/README.zh-CN.md @@ -277,7 +277,8 @@ appcard/ 原生 AppCard 助手 ``` apps//bundle/ - manifest.json id、version、name、capabilities、network.hosts、integrity + manifest.json id、version、name、capabilities、network.hosts、integrity; + 可选的 window:桌面窗口的尺寸(schema_minor 1) main.splash 程序 icon.png|svg 可选的启动器图案;外形由 Shell 统一控制 thumbs/ ... 应用加载的其他文件,路径为 {{assets}}/ diff --git a/apps/pdftools/README.md b/apps/pdftools/README.md index d90f47fc..a1799af3 100644 --- a/apps/pdftools/README.md +++ b/apps/pdftools/README.md @@ -33,14 +33,25 @@ decision that applies. | Right panel | Comment: the threads. Fill & Sign: the form's fields and Add text, Add date, Add initials. Edit: the paragraph being edited | | Status line | Saved or Edited, and the storage the PDFs use ("14.6 of 64 MB used", "37 KB of 64 MB used") | -The designs are drawn at 1536 x 1024, but the desktop opens every app's -window at 72% of the desk's width and 76% of its height, at most 1000 x 720 -points (`crates/shell/src/desktop_layout.rs`); in the runs here PDF Tools got -990 x 603 points. An app has no way to ask for a larger window; the person -can resize or maximize it. Below 1180 points wide the mode tabs and Save trim -their padding, both right panels take 320 points, a right panel takes the -left panel's place, and until the person zooms, the page fits the canvas, up -to 100%. +The designs are drawn at 1536 x 1024, and the manifest asks the desktop to +open the window at that size: `"window": {"width": 1536, "height": 1024}`, +App Hub's optional window hint, which needs `"schema_minor": 1`. The desk +clamps it to its own size, less the margins every new window keeps (42 +points left and right, 32 top and bottom). Where the dock floats over the +desk, as OctoSense's does, the bottom margin is kept above the dock, so the +zoom bar and the status line are never behind it +(`crates/shell/src/desktop_layout.rs`). In the hidden shell's 1400 x 897 +window, PDF Tools opens at 1296 x 703 points with an app area of 1292 x 667, +its wide layout +([tests/evidence/window-20261010](tests/evidence/window-20261010/README.md)). +Before the hint it got the default window, 72% of the desk's width and 76% +of its height, at most 1000 x 720 points: an app area of 990 x 603. A +shell that predates the hint ignores it and opens the default. The person +can resize or maximize the window either way. At 1180 points wide and up +(the wide layout), a right panel opens beside the left panel. Below 1180 the +mode tabs and Save trim their padding, both right panels take 320 points, a +right panel takes the left panel's place, and until the person zooms, the +page fits the canvas, up to 100%. ## Modes @@ -190,19 +201,30 @@ that refusal. (and again with `--budget-ms 1000` while other builds loaded the machine); the `missing` and `fixture` runs in card-host, light and dark. `fixture` ran with `--budget-ms 1000`: its stand-in engine answers in script, and at - 64 ms one of its answers overran in the dark pass. + 64 ms one of its answers overran in the dark pass. Those runs had the + default window, an app area of 990 x 603 points. +- **Verified on 10 Oct 2026 at the window's own size** (macOS, Apple silicon; + [tests/evidence/window-20261010](tests/evidence/window-20261010/README.md)): + the `shell` run, light and dark, with the real engine at the runtime's 64 ms + budget, in the wide layout (an app area of 1292 x 662); and, opened from the + shell's menu, a 1296 x 703 window on a 1380 x 845 work area, 32 points + above the dock. - **Not verified**: the host's file dialog and a real import (a hidden window cannot open it); with the real engine, form fields, outlines, Redo, the other comment and page operations, password-protected PDFs, the eight-PDF cap and the unsaved-changes question (the dev fixture covers their screens); - window sizes other than the shell's default and 1536 x 1024; Linux, - Windows, phones. + the `restart`, `full` and `empty` runs at the window's own size; other + screen sizes; Linux, Windows, phones. - **Known**: a comment's date is the engine's UTC time read as local time, so a comment made in the evening west of UTC shows the next day's date, until the pdf service converts it. The status line counts only the PDFs in the library: the files service does not report the storage's use. On a heavily loaded machine a tap can overrun the 64 ms budget and be lost (seen at load - averages of 60 to 150 before `library.json` moved off the tap path). + averages of 60 to 150 before `library.json` moved off the tap path). On a + screen 900 points tall the app area is about 1292 x 662. There, with two + PDFs in Combine, the card's Combine button needs one scroll (85 points + short). Home's last caption line (when each PDF was last opened) also sits + at the fold. - **Not part of this version**: export (no approved design), opening password-protected PDFs, keyboard shortcuts (the runtime gives script apps no key events), the Edit panel's Alignment and Colour (`pdf.edit_text` takes diff --git a/apps/pdftools/README.zh-CN.md b/apps/pdftools/README.zh-CN.md index d56f6eac..29f82e1f 100644 --- a/apps/pdftools/README.zh-CN.md +++ b/apps/pdftools/README.zh-CN.md @@ -29,11 +29,18 @@ craft 引擎只在桌面版中提供(`craft-engines`)。 | 右侧面板 | Comment:评论线程。Fill & Sign:表单字段以及 Add text、Add date、Add initials。Edit:正在编辑的段落 | | 状态栏 | Saved 或 Edited,以及 PDF 占用的存储(“14.6 of 64 MB used”、“37 KB of 64 MB used”) | -设计图按 1536 x 1024 绘制,但桌面版打开每个应用的窗口时,宽度取桌面的 72%、高度取 76%, -最大 1000 x 720 点(`crates/shell/src/desktop_layout.rs`);在这里的运行中 PDF Tools 得到 -990 x 603 点。应用没有办法请求更大的窗口;用户可以调整窗口大小或将其最大化。窗口宽度小于 -1180 点时,模式标签和 Save 会收紧内边距,两种右侧面板都取 320 点宽,右侧面板会取代左侧面板 -的位置;在用户自己缩放之前,页面会适合画布宽度,最大 100%。 +设计图按 1536 x 1024 绘制,清单请求桌面按这个尺寸打开窗口:`"window": {"width": 1536, +"height": 1024}`,即 App Hub 的可选窗口提示,需要 `"schema_minor": 1`。桌面会把它限制在自身 +尺寸以内,并减去每个新窗口都保留的边距(左右各 42 点,上下各 32 点)。程序坞浮在桌面之上时 +(OctoSense 的程序坞就是这样),底部边距从程序坞上沿算起,所以缩放条和状态栏永远不会位于 +程序坞后面(`crates/shell/src/desktop_layout.rs`)。在隐藏 Shell 的 1400 x 897 窗口中, +PDF Tools 以 1296 x 703 点打开,应用区域为 1292 x 667,即它的宽布局 +([tests/evidence/window-20261010](tests/evidence/window-20261010/README.zh-CN.md))。有这个 +提示之前,它得到的是默认窗口:宽度取桌面的 72%、高度取 76%,最大 1000 x 720 点,应用区域为 +990 x 603。早于这个提示的 Shell 会忽略它,打开默认窗口。两种情况下用户都可以调整窗口大小或将其 +最大化。窗口宽度达到 1180 点及以上时(宽布局),右侧面板在左侧面板旁边打开。宽度小于 +1180 点时,模式标签和 Save 会收紧内边距,两种右侧面板都取 320 点宽,右侧面板会取代左侧面板的 +位置;在用户自己缩放之前,页面会适合画布宽度,最大 100%。 ## 模式 @@ -158,13 +165,20 @@ python3 apps/pdftools/dev-fixture/make_fixture.py /os.pdftoo `full` 和 `empty` 运行(在其他构建占用机器时又用 `--budget-ms 1000` 运行了一次);在 card-host 中进行的 `missing` 和 `fixture` 运行,浅色和深色。`fixture` 使用了 `--budget-ms 1000`:它的替身引擎在脚本中作答,按 64 ms 运行时,深色一轮中有一次作答超时。 + 这些运行使用的是默认窗口,应用区域为 990 x 603 点。 +- **已于 2026 年 10 月 10 日在窗口自身尺寸下验证**(macOS,Apple 芯片; + [tests/evidence/window-20261010](tests/evidence/window-20261010/README.zh-CN.md)):使用真实 + 引擎、按运行时 64 ms 的预算,以宽布局(应用区域 1292 x 662)进行的 `shell` 运行,浅色和深色; + 以及从 Shell 菜单打开时,在 1380 x 845 的工作区上打开的 1296 x 703 窗口,距程序坞 32 点。 - **未验证**:宿主的文件对话框和真实的导入(隐藏窗口无法打开它);使用真实引擎时的表单 字段、大纲、Redo、其他评论和页面操作、受密码保护的 PDF、八个 PDF 的上限以及未保存更改 - 的询问(开发夹具覆盖了它们的界面);Shell 默认窗口和 1536 x 1024 以外的窗口尺寸; - Linux、Windows、手机。 + 的询问(开发夹具覆盖了它们的界面);窗口自身尺寸下的 `restart`、`full` 和 `empty` 运行; + 其他屏幕尺寸;Linux、Windows、手机。 - **已知问题**:评论的日期是引擎的 UTC 时间被当作本地时间读取,因此在 UTC 以西的晚上做的 评论会显示第二天的日期,直到 pdf 服务完成转换。状态栏只统计资料库中的 PDF:文件服务不 报告存储的用量。在负载很高的机器上,一次点按可能超出 64 ms 预算而丢失(在 `library.json` - 移出点按路径之前,于 60 到 150 的平均负载下见到过)。 + 移出点按路径之前,于 60 到 150 的平均负载下见到过)。在 900 点高的屏幕上,应用区域约为 + 1292 x 662:合并中列出两个 PDF 时,卡片的 Combine 按钮需要滚动一次才能看到(差 85 点), + 首页卡片的最后一行说明(每个 PDF 上次打开的时间)正好在可见区域边缘。 - **不在这个版本中**:导出(没有已批准的设计)、打开受密码保护的 PDF、键盘快捷键(运行时 不给脚本应用按键事件)、Edit 面板中的 Alignment 和 Colour(`pdf.edit_text` 只接收文字)。 diff --git a/apps/pdftools/bundle/manifest.json b/apps/pdftools/bundle/manifest.json index a5807e48..347387b1 100644 --- a/apps/pdftools/bundle/manifest.json +++ b/apps/pdftools/bundle/manifest.json @@ -1,8 +1,10 @@ { "schema": 1, + "schema_minor": 1, "id": "os.pdftools", "version": "0.2.0", "name": "PDF Tools", "integrity": { "bundle_blake3": "" }, - "capabilities": ["storage", "files", "pdf"] + "capabilities": ["storage", "files", "pdf"], + "window": { "width": 1536, "height": 1024 } } diff --git a/apps/pdftools/tests/evidence/window-20261010/README.md b/apps/pdftools/tests/evidence/window-20261010/README.md new file mode 100644 index 00000000..facdda5e --- /dev/null +++ b/apps/pdftools/tests/evidence/window-20261010/README.md @@ -0,0 +1,49 @@ +# PDF Tools at its own window size — 2026-10-10 + +English | [简体中文](README.zh-CN.md) + +PDF Tools' manifest asks the desktop to open its window at the designs' +1536 x 1024 points (`"window"`, with `"schema_minor": 1`). The shell clamps +that to the desk, less the margins every new window keeps. OctoSense's dock +floats over the desk instead of reserving a strip, so the bottom margin is +kept above the dock. A hidden desktop shell built from this branch showed +it, with the real `pdf` engine (macOS, Apple silicon; +[receipt.json](receipt.json) holds the binary's digest, the commands, the +numbers and the findings). Each run had a fresh `OCTOSENSE_HOME` and started +from a Terminal.app tab with `MAKEPAD_HIDE_WINDOWS=1` and +`MAKEPAD_REMOTE=127.0.0.1:8915`, one instance on the machine. Each was +driven over the remote bridge and ended with `/quit`. Frames are `/g` grabs +at one pixel per point. + +- [opened-from-the-menu.png](opened-from-the-menu.png): the whole hidden + window, after the shell's menu, "pdf tools", Return. The 1400 x 897 shell + has a 1380 x 845 work area whose bottom 78 points the dock covers. The + window opens at 1296 x 703 points, 32 points above the dock, and the app + gets 1292 x 667. Before this change the app got the default window, an app + area of 990 x 603 ([v2-20261010](../v2-20261010/README.md)). +- [wide/](wide): [ui.py](../../ui.py)'s `shell` run, unchanged, light and + then dark, with PDF Tools opened at startup (`MAKEPAD_WM_TEST_APP`). The + window was 1296 x 698, sized on the shell's startup proportions as every + window opened before the desk's first draw is, and the app had 1292 x 662. + That is its wide layout (1180 points and up), where a right panel opens + beside the left panel. Every step of both passes passed at the runtime's 64 ms script + budget, with no script errors. The frames are the app's area: Home, + Reading, Find, Comment with a highlight, Combine (before and after one + scroll), Edit, and Reading in the dark pass. In every frame the status line + ("Saved" or "Edited", and the storage used) and the zoom bar sit above the + dock. + +What the run showed: + +- **Combine:** with two PDFs listed, the card's own Combine button is 85 + points below the fold at this size. [06-combine.png](wide/06-combine.png) + ends at "One PDF of 5 pages". One scroll shows the name and the button + ([06a-combine-end.png](wide/06a-combine-end.png)). +- **Home:** the cards' third caption line (when each PDF was last opened) + sits at the fold, with 2 of its 18 points showing. +- Both come from height. Keeping the window above the dock costs 78 points on + this 898-point-tall window. Before that change the app area was 740 points + tall: Home's captions fitted and the Combine button was about 7 points + short. +- A comment made on 10 Oct (PDT) is dated 11 Oct: the known UTC reading in + the [README](../../../README.md#status). diff --git a/apps/pdftools/tests/evidence/window-20261010/README.zh-CN.md b/apps/pdftools/tests/evidence/window-20261010/README.zh-CN.md new file mode 100644 index 00000000..477557b0 --- /dev/null +++ b/apps/pdftools/tests/evidence/window-20261010/README.zh-CN.md @@ -0,0 +1,34 @@ +# PDF Tools 按自身窗口尺寸运行 — 2026-10-10 + +[English](README.md) | 简体中文 + +PDF Tools 的清单请求桌面按设计图的 1536 x 1024 点打开它的窗口(`"window"`,并写明 +`"schema_minor": 1`)。Shell 会把这个尺寸限制在桌面以内,并减去每个新窗口都保留的边距。 +OctoSense 的程序坞浮在桌面之上、不预留区域,所以底部边距从程序坞上沿算起。用这个分支构建的 +隐藏桌面 Shell 配合真实的 `pdf` 引擎验证了这一点(macOS,Apple 芯片; +[receipt.json](receipt.json) 记录了二进制文件的摘要、命令、数值和发现的问题)。每次运行都使用 +全新的 `OCTOSENSE_HOME`,从 Terminal.app 标签页以 `MAKEPAD_HIDE_WINDOWS=1` 和 +`MAKEPAD_REMOTE=127.0.0.1:8915` 启动,机器上同时只运行一个实例。每次运行都通过远程桥驱动, +并以 `/quit` 结束。截图是每点一个像素的 `/g` 抓图。 + +- [opened-from-the-menu.png](opened-from-the-menu.png):打开 Shell 的菜单,输入“pdf tools”, + 按 Return 之后的整个隐藏窗口。1400 x 897 的 Shell 的工作区为 1380 x 845,程序坞覆盖其底部 + 78 点。窗口以 1296 x 703 点打开,距程序坞 32 点,应用得到 1292 x 667。此前应用得到的是 + 默认窗口,应用区域为 990 x 603([v2-20261010](../v2-20261010/README.zh-CN.md))。 +- [wide/](wide):未经修改的 [ui.py](../../ui.py) `shell` 运行,先浅色后深色,PDF Tools 在启动 + 时打开(`MAKEPAD_WM_TEST_APP`)。窗口为 1296 x 698:Shell 在桌面首次绘制之前打开的每个 + 窗口都按启动时的比例确定尺寸。应用得到 1292 x 662,即宽布局(1180 点及以上):右侧面板在 + 左侧面板旁边打开。两轮的每一步都在运行时 64 ms 的脚本预算下通过,没有脚本错误。截图是应用区域: + 首页、阅读、查找、带高亮的评论、合并(滚动前和滚动一次之后)、编辑,以及深色一轮中的阅读。 + 每张截图里的状态栏(“Saved”或“Edited”,以及存储用量)和缩放条都位于程序坞上方。 + +这次运行发现: + +- **合并:**列出两个 PDF 时,在这个尺寸下卡片自己的 Combine 按钮比可见区域低 85 点。 + [06-combine.png](wide/06-combine.png) 只显示到“One PDF of 5 pages”,滚动一次才能看到名称 + 和按钮([06a-combine-end.png](wide/06a-combine-end.png))。 +- **首页:**卡片的第三行说明(PDF 上次打开的时间)正好在可见区域边缘,18 点中只露出 2 点。 +- 两者都是高度不够造成的。让窗口保持在程序坞上方,会在这个 898 点高的窗口上占去 78 点。 + 做这项改动之前,应用区域高 740 点:首页的说明放得下,Combine 按钮约差 7 点。 +- 10 月 10 日(太平洋夏令时)做的评论显示为 10 月 11 日:即 [README](../../../README.zh-CN.md#状态) + 中列出的已知 UTC 读取问题。 diff --git a/apps/pdftools/tests/evidence/window-20261010/opened-from-the-menu.png b/apps/pdftools/tests/evidence/window-20261010/opened-from-the-menu.png new file mode 100644 index 00000000..af673cad Binary files /dev/null and b/apps/pdftools/tests/evidence/window-20261010/opened-from-the-menu.png differ diff --git a/apps/pdftools/tests/evidence/window-20261010/receipt.json b/apps/pdftools/tests/evidence/window-20261010/receipt.json new file mode 100644 index 00000000..f825c50f --- /dev/null +++ b/apps/pdftools/tests/evidence/window-20261010/receipt.json @@ -0,0 +1,72 @@ +{ + "result": "pass", + "date": "2026-10-10", + "platform": "macOS 26.6.2, Apple silicon (arm64)", + "commit": "487c79dbd336d2e26c3d79e67d38c7f71ed8588d", + "commit_note": "The binary was built from this commit, before the branch was rebased onto main; these trees are the same in the pull request's head.", + "trees": { + "crates/shell": "ec73b9b2e4a728a830b029452b6cf665141a7d09", + "apps/pdftools/bundle": "12279a3c1b6aab6493633519ad28b576c3d659e5", + "desktop": "81cc112903712f5901ebfa1c237c49f9502c88a4", + "Cargo.lock": "96ba999344171d551a5e80a79521877d0c765441" + }, + "app_hub_revision": "d86101d7206162b73ea873e2a31b48364600a110", + "binary": { + "build": "cargo build --locked -p octosense --no-default-features --features app-hub,craft-engines", + "path": "target/debug/octosense", + "sha256": "90aa09e6596ac40dc4c10aca6603963c9ebf2a5bec0b7d68bda00ff6a50d1c9a" + }, + "source_sha256": { + "apps/pdftools/bundle/manifest.json": "3c062555c5521ec7de41a0c2c86b1bac75be0d005cd7727f9d32c55535000236", + "apps/pdftools/bundle/main.splash": "d6db2eca74df93236a11f55d38b824ad71deddd56eb307631c3a512a22780d8a", + "apps/pdftools/tests/ui.py": "3039646310fe807dbec7a824193d80be04f8373c83182493b9cecdf4e8878bd5" + }, + "manifest": {"schema_minor": 1, "window": {"width": 1536, "height": 1024}}, + "launch": "A Terminal.app tab (osascript) under the machine's shell-run lock, with no other OctoSense running: OCTOSENSE_HOME= OCTOS_APP_CORE_DIR=/octos-home/.octos MAKEPAD_HOME=/makepad MAKEPAD_HIDE_WINDOWS=1 MAKEPAD_REMOTE=127.0.0.1:8915; ended with /quit. Points are the shell's layout points; frames are /g grabs at one pixel per point.", + "runs": [ + { + "name": "opened from the menu", + "how": "No MAKEPAD_WM_TEST_APP. Once the desk had drawn: the shell's menu (Cmd+Space), 'pdf tools', Return.", + "frame": "opened-from-the-menu.png (the whole hidden window)", + "shell_window_points": [1400, 897], + "desk_points": [0, 32, 1400, 865], + "log": "wm: pdftools asks for a 1536x1024 window; it opens at 1296x703 (the dock covers 78 points of the desk)", + "window_points": [52, 74, 1296, 703], + "app_area_points": [54, 108, 1292, 667], + "dock_top": 809, + "window_bottom": 777, + "note": "The work area is 1380 x 845 at (10, 42); OctoSense's dock covers its bottom 78 points, so the window keeps 42 points left and right, 32 below the desk's top and 32 above the dock. The status line ('0 bytes of 64 MB used') shows above the dock." + }, + { + "name": "the shell journey", + "how": "apps/pdftools/tests/ui.py's `shell` run, unchanged (light, then dark through the shell's style menu), on the samples octosense-pdf-service's pdftools_fixture example writes, with MAKEPAD_WM_TEST_APP=pdftools; a wrapper also kept each grab's /snap.", + "engine": "real: every pdf call went to the shell's pdf service", + "script_budget": "the runtime's 64 ms (load average 30 to 70)", + "result": "pass: every step of both passes, no script errors in the log, the shell exited after /quit", + "log": "wm: pdftools asks for a 1536x1024 window; it opens at 1296x698 (the dock covers 78 points of the desk)", + "window_note": "Opened before the desk's first draw, so sized on desk_area's fallback 1400 x 860 desk.", + "shell_window_points": [1400, 898], + "app_area_points": [54, 76, 1292, 662], + "layout": "wide: the app is 1292 points wide, above the 1180 where its narrow layout starts (main.splash narrow()); a right panel opens beside the left panel", + "frames": { + "wide/01-home.png": "Home, the library's covers", + "wide/02-reading.png": "Read, page 1, the Pages panel", + "wide/03-find.png": "Find: 'revenue', 9 matches on 4 pages, the first highlighted", + "wide/04-comment.png": "Comment: a highlight on the Summary's first line, its thread on the right", + "wide/06-combine.png": "Combine with Board minutes' page 1, before any scroll: 'One PDF of 5 pages' shows, the card's Combine button does not", + "wide/06a-combine-end.png": "the same card after one scroll: the name and the Combine button", + "wide/08-edit.png": "Edit: clause 1 in the editor with ' (amended)' typed, the Edit text panel on the right", + "wide/d02-reading.png": "the dark pass: Read, page 1" + }, + "status_line": "In every frame the status line ('Saved' or 'Edited', and the storage used) is at y 707 to 725 and the zoom bar (628 to 681 in Read) ends above it; the dock's top is at y 810.", + "canvas_points": [139, 184, 1207, 509] + } + ], + "findings": [ + "Combine: with two PDFs listed, the card's own Combine button is not visible without scrolling at this size. Its bottom would be at y 778, and the canvas ends at y 693, so it is 85 points short. One scroll (109 points) shows it (wide/06a-combine-end.png). Each further PDF adds about 114 points.", + "Home: the cards' third caption line (when the PDF was last opened, here 'Not opened yet') sits at the fold: 2 of its 18 points show.", + "Both come from height: keeping the window above the dock costs 78 points on this 898-point-tall window. Before that change the startup window's app area was 740 points tall: Home's captions fitted, and the Combine button was about 7 points short.", + "A comment made on 10 Oct (PDT) is dated 11 Oct: the known UTC reading listed in apps/pdftools/README.md." + ], + "not_verified": "Taller screens; other styles; the narrow layout at this commit (the v2-20261010 runs covered it at 990 x 603); Linux, Windows, phones." +} diff --git a/apps/pdftools/tests/evidence/window-20261010/wide/01-home.png b/apps/pdftools/tests/evidence/window-20261010/wide/01-home.png new file mode 100644 index 00000000..b5e2045e Binary files /dev/null and b/apps/pdftools/tests/evidence/window-20261010/wide/01-home.png differ diff --git a/apps/pdftools/tests/evidence/window-20261010/wide/02-reading.png b/apps/pdftools/tests/evidence/window-20261010/wide/02-reading.png new file mode 100644 index 00000000..ef159a49 Binary files /dev/null and b/apps/pdftools/tests/evidence/window-20261010/wide/02-reading.png differ diff --git a/apps/pdftools/tests/evidence/window-20261010/wide/03-find.png b/apps/pdftools/tests/evidence/window-20261010/wide/03-find.png new file mode 100644 index 00000000..43df7817 Binary files /dev/null and b/apps/pdftools/tests/evidence/window-20261010/wide/03-find.png differ diff --git a/apps/pdftools/tests/evidence/window-20261010/wide/04-comment.png b/apps/pdftools/tests/evidence/window-20261010/wide/04-comment.png new file mode 100644 index 00000000..70baa2f6 Binary files /dev/null and b/apps/pdftools/tests/evidence/window-20261010/wide/04-comment.png differ diff --git a/apps/pdftools/tests/evidence/window-20261010/wide/06-combine.png b/apps/pdftools/tests/evidence/window-20261010/wide/06-combine.png new file mode 100644 index 00000000..80c47bc5 Binary files /dev/null and b/apps/pdftools/tests/evidence/window-20261010/wide/06-combine.png differ diff --git a/apps/pdftools/tests/evidence/window-20261010/wide/06a-combine-end.png b/apps/pdftools/tests/evidence/window-20261010/wide/06a-combine-end.png new file mode 100644 index 00000000..da31b600 Binary files /dev/null and b/apps/pdftools/tests/evidence/window-20261010/wide/06a-combine-end.png differ diff --git a/apps/pdftools/tests/evidence/window-20261010/wide/08-edit.png b/apps/pdftools/tests/evidence/window-20261010/wide/08-edit.png new file mode 100644 index 00000000..783a6149 Binary files /dev/null and b/apps/pdftools/tests/evidence/window-20261010/wide/08-edit.png differ diff --git a/apps/pdftools/tests/evidence/window-20261010/wide/d02-reading.png b/apps/pdftools/tests/evidence/window-20261010/wide/d02-reading.png new file mode 100644 index 00000000..dd1bcefe Binary files /dev/null and b/apps/pdftools/tests/evidence/window-20261010/wide/d02-reading.png differ diff --git a/crates/shell/src/apps.rs b/crates/shell/src/apps.rs index 84235978..d2fef4c8 100644 --- a/crates/shell/src/apps.rs +++ b/crates/shell/src/apps.rs @@ -100,6 +100,40 @@ pub fn card_manifest_id(app: &crate::clients::AppDef) -> Option<&str> { app.id.strip_prefix("hub:").or_else(|| app.args.iter().find_map(|a| a.strip_prefix(SYSTEM_ARG))) } +/// The size, in points, a script app's manifest asks its desktop window to +/// open at (App Hub's optional `window`, schema_minor 1), read from the +/// bundle the Card runner is about to open: a system app's admitted build +/// (unpacked now if this is its first launch) or an installed app's bundle. +/// `None` for any other app, and for a manifest that does not ask or does +/// not parse (the Card runner then refuses it, or opens it as before). The +/// hint grants nothing; the desk clamps it +/// (`desktop_layout::preferred_size`). +#[cfg(any(feature = "app-hub", native_mobile))] +pub fn window_hint(cx: &mut makepad_widgets::Cx, app: &crate::clients::AppDef) -> Option<(f64, f64)> { + window_hint_in(&octosense_app_hub_app::data_root(cx), app) +} + +/// [`window_hint`] under App Hub's apps root `root`, the one the Card runner +/// opens its apps from. +#[cfg(any(feature = "app-hub", native_mobile))] +fn window_hint_in(root: &Path, app: &crate::clients::AppDef) -> Option<(f64, f64)> { + let manifest_id = card_manifest_id(app)?; + let system = octosense_app_hub_app::system_apps().into_iter().find(|system| system.id == manifest_id); + let bundle = match system { + Some(system) => octosense_appstore::system::prepare(root, &system).ok()?.0, + None => installed_bundle(root, manifest_id), + }; + let text = std::fs::read_to_string(bundle.join(octosense_app_contract::MANIFEST_FILE)).ok()?; + let window = octosense_app_contract::AppManifest::parse(&text).ok()?.window?; + Some((f64::from(window.width), f64::from(window.height))) +} + +/// Without App Hub there is no script app to ask. +#[cfg(not(any(feature = "app-hub", native_mobile)))] +pub fn window_hint(_cx: &mut makepad_widgets::Cx, _app: &crate::clients::AppDef) -> Option<(f64, f64)> { + None +} + /// A system app's launcher row carries its manifest id as this argument. const SYSTEM_ARG: &str = "--system="; @@ -951,6 +985,61 @@ mod tests { assert_eq!(installed_bundle(&root, "dev.example.none"), root.join("dev.example.none/bundle")); let _ = std::fs::remove_dir_all(&root); } + + /// A script app's manifest `window` is the size its desktop window asks + /// for (`window_hint`), read from the bundle the Card runner opens: a + /// system app's admitted build, unpacked here as on a first launch, or + /// an installed app's bundle. No hint, a manifest App Hub refuses, or an + /// app that is not a script app: the default size. + #[cfg(feature = "app-hub")] + #[test] + fn a_script_apps_manifest_window_is_the_size_it_asks_for() { + let root = std::env::temp_dir().join(format!("shell-window-hint-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&root); + // A system app, packed by digest as the build packs one. + let source = root.join("source"); + std::fs::create_dir_all(&source).unwrap(); + std::fs::write(source.join("main.splash"), "use mod.widgets.*\nApp { Label { text: \"Probe\" } }\n").unwrap(); + std::fs::write(source.join("manifest.json"), serde_json::json!({ + "schema": 1, "schema_minor": 1, "id": "os.windowprobe", "version": "1.0.0", "name": "Window probe", + "integrity": {"bundle_blake3": ""}, "window": {"width": 900, "height": 700} + }).to_string()).unwrap(); + let packed = octosense_app_hub::pack::pack_system_app(&source).unwrap(); + test_system_apps::register(octosense_appstore::system::SystemApp { + id: "os.windowprobe", name: "Window probe", pack: Box::leak(packed.pack_json.into_boxed_str()), assets: &[], + }); + let probe = card_row("windowprobe".into(), "Window probe".into(), vec![format!("{SYSTEM_ARG}os.windowprobe")]); + assert_eq!(window_hint_in(&root, &probe), Some((900.0, 700.0))); + assert!(root.join(".system/os.windowprobe").is_dir(), "unpacked as the Card runner unpacks it"); + // An installed app: its bundle's manifest. + let bundle = root.join(".bundles/org.example.wide/bundle"); + std::fs::create_dir_all(&bundle).unwrap(); + let installed = card_row(installed_launch_id("org.example.wide"), "Wide".into(), Vec::new()); + let manifest = |extra: serde_json::Value| { + let mut manifest = serde_json::json!({ + "schema": 1, "id": "org.example.wide", "version": "1.0.0", "name": "Wide", + "integrity": {"bundle_blake3": "0".repeat(64)} + }); + manifest.as_object_mut().unwrap().extend(extra.as_object().unwrap().clone()); + std::fs::write(bundle.join("manifest.json"), manifest.to_string()).unwrap(); + }; + manifest(serde_json::json!({"schema_minor": 1, "window": {"width": 1200, "height": 800}})); + assert_eq!(window_hint_in(&root, &installed), Some((1200.0, 800.0))); + manifest(serde_json::json!({})); + assert_eq!(window_hint_in(&root, &installed), None, "no hint"); + manifest(serde_json::json!({"window": {"width": 1200, "height": 800}})); + assert_eq!(window_hint_in(&root, &installed), None, "App Hub refuses window below schema_minor 1"); + let mut native = installed.clone(); + native.bin = "notes".into(); + assert_eq!(window_hint_in(&root, &native), None, "not a script app"); + // The build's own PDF Tools (desktop builds pack it) asks for its + // designs' 1536 x 1024, admitted as the Card runner admits it. + if system_app_ids().contains(&"pdftools") { + let pdftools = card_row("pdftools".into(), "PDF Tools".into(), vec![format!("{SYSTEM_ARG}os.pdftools")]); + assert_eq!(window_hint_in(&root, &pdftools), Some((1536.0, 1024.0))); + } + let _ = std::fs::remove_dir_all(&root); + } use super::*; /// ADR 0004 §3: no script app takes a native app's id or namespace. diff --git a/crates/shell/src/desktop.rs b/crates/shell/src/desktop.rs index f3bef294..bd085cab 100644 --- a/crates/shell/src/desktop.rs +++ b/crates/shell/src/desktop.rs @@ -478,6 +478,41 @@ mod tests { assert_eq!(shelf_geometry(screen, &t, n), mac, "settled OctoSense"); } #[test] + fn the_dock_overlays_the_desk_only_where_it_floats_over_it() { + // The hidden shell: a 1400 x 899 window, the desk from y = 32 to its + // bottom, a work area 10 points (the outer gap) inside it. + let (desk_bottom, gap) = (899.0, crate::desk::GAPS_OUT); + let mut t = StyleTween::default(); + for style in [DesktopStyle::OctoSense, DesktopStyle::Macos] { + t.select(style); + t.step(1.0); + let area_bottom = desk_bottom - gap - t.reserved_height(); + assert_eq!(dock_overlay(&t, desk_bottom, area_bottom), 78.0, "{style:?}"); + // The dock it covers with is the one the shelf draws. + let drawn = shelf_geometry(rect(0.0, 0.0, 1400.0, desk_bottom), &t, 20); + assert_eq!(dock_overlay(&t, desk_bottom, area_bottom), area_bottom - drawn.pos.y, "{style:?}"); + } + // Windows reserves its taskbar's strip; the other rows float nothing + // over the desk's bottom. + for style in [ + DesktopStyle::Windows, + DesktopStyle::Windows2000, + DesktopStyle::Omarchy, + DesktopStyle::NextStep, + DesktopStyle::Ios, + DesktopStyle::Android, + ] { + t.select(style); + t.step(1.0); + let area_bottom = desk_bottom - gap - t.reserved_height(); + assert_eq!(dock_overlay(&t, desk_bottom, area_bottom), 0.0, "{style:?}"); + } + // Mid-tween it reads the style the tween is heading to. + t.select(DesktopStyle::OctoSense); + t.step(0.2); + assert_eq!(dock_overlay(&t, desk_bottom, desk_bottom - gap), 78.0); + } + #[test] fn chrome_follows_the_flag_for_styles_with_both_appearances() { for style in [DesktopStyle::OctoSense, DesktopStyle::Macos, DesktopStyle::Windows, DesktopStyle::Ios, DesktopStyle::Android] { assert!(dark_chrome(style, true), "{style:?}"); @@ -1052,6 +1087,25 @@ fn dock_app_ids(state: &WmState) -> Vec { pub fn dock_bounds(state: &WmState, size: Vec2d) -> Rect { shelf_geometry(rect(0.0,0.0,size.x,size.y), &state.style, dock_app_ids(state).len()) } + +/// How many points of the work area's bottom the dock covers, on a desk +/// that ends at `desk_bottom` with a work area that ends at `area_bottom` +/// (window coordinates, where [`shelf_geometry`] places the shelf): macOS's +/// and OctoSense's docks float over the desk and reserve no strip. 0 for a +/// shelf that reserves its strip (Windows: the work area already stops above +/// it), sits elsewhere (NeXTSTEP's column, Omarchy's bar) or is absent (the +/// phone rows). Read for the style the tween is heading to, settled. A window +/// that asks for its own size ends above it (`desktop_layout::room`). +pub fn dock_overlay(style: &StyleTween, desk_bottom: f64, area_bottom: f64) -> f64 { + if !matches!(style.target, DesktopStyle::Macos | DesktopStyle::OctoSense) { + return 0.0; + } + let mut settled = StyleTween::default(); + settled.select(style.target); + settled.step(1.0); + let dock = shelf_geometry(rect(0.0, 0.0, 1.0, desk_bottom), &settled, 0); + (area_bottom - dock.pos.y).max(0.0) +} pub fn dock_icon_bounds(state: &WmState, size: Vec2d, app: &str) -> Rect { let apps=dock_app_ids(state); let dock=shelf_geometry(rect(0.0,0.0,size.x,size.y), &state.style, apps.len()); diff --git a/crates/shell/src/desktop_layout.rs b/crates/shell/src/desktop_layout.rs index 64ad41cc..c16ee135 100644 --- a/crates/shell/src/desktop_layout.rs +++ b/crates/shell/src/desktop_layout.rs @@ -15,15 +15,60 @@ pub struct DesktopWindows { pub enabled: bool, pub windows: Vec, } + +/// The margins a new window keeps from the desk's edges: it opens this far +/// in from the left and the top (each later window cascading 30 points +/// further), and a window opened at an app's own size ([`preferred_size`]) +/// keeps them on the right and the bottom too. +pub const MARGIN_X: f64 = 42.0; +pub const MARGIN_Y: f64 = 32.0; +/// The smallest a window gets: what a resize drag leaves it. +pub const MIN_W: f64 = 80.0; +pub const MIN_H: f64 = 60.0; + +/// The part of the desk `area` a window at its app's own size may use: all +/// of it, less the `dock` points a dock that floats over the desk covers at +/// its bottom (`desktop::dock_overlay`; 0 for a dock that reserves its strip +/// or is absent). +pub fn room(area: LRect, dock: f64) -> LRect { + LRect::new(area.x, area.y, area.w, (area.h - dock.max(0.0)).max(1.0)) +} + +/// The size a window whose app asks for `w` x `h` points (a script app's +/// manifest `window`) opens at on `area`, under a dock that covers `dock` +/// points of its bottom: that size, clamped to the [`room`] above the dock +/// minus [`MARGIN_X`] and [`MARGIN_Y`] on each side, so the window ends above +/// the dock, and never below [`MIN_W`] x [`MIN_H`]. +pub fn preferred_size(w: f64, h: f64, area: LRect, dock: f64) -> (f64, f64) { + let room = room(area, dock); + (w.min(room.w - 2.0 * MARGIN_X).max(MIN_W), h.min(room.h - 2.0 * MARGIN_Y).max(MIN_H)) +} + impl DesktopWindows { + /// The client's window, made at the default size if it has none: 72% of + /// the desk's width and 76% of its height, at most 1000 x 720 points. pub fn ensure(&mut self, client: ClientId, area: LRect) { + self.ensure_sized(client, area, None, 0.0); + } + + /// [`ensure`](Self::ensure) for a client whose app asks to open at + /// `preferred` (width, height) points: [`preferred_size`] instead of the + /// default size, placed as every window is, in the [`room`] above a dock + /// that covers `dock` points of the desk's bottom, so a cascaded window + /// ends above the dock too. `None` is the default size on the whole desk, + /// whatever the dock. + pub fn ensure_sized(&mut self, client: ClientId, area: LRect, preferred: Option<(f64, f64)>, dock: f64) { if self.get(client).is_some() { return; } let offset = (self.windows.len() % 7) as f64 * 30.0; - let mut rect = area.centered((area.w * 0.72).min(1000.0), (area.h * 0.76).min(720.0)); - rect.x = (area.x + 42.0 + offset).min(area.x + area.w - rect.w); - rect.y = (area.y + 32.0 + offset).min(area.y + area.h - rect.h); + let (bounds, (w, h)) = match preferred { + Some((w, h)) => (room(area, dock), preferred_size(w, h, area, dock)), + None => (area, ((area.w * 0.72).min(1000.0), (area.h * 0.76).min(720.0))), + }; + let mut rect = bounds.centered(w, h); + rect.x = (bounds.x + MARGIN_X + offset).min(bounds.x + bounds.w - rect.w); + rect.y = (bounds.y + MARGIN_Y + offset).min(bounds.y + bounds.h - rect.h); self.windows.push(DesktopWindow { client, rect, @@ -100,6 +145,96 @@ mod tests { LRect::new(230.0, 212.0, 300.0, 200.0) ); } + #[test] + fn without_a_hint_a_window_opens_at_the_default_size() { + // 72% x 76% of the desk, at most 1000 x 720, 42 and 32 points in. + let area = LRect::new(0.0, 30.0, 1200.0, 800.0); + let mut d = DesktopWindows::default(); + d.ensure(1, area); + d.ensure_sized(2, area, None, 0.0); + assert_eq!(d.get(1).unwrap().rect, LRect::new(42.0, 62.0, 864.0, 608.0)); + // The second cascades 30 points, exactly as before. + assert_eq!(d.get(2).unwrap().rect, LRect::new(72.0, 92.0, 864.0, 608.0)); + let big = LRect::new(0.0, 30.0, 2560.0, 1400.0); + d.ensure(3, big); + assert_eq!(d.get(3).unwrap().rect, LRect::new(102.0, 122.0, 1000.0, 720.0)); + } + + #[test] + fn a_hint_smaller_than_the_desk_opens_at_its_size() { + // PDF Tools asks for 1536 x 1024: larger than the default's cap. + let area = LRect::new(0.0, 30.0, 2560.0, 1400.0); + let mut d = DesktopWindows::default(); + d.ensure_sized(1, area, Some((1536.0, 1024.0)), 0.0); + assert_eq!(d.get(1).unwrap().rect, LRect::new(42.0, 62.0, 1536.0, 1024.0)); + // Placed as every window is: a later one cascades. + d.ensure_sized(2, area, Some((1536.0, 1024.0)), 0.0); + assert_eq!(d.get(2).unwrap().rect, LRect::new(72.0, 92.0, 1536.0, 1024.0)); + } + + #[test] + fn a_hint_larger_than_the_desk_is_clamped_to_it_minus_the_margins() { + // The hidden shell's desk on a 1400 x 900 screen. + let area = LRect::new(12.0, 44.0, 1375.0, 793.0); + let mut d = DesktopWindows::default(); + d.ensure_sized(1, area, Some((1536.0, 1024.0)), 0.0); + let r = d.get(1).unwrap().rect; + assert_eq!(r, LRect::new(54.0, 76.0, 1291.0, 729.0)); + // The margins hold on the right and the bottom too. + assert_eq!((area.x + area.w) - (r.x + r.w), MARGIN_X); + assert_eq!((area.y + area.h) - (r.y + r.h), MARGIN_Y); + // Cascaded, it moves as every window does and still ends inside the desk. + d.ensure_sized(2, area, Some((1536.0, 1024.0)), 0.0); + let r2 = d.get(2).unwrap().rect; + assert_eq!(r2, LRect::new(84.0, 106.0, 1291.0, 729.0)); + assert!(r2.x + r2.w <= area.x + area.w && r2.y + r2.h <= area.y + area.h); + // Only the side that is too large is clamped. + d.ensure_sized(3, area, Some((1536.0, 500.0)), 0.0); + assert_eq!((d.get(3).unwrap().rect.w, d.get(3).unwrap().rect.h), (1291.0, 500.0)); + } + + #[test] + fn a_hint_ends_above_a_dock_that_floats_over_the_desk() { + // The hidden shell's desk: OctoSense's dock covers its bottom 78 + // points, from y = 811 down. + let area = LRect::new(10.0, 42.0, 1380.0, 847.0); + let dock_top = area.y + area.h - 78.0; + let mut d = DesktopWindows::default(); + d.ensure_sized(1, area, Some((1536.0, 1024.0)), 78.0); + // The margins hold above the dock as they do above the desk's edge. + let r = d.get(1).unwrap().rect; + assert_eq!(r, LRect::new(52.0, 74.0, 1296.0, 705.0)); + assert_eq!(dock_top - (r.y + r.h), MARGIN_Y); + // Cascaded windows move up rather than down behind the dock. + for client in 2..=7 { + d.ensure_sized(client, area, Some((1536.0, 1024.0)), 78.0); + let r = d.get(client).unwrap().rect; + assert!(r.y + r.h <= dock_top, "window {client} ends at {} below the dock's top {dock_top}", r.y + r.h); + } + assert_eq!(d.get(3).unwrap().rect, LRect::new(94.0, 106.0, 1296.0, 705.0)); + // A hint the room above the dock holds is not shrunk. + d.ensure_sized(8, area, Some((900.0, 600.0)), 78.0); + assert_eq!((d.get(8).unwrap().rect.w, d.get(8).unwrap().rect.h), (900.0, 600.0)); + // A dock that reserves its strip (the work area already stops above + // it) or no dock at all: 0, the size as before. + assert_eq!(preferred_size(1536.0, 1024.0, area, 0.0), (1296.0, 783.0)); + // A default-size window keeps the whole desk, whatever the dock. + let mut plain = DesktopWindows::default(); + plain.ensure(1, area); + let mut docked = DesktopWindows::default(); + docked.ensure_sized(1, area, None, 78.0); + assert_eq!(docked.get(1).unwrap().rect, plain.get(1).unwrap().rect); + } + + #[test] + fn a_hint_never_opens_a_window_below_the_minimum() { + let area = LRect::new(0.0, 0.0, 150.0, 100.0); + let mut d = DesktopWindows::default(); + d.ensure_sized(1, area, Some((1536.0, 1024.0)), 0.0); + assert_eq!(d.get(1).unwrap().rect, LRect::new(42.0, 32.0, MIN_W, MIN_H)); + assert_eq!(preferred_size(320.0, 240.0, LRect::new(0.0, 0.0, 10.0, 10.0), 0.0), (MIN_W, MIN_H)); + } + #[test] fn snapping_keeps_restore_size_and_tracks_a_resized_workarea() { let area=LRect::new(10.,42.,1380.,794.); diff --git a/crates/shell/src/layout.rs b/crates/shell/src/layout.rs index b76b36c9..4657a2d2 100644 --- a/crates/shell/src/layout.rs +++ b/crates/shell/src/layout.rs @@ -506,6 +506,16 @@ impl WmLayout { self.insert_on(self.active, client, area, gap); } + /// [`insert`](Self::insert) for a client whose app asks its desktop + /// window to open at `preferred` (width, height) points: a script app's + /// manifest `window`, which the desk clamps, above a dock that covers + /// `dock` points of the desk's bottom (`DesktopWindows::ensure_sized`). + /// `None` is `insert`. + pub fn insert_sized(&mut self, client: ClientId, area: LRect, gap: f64, preferred: Option<(f64, f64)>, dock: f64) { + self.desktop.ensure_sized(client, area, preferred, dock); + self.insert(client, area, gap); + } + pub fn insert_on(&mut self, ws: usize, client: ClientId, area: LRect, gap: f64) { self.desktop.ensure(client, area); self.insert_at(ws, client, area, gap, !self.desktop.enabled); diff --git a/crates/shell/src/lib.rs b/crates/shell/src/lib.rs index 78b391e8..a0e3c1b8 100644 --- a/crates/shell/src/lib.rs +++ b/crates/shell/src/lib.rs @@ -736,6 +736,16 @@ impl App { self.state.as_mut().expect("state after startup") } + /// How many points of the work area `area`'s bottom the dock covers + /// (`desktop::dock_overlay`): a window that asks for its own size ends + /// above it. Before the desk's first draw, on `desk_area`'s fallback + /// desk, 860 points tall from the window's top. + fn dock_overlay(&self, cx: &mut Cx, area: LRect) -> f64 { + let rect = self.desk(cx).borrow_mut::().map(|d| d.desk_rect).unwrap_or_default(); + let desk_bottom = if rect.size.x > 1.0 { rect.pos.y + rect.size.y } else { 860.0 }; + self.state.as_ref().map_or(0.0, |s| desktop::dock_overlay(&s.style, desk_bottom, area.y + area.h)) + } + fn desk_area(&self, cx: &mut Cx) -> LRect { let desk = self.desk(cx); let rect = desk @@ -2631,7 +2641,18 @@ impl App { .clients .insert(id, clients::ClientSlot::module(id, &app.id, &app.label)); let gap = self.state_mut().gap; - self.state_mut().layout.insert(id, area, gap); + // A script app may ask its desktop window to open at a size (its + // manifest's `window`), which the desk clamps; without one, or for + // any other module, the default size. Phones ignore it: their apps + // are full-screen. + // It ends above a dock that floats over the desk. + let preferred = if self.state_mut().style.target.mobile() { None } else { apps::window_hint(cx, app) }; + let dock = if preferred.is_some() { self.dock_overlay(cx, area) } else { 0.0 }; + self.state_mut().layout.insert_sized(id, area, gap, preferred, dock); + if let (Some((w, h)), Some(window)) = (preferred, self.state_mut().layout.desktop.get(id)) { + log!("wm: {} asks for a {}x{} window; it opens at {}x{} (the dock covers {} points of the desk)", + app.id, w, h, window.rect.w, window.rect.h, dock); + } // The tile is a module tile from its first draw; the root is seated // in it before anything asks it to draw. self.desk(cx).borrow_mut::().map(|mut d| { @@ -4966,17 +4987,18 @@ impl App { let rect = if resize { // The grabbed corner moves with the pointer; the opposite // one is fixed (DragController.cpp:413-423). + use crate::desktop_layout::{MIN_H, MIN_W}; let (x, w) = if !resize_x {(start_rect.x,start_rect.w)}else if grab_left { - let w = (start_rect.w - d.x).max(80.0); + let w = (start_rect.w - d.x).max(MIN_W); (start_rect.x + start_rect.w - w, w) } else { - (start_rect.x, (start_rect.w + d.x).max(80.0)) + (start_rect.x, (start_rect.w + d.x).max(MIN_W)) }; let (y, h) = if !resize_y {(start_rect.y,start_rect.h)}else if grab_top { - let h = (start_rect.h - d.y).max(60.0); + let h = (start_rect.h - d.y).max(MIN_H); (start_rect.y + start_rect.h - h, h) } else { - (start_rect.y, (start_rect.h + d.y).max(60.0)) + (start_rect.y, (start_rect.h + d.y).max(MIN_H)) }; LRect::new(x, y, w, h) } else { diff --git a/crates/shell/src/pdftools_model_tests.rs b/crates/shell/src/pdftools_model_tests.rs index 11ad89e7..babbaa5c 100644 --- a/crates/shell/src/pdftools_model_tests.rs +++ b/crates/shell/src/pdftools_model_tests.rs @@ -152,3 +152,14 @@ fn a_comment_date_is_read_as_the_service_writes_it() { ); assert_eq!(pdftools_model("comment_time(\"later\").to_json()"), json!(null)); } + +/// The designs are drawn at 1536 x 1024, and the shipped manifest asks the +/// desktop to open PDF Tools' window at that size: App Hub's `window`, which +/// needs `schema_minor` 1. The desk clamps it (`desktop_layout`). +#[cfg(any(feature = "app-hub", native_mobile))] +#[test] +fn the_shipped_manifest_asks_for_the_designs_window_size() { + let manifest = octosense_app_contract::AppManifest::parse(include_str!("../../../apps/pdftools/bundle/manifest.json")).unwrap(); + assert_eq!(manifest.schema_minor, 1); + assert_eq!(manifest.window, Some(octosense_app_contract::WindowHint::new(1536, 1024))); +} diff --git a/desktop/docs/code-walkthrough.md b/desktop/docs/code-walkthrough.md index ec11fd51..935cff49 100644 --- a/desktop/docs/code-walkthrough.md +++ b/desktop/docs/code-walkthrough.md @@ -119,6 +119,21 @@ tests installation into the shell before publication. `agent_apps` and `register_host_services`. App Hub's native `CARD_MODULE` hosts these interpreted programs. The optional AppCard assistant has its own module. +On the desktop, a script app's window opens at the size its manifest asks +for in `window`, App Hub's optional hint, which needs `"schema_minor": 1`. +`App::launch_module_as` in [`lib.rs`](../../crates/shell/src/lib.rs) reads it +with `apps::window_hint`, from the bundle the Card runner opens, and +`preferred_size` in +[`desktop_layout.rs`](../../crates/shell/src/desktop_layout.rs) clamps it to +the desk minus the margins every new window keeps (42 points left and right, +32 top and bottom), never below 80 x 60. Where the dock floats over the desk +instead of reserving a strip (the macOS and OctoSense styles), the bottom +margin is kept above the dock (`desktop::dock_overlay`), so the window's +bottom edge is never behind it. Without `window`, a window opens at +72% of the desk's width and 76% of its height, at most 1000 x 720 points. +The person can resize either, and phones ignore the hint. PDF Tools asks for +its designs' 1536 x 1024. + Admission verifies the bundle and resolves its metadata and resource limits. Capability names disclose usage, not execution permission. A script's `host.request(...)` runs under that app's identity. App Hub's diff --git a/desktop/docs/code-walkthrough.zh-CN.md b/desktop/docs/code-walkthrough.zh-CN.md index dacc9649..ca400334 100644 --- a/desktop/docs/code-walkthrough.zh-CN.md +++ b/desktop/docs/code-walkthrough.zh-CN.md @@ -100,6 +100,15 @@ MAKEPAD_APP_CONFIG='{"mail_demo":true}' cargo run --locked --release -p octosens `register_host_services`。App Hub 的原生 `CARD_MODULE` 托管这些解释执行程序; 可选的 AppCard 助手使用自己的模块。 +在桌面上,脚本应用的窗口按清单 `window` 请求的尺寸打开。`window` 是 App Hub 的可选提示, +需要 `"schema_minor": 1`。[`lib.rs`](../../crates/shell/src/lib.rs) 中的 +`App::launch_module_as` 用 `apps::window_hint` 从 Card 运行器将要打开的应用包中读取它, +[`desktop_layout.rs`](../../crates/shell/src/desktop_layout.rs) 中的 `preferred_size` +再把它限制在桌面减去每个新窗口都保留的边距之内(左右各 42 点,上下各 32 点),且不小于 +80 x 60。程序坞浮在桌面之上、不预留区域时(macOS 和 OctoSense 风格),底部边距改为从程序坞 +上沿算起(`desktop::dock_overlay`),所以窗口的下边缘不会位于程序坞后面。没有 `window` 时,窗口宽度取桌面的 72%、高度取 76%,最大 1000 x 720 点。 +两种情况下用户都可以调整窗口大小;手机忽略这个提示。PDF Tools 请求它设计图的 1536 x 1024。 + 准入验证应用包,解析元数据与资源上限。能力名称披露用途,不是执行许可。脚本的 `host.request(...)` 按应用身份执行。 App Hub 的 `crates/appstore/src/services.rs` 定义 `HostService`、`ServiceCall` 和回复机制; 调用携带应用身份和宿主目录,供服务检查。只有明确声明的 Agent 工具,才会把相应操作提供给模型。 diff --git a/native-apps.json b/native-apps.json index bc1cb001..597b018c 100644 --- a/native-apps.json +++ b/native-apps.json @@ -114,7 +114,7 @@ "id": "apphub", "feature": "app-hub", "crate": "octosense-app-hub-app", - "source": { "git": "https://github.com/OctoSense-org/OctoSense-App-Hub", "rev": "80a64b4557aaba71f7c826c5dc69721c21c0f0fc", "local": null }, + "source": { "git": "https://github.com/OctoSense-org/OctoSense-App-Hub", "rev": "d86101d7206162b73ea873e2a31b48364600a110", "local": null }, "module": ["octosense_app_hub_app::APP_HUB_MODULE", "octosense_app_hub_app::CARD_MODULE"], "bin": null, "default_features": true,