Skip to content

Commit f301728

Browse files
authored
feat(windows): let a parent resolve its children's percentage sizes (#11468)
* feat(windows): let a parent resolve its children's percentage sizes View gains _setChildPercentSize(child, horizontal, fraction), which returns false. A percentage width or height is handed to the parent's; when it returns true, core leaves the XAML Width/Height unset and skips its own percent sizing. Setting a length clears it (fraction null). * feat(windows): view location in window, on screen and relative to a view (#11515) getLocationInWindow and getLocationRelativeTo transform the element's origin through XAML; getLocationOnScreen adds the window's client origin, read off its AppWindow (cached per XamlRoot) by a window-helper util.
1 parent 86b4407 commit f301728

3 files changed

Lines changed: 100 additions & 1 deletion

File tree

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,46 @@ export function getCurrentWindowScale(preferredElement?: FrameworkElementLike |
283283
return 1;
284284
}
285285

286+
const _appWindows = new WeakMap<object, any>();
287+
288+
function getAppWindow(xamlRoot: any): any {
289+
const cached = _appWindows.get(xamlRoot);
290+
if (cached) {
291+
return cached;
292+
}
293+
let appWindow = null;
294+
try {
295+
const id = xamlRoot.ContentIslandEnvironment?.AppWindowId;
296+
appWindow = id ? Microsoft.UI.Windowing.AppWindow.GetFromWindowId(id) : null;
297+
} catch (_e) { }
298+
appWindow ??= (getCurrentWindow() as any)?.AppWindow ?? null;
299+
if (appWindow) {
300+
_appWindows.set(xamlRoot, appWindow);
301+
}
302+
return appWindow;
303+
}
304+
305+
export function getClientOriginOnScreen(xamlRoot: any): { x: number; y: number } | null {
306+
if (!xamlRoot) {
307+
return null;
308+
}
309+
const appWindow = getAppWindow(xamlRoot);
310+
if (!appWindow) {
311+
return null;
312+
}
313+
try {
314+
const position = appWindow.Position;
315+
const size = appWindow.Size;
316+
const client = appWindow.ClientSize;
317+
const border = Math.max(0, (size.Width - client.Width) / 2);
318+
const top = Math.max(0, size.Height - client.Height - border);
319+
const scale = getCurrentWindowScale();
320+
return { x: (position.X + border) / scale, y: (position.Y + top) / scale };
321+
} catch (_e) {
322+
return null;
323+
}
324+
}
325+
286326
export function getCurrentWindowBounds(preferredElement?: FrameworkElementLike | null): { Width: number; Height: number } | null {
287327
return getWindowBounds(resolveCurrentWindow() as unknown as Microsoft.UI.Xaml.Window, preferredElement);
288328
}

‎packages/core/ui/core/view/index.d.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -991,6 +991,12 @@ export abstract class View extends ViewCommon {
991991
*/
992992
hasGestureObservers?(): boolean;
993993

994+
/**
995+
* Windows only. A parent that lays out its children itself can take over a child's percentage
996+
* width or height (`fraction` 0-1, `null` once it is no longer a percentage) by returning `true`.
997+
*/
998+
_setChildPercentSize?(child: View, horizontal: boolean, fraction: number | null): boolean;
999+
9941000
/**
9951001
* Android only to set the touch listener
9961002
*/

‎packages/core/ui/core/view/index.windows.ts‎

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export * from '../properties';
44

55
import { ViewCommon, originXProperty, originYProperty } from './view-common';
66
import type { CoreTypes } from '../../../core-types';
7+
import type { Point } from './view-interfaces';
78
import { visibilityProperty, opacityProperty, backgroundInternalProperty, translateXProperty, translateYProperty, scaleXProperty, scaleYProperty, rotateProperty, rotateXProperty, rotateYProperty, perspectiveProperty, horizontalAlignmentProperty, verticalAlignmentProperty, paddingTopProperty, paddingRightProperty, paddingBottomProperty, paddingLeftProperty } from '../../styling/style-properties';
89
import { LinearGradient } from '../../styling/linear-gradient';
910
import { widthProperty, heightProperty, minWidthProperty, minHeightProperty, marginLeftProperty, marginTopProperty, marginRightProperty, marginBottomProperty } from '../../styling/style-properties';
@@ -13,7 +14,7 @@ import { Background } from '../../styling/background';
1314
import { BoxShadow } from '../../styling/box-shadow';
1415
import { Color } from '../../../color';
1516
import { hiddenProperty } from '../view-base';
16-
import { getCurrentWindowBounds, getCurrentWindowContent, getWindowBounds } from '../../../application/window-helper.windows';
17+
import { getClientOriginOnScreen, getCurrentWindowBounds, getCurrentWindowContent, getWindowBounds } from '../../../application/window-helper.windows';
1718
import { ImageSource } from '../../../image-source';
1819
import { ClipPathFunction } from '../../styling/clip-path-function';
1920
import { nativeChildIndex, removeNativeChild } from './native-children.windows';
@@ -630,6 +631,11 @@ class CompositionBorderHandler {
630631
}
631632
}
632633

634+
let _zeroPoint: Windows.Foundation.Point | undefined;
635+
function zeroPoint(): Windows.Foundation.Point {
636+
return (_zeroPoint ??= Microsoft.UI.Xaml.PointHelper.FromCoordinates(0, 0));
637+
}
638+
633639
export class View extends ViewCommon {
634640
nativeViewProtected: Microsoft.UI.Xaml.FrameworkElement;
635641

@@ -1178,6 +1184,32 @@ export class View extends ViewCommon {
11781184
}
11791185
}
11801186

1187+
private _originIn(target: Microsoft.UI.Xaml.UIElement | null): Point {
1188+
try {
1189+
const p = (this.nativeViewProtected as any).TransformToVisual(target).TransformPoint(zeroPoint());
1190+
return { x: p.X, y: p.Y };
1191+
} catch (_e) {
1192+
return undefined;
1193+
}
1194+
}
1195+
1196+
public getLocationInWindow(): Point {
1197+
return (this.nativeViewProtected as any)?.XamlRoot ? this._originIn(null) : undefined;
1198+
}
1199+
1200+
public getLocationOnScreen(): Point {
1201+
const root = (this.nativeViewProtected as any)?.XamlRoot;
1202+
const inWindow = root ? this._originIn(null) : undefined;
1203+
const client = inWindow ? getClientOriginOnScreen(root) : null;
1204+
return client ? { x: client.x + inWindow.x, y: client.y + inWindow.y } : undefined;
1205+
}
1206+
1207+
public getLocationRelativeTo(otherView: ViewCommon): Point {
1208+
const other = otherView?.nativeViewProtected as any;
1209+
const root = (this.nativeViewProtected as any)?.XamlRoot;
1210+
return root && other?.XamlRoot === root ? this._originIn(other) : undefined;
1211+
}
1212+
11811213
// Button-family controls override their template's brush resources. Probed on first need: most
11821214
// views never set a colour or background, and the probe is a WinRT call.
11831215
protected _nativeIsButton(): boolean {
@@ -1476,6 +1508,15 @@ export class View extends ViewCommon {
14761508
} catch (_e) { }
14771509
}
14781510

1511+
/**
1512+
* A parent that lays out its children itself can take over a child's percentage width or height
1513+
* (`fraction` 0-1, `null` once it is no longer a percentage) by returning `true`: the child then
1514+
* leaves its XAML size unset and skips its own percent sizing.
1515+
*/
1516+
_setChildPercentSize(child: View, horizontal: boolean, fraction: number | null): boolean {
1517+
return false;
1518+
}
1519+
14791520
private _applyPercentSizing(): void {
14801521
const nativeView = this.nativeViewProtected as any;
14811522
if (!nativeView) {
@@ -1521,6 +1562,11 @@ export class View extends ViewCommon {
15211562

15221563
if (value && typeof value === 'object' && (value as any).unit === '%') {
15231564
const pct = (value as any).value;
1565+
if (this.parent instanceof View && this.parent._setChildPercentSize(this, true, pct)) {
1566+
this._percentWidth = null;
1567+
this.nativeViewProtected.Width = NaN;
1568+
return;
1569+
}
15241570
this._percentWidth = pct;
15251571
this._ensureSizeWatch(); // re-apply percent when the parent resizes
15261572
this.nativeViewProtected.Width = NaN;
@@ -1543,6 +1589,7 @@ export class View extends ViewCommon {
15431589
}
15441590

15451591
this._percentWidth = null;
1592+
if (this.parent instanceof View) this.parent._setChildPercentSize(this, true, null);
15461593
// Clear any previously set flex-basis so the C++ widget falls back to DesiredSize.
15471594
try {
15481595
(NativeScript as any).Widgets.FlexboxLayout.SetFlexBasisPercent(
@@ -1560,6 +1607,11 @@ export class View extends ViewCommon {
15601607

15611608
if (value && typeof value === 'object' && (value as any).unit === '%') {
15621609
const pct = (value as any).value;
1610+
if (this.parent instanceof View && this.parent._setChildPercentSize(this, false, pct)) {
1611+
this._percentHeight = null;
1612+
this.nativeViewProtected.Height = NaN;
1613+
return;
1614+
}
15631615
this._percentHeight = pct;
15641616
this._ensureSizeWatch(); // re-apply percent when the parent resizes
15651617
this.nativeViewProtected.Height = NaN;
@@ -1577,6 +1629,7 @@ export class View extends ViewCommon {
15771629
}
15781630

15791631
this._percentHeight = null;
1632+
if (this.parent instanceof View) this.parent._setChildPercentSize(this, false, null);
15801633
this.nativeViewProtected.Height = toXamlLength(value);
15811634
}
15821635

0 commit comments

Comments
 (0)