lib/sys/src/desktop.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 
  3 const capabilities = @import("capabilities.zig");
  4 const window_chrome = @import("chrome.zig");
  5 
  6 pub const required_capabilities = capabilities.pure();
  7 
  8 pub const GuiPolicyOptions = struct {
  9     uses_client_chrome_hit_test: bool = false,
 10 };
 11 
 12 pub const GuiPolicy = struct {
 13     use_vsync_hint: bool,
 14     use_custom_frame_control: bool,
 15     use_highdpi_framebuffer: bool,
 16     use_manual_framebuffer_draw_scale: bool,
 17     window_chrome: window_chrome.Mode,
 18     client_decorations: bool,
 19 };
 20 
 21 pub const DpiScale = struct {
 22     x: f32,
 23     y: f32,
 24 };
 25 
 26 pub const DrawArea = struct {
 27     width: i32,
 28     height: i32,
 29 };
 30 
 31 pub const Pointer = struct {
 32     x: f32,
 33     y: f32,
 34 };
 35 
 36 pub const PointerVector = struct {
 37     x: f32,
 38     y: f32,
 39 
 40     pub fn nonzero(self: PointerVector) bool {
 41         return self.x != 0 or self.y != 0;
 42     }
 43 };
 44 
 45 pub const DisplayScaleEvidence = struct {
 46     window_display: f32 = 1.0,
 47     display_content: f32 = 1.0,
 48     physical_dpi: f32 = 1.0,
 49     display_density: f32 = 1.0,
 50     pixel_density: f32 = 1.0,
 51 };
 52 
 53 pub const WindowScale = struct {
 54     ui: f32 = 1.0,
 55     window_display: f32 = 1.0,
 56     display_content: f32 = 1.0,
 57     physical_dpi: f32 = 1.0,
 58     display_density: f32 = 1.0,
 59     pixel_density: f32 = 1.0,
 60 };
 61 
 62 pub const WindowScaleEvidence = struct {
 63     has_window_content_scale: bool = false,
 64     window_display: f32 = 1.0,
 65     display_content: f32 = 1.0,
 66     physical_dpi: f32 = 1.0,
 67     display_density: f32 = 1.0,
 68     pixel_density: f32 = 1.0,
 69     ui_override: ?f32 = null,
 70 };
 71 
 72 pub const FramebufferScaleEvidence = struct {
 73     dpi_x: f32 = 1.0,
 74     dpi_y: f32 = 1.0,
 75     screen_width: f32 = 0,
 76     screen_height: f32 = 0,
 77     render_width: f32 = 0,
 78     render_height: f32 = 0,
 79 };
 80 
 81 const custom_frame_idle_wait_seconds: f64 = 0.001;
 82 
 83 pub fn guiPolicy(options: GuiPolicyOptions) GuiPolicy {
 84     return guiPolicyFor(capabilities.current.os, options);
 85 }
 86 
 87 pub fn guiPolicyFor(os_tag: std.Target.Os.Tag, options: GuiPolicyOptions) GuiPolicy {
 88     const chrome = defaultWindowChromeFor(os_tag, options);
 89     return .{
 90         .use_vsync_hint = !os_tag.isDarwin(),
 91         .use_custom_frame_control = os_tag == .macos,
 92         .use_highdpi_framebuffer = true,
 93         .use_manual_framebuffer_draw_scale = os_tag.isDarwin() and options.uses_client_chrome_hit_test,
 94         .window_chrome = chrome,
 95         .client_decorations = chrome == .client,
 96     };
 97 }
 98 
 99 pub fn customFrameWaitSeconds(target_fps: ?i32) f64 {
100     const fps = target_fps orelse return custom_frame_idle_wait_seconds;
101     if (fps <= 0) return custom_frame_idle_wait_seconds;
102     return 1.0 / @as(f64, @floatFromInt(fps));
103 }
104 
105 pub fn shouldManualSwapScreenBuffer(policy: GuiPolicy, drew_frame: bool) bool {
106     return policy.use_custom_frame_control and drew_frame;
107 }
108 
109 pub fn shouldUseDensityScaleFallback(scale: DisplayScaleEvidence) bool {
110     return shouldUseDensityScaleFallbackFor(capabilities.current.os, scale);
111 }
112 
113 pub fn shouldUseDensityScaleFallbackFor(os_tag: std.Target.Os.Tag, scale: DisplayScaleEvidence) bool {
114     if (os_tag != .linux) return false;
115     return scale.window_display <= 1.0 and
116         scale.display_content <= 1.0 and
117         scale.pixel_density <= 1.0 and
118         @max(scale.physical_dpi, scale.display_density) > 1.0;
119 }
120 
121 pub fn scaleEvidence(scale: WindowScale) DisplayScaleEvidence {
122     return .{
123         .window_display = scale.window_display,
124         .display_content = scale.display_content,
125         .physical_dpi = scale.physical_dpi,
126         .display_density = scale.display_density,
127         .pixel_density = scale.pixel_density,
128     };
129 }
130 
131 pub fn windowScaleFromEvidence(evidence: WindowScaleEvidence) WindowScale {
132     return windowScaleFromEvidenceFor(capabilities.current.os, evidence);
133 }
134 
135 pub fn windowScaleFromEvidenceFor(os_tag: std.Target.Os.Tag, evidence: WindowScaleEvidence) WindowScale {
136     var scale = WindowScale{
137         .window_display = usableScale(evidence.window_display),
138         .display_content = usableScale(evidence.display_content),
139         .physical_dpi = usableScale(evidence.physical_dpi),
140         .display_density = usableScale(evidence.display_density),
141         .pixel_density = usableScale(evidence.pixel_density),
142     };
143     scale.ui = logicalContentScale(scale.window_display, scale.display_content, scale.pixel_density);
144 
145     if ((!evidence.has_window_content_scale or shouldUseDensityScaleFallbackFor(os_tag, scaleEvidence(scale))) and scale.ui <= 1.0) {
146         scale.ui = @max(scale.physical_dpi, scale.display_density);
147     }
148 
149     if (evidence.ui_override) |override| {
150         scale.ui = usableScale(override);
151     }
152 
153     return scale;
154 }
155 
156 pub fn logicalContentScale(window_display: f32, display_content: f32, pixel_density: f32) f32 {
157     var scale: f32 = 1.0;
158     const density = usableScale(pixel_density);
159     const window_scale = usableScale(window_display);
160     if (window_scale > 0 and density > 0) {
161         scale = @max(scale, window_scale / density);
162     }
163     scale = @max(scale, usableScale(display_content));
164     return scale;
165 }
166 
167 pub fn framebufferScaleFromDimensions(
168     screen_width: f32,
169     screen_height: f32,
170     render_width: f32,
171     render_height: f32,
172 ) ?f32 {
173     var scale: f32 = 1.0;
174     var have_dimension = false;
175     if (screen_width > 0 and render_width > 0) {
176         scale = @max(scale, render_width / screen_width);
177         have_dimension = true;
178     }
179     if (screen_height > 0 and render_height > 0) {
180         scale = @max(scale, render_height / screen_height);
181         have_dimension = true;
182     }
183     return if (have_dimension) scale else null;
184 }
185 
186 pub fn framebufferScaleFromEvidence(evidence: FramebufferScaleEvidence) f32 {
187     var scale = @max(usableScale(evidence.dpi_x), usableScale(evidence.dpi_y));
188     if (framebufferScaleFromDimensions(evidence.screen_width, evidence.screen_height, evidence.render_width, evidence.render_height)) |dimension_scale| {
189         scale = @max(scale, dimension_scale);
190     }
191     return scale;
192 }
193 
194 pub fn displayDensityScaleFor(width: f32, height: f32, display_name: ?[]const u8) f32 {
195     if (width <= 0 or height <= 0) return 1.0;
196 
197     if (display_name) |name| {
198         if (displayNameDiagonalInches(name)) |diagonal_inches| {
199             if (diagonal_inches > 0) {
200                 const diagonal_pixels = @sqrt(width * width + height * height);
201                 return scaleFromDpi(diagonal_pixels / diagonal_inches);
202             }
203         }
204     }
205 
206     const long_edge = @max(width, height);
207     const short_edge = @min(width, height);
208     if (long_edge >= 3800 and short_edge >= 2100) return 1.25;
209     if (long_edge >= 3000 and short_edge >= 1800) return 1.15;
210     return 1.0;
211 }
212 
213 pub fn displayNameDiagonalInches(name: []const u8) ?f32 {
214     const quote_index = std.mem.lastIndexOfScalar(u8, name, '"') orelse return null;
215     var end = quote_index;
216     while (end > 0 and std.ascii.isWhitespace(name[end - 1])) : (end -= 1) {}
217 
218     var start = end;
219     while (start > 0) {
220         const byte = name[start - 1];
221         if (!std.ascii.isDigit(byte) and byte != '.') break;
222         start -= 1;
223     }
224     if (start == end) return null;
225     return std.fmt.parseFloat(f32, name[start..end]) catch null;
226 }
227 
228 pub fn scaleFromDpi(dpi: f32) f32 {
229     const scale = dpi / 96.0;
230     if (!std.math.isFinite(scale) or scale < 1.15) return 1.0;
231     return scale;
232 }
233 
234 pub fn scaleFromPhysicalWidthMm(pixel_width: f32, physical_width_mm: f32) f32 {
235     if (pixel_width <= 0 or physical_width_mm <= 0) return 1.0;
236     return scaleFromDpi(pixel_width / (physical_width_mm / 25.4));
237 }
238 
239 fn usableScale(raw_scale: f32) f32 {
240     if (!std.math.isFinite(raw_scale) or raw_scale <= 0) return 1.0;
241     return raw_scale;
242 }
243 
244 pub fn supportsGraphicalWindows() bool {
245     return supportsGraphicalWindowsFor(capabilities.current.os);
246 }
247 
248 pub fn supportsGraphicalWindowsFor(os_tag: std.Target.Os.Tag) bool {
249     return os_tag == .linux or os_tag == .macos;
250 }
251 
252 fn defaultWindowChromeFor(os_tag: std.Target.Os.Tag, options: GuiPolicyOptions) window_chrome.Mode {
253     if (os_tag.isDarwin()) return .native;
254     return if (options.uses_client_chrome_hit_test) .client else .native;
255 }
256 
257 test "GUI policy captures desktop platform choices" {
258     const mac = guiPolicyFor(.macos, .{ .uses_client_chrome_hit_test = true });
259     try std.testing.expect(!mac.use_vsync_hint);
260     try std.testing.expect(mac.use_custom_frame_control);
261     try std.testing.expect(mac.use_highdpi_framebuffer);
262     try std.testing.expect(mac.use_manual_framebuffer_draw_scale);
263     try std.testing.expectEqual(window_chrome.Mode.native, mac.window_chrome);
264     try std.testing.expect(shouldManualSwapScreenBuffer(mac, true));
265     try std.testing.expect(!shouldManualSwapScreenBuffer(mac, false));
266     try std.testing.expectEqual(@as(f64, custom_frame_idle_wait_seconds), customFrameWaitSeconds(null));
267     try std.testing.expectEqual(@as(f64, 1.0 / 30.0), customFrameWaitSeconds(30));
268 
269     const linux = guiPolicyFor(.linux, .{ .uses_client_chrome_hit_test = true });
270     try std.testing.expect(linux.use_vsync_hint);
271     try std.testing.expect(!linux.use_custom_frame_control);
272     try std.testing.expect(linux.use_highdpi_framebuffer);
273     try std.testing.expect(!linux.use_manual_framebuffer_draw_scale);
274     try std.testing.expectEqual(window_chrome.Mode.client, linux.window_chrome);
275     try std.testing.expect(linux.client_decorations);
276     try std.testing.expect(!shouldManualSwapScreenBuffer(linux, true));
277 }
278 
279 test "Linux density fallback requires only density evidence" {
280     try std.testing.expect(shouldUseDensityScaleFallbackFor(.linux, .{
281         .window_display = 1.0,
282         .display_content = 1.0,
283         .physical_dpi = 1.0,
284         .display_density = 1.43,
285         .pixel_density = 1.0,
286     }));
287     try std.testing.expect(!shouldUseDensityScaleFallbackFor(.macos, .{
288         .window_display = 1.0,
289         .display_content = 1.0,
290         .physical_dpi = 1.0,
291         .display_density = 1.43,
292         .pixel_density = 1.0,
293     }));
294 }
295 
296 test "display scale helpers separate framebuffer and logical UI scale" {
297     try std.testing.expectApproxEqAbs(@as(f32, 1.0), logicalContentScale(2.0, 1.0, 2.0), 0.001);
298     try std.testing.expectApproxEqAbs(@as(f32, 1.25), logicalContentScale(2.5, 1.25, 2.0), 0.001);
299     try std.testing.expectApproxEqAbs(@as(f32, 1.5), logicalContentScale(1.5, 1.0, 1.0), 0.001);
300     try std.testing.expectApproxEqAbs(@as(f32, 1.0), framebufferScaleFromDimensions(640, 480, 640, 480).?, 0.001);
301     try std.testing.expectApproxEqAbs(@as(f32, 2.0), framebufferScaleFromDimensions(640, 480, 1280, 960).?, 0.001);
302     try std.testing.expectApproxEqAbs(@as(f32, 2.0), framebufferScaleFromEvidence(.{ .dpi_x = 1.0, .dpi_y = 1.0, .screen_width = 640, .screen_height = 480, .render_width = 1280, .render_height = 960 }), 0.001);
303     try std.testing.expectApproxEqAbs(@as(f32, 2.0), framebufferScaleFromEvidence(.{ .dpi_x = 2.0, .dpi_y = 1.0 }), 0.001);
304     try std.testing.expectApproxEqAbs(@as(f32, 1.0), scaleFromPhysicalWidthMm(1920, 508), 0.001);
305     try std.testing.expectApproxEqAbs(@as(f32, 2.0), scaleFromPhysicalWidthMm(1920, 254), 0.001);
306 }
307 
308 test "window scale evidence applies platform fallback policy" {
309     const linux = windowScaleFromEvidenceFor(.linux, .{
310         .has_window_content_scale = true,
311         .window_display = 1.0,
312         .display_content = 1.0,
313         .physical_dpi = 1.0,
314         .display_density = 1.43,
315         .pixel_density = 1.0,
316     });
317     try std.testing.expectApproxEqAbs(@as(f32, 1.43), linux.ui, 0.001);
318 
319     const mac_without_window_evidence = windowScaleFromEvidenceFor(.macos, .{
320         .has_window_content_scale = false,
321         .physical_dpi = 2.0,
322     });
323     try std.testing.expectApproxEqAbs(@as(f32, 2.0), mac_without_window_evidence.ui, 0.001);
324 
325     const override = windowScaleFromEvidenceFor(.linux, .{
326         .ui_override = 1.75,
327         .display_density = 1.25,
328     });
329     try std.testing.expectApproxEqAbs(@as(f32, 1.75), override.ui, 0.001);
330 }
331 
332 test "display density scale uses monitor names and resolution fallbacks" {
333     try std.testing.expectApproxEqAbs(@as(f32, 32), displayNameDiagonalInches("HDMI-A-2 32\"").?, 0.001);
334     try std.testing.expectApproxEqAbs(@as(f32, 13.5), displayNameDiagonalInches("Built-in Display 13.5\"").?, 0.001);
335     try std.testing.expectEqual(@as(?f32, null), displayNameDiagonalInches("HDMI-A-2"));
336     try std.testing.expectApproxEqAbs(@as(f32, 1.25), displayDensityScaleFor(3840, 2160, null), 0.001);
337     try std.testing.expectApproxEqAbs(@as(f32, 1.15), displayDensityScaleFor(3000, 1800, null), 0.001);
338 }
339 
340 test "graphical window capability names supported desktop targets" {
341     try std.testing.expect(supportsGraphicalWindowsFor(.linux));
342     try std.testing.expect(supportsGraphicalWindowsFor(.macos));
343     try std.testing.expect(!supportsGraphicalWindowsFor(.windows));
344 }