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
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ Data flow at runtime: timers poll `/api/now-playing`, `/api/state`, `/api/themes

- **A `/` in a scene window title crashes GTK at app_start on Linux.** Keep the branded "SUB/WAVE" slash out of `app.zon` / `shell_windows` titles; it's fine in `runWithOptions.window_title`.
- **The SDK cannot resize a live window.** Per-mode window shapes apply at next launch only.
- **A `hidden_inset_tall` masthead must pad `insets.left` AND `insets.right`.** The window-control cluster sits on a different edge per platform — macOS traffic lights lead, Windows min/max/close trail — so `onChrome` maps both into `chrome_leading` / `chrome_trailing` and `player-top.native` spacers both ends. Dropping the trailing one put the gear button under the DWM caption buttons on Windows, and fed the host's per-present caption-colour sampler (which reads the pixel 8px leading of the cluster and pushes it through `DWMWA_CAPTION_COLOR` + `USE_IMMERSIVE_DARK_MODE`) an antialiased glyph edge instead of flat header — the caption buttons flickered.
- **The FFT spectrum feed only emits while a window is visibly on screen** (occlusion gate in the SDK). A flat visualizer from an app launched in the background is not a bug — activate the app first.
- SDK 0.8.4 still has **no OS media-controls surface** (no MPNowPlayingInfoCenter / MPRemoteCommandCenter / MPRIS / hardware media keys — re-checked at the 0.8.4 upgrade). The substitutes are the tray extra, the in-window keyboard transport, and the background track toast: 0.8.4 added `fx.showNotification`, so a track change while the app is backgrounded posts a desktop notification. It is opt-in (back panel → NOTIFICATIONS) and the whole decision lives in `Model.shouldNotifyTrack` — pure, because the effect is inert and unrecorded under fake execution, so no test can observe the call itself.
- SDK 0.8.4 also has **no audio output-device API** — the platform seam is load/play/pause/stop/seek/volume, with no enumeration and no device property, so there is no in-app "play through these speakers" picker to build. Route it at the OS (`pavucontrol`/PipeWire, Windows volume mixer; macOS has nothing native). The upstream request lives in `docs/sdk-audio-device-request.md`.
Expand Down
10 changes: 8 additions & 2 deletions src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -163,12 +163,18 @@ fn onLifecycle(event: native_sdk.LifecycleEvent) ?Msg {
};
}

// Hidden-titlebar chrome geometry → model (masthead pads around the traffic
// lights). All-zero on standard chrome / fullscreen / non-macOS.
// Hidden-titlebar chrome geometry → model (the masthead pads around the window
// controls). BOTH horizontal edges are mapped because the cluster sits on a
// different one per platform: macOS puts the traffic lights left, Windows puts
// min/max/close right. Dropping `right` left the gear button underneath the DWM
// caption buttons on Windows, which also fed the host's caption-colour sampler
// a glyph edge instead of flat header — see CLAUDE.md's gotchas. All-zero on
// standard chrome and in fullscreen.
fn onChrome(chrome: native_sdk.platform.WindowChrome) ?Msg {
return Msg{ .chrome_changed = .{
.top = chrome.insets.top,
.leading = chrome.insets.left,
.trailing = chrome.insets.right,
} };
}

Expand Down
29 changes: 27 additions & 2 deletions src/model.zig
Original file line number Diff line number Diff line change
Expand Up @@ -165,10 +165,14 @@ pub const DayTab = struct {
label: []const u8,
};

/// Payload for chrome_changed (hidden-titlebar insets, macOS).
/// Payload for chrome_changed (hidden-titlebar insets). The window-control
/// cluster lands on the edge its platform puts it on — macOS reports the
/// traffic lights as `leading`, Windows reports min/max/close as `trailing` —
/// so the masthead pads BOTH and the unused edge is honestly zero.
pub const ChromeInsets = struct {
top: f32 = 0,
leading: f32 = 0,
trailing: f32 = 0,
};

// ------------------------------------------------------------------ model
Expand Down Expand Up @@ -317,9 +321,11 @@ pub const Model = struct {
sheet: Sheet = .none,
mini_open: bool = false,

// hidden-titlebar chrome insets (macOS traffic lights)
// hidden-titlebar chrome insets (macOS traffic lights lead, Windows
// min/max/close trails)
chrome_top: f32 = 0,
chrome_leading: f32 = 0,
chrome_trailing: f32 = 0,

// sleep timer (wall-clock deadline; 0 = off)
sleep_deadline_ms: i64 = 0,
Expand Down Expand Up @@ -2731,6 +2737,7 @@ pub fn update(model: *Model, msg: Msg, fx: *Effects) void {
.chrome_changed => |c| {
model.chrome_top = c.top;
model.chrome_leading = c.leading;
model.chrome_trailing = c.trailing;
},

// ------------------------------------------------------ request slip
Expand Down Expand Up @@ -4357,6 +4364,24 @@ test "update check: newer tag arms the notice, older clears it, failures keep st
try testing.expect(m.update_available());
}

test "chrome insets land on both edges, so the masthead clears controls on any platform" {
var fx = Effects.init(testing.allocator);
defer fx.deinit();
fx.executor = .fake;
var m: Model = .{};
// macOS shape: traffic lights lead, nothing trails.
update(&m, .{ .chrome_changed = .{ .top = 52, .leading = 78, .trailing = 0 } }, &fx);
try testing.expectEqual(@as(f32, 52), m.chrome_top);
try testing.expectEqual(@as(f32, 78), m.chrome_leading);
try testing.expectEqual(@as(f32, 0), m.chrome_trailing);
// Windows shape: the min/max/close cluster trails instead. Dropping it is
// what put the gear button under the DWM caption buttons, whose colour the
// host samples from the pixel just leading of the cluster every present.
update(&m, .{ .chrome_changed = .{ .top = 32, .leading = 0, .trailing = 138 } }, &fx);
try testing.expectEqual(@as(f32, 0), m.chrome_leading);
try testing.expectEqual(@as(f32, 138), m.chrome_trailing);
}

test "log_dir_value reports the diagnostic log folder" {
const m: Model = .{};
// Unopened in the suite, so it reads empty rather than crashing.
Expand Down
6 changes: 6 additions & 0 deletions src/views/player-top.native
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@
<button variant="ghost" icon="settings" label="Back panel" on-press="open_panel"/>
<tooltip anchor="below">Back panel</tooltip>
</stack>
<!-- Windows: clears the DWM min/max/close cluster, which overlays this
band under hidden_inset_tall. Zero on macOS (lights lead instead) and
on standard chrome. The row's gap also keeps the pixel the host
samples for the caption colour on flat header instead of the gear's
antialiased edge. -->
<spacer width="{chrome_trailing}"/>
</row>
<separator/>

Expand Down
Loading