@@ -5,6 +5,10 @@ import type { View } from '../ui/core/view';
55import { setRootView } from './helpers-common' ;
66import { CoreTypes } from 'index' ;
77import { 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
1014enum 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
0 commit comments