Skip to content

Commit 9554dfc

Browse files
committed
chore(windows): polish
1 parent 3342802 commit 9554dfc

57 files changed

Lines changed: 2151 additions & 270 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/toolbox/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"@nativescript/visionos": "~9.0.0",
1818
"@nativescript/vite": "file:../../dist/packages/vite",
1919
"@nativescript/webpack": "file:../../dist/packages/webpack5",
20-
"@nativescript/windows": "0.1.0-alpha.133",
20+
"@nativescript/windows": "0.1.0-alpha.134",
2121
"typescript": "~5.8.0"
2222
}
2323
}

‎apps/ui/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"@nativescript/ios": "~9.0.0",
1616
"@nativescript/visionos": "~9.0.0",
1717
"@nativescript/webpack": "file:../../dist/packages/webpack5",
18-
"@nativescript/windows": "0.1.0-alpha.133",
18+
"@nativescript/windows": "0.1.0-alpha.134",
1919
"typescript": "~5.8.0"
2020
},
2121
"gitHead": "8ab7726d1ee9991706069c1359c552e67ee0d1a4",

‎packages/core/application/application-common.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type { View } from '../ui/core/view';
1010
import type { Frame } from '../ui/frame';
1111
import type { NavigationEntry } from '../ui/frame/frame-interfaces';
1212
import type { StyleScope } from '../ui/styling/style-scope';
13-
import type { AndroidApplication as AndroidApplicationType, iOSApplication as iOSApplicationType } from '.';
13+
import type { AndroidApplication as AndroidApplicationType, iOSApplication as iOSApplicationType, WindowsApplication as WindowsApplicationType } from '.';
1414
import type { ApplicationEventData, CssChangedEventData, DiscardedErrorEventData, FontScaleChangedEventData, InitRootViewEventData, LaunchEventData, LoadAppCSSEventData, NativeScriptError, OrientationChangedEventData, SystemAppearanceChangedEventData, LayoutDirectionChangedEventData, UnhandledErrorEventData } from './application-interfaces';
1515
import { readyInitAccessibilityCssHelper, readyInitFontScale } from '../accessibility/accessibility-common';
1616
import { getAppMainEntry, isAppInBackground, setAppInBackground, setAppMainEntry } from './helpers-common';
@@ -747,7 +747,7 @@ export class ApplicationCommon {
747747
return undefined;
748748
}
749749

