From 0da5450416d32555fce1aa816ac30a5fe2e56443 Mon Sep 17 00:00:00 2001 From: Max <112043822+Maxaubert@users.noreply.github.com> Date: Sat, 10 Oct 2026 05:05:44 +0200 Subject: [PATCH] chore: bring #424 onto main as tested in the combined build; 0.24.20 The branch is main plus exactly the combined test build's state after this PR (conflicts with the PRs merged before it resolved as tested), version 0.24.20. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011zPmivSAQeivGUdBjTsaMu --- docs/architecture/09-settings-ui.md | 4 ++-- src/version.h | 4 ++-- ui/src/design/Preview.svelte | 1 - ui/src/settings-schema.js | 24 +++++++++--------------- ui/tests/a11y.spec.js | 2 +- ui/tests/prefs.spec.js | 5 +++-- ui/tests/schema.spec.js | 11 +++++------ ui/tests/settings.spec.js | 19 +++++++++---------- ui/tests/shell.spec.js | 2 +- 9 files changed, 32 insertions(+), 40 deletions(-) diff --git a/docs/architecture/09-settings-ui.md b/docs/architecture/09-settings-ui.md index ee7fba50..df402f8b 100644 --- a/docs/architecture/09-settings-ui.md +++ b/docs/architecture/09-settings-ui.md @@ -73,8 +73,8 @@ the reply invalid and the page waits forever. ## The schema-driven app -The page is generated from `groups` in `ui/src/settings-schema.js`. Seven groups: Hotkeys, Zoom, -View, Screen, then below a divider Preferences, Tray menu and About. Each group has a label, an +The page is generated from `groups` in `ui/src/settings-schema.js`. Six groups: Hotkeys, Zoom, +View, then below a divider Preferences (with the Screen light card), Tray menu and About. Each group has a label, an icon, a banner description and cards of rows; each row names its ini key, type, label, description and default. `Settings.svelte` is the shell (title bar, sidebar, banner, save capsule, search), and `controls/SettingRow.svelte` renders every row by type: diff --git a/src/version.h b/src/version.h index 616d9056..5378472d 100644 --- a/src/version.h +++ b/src/version.h @@ -4,7 +4,7 @@ #define WIND_VER_MAJOR 0 #define WIND_VER_MINOR 24 -#define WIND_VER_PATCH 19 +#define WIND_VER_PATCH 20 // String form for logs/snapshot/UI. Keep in sync with the numeric parts above. -#define WIND_VERSION_STR "0.24.19" +#define WIND_VERSION_STR "0.24.20" diff --git a/ui/src/design/Preview.svelte b/ui/src/design/Preview.svelte index 248d22dd..a769db52 100644 --- a/ui/src/design/Preview.svelte +++ b/ui/src/design/Preview.svelte @@ -18,7 +18,6 @@ { id: 'hotkeys', label: 'Hotkeys', icon: 'hotkeys' }, { id: 'zoom', label: 'Zoom', icon: 'zoom' }, { id: 'view', label: 'View', icon: 'view' }, - { id: 'screen', label: 'Screen', icon: 'screen' }, ]; const bottom = [ { id: 'prefs', label: 'Preferences', icon: 'general' }, diff --git a/ui/src/settings-schema.js b/ui/src/settings-schema.js index fa6c3eae..5d9f7251 100644 --- a/ui/src/settings-schema.js +++ b/ui/src/settings-schema.js @@ -20,8 +20,6 @@ // its label, no symbols or product names. const hybrid = { key: 'model', eq: 'hybrid' }; -// The Transform engine follows the pointer directly, so the mouse speed and smoothing sliders do nothing there. -const notTransform = { key: 'model', ne: 'transform' }; const engineOpts = ['auto', 'transform', 'render']; const engineLabels = { auto: 'Auto', transform: 'Transform', render: 'Render' }; @@ -100,8 +98,8 @@ export const groups = [ cards: [ { caption: 'Speed', rows: [ { key: 'panSpeed', type: 'slider', label: 'Arrow key speed', keywords: ['pan', 'pan speed', 'arrows', 'move', 'scroll', 'keyboard', 'how fast', 'velocity', 'shift view', 'rate'], desc: 'How fast the arrow keys move the view.', min: 0.25, max: 4, step: 0.05, def: 1.0, unit: 'times' }, - { key: 'cursorSensitivity', type: 'slider', label: 'Mouse speed', keywords: ['sensitivity', 'pointer speed', 'cursor speed', 'dpi', 'pan', 'follow', 'tracking speed', 'how fast', 'velocity', 'mouse sensitivity', 'rate'], desc: 'Speed of the view in the Render engine and in mouse-locked games. In the Transform engine the view follows the pointer directly, so this has no effect there.', min: 0.25, max: 4, step: 0.05, def: 1.0, unit: 'times', showIf: notTransform }, - { key: 'cursorSmoothing', type: 'slider', adv: true, label: 'Pan smoothing', keywords: ['inertia', 'smooth', 'smoothing', 'glide', 'momentum', 'lag', 'delay', 'damping', 'gentle', 'filter', 'mouse'], desc: 'Adds gentle inertia when the view moves in the Render engine. In the Transform engine the view follows the pointer directly, so this has no effect there.', min: 0, max: 0.95, step: 0.05, def: 0.4, showIf: notTransform }, + { key: 'cursorSensitivity', type: 'slider', label: 'Mouse speed', keywords: ['sensitivity', 'pointer speed', 'cursor speed', 'dpi', 'pan', 'follow', 'tracking speed', 'how fast', 'velocity', 'mouse sensitivity', 'rate'], desc: 'How fast the view moves with the mouse in the Render engine, in Inspect mode and in mouse-locked games. With Transform the view otherwise follows the pointer directly.', min: 0.25, max: 4, step: 0.05, def: 1.0, unit: 'times' }, + { key: 'cursorSmoothing', type: 'slider', label: 'Pan smoothing', keywords: ['inertia', 'smooth', 'smoothing', 'glide', 'momentum', 'lag', 'delay', 'damping', 'gentle', 'filter', 'mouse'], desc: 'Gentle inertia when the view pans in the Render engine, in Inspect mode and in mouse-locked games. With Transform the view otherwise follows the pointer directly.', min: 0, max: 0.95, step: 0.05, def: 0.4 }, ] }, { caption: 'Pointer', rows: [ { key: 'mouseAlign', type: 'select', label: 'Pointer position', keywords: ['mouse position', 'mouse', 'cursor', 'pointer', 'where', 'centred', 'centered', 'center', 'centre', 'middle', 'edge', 'edges', 'within the edges', 'align', 'alignment', 'lock to center', 'follow mode', 'placement'], desc: 'Where the mouse pointer sits while the view moves.', options: ['0', '1'], optionLabels: { '0': 'Centred', '1': 'Within the edges' }, def: '0' }, @@ -124,18 +122,8 @@ export const groups = [ ] }, ] }, - // Screen (#288): warmth and brightness, one DWM colour matrix (the render engine applies it in its shader). - { id: 'screen', label: 'Screen', icon: 'screen', - desc: 'Warmth and brightness of the screen.', - cards: [ - { caption: 'Screen light', rows: [ - { key: 'colorWarmPct', type: 'slider', label: 'Warmth', keywords: ['night light', 'blue light', 'yellow', 'orange', 'warm', 'colour temperature', 'color temperature', 'temperature', 'eye strain', 'tint', 'filter', 'evening', 'sleep', 'amber', 'red', 'colour', 'color', 'reduce blue', 'kelvin'], desc: 'Makes the screen warmer.', min: 0, max: 100, step: 5, def: 0, unit: '%' }, - { key: 'colorDimPct', type: 'slider', label: 'Brightness', keywords: ['dim', 'dark', 'darker', 'dimmer', 'light', 'luminance', 'backlight', 'night', 'screen', 'intensity', 'contrast', 'colour', 'color', 'lower', 'reduce'], desc: 'Dims the whole screen.', min: 1, max: 100, step: 1, def: 100, unit: '%' }, - ] }, - ] }, - { id: 'prefs', label: 'Preferences', icon: 'general', bottom: true, - desc: 'Appearance, profiles and troubleshooting.', + desc: 'Appearance, screen light, profiles and troubleshooting.', cards: [ { caption: 'General', rows: [ // Global UI-only key (uiPalette): the built-in theme, shared by Settings and the tray menu. One row of mini window cards, right-aligned like the other controls. @@ -144,6 +132,12 @@ export const groups = [ // Global UI-only key: shows the advanced rows of every page. { key: 'showAdvanced', type: 'toggle', label: 'Show advanced settings', keywords: ['expert', 'extra', 'more', 'hidden', 'options', 'all settings', 'power user', 'developer', 'reveal', 'show all', 'additional'], desc: 'Shows extra options on every page.', def: 0 }, ] }, + // Screen light (#288): warmth and brightness, one DWM colour matrix (the render engine applies it + // in its shader). Was its own page; two sliders read better next to the other preferences (#423). + { caption: 'Screen light', rows: [ + { key: 'colorWarmPct', type: 'slider', label: 'Warmth', keywords: ['night light', 'blue light', 'yellow', 'orange', 'warm', 'colour temperature', 'color temperature', 'temperature', 'eye strain', 'tint', 'filter', 'evening', 'sleep', 'amber', 'red', 'colour', 'color', 'reduce blue', 'kelvin'], desc: 'Makes the screen warmer.', min: 0, max: 100, step: 5, def: 0, unit: '%' }, + { key: 'colorDimPct', type: 'slider', label: 'Brightness', keywords: ['dim', 'dark', 'darker', 'dimmer', 'light', 'luminance', 'backlight', 'night', 'screen', 'intensity', 'contrast', 'colour', 'color', 'lower', 'reduce'], desc: 'Dims the whole screen.', min: 1, max: 100, step: 1, def: 100, unit: '%' }, + ] }, { caption: 'Troubleshooting', rows: [ // Always visible: these are not advanced settings. { key: 'diagnostics', type: 'toggle', label: 'Frame time logging', keywords: ['log', 'logging', 'fps', 'performance', 'diagnostics', 'debug', 'stutter', 'profiling', 'telemetry', 'trace', 'measure', 'benchmark', 'file'], desc: 'Writes frame times to a log file.', def: 0 }, diff --git a/ui/tests/a11y.spec.js b/ui/tests/a11y.spec.js index b8e635fa..4e626281 100644 --- a/ui/tests/a11y.spec.js +++ b/ui/tests/a11y.spec.js @@ -6,7 +6,7 @@ // them is invisible in a screenshot. import { test, expect } from '@playwright/test'; -const GROUPS = ['hotkeys', 'zoom', 'view', 'screen', 'prefs', 'tray', 'about']; +const GROUPS = ['hotkeys', 'zoom', 'view', 'prefs', 'tray', 'about']; test.beforeEach(async ({ page }) => { await page.addInitScript(() => { diff --git a/ui/tests/prefs.spec.js b/ui/tests/prefs.spec.js index ad377cc5..917f2d09 100644 --- a/ui/tests/prefs.spec.js +++ b/ui/tests/prefs.spec.js @@ -371,9 +371,10 @@ test('New profile: Tab walks name, Cancel, New, Duplicate current, and Cancel cr test('Troubleshooting is always there, and the advanced switch is the last row of General', async ({ page }) => { await page.goto('/'); await go(page, 'prefs'); - await expect(page.locator('main .cap')).toHaveText(['General', 'Troubleshooting']); + await expect(page.locator('main .cap')).toHaveText(['General', 'Screen light', 'Troubleshooting']); await expect(page.locator('main .card').first().locator('.row .label')).toHaveText(['Theme', 'Profile', 'Show advanced settings']); - await expect(page.locator('main .card').nth(1).locator('.row .label')).toHaveText(['Frame time logging', 'Export diagnostics', 'Open settings file']); + await expect(page.locator('main .card').nth(1).locator('.row .label')).toHaveText(['Warmth', 'Brightness']); + await expect(page.locator('main .card').nth(2).locator('.row .label')).toHaveText(['Frame time logging', 'Export diagnostics', 'Open settings file']); }); test('prefs files contain no em-dash', () => { diff --git a/ui/tests/schema.spec.js b/ui/tests/schema.spec.js index c83031c8..04a1b95a 100644 --- a/ui/tests/schema.spec.js +++ b/ui/tests/schema.spec.js @@ -24,7 +24,7 @@ const OLD_BIND_KEYS = [ const NEW_KEYS = ['uiPalette', '__profiles', '__diagnostics', '__openIni', 'trayPerf', 'showAdvanced']; test('group ids, order and shape follow the spec', () => { - expect(groups.map((g) => g.id)).toEqual(['hotkeys', 'zoom', 'view', 'screen', 'prefs', 'tray', 'about']); + expect(groups.map((g) => g.id)).toEqual(['hotkeys', 'zoom', 'view', 'prefs', 'tray', 'about']); expect(groups.filter((g) => g.bottom).map((g) => g.id)).toEqual(['prefs', 'tray', 'about']); // below the divider for (const g of groups) { expect(typeof g.label).toBe('string'); @@ -37,8 +37,7 @@ test('group ids, order and shape follow the spec', () => { expect(cap('hotkeys')).toEqual(['Zoom', 'Extra keys']); expect(cap('zoom')).toEqual(['Level and speed', 'Easing', 'Engine']); expect(cap('view')).toEqual(['Speed', 'Pointer', 'Typing and focus']); - expect(cap('screen')).toEqual(['Screen light']); - expect(cap('prefs')).toEqual(['General', 'Troubleshooting']); + expect(cap('prefs')).toEqual(['General', 'Screen light', 'Troubleshooting']); expect(cap('tray')).toEqual(['']); }); @@ -58,7 +57,7 @@ test('a captioned section has two or more rows, and the advanced rows are the on if (base.length) expect(base.length, g.id + ' / ' + c.caption + ' (advanced off)').toBeGreaterThanOrEqual(2); } expect(allRows.filter((r) => r.adv).map((r) => r.key).sort()).toEqual( - ['cursorSmoothing', 'engineAcrylic', 'engineDesktop', 'engineGame', 'engineOther', 'lockApps', 'model', 'noSwallowApps', 'smoothZoomAccel', 'smoothZoomRamp'].sort()); + ['engineAcrylic', 'engineDesktop', 'engineGame', 'engineOther', 'lockApps', 'model', 'noSwallowApps', 'smoothZoomAccel', 'smoothZoomRamp'].sort()); // Troubleshooting is never advanced, and the extra keys carry their switch key. for (const k of ['diagnostics', '__diagnostics', '__openIni']) expect(allRows.find((r) => r.key === k).adv).toBeUndefined(); expect(Object.fromEntries(allRows.filter((r) => r.onKey).map((r) => [r.key, r.onKey]))).toEqual( @@ -104,7 +103,7 @@ test('rows land in the groups the spec names', () => { expect(where('txSamplingMode')).toBe('view'); expect(where('lockApps')).toBe('view'); expect(where('trackAlign')).toBe('view'); - expect(where('colorDimPct')).toBe('screen'); + expect(where('colorDimPct')).toBe('prefs'); expect(where('__profiles')).toBe('prefs'); expect(where('showAdvanced')).toBe('prefs'); expect(where('diagnostics')).toBe('prefs'); @@ -224,7 +223,7 @@ test('about page shows the logo and link; controls read on the dark surface', as await expect(page.getByText('Barely there. Everywhere.')).toBeVisible(); await page.getByRole('button', { name: 'Star on GitHub' }).click(); expect(await page.evaluate(() => window.__calls)).toEqual(['repo']); - await page.goto('/controls.html?group=screen'); + await page.goto('/controls.html?group=prefs'); const sl = ctl(page, 'colorWarmPct').locator('input'); expect(await css(sl, 'accent-color')).not.toBe(''); expect(await css(ctl(page, 'colorWarmPct').locator('.val'), 'color')).toBe('rgb(242, 242, 242)'); diff --git a/ui/tests/settings.spec.js b/ui/tests/settings.spec.js index 48e7cb69..da41d699 100644 --- a/ui/tests/settings.spec.js +++ b/ui/tests/settings.spec.js @@ -49,11 +49,11 @@ test('Settings opens on Hotkeys; the sidebar is four pages, a divider, then thre await page.goto('/'); await expect(page.locator('h1')).toHaveText('Hotkeys'); await expect(page.locator('.side .it.sel')).toContainText('Hotkeys'); - await expect(page.locator('.side nav:not(.bottom) .it')).toHaveText([/Hotkeys/, /Zoom/, /View/, /Screen/]); + await expect(page.locator('.side nav:not(.bottom) .it')).toHaveText([/Hotkeys/, /Zoom/, /View/]); await expect(page.locator('.side nav.bottom .it')).toHaveText([/Preferences/, /Tray menu/, /About/]); await expect(page.locator('.side .lbl')).toHaveCount(0); await expect(page.locator('.side .it[data-g="advanced"]')).toHaveCount(0); // no Advanced tab - for (const [g, title] of [['zoom', 'Zoom'], ['view', 'View'], ['screen', 'Screen'], ['prefs', 'Preferences'], + for (const [g, title] of [['zoom', 'Zoom'], ['view', 'View'], ['prefs', 'Preferences'], ['tray', 'Tray menu'], ['about', 'About'], ['hotkeys', 'Hotkeys']]) { await go(page, g); await expect(page.locator('h1')).toHaveText(title); @@ -70,12 +70,10 @@ test('pages show their sections; a section with nothing to show is dropped', asy await expect(page.locator('main .cap')).toHaveText(['Level and speed']); // Easing and Engine are advanced await go(page, 'view'); await expect(page.locator('main .cap')).toHaveText(['Speed', 'Pointer', 'Typing and focus']); - await go(page, 'screen'); - await expect(page.locator('main .cap')).toHaveText(['Screen light']); await go(page, 'prefs'); - await expect(page.locator('main .cap')).toHaveText(['General', 'Troubleshooting']); + await expect(page.locator('main .cap')).toHaveText(['General', 'Screen light', 'Troubleshooting']); // Never fewer than two rows in a section while the advanced switch is off. - for (const g of ['hotkeys', 'zoom', 'view', 'screen', 'prefs']) { + for (const g of ['hotkeys', 'zoom', 'view', 'prefs']) { await go(page, g); const counts = await page.locator('main .card').evaluateAll((cs) => cs.map((c) => c.querySelectorAll('.row').length)); for (const n of counts) expect(n, g).toBeGreaterThanOrEqual(2); @@ -123,7 +121,8 @@ test('Show advanced settings reveals the advanced rows inline, with no marker, a await go(page, 'zoom'); for (const k of ['model', 'engineGame', 'smoothZoomAccel', 'smoothZoomRamp']) await expect(key(page, k)).toHaveCount(0); await go(page, 'view'); - for (const k of ['cursorSmoothing', 'lockApps']) await expect(key(page, k)).toHaveCount(0); + await expect(key(page, 'lockApps')).toHaveCount(0); + await expect(key(page, 'cursorSmoothing')).toBeVisible(); // Pan smoothing is a plain row (#423) await expect(key(page, 'mouseAlign')).toBeVisible(); // Troubleshooting is always there; the switch itself is a global key, so it never raises the capsule. await go(page, 'prefs'); @@ -136,7 +135,7 @@ test('Show advanced settings reveals the advanced rows inline, with no marker, a for (const k of ['model', 'engineGame', 'smoothZoomAccel', 'smoothZoomRamp']) await expect(key(page, k)).toBeVisible(); await expect(page.locator('main .cap')).toHaveText(['Level and speed', 'Easing', 'Engine']); await go(page, 'view'); - for (const k of ['cursorSmoothing', 'lockApps']) await expect(key(page, k)).toBeVisible(); + await expect(key(page, 'lockApps')).toBeVisible(); await go(page, 'hotkeys'); await expect(key(page, 'noSwallowApps')).toBeVisible(); // No marker: an advanced row reads exactly like a plain one. @@ -196,9 +195,9 @@ test('Typing and focus: caret on, focus off by default; a toggle writes at once' await expect(page.locator('.capsule')).toContainText('1 unsaved change'); }); -test('Screen page: only warmth and brightness, neutral by default (no warmth, full brightness)', async ({ page }) => { +test('Screen light (in Preferences): warmth and brightness, neutral by default (no warmth, full brightness)', async ({ page }) => { await page.goto('/'); - await go(page, 'screen'); + await go(page, 'prefs'); await expect(page.locator('main input[type=range]')).toHaveCount(2); await expect(key(page, 'colorWarmPct').locator('input[type=range]')).toHaveValue('0'); await expect(key(page, 'colorDimPct').locator('input[type=range]')).toHaveValue('100'); diff --git a/ui/tests/shell.spec.js b/ui/tests/shell.spec.js index ea262451..be33042b 100644 --- a/ui/tests/shell.spec.js +++ b/ui/tests/shell.spec.js @@ -40,7 +40,7 @@ test('the title bar carries only the window buttons and the sidebar has a divide await expect(page.locator('header.tb button')).toHaveCount(3); await expect(page.locator('[data-theme-cycle]')).toHaveCount(0); await expect(page.locator('.side .lbl')).toHaveCount(0); - await expect(page.locator('.side nav:not(.bottom) .it')).toHaveCount(4); + await expect(page.locator('.side nav:not(.bottom) .it')).toHaveCount(3); await expect(page.locator('.side nav.bottom .it')).toHaveCount(3); expect(await css(page.locator('.side nav.bottom'), 'border-top-width')).toBe('1px'); });