diff options
Diffstat (limited to 'src/gui/gui.zig')
| -rw-r--r-- | src/gui/gui.zig | 84 |
1 files changed, 81 insertions, 3 deletions
diff --git a/src/gui/gui.zig b/src/gui/gui.zig index ab281cb6..491373a8 100644 --- a/src/gui/gui.zig +++ b/src/gui/gui.zig @@ -1857,6 +1857,12 @@ const Gui = struct { scroll_edge: []pardes.Cell = &.{}, // the row that just left the pane scroll_edge_len: u16 = 0, + /// The cursor's blink (blinkNow): when the frame drawn last arrived, + /// a redraw owed for an edge, and this frame's cursor mode and alpha. + frame_arrival_ns: u64 = 0, + blink_due: bool = false, + no_blink: bool = false, + cursor_quad_alpha: f32 = 0, scene_tex: ?*c.SDL_GPUTexture = null, scene_tex_w: u32 = 0, scene_tex_h: u32 = 0, @@ -2652,6 +2658,8 @@ fn localSession( .test_hz = if (std.c.getenv("PARDES_TEST_HZ")) |hz| @max(1, std.fmt.parseInt(u64, std.mem.span(hz), 10) catch 60) else 60, }; defer shell.shutdownPtys(); + // A virtual clock never blinks: goldens and feel series hold still. + g.no_blink = shell.test_clock != null; const host = shell.host(); core.host = host; @@ -4186,7 +4194,14 @@ fn waitInput(ctx: ?*anyopaque, timeout_ms: u32) void { @intCast(@max(1, refreshNs(g) / std.time.ns_per_ms)) else 16; - const ms: c_int = if (latency_fd >= 0) 0 else if (minimized) -1 else if (timeout_ms != 0) @min(@as(c_int, @intCast(@min(timeout_ms, std.math.maxInt(c_int)))), if (polls) poll_ms else std.math.maxInt(c_int)) else if (polls) poll_ms else -1; + const waited: c_int = if (latency_fd >= 0) 0 else if (minimized) -1 else if (timeout_ms != 0) @min(@as(c_int, @intCast(@min(timeout_ms, std.math.maxInt(c_int)))), if (polls) poll_ms else std.math.maxInt(c_int)) else if (polls) poll_ms else -1; + // The cursor's blink is the shell's to time (Chrome.cursor_idle_ms): + // a frame a loop through an edge, else a wake at the next one; none + // while the window is minimized or unfocused. A virtual clock never + // blinks, so goldens hold still. + const blink = if (s.test_clock != null or minimized) null else blinkNow(g, if (s.surface) |frame| &frame.chrome else null); + const blink_ms: ?c_int = if (blink) |b| (if (b.edge) 1 else if (b.next_ns) |next| @as(c_int, @intCast(@min(std.math.maxInt(c_int), next / std.time.ns_per_ms + 1))) else null) else null; + const ms: c_int = if (blink_ms) |b| (if (waited < 0) b else @min(waited, b)) else waited; // The wait is the 9P connections' turn with the core. pardes.turn.rest(); const got = c.SDL_WaitEventTimeout(&sev, ms); @@ -4196,6 +4211,9 @@ fn waitInput(ctx: ?*anyopaque, timeout_ms: u32) void { // (PARDES_TEST_HZ), events or not, as a real display's present // would; otherwise a wait that ran out moves it exactly to the // core's wake. + // Through an edge, or woken for the next: the shell redraws the last + // frame with the new alpha, no core render (ยง5.5 level B). + if (blink_ms != null and (blink.?.edge or !got)) g.blink_due = true; if (s.test_clock) |*virtual| { if (core.continuous()) { const display = virtual.* + std.time.ns_per_s / s.test_hz; @@ -4280,6 +4298,27 @@ fn pollFrame(ctx: ?*anyopaque) void { // early is on time: the wait that paces it is in whole milliseconds). if (g.post.animating(core.settings.shader_animation) and !core.needs_frame and s.presented and c.SDL_GetTicksNS() -| g.post.last_ns + std.time.ns_per_ms >= refreshNs(g)) redrawPost(g, s.gpa); + // A blink edge: the last frame again, the cursor's alpha moved on. + if (g.blink_due and !core.needs_frame and s.presented) if (s.surface) |frame| { + g.blink_due = false; + blink_redraws += 1; + s.presented = renderFrame(g, s.gpa, core, frame, core.settings.debug) catch false; + }; +} + +/// Frames drawn for a blink alone, with no core render (tests, tracing). +var blink_redraws: u64 = 0; + +/// The blink now, from the frame's time since input and how long ago it +/// arrived; null where nothing blinks (off, idle, an unfocused window). +fn blinkNow(g: *const Gui, chrome: ?*const pardes.Chrome) ?pardes.animation.Blink { + const frame = chrome orelse return null; + if (g.no_blink or frame.cursor_glide & 2 == 0 or frame.cursor_idle_ms == std.math.maxInt(u32)) return null; + if (c.SDL_GetWindowFlags(g.window) & c.SDL_WINDOW_INPUT_FOCUS == 0) return null; + const since = @as(u64, frame.cursor_idle_ms) * std.time.ns_per_ms + (monoNs() -| g.frame_arrival_ns); + // Relative to now: `next_ns` is how long until the next edge. + const b = pardes.animation.Blink.at(since, 0); + return .{ .alpha = b.alpha, .edge = b.edge, .next_ns = if (b.next_ns) |next| next -| since else null }; } /// One refresh of the display the window is on, in ns; a frame of the @@ -4347,6 +4386,7 @@ fn present(ctx: ?*anyopaque, surface: *const pardes.Surface) void { frame_started_ns = monoNs(); const frame = @constCast(surface); s.surface = frame; + g.frame_arrival_ns = monoNs(); const scene_requested = g.post.ready(); if (!scene_requested) g.scene_failures = 0; s.presented = renderFrame( @@ -5366,8 +5406,13 @@ fn renderFrame( const layout = fixedCellLayout(g); const chrome = &surface.chrome; var groups = makeGroups(surface.panelTracks(), surface.hasPanelDiff()); - // Cursors stand still while panes move, as they always have. - const cursors_shown = groups.tracks == 0; + // Cursors stand still while panes move, as they always have; while the + // focused one glides or blinks, it is the overlay's quad instead. One + // or the other every frame (none only at a blink's off half). + const blink_alpha: f32 = if (blinkNow(g, chrome)) |b| b.alpha else 1; + const quad_mode = chrome.cursor_glide & 2 != 0 and (chrome.cursor_glide & 1 != 0 or blink_alpha < 1); + g.cursor_quad_alpha = if (quad_mode) chrome.cursor_alpha * blink_alpha else 0; + const cursors_shown = groups.tracks == 0 and !quad_mode; var overlay_count = buildOverlay(g, core, surface, sw, sh, debug_on); if (overlay_count != 0 and !uploadOverlayGpu(g, cmd, overlay_count)) overlay_count = 0; @@ -8505,10 +8550,43 @@ fn buildOverlay( if (g.overlay_vertices.len == 0 or sw == 0 or sh == 0) return 0; var builder = OverlayBuilder{ .vertices = g.overlay_vertices, .win_w = @floatFromInt(sw), .win_h = @floatFromInt(sh) }; appendWorkspacePet(g, core, surface, &builder); + appendCursorGlide(g, surface, &builder); appendTouchOverlay(g, &builder, debug_on); return @intCast(builder.len); } +/// G3: the focused cursor while it glides or blinks, its quad from the core +/// (Chrome.cursor_quad, grid cells) in its cursor colour; a little less than +/// solid, so the glyphs it passes over stay legible. +fn appendCursorGlide(g: *Gui, surface: *const pardes.Surface, builder: *OverlayBuilder) void { + const chrome = &surface.chrome; + if (g.cursor_quad_alpha <= 0.001) return; + if (surface.panelTracks().len != 0) return; + const layout = fixedCellLayout(g); + var px: [4][2]f32 = undefined; + var centre: [2]f32 = .{ 0, 0 }; + for (&px, 0..) |*point, i| { + point.* = .{ chrome.cursor_quad[i * 2] * layout.w, chrome.cursor_quad[i * 2 + 1] * layout.h }; + centre[0] += point[0] / 4; + centre[1] += point[1] / 4; + } + // Its colour: the cursor the cells would draw nearest the quad. + var boxes: [max_cursors]CursorBox = undefined; + var rgb: [3]u8 = chrome.fg orelse fg_default; + var nearest: f32 = std.math.inf(f32); + for (boxes[0..cursorBoxes(g, surface, layout, builder.win_h, &boxes)]) |box| { + const dx = (box.x0 + box.x1) / 2 - centre[0]; + const dy = (box.y0 + box.y1) / 2 - centre[1]; + if (dx * dx + dy * dy < nearest) { + nearest = dx * dx + dy * dy; + rgb = box.rgb; + } + } + const color: OverlayColor = .{ .r = @as(f32, @floatFromInt(rgb[0])) / 255, .g = @as(f32, @floatFromInt(rgb[1])) / 255, .b = @as(f32, @floatFromInt(rgb[2])) / 255, .a = g.cursor_quad_alpha }; + builder.addTri(px[0][0], px[0][1], px[1][0], px[1][1], px[2][0], px[2][1], color); + builder.addTri(px[0][0], px[0][1], px[2][0], px[2][1], px[3][0], px[3][1], color); +} + fn appendWorkspacePet(g: *Gui, core: ?*const pardes.Pardes, surface: *const pardes.Surface, builder: *OverlayBuilder) void { const p = core orelse return; const kind: pet.Kind = @enumFromInt(@intFromEnum(p.settings.pet)); |