750-
get windows() {
750+
get windows(): WindowsApplicationType {
751751
return undefined;
752752
}
753753

‎packages/core/application/application.d.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,15 @@ export class iOSApplication extends ApplicationCommon {
272272
*/
273273
shouldDelayLaunchEvent: boolean;
274274
}
275+
export class WindowsApplication extends ApplicationCommon {
276+
277+
getRootView(): View;
278+
279+
run(entry?: string | NavigationEntry): void;
280+
281+
getNativeApplication(): Microsoft.UI.Xaml.Application;
282+
283+
}
275284

276285
export const VALID_FONT_SCALES: number[];
277286
export function getCurrentFontScale(): number;

‎packages/core/application/application.windows.ts‎

Lines changed: 155 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import type { View } from '../ui/core/view';
55
import { setRootView } from './helpers-common';
66
import { CoreTypes } from 'index';
77
import { activateCurrentWindow, getCurrentWindowContent, setCurrentWindowContent, getCurrentWindowBounds, setCurrentWindowTitle } from './window-helper.windows';
8+
import { windows } from '../ui/styling/font';
9+
import { resolveModuleName, ModuleNameResolver } from '../module-name-resolver';
10+
import { _setResolver } from '../module-name-resolver/helpers';
11+
import { Screen, Device } from '../platform';
812

913

1014
enum AppEventKind {
@@ -24,6 +28,10 @@ export class WindowsApplication extends ApplicationCommon {
2428
return this._rootView;
2529
}
2630

31+
get windows() {
32+
return this;
33+
}
34+
2735
run(entry?: string | NavigationEntry): void {
2836
if (this.started) {
2937
throw new Error('Application is already started.');
@@ -33,6 +41,7 @@ export class WindowsApplication extends ApplicationCommon {
3341

3442
this.started = true;
3543
setAppMainEntry(typeof entry === 'string' ? { moduleName: entry } : entry);
44+
windows.triggerFontScan();
3645
this.setWindowContent();
3746
}
3847

@@ -52,26 +61,26 @@ export class WindowsApplication extends ApplicationCommon {
5261
if (!setCurrentWindowContent(rootView.nativeViewProtected)) {
5362
throw new Error('Unable to resolve the current Windows window.');
5463
}
55-
this.setupOrientationTracking(rootView.nativeViewProtected);
5664
activateCurrentWindow();
65+
this.setupOrientationTracking(rootView.nativeViewProtected);
5766
this._applyWindowTitle();
5867

5968
this.initRootView(rootView);
6069
rootView.callLoaded();
6170
}
6271

63-
// Give the window a sensible title. A code-created WinUI3 Window starts with an empty Title and
72+
// A code-created WinUI3 Window starts with an empty Title and
6473
// does NOT inherit the package DisplayName, so the caption otherwise shows the host exe name.
6574
private _applyWindowTitle(): void {
6675
try {
6776
let title = '';
6877
try {
6978
title = Windows.ApplicationModel.Package.Current.DisplayName || '';
70-
} catch (_e) { /* Package projection unavailable (e.g. unpackaged) — fall through */ }
79+
} catch (_e) { }
7180
if (title) {
7281
setCurrentWindowTitle(title);
7382
}
74-
} catch (_e) { /* never let window titling break app startup */ }
83+
} catch (_e) { }
7584
}
7685

7786
getNativeApplication() {
@@ -176,15 +185,150 @@ export class WindowsApplication extends ApplicationCommon {
176185
} catch (_e) { }
177186
}
178187

179-
// Orientation derives from window size, so re-evaluate when the root content resizes.
180-
private _orientationDelegate: any = null; // held so the handler isn't GC'd
188+
private _orientationDelegate: Microsoft.UI.Xaml.SizeChangedEventHandler | null = null;
189+
private _resizeDebounceId: any = null;
190+
private _lastResizeW = NaN;
191+
private _lastResizeH = NaN;
181192
private setupOrientationTracking(content: Microsoft.UI.Xaml.UIElement): void {
182-
const element = content as Microsoft.UI.Xaml.FrameworkElement;
183-
if (element && element.SizeChanged !== undefined) {
184-
// Wire via asDelegate (a raw-fn assignment does not reliably subscribe in this host) + hold it.
185-
this._orientationDelegate = NSWinRT.asDelegate('Microsoft.UI.Xaml.SizeChangedEventHandler', () => this.setOrientation(this.getOrientation()));
186-
(element as any).SizeChanged = this._orientationDelegate;
193+
const element = content as Microsoft.UI.Xaml.FrameworkElement;
194+
if (!element) return;
195+
const bounds = getCurrentWindowBounds();
196+
this._lastResizeW = bounds?.Width ?? NaN;
197+
this._lastResizeH = bounds?.Height ?? NaN;
198+
try {
199+
this._orientationDelegate = NSWinRT.asDelegate('Microsoft.UI.Xaml.SizeChangedEventHandler', () => this._onWindowResized());
200+
element.SizeChanged = this._orientationDelegate;
201+
} catch (_e) { }
202+
}
203+
204+
205+
private _onWindowResized(): void {
206+
if (this._resizeDebounceId) {
207+
clearTimeout(this._resizeDebounceId);
208+
}
209+
this._resizeDebounceId = setTimeout(() => {
210+
this._resizeDebounceId = null;
211+
this._applyResize();
212+
}, 80);
213+
}
214+
215+
// Desktop windows resize continuously, so width-based @media rules and matchMedia listeners must
216+
// re-evaluate on resize — not just on portrait/landscape flips. setOrientation() short-circuits
217+
// when the orientation value is unchanged, so we additionally force a CSS/media re-evaluation
218+
// when only the dimensions changed.
219+
private _applyResize(): void {
220+
const bounds = getCurrentWindowBounds();
221+
const w = bounds?.Width ?? NaN;
222+
const h = bounds?.Height ?? NaN;
223+
if (!(w > 0) || !(h > 0)) {
224+
return;
225+
}
226+
if (Math.abs(w - this._lastResizeW) < 1 && Math.abs(h - this._lastResizeH) < 1) {
227+
return; // size unchanged
228+
}
229+
this._lastResizeW = w;
230+
this._lastResizeH = h;
231+
232+
// Capture the page variant currently on screen BEFORE the orientation handling below — the
233+
// framework resets the module resolver on orientationChanged (application-common), so reading
234+
// it afterwards would already reflect the NEW metrics, hiding the change.
235+
const displayed = this._currentPageVariant();
236+
237+
const newOrientation = this.getOrientation();
238+
const orientationChanged = newOrientation !== this.orientation();
239+
// Real portrait/landscape flip: updates the cached value, re-applies the ns-portrait/
240+
// ns-landscape class, re-styles, and fires orientationChangedEvent.
241+
this.setOrientation(newOrientation);
242+
243+
if (!orientationChanged) {
244+
// Same orientation but dimensions changed — re-match @media against the (live) Screen
245+
// metrics and notify matchMedia listeners (which key off orientationChangedEvent).
246+
const rootView = this.getRootView();
247+
if (rootView) {
248+
this.orientationChanged(rootView, newOrientation);
249+
}
250+
this.notify({
251+
eventName: this.orientationChangedEvent,
252+
object: this,
253+
windows: this.windows,
254+
newValue: newOrientation,
255+
} as never);
256+
}
257+
258+
// File-name screen qualifiers (main-page.minWH360.xml, .land/.port, etc.) are resolved at
259+
// navigation time. On desktop the window resizes continuously, so re-resolve the current
260+
// page's variant against the new metrics and hot-swap it when it crosses a qualifier
261+
// threshold. (Mobile never resizes continuously, so this is desktop specific.)
262+
this._swapQualifiedPageIfChanged(displayed);
263+
}
264+
265+
// Resolves the current page's on-screen variant using the (pre-orientation-reset) global resolver.
266+
private _currentPageVariant(): { frame: any; moduleName: string; xml: string; js: string } | null {
267+
// Find the current frame from the root view.
268+
// Descends through nested frames to the deepest active one.
269+
let frame: any = this.getRootView();
270+
let guard = 0;
271+
while (frame && guard++ < 10) {
272+
const child = frame.currentPage?.content;
273+
if (child && typeof child.replacePage === 'function') {
274+
frame = child;
275+
} else {
276+
break;
187277
}
278+
}
279+
if (!frame || typeof frame.replacePage !== 'function') {
280+
return null;
281+
}
282+
const moduleName: string = frame?._currentEntry?.entry?.moduleName;
283+
if (!moduleName) {
284+
return null;
285+
}
286+
try {
287+
return { frame, moduleName, xml: resolveModuleName(moduleName, 'xml'), js: resolveModuleName(moduleName, '') };
288+
} catch (_e) {
289+
return null;
290+
}
291+
}
292+
293+
private _swapQualifiedPageIfChanged(displayed: { frame: any; moduleName: string; xml: string; js: string } | null): void {
294+
if (!displayed) {
295+
return;
296+
}
297+
const { frame, moduleName } = displayed;
298+
// Resolve against the CURRENT (resized) metrics via a throwaway resolver — independent of the
299+
// global resolver's state (which the orientation handler may or may not have reset).
300+
const ctx = {
301+
width: Screen.mainScreen.widthDIPs,
302+
height: Screen.mainScreen.heightDIPs,
303+
os: Device.os,
304+
deviceType: Device.deviceType,
305+
};
306+
let candidateXml: string, candidateJs: string;
307+
try {
308+
const probe = new ModuleNameResolver(ctx);
309+
candidateXml = probe.resolveModuleName(moduleName, 'xml');
310+
candidateJs = probe.resolveModuleName(moduleName, '');
311+
} catch (_e) {
312+
return;
313+
}
314+
if (candidateXml === displayed.xml && candidateJs === displayed.js) {
315+
return; // no qualifier boundary crossed — keep the current page
316+
}
317+
318+
// Variant changed: point the global resolver at the new metrics so Builder resolves the new
319+
// variant, flush the Windows page cache (keyed by base module name) so the freshly-built
320+
// variant isn't overridden by a stale cached page, then swap.
321+
try {
322+
_setResolver(new ModuleNameResolver(ctx));
323+
} catch (_e) {
324+
return;
325+
}
326+
try {
327+
frame._flushPageCache?.();
328+
} catch (_e) { }
329+
try {
330+
frame.replacePage({ moduleName });
331+
} catch (_e) { }
188332
}
189333
}
190334

‎packages/core/application/window-helper.windows.ts‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,14 +154,23 @@ export function getCurrentWindowBounds(preferredElement?: FrameworkElementLike |
154154
return windowBounds;
155155
}
156156

157+
// XamlRoot.Size is the authoritative WinUI3 window client size (DIPs) and is valid as soon as
158+
// the window is shown — before content layout completes. Read it from the window's own content
159+
// first (Screen.mainScreen calls this with no preferredElement), then any preferredElement.
160+
const content = (getCurrentWindowContent<FrameworkElementLike>() ?? preferredElement) as FrameworkElementLike | null;
161+
const contentXamlRootBounds = getBoundsFromSize(content?.XamlRoot?.Size);
162+
if (contentXamlRootBounds) {
163+
return contentXamlRootBounds;
164+
}
165+
157166
const xamlRootBounds = getBoundsFromSize(preferredElement?.XamlRoot?.Size);
158167
if (xamlRootBounds) {
159168
return xamlRootBounds;
160169
}
161170

162-
const content = (window?.Content ?? preferredElement) as any;
163-
const width = getPositiveNumber(content?.ActualWidth ?? content?.Width);
164-
const height = getPositiveNumber(content?.ActualHeight ?? content?.Height);
171+
const measured = (content ?? window?.Content) as any;
172+
const width = getPositiveNumber(measured?.ActualWidth ?? measured?.Width);
173+
const height = getPositiveNumber(measured?.ActualHeight ?? measured?.Height);
165174
if (width == null || height == null) {
166175
return null;
167176
}

‎packages/core/file-system/file-system-access.windows.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,12 @@ export class FileSystemAccess {
375375
return `${l}\\${r}`;
376376
}
377377

378-
public joinPaths(...paths: string[]): string {
378+
// Single string[] arg (matches the interface and how path.join calls it). A rest param made the
379+
// array arrive as [[a,b]], so reduce returned it unchanged and path.join produced an array.
380+
public joinPaths(paths: string[]): string {
381+
if (!paths || paths.length === 0) {
382+
return '';
383+
}
379384
return paths.reduce((acc, p) => this.joinPath(acc, p));
380385
}
381386

‎packages/core/image-asset/index.windows.ts‎

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { ImageAssetBase } from './image-asset-common';
2-
import { path as fsPath, knownFolders } from '../file-system';
32

43
export * from './image-asset-common';
54

@@ -8,14 +7,10 @@ export class ImageAsset extends ImageAssetBase {
87

98
constructor(asset: string | any) {
109
super();
11-
if (typeof asset === 'string') {
12-
if (asset.indexOf('~/') === 0) {
13-
asset = fsPath.join(knownFolders.currentApp().path, asset.replace('~/', ''));
14-
}
15-
this._windows = asset;
16-
} else {
17-
this._windows = asset;
18-
}
10+
// Keep the path/object as-is. Converting ~/ to an InstalledLocation filesystem path pointed at a
11+
// missing file (no app/ subfolder, not file:// loadable) and stalled fromFile; ImageSource
12+
// resolves ~/ via ms-appx:///app/.
13+
this._windows = asset;
1914
}
2015

2116
get windows(): any {

0 commit comments

Comments
 (0)