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 }