From 76279a83ead98e107519ff5f57cfa8d2d61f7faa Mon Sep 17 00:00:00 2001 From: Colin Neilens Date: Sun, 20 Sep 2026 19:48:57 -0700 Subject: [PATCH] Improve Windows input and DPI parity Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Colin Neilens --- graphcode-windows/src/App.zig | 25 +++++++++++++++ graphcode-windows/src/Dpi.zig | 37 ++++++++++++++++++++++ graphcode-windows/src/GraphContextMenu.zig | 16 +++++----- 3 files changed, 70 insertions(+), 8 deletions(-) create mode 100644 graphcode-windows/src/Dpi.zig diff --git a/graphcode-windows/src/App.zig b/graphcode-windows/src/App.zig index 2002db6b..2e4a096a 100644 --- a/graphcode-windows/src/App.zig +++ b/graphcode-windows/src/App.zig @@ -15,6 +15,7 @@ const InputRouter = @import("InputRouter.zig"); const MainWindow = @import("MainWindow.zig"); const TerminalWorkspace = @import("TerminalWorkspace.zig"); const Tokens = @import("DesignTokens.zig"); +const Dpi = @import("Dpi.zig"); const Wire = @import("Wire.zig"); const WorktreeStatus = @import("WorktreeStatus.zig"); const TrayModule = @import("Tray.zig"); @@ -191,6 +192,7 @@ pub const App = struct { workspace: ?*TerminalWorkspace.Workspace = null, navigation_cursor: Navigation.Cursor = .{}, workspace_controls: WorkspaceControls.State = .{ .panel_visible = false }, + dpi: u32 = Dpi.base_dpi, surface: GraphCanvas.Surface = .project, canvas_layout_store: ?CanvasLayoutStore.Store = null, quick_chats_requested: bool = false, @@ -4034,6 +4036,29 @@ fn onWindowMessage( result.* = 0; return true; }, + c.WM_DPICHANGED => { + const dpi = @as(u32, @intCast(wparam & 0xffff)); + app.dpi = Dpi.normalize(dpi); + if (lparam != 0) { + const suggested: *const c.RECT = @ptrFromInt(@as(usize, @bitCast(lparam))); + _ = c.SetWindowPos( + hwnd, + null, + suggested.left, + suggested.top, + suggested.right - suggested.left, + suggested.bottom - suggested.top, + c.SWP_NOZORDER | c.SWP_NOACTIVATE, + ); + } + app.layoutWorkspace(); + app.clampSidebarScroll(); + app.layoutEmptyStateControls(); + app.syncAccessibility(); + _ = c.InvalidateRect(hwnd, null, 0); + result.* = 0; + return true; + }, c.WM_TIMER => if (wparam == MainWindow.timer_id) { app.smoke_tick += 1; if (!app.tray.added and app.smoke_tick % 10 == 0) { diff --git a/graphcode-windows/src/Dpi.zig b/graphcode-windows/src/Dpi.zig new file mode 100644 index 00000000..2e74a7ac --- /dev/null +++ b/graphcode-windows/src/Dpi.zig @@ -0,0 +1,37 @@ +const std = @import("std"); + +pub const base_dpi: u32 = 96; + +pub fn normalize(dpi: u32) u32 { + return if (dpi == 0) base_dpi else dpi; +} + +pub fn scale(value: i32, dpi: u32) i32 { + const normalized = normalize(dpi); + const scaled = @divTrunc( + @as(i64, value) * @as(i64, normalized) + @as(i64, base_dpi / 2), + @as(i64, base_dpi), + ); + return @intCast(scaled); +} + +pub fn unscale(value: i32, dpi: u32) i32 { + const normalized = normalize(dpi); + const scaled = @divTrunc( + @as(i64, value) * @as(i64, base_dpi) + @as(i64, normalized / 2), + @as(i64, normalized), + ); + return @intCast(scaled); +} + +test "DPI scaling rounds at the native boundary" { + try std.testing.expectEqual(@as(i32, 100), scale(100, 96)); + try std.testing.expectEqual(@as(i32, 125), scale(100, 120)); + try std.testing.expectEqual(@as(i32, 150), scale(100, 144)); + try std.testing.expectEqual(@as(i32, 100), unscale(125, 120)); +} + +test "zero DPI falls back to the Windows base DPI" { + try std.testing.expectEqual(base_dpi, normalize(0)); + try std.testing.expectEqual(@as(i32, 42), scale(42, 0)); +} diff --git a/graphcode-windows/src/GraphContextMenu.zig b/graphcode-windows/src/GraphContextMenu.zig index a6d3f2de..d6909868 100644 --- a/graphcode-windows/src/GraphContextMenu.zig +++ b/graphcode-windows/src/GraphContextMenu.zig @@ -128,7 +128,7 @@ pub fn show( .quick_chats => append(menu, ids.new_quick_chat, "New Chat"), .project => |project| { append(menu, ids.open_project, "Open Project"); - append(menu, ids.new_project_loop, "New Loop..."); + append(menu, ids.new_project_loop, "New Loop...\tCtrl+N"); separator(menu); append(menu, ids.inspect_project_worktrees, "Worktrees..."); append(menu, ids.project_settings, "Project Settings..."); @@ -146,7 +146,7 @@ pub fn show( append(menu, ids.delete_project_loops, "Delete All Loops..."); }, .node => |node| { - append(menu, ids.open_terminal, "Open Terminal"); + append(menu, ids.open_terminal, "Open Terminal\tEnter"); if (node.unwired) { append(menu, ids.wire_node, "Wire it up"); append(menu, ids.mark_entry, "Mark as entry"); @@ -158,10 +158,10 @@ pub fn show( appendEnabled(menu, ids.arm_composite, "Arm Schedule", node.can_arm); separator(menu); } - append(menu, ids.edit_node, "Edit Details..."); - append(menu, ids.rename_node, "Rename..."); - append(menu, ids.stop_node, "Stop"); - append(menu, ids.delete_node, "Delete Loop..."); + append(menu, ids.edit_node, "Edit Details...\tCtrl+E"); + append(menu, ids.rename_node, "Rename...\tF2"); + append(menu, ids.stop_node, "Stop\tCtrl+S"); + append(menu, ids.delete_node, "Delete Loop...\tDelete"); }, .edge => { append(menu, ids.edit_edge, "Edit Edge..."); @@ -169,8 +169,8 @@ pub fn show( }, .quick_chat => { append(menu, ids.open_quick_chat, "Open Chat"); - append(menu, ids.rename_quick_chat, "Rename..."); - append(menu, ids.delete_quick_chat, "Delete Chat..."); + append(menu, ids.rename_quick_chat, "Rename...\tCtrl+Shift+Q"); + append(menu, ids.delete_quick_chat, "Delete Chat...\tCtrl+Shift+Delete"); }, } const command = c.TrackPopupMenu(