diff --git a/build.zig b/build.zig index 51127d5f..018e89fb 100755 --- a/build.zig +++ b/build.zig @@ -90,6 +90,8 @@ pub fn build(b: *std.Build) void { }); exe.headerpad_max_install_names = true; + if (target.result.os.tag == .macos) exe.linkFramework("CoreGraphics"); + const deps = .{ .vaxis = b.dependency("vaxis", .{ .target = target, .optimize = optimize }), .fzwatch = b.dependency("fzwatch", .{ .target = target, .optimize = optimize }), diff --git a/docs/config.md b/docs/config.md index bb80e969..4bbc72b9 100644 --- a/docs/config.md +++ b/docs/config.md @@ -62,6 +62,7 @@ Because fancy-cat provides sensible defaults, you only need to specify the optio "scroll_step": 100.0, "retry_delay": 0.2, "timeout": 5.0, + "detect_dpi": true, "dpi": 96.0, "history": 1000 }, @@ -208,7 +209,8 @@ The `General` section includes various display and timing settings. | `zoom_step` | Float | Zoom multiplier per keystroke | | `zoom_min` | Float | Minimum zoom level allowed | | `scroll_step` | Float (pixels) | Distance the viewport moves per scroll keystroke | -| `dpi` | Float | Resolution used for 100% zoom calculation | +| `detect_dpi` | Boolean | Enables pixel-density detection so that 100% zoom = actual size | +| `dpi` | Float | Pixel density to use if `detect_dpi` is false, or fallback if detection fails | | `retry_delay` | Float (seconds) | Delay before retrying to load a document or render a page | | `timeout` | Float (seconds) | Maximum time to keep retrying before giving up on loading a document or rendering a page | | `history` | Integer | Maximum number of entries in command history | diff --git a/src/config/Config.zig b/src/config/Config.zig index 5d49baf3..d4923337 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -88,6 +88,7 @@ pub const General = struct { retry_delay: f32 = 0.2, timeout: f32 = 5.0, // resolution + detect_dpi: bool = true, dpi: f32 = 96.0, // whole number (possibly 0) history: u32 = 1000, @@ -119,6 +120,7 @@ pub const General = struct { general.scroll_step = parseType(f32, val.object, "scroll_step", allocator, general.scroll_step); general.retry_delay = parseType(f32, val.object, "retry_delay", allocator, general.retry_delay); general.timeout = parseType(f32, val.object, "timeout", allocator, general.timeout); + general.detect_dpi = parseType(bool, val.object, "detect_dpi", allocator, general.detect_dpi); general.dpi = parseType(f32, val.object, "dpi", allocator, general.dpi); general.history = parseType(u32, val.object, "history", allocator, general.history); diff --git a/src/handlers/DocumentHandler.zig b/src/handlers/DocumentHandler.zig index 914b5704..e5371b93 100644 --- a/src/handlers/DocumentHandler.zig +++ b/src/handlers/DocumentHandler.zig @@ -76,8 +76,8 @@ pub fn zoomOut(self: *Self) void { self.pdf_handler.zoomOut(); } -pub fn setZoom(self: *Self, zoom_factor: f32) void { - self.pdf_handler.active_zoom = @max(zoom_factor, self.pdf_handler.config.general.zoom_min); +pub fn setZoom(self: *Self, percent: f32) void { + self.pdf_handler.setZoom(percent); } pub fn toggleColor(self: *Self) void { diff --git a/src/handlers/PdfHandler.zig b/src/handlers/PdfHandler.zig index 03a256b6..731d6a9f 100644 --- a/src/handlers/PdfHandler.zig +++ b/src/handlers/PdfHandler.zig @@ -4,6 +4,8 @@ const fastb64z = @import("fastb64z"); const vaxis = @import("vaxis"); const Config = @import("../config/Config.zig"); const types = @import("./types.zig"); +const Utilities = @import("../utilities/Utilities.zig"); + const c = @cImport({ @cInclude("fitz-z.h"); @cInclude("mupdf/fitz.h"); @@ -226,6 +228,13 @@ pub fn zoomOut(self: *Self) void { self.active_zoom /= self.config.general.zoom_step; } +pub fn setZoom(self: *Self, percent: f32) void { + var dpi = self.config.general.dpi; + if (self.config.general.detect_dpi) dpi = Utilities.getDPI() orelse dpi; + + self.active_zoom = @max(percent * dpi / 7200.0, self.config.general.zoom_min); +} + pub fn toggleColor(self: *Self) void { self.config.general.colorize = !self.config.general.colorize; } diff --git a/src/modes/CommandMode.zig b/src/modes/CommandMode.zig index 71a4a580..0cb2c31b 100644 --- a/src/modes/CommandMode.zig +++ b/src/modes/CommandMode.zig @@ -140,10 +140,7 @@ fn handleZoom(self: *Self, cmd: []const u8) bool { const number_str = cmd[0 .. cmd.len - 1]; if (std.fmt.parseFloat(f32, number_str)) |percent| { - // TODO detect DPI - const dpi = self.context.document_handler.pdf_handler.config.general.dpi; - const zoom_factor = (percent * dpi) / 7200.0; - self.context.document_handler.setZoom(zoom_factor); + self.context.document_handler.setZoom(percent); self.context.resetCurrentPage(); return true; } else |_| { diff --git a/src/utilities/Utilities.zig b/src/utilities/Utilities.zig new file mode 100644 index 00000000..49c70bec --- /dev/null +++ b/src/utilities/Utilities.zig @@ -0,0 +1,11 @@ +const builtin = @import("builtin"); +const utilities = struct { + pub const macos = @import("./macos.zig"); +}; + +pub fn getDPI() ?f32 { + return switch (builtin.os.tag) { + .macos => utilities.macos.getDPI(), + else => null, + }; +} diff --git a/src/utilities/macos.zig b/src/utilities/macos.zig new file mode 100644 index 00000000..36ad2a5a --- /dev/null +++ b/src/utilities/macos.zig @@ -0,0 +1,37 @@ +const std = @import("std"); +pub const c = @cImport({ + @cInclude("CoreGraphics/CoreGraphics.h"); +}); + +pub fn getDPI() ?f32 { + const display = getDisplay(); + + if (c.CGDisplayCopyDisplayMode(display)) |mode| { + defer c.CGDisplayModeRelease(mode); + const width_px = @as(f32, @floatFromInt(c.CGDisplayModeGetPixelWidth(mode))); + const width_mm = @as(f32, @floatCast(c.CGDisplayScreenSize(display).width)); + if (width_mm != 0) return std.math.round(width_px / width_mm * 25.4); + } + return null; +} + +fn getDisplay() c.CGDirectDisplayID { + const main_display = c.CGMainDisplayID(); + + var disp_count: u32 = 0; + if (c.CGGetActiveDisplayList(0, null, &disp_count) != c.kCGErrorSuccess or disp_count <= 1) return main_display; + + const win_list = c.CGWindowListCopyWindowInfo(c.kCGWindowListOptionOnScreenOnly | c.kCGWindowListExcludeDesktopElements, c.kCGNullWindowID) orelse return main_display; + defer c.CFRelease(win_list); + if (c.CFArrayGetCount(win_list) == 0) return main_display; + + const win = @as(c.CFDictionaryRef, @ptrCast(c.CFArrayGetValueAtIndex(win_list, 0))); + const win_bounds = c.CFDictionaryGetValue(win, c.kCGWindowBounds) orelse return main_display; + var win_rect: c.CGRect = undefined; + if (!c.CGRectMakeWithDictionaryRepresentation(@as(c.CFDictionaryRef, @ptrCast(win_bounds)), &win_rect)) return main_display; + + var display: c.CGDirectDisplayID = 0; + if (c.CGGetDisplaysWithRect(win_rect, 1, &display, &disp_count) == c.kCGErrorSuccess and disp_count > 0) return display; + + return main_display; +}