winui-svelte 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/API.md +118 -6
  2. package/README.md +70 -90
  3. package/dist/bindings/.dynwinrt-managed +1 -1
  4. package/dist/components/convert.d.ts +3 -3
  5. package/dist/components/convert.d.ts.map +1 -1
  6. package/dist/components/fluent-glyph.d.ts +4 -0
  7. package/dist/components/fluent-glyph.d.ts.map +1 -1
  8. package/dist/components/generated/controls/FontIcon.svelte.d.ts +1 -1
  9. package/dist/components/generated/controls/FontIconSource.svelte.d.ts +1 -1
  10. package/dist/components/generated/controls/UIElement.svelte.d.ts +1 -1
  11. package/dist/components/index.d.ts +3 -1
  12. package/dist/components/index.d.ts.map +1 -1
  13. package/dist/css/runtime/resources.d.ts +1 -1
  14. package/dist/dev/dev-window.d.ts +23 -0
  15. package/dist/dev/dev-window.d.ts.map +1 -0
  16. package/dist/dev/dev-window.js +42 -0
  17. package/dist/dev/dev-window.js.map +1 -0
  18. package/dist/dev/index.d.ts +3 -0
  19. package/dist/dev/index.d.ts.map +1 -0
  20. package/dist/dev/index.js +2 -0
  21. package/dist/dev/index.js.map +1 -0
  22. package/dist/dev/start-dev.d.ts +47 -0
  23. package/dist/dev/start-dev.d.ts.map +1 -0
  24. package/dist/dev/start-dev.js +81 -0
  25. package/dist/dev/start-dev.js.map +1 -0
  26. package/dist/enums.d.ts +2 -0
  27. package/dist/enums.d.ts.map +1 -1
  28. package/dist/enums.js +2 -0
  29. package/dist/enums.js.map +1 -1
  30. package/dist/web-apis/window-geometry.d.ts +39 -0
  31. package/dist/web-apis/window-geometry.d.ts.map +1 -0
  32. package/dist/web-apis/window-geometry.js +95 -0
  33. package/dist/web-apis/window-geometry.js.map +1 -0
  34. package/dist/win2d/Canvas.svelte +1 -0
  35. package/dist/window/anchor.d.ts +92 -0
  36. package/dist/window/anchor.d.ts.map +1 -0
  37. package/dist/window/anchor.js +100 -0
  38. package/dist/window/anchor.js.map +1 -0
  39. package/dist/window/backdrop.d.ts +4 -1
  40. package/dist/window/backdrop.d.ts.map +1 -1
  41. package/dist/window/backdrop.js +10 -2
  42. package/dist/window/backdrop.js.map +1 -1
  43. package/dist/window/create-window.d.ts +1 -1
  44. package/dist/window/create-window.d.ts.map +1 -1
  45. package/dist/window/create-window.js +3 -1
  46. package/dist/window/create-window.js.map +1 -1
  47. package/dist/window/css-pixels.d.ts +14 -0
  48. package/dist/window/css-pixels.d.ts.map +1 -0
  49. package/dist/window/css-pixels.js +12 -0
  50. package/dist/window/css-pixels.js.map +1 -0
  51. package/dist/window/dock.d.ts +51 -0
  52. package/dist/window/dock.d.ts.map +1 -0
  53. package/dist/window/dock.js +69 -0
  54. package/dist/window/dock.js.map +1 -0
  55. package/dist/window/index.d.ts +4 -0
  56. package/dist/window/index.d.ts.map +1 -1
  57. package/dist/window/index.js +2 -0
  58. package/dist/window/index.js.map +1 -1
  59. package/dist/window/native.d.ts +2 -0
  60. package/dist/window/native.d.ts.map +1 -1
  61. package/dist/window/native.js +10 -0
  62. package/dist/window/native.js.map +1 -1
  63. package/dist/window/transparent-backdrop.d.ts +11 -0
  64. package/dist/window/transparent-backdrop.d.ts.map +1 -0
  65. package/dist/window/transparent-backdrop.js +35 -0
  66. package/dist/window/transparent-backdrop.js.map +1 -0
  67. package/dist/window/window-handle.d.ts +17 -0
  68. package/dist/window/window-handle.d.ts.map +1 -1
  69. package/dist/window/window-handle.js +23 -0
  70. package/dist/window/window-handle.js.map +1 -1
  71. package/dist/window/window-host.svelte.d.ts +17 -12
  72. package/dist/window/window-host.svelte.d.ts.map +1 -1
  73. package/dist/window/window-host.svelte.js +168 -14
  74. package/dist/window/window-host.svelte.js.map +1 -1
  75. package/dist/winui.d.ts +5 -0
  76. package/dist/winui.d.ts.map +1 -1
  77. package/dist/winui.js +5 -0
  78. package/dist/winui.js.map +1 -1
  79. package/package.json +25 -7
  80. package/src/components/convert.ts +3 -3
  81. package/src/components/fluent-glyph.ts +6 -0
  82. package/src/components/generated/controls/FontIcon.svelte.ts +1 -1
  83. package/src/components/generated/controls/FontIconSource.svelte.ts +1 -1
  84. package/src/components/generated/controls/UIElement.svelte.ts +1 -1
  85. package/src/components/index.ts +3 -1
  86. package/src/dev/dev-window.ts +63 -0
  87. package/src/dev/index.ts +2 -0
  88. package/src/dev/start-dev.ts +120 -0
  89. package/src/enums.ts +2 -0
  90. package/src/web-apis/window-geometry.ts +106 -0
  91. package/src/window/anchor.ts +156 -0
  92. package/src/window/backdrop.ts +11 -2
  93. package/src/window/create-window.ts +5 -1
  94. package/src/window/css-pixels.ts +18 -0
  95. package/src/window/dock.ts +99 -0
  96. package/src/window/index.ts +4 -0
  97. package/src/window/native.ts +12 -0
  98. package/src/window/transparent-backdrop.ts +38 -0
  99. package/src/window/window-handle.ts +26 -0
  100. package/src/window/window-host.svelte.ts +210 -27
  101. package/src/winui.ts +5 -0
  102. package/web-types.json +3203 -1507
