also looking at this
feat: portable typed window controls on Linux and Windows
#326
9 files
+652
-32
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -77,6 +77,8 @@ await settings.toggleFullscreen() | ||
| 77 | 77 | The bounds update preserves omitted coordinates and dimensions. Native window |
| 78 | 78 | managers can adjust requested geometry for decorations or screen constraints; |
| 79 | 79 | read `getBounds()` after a resize event if you need the actual result. |
| 80 | Creation-time `minWidth`/`minHeight` and `maxWidth`/`maxHeight` may constrain | |
| 81 | either axis independently; the native window system's own minimum still applies. | |
| 80 | 82 | |
| 81 | 83 | The older `createWindow(html, options)` overload remains available when the |
| 82 | 84 | content is already in a string. |
| @@ -1189,8 +1189,10 @@ fn setWindowFullscreen(entry: desktop_window_registry.Entry, fullscreen: bool) ! | ||
| 1189 | 1189 | setWindowStyle(hwnd, (style & ~WS_OVERLAPPEDWINDOW) | WS_POPUP); |
| 1190 | 1190 | const width = @as(i64, info.rcMonitor.right) - @as(i64, info.rcMonitor.left); |
| 1191 | 1191 | const height = @as(i64, info.rcMonitor.bottom) - @as(i64, info.rcMonitor.top); |
| 1192 | // HWND_TOP (null) raises a child requested by another page. Keeping | |
| 1193 | // its old Z-order can leave a borderless "fullscreen" behind that page. | |
| 1192 | 1194 | if (width <= 0 or height <= 0 or width > std.math.maxInt(c_int) or height > std.math.maxInt(c_int) or |
| 1193 | SetWindowPos(hwnd, null, @intCast(info.rcMonitor.left), @intCast(info.rcMonitor.top), @intCast(width), @intCast(height), SWP_NOZORDER | SWP_FRAMECHANGED | SWP_NOACTIVATE) == 0) | |
| 1195 | SetWindowPos(hwnd, null, @intCast(info.rcMonitor.left), @intCast(info.rcMonitor.top), @intCast(width), @intCast(height), SWP_FRAMECHANGED | SWP_NOACTIVATE) == 0) | |
| 1194 | 1196 | { |
| 1195 | 1197 | setWindowStyle(hwnd, style); |
| 1196 | 1198 | _ = SetWindowPos(hwnd, null, bounds.x, bounds.y, @intCast(bounds.width), @intCast(bounds.height), SWP_NOZORDER | SWP_FRAMECHANGED | SWP_NOACTIVATE); |
| @@ -1216,6 +1218,32 @@ fn setWindowFullscreen(entry: desktop_window_registry.Entry, fullscreen: bool) ! | ||
| 1216 | 1218 | } |
| 1217 | 1219 | } |
| 1218 | 1220 | |
| 1221 | fn applyTrackingLimits(info: *MINMAXINFO, limits: desktop_window_controls.Limits) void { | |
| 1222 | if (limits.minimum) |min| { | |
| 1223 | info.ptMinTrackSize.x = @max(info.ptMinTrackSize.x, @as(c_long, @intCast(min.width))); | |
| 1224 | info.ptMinTrackSize.y = @max(info.ptMinTrackSize.y, @as(c_long, @intCast(min.height))); | |
| 1225 | } | |
| 1226 | if (limits.maximum) |max| { | |
| 1227 | info.ptMaxTrackSize.x = @max(info.ptMinTrackSize.x, @min(info.ptMaxTrackSize.x, @as(c_long, @intCast(max.width)))); | |
| 1228 | info.ptMaxTrackSize.y = @max(info.ptMinTrackSize.y, @min(info.ptMaxTrackSize.y, @as(c_long, @intCast(max.height)))); | |
| 1229 | } | |
| 1230 | } | |
| 1231 | ||
| 1232 | test "Windows tracking limits retain native defaults on an omitted axis" { | |
| 1233 | var info: MINMAXINFO = .{ | |
| 1234 | .ptReserved = .{ .x = 0, .y = 0 }, | |
| 1235 | .ptMaxSize = .{ .x = 0, .y = 0 }, | |
| 1236 | .ptMaxPosition = .{ .x = 0, .y = 0 }, | |
| 1237 | .ptMinTrackSize = .{ .x = 120, .y = 80 }, | |
| 1238 | .ptMaxTrackSize = .{ .x = 2000, .y = 1300 }, | |
| 1239 | }; | |
| 1240 | applyTrackingLimits(&info, try desktop_window_controls.parseCreateLimits("{\"minWidth\":320,\"maxWidth\":1024}")); | |
| 1241 | try std.testing.expectEqual(@as(c_long, 320), info.ptMinTrackSize.x); | |
| 1242 | try std.testing.expectEqual(@as(c_long, 80), info.ptMinTrackSize.y); | |
| 1243 | try std.testing.expectEqual(@as(c_long, 1024), info.ptMaxTrackSize.x); | |
| 1244 | try std.testing.expectEqual(@as(c_long, 1300), info.ptMaxTrackSize.y); | |
| 1245 | } | |
| 1246 | ||
| 1219 | 1247 | fn windowTitle(allocator: std.mem.Allocator, hwnd: HWND) ![:0]u8 { |
| 1220 | 1248 | const length = GetWindowTextLengthW(hwnd); |
| 1221 | 1249 | if (length < 0) return error.NativeCallFailed; |
| @@ -1698,12 +1726,7 @@ fn WindowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.c | ||
| 1698 | 1726 | WM_GETMINMAXINFO => { |
| 1699 | 1727 | if (desktop_windows.byWindow(@intFromPtr(hwnd))) |entry| { |
| 1700 | 1728 | const info: *MINMAXINFO = @ptrFromInt(@as(usize, @bitCast(lParam))); |
| 1701 | if (entry.limits.minimum) |min| { | |
| 1702 | info.ptMinTrackSize = .{ .x = @intCast(min.width), .y = @intCast(min.height) }; | |
| 1703 | } | |
| 1704 | if (entry.limits.maximum) |max| { | |
| 1705 | info.ptMaxTrackSize = .{ .x = @intCast(max.width), .y = @intCast(max.height) }; | |
| 1706 | } | |
| 1729 | applyTrackingLimits(info, entry.limits); | |
| 1707 | 1730 | return 0; |
| 1708 | 1731 | } |
| 1709 | 1732 | }, |
| @@ -24,7 +24,7 @@ const mainPage = `<!doctype html><script> | ||
| 24 | 24 | const child = (cycle) => window.craft.window.open({ |
| 25 | 25 | name: 'settings', title: 'Child ' + cycle, |
| 26 | 26 | url: location.origin + '/child?cycle=' + cycle, |
| 27 | minWidth: 300, minHeight: 200, maxWidth: 1200, maxHeight: 900, | |
| 27 | minWidth: 300, maxWidth: 1200, | |
| 28 | 28 | }) |
| 29 | 29 | const call = (action, data = {}, id = 'settings') => window.craft.window._call(action, data, id) |
| 30 | 30 | const waitSize = async (predicate, label) => { |
| @@ -53,7 +53,7 @@ const mainPage = `<!doctype html><script> | ||
| 53 | 53 | const bounds = await window.craft.window._call('getBounds', {}, 'settings') |
| 54 | 54 | if (!(bounds.width > 0 && bounds.height > 0)) throw new Error('child bounds not routed to creator') |
| 55 | 55 | await call('setSize', { width: 100, height: 100 }) |
| 56 | await waitSize(size => size.width >= 280 && size.height >= 180, 'creation limits') | |
| 56 | await waitSize(size => size.width >= 280, 'one-axis creation limits') | |
| 57 | 57 | await window.craft.window._call('setSize', { width: 640, height: 480 }, 'settings') |
| 58 | 58 | let resized = false |
| 59 | 59 | for (let attempt = 0; attempt < 40; attempt++) { |