Something went wrong. Try again.
A history browser for git, a replacement for gitk, drawn with Vulkan on Wayland.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! The find bar, under the commit list, as gitk has it: what to find —//! commits containing text, touching paths, adding or removing a string,//! changing lines that match — the text, how it matches and in which//! fields, buttons for the matches before and after the selected commit,//! and how many there are.
const std = @import("std");const aerie = @import("aerie");const Canvas = aerie.gpu.Canvas;const Rect = Canvas.Rect;
const Query = @import("../find/Query.zig");const Text = @import("Text.zig");const TextField = @import("TextField.zig");const theme = @import("theme.zig");const l10n = @import("../l10n.zig");
const FindBar = @This();
rect: Rect = .{ .x = 0, .y = 0, .width = 0, .height = 0 },/// Where each control is, laid out by `draw` and kept for hit-testing.controls: [@typeInfo(Control).@"enum".field_names.len]Rect = @splat(.{ .x = 0, .y = 0, .width = 0, .height = 0 }),hovered: ?Control = null,/// Where the count of matches is written.status_buf: [64]u8 = undefined,
pub const Control = enum { previous, next, mode, field, matching, fields };
pub fn controlAt(b: *const FindBar, x: f32, y: f32) ?Control { for (b.controls, 0..) |r, i| if (r.contains(x, y)) return @fromBackingInt(@intCast(i)); return null;}
/// What the bar says at its right: how the find is going.pub const Status = union(enum) { idle, searching, matches: usize, /// Something to say: a pattern that is no pattern, git's complaint. message: []const u8,};
pub fn draw(b: *FindBar, canvas: *Canvas, t: *Text, field: *TextField, query: Query, focused: bool, status: Status, scale: f32) !void { const r = b.rect; try canvas.rect(r.x, r.y, r.width, r.height, theme.current.bar_background); const saved = canvas.pushClip(r); defer canvas.setClip(saved); const pad = @round(8 * scale); const gap = @round(6 * scale); const inset = @round(3 * scale); const h = r.height - 2 * inset; const baseline = r.y + t.baselineIn(.ui, r.height); var x = r.x + pad;
x += try t.draw(canvas, .ui, l10n.text("find-label"), x, baseline, theme.current.dim_foreground) + gap; const arrow_w = @round(24 * scale); x += try b.button(canvas, t, .{ .x = x, .y = r.y + inset, .width = arrow_w, .height = h }, "↑", .previous, baseline) + @round(2 * scale); x += try b.button(canvas, t, .{ .x = x, .y = r.y + inset, .width = arrow_w, .height = h }, "↓", .next, baseline) + gap; x += try b.dropdown(canvas, t, x, r.y + inset, h, l10n.text(query.mode.label()), .mode, baseline) + gap;
// The field takes what the rest leave, but not less than a little. const status_text = switch (status) { .idle => "", .searching => l10n.text("find-searching"), .matches => |n| blk: { var w: std.Io.Writer = .fixed(&b.status_buf); l10n.write(&w, "find-matches", &.{l10n.num("count", n)}) catch {}; break :blk w.buffered(); }, .message => |m| m, }; const right_w = try dropdownWidth(t, l10n.text(query.matching.label()), scale) + gap + (if (query.mode == .containing) try dropdownWidth(t, l10n.text(query.fields.label()), scale) + gap else 0) + statusWidth(scale) + pad; const field_w = @max(@round(120 * scale), r.right() - x - right_w - gap); const field_rect: Rect = .{ .x = x, .y = r.y + inset, .width = field_w, .height = h }; b.controls[@backingInt(Control.field)] = field_rect; try field.draw(canvas, t, field_rect, focused, scale); x += field_w + gap;
x += try b.dropdown(canvas, t, x, r.y + inset, h, l10n.text(query.matching.label()), .matching, baseline) + gap; if (query.mode == .containing) { x += try b.dropdown(canvas, t, x, r.y + inset, h, l10n.text(query.fields.label()), .fields, baseline) + gap; } else b.controls[@backingInt(Control.fields)] = .{ .x = 0, .y = 0, .width = 0, .height = 0 }; _ = try t.draw(canvas, .ui, status_text, x, baseline, if (status == .message) theme.current.error_foreground else theme.current.dim_foreground);}
/// Room for what the bar says at its right, the same whatever it says, so/// that nothing moves as a count comes and goes.fn statusWidth(scale: f32) f32 { return @round(170 * scale);}
fn dropdownWidth(t: *Text, label: []const u8, scale: f32) !f32 { return try t.measure(.ui, label) + try t.measure(.ui, " ▾") + 2 * @round(6 * scale);}
/// A button showing `label` and a ▾, which opens a menu of choices.fn dropdown(b: *FindBar, canvas: *Canvas, t: *Text, x: f32, y: f32, h: f32, label: []const u8, c: Control, baseline: f32) !f32 { var buf: [64]u8 = undefined; const s = std.fmt.bufPrint(&buf, "{s} ▾", .{label}) catch label; const w = try t.measure(.ui, s) + 2 * @round(6 * (h / 20)); _ = try b.button(canvas, t, .{ .x = x, .y = y, .width = w, .height = h }, s, c, baseline); return w;}
fn button(b: *FindBar, canvas: *Canvas, t: *Text, rect: Rect, label: []const u8, c: Control, baseline: f32) !f32 { b.controls[@backingInt(c)] = rect; if (b.hovered == c) try canvas.rect(rect.x, rect.y, rect.width, rect.height, theme.current.control_hover); const w = try t.measure(.ui, label); _ = try t.draw(canvas, .ui, label, rect.x + @round((rect.width - w) / 2), baseline, theme.current.foreground); return rect.width;}