@@ -0,0 +1,99 @@
1
+ import type { WindowAnchor } from "./anchor.ts";
2
+ import { dockedBounds } from "./anchor.ts";
3
+ import { foregroundWindowHandle } from "./native.ts";
4
+ import type { AppWindow } from "#src/winui.js";
5
+ import { DisplayArea, DisplayAreaFallback } from "#src/winui.js";
6
+
7
+ /**
8
+ * Which display a window is docked to: the one the user is working on (the
9
+ * foreground window's), the one the window is on, or the primary one.
10
+ */
11
+ export const DockDisplay = {
12
+ active: "active",
13
+ current: "current",
14
+ primary: "primary"
15
+ } as const;
16
+
17
+ /** One of `DockDisplay`'s values. */
18
+ export type DockDisplay = (typeof DockDisplay)[keyof typeof DockDisplay];
19
+
20
+ const NO_WINDOW = 0n;
21
+
22
+ function activeDisplayArea(appWindow: AppWindow) {
23
+ const foreground = foregroundWindowHandle();
24
+ const hasForeground = foreground !== NO_WINDOW;
25
+ const windowId = hasForeground ? { value: foreground } : appWindow.id;
26
+ return DisplayArea.getFromWindowId(windowId, DisplayAreaFallback.Nearest);
27
+ }
28
+
29
+ const DISPLAY_FINDERS = {
30
+ [DockDisplay.active]: activeDisplayArea,
31
+ [DockDisplay.current]: appWindow => DisplayArea.getFromWindowId(appWindow.id, DisplayAreaFallback.Nearest),
32
+ [DockDisplay.primary]: () => DisplayArea.primary
33
+ } as const satisfies Record<DockDisplay, (appWindow: AppWindow) => DisplayArea>;
34
+
35
+ /**
36
+ * The display area `display` names for `appWindow`. The `active` one is the
37
+ * display of the foreground window, so an overlay that shows up while the user
38
+ * works on a second monitor lands there.
39
+ *
40
+ * @param options - The window and which display to find.
41
+ * @returns The WinUI `DisplayArea`: `workArea` is its desktop minus the taskbar, in device pixels.
42
+ */
43
+ export function displayAreaOf({ appWindow, display }: {
44
+ appWindow: AppWindow;
45
+ display: DockDisplay;
46
+ }) {
47
+ return DISPLAY_FINDERS[display](appWindow);
48
+ }
49
+
50
+ /** Where `dockWindow` puts a window. */
51
+ export interface DockOptions {
52
+ /** The corner of the work area the window sits against. */
53
+ anchor: WindowAnchor;
54
+
55
+ /** Device pixels between the window and the two edges at that corner; 0 by default. */
56
+ inset?: number;
57
+
58
+ /** The display to dock to; the active one by default. */
59
+ display?: DockDisplay;
60
+ }
61
+
62
+ /**
63
+ * Moves the window against the `anchor` corner of a display's work area (the
64
+ * display minus the taskbar), `inset` device pixels in from both edges, with
65
+ * one `AppWindow.MoveAndResize`. The window keeps its size.
66
+ *
67
+ * A window moved to a display with another scale first goes there at its old
68
+ * size and is docked once Windows has rescaled it, so the corner it ends on is
69
+ * the one asked for at the size it really has.
70
+ *
71
+ * @param options - The window, the corner, the inset in device pixels (0 by
72
+ * default) and the display (the active one by default).
73
+ */
74
+ export function dockWindow({ appWindow, anchor, inset = 0, display = DockDisplay.active }: DockOptions & {
75
+ appWindow: AppWindow;
76
+ }) {
77
+ const area = displayAreaOf({
78
+ appWindow,
79
+ display
80
+ });
81
+ const { workArea } = area;
82
+ const currentArea = DisplayArea.getFromWindowId(appWindow.id, DisplayAreaFallback.Nearest);
83
+ const isOnOtherDisplay = currentArea.displayId.value !== area.displayId.value;
84
+ if (isOnOtherDisplay) {
85
+ appWindow.move({
86
+ x: workArea.x,
87
+ y: workArea.y
88
+ });
89
+ }
90
+
91
+ appWindow.moveAndResize(
92
+ dockedBounds({
93
+ area: workArea,
94
+ size: appWindow.size,
95
+ anchor,
96
+ inset
97
+ })
98
+ );
99
+ }
@@ -2,6 +2,10 @@ export type { RunWindowOptions } from "./application.ts";
2
2
  export { exitApplication, runWindow } from "./application.ts";
