From 0bc9e13fc6a273d4db014b7281159edc3e331b38 Mon Sep 17 00:00:00 2001 From: "Yifeng \"Evan\" Wang" <7312949+doodlewind@users.noreply.github.com> Date: Wed, 7 Oct 2026 03:29:40 +0800 Subject: [PATCH 1/4] fix(desktop): the Start panel is as wide as its rows, so a Japanese places column stays inside its frame Co-Authored-By: Claude Opus 5.5 --- shells/desktop/src/system-ui/app.tsx | 25 ++++++++- shells/desktop/src/system-ui/chrome.tsx | 38 ++++++++++++-- shells/desktop/src/system-ui/theme.ts | 15 ++++++ shells/desktop/src/system-ui/wm.ts | 45 +++++++++++++--- shells/desktop/test/system-ui.test.ts | 68 ++++++++++++++++++++++++- 5 files changed, 177 insertions(+), 14 deletions(-) diff --git a/shells/desktop/src/system-ui/app.tsx b/shells/desktop/src/system-ui/app.tsx index d8b508e..c1ec3f3 100644 --- a/shells/desktop/src/system-ui/app.tsx +++ b/shells/desktop/src/system-ui/app.tsx @@ -1128,8 +1128,27 @@ export default function App() { return classicStartItems(); }; - /** The panel rectangle both the render and hit testing read. */ - const startGeo = () => startLayout(startItems(), vp().h, metrics()); + /** A launcher row's natural width: its label in the face it is drawn in + * (XP sets the pinned rows in bold), the icon slot, padding and arrow. */ + const startRowW = (item: PopupItem) => + item.sep + ? 0 + : metrics().startRowInset + + measure(item.label, item.bottom ? theme().fontSlot("bold") : uiSlot()) + + (item.sub ? metrics().startArrowW : 0); + + /** The panel rectangle both the render and hit testing read: as wide as + * its rows, within the screen. */ + const startGeo = () => { + const items = startItems(); + return startLayout( + items, + vp().h, + metrics(), + (i) => startRowW(items[i]), + vp().w - metrics().startX - 2, + ); + }; function startItemAt(x: number, y: number): number { return startRowAt(startGeo(), x, y); @@ -2310,6 +2329,8 @@ export default function App() { y={startGeo().y} w={startGeo().w} h={startGeo().h} + leftW={startGeo().leftW} + rightW={startGeo().rightW} items={startItems()} hover={startHover()} user="Pocket" diff --git a/shells/desktop/src/system-ui/chrome.tsx b/shells/desktop/src/system-ui/chrome.tsx index 2f390b4..f2c65aa 100644 --- a/shells/desktop/src/system-ui/chrome.tsx +++ b/shells/desktop/src/system-ui/chrome.tsx @@ -333,10 +333,25 @@ export function PopupPanel(props: { ); } +/** The label a launcher row shows in a row `rowW` wide: whole when the + * panel grew to fit it (wm.ts startLayout), cut with "..." past the panel's + * maximum. */ +function startLabel(item: Popup["items"][number], rowW: number, theme: DesktopTheme, bold = false): string { + const m = theme.metrics; + const room = rowW - m.startRowInset - (item.sub ? m.startArrowW : 0); + const slot = theme.fontSlot(bold ? "bold" : "ui"); + const measure = (text: string) => { + const ops = getOps(); + return ops.measureText ? ops.measureText(text, slot) : text.length * 7; + }; + return fitLabel(item.label, room, measure); +} + /** One Start row: icon slot, label, submenu arrow. Shared by both panels. */ function StartRow(props: { item: Popup["items"][number]; hover: boolean; + rowW: number; theme: DesktopTheme; }) { return ( @@ -357,7 +372,7 @@ function StartRow(props: { ? "hover" : "normal", )} - t={props.item.label} + t={startLabel(props.item, props.rowW, props.theme, props.item.bottom)} /> {props.item.sub ? ( @@ -383,6 +398,9 @@ export function StartPanel(props: { y: number; w: number; h: number; + /** The two columns' widths from wm.ts startLayout. */ + leftW: number; + rightW: number; items: Popup["items"]; hover: number; user: string; @@ -421,7 +439,7 @@ export function StartPanel(props: { /> - + {at((it) => !it.foot && it.col !== "right" && !it.bottom).map((e) => e.item.sep ? ( @@ -429,6 +447,7 @@ export function StartPanel(props: { ), @@ -441,6 +460,7 @@ export function StartPanel(props: { ), @@ -455,6 +475,7 @@ export function StartPanel(props: { ), @@ -540,7 +561,7 @@ export function StartMenu(props: { ? "hover" : "normal", )} - t={item.label} + t={startLabel(item, props.w - props.theme.metrics.startPadX * 2 - props.theme.metrics.startRailW, props.theme)} /> {item.sub ? ( @@ -581,10 +602,17 @@ export function desktopLabelText( measure: (text: string) => number, crowded = true, ): string { - if (selected || !crowded || measure(label) <= DESK_LABEL_MAX_W) return label; + if (selected || !crowded) return label; + return fitLabel(label, DESK_LABEL_MAX_W, measure); +} + +/** A label whole when it is no wider than `maxW`, else cut with "..." until + * it fits. */ +export function fitLabel(label: string, maxW: number, measure: (text: string) => number): string { + if (measure(label) <= maxW) return label; const chars = [...label]; let n = chars.length; - while (n > 1 && measure(`${chars.slice(0, n).join("").trimEnd()}...`) > DESK_LABEL_MAX_W) n--; + while (n > 1 && measure(`${chars.slice(0, n).join("").trimEnd()}...`) > maxW) n--; return `${chars.slice(0, n).join("").trimEnd()}...`; } diff --git a/shells/desktop/src/system-ui/theme.ts b/shells/desktop/src/system-ui/theme.ts index 62982fa..9b3a97c 100644 --- a/shells/desktop/src/system-ui/theme.ts +++ b/shells/desktop/src/system-ui/theme.ts @@ -161,6 +161,12 @@ export interface ChromeMetrics { /** Panel padding around the launcher rows (mirrors startMenu's p-[…]). */ startPadX: number; startPadY: number; + /** A launcher row's width less its label: the row's left and right + * padding, and the icon slot with its gap when the theme draws icons + * (mirrors startItem's pl-[…], pr-[…], gap-[…]). */ + startRowInset: number; + /** What a submenu arrow adds to a row: the 8px arrow and its gap. */ + startArrowW: number; /** Dropdown / context / flyout popup rows and panel padding. */ popupRowH: number; popupSepH: number; @@ -593,6 +599,9 @@ export const CLASSIC_THEME: DesktopTheme = { startLeftW: 0, startPadX: 1, startPadY: 1, + // pl 6 + icon 16 + gap 6 + pr 6; the arrow is gap 6 + 8. + startRowInset: 34, + startArrowW: 14, popupRowH: 18, popupSepH: 8, popupPadX: 1, @@ -895,6 +904,9 @@ export const XP_THEME: DesktopTheme = { startLeftW: 172, startPadX: 1, startPadY: 1, + // pl 8 + icon 16 + gap 7 + pr 8; the arrow is gap 7 + 8. + startRowInset: 39, + startArrowW: 15, popupRowH: 19, popupSepH: 8, popupPadX: 2, @@ -1289,6 +1301,9 @@ export const AQUA_THEME: DesktopTheme = { // a 4px breath above the first and below the last row. startPadX: 0, startPadY: 4, + // pl 21 + pr 16, no icons and no gap; the arrow is 8. + startRowInset: 37, + startArrowW: 8, popupRowH: 19, popupSepH: 9, popupPadX: 0, diff --git a/shells/desktop/src/system-ui/wm.ts b/shells/desktop/src/system-ui/wm.ts index 491ca00..1fa84df 100644 --- a/shells/desktop/src/system-ui/wm.ts +++ b/shells/desktop/src/system-ui/wm.ts @@ -508,6 +508,8 @@ export interface StartLayout { h: number; /** Column split (0 when the theme paints one column beside a rail). */ leftW: number; + /** The places column's width (0 for one column). */ + rightW: number; headerH: number; footerH: number; bodyY: number; @@ -528,21 +530,51 @@ interface StartItem { * programs column and lays `foot` items along the bottom strip, Aqua stacks * one column under the screen bar. Paint (chrome.tsx) and hit testing * (app.tsx) both read these rectangles, so a theme switch can never leave - * one of them behind. */ + * one of them behind. + * + * The panel is as wide as its rows: `rowW(i)` is item i's natural width + * (its label, icon slot, arrow and padding), and a column the theme draws + * at `startLeftW` or `startW` grows to its widest row, up to `maxW` for the + * whole panel. A row wider than what is left past `maxW` has its label cut + * (`fitLabel` in chrome.tsx). Without `rowW` the panel keeps the theme's widths. */ export function startLayout( items: readonly StartItem[], vpH: number, metrics: ChromeMetrics = DEFAULT_METRICS, + rowW: (index: number) => number = () => 0, + maxW = Number.POSITIVE_INFINITY, ): StartLayout { const { startRowH: row, startSepH: sep, - startLeftW: leftW, startPadX: padX, startPadY: padY, } = metrics; const twoColumn = metrics.startHeaderH > 0; - const rightW = metrics.startW - leftW - padX * 2; + const widest = (which: (it: StartItem) => boolean) => + items.reduce((a, it, i) => (it.sep || it.foot || !which(it) ? a : Math.max(a, Math.ceil(rowW(i)))), 0); + + let leftW = metrics.startLeftW; + let rightW = metrics.startW - metrics.startLeftW - padX * 2; + let panelW = metrics.startW; + if (twoColumn) { + const leftMin = leftW; + const rightMin = rightW; + leftW = Math.max(leftMin, widest((it) => it.col !== "right")); + rightW = Math.max(rightMin, widest((it) => it.col === "right")); + // Past the maximum the places column gives back its growth first. + let over = padX * 2 + leftW + rightW - Math.max(metrics.startW, maxW); + if (over > 0) { + const back = Math.min(over, rightW - rightMin); + rightW -= back; + over -= back; + leftW -= Math.min(over, leftW - leftMin); + } + panelW = padX * 2 + leftW + rightW; + } else { + const content = widest(() => true) + padX * 2 + metrics.startRailW; + panelW = Math.max(metrics.startW, Math.min(content, maxW)); + } const height = (which: (it: StartItem) => boolean) => items.filter(which).reduce((a, it) => a + (it.sep ? sep : row), 0); @@ -571,7 +603,7 @@ export function startLayout( const bottomH = height((it) => !it.foot && !!it.bottom); const bottomY = bodyY + bodyH - bottomH; let by = bottomY; - let fx = x + metrics.startW - padX; + let fx = x + panelW - padX; for (let i = 0; i < items.length; i++) { const it = items[i]; const h = it.sep ? sep : row; @@ -611,7 +643,7 @@ export function startLayout( index: i, x: x + padX + metrics.startRailW, y: oy, - w: metrics.startW - padX * 2 - metrics.startRailW, + w: panelW - padX * 2 - metrics.startRailW, h, }); oy += h; @@ -621,9 +653,10 @@ export function startLayout( return { x, y, - w: metrics.startW, + w: panelW, h, leftW: twoColumn ? leftW : 0, + rightW: twoColumn ? rightW : 0, headerH: metrics.startHeaderH, footerH: metrics.startFooterH, bodyY, diff --git a/shells/desktop/test/system-ui.test.ts b/shells/desktop/test/system-ui.test.ts index 69437ab..5d82170 100644 --- a/shells/desktop/test/system-ui.test.ts +++ b/shells/desktop/test/system-ui.test.ts @@ -89,7 +89,7 @@ import { readWindowTitlePrefix, } from "../src/system-ui/pocket-apps.ts"; import { appTitle, applyLang, lw, themeWord, wordsIn } from "../src/system-ui/words.ts"; -import { DESK_LABEL_MAX_W, desktopLabelShift, desktopLabelText } from "../src/system-ui/chrome.tsx"; +import { DESK_LABEL_MAX_W, desktopLabelShift, desktopLabelText, fitLabel } from "../src/system-ui/chrome.tsx"; import about from "../pocket.about.json"; import system from "../pocket.system.json"; import { @@ -566,6 +566,19 @@ describe("content parts", () => { expect(theme.groupBox).toContain(`px-[${m.groupPadX}]`); expect(theme.listWell).toContain(`p-[${m.listPad}]`); } + + }); + + test("a launcher row's inset and arrow are the row's padding, icon slot and gaps", () => { + for (const theme of THEMES) { + const m = theme.metrics; + const cls = theme.startItem(false); + const px = (name: string) => Number(cls.match(new RegExp(`\\b${name}-\\[(\\d+)\\]`))?.[1] ?? 0); + const gap = px("gap"); + const icon = theme.launcherIcons || m.startHeaderH > 0 ? 16 + gap : 0; + expect(m.startRowInset).toBe(px("pl") + px("pr") + icon); + expect(m.startArrowW).toBe(8 + gap); + } }); test("the controls a Pocket app presses carry the focus and pressed variants", () => { @@ -1330,6 +1343,59 @@ describe("aqua theme geometry", () => { expect(launcherHit(aqua.taskStartW + 1, 5, 600, aqua)).toBe(false); }); + test("the XP panel grows to its widest row, and keeps its width when the rows fit", () => { + const xp = XP_THEME.metrics; + const items = [{}, { sep: true }, {}, { col: "right" as const }, { col: "right" as const }, { foot: true }]; + // English rows fit the theme's 172 + 130. + const fits = startLayout(items, 600, xp, () => 120); + expect(fits.w).toBe(xp.startW); + expect(fits.leftW).toBe(xp.startLeftW); + // A places row 166 wide (「マイ ドキュメント」 with its arrow) widens the + // places column, and the frame, the header and the strip with it. + const wide = startLayout(items, 600, xp, (i) => (i === 3 ? 166 : 120)); + expect(wide.leftW).toBe(xp.startLeftW); + expect(wide.rightW).toBe(166); + expect(wide.w).toBe(xp.startPadX * 2 + xp.startLeftW + 166); + const right = wide.rows.find((r) => r.index === 3)!; + expect(right.x + right.w).toBe(wide.x + wide.w - xp.startPadX); + const foot = wide.rows.find((r) => r.index === 5)!; + expect(foot.x + foot.w).toBe(wide.x + wide.w - xp.startPadX); + // A programs row wider than 172 widens the programs column. + const left = startLayout(items, 600, xp, (i) => (i === 0 ? 200 : 120)); + expect(left.leftW).toBe(200); + expect(left.w).toBe(xp.startPadX * 2 + 200 + xp.startW - xp.startLeftW - xp.startPadX * 2); + }); + + test("past its maximum the panel gives back the places column's growth first", () => { + const xp = XP_THEME.metrics; + const items = [{}, { col: "right" as const }]; + const capped = startLayout(items, 600, xp, (i) => (i === 0 ? 220 : 240), 400); + expect(capped.w).toBe(400); + expect(capped.leftW).toBe(220); + expect(capped.rightW).toBe(400 - 220 - xp.startPadX * 2); + // Never narrower than the theme's own panel. + expect(startLayout(items, 600, xp, () => 500, 100).w).toBe(xp.startW); + }); + + test("a one-column launcher grows to its widest row beside its rail", () => { + const items = [{}, { sep: true }, {}]; + expect(startLayout(items, 600, classic, () => 100).w).toBe(classic.startW); + const wide = startLayout(items, 600, classic, (i) => (i === 2 ? 190 : 100)); + expect(wide.w).toBe(190 + classic.startPadX * 2 + classic.startRailW); + expect(wide.rows[1].w).toBe(190); + expect(startLayout(items, 600, classic, () => 900, 300).w).toBe(300); + expect(startLayout(items, 600, aqua, (i) => (i === 0 ? 240 : 100)).w).toBe(240); + }); + + test("a label is cut with ... only when it is wider than its room", () => { + const measure = (text: string) => [...text].length * 10; + expect(fitLabel("マイ ドキュメント", 90, measure)).toBe("マイ ドキュメント"); + expect(fitLabel("マイ ドキュメント", 70, measure)).toBe("マイ ド..."); + // a cut never leaves a space before the dots + expect(fitLabel("マイ ドキュメント", 60, measure)).toBe("マイ..."); + expect(measure(fitLabel("インストールガイド", 55, measure))).toBeLessThanOrEqual(55); + }); + test("the launcher panel hangs from the screen bar", () => { const items = [{}, { sep: true }, {}]; const layout = startLayout(items, 600, aqua); From b53f506afb2887cf7dac636506c00ed5b7d80599 Mon Sep 17 00:00:00 2001 From: "Yifeng \"Evan\" Wang" <7312949+doodlewind@users.noreply.github.com> Date: Wed, 7 Oct 2026 03:35:36 +0800 Subject: [PATCH 2/4] fix(desktop): a task button's title is cut with ... to fit its button Co-Authored-By: Claude Opus 5.5 --- shells/desktop/src/system-ui/chrome.tsx | 13 ++++++++++++- shells/desktop/src/system-ui/theme.ts | 10 ++++++++++ shells/desktop/test/system-ui.test.ts | 5 +++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/shells/desktop/src/system-ui/chrome.tsx b/shells/desktop/src/system-ui/chrome.tsx index f2c65aa..8ef97f9 100644 --- a/shells/desktop/src/system-ui/chrome.tsx +++ b/shells/desktop/src/system-ui/chrome.tsx @@ -162,6 +162,17 @@ export function ScreenBar(props: { /** The task strip: the launcher button, one button per window, the clock * tray. With a screen bar the launcher and the clock live up there and the * strip is a Dock — a centered shelf of icon tiles with running marks. */ +/** A task button's title: whole when it fits the button, else cut with + * "..." the way a taskbar shortens a long window title. */ +function taskTitle(title: string, buttonW: number, active: boolean, theme: DesktopTheme): string { + const slot = theme.fontSlot(active ? "bold" : "ui"); + const measure = (text: string) => { + const ops = getOps(); + return ops.measureText ? ops.measureText(text, slot) : text.length * 7; + }; + return fitLabel(title, buttonW - theme.metrics.taskButtonInset, measure); +} + export function Taskbar(props: { entries: TaskEntry[]; activeId: number; @@ -226,7 +237,7 @@ export function Taskbar(props: { theme={props.theme} bold={entry.id === props.activeId} cls={props.theme.taskText(entry.id === props.activeId)} - t={entry.title} + t={taskTitle(entry.title, props.buttonW, entry.id === props.activeId, props.theme)} /> ) : null} diff --git a/shells/desktop/src/system-ui/theme.ts b/shells/desktop/src/system-ui/theme.ts index 9b3a97c..1db0a6e 100644 --- a/shells/desktop/src/system-ui/theme.ts +++ b/shells/desktop/src/system-ui/theme.ts @@ -142,6 +142,10 @@ export interface ChromeMetrics { /** Horizontal padding of the centered shelf (0 for a left-aligned strip). */ taskPad: number; taskButtonMaxW: number; + /** A task button's width less its title: side padding, the 16px icon and + * its gap (mirrors taskButton's px-[…] and gap-[…]; 0 where buttons show + * no title). */ + taskButtonInset: number; /** Width reserved on the right for the clock tray (0 when the clock lives * in the screen bar). */ taskTrayW: number; @@ -588,6 +592,8 @@ export const CLASSIC_THEME: DesktopTheme = { taskAlign: "left", taskPad: 0, taskButtonMaxW: 160, + // px 4 + icon 16 + gap 4 + px 4 + taskButtonInset: 28, taskTrayW: 72, startX: 2, startW: 182, @@ -893,6 +899,8 @@ export const XP_THEME: DesktopTheme = { taskAlign: "left", taskPad: 0, taskButtonMaxW: 160, + // px 6 + icon 16 + gap 5 + px 6 + taskButtonInset: 33, taskTrayW: 72, startX: 0, startW: 304, @@ -1288,6 +1296,8 @@ export const AQUA_THEME: DesktopTheme = { taskAlign: "center", taskPad: 8, taskButtonMaxW: 44, + // the Dock shows icons alone + taskButtonInset: 0, taskTrayW: 0, startX: 0, startW: 208, diff --git a/shells/desktop/test/system-ui.test.ts b/shells/desktop/test/system-ui.test.ts index 5d82170..930eace 100644 --- a/shells/desktop/test/system-ui.test.ts +++ b/shells/desktop/test/system-ui.test.ts @@ -578,6 +578,11 @@ describe("content parts", () => { const icon = theme.launcherIcons || m.startHeaderH > 0 ? 16 + gap : 0; expect(m.startRowInset).toBe(px("pl") + px("pr") + icon); expect(m.startArrowW).toBe(8 + gap); + if (theme.taskShowLabel) { + const task = theme.taskButton(false); + const tpx = (name: string) => Number(task.match(new RegExp(`\\b${name}-\\[(\\d+)\\]`))?.[1] ?? 0); + expect(m.taskButtonInset).toBe(tpx("px") * 2 + 16 + tpx("gap")); + } } }); From 60c1910740e7353805eb0592ccd8967d3b110cb3 Mon Sep 17 00:00:00 2001 From: "Yifeng \"Evan\" Wang" <7312949+doodlewind@users.noreply.github.com> Date: Wed, 7 Oct 2026 03:37:26 +0800 Subject: [PATCH 3/4] fix(desktop): the About dialog grows with a body longer than four lines Co-Authored-By: Claude Opus 5.5 --- shells/desktop/src/system-ui/app.tsx | 11 ++++++++--- shells/desktop/src/system-ui/programs.tsx | 10 ++++++++++ shells/desktop/test/system-ui.test.ts | 7 +++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/shells/desktop/src/system-ui/app.tsx b/shells/desktop/src/system-ui/app.tsx index c1ec3f3..75f7151 100644 --- a/shells/desktop/src/system-ui/app.tsx +++ b/shells/desktop/src/system-ui/app.tsx @@ -31,7 +31,7 @@ import { Image, View } from "@pocketjs/framework/components"; import { onFrame } from "@pocketjs/framework/lifecycle"; import { virtualNow } from "@pocketjs/framework/clock"; import { connectSvc, type CursorKind, type HostEvent } from "./svc.ts"; -import { applyLang, appTitle, lw } from "./words.ts"; +import { applyLang, appTitle, lang, lw } from "./words.ts"; import { cascadePos, clampMove, @@ -77,7 +77,7 @@ import { type WinCtl, } from "./state.ts"; import { - ABOUT_GEO, + aboutGeo, AboutView, aboutHit, FolderView, @@ -121,8 +121,10 @@ import { } from "./notepad.ts"; import { newMines, reveal, toggleFlag } from "./mines.ts"; import { + DESKTOP_ABOUT, DESKTOP_NAME, POCKET_APPS, + aboutBody, appWindowTitle, pocketAppByPackage, type PocketAppSpec, @@ -860,7 +862,10 @@ export default function App() { kind: "about", title: aboutLabel(), icon: "computer", - geo: centered(ABOUT_GEO.w, ABOUT_GEO.h), + geo: (() => { + const size = aboutGeo(aboutBody(DESKTOP_ABOUT, lang()).length); + return centered(size.w, size.h); + })(), buttons: ["close"], resizable: false, data, diff --git a/shells/desktop/src/system-ui/programs.tsx b/shells/desktop/src/system-ui/programs.tsx index b374483..913f340 100644 --- a/shells/desktop/src/system-ui/programs.tsx +++ b/shells/desktop/src/system-ui/programs.tsx @@ -640,6 +640,16 @@ export function FolderView(props: { // Classic outer sizes; other themes reframe them around the same client // rectangle. About leaves room for the taller antialiased faces' line boxes. export const ABOUT_GEO = { w: 340, h: 260 } as const; +/** The body lines ABOUT_GEO's height holds, and what each further line adds: + * the tallest theme's line box and the dialog's 8px gap. */ +export const ABOUT_BODY_LINES = 4; +export const ABOUT_LINE_STEP = 24; + +/** The About dialog's outer size for a body of `lines` lines: a product's + * About (pocket.about.json) can run longer in one language than another. */ +export function aboutGeo(lines: number): { w: number; h: number } { + return { w: ABOUT_GEO.w, h: ABOUT_GEO.h + Math.max(0, lines - ABOUT_BODY_LINES) * ABOUT_LINE_STEP }; +} export const SHUTDOWN_GEO = { w: 300, h: 176 } as const; /** Dialog push button; armed = pressed face + 1px content nudge. `primary` diff --git a/shells/desktop/test/system-ui.test.ts b/shells/desktop/test/system-ui.test.ts index 930eace..177ce2c 100644 --- a/shells/desktop/test/system-ui.test.ts +++ b/shells/desktop/test/system-ui.test.ts @@ -90,6 +90,7 @@ import { } from "../src/system-ui/pocket-apps.ts"; import { appTitle, applyLang, lw, themeWord, wordsIn } from "../src/system-ui/words.ts"; import { DESK_LABEL_MAX_W, desktopLabelShift, desktopLabelText, fitLabel } from "../src/system-ui/chrome.tsx"; +import { ABOUT_GEO, ABOUT_LINE_STEP, aboutGeo } from "../src/system-ui/programs.tsx"; import about from "../pocket.about.json"; import system from "../pocket.system.json"; import { @@ -1392,6 +1393,12 @@ describe("aqua theme geometry", () => { expect(startLayout(items, 600, aqua, (i) => (i === 0 ? 240 : 100)).w).toBe(240); }); + test("the About dialog grows a line step for each body line past four", () => { + expect(aboutGeo(3)).toEqual(ABOUT_GEO); + expect(aboutGeo(4)).toEqual(ABOUT_GEO); + expect(aboutGeo(5)).toEqual({ w: ABOUT_GEO.w, h: ABOUT_GEO.h + ABOUT_LINE_STEP }); + }); + test("a label is cut with ... only when it is wider than its room", () => { const measure = (text: string) => [...text].length * 10; expect(fitLabel("マイ ドキュメント", 90, measure)).toBe("マイ ドキュメント"); From 76c9e7a0879df36909a8c10975f5d1132aed457f Mon Sep 17 00:00:00 2001 From: "Yifeng \"Evan\" Wang" <7312949+doodlewind@users.noreply.github.com> Date: Wed, 7 Oct 2026 03:41:06 +0800 Subject: [PATCH 4/4] Revert "fix(desktop): a task button's title is cut with ... to fit its button" This reverts commit b53f506afb2887cf7dac636506c00ed5b7d80599. --- shells/desktop/src/system-ui/chrome.tsx | 13 +------------ shells/desktop/src/system-ui/theme.ts | 10 ---------- shells/desktop/test/system-ui.test.ts | 5 ----- 3 files changed, 1 insertion(+), 27 deletions(-) diff --git a/shells/desktop/src/system-ui/chrome.tsx b/shells/desktop/src/system-ui/chrome.tsx index 8ef97f9..f2c65aa 100644 --- a/shells/desktop/src/system-ui/chrome.tsx +++ b/shells/desktop/src/system-ui/chrome.tsx @@ -162,17 +162,6 @@ export function ScreenBar(props: { /** The task strip: the launcher button, one button per window, the clock * tray. With a screen bar the launcher and the clock live up there and the * strip is a Dock — a centered shelf of icon tiles with running marks. */ -/** A task button's title: whole when it fits the button, else cut with - * "..." the way a taskbar shortens a long window title. */ -function taskTitle(title: string, buttonW: number, active: boolean, theme: DesktopTheme): string { - const slot = theme.fontSlot(active ? "bold" : "ui"); - const measure = (text: string) => { - const ops = getOps(); - return ops.measureText ? ops.measureText(text, slot) : text.length * 7; - }; - return fitLabel(title, buttonW - theme.metrics.taskButtonInset, measure); -} - export function Taskbar(props: { entries: TaskEntry[]; activeId: number; @@ -237,7 +226,7 @@ export function Taskbar(props: { theme={props.theme} bold={entry.id === props.activeId} cls={props.theme.taskText(entry.id === props.activeId)} - t={taskTitle(entry.title, props.buttonW, entry.id === props.activeId, props.theme)} + t={entry.title} /> ) : null} diff --git a/shells/desktop/src/system-ui/theme.ts b/shells/desktop/src/system-ui/theme.ts index 1db0a6e..9b3a97c 100644 --- a/shells/desktop/src/system-ui/theme.ts +++ b/shells/desktop/src/system-ui/theme.ts @@ -142,10 +142,6 @@ export interface ChromeMetrics { /** Horizontal padding of the centered shelf (0 for a left-aligned strip). */ taskPad: number; taskButtonMaxW: number; - /** A task button's width less its title: side padding, the 16px icon and - * its gap (mirrors taskButton's px-[…] and gap-[…]; 0 where buttons show - * no title). */ - taskButtonInset: number; /** Width reserved on the right for the clock tray (0 when the clock lives * in the screen bar). */ taskTrayW: number; @@ -592,8 +588,6 @@ export const CLASSIC_THEME: DesktopTheme = { taskAlign: "left", taskPad: 0, taskButtonMaxW: 160, - // px 4 + icon 16 + gap 4 + px 4 - taskButtonInset: 28, taskTrayW: 72, startX: 2, startW: 182, @@ -899,8 +893,6 @@ export const XP_THEME: DesktopTheme = { taskAlign: "left", taskPad: 0, taskButtonMaxW: 160, - // px 6 + icon 16 + gap 5 + px 6 - taskButtonInset: 33, taskTrayW: 72, startX: 0, startW: 304, @@ -1296,8 +1288,6 @@ export const AQUA_THEME: DesktopTheme = { taskAlign: "center", taskPad: 8, taskButtonMaxW: 44, - // the Dock shows icons alone - taskButtonInset: 0, taskTrayW: 0, startX: 0, startW: 208, diff --git a/shells/desktop/test/system-ui.test.ts b/shells/desktop/test/system-ui.test.ts index 177ce2c..ade02c3 100644 --- a/shells/desktop/test/system-ui.test.ts +++ b/shells/desktop/test/system-ui.test.ts @@ -579,11 +579,6 @@ describe("content parts", () => { const icon = theme.launcherIcons || m.startHeaderH > 0 ? 16 + gap : 0; expect(m.startRowInset).toBe(px("pl") + px("pr") + icon); expect(m.startArrowW).toBe(8 + gap); - if (theme.taskShowLabel) { - const task = theme.taskButton(false); - const tpx = (name: string) => Number(task.match(new RegExp(`\\b${name}-\\[(\\d+)\\]`))?.[1] ?? 0); - expect(m.taskButtonInset).toBe(tpx("px") * 2 + 16 + tpx("gap")); - } } });