diff --git a/build.zig b/build.zig index b400a66..b27f198 100644 --- a/build.zig +++ b/build.zig @@ -1,6 +1,8 @@ const std = @import("std"); const builtin = @import("builtin"); +const optimize_deps = .ReleaseFast; + pub fn build(b: *std.Build) void { const release = b.option(bool, "package_release", "Build all release targets") orelse false; const tracy_enabled = b.option(bool, "enable_tracy", "Enable tracy client library (default: no)") orelse false; @@ -227,14 +229,14 @@ pub fn build_exe( const syntax_dep = b.dependency("syntax", .{ .target = target, - .optimize = optimize, + .optimize = optimize_deps, .use_tree_sitter = use_tree_sitter, }); const syntax_mod = syntax_dep.module("syntax"); const thespian_dep = b.dependency("thespian", .{ .target = target, - .optimize = optimize, + .optimize = optimize_deps, .enable_tracy = tracy_enabled, }); @@ -316,16 +318,13 @@ pub fn build_exe( const renderer_mod = blk: { if (gui) switch (target.result.os.tag) { .windows => { - const direct2d_dep = b.lazyDependency("direct2d", .{}) orelse break :blk tui_renderer_mod; - - const win32_dep = direct2d_dep.builder.dependency("win32", .{}); - const win32_mod = win32_dep.module("zigwin32"); + const win32_dep = b.lazyDependency("win32", .{}) orelse break :blk tui_renderer_mod; + const win32_mod = win32_dep.module("win32"); const gui_mod = b.createModule(.{ .root_source_file = b.path("src/win32/gui.zig"), .imports = &.{ .{ .name = "build_options", .module = options_mod }, .{ .name = "win32", .module = win32_mod }, - .{ .name = "ddui", .module = direct2d_dep.module("ddui") }, .{ .name = "cbor", .module = cbor_mod }, .{ .name = "thespian", .module = thespian_mod }, .{ .name = "input", .module = input_mod }, @@ -333,6 +332,7 @@ pub fn build_exe( .{ .name = "vaxis", .module = vaxis_mod }, .{ .name = "color", .module = color_mod }, .{ .name = "gui_config", .module = gui_config_mod }, + .{ .name = "tracy", .module = tracy_mod }, }, }); gui_mod.addIncludePath(b.path("src/win32")); diff --git a/build.zig.zon b/build.zig.zon index e38c3f8..d04bd53 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -10,16 +10,16 @@ .hash = "1220930a42f8da3fb7f723e3ad3f6dcc6db76327dd8d26274566423192d53e91b2bb", }, .tracy = .{ - .url = "https://github.com/neurocyte/zig-tracy/archive/58999b786089e5319dd0707f6afbfca04c6340e7.tar.gz", - .hash = "1220a2c8f8db1b5265458ac967ea1f7cc0a8ddcd1d774df3b73d86c4f529aadbfb94", + .url = "https://github.com/neurocyte/zig-tracy/archive/e04e31c64498149a324491b8534758e6af43a5c2.tar.gz", + .hash = "1220d0fb2bff7b453dbb39d1db3eb472b6680e2564f2b23b0e947671be47bbdd188f", }, .dizzy = .{ .url = "https://github.com/neurocyte/dizzy/archive/455d18369cbb2a0458ba70be919cd378338d695e.tar.gz", .hash = "1220220dbc7fe91c1c54438193ca765cebbcb7d58f35cdcaee404a9d2245a42a4362", }, .thespian = .{ - .url = "https://github.com/neurocyte/thespian/archive/b7ace533875217f2b002f0ac8dfe32169cab7138.tar.gz", - .hash = "1220d25045c26cc8d2d46995d670ce10b64d0e3a3fa23cfded9a35d353b4d08cff08", + .url = "https://github.com/neurocyte/thespian/archive/d2b65df7c59b58f5e86b1ce3403cd3589d06a367.tar.gz", + .hash = "122040a2dcb9569ee26ae9c205f944db4ee9e3eb7c9d5ec610c7d84cf82ab6909fa9", }, .themes = .{ .url = "https://github.com/neurocyte/flow-themes/releases/download/master-770924d1aba9cc33440b66e865bd4f7dca871074/flow-themes.tar.gz", @@ -37,9 +37,9 @@ .url = "https://github.com/rockorager/zeit/archive/9cca8ec620a54c3b07cd249f25e5bcb3153d03d7.tar.gz", .hash = "1220755ea2a5aa6bb3713437aaafefd44812169fe43f1da755c3ee6101b85940f441", }, - .direct2d = .{ - .url = "https://github.com/marler8997/direct2d-zig/archive/0d031389a26653bb71f81c2340d1b8ba6bd339c3.tar.gz", - .hash = "122069b40656962c6ba9b9b3f9f882ba2e9cf4c5e1afebac7b7501404129e6bb4705", + .win32 = .{ + .url = "https://github.com/marlersoft/zigwin32/archive/259b6f353a48968d7e3171573db4fd898b046188.tar.gz", + .hash = "1220925614447b54ccc9894bbba8b202c6a8b750267890edab7732064867e46f3217", .lazy = true, }, }, diff --git a/contrib/make_release b/contrib/make_release index 6d57d73..437c332 100755 --- a/contrib/make_release +++ b/contrib/make_release @@ -1,12 +1,13 @@ #!/bin/bash set -e +DESTDIR="$(pwd)/release" BASEDIR="$(cd "$(dirname "$0")/.." && pwd)" APPNAME="$(basename "$BASEDIR")" cd "$BASEDIR" -if [ -e "release" ]; then +if [ -e "$DESTDIR" ]; then echo directory \"release\" already exists exit 1 fi @@ -17,9 +18,9 @@ echo running tests... echo building... -./zig build -Dpackage_release --prefix release/build +./zig build -Dpackage_release --prefix "$DESTDIR/build" -cd release/build +cd "$DESTDIR/build" VERSION=$(/bin/cat version) TARGETS=$(/bin/ls) @@ -44,7 +45,7 @@ for tarfile in $TARFILES; do sha256sum -b "$tarfile" > "${tarfile}.sha256" done -echo "done making release $VERSION" +echo "done making release $VERSION @ $DESTDIR" echo /bin/ls -lah diff --git a/src/buffer/Buffer.zig b/src/buffer/Buffer.zig index 964f483..215a179 100644 --- a/src/buffer/Buffer.zig +++ b/src/buffer/Buffer.zig @@ -10,6 +10,7 @@ const max_imbalance = 7; pub const Root = *const Node; pub const unicode = @import("unicode.zig"); +pub const Manager = @import("Manager.zig"); pub const Cursor = @import("Cursor.zig"); pub const View = @import("View.zig"); pub const Selection = @import("Selection.zig"); @@ -43,6 +44,9 @@ undo_history: ?*UndoNode = null, redo_history: ?*UndoNode = null, curr_history: ?*UndoNode = null, +mtime: i64, +utime: i64, + pub const EolMode = enum { lf, crlf }; pub const EolModeTag = @typeInfo(EolMode).Enum.tag_type; @@ -143,7 +147,7 @@ pub const Leaf = struct { bol: bool = true, eol: bool = true, - fn new(allocator: Allocator, piece: []const u8, bol: bool, eol: bool) !*const Node { + fn new(allocator: Allocator, piece: []const u8, bol: bool, eol: bool) error{OutOfMemory}!*const Node { if (piece.len == 0) return if (!bol and !eol) &empty_leaf else if (bol and !eol) &empty_bol_leaf else if (!bol and eol) &empty_eol_leaf else &empty_line_leaf; const node = try allocator.create(Node); @@ -1043,7 +1047,7 @@ const Node = union(enum) { } }; -pub fn create(allocator: Allocator) !*Self { +pub fn create(allocator: Allocator) error{OutOfMemory}!*Self { const self = try allocator.create(Self); const arena_a = if (builtin.is_test) allocator else std.heap.page_allocator; self.* = .{ @@ -1051,6 +1055,8 @@ pub fn create(allocator: Allocator) !*Self { .allocator = self.arena.allocator(), .external_allocator = allocator, .root = try Node.new(self.allocator, &empty_leaf, &empty_leaf), + .mtime = std.time.milliTimestamp(), + .utime = std.time.milliTimestamp(), }; return self; } @@ -1062,12 +1068,27 @@ pub fn deinit(self: *Self) void { self.external_allocator.destroy(self); } -fn new_file(self: *const Self, file_exists: *bool) !Root { +pub fn update_last_used_time(self: *Self) void { + self.utime = std.time.milliTimestamp(); +} + +fn new_file(self: *const Self, file_exists: *bool) error{OutOfMemory}!Root { file_exists.* = false; return Leaf.new(self.allocator, "", true, false); } -pub fn load(self: *const Self, reader: anytype, size: usize, eol_mode: *EolMode, utf8_sanitized: *bool) !Root { +pub fn LoadError(comptime reader_error: anytype) type { + return error{ + OutOfMemory, + BufferUnderrun, + DanglingSurrogateHalf, + ExpectedSecondSurrogateHalf, + UnexpectedSecondSurrogateHalf, + Unexpected, + } || reader_error; +} + +pub fn load(self: *const Self, reader: anytype, size: usize, eol_mode: *EolMode, utf8_sanitized: *bool) LoadError(@TypeOf(reader).Error)!Root { const lf = '\n'; const cr = '\r'; var buf = try self.external_allocator.alloc(u8, size); @@ -1117,20 +1138,69 @@ pub fn load(self: *const Self, reader: anytype, size: usize, eol_mode: *EolMode, return Node.merge_in_place(leaves, self.allocator); } -pub fn load_from_string(self: *const Self, s: []const u8, eol_mode: *EolMode, utf8_sanitized: *bool) !Root { +pub const LoadFromStringError = LoadError(error{}); + +pub fn load_from_string(self: *const Self, s: []const u8, eol_mode: *EolMode, utf8_sanitized: *bool) LoadFromStringError!Root { var stream = std.io.fixedBufferStream(s); return self.load(stream.reader(), s.len, eol_mode, utf8_sanitized); } -pub fn load_from_string_and_update(self: *Self, file_path: []const u8, s: []const u8) !void { +pub fn load_from_string_and_update(self: *Self, file_path: []const u8, s: []const u8) LoadFromStringError!void { self.root = try self.load_from_string(s, &self.file_eol_mode, &self.file_utf8_sanitized); self.file_path = try self.allocator.dupe(u8, file_path); self.last_save = self.root; self.last_save_eol_mode = self.file_eol_mode; self.file_exists = false; + self.mtime = std.time.milliTimestamp(); } -pub fn load_from_file(self: *const Self, file_path: []const u8, file_exists: *bool, eol_mode: *EolMode, utf8_sanitized: *bool) !Root { +pub const LoadFromFileError = error{ + OutOfMemory, + Unexpected, + FileTooBig, + NoSpaceLeft, + DeviceBusy, + AccessDenied, + SystemResources, + WouldBlock, + IsDir, + SharingViolation, + PathAlreadyExists, + FileNotFound, + PipeBusy, + NameTooLong, + InvalidUtf8, + InvalidWtf8, + BadPathName, + NetworkNotFound, + AntivirusInterference, + SymLinkLoop, + ProcessFdQuotaExceeded, + SystemFdQuotaExceeded, + NoDevice, + NotDir, + FileLocksNotSupported, + FileBusy, + InputOutput, + BrokenPipe, + OperationAborted, + ConnectionResetByPeer, + ConnectionTimedOut, + NotOpenForReading, + SocketNotConnected, + BufferUnderrun, + DanglingSurrogateHalf, + ExpectedSecondSurrogateHalf, + UnexpectedSecondSurrogateHalf, +}; + +pub fn load_from_file( + self: *const Self, + file_path: []const u8, + file_exists: *bool, + eol_mode: *EolMode, + utf8_sanitized: *bool, +) LoadFromFileError!Root { const file = cwd().openFile(file_path, .{ .mode = .read_only }) catch |e| switch (e) { error.FileNotFound => return self.new_file(file_exists), else => return e, @@ -1142,7 +1212,7 @@ pub fn load_from_file(self: *const Self, file_path: []const u8, file_exists: *bo return self.load(file.reader(), @intCast(stat.size), eol_mode, utf8_sanitized); } -pub fn load_from_file_and_update(self: *Self, file_path: []const u8) !void { +pub fn load_from_file_and_update(self: *Self, file_path: []const u8) LoadFromFileError!void { var file_exists: bool = false; var eol_mode: EolMode = .lf; var utf8_sanitized: bool = false; @@ -1153,6 +1223,15 @@ pub fn load_from_file_and_update(self: *Self, file_path: []const u8) !void { self.file_eol_mode = eol_mode; self.file_utf8_sanitized = utf8_sanitized; self.last_save_eol_mode = eol_mode; + self.mtime = std.time.milliTimestamp(); +} + +pub fn reset_to_last_saved(self: *Self) void { + if (self.last_save) |last_save| { + self.store_undo(&[_]u8{}) catch {}; + self.root = last_save; + self.mtime = std.time.milliTimestamp(); + } } pub fn store_to_string(self: *const Self, allocator: Allocator, eol_mode: EolMode) ![]u8 { @@ -1161,7 +1240,7 @@ pub fn store_to_string(self: *const Self, allocator: Allocator, eol_mode: EolMod return s.toOwnedSlice(); } -fn store_to_file_const(self: *const Self, file: anytype) !void { +fn store_to_file_const(self: *const Self, file: anytype) StoreToFileError!void { const buffer_size = 4096 * 16; // 64KB const BufferedWriter = std.io.BufferedWriter(buffer_size, std.fs.File.Writer); const Writer = std.io.Writer(*BufferedWriter, BufferedWriter.Error, BufferedWriter.write); @@ -1173,7 +1252,46 @@ fn store_to_file_const(self: *const Self, file: anytype) !void { try buffered_writer.flush(); } -pub fn store_to_existing_file_const(self: *const Self, file_path: []const u8) !void { +pub const StoreToFileError = error{ + AccessDenied, + AntivirusInterference, + BadPathName, + BrokenPipe, + ConnectionResetByPeer, + DeviceBusy, + DiskQuota, + FileBusy, + FileLocksNotSupported, + FileNotFound, + FileTooBig, + InputOutput, + InvalidArgument, + InvalidUtf8, + InvalidWtf8, + IsDir, + LinkQuotaExceeded, + LockViolation, + NameTooLong, + NetworkNotFound, + NoDevice, + NoSpaceLeft, + NotDir, + NotOpenForWriting, + OperationAborted, + PathAlreadyExists, + PipeBusy, + ProcessFdQuotaExceeded, + ReadOnlyFileSystem, + RenameAcrossMountPoints, + SharingViolation, + SymLinkLoop, + SystemFdQuotaExceeded, + SystemResources, + Unexpected, + WouldBlock, +}; + +pub fn store_to_existing_file_const(self: *const Self, file_path: []const u8) StoreToFileError!void { const stat = try cwd().statFile(file_path); var atomic = try cwd().atomicFile(file_path, .{ .mode = stat.mode }); defer atomic.deinit(); @@ -1181,13 +1299,13 @@ pub fn store_to_existing_file_const(self: *const Self, file_path: []const u8) !v try atomic.finish(); } -pub fn store_to_new_file_const(self: *const Self, file_path: []const u8) !void { +pub fn store_to_new_file_const(self: *const Self, file_path: []const u8) StoreToFileError!void { const file = try cwd().createFile(file_path, .{ .read = true, .truncate = true }); defer file.close(); try self.store_to_file_const(file); } -pub fn store_to_file_and_clean(self: *Self, file_path: []const u8) !void { +pub fn store_to_file_and_clean(self: *Self, file_path: []const u8) StoreToFileError!void { self.store_to_existing_file_const(file_path) catch |e| switch (e) { error.FileNotFound => try self.store_to_new_file_const(file_path), else => return e, @@ -1213,6 +1331,7 @@ pub fn version(self: *const Self) usize { pub fn update(self: *Self, root: Root) void { self.root = root; + self.mtime = std.time.milliTimestamp(); } pub fn store_undo(self: *Self, meta: []const u8) !void { @@ -1263,6 +1382,7 @@ pub fn undo(self: *Self, meta: []const u8) error{Stop}![]const u8 { self.curr_history = h; self.root = h.root; self.push_redo(r); + self.mtime = std.time.milliTimestamp(); return h.meta; } @@ -1274,5 +1394,6 @@ pub fn redo(self: *Self) error{Stop}![]const u8 { self.curr_history = h; self.root = h.root; self.push_undo(u); + self.mtime = std.time.milliTimestamp(); return h.meta; } diff --git a/src/buffer/Manager.zig b/src/buffer/Manager.zig new file mode 100644 index 0000000..6b68e78 --- /dev/null +++ b/src/buffer/Manager.zig @@ -0,0 +1,87 @@ +const std = @import("std"); +const Buffer = @import("Buffer.zig"); + +const Self = @This(); + +allocator: std.mem.Allocator, +buffers: std.StringHashMapUnmanaged(*Buffer), + +pub fn init(allocator: std.mem.Allocator) Self { + return .{ + .allocator = allocator, + .buffers = .{}, + }; +} + +pub fn deinit(self: *Self) void { + var i = self.buffers.iterator(); + while (i.next()) |p| { + self.allocator.free(p.key_ptr.*); + p.value_ptr.*.deinit(); + } + self.buffers.deinit(self.allocator); +} + +pub fn open_file(self: *Self, file_path: []const u8) Buffer.LoadFromFileError!*Buffer { + const buffer = if (self.buffers.get(file_path)) |buffer| buffer else blk: { + var buffer = try Buffer.create(self.allocator); + errdefer buffer.deinit(); + try buffer.load_from_file_and_update(file_path); + try self.buffers.put(self.allocator, try self.allocator.dupe(u8, file_path), buffer); + break :blk buffer; + }; + buffer.update_last_used_time(); + return buffer; +} + +pub fn open_scratch(self: *Self, file_path: []const u8, content: []const u8) Buffer.LoadFromStringError!*Buffer { + const buffer = if (self.buffers.get(file_path)) |buffer| buffer else blk: { + var buffer = try Buffer.create(self.allocator); + errdefer buffer.deinit(); + try buffer.load_from_string_and_update(file_path, content); + buffer.file_exists = true; + try self.buffers.put(self.allocator, try self.allocator.dupe(u8, file_path), buffer); + break :blk buffer; + }; + buffer.update_last_used_time(); + return buffer; +} + +pub fn retire(_: *Self, buffer: *Buffer) void { + buffer.update_last_used_time(); +} + +pub fn list_most_recently_used(self: *Self, allocator: std.mem.Allocator) error{OutOfMemory}![]*Buffer { + var buffers: std.ArrayListUnmanaged(*Buffer) = .{}; + var i = self.buffers.iterator(); + while (i.next()) |kv| + (try buffers.addOne(allocator)).* = kv.value_ptr.*; + + std.mem.sort(*Buffer, buffers.items, {}, struct { + fn less_fn(_: void, lhs: *Buffer, rhs: *Buffer) bool { + return lhs.utime > rhs.utime; + } + }.less_fn); + + return buffers.toOwnedSlice(allocator); +} + +pub fn is_dirty(self: *const Self) bool { + var i = self.buffers.iterator(); + while (i.next()) |kv| + if (kv.value_ptr.*.is_dirty()) + return true; + return false; +} + +pub fn is_buffer_dirty(self: *const Self, file_path: []const u8) bool { + return if (self.buffers.get(file_path)) |buffer| buffer.is_dirty() else false; +} + +pub fn save_all(self: *const Self) Buffer.StoreToFileError!void { + var i = self.buffers.iterator(); + while (i.next()) |kv| { + const buffer = kv.value_ptr.*; + try buffer.store_to_file_and_clean(buffer.file_path); + } +} diff --git a/src/keybind/builtin/flow.json b/src/keybind/builtin/flow.json index 33c5ec9..6876aee 100644 --- a/src/keybind/builtin/flow.json +++ b/src/keybind/builtin/flow.json @@ -1,6 +1,7 @@ { "project": { "press": [ + ["ctrl+shift+e", "switch_buffers"], ["ctrl+0", "reset_fontsize"], ["ctrl+plus", "adjust_fontsize", 1.0], ["ctrl+minus", "adjust_fontsize", -1.0], @@ -250,6 +251,7 @@ ["ctrl+p", "palette_menu_up"], ["ctrl+n", "palette_menu_down"], ["ctrl+e", "palette_menu_down"], + ["ctrl+shift+e", "palette_menu_down"], ["ctrl+r", "palette_menu_down"], ["ctrl+t", "palette_menu_down"], ["ctrl+v", "system_paste"], diff --git a/src/renderer/win32/renderer.zig b/src/renderer/win32/renderer.zig index ac0f899..6c63388 100644 --- a/src/renderer/win32/renderer.zig +++ b/src/renderer/win32/renderer.zig @@ -19,6 +19,8 @@ const gui = @import("gui"); const DropWriter = gui.DropWriter; pub const style = StyleBits; +pub const panic = win32.messageBoxThenPanic(.{ .title = "Flow Panic" }); + allocator: std.mem.Allocator, vx: vaxis.Vaxis, @@ -84,25 +86,6 @@ pub fn deinit(self: *Self) void { self.title_buf.deinit(); } -threadlocal var thread_is_panicing = false; - -pub fn panic( - msg: []const u8, - error_return_trace: ?*std.builtin.StackTrace, - ret_addr: ?usize, -) noreturn { - if (!thread_is_panicing) { - thread_is_panicing = true; - const msg_z: [:0]const u8 = if (std.fmt.allocPrintZ( - std.heap.page_allocator, - "{s}", - .{msg}, - )) |msg_z| msg_z else |_| "failed allocate error message"; - _ = win32.MessageBoxA(null, msg_z, "Flow Panic", .{ .ICONASTERISK = 1 }); - } - std.builtin.default_panic(msg, error_return_trace, ret_addr); -} - pub fn run(self: *Self) !void { if (self.thread) |_| return; @@ -353,7 +336,7 @@ fn update_window_title(self: *Self) void { const title = self.title_buf.toOwnedSliceSentinel(0) catch @panic("OOM:update_window_title"); if (win32.SetWindowTextW(hwnd, title) == 0) { - std.log.warn("SetWindowText failed with {}", .{win32.GetLastError().fmt()}); + std.log.warn("SetWindowText failed, error={}", .{win32.GetLastError()}); self.title_buf = std.ArrayList(u16).fromOwnedSlice(self.allocator, title); } else { self.allocator.free(title); diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 62e4f45..cb41b7f 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -242,6 +242,7 @@ pub const Editor = struct { file_path: ?[]const u8, buffer: ?*Buffer, + buffer_manager: *Buffer.Manager, lsp_version: usize = 1, pause_undo: bool = false, @@ -382,7 +383,7 @@ pub const Editor = struct { self.clamp(); } - fn init(self: *Self, allocator: Allocator, n: Plane) void { + fn init(self: *Self, allocator: Allocator, n: Plane, buffer_manager: *Buffer.Manager) void { const logger = log.logger("editor"); const frame_rate = tp.env.get().num("frame-rate"); const indent_size = tui.current().config.indent_size; @@ -396,6 +397,7 @@ pub const Editor = struct { .logger = logger, .file_path = null, .buffer = null, + .buffer_manager = buffer_manager, .handlers = EventHandler.List.init(allocator), .animation_lag = get_animation_max_lag(), .animation_frame_rate = frame_rate, @@ -417,7 +419,7 @@ pub const Editor = struct { self.matches.deinit(); self.handlers.deinit(); self.logger.deinit(); - if (self.buffer) |p| p.deinit(); + if (self.buffer) |p| self.buffer_manager.retire(p); if (self.case_data) |cd| cd.deinit(); } @@ -483,28 +485,16 @@ pub const Editor = struct { self.view.cols = pos.w; } - pub fn is_dirty(self: *Self) bool { - const b = self.buffer orelse return false; - return b.is_dirty(); - } - fn open(self: *Self, file_path: []const u8) !void { - var new_buf = try Buffer.create(self.allocator); - errdefer new_buf.deinit(); - try new_buf.load_from_file_and_update(file_path); - return self.open_buffer(file_path, new_buf); + return self.open_buffer(file_path, try self.buffer_manager.open_file(file_path)); } fn open_scratch(self: *Self, file_path: []const u8, content: []const u8) !void { - var new_buf = try Buffer.create(self.allocator); - errdefer new_buf.deinit(); - try new_buf.load_from_string_and_update(file_path, content); - new_buf.file_exists = true; - return self.open_buffer(file_path, new_buf); + return self.open_buffer(file_path, try self.buffer_manager.open_scratch(file_path, content)); } fn open_buffer(self: *Self, file_path: []const u8, new_buf: *Buffer) !void { - errdefer new_buf.deinit(); + errdefer self.buffer_manager.retire(new_buf); self.cancel_all_selections(); self.get_primary().reset(); self.file_path = try self.allocator.dupe(u8, file_path); @@ -543,17 +533,7 @@ pub const Editor = struct { } fn close(self: *Self) !void { - return self.close_internal(false); - } - - fn close_dirty(self: *Self) !void { - return self.close_internal(true); - } - - fn close_internal(self: *Self, allow_dirty_close: bool) !void { - const b = self.buffer orelse return error.Stop; - if (!allow_dirty_close and b.is_dirty()) return tp.exit("unsaved changes"); - if (self.buffer) |b_mut| b_mut.deinit(); + if (self.buffer) |b_mut| self.buffer_manager.retire(b_mut); self.buffer = null; self.plane.erase(); self.plane.home(); @@ -3805,7 +3785,8 @@ pub const Editor = struct { pub fn close_file_without_saving(self: *Self, _: Context) Result { self.cancel_all_selections(); - try self.close_dirty(); + if (self.buffer) |buffer| buffer.reset_to_last_saved(); + try self.close(); } pub const close_file_without_saving_meta = .{ .description = "Close file without saving" }; @@ -4794,8 +4775,8 @@ pub const Editor = struct { pub const set_file_type_meta = .{ .arguments = &.{.string} }; }; -pub fn create(allocator: Allocator, parent: Widget) !Widget { - return EditorWidget.create(allocator, parent); +pub fn create(allocator: Allocator, parent: Widget, buffer_manager: *Buffer.Manager) !Widget { + return EditorWidget.create(allocator, parent, buffer_manager); } pub const EditorWidget = struct { @@ -4817,10 +4798,10 @@ pub const EditorWidget = struct { const Self = @This(); const Commands = command.Collection(Editor); - fn create(allocator: Allocator, parent: Widget) !Widget { + fn create(allocator: Allocator, parent: Widget, buffer_manager: *Buffer.Manager) !Widget { const container = try WidgetList.createH(allocator, parent, "editor.container", .dynamic); const self: *Self = try allocator.create(Self); - try self.init(allocator, container.widget()); + try self.init(allocator, container.widget(), buffer_manager); try self.commands.init(&self.editor); const editorWidget = Widget.to(self); try container.add(try editor_gutter.create(allocator, container.widget(), editorWidget, &self.editor)); @@ -4829,7 +4810,7 @@ pub const EditorWidget = struct { return container.widget(); } - fn init(self: *Self, allocator: Allocator, parent: Widget) !void { + fn init(self: *Self, allocator: Allocator, parent: Widget, buffer_manager: *Buffer.Manager) !void { var n = try Plane.init(&(Widget.Box{}).opts("editor"), parent.plane.*); errdefer n.deinit(); @@ -4838,7 +4819,7 @@ pub const EditorWidget = struct { .plane = n, .editor = undefined, }; - self.editor.init(allocator, n); + self.editor.init(allocator, n, buffer_manager); errdefer self.editor.deinit(); try self.editor.push_cursor(); } diff --git a/src/tui/home.zig b/src/tui/home.zig index afba9f0..079e67a 100644 --- a/src/tui/home.zig +++ b/src/tui/home.zig @@ -245,6 +245,12 @@ const cmds = struct { const Ctx = command.Context; const Result = command.Result; + pub fn save_all(_: *Self, _: Ctx) Result { + if (tui.get_buffer_manager()) |bm| + bm.save_all() catch |e| return tp.exit_error(e, @errorReturnTrace()); + } + pub const save_all_meta = .{ .description = "Save all changed files" }; + pub fn home_menu_down(self: *Self, _: Ctx) Result { self.menu.select_down(); } diff --git a/src/tui/mainview.zig b/src/tui/mainview.zig index 9727898..d1cdcdf 100644 --- a/src/tui/mainview.zig +++ b/src/tui/mainview.zig @@ -14,6 +14,7 @@ const build_options = @import("build_options"); const Plane = @import("renderer").Plane; const input = @import("input"); const command = @import("command"); +const BufferManager = @import("Buffer").Manager; const tui = @import("tui.zig"); const Box = @import("Box.zig"); @@ -47,6 +48,7 @@ active_view: ?usize = 0, panels: ?*WidgetList = null, last_match_text: ?[]const u8 = null, location_history: location_history, +buffer_manager: BufferManager, file_stack: std.ArrayList([]const u8), find_in_files_state: enum { init, adding, done } = .done, file_list_type: FileListType = .find_in_files, @@ -70,6 +72,7 @@ pub fn create(allocator: std.mem.Allocator) !Widget { .file_stack = std.ArrayList([]const u8).init(allocator), .views = undefined, .views_widget = undefined, + .buffer_manager = BufferManager.init(allocator), }; try self.commands.init(self); const w = Widget.to(self); @@ -104,6 +107,7 @@ pub fn deinit(self: *Self, allocator: std.mem.Allocator) void { self.commands.deinit(); self.widgets.deinit(allocator); self.floating_views.deinit(); + self.buffer_manager.deinit(); allocator.destroy(self); } @@ -237,15 +241,8 @@ fn toggle_view(self: *Self, view: anytype) !void { } fn check_all_not_dirty(self: *const Self) command.Result { - for (self.editors.items) |editor| - if (editor.is_dirty()) - return tp.exit("unsaved changes"); -} - -fn check_active_not_dirty(self: *const Self) command.Result { - if (self.active_editor) |idx| - if (self.editors.items[idx].is_dirty()) - return tp.exit("unsaved changes"); + if (self.buffer_manager.is_dirty()) + return tp.exit("unsaved changes"); } const cmds = struct { @@ -296,6 +293,8 @@ const cmds = struct { self.clear_find_in_files_results(.diagnostics); if (self.file_list_type == .diagnostics and self.is_panel_view_showing(filelist_view)) try self.toggle_panel_view(filelist_view, false); + self.buffer_manager.deinit(); + self.buffer_manager = BufferManager.init(self.allocator); try project_manager.open(project_dir); const project = tp.env.get().str("project"); tui.current().rdr.set_terminal_working_directory(project); @@ -352,7 +351,6 @@ const cmds = struct { if (!same_file) { if (self.get_active_editor()) |editor| { - try self.check_active_not_dirty(); editor.send_editor_jump_source() catch {}; } try self.create_editor(); @@ -395,7 +393,6 @@ const cmds = struct { pub const open_gui_config_meta = .{ .description = "Edit gui configuration file" }; pub fn create_scratch_buffer(self: *Self, ctx: Ctx) Result { - try self.check_all_not_dirty(); tui.reset_drag_context(); try self.create_editor(); try command.executeName("open_scratch_buffer", ctx); @@ -863,7 +860,7 @@ fn create_editor(self: *Self) !void { if (self.get_active_file_path()) |file_path| self.push_file_stack(file_path) catch {}; try self.delete_active_view(); command.executeName("enter_mode_default", .{}) catch {}; - var editor_widget = try ed.create(self.allocator, Widget.to(self)); + var editor_widget = try ed.create(self.allocator, Widget.to(self), &self.buffer_manager); errdefer editor_widget.deinit(self.allocator); const editor = editor_widget.get("editor") orelse @panic("mainview editor not found"); if (self.top_bar) |bar| editor.subscribe(EventHandler.to_unowned(bar)) catch @panic("subscribe unsupported"); diff --git a/src/tui/mode/mini/open_file.zig b/src/tui/mode/mini/open_file.zig index 407ffeb..de60c07 100644 --- a/src/tui/mode/mini/open_file.zig +++ b/src/tui/mode/mini/open_file.zig @@ -11,7 +11,6 @@ pub const create = Type.create; pub fn load_entries(self: *Type) error{ Exit, OutOfMemory }!void { const editor = tui.get_active_editor() orelse return; - if (editor.is_dirty()) return tp.exit("unsaved changes"); if (editor.file_path) |old_path| if (std.mem.lastIndexOf(u8, old_path, "/")) |pos| try self.file_path.appendSlice(old_path[0 .. pos + 1]); diff --git a/src/tui/mode/overlay/buffer_palette.zig b/src/tui/mode/overlay/buffer_palette.zig new file mode 100644 index 0000000..96751a9 --- /dev/null +++ b/src/tui/mode/overlay/buffer_palette.zig @@ -0,0 +1,49 @@ +const std = @import("std"); +const cbor = @import("cbor"); +const tp = @import("thespian"); +const root = @import("root"); +const command = @import("command"); + +const tui = @import("../../tui.zig"); +pub const Type = @import("palette.zig").Create(@This()); + +pub const label = "Switch buffers"; +pub const name = " buffer"; +pub const description = "buffer"; +const dirty_indicator = ""; + +pub const Entry = struct { + label: []const u8, + hint: []const u8, +}; + +pub fn load_entries(palette: *Type) !usize { + const buffer_manager = tui.get_buffer_manager() orelse return 0; + const buffers = try buffer_manager.list_most_recently_used(palette.allocator); + defer palette.allocator.free(buffers); + for (buffers) |buffer| { + const hint = if (buffer.is_dirty()) dirty_indicator else ""; + (try palette.entries.addOne()).* = .{ .label = buffer.file_path, .hint = hint }; + } + return if (palette.entries.items.len == 0) label.len else 2; +} + +pub fn add_menu_entry(palette: *Type, entry: *Entry, matches: ?[]const usize) !void { + var value = std.ArrayList(u8).init(palette.allocator); + defer value.deinit(); + const writer = value.writer(); + try cbor.writeValue(writer, entry.label); + try cbor.writeValue(writer, entry.hint); + try cbor.writeValue(writer, matches orelse &[_]usize{}); + try palette.menu.add_item_with_handler(value.items, select); + palette.items += 1; +} + +fn select(menu: **Type.MenuState, button: *Type.ButtonState) void { + var file_path: []const u8 = undefined; + var iter = button.opts.label; + if (!(cbor.matchString(&iter, &file_path) catch false)) return; + tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| menu.*.opts.ctx.logger.err("navigate", e); + tp.self_pid().send(.{ "cmd", "navigate", .{} }) catch |e| menu.*.opts.ctx.logger.err("navigate", e); + tp.self_pid().send(.{ "cmd", "navigate", .{ .file = file_path } }) catch |e| menu.*.opts.ctx.logger.err("navigate", e); +} diff --git a/src/tui/mode/overlay/open_recent.zig b/src/tui/mode/overlay/open_recent.zig index 0c4c5b7..cf490c1 100644 --- a/src/tui/mode/overlay/open_recent.zig +++ b/src/tui/mode/overlay/open_recent.zig @@ -10,6 +10,7 @@ const keybind = @import("keybind"); const project_manager = @import("project_manager"); const command = @import("command"); const EventHandler = @import("EventHandler"); +const BufferManager = @import("Buffer").Manager; const tui = @import("../../tui.zig"); const MessageFilter = @import("../../MessageFilter.zig"); @@ -34,6 +35,7 @@ need_reset: bool = false, need_select_first: bool = true, longest: usize = 0, commands: Commands = undefined, +buffer_manager: ?*BufferManager, pub fn create(allocator: std.mem.Allocator) !tui.Mode { const mv = tui.current().mainview.dynamic_cast(mainview) orelse return error.NotFound; @@ -51,6 +53,7 @@ pub fn create(allocator: std.mem.Allocator) !tui.Mode { .ctx = self, .label = "Search files by name", }))).dynamic_cast(InputBox.State(*Self)) orelse unreachable, + .buffer_manager = tui.get_buffer_manager(), }; try self.commands.init(self); try tui.current().message_filters.add(MessageFilter.bind(self, receive_project_manager)); @@ -93,7 +96,7 @@ inline fn max_menu_width() usize { return @max(15, width - (width / 5)); } -fn on_render_menu(_: *Self, button: *Button.State(*Menu.State(*Self)), theme: *const Widget.Theme, selected: bool) bool { +fn on_render_menu(self: *Self, button: *Button.State(*Menu.State(*Self)), theme: *const Widget.Theme, selected: bool) bool { const style_base = theme.editor_widget; const style_label = if (button.active) theme.editor_cursor else if (button.hover or selected) theme.editor_selection else theme.editor_widget; const style_keybind = if (tui.find_scope_style(theme, "entity.name")) |sty| sty.style else style_base; @@ -110,7 +113,8 @@ fn on_render_menu(_: *Self, button: *Button.State(*Menu.State(*Self)), theme: *c if (!(cbor.matchString(&iter, &file_path) catch false)) file_path = "#ERROR#"; button.plane.set_style(style_keybind); - const pointer = if (selected) "⏵" else " "; + const dirty = if (self.buffer_manager) |bm| if (bm.is_buffer_dirty(file_path)) "" else " " else " "; + const pointer = if (selected) "⏵" else dirty; _ = button.plane.print("{s}", .{pointer}) catch {}; var buf: [std.fs.max_path_bytes]u8 = undefined; var removed_prefix: usize = 0; diff --git a/src/tui/tui.zig b/src/tui/tui.zig index e22fbca..7288953 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -56,6 +56,7 @@ mouse_idle_timer: ?tp.Cancellable = null, default_cursor: keybind.CursorShape = .default, fontface: []const u8 = "", fontfaces: ?std.ArrayList([]const u8) = null, +enable_mouse_idle_timer: bool = false, const keepalive = std.time.us_per_day * 365; // one year const idle_frames = 0; @@ -222,6 +223,7 @@ fn listen_sigwinch(self: *Self) tp.result { } fn update_mouse_idle_timer(self: *Self) void { + if (!self.enable_mouse_idle_timer) return; const delay = std.time.us_per_ms * @as(u64, mouse_idle_time_milliseconds); if (self.mouse_idle_timer) |*t| { t.cancel() catch {}; @@ -309,6 +311,8 @@ fn receive_safe(self: *Self, from: tp.pid_ref, m: tp.message) !void { if (try m.match(.{"resize"})) { self.resize(); + const box = self.screen(); + message("{d}x{d}", .{ box.w, box.h }); return; } @@ -811,6 +815,11 @@ const cmds = struct { } pub const open_recent_project_meta = .{ .description = "Open recent project" }; + pub fn switch_buffers(self: *Self, _: Ctx) Result { + return self.enter_overlay_mode(@import("mode/overlay/buffer_palette.zig").Type); + } + pub const switch_buffers_meta = .{ .description = "Switch buffers" }; + pub fn change_theme(self: *Self, _: Ctx) Result { return self.enter_overlay_mode(@import("mode/overlay/theme_palette.zig").Type); } @@ -987,6 +996,10 @@ pub fn get_active_selection(allocator: std.mem.Allocator) ?[]u8 { return editor.get_selection(sel, allocator) catch null; } +pub fn get_buffer_manager() ?*@import("Buffer").Manager { + return if (current().mainview.dynamic_cast(mainview)) |mv_| &mv_.buffer_manager else null; +} + fn context_check() void { if (instance_ == null) @panic("tui call out of context"); } @@ -1176,3 +1189,8 @@ pub fn is_cursor_beam(self: *Self) bool { pub fn get_selection_style(self: *Self) @import("Buffer").Selection.Style { return if (self.input_mode) |mode| mode.selection_style else .normal; } + +pub fn message(comptime fmt: anytype, args: anytype) void { + var buf: [256]u8 = undefined; + tp.self_pid().send(.{ "message", std.fmt.bufPrint(&buf, fmt, args) catch @panic("too large") }) catch {}; +} diff --git a/src/win32/d3d11.zig b/src/win32/d3d11.zig index 21b6caa..7dbe105 100644 --- a/src/win32/d3d11.zig +++ b/src/win32/d3d11.zig @@ -309,7 +309,7 @@ pub fn paint( for (0..shader_row_count) |row| { const src_row = blk: { const r = top + row; - break :blk r - if (r >= row_count) row_count else 0; + break :blk if (r < row_count) r else 0; }; const src_row_offset = src_row * col_count; const dst_row_offset = row * shader_col_count; diff --git a/src/win32/gui.zig b/src/win32/gui.zig index 7dc5456..71761eb 100644 --- a/src/win32/gui.zig +++ b/src/win32/gui.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const tracy = @import("tracy"); const build_options = @import("build_options"); const root = @import("root"); @@ -92,6 +93,8 @@ const window_style_ex = win32.WINDOW_EX_STYLE{ const window_style = win32.WS_OVERLAPPEDWINDOW; pub fn init() void { + const frame = tracy.initZone(@src(), .{ .name = "gui init" }); + defer frame.deinit(); std.debug.assert(!global.init_called); global.init_called = true; render.init(.{}); @@ -102,6 +105,8 @@ const Icons = struct { large: win32.HICON, }; fn getIcons(dpi: XY(u32)) Icons { + const frame = tracy.initZone(@src(), .{ .name = "gui getIcons" }); + defer frame.deinit(); const small_x = win32.GetSystemMetricsForDpi(@intFromEnum(win32.SM_CXSMICON), dpi.x); const small_y = win32.GetSystemMetricsForDpi(@intFromEnum(win32.SM_CYSMICON), dpi.y); const large_x = win32.GetSystemMetricsForDpi(@intFromEnum(win32.SM_CXICON), dpi.x); @@ -118,7 +123,7 @@ fn getIcons(dpi: XY(u32)) Icons { small_x, small_y, win32.LR_SHARED, - ) orelse fatalWin32("LoadImage for small icon", win32.GetLastError()); + ) orelse win32.panicWin32("LoadImage for small icon", win32.GetLastError()); const large = win32.LoadImageW( win32.GetModuleHandleW(null), @ptrFromInt(c.ID_ICON_FLOW), @@ -126,7 +131,7 @@ fn getIcons(dpi: XY(u32)) Icons { large_x, large_y, win32.LR_SHARED, - ) orelse fatalWin32("LoadImage for large icon", win32.GetLastError()); + ) orelse win32.panicWin32("LoadImage for large icon", win32.GetLastError()); return .{ .small = @ptrCast(small), .large = @ptrCast(large) }; } @@ -190,6 +195,8 @@ fn getFontSize() f32 { } fn getFont(dpi: u32, size: f32, face: *const FontFace) render.Font { + const frame = tracy.initZone(@src(), .{ .name = "gui getFont" }); + defer frame.deinit(); if (global.font) |*font| { if (font.dpi == dpi and font.size == size and font.face.eql(face)) return font.render_object; @@ -251,6 +258,8 @@ fn calcWindowPlacement( initial_window_x: u16, initial_window_y: u16, ) WindowPlacement { + const frame = tracy.initZone(@src(), .{ .name = "gui calcWindowPlacement" }); + defer frame.deinit(); var result = WindowPlacement.default; const monitor = maybe_monitor orelse return result; @@ -259,7 +268,7 @@ fn calcWindowPlacement( var info: win32.MONITORINFO = undefined; info.cbSize = @sizeOf(win32.MONITORINFO); if (0 == win32.GetMonitorInfoW(monitor, &info)) { - std.log.warn("GetMonitorInfo failed with {}", .{win32.GetLastError().fmt()}); + std.log.warn("GetMonitorInfo failed, error={}", .{win32.GetLastError()}); return result; } break :blk info.rcWork; @@ -326,7 +335,7 @@ fn entry(pid: thespian.pid) !void { }, win32.MONITOR_DEFAULTTOPRIMARY, ) orelse { - std.log.warn("MonitorFromPoint failed with {}", .{win32.GetLastError().fmt()}); + std.log.warn("MonitorFromPoint failed, error={}", .{win32.GetLastError()}); break :blk null; }; }; @@ -375,7 +384,7 @@ fn entry(pid: thespian.pid) !void { .lpszClassName = CLASS_NAME, .hIconSm = global.icons.small, }; - if (0 == win32.RegisterClassExW(&wc)) fatalWin32( + if (0 == win32.RegisterClassExW(&wc)) win32.panicWin32( "RegisterClass for main window", win32.GetLastError(), ); @@ -394,7 +403,7 @@ fn entry(pid: thespian.pid) !void { null, // Menu win32.GetModuleHandleW(null), @ptrCast(&create_args), - ) orelse fatalWin32("CreateWindow", win32.GetLastError()); + ) orelse win32.panicWin32("CreateWindow", win32.GetLastError()); // NEVER DESTROY THE WINDOW! // This allows us to send the hwnd to other thread/parts // of the app and it will always be valid. @@ -421,7 +430,7 @@ fn entry(pid: thespian.pid) !void { ); } - if (0 == win32.UpdateWindow(hwnd)) fatalWin32("UpdateWindow", win32.GetLastError()); + if (0 == win32.UpdateWindow(hwnd)) win32.panicWin32("UpdateWindow", win32.GetLastError()); _ = win32.ShowWindow(hwnd, win32.SW_SHOWNORMAL); // try some things to bring our window to the top @@ -531,12 +540,14 @@ fn updateWindowSize( edge: ?win32.WPARAM, bounds_ref: *?WindowBounds, ) void { + const frame = tracy.initZone(@src(), .{ .name = "gui updateWindowSize" }); + defer frame.deinit(); const dpi = win32.dpiFromHwnd(hwnd); const font = getFont(dpi, getFontSize(), getFontFace()); const cell_size = font.getCellSize(i32); var window_rect: win32.RECT = undefined; - if (0 == win32.GetWindowRect(hwnd, &window_rect)) fatalWin32( + if (0 == win32.GetWindowRect(hwnd, &window_rect)) win32.panicWin32( "GetWindowRect", win32.GetLastError(), ); @@ -564,6 +575,8 @@ fn updateWindowSize( } fn getFontFaces(state: *State) void { + const frame = tracy.initZone(@src(), .{ .name = "gui getFontFaces" }); + defer frame.deinit(); const fonts = render.Fonts.init(); defer fonts.deinit(); var buf: [FontFace.max * 2]u8 = undefined; @@ -631,7 +644,9 @@ fn sendMouse( wparam: win32.WPARAM, lparam: win32.LPARAM, ) void { - const point = win32ext.pointFromLparam(lparam); + const frame = tracy.initZone(@src(), .{ .name = "gui sendMouse" }); + defer frame.deinit(); + const point = win32.pointFromLparam(lparam); const state = stateFromHwnd(hwnd); const dpi = win32.dpiFromHwnd(hwnd); const cell_size = getFont(dpi, getFontSize(), getFontFace()).getCellSize(i32); @@ -682,7 +697,10 @@ fn sendMouseWheel( wparam: win32.WPARAM, lparam: win32.LPARAM, ) void { - const point = win32ext.pointFromLparam(lparam); + const frame = tracy.initZone(@src(), .{ .name = "gui sendMouseWheel" }); + defer frame.deinit(); + var point = win32.pointFromLparam(lparam); + _ = win32.ScreenToClient(hwnd, &point); const state = stateFromHwnd(hwnd); const dpi = win32.dpiFromHwnd(hwnd); const cell_size = getFont(dpi, getFontSize(), getFontFace()).getCellSize(i32); @@ -731,10 +749,12 @@ fn sendKey( wparam: win32.WPARAM, lparam: win32.LPARAM, ) void { + const frame = tracy.initZone(@src(), .{ .name = "gui sendKey" }); + defer frame.deinit(); const state = stateFromHwnd(hwnd); var keyboard_state: [256]u8 = undefined; - if (0 == win32.GetKeyboardState(&keyboard_state)) fatalWin32( + if (0 == win32.GetKeyboardState(&keyboard_state)) win32.panicWin32( "GetKeyboardState", win32.GetLastError(), ); @@ -988,6 +1008,8 @@ fn WndProc( wparam: win32.WPARAM, lparam: win32.LPARAM, ) callconv(std.os.windows.WINAPI) win32.LRESULT { + const frame = tracy.initZone(@src(), .{ .name = "gui WndProc" }); + defer frame.deinit(); var msg_node: windowmsg.MessageNode = undefined; msg_node.init(&global_msg_tail, hwnd, msg, wparam, lparam); defer msg_node.deinit(); @@ -1035,11 +1057,17 @@ fn WndProc( return 0; }, win32.WM_PAINT => { + const frame_ = tracy.initZone(@src(), .{ .name = "gui WM_PAINT" }); + defer frame_.deinit(); const state = stateFromHwnd(hwnd); const dpi = win32.dpiFromHwnd(hwnd); const font = getFont(dpi, getFontSize(), getFontFace()); const client_size = getClientSize(u32, hwnd); + var ps: win32.PAINTSTRUCT = undefined; + _ = win32.BeginPaint(hwnd, &ps) orelse return win32.panicWin32("BeginPaint", win32.GetLastError()); + defer if (0 == win32.EndPaint(hwnd, &ps)) win32.panicWin32("EndPaint", win32.GetLastError()); + global.render_cells.resize( global.render_cells_arena.allocator(), global.screen.buf.len, @@ -1070,6 +1098,8 @@ fn WndProc( return 0; }, win32.WM_GETDPISCALEDSIZE => { + const frame_ = tracy.initZone(@src(), .{ .name = "gui WM_GETDPISCALEDSIZE" }); + defer frame_.deinit(); const inout_size: *win32.SIZE = @ptrFromInt(@as(usize, @bitCast(lparam))); const new_dpi: u32 = @intCast(0xffffffff & wparam); // we don't want to update the font with the new dpi until after @@ -1105,6 +1135,8 @@ fn WndProc( return 1; }, win32.WM_DPICHANGED => { + const frame_ = tracy.initZone(@src(), .{ .name = "gui WM_DPICHANGED" }); + defer frame_.deinit(); const state = stateFromHwnd(hwnd); const dpi = win32.dpiFromHwnd(hwnd); if (dpi != win32.hiword(wparam)) @panic("unexpected hiword dpi"); @@ -1115,10 +1147,14 @@ fn WndProc( return 0; }, win32.WM_WINDOWPOSCHANGED => { + const frame_ = tracy.initZone(@src(), .{ .name = "gui WM_WINDOWPOSCHANGED" }); + defer frame_.deinit(); sendResize(hwnd); return 0; }, win32.WM_SIZING => { + const frame_ = tracy.initZone(@src(), .{ .name = "gui WM_SIZING" }); + defer frame_.deinit(); const rect: *win32.RECT = @ptrFromInt(@as(usize, @bitCast(lparam))); const dpi = win32.dpiFromHwnd(hwnd); const font = getFont(dpi, getFontSize(), getFontFace()); @@ -1133,6 +1169,8 @@ fn WndProc( return 0; }, win32.WM_DISPLAYCHANGE => { + const frame_ = tracy.initZone(@src(), .{ .name = "gui WM_DISPLAYCHANGE" }); + defer frame_.deinit(); win32.invalidateHwnd(hwnd); return 0; }, @@ -1146,6 +1184,8 @@ fn WndProc( return WM_APP_EXIT_RESULT; }, WM_APP_SET_BACKGROUND => { + const frame_ = tracy.initZone(@src(), .{ .name = "gui WM_APP_SET_BACKGROUND" }); + defer frame_.deinit(); const rgb = RGB.from_u24(@intCast(0xffffff & wparam)); render.setBackground( &stateFromHwnd(hwnd).render_state, @@ -1155,6 +1195,8 @@ fn WndProc( return WM_APP_SET_BACKGROUND_RESULT; }, WM_APP_ADJUST_FONTSIZE => { + const frame_ = tracy.initZone(@src(), .{ .name = "gui WM_APP_ADJUST_FONTSIZE" }); + defer frame_.deinit(); const state = stateFromHwnd(hwnd); const amount: f32 = @bitCast(@as(u32, @intCast(0xFFFFFFFFF & wparam))); global.fontsize = @max(getFontSize() + amount, 1.0); @@ -1163,6 +1205,8 @@ fn WndProc( return WM_APP_ADJUST_FONTSIZE_RESULT; }, WM_APP_SET_FONTSIZE => { + const frame_ = tracy.initZone(@src(), .{ .name = "gui WM_APP_SET_FONTSIZE" }); + defer frame_.deinit(); const state = stateFromHwnd(hwnd); const fontsize: f32 = @bitCast(@as(u32, @intCast(0xFFFFFFFFF & wparam))); global.fontsize = @max(fontsize, 1.0); @@ -1171,6 +1215,8 @@ fn WndProc( return WM_APP_SET_FONTSIZE_RESULT; }, WM_APP_RESET_FONTSIZE => { + const frame_ = tracy.initZone(@src(), .{ .name = "gui WM_APP_RESET_FONTSIZE" }); + defer frame_.deinit(); const state = stateFromHwnd(hwnd); global.fontsize = null; updateWindowSize(hwnd, win32.WMSZ_BOTTOMRIGHT, &state.bounds); @@ -1178,6 +1224,8 @@ fn WndProc( return WM_APP_SET_FONTSIZE_RESULT; }, WM_APP_SET_FONTFACE => { + const frame_ = tracy.initZone(@src(), .{ .name = "gui WM_APP_SET_FONTFACE" }); + defer frame_.deinit(); const state = stateFromHwnd(hwnd); setFontFace(@ptrFromInt(wparam)); updateWindowSize(hwnd, win32.WMSZ_BOTTOMRIGHT, &state.bounds); @@ -1185,6 +1233,8 @@ fn WndProc( return WM_APP_SET_FONTFACE_RESULT; }, WM_APP_RESET_FONTFACE => { + const frame_ = tracy.initZone(@src(), .{ .name = "gui WM_APP_RESET_FONTFACE" }); + defer frame_.deinit(); const state = stateFromHwnd(hwnd); global.fontface = null; updateWindowSize(hwnd, win32.WMSZ_BOTTOMRIGHT, &state.bounds); @@ -1192,11 +1242,15 @@ fn WndProc( return WM_APP_SET_FONTFACE_RESULT; }, WM_APP_GET_FONTFACES => { + const frame_ = tracy.initZone(@src(), .{ .name = "gui WM_APP_GET_FONTFACES" }); + defer frame_.deinit(); const state = stateFromHwnd(hwnd); getFontFaces(state); return WM_APP_GET_FONTFACES_RESULT; }, WM_APP_UPDATE_SCREEN => { + const frame_ = tracy.initZone(@src(), .{ .name = "gui WM_APP_UPDATE_SCREEN" }); + defer frame_.deinit(); const screen: *const vaxis.Screen = @ptrFromInt(wparam); _ = global.screen_arena.reset(.retain_capacity); const buf = global.screen_arena.allocator().alloc(vaxis.Cell, screen.buf.len) catch |e| oom(e); @@ -1225,6 +1279,8 @@ fn WndProc( return WM_APP_UPDATE_SCREEN_RESULT; }, win32.WM_CREATE => { + const frame_ = tracy.initZone(@src(), .{ .name = "gui WM_CREATE" }); + defer frame_.deinit(); std.debug.assert(global.state == null); const create_struct: *win32.CREATESTRUCTW = @ptrFromInt(@as(usize, @bitCast(lparam))); const create_args: *CreateWindowArgs = @alignCast(@ptrCast(create_struct.lpCreateParams)); @@ -1242,13 +1298,19 @@ fn WndProc( // hwnd reference @panic("gui window erroneously destroyed"); }, - else => return win32.DefWindowProcW(hwnd, msg, wparam, lparam), + else => { + const frame_ = tracy.initZone(@src(), .{ .name = "gui DefWindowProcW" }); + defer frame_.deinit(); + return win32.DefWindowProcW(hwnd, msg, wparam, lparam); + }, } } fn sendResize( hwnd: win32.HWND, ) void { + const frame = tracy.initZone(@src(), .{ .name = "gui sendResize" }); + defer frame.deinit(); const dpi = win32.dpiFromHwnd(hwnd); const state = stateFromHwnd(hwnd); @@ -1281,19 +1343,13 @@ fn renderColorFromVaxis(color: vaxis.Color) render.Color { }; } -fn fatalWin32(what: []const u8, err: win32.WIN32_ERROR) noreturn { - std.debug.panic("{s} failed with {}", .{ what, err.fmt() }); -} -fn fatalHr(what: []const u8, hresult: win32.HRESULT) noreturn { - std.debug.panic("{s} failed, hresult=0x{x}", .{ what, @as(u32, @bitCast(hresult)) }); -} fn deleteObject(obj: ?win32.HGDIOBJ) void { - if (0 == win32.DeleteObject(obj)) fatalWin32("DeleteObject", win32.GetLastError()); + if (0 == win32.DeleteObject(obj)) win32.panicWin32("DeleteObject", win32.GetLastError()); } fn getClientSize(comptime T: type, hwnd: win32.HWND) XY(T) { var rect: win32.RECT = undefined; if (0 == win32.GetClientRect(hwnd, &rect)) - fatalWin32("GetClientRect", win32.GetLastError()); + win32.panicWin32("GetClientRect", win32.GetLastError()); std.debug.assert(rect.left == 0); std.debug.assert(rect.top == 0); return .{ .x = @intCast(rect.right), .y = @intCast(rect.bottom) }; @@ -1305,6 +1361,8 @@ fn calcWindowRect( maybe_edge: ?win32.WPARAM, cell_size: XY(i32), ) win32.RECT { + const frame = tracy.initZone(@src(), .{ .name = "gui calcWindowRect" }); + defer frame.deinit(); const client_inset = getClientInset(dpi); const bounding_client_size: XY(i32) = .{ .x = (bounding_rect.right - bounding_rect.left) - client_inset.x, @@ -1364,7 +1422,7 @@ fn getClientInset(dpi: u32) XY(i32) { 0, window_style_ex, dpi, - )) fatalWin32( + )) win32.panicWin32( "AdjustWindowRect", win32.GetLastError(), ); @@ -1392,5 +1450,5 @@ fn setWindowPosRect(hwnd: win32.HWND, rect: win32.RECT) void { rect.right - rect.left, rect.bottom - rect.top, .{ .NOZORDER = 1 }, - )) fatalWin32("SetWindowPos", win32.GetLastError()); + )) win32.panicWin32("SetWindowPos", win32.GetLastError()); } diff --git a/src/win32/win32ext.zig b/src/win32/win32ext.zig index f70d244..fc93085 100644 --- a/src/win32/win32ext.zig +++ b/src/win32/win32ext.zig @@ -1,17 +1,6 @@ const std = @import("std"); const win32 = @import("win32").everything; -// todo: these should be available in zigwin32 -fn xFromLparam(lparam: win32.LPARAM) i16 { - return @bitCast(win32.loword(lparam)); -} -fn yFromLparam(lparam: win32.LPARAM) i16 { - return @bitCast(win32.hiword(lparam)); -} -pub fn pointFromLparam(lparam: win32.LPARAM) win32.POINT { - return win32.POINT{ .x = xFromLparam(lparam), .y = yFromLparam(lparam) }; -} - // TODO: update zigwin32 with a way to get the corresponding IID for any COM interface pub fn queryInterface(obj: anytype, comptime Interface: type) *Interface { const obj_basename_start: usize = comptime if (std.mem.lastIndexOfScalar(u8, @typeName(@TypeOf(obj)), '.')) |i| (i + 1) else 0;