3
3
  export type { AppWindow, WindowOptions } from "./create-window.ts";
4
4
  export { Backdrop } from "./backdrop.ts";
5
+ export type { WindowBounds, WindowSize } from "./anchor.ts";
6
+ export { SizeToContent, WindowAnchor } from "./anchor.ts";
7
+ export type { DockOptions } from "./dock.ts";
8
+ export { displayAreaOf, DockDisplay, dockWindow } from "./dock.ts";
5
9
  export type { CaptionButtonColors, WindowRegion } from "./chrome.ts";
6
10
  export { TitleBarHeight, WindowPresentation, WindowTheme } from "./chrome.ts";
7
11
  export { createWindow } from "./create-window.ts";
@@ -58,6 +58,18 @@ export function scaleOf(hwnd: bigint) {
58
58
  return dpi > 0 ? dpi / BASE_DPI : 1;
59
59
  }
60
60
 
61
+ const getForegroundWindow = bind({
62
+ dll: "user32.dll",
63
+ entryPoint: "GetForegroundWindow",
64
+ parameters: [],
65
+ returnType: "pointer"
66
+ });
67
+
68
+ /** The handle of the window the user is working in; 0 when no window has the foreground. */
69
+ export function foregroundWindowHandle() {
70
+ return addressOf(getForegroundWindow.invoke([]).returnValue);
71
+ }
72
+
61
73
  /**
62
74
  * Makes the window `owner` own the window `hwnd`: the owned window stays above
63
75
  * its owner, and closes, minimizes and restores with it.
@@ -0,0 +1,38 @@
1
+ import { write } from "#src/renderer/host/reflect.js";
2
+ import type { Window } from "#src/winui.js";
3
+ import { ICompositionSupportsSystemBackdrop, SystemCompositor } from "#src/winui.js";
4
+
5
+ // The Windows App SDK's own Microsoft.UI.Xaml.Media.TransparentTintBackdrop is
6
+ // not in the metadata of any release this package restores (2.2 and 2.3 have
7
+ // DesktopAcrylicBackdrop, MicaBackdrop and the abstract SystemBackdrop only),
8
+ // so it cannot be generated. What it does is what a window offers through
9
+ // ICompositionSupportsSystemBackdrop: a composition brush placed behind the
10
+ // content, one of the system's (Windows.UI.Composition) and not of the window's
11
+ // own compositor (Microsoft.UI.Composition), which is what the interface takes.
12
+ // A color brush with alpha 0 there makes the window show the desktop through
13
+ // every pixel the content leaves clear, with per-pixel alpha.
14
+
15
+ const CLEAR = {
16
+ a: 0,
17
+ r: 0,
18
+ g: 0,
19
+ b: 0
20
+ };
21
+
22
+ /**
23
+ * Puts a fully transparent system backdrop behind `window`'s content: the
24
+ * desktop shows through wherever the content paints nothing, and a partly
25
+ * transparent pixel blends with it. The window must have no XAML backdrop of
26
+ * its own (`applyBackdrop` takes it away first).
27
+ *
28
+ * @returns What takes the backdrop away again.
29
+ */
30
+ export function attachTransparentBackdrop(window: Window) {
31
+ const target = window.as(ICompositionSupportsSystemBackdrop);
32
+ target.systemBackdrop = new SystemCompositor().createColorBrush(CLEAR);
33
+ return () => {
34
+ // The binding types the property as non-null, but null is how the target
35
+ // lets a backdrop go.
36
+ write(target, "systemBackdrop", null);
37
+ };
38
+ }
@@ -1,7 +1,10 @@
1
1
  import { applyShownInSwitchers } from "./apply-chrome.ts";
