From 513792346e8705a17ed4072014bfaf8601799145 Mon Sep 17 00:00:00 2001 From: bubio Date: Wed, 10 Jun 2026 18:42:54 +0900 Subject: [PATCH] Make status-bar volume slider clickable and focus-independent The volume control used Nuklear's nk_slide_float, which only changes value while dragging its small knob (track clicks are ignored, and at full volume the knob is jammed against the right edge, nearly impossible to grab). It also went read-only whenever the status-bar window was not the top window, e.g. while a dialog was open, since nk_slider_float honors NK_WINDOW_ROM. The neighboring mute icon kept working because it polls ctx.input directly, producing the "icon works but slider doesn't" symptom. Replace it with a hand-drawn track hit-tested via ctx.input, mirroring the mute icon: a click or drag anywhere along the bar sets the level, drag keeps tracking once started in the cell, and the control is unaffected by window focus/ROM state. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/ui.zig | 49 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/src/ui.zig b/src/ui.zig index 5d74466..563948e 100644 --- a/src/ui.zig +++ b/src/ui.zig @@ -568,17 +568,50 @@ fn drawVolumeIcon(ctx: *c.nk_context) void { } } +// Master-volume bar. Rendered and hit-tested by hand instead of nk_slide_float: +// the stock Nuklear slider only moves while you drag its small knob (a click on +// the track is ignored, and at full volume the knob sits jammed against the +// right edge where it is nearly impossible to grab). It also goes dead whenever +// the status-bar window is flagged read-only — which Nuklear does to every +// non-top window, e.g. while a dialog is open. We instead poll ctx.input +// directly, exactly like drawVolumeIcon, so a click or drag anywhere along the +// bar sets the level and the control keeps working regardless of window focus. fn drawVolumeSlider(ctx: *c.nk_context) void { + const canvas = c.nk_window_get_canvas(ctx); + var b: c.struct_nk_rect = undefined; + _ = c.nk_widget(&b, ctx); + const cfg = &cz.c.np2cfg; - const cur = cfg.vol_master; - var vf: f32 = @floatFromInt(cur); - vf = c.nk_slide_float(ctx, 0, vf, vol_max, 1); - const nv: u8 = @intFromFloat(vf); - if (nv != cur) { - cfg.vol_master = nv; - if (nv > 0) pre_mute_vol = nv; - cz.usa_sound_apply_volumes(); + + // Track geometry: a thin horizontal rail inset slightly from the cell edges + // so the knob does not overflow at the 0 / 100 extremes. + const pad: f32 = 6; + const tx = b.x + pad; + const tw = b.w - 2 * pad; + + // A press that began inside the cell (and is still held) drives the value + // from the pointer's x — this covers both a single click and a drag. + if (tw > 0 and c.nk_input_has_mouse_click_down_in_rect(&ctx.input, c.NK_BUTTON_LEFT, b, c.nk_true) != 0) { + const rel = std.math.clamp((ctx.input.mouse.pos.x - tx) / tw, 0.0, 1.0); + const nv: u8 = @intFromFloat(@round(rel * vol_max)); + if (nv != cfg.vol_master) { + cfg.vol_master = nv; + if (nv > 0) pre_mute_vol = nv; + cz.usa_sound_apply_volumes(); + } } + + const ratio: f32 = @as(f32, @floatFromInt(cfg.vol_master)) / vol_max; + const cy = b.y + b.h / 2.0; + const track_h: f32 = 4; + const track_y = cy - track_h / 2.0; + const knob_r: f32 = 5; + const kx = tx + tw * ratio; + + // Unfilled rail, filled portion up to the current level, then the knob. + c.nk_fill_rect(canvas, c.nk_rect(tx, track_y, tw, track_h), 2, c.nk_rgb(0x50, 0x50, 0x50)); + c.nk_fill_rect(canvas, c.nk_rect(tx, track_y, tw * ratio, track_h), 2, c.nk_rgb(0xC0, 0xC0, 0xC0)); + c.nk_fill_circle(canvas, c.nk_rect(kx - knob_r, cy - knob_r, knob_r * 2, knob_r * 2), c.nk_rgb(0xE8, 0xE8, 0xE8)); } // Mouse icon reflecting capture state: filled orange while the pointer is