// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! The file list: the files the selected commit changes, under an entry for //! its details. Clicking one scrolls the diff to it, and the one the diff //! is showing is marked. const std = @import("std"); const aerie = @import("aerie"); const Canvas = aerie.gpu.Canvas; const Rect = Canvas.Rect; const Change = @import("../history/Change.zig"); const Text = @import("Text.zig"); const theme = @import("theme.zig"); const Scrollbar = @import("Scrollbar.zig"); const l10n = @import("../l10n.zig"); const FileList = @This(); rect: Rect = .{ .x = 0, .y = 0, .width = 0, .height = 0 }, /// The scrollbar's track, beside the rows. vtrack: Rect = .{ .x = 0, .y = 0, .width = 0, .height = 0 }, row_height: f32 = 1, scroll: f32 = 0, /// What a row is: the details, or a file by its index. pub const Entry = union(enum) { details, file: usize, }; pub fn count(files: usize) usize { return files + 1; } pub fn entryAt(l: *const FileList, y: f32, files: usize) ?Entry { if (y < l.rect.y or y >= l.rect.bottom()) return null; const row: usize = @intFromFloat(@floor((y - l.rect.y + l.scroll) / l.row_height)); if (row >= count(files)) return null; return if (row == 0) .details else .{ .file = row - 1 }; } pub fn scrollbar(l: *const FileList, files: usize) Scrollbar { return .{ .track = l.vtrack, .axis = .vertical, .content = @as(f32, @floatFromInt(count(files))) * l.row_height, .view = l.rect.height, .offset = l.scroll }; } pub fn scrollBy(l: *FileList, dy: f32, files: usize) bool { const before = l.scroll; l.scroll = std.math.clamp(l.scroll + dy, 0, l.scrollbar(files).maxOffset()); return l.scroll != before; } /// The letter `gitk` and `git status` give each kind of change. fn letter(s: Change.Status) []const u8 { return switch (s) { .added => "A", .deleted => "D", .modified => "M", .renamed => "R", .copied => "C", .type_changed => "T", .unmerged => "U", .unknown => "?", }; } /// Draws the list. `current` is the entry the diff is showing. pub fn draw(l: *const FileList, canvas: *Canvas, text: *Text, files: []const Change.File, current: ?Entry, dragging: bool, scale: f32) !void { try l.scrollbar(files.len).draw(canvas, dragging, scale); try canvas.rect(l.rect.x, l.rect.y, l.rect.width, l.rect.height, theme.current.list_background); const saved = canvas.pushClip(l.rect); defer canvas.setClip(saved); const pad = @round(6 * scale); const letter_w = @round(16 * scale); const n = count(files.len); const first: usize = @intFromFloat(@floor(l.scroll / l.row_height)); const end = @min(n, first + @as(usize, @intFromFloat(@ceil(l.rect.height / l.row_height))) + 1); for (first..end) |row| { const top = l.rect.y + @as(f32, @floatFromInt(row)) * l.row_height - l.scroll; const entry: Entry = if (row == 0) .details else .{ .file = row - 1 }; const is_current = if (current) |c| std.meta.eql(c, entry) else false; if (is_current) try canvas.rect(l.rect.x, top, l.rect.width, l.row_height, theme.current.selection_background); const fg = if (is_current) theme.current.selection_foreground else theme.current.foreground; const dim = if (is_current) theme.current.selection_foreground else theme.current.dim_foreground; const baseline = top + text.baselineIn(.ui, l.row_height); switch (entry) { .details => _ = try text.draw(canvas, .ui, l10n.text("files-details"), l.rect.x + pad, baseline, dim), .file => |i| { const f = files[i]; _ = try text.draw(canvas, .mono, letter(f.status), l.rect.x + pad, top + text.baselineIn(.mono, l.row_height), dim); _ = try text.draw(canvas, .ui, f.path, l.rect.x + pad + letter_w, baseline, fg); }, } } }