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: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ User-facing changes to Beam are documented in this file.

## [Unreleased]

### Fixed

- HUD and recording controls start with an explicitly transparent, fixed native surface on Windows, without the system resize frame. Ordinary HUD/Recorder switches no longer reset the native backdrop; preparation still restores its material when it closes.

### Changed

- Refreshed the repository README with current Beam workflows, website imagery, platform downloads, CLI guidance, and the community's non-expiring Discord invite.
Expand Down
8 changes: 6 additions & 2 deletions apps/desktop/electron/lifecycle/renderer-setup.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,14 @@ function createRendererSetup({
height: initialSize.height,
frame: false,
transparent: true,
backgroundColor: '#00000000',
// Transparent overlays must be non-resizable from their first native frame.
// Changing that after ready-to-show can leave an opaque Windows surround.
thickFrame: false,
alwaysOnTop: true,
icon: appIconPath,
resizable: true,
maximizable: true,
resizable: false,
maximizable: false,
hasShadow: false,
visualEffectState: 'active',
show: false,
Expand Down
4 changes: 3 additions & 1 deletion apps/desktop/electron/window/window-controller.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ class WindowController {

setMode(mode, { restoreMaximized = true } = {}) {
if (!['hud', 'recorder', 'preparation'].includes(mode)) throw new Error(`Mode de fenêtre invalide: ${mode}`);
const changesPreparationMaterial = (this.mode === 'preparation') !== (mode === 'preparation');
if (this.mode === 'recorder' && mode !== 'recorder') {
this.rememberRecorderPosition();
this.flushRecorderPosition();
Expand All @@ -209,7 +210,8 @@ class WindowController {
}
if (this.mode === 'preparation' && mode !== 'preparation') this.window.emit('capture-preparation-ended');
this.mode = mode;
applyPreparationMaterial(this.window, this.platform, mode === 'preparation');
// Do not reset the Windows native backdrop during ordinary HUD/Recorder switches.
if (changesPreparationMaterial) applyPreparationMaterial(this.window, this.platform, mode === 'preparation');
this.hudDragging = false;
if (mode === 'hud') this.hudSize = this.readHudSize();
this.applySizeConstraints();
Expand Down
2 changes: 2 additions & 0 deletions docs/electron_window.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ The shared overlay entry selects camera, Quick Snip settings or Crop Bar through

The transparent main window is controlled by `apps/desktop/electron/window/window-controller.cjs` and owns the HUD and Recorder modes. The editor uses a separate opaque window created by `apps/desktop/electron/window/editor-window.cjs`; this separation is required for native window animations and Windows Snap Layouts.

Create the shared HUD/Recorder window with an explicitly transparent native background, `resizable: false`, `maximizable: false` and `thickFrame: false` before its first paint. Changing a resizable transparent window only after `ready-to-show` can invalidate transparency on Windows. Native bounds changes still resize the fixed overlay for its current mode; opaque editor windows retain their native frame. Toggle preparation vibrancy/acrylic only when entering or leaving preparation, never during ordinary HUD/Recorder switches or repeated mode reports.

| Mode | Bounds / behavior | Interaction |
| ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `hud` | Compact at `672 × 268` (temporarily taller while a popover is open): a `640 × 236` HUD card plus 16 px of room on every edge for its border and shadow. The canonical dimensions live in `preferences.json` as `hudWindow`; startup normalization restores them when missing or unexpected. It returns to its prior HUD position after Recorder mode. Dragging is bounded by the physical display bounds, not the taskbar-excluded work area. | macOS/Windows: transparent pixels pass through; renderer enables mouse handling only over interactive HUD elements. Linux: forced X11/XWayland, fully interactive compact native bounds; HUD popovers scroll within them. |
Expand Down
32 changes: 32 additions & 0 deletions test/renderer-setup-window.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,38 @@ test('loads the HUD from its session port and trusts only that renderer origin',
assert.equal(setup.isTrustedRenderer('http://localhost:6508/html/untrusted.html'), false);
});

test('creates the shared HUD/Recorder with a transparent native backing before its first paint', () => {
const fixture = createFixture();
assert.equal(fixture.window.options.transparent, true);
assert.equal(fixture.window.options.backgroundColor, '#00000000');
assert.equal(
fixture.calls.some(([name]) => name === 'showInactive'),
false,
);
assert.equal(
fixture.calls.some(([name]) => name === 'setResizable'),
false,
);
});

test('starts transparent overlays non-resizable instead of correcting them after ready-to-show', () => {
const fixture = createFixture();
assert.equal(fixture.window.options.resizable, false);
assert.equal(fixture.window.options.maximizable, false);
fixture.window.emit('ready-to-show');
assert.ok(fixture.calls.filter(([name]) => name === 'setResizable').every((call) => call[1] === false));
assert.ok(fixture.calls.filter(([name]) => name === 'setMaximizable').every((call) => call[1] === false));
});

test('excludes the Windows native frame and shadow from the transparent recorder surface', () => {
const fixture = createFixture();
assert.equal(fixture.window.options.frame, false);
assert.equal(fixture.window.options.thickFrame, false);
assert.equal(fixture.window.options.hasShadow, false);
assert.equal(fixture.window.options.width, 672);
assert.equal(fixture.window.options.height, 268);
});

test('packaged renderers cannot obtain media permissions from a development origin', () => {
const setup = createRendererSetup({
app: { isPackaged: true },
Expand Down
55 changes: 55 additions & 0 deletions test/window-controller.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -799,6 +799,61 @@ test('failed HUD wake remains hidden and reports the failure', async (t) => {
assert.equal(errors[0][0], '[HUD wake]');
});

for (const platform of ['linux', 'darwin', 'win32'])
test(`ordinary HUD/Recorder mode changes leave native backdrop state alone on ${platform}`, () => {
const win = fakeWindow();
win.setVibrancy = (value) => win.calls.push(['vibrancy', value]);
win.setBackgroundMaterial = (value) => win.calls.push(['material', value]);
const controller = new WindowController(win, {
platform,
screenModule: {
getCursorScreenPoint: () => ({ x: 960, y: 540 }),
getDisplayNearestPoint: () => ({ id: 1, workArea: { x: 0, y: 0, width: 1920, height: 1040 } }),
},
});
controller.setMode('hud');
controller.setMode('recorder');
controller.setMode('recorder');
controller.setMode('hud');
assert.deepEqual(
win.calls.filter(([name]) => ['vibrancy', 'material'].includes(name)),
[],
);
assert.ok(win.calls.some(([name, bounds]) => name === 'bounds' && bounds.width === 352 && bounds.height === 88));
assert.ok(win.calls.filter(([name]) => name === 'resizable').every((call) => call[1] === false));
});

for (const platform of ['darwin', 'win32'])
test(`preparation applies and removes its native backdrop exactly once on ${platform}`, () => {
const win = fakeWindow();
win.setVibrancy = (value) => win.calls.push(['vibrancy', value]);
win.setBackgroundMaterial = (value) => win.calls.push(['material', value]);
const controller = new WindowController(win, {
platform,
screenModule: {
getCursorScreenPoint: () => ({ x: 960, y: 540 }),
getDisplayNearestPoint: () => ({ id: 1, workArea: { x: 0, y: 0, width: 1920, height: 1040 } }),
},
});
controller.setMode('preparation');
controller.setMode('preparation');
controller.setMode('recorder');
controller.setMode('recorder');
controller.setMode('hud');
assert.deepEqual(
win.calls.filter(([name]) => ['vibrancy', 'material'].includes(name)),
platform === 'win32'
? [
['material', 'acrylic'],
['material', 'none'],
]
: [
['vibrancy', 'under-window'],
['vibrancy', null],
],
);
});

for (const platform of ['linux', 'darwin', 'win32'])
test(`preparation stays compact, protected and topmost on ${platform} and restores native material`, () => {
const win = fakeWindow();
Expand Down
Loading