2
+ import type { DockOptions } from "./dock.ts";
3
+ import { dockWindow } from "./dock.ts";
2
4
  import { scaleOf } from "./native.ts";
3
5
  import type { OpenFilePickerOptions, PickerOptions, SaveFilePickerOptions } from "./pickers.ts";
4
6
  import { pickFile, pickFilePaths, pickFolder, saveFile } from "./pickers.ts";
7
+ import { WindowGeometry } from "#src/web-apis/window-geometry.js";
5
8
  import type { Window } from "#src/winui.js";
6
9
  import {
7
10
  FileOpenPicker,
@@ -18,11 +21,18 @@ import {
18
21
  export class WindowHandle {
19
22
  /** The native WinUI window. */
20
23
  readonly window: Window;
24
+
25
+ /**
26
+ * The window's size and place as the web has them: `outerWidth`,
27
+ * `screenX`, `resizeTo`, `moveBy` and the rest, in CSS pixels.
28
+ */
29
+ readonly geometry: WindowGeometry;
21
30
  private isClosed = false;
22
31
  private closeGuard = () => false;
23
32
 
24
33
  constructor(window: Window) {
25
34
  this.window = window;
35
+ this.geometry = new WindowGeometry(window.appWindow, () => this.scale);
26
36
  window.onClosed(() => {
27
37
  this.isClosed = true;
28
38
  });
@@ -59,6 +69,22 @@ export class WindowHandle {
59
69
  });
60
70
  }
61
71
 
72
+ /**
73
+ * Moves the window against a corner of a display's work area (the display
74
+ * minus the taskbar), `inset` device pixels in from both edges: an overlay
75
+ * that sits bottom-right above the taskbar of the display the user is on.
76
+ * With `sizeToContent` and the same `anchor` on the window, the corner stays
77
+ * as the content grows.
78
+ *
79
+ * @param options - The corner, the inset in device pixels and the display (the active one by default).
80
+ */
81
+ dock(options: DockOptions) {
82
+ dockWindow({
83
+ appWindow: this.appWindow,
84
+ ...options
85
+ });
86
+ }
87
+
62
88
  /** Shows the window and gives it focus. */
63
89
  activate() {
64
90
  this.window.activate();
@@ -1,3 +1,5 @@
1
+ import type { SizeToContent, WindowAnchor, WindowBounds, WindowSize } from "./anchor.ts";
2
+ import { fittedBounds, SizeToContent as SizeToContentKind, WindowAnchor as WindowAnchorKind } from "./anchor.ts";
1
3
  import {
2
4
  applyCaptionButtonColors,
3
5
  applyPresentation,
@@ -13,6 +15,7 @@ import { isBackgroundRun } from "./background-mode.ts";
13
15
  import { backgroundPlacement, keepInBackground } from "./background.ts";
14
16
  import type { CaptionButtonColors, CaptionButtonProperty, TitleBarHeight, WindowRegion } from "./chrome.ts";
15
17
  import { TitleBarHeight as TitleBarHeightKind, WindowPresentation, WindowTheme } from "./chrome.ts";
18
+ import { toDevicePixels } from "./css-pixels.ts";
16
19
  import {
17
20
  clearWindowClip,
18
21
  clipToRoundedRectangle,
@@ -28,22 +31,20 @@ import type { FrameworkElement } from "#src/winui.js";
28
31
  import {
29
32
  DisplayArea,
30
33
  DisplayAreaFallback,
34
+ Grid,
31
35
  NonClientRegionKind,
36
+ ScrollBarVisibility,
37
+ ScrollMode,
38
+ ScrollViewer,
32
39
  Window,
33
40
  WindowActivationState
34
41
  } from "#src/winui.js";
35
42
 
43
+ export type { WindowBounds, WindowSize } from "./anchor.ts";
44
+
36
45
  // What <Window> (components/hand-written/Window.svelte) does: a native window
37
46
  // around the component's content, kept in line with its props.
38
47
 
39
- /** The window's position and size, in device pixels, as AppWindow has them. */
40
- export interface WindowBounds {
41
- x: number;
42
- y: number;
43
- width: number;
44
- height: number;
45
- }
46
-
47
48
  /**
48
49
  * Where the window opens, in device pixels; a size left out is the width or
49
50
  * height prop's.
@@ -110,6 +111,23 @@ export interface WindowProps {
110
111
  width?: number;
111
112
  height?: number;
112
113
 
114
+ /**
115
+ * Which sides of the window follow its content: the content is measured
116
+ * after every layout pass and the window is resized to it (frame included,
117
+ * within the size limits) with one `AppWindow.MoveAndResize`, so a growing
118
+ * content never flickers. A side that follows the content ignores the
119
+ * `width` or `height` prop and the user's resizing. Windowed presentation
120
+ * only; the default is `none`.
121
+ */
122
+ sizeToContent?: SizeToContent | undefined;
123
+
124
+ /**
125
+ * The corner that stays where it is when the window is sized to its content
126
+ * (and when the scale changes the size): a window docked bottom-right whose
127
+ * content grows taller grows upward. The default is `topLeft`.
128
+ */
129
+ anchor?: WindowAnchor | undefined;
130
+
113
131
  /** The least the user can resize to, in device-independent pixels. */
114
132
  minWidth?: number;
115
133
  minHeight?: number;
@@ -228,12 +246,6 @@ export interface WindowProps {
228
246
  onboundschanged?: (bounds: WindowBounds) => void;
229
247
  }
230
248
 
231
- /** A size in device pixels. */
232
- export interface WindowSize {
233
- width: number;
234
- height: number;
235
- }
236
-
237
249
  /** What `attachWindow` opens a window around, and how. */
238
250
  export interface AttachOptions {
239
251
  content: FrameworkElement;
@@ -244,11 +256,40 @@ export interface AttachOptions {
244
256
  size?: WindowSize | undefined;
245
257
  }
246
258
 
247
- function round({ value, scale }: {
248
- value: number;
259
+ // A side of the window in device pixels: the prop's, unless the prop is left
260
+ // out or the content sets that side.
261
+ function sideOf({ value, current, isFitted, scale }: {
262
+ value: number | undefined;
263
+ current: number;
264
+ isFitted: boolean;
265
+ scale: number;
266
+ }) {
267
+ const isKept = value === undefined || isFitted;
268
+ if (isKept) {
269
+ return current;
270
+ }
271
+
272
+ return toDevicePixels({
273
+ value,
274
+ scale
275
+ });
276
+ }
277
+
278
+ // A size limit in device pixels; `fallback` when the app names none.
279
+ function limitOf({ value, fallback, scale }: {
280
+ value: number | undefined;
281
+ fallback: number;
249
282
  scale: number;
250
283
  }) {
251
- return Math.round(value * scale);
284
+ const isNamed = value !== undefined;
285
+ if (!isNamed) {
286
+ return fallback;
287
+ }
288
+
289
+ return toDevicePixels({
290
+ value,
291
+ scale
292
+ });
252
293
  }
253
294
 
254
295
  // A prop the app left out is undefined; a guard names the test.
@@ -281,6 +322,8 @@ class WindowHost {
281
322
  private isClipApplied = false;
282
323
  private isTopApplied = false;
283
324
  private hasShown = false;
325
+ private isFitting = false;
326
+ private scroller: ScrollViewer | null = null;
284
327
  private readonly isTakenOver: boolean;
285
328
  private readonly openSize: WindowSize | undefined;
286
329
  private appliedSize: {
@@ -314,6 +357,7 @@ class WindowHost {
314
357
 
315
358
  this.handle.guardClose(() => this.isCloseRefused());
316
359
  window.content = content;
360
+ this.keepInLine(() => this.syncContainer());
317
361
  this.keepInLine(() => this.syncText());
318
362
  this.keepInLine(() => this.syncBackdrop());
319
363
  this.keepInLine(() => this.syncTheme());
@@ -330,6 +374,7 @@ class WindowHost {
330
374
  }
331
375
 
332
376
  $effect(() => this.syncSize());
377
+ $effect(() => this.fitToContent());
333
378
  this.syncOwner();
334
379
  this.listen();
335
380
  this.watchScale();
@@ -379,6 +424,51 @@ class WindowHost {
379
424
  $effect(sync);
380
425
  }
381
426
 
427
+ // A measure offers an element the space it has and clamps what it asks for
428
+ // to that, so a root as large as the window could never ask for more or
429
+ // less than the window is. Inside a ScrollViewer that may scroll along a
430
+ // side the content sets, the root is offered all the room there (the
431
+ // scroll bars stay hidden), and what it asks for is what it needs.
432
+ private syncContainer() {
433
+ const { isWidthFitted, isHeightFitted } = this.fittedSides;
434
+ const isAnySideFitted = isWidthFitted || isHeightFitted;
435
+ const isFitted = isAnySideFitted && this.isWindowed;
436
+ const { window } = this.handle;
437
+ if (!isFitted) {
438
+ this.uncontain();
439
+ return;
440
+ }
441
+
442
+ const scroller = this.scroller ?? new ScrollViewer();
443
+ scroller.horizontalScrollBarVisibility = isWidthFitted ? ScrollBarVisibility.Hidden : ScrollBarVisibility.Disabled;
444
+ scroller.horizontalScrollMode = isWidthFitted ? ScrollMode.Enabled : ScrollMode.Disabled;
445
+ scroller.verticalScrollBarVisibility = isHeightFitted ? ScrollBarVisibility.Hidden : ScrollBarVisibility.Disabled;
446
+ scroller.verticalScrollMode = isHeightFitted ? ScrollMode.Enabled : ScrollMode.Disabled;
447
+ const isContained = this.scroller !== null;
448
+ if (isContained) {
449
+ return;
450
+ }
451
+
452
+ window.content = new Grid();
453
+ scroller.content = this.content;
454
+ window.content = scroller;
455
+ this.scroller = scroller;
456
+ }
457
+
458
+ private uncontain() {
459
+ const { scroller } = this;
460
+ const isContained = scroller !== null;
461
+ if (!isContained) {
462
+ return;
463
+ }
464
+
465
+ const { window } = this.handle;
466
+ window.content = new Grid();
467
+ scroller.content = null;
468
+ window.content = this.content;
469
+ this.scroller = null;
470
+ }
471
+
382
472
  private syncText() {
383
473
  const { window } = this.handle;
384
474
  window.title = this.props.title ?? "";
@@ -540,28 +630,28 @@ class WindowHost {
540
630
  }
541
631
 
542
632
  if (isGiven(minWidth)) {
543
- presenter.preferredMinimumWidth = round({
633
+ presenter.preferredMinimumWidth = toDevicePixels({
544
634
  value: minWidth,
545
635
  scale
546
636
  });
547
637
  }
548
638
 
549
639
  if (isGiven(minHeight)) {
550
- presenter.preferredMinimumHeight = round({
640
+ presenter.preferredMinimumHeight = toDevicePixels({
551
641
  value: minHeight,
552
642
  scale
553
643
  });
554
644
  }
555
645
 
556
646
  if (isGiven(maxWidth)) {
557
- presenter.preferredMaximumWidth = round({
647
+ presenter.preferredMaximumWidth = toDevicePixels({
558
648
  value: maxWidth,
559
649
  scale
560
650
  });
561
651
  }
562
652
 
563
653
  if (isGiven(maxHeight)) {
564
- presenter.preferredMaximumHeight = round({
654
+ presenter.preferredMaximumHeight = toDevicePixels({
565
655
  value: maxHeight,
566
656
  scale
567
657
  });
@@ -665,7 +755,7 @@ class WindowHost {
665
755
  hwnd,
666
756
  width,
667
757
  height,
668
- radius: round({
758
+ radius: toDevicePixels({
669
759
  value: cornerRadius,
670
760
  scale: this.handle.scale
671
761
  })
@@ -720,11 +810,11 @@ class WindowHost {
720
810
  const { scale } = this.handle;
721
811
  const current = this.openSize ?? appWindow.size;
722
812
  const size = {
723
- width: props.width === undefined ? current.width : round({
813
+ width: props.width === undefined ? current.width : toDevicePixels({
724
814
  value: props.width,
725
815
  scale
726
816
  }),
727
- height: props.height === undefined ? current.height : round({
817
+ height: props.height === undefined ? current.height : toDevicePixels({
728
818
  value: props.height,
729
819
  scale
730
820
  })
@@ -769,7 +859,8 @@ class WindowHost {
769
859
  }
770
860
 
771
861
  // A new width or height resizes the window; the user's own resizing is not
772
- // undone, since the props did not change.
862
+ // undone, since the props did not change. A side that follows the content
863
+ // (sizeToContent) is the content's to set.
773
864
  private syncSize() {
774
865
  const { width, height } = this.props;
775
866
  const isChanged = width !== this.appliedSize.width || height !== this.appliedSize.height;
@@ -783,18 +874,108 @@ class WindowHost {
783
874
  };
784
875
  const { appWindow } = this.handle.window;
785
876
  const { scale } = this.handle;
877
+ const { isWidthFitted, isHeightFitted } = this.fittedSides;
786
878
  appWindow.resize({
787
- width: width === undefined ? appWindow.size.width : round({
879
+ width: sideOf({
788
880
  value: width,
881
+ current: appWindow.size.width,
882
+ isFitted: isWidthFitted,
789
883
  scale
790
884
  }),
791
- height: height === undefined ? appWindow.size.height : round({
885
+ height: sideOf({
792
886
  value: height,
887
+ current: appWindow.size.height,
888
+ isFitted: isHeightFitted,
793
889
  scale
794
890
  })
795
891
  });
796
892
  }
797
893
 
894
+ private get fittedSides() {
895
+ const mode = this.props.sizeToContent ?? SizeToContentKind.none;
896
+ return {
897
+ isWidthFitted: mode === SizeToContentKind.width || mode === SizeToContentKind.both,
898
+ isHeightFitted: mode === SizeToContentKind.height || mode === SizeToContentKind.both
899
+ };
900
+ }
901
+
902
+ // Sizes the window to what its content asks for, whenever the prop, the
903
+ // limits, the scale or a layout pass changes it: one MoveAndResize, so the
904
+ // anchor corner never moves between the size and the position.
905
+ private fitToContent() {
906
+ const mode = this.props.sizeToContent ?? SizeToContentKind.none;
907
+ const anchor = this.props.anchor ?? WindowAnchorKind.topLeft;
908
+ const { minWidth, minHeight, maxWidth, maxHeight } = this.props;
909
+ const isIdle = mode === SizeToContentKind.none || !this.isWindowed || this.isFitting;
910
+ if (isIdle) {
911
+ return;
912
+ }
913
+
914
+ const { desiredSize } = this.content;
915
+ const isMeasured = desiredSize.width > 0 && desiredSize.height > 0;
916
+ if (!isMeasured) {
917
+ return;
918
+ }
919
+
920
+ const { appWindow, scale } = this.handle;
921
+ const { size, position, clientSize } = appWindow;
922
+ const frame = {
923
+ width: size.width - clientSize.width,
924
+ height: size.height - clientSize.height
925
+ };
926
+ const target = fittedBounds({
927
+ bounds: {
928
+ ...position,
929
+ ...size
930
+ },
931
+ desired: {
932
+ width: toDevicePixels({
933
+ value: desiredSize.width,
934
+ scale
935
+ }) + frame.width,
936
+ height: toDevicePixels({
937
+ value: desiredSize.height,
938
+ scale
939
+ }) + frame.height
940
+ },
941
+ mode,
942
+ anchor,
943
+ limits: {
944
+ minWidth: limitOf({
945
+ value: minWidth,
946
+ fallback: 0,
947
+ scale
948
+ }),
949
+ minHeight: limitOf({
950
+ value: minHeight,
951
+ fallback: 0,
952
+ scale
953
+ }),
954
+ maxWidth: limitOf({
955
+ value: maxWidth,
956
+ fallback: Infinity,
957
+ scale
958
+ }),
959
+ maxHeight: limitOf({
960
+ value: maxHeight,
961
+ fallback: Infinity,
962
+ scale
963
+ })
964
+ }
965
+ });
966
+ const hasTarget = target !== null;
967
+ if (!hasTarget) {
968
+ return;
969
+ }
970
+
971
+ this.isFitting = true;
972
+ try {
973
+ appWindow.moveAndResize(target);
974
+ } finally {
975
+ this.isFitting = false;
976
+ }
977
+ }
978
+
798
979
  private isCloseRefused() {
799
980
  return this.props.onclosing?.() === true;
800
981
  }
@@ -815,6 +996,7 @@ class WindowHost {
815
996
  this.releaseBackdrop = null;
816
997
  this.props.onclosed?.();
817
998
  }),
999
+ this.content.onLayoutUpdated(() => this.fitToContent()),
818
1000
  window.appWindow.onChanged((_sender, event) => {
819
1001
  const isMoved = event.didPositionChange || event.didSizeChange;
820
1002
  if (isMoved) {
@@ -836,6 +1018,7 @@ class WindowHost {
836
1018
  this.syncLimits();
837
1019
  this.syncRegions();
838
1020
  this.syncShape();
1021
+ this.fitToContent();
839
1022
  this.props.onscalechanged?.(scale);
840
1023
  }
841
1024
  });
package/src/winui.ts CHANGED
@@ -8,6 +8,9 @@ export { Application } from "#winui/microsoft/ui/xaml/Application";
8
8
  export { ColumnDefinition } from "#winui/microsoft/ui/xaml/controls/ColumnDefinition";
9
9
  export { Grid } from "#winui/microsoft/ui/xaml/controls/Grid";
10
10
  export { RowDefinition } from "#winui/microsoft/ui/xaml/controls/RowDefinition";
11
+ export { ScrollBarVisibility } from "#winui/microsoft/ui/xaml/controls/ScrollBarVisibility";
12
+ export { ScrollMode } from "#winui/microsoft/ui/xaml/controls/ScrollMode";
13
+ export { ScrollViewer } from "#winui/microsoft/ui/xaml/controls/ScrollViewer";
11
14
  export { Run } from "#winui/microsoft/ui/xaml/documents/Run";
12
15
  export { DispatcherQueue } from "#winui/microsoft/ui/dispatching/DispatcherQueue";
13
16
  export { DesktopAcrylicBackdrop } from "#winui/microsoft/ui/xaml/media/DesktopAcrylicBackdrop";
@@ -18,6 +21,8 @@ export { DesktopAcrylicKind } from "#winui/microsoft/ui/composition/system-backd
18
21
  export { SystemBackdropConfiguration } from "#winui/microsoft/ui/composition/system-backdrops/SystemBackdropConfiguration";
19
22
  export { SystemBackdropTheme } from "#winui/microsoft/ui/composition/system-backdrops/SystemBackdropTheme";
20
23
  export { ICompositionSupportsSystemBackdrop } from "#winui/microsoft/ui/composition/ICompositionSupportsSystemBackdrop";
24
+ export { Compositor as SystemCompositor } from "#winui/windows/ui/composition/Compositor";
25
+ export { CompositionColorBrush as SystemCompositionColorBrush } from "#winui/windows/ui/composition/CompositionColorBrush";
21
26
  export { MicaKind } from "#winui/microsoft/ui/composition/system-backdrops/MicaKind";
22
27
  export { AppWindow } from "#winui/microsoft/ui/windowing/AppWindow";
23
28
  export { AppWindowPresenterKind } from "#winui/microsoft/ui/windowing/AppWindowPresenterKind";