const std = @import("std"); const dvui = @import("dvui"); const entypo = dvui.entypo; const Backend = @import("SDLBackend"); var gpa_instance = std.heap.GeneralPurposeAllocator(.{}){}; const gpa = gpa_instance.allocator(); const vsync = true; pub fn main() !void { var backend = try Backend.init(.{ .width = 480, .height = 360, .vsync = vsync, .title = "File Picker", }); defer backend.deinit(); var win = try dvui.Window.init(@src(), 0, gpa, backend.backend()); defer win.deinit(); win.theme = &dvui.Adwaita.light; var ctx = try Ctx.init(); defer ctx.deinit(); loop: while (true) { var nstime = win.beginWait(backend.hasEvent()); try win.begin(nstime); backend.clear(); const quit = try backend.addAllEvents(&win); if (quit or .exit == try ctx.frame()) break :loop; const end_micros = try win.end(.{}); backend.setCursor(win.cursorRequested()); backend.renderPresent(); const wait_event_micros = win.waitTime(end_micros, null); backend.waitEventTimeout(wait_event_micros); } } pub const Ctx = struct { path: []u8, filename: []u8, entries: std.ArrayListUnmanaged(std.fs.IterableDir.Entry) = .{}, pub fn init() !Ctx { var ret = Ctx{ .path = try gpa.alloc(u8, std.fs.MAX_PATH_BYTES), .filename = try gpa.alloc(u8, std.fs.MAX_PATH_BYTES), }; const path = ret.path[0..std.fs.MAX_PATH_BYTES]; ret.path = try std.os.realpath(".", path); ret.filename.len = 0; try ret.list_files(); return ret; } pub fn deinit(ctx: *Ctx) void { for (ctx.entries.items) |entry| gpa.free(entry.name); ctx.entries.deinit(gpa); gpa.free(ctx.filename.ptr[0..std.fs.MAX_PATH_BYTES]); gpa.free(ctx.path.ptr[0..std.fs.MAX_PATH_BYTES]); } const bg = dvui.Options{ .background = true, .expand = .horizontal, .corner_radius = dvui.Rect.all(0), }; const opts = dvui.Options{ .corner_radius = dvui.Rect.all(0), }; pub const ExitStatus = enum { ok, exit }; pub fn frame(ctx: *Ctx) !ExitStatus { const box = try dvui.box(@src(), .vertical, bg.override(.{ .expand = .both })); defer box.deinit(); try ctx.draw_top_path(); if (try ctx.draw_bottom_filename()) |status| return status; try ctx.draw_file_list(); return .ok; } pub fn list_files(ctx: *Ctx) !void { for (ctx.entries.items) |entry| gpa.free(entry.name); ctx.entries.clearRetainingCapacity(); var dir = try std.fs.openIterableDirAbsolute(ctx.path, .{}); defer dir.close(); var iterator = dir.iterate(); while (try iterator.next()) |iter_entry| { const entry = try ctx.entries.addOne(gpa); entry.name = try gpa.dupe(u8, iter_entry.name); entry.kind = iter_entry.kind; } const Entry = std.fs.IterableDir.Entry; const items = ctx.entries.items; const sort = struct { pub fn less_than_name(_: void, l: Entry, r: Entry) bool { return std.mem.lessThan(u8, l.name, r.name); } pub fn less_than_kind(_: void, l: Entry, r: Entry) bool { return @intFromEnum(l.kind) < @intFromEnum(r.kind); } }; std.mem.sort(Entry, items, {}, sort.less_than_name); std.mem.sort(Entry, items, {}, sort.less_than_kind); } fn draw_top_path(ctx: *Ctx) !void { var box = try dvui.box(@src(), .horizontal, bg); defer box.deinit(); if (try dvui.button(@src(), "..", opts)) { try ctx.goto(".."); } const button_pressed = try dvui.button(@src(), "go", opts.override(.{ .gravity_x = 1 })); if (button_pressed) { try ctx.goto(ctx.path); } const text = try dvui.textEntry(@src(), .{ .text = ctx.path }, bg); defer text.deinit(); } fn goto(ctx: *Ctx, dir: []const u8) !void { var tmp_buf = std.mem.zeroes([std.fs.MAX_PATH_BYTES]u8); var tmp = std.heap.FixedBufferAllocator.init(&tmp_buf); const path = try std.fs.path.resolve(tmp.allocator(), &.{ ctx.path, dir }); ctx.path.len = path.len; const buf = ctx.path.ptr[0..std.fs.MAX_PATH_BYTES]; @memcpy(buf, &tmp_buf); try ctx.list_files(); } fn draw_bottom_filename(ctx: *Ctx) !?ExitStatus { var box = try dvui.box(@src(), .horizontal, opts.override(.{ .expand = .horizontal, .gravity_y = 1, })); defer box.deinit(); const button_enabled = ctx.filename.len != 0; var button_opts = opts.override(.{ .gravity_x = 1 }); if (button_enabled) { button_opts.color_style = .accent; } else { button_opts.color_style = .control; button_opts.color_hover = button_opts.color(.fill); } if (try dvui.button(@src(), "ok", button_opts) and button_enabled) { std.debug.print("{s}/{s}\n", .{ ctx.path, ctx.filename }); return .exit; } const tl = try dvui.textEntry(@src(), .{ .text = ctx.filename }, bg.override(.{ .expand = .horizontal, })); defer tl.deinit(); return null; } fn draw_file_list(ctx: *Ctx) !void { var box = try dvui.box(@src(), .vertical, .{ .expand = .both, .margin = dvui.Rect.all(12), }); defer box.deinit(); var scroll = try dvui.scrollArea(@src(), .{}, .{ .expand = .both }); defer scroll.deinit(); files: for (ctx.entries.items, 0..) |entry, i| { const name, const icon, const color_style = @as( struct { []const u8, []const u8, dvui.Theme.ColorStyle }, switch (entry.kind) { .directory => .{ "folder", entypo.folder, .control }, .file => .{ "file", entypo.text_document, .window }, else => .{ "other", entypo.help, .content }, }, ); const id = opts.override(.{ .id_extra = i }); const wide = id.override(.{ .color_style = color_style, .expand = .horizontal, .margin = dvui.Rect.all(0), }); const clicked = clicked: { var button = dvui.ButtonWidget.init(@src(), .{}, wide); defer button.deinit(); try button.install(); button.processEvents(); try button.drawBackground(); const inner = try dvui.box(@src(), .horizontal, id); defer inner.deinit(); try dvui.icon(@src(), name, icon, id.override(.{ .gravity_y = 0.4 })); try dvui.labelNoFmt(@src(), entry.name, id); try button.drawFocus(); break :clicked button.clicked(); }; if (clicked) switch (entry.kind) { .directory => { try ctx.goto(entry.name); break :files; }, else => { ctx.filename.len = entry.name.len; std.mem.copy(u8, ctx.filename, entry.name); }, }; } } };