Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/architecture/09-settings-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions src/version.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"
1 change: 0 additions & 1 deletion ui/src/design/Preview.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
24 changes: 9 additions & 15 deletions ui/src/settings-schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' };

Expand Down Expand Up @@ -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' },
Expand All @@ -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.
Expand All @@ -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 },
Expand Down
2 changes: 1 addition & 1 deletion ui/tests/a11y.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down
5 changes: 3 additions & 2 deletions ui/tests/prefs.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
11 changes: 5 additions & 6 deletions ui/tests/schema.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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(['']);
});

Expand All @@ -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(
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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)');
Expand Down
19 changes: 9 additions & 10 deletions ui/tests/settings.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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');
Expand All @@ -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.
Expand Down Expand Up @@ -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');
Expand Down
2 changes: 1 addition & 1 deletion ui/tests/shell.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
Expand Down