Skip to content
1 change: 1 addition & 0 deletions apps/toolbox/src/main-page.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
<Button text="webview" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
<Button text="winter-tc" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
<Button text="ete" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
<Button text="ete/hub" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
</StackLayout>
</ScrollView>
</StackLayout>
Expand Down
60 changes: 60 additions & 0 deletions apps/toolbox/src/pages/ete/bars.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Application, Color, EventData, Frame, Observable, Page, Utils } from '@nativescript/core';
import { deviceSummary } from './shared';

class Bars extends Observable {
private dark = true;
private lightContent = false;
private tinted = false;

constructor(private page: Page) {
super();
this.publish();

const onAppearance = () => this.publish();
Application.on(Application.systemAppearanceChangedEvent, onAppearance);
this.page.on(Page.navigatedFromEvent, () => Application.off(Application.systemAppearanceChangedEvent, onAppearance));
}

toggleStyle = () => {
this.dark = !this.dark;
this.publish();
};

toggleContent = () => {
this.lightContent = !this.lightContent;
this.publish();
};

/** The colours only show up behind translucent bars, which is the edge-to-edge case. */
toggleBarColors = () => {
this.tinted = !this.tinted;
const activity = Utils.android.getCurrentActivity() as androidx.appcompat.app.AppCompatActivity;
if (this.tinted) {
Utils.android.setStatusBarColor({ activity, lightColor: new Color('#3320c997'), darkColor: new Color('#33064e3b') });
Utils.android.setNavigationBarColor({ activity, lightColor: new Color('#33f59e0b'), darkColor: new Color('#33451a03') });
} else {
Utils.android.setStatusBarColor({ activity, lightColor: new Color('transparent'), darkColor: new Color('transparent') });
Utils.android.setNavigationBarColor({ activity, lightColor: new Color('transparent'), darkColor: new Color('transparent') });
}
this.publish();
};

back = () => {
Frame.topmost().goBack();
};

private publish() {
const style = this.dark ? 'dark' : 'light';
this.set('statusBarStyle', style);
this.set('statusBarLine', `statusBarStyle: ${style}`);
this.set('contentColor', this.lightContent ? '#f8fafc' : '#111827');
this.set('appearanceLine', `system appearance: ${Application.systemAppearance()}`);
this.set('ignoreOlderLine', `ignoreEdgeToEdgeOnOlderDevices: ${Utils.android.getIgnoreEdgeToEdgeOnOlderDevices()}`);
this.set('device', deviceSummary());
}
}

export function navigatingTo(args: EventData) {
const page = <Page>args.object;
page.bindingContext = new Bars(page);
}
32 changes: 32 additions & 0 deletions apps/toolbox/src/pages/ete/bars.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" actionBarHidden="true" androidOverflowEdge="ignore" backgroundColor="#3f0d12" statusBarStyle="{{ statusBarStyle }}">
<GridLayout id="probe" backgroundColor="#166534" androidOverflowEdge="top,bottom">
<GridLayout backgroundColor="{{ contentColor }}" rows="auto, auto, *">
<StackLayout row="0" padding="12" marginTop="48">
<Label text="System bars" color="#ffffff" fontWeight="bold" fontSize="17" />
<Label text="The probe overflows top and bottom, so the content colour runs under both bars - that is the only way to tell whether the bar icons are still readable." color="#e5e7eb" fontSize="13" textWrap="true" />
</StackLayout>

<StackLayout row="1" padding="8">
<GridLayout columns="*, *">
<Button col="0" text="statusBarStyle" tap="{{ toggleStyle }}" color="#ffffff" backgroundColor="#111827" />
<Button col="1" text="content colour" tap="{{ toggleContent }}" color="#ffffff" backgroundColor="#111827" />
</GridLayout>
<GridLayout columns="*, *" marginTop="4">
<Button col="0" text="bar colours" tap="{{ toggleBarColors }}" color="#ffffff" backgroundColor="#111827" />
<Button col="1" text="back" tap="{{ back }}" color="#ffffff" backgroundColor="#111827" />
</GridLayout>
</StackLayout>

<ScrollView row="2">
<StackLayout padding="12">
<Label text="{{ statusBarLine }}" color="#ffffff" fontFamily="monospace" fontSize="14" />
<Label text="{{ appearanceLine }}" color="#ffffff" fontFamily="monospace" fontSize="14" marginTop="4" />
<Label text="{{ ignoreOlderLine }}" color="#ffffff" fontFamily="monospace" fontSize="14" marginTop="4" />
<Label text="{{ device }}" color="#e5e7eb" fontSize="13" textWrap="true" marginTop="8" />
<Label text="Switch the system theme while this page is open: the bar icons have to flip with it, not stay on the old theme." color="#fde68a" fontSize="13" textWrap="true" marginTop="12" />
</StackLayout>
</ScrollView>

</GridLayout>
</GridLayout>
</Page>
81 changes: 81 additions & 0 deletions apps/toolbox/src/pages/ete/edges.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { CoreTypes, EventData, Frame, GridLayout, Observable, Page } from '@nativescript/core';
import { readEdges, readWindowInsets } from './shared';

interface Case {
edge: CoreTypes.AndroidOverflow;
description: string;
}

/**
* One entry per shape androidOverflowEdge can take. Walk them with prev/next and the
* green ring tells you which edges the probe padded.
*/
const CASES: Case[] = [
{ edge: 'none', description: 'pads every edge and consumes them - expect a green ring all the way round' },
{ edge: 'ignore', description: 'takes no part at all - expect no green ring, and no ring left over from the previous case' },
{ edge: 'top', description: 'overflows the top - blue should reach up under the status bar, green on the other three' },
{ edge: 'bottom', description: 'overflows the bottom - blue should reach down under the navigation bar' },
{ edge: 'left', description: 'overflows the left - only visible when there is a left inset (landscape / gesture nav)' },
{ edge: 'right', description: 'overflows the right - only visible when there is a right inset' },
{ edge: 'top,bottom', description: 'overflows both - green only on left/right, which are usually 0 in portrait' },
{ edge: 'left,right', description: 'overflows the sides - green top and bottom' },
{ edge: 'all-but-top', description: 'only the top is padded' },
{ edge: 'all-but-bottom', description: 'only the bottom is padded' },
{ edge: 'top-dont-consume', description: 'pads the top AND passes the top inset on to children' },
{ edge: 'bottom-dont-consume', description: 'pads the bottom AND passes the bottom inset on to children' },
{ edge: 'dont-apply', description: 'hands the insets to JS and pads nothing - expect no green ring' },
{ edge: 'none,none', description: 'a stacked value that resolves to none - must behave exactly like the first case' },
{ edge: 'ignore,bottom', description: 'ignore wins over anything after it - must behave exactly like ignore' },
{ edge: 'none,cutout', description: 'like none, but the display cutout counts too - rotate to landscape, where the camera sits on an edge with no system bar' },
{ edge: 'top,cutout', description: 'overflows the top while still keeping clear of the cutout on the other edges' },
];

class EdgeMatrix extends Observable {
private index = 0;

constructor(private page: Page) {
super();
this.apply();
}

get probe(): GridLayout {
return this.page.getViewById<GridLayout>('probe');
}

next = () => {
this.index = (this.index + 1) % CASES.length;
this.apply();
};

prev = () => {
this.index = (this.index - 1 + CASES.length) % CASES.length;
this.apply();
};

back = () => {
Frame.topmost().goBack();
};

private apply() {
const current = CASES[this.index];
this.set('edge', current.edge);
this.set('heading', `androidOverflowEdge = "${current.edge}"`);
this.set('description', current.description);
this.set('hint', `green ring = padded edge, blue to the screen edge = overflowing (${this.index + 1} of ${CASES.length})`);
// The insets land on the next layout pass, so read the probe after it.
setTimeout(() => this.refresh(), 80);
}

refresh() {
const readout = readEdges(this.probe);
this.set('padding', readout.padding);
this.set('edgeInsets', readout.edgeInsets);
this.set('imeInsets', readout.imeInsets);
this.set('windowInsets', readWindowInsets(this.page));
}
}

export function navigatingTo(args: EventData) {
const page = <Page>args.object;
page.bindingContext = new EdgeMatrix(page);
}
32 changes: 32 additions & 0 deletions apps/toolbox/src/pages/ete/edges.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" actionBarHidden="true" androidOverflowEdge="ignore" backgroundColor="#3f0d12">
<!-- The page ignores insets so the probe below is the only view distributing them. -->
<GridLayout id="probe" backgroundColor="#166534" androidOverflowEdge="{{ edge }}">
<GridLayout backgroundColor="#1d4ed8" rows="auto, auto, *">
<StackLayout row="0" padding="12">
<Label text="{{ heading }}" color="#ffffff" fontWeight="bold" fontSize="17" textWrap="true" />
<Label text="{{ description }}" color="#bfdbfe" fontSize="13" textWrap="true" />
</StackLayout>

<GridLayout row="1" columns="*, *, *" padding="8">
<Button col="0" text="&lt; prev" tap="{{ prev }}" color="#ffffff" backgroundColor="#1e40af" />
<Button col="1" text="next &gt;" tap="{{ next }}" color="#ffffff" backgroundColor="#1e40af" />
<Button col="2" text="back" tap="{{ back }}" color="#ffffff" backgroundColor="#1e40af" />
</GridLayout>

<ScrollView row="2">
<StackLayout padding="12">
<Label text="probe padding" color="#ffffff" fontWeight="bold" />
<Label text="{{ padding }}" color="#bfdbfe" fontFamily="monospace" fontSize="13" textWrap="true" />
<Label text="getEdgeInsets (l,t,r,b)" color="#ffffff" fontWeight="bold" marginTop="8" />
<Label text="{{ edgeInsets }}" color="#bfdbfe" fontFamily="monospace" fontSize="13" textWrap="true" />
<Label text="getImeInsets (l,t,r,b)" color="#ffffff" fontWeight="bold" marginTop="8" />
<Label text="{{ imeInsets }}" color="#bfdbfe" fontFamily="monospace" fontSize="13" textWrap="true" />
<Label text="window insets" color="#ffffff" fontWeight="bold" marginTop="8" />
<Label text="{{ windowInsets }}" color="#bfdbfe" fontFamily="monospace" fontSize="13" textWrap="true" />
<Label text="{{ hint }}" color="#fde68a" fontSize="13" textWrap="true" marginTop="12" />
</StackLayout>
</ScrollView>

</GridLayout>
</GridLayout>
</Page>
15 changes: 15 additions & 0 deletions apps/toolbox/src/pages/ete/hub.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { EventData, Frame, Observable, Page, View } from '@nativescript/core';
import { deviceSummary, readWindowInsets } from './shared';

export function navigatingTo(args: EventData) {
const page = <Page>args.object;
const model = new Observable();
model.set('device', deviceSummary());
model.set('windowInsets', 'reading...');
model.set('open', (tap: EventData) => {
Frame.topmost().navigate({ moduleName: `pages/${(tap.object as View).id}` });
});
page.bindingContext = model;

setTimeout(() => model.set('windowInsets', readWindowInsets(page)), 120);
}
31 changes: 31 additions & 0 deletions apps/toolbox/src/pages/ete/hub.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" androidOverflowEdge="none" backgroundColor="#3f0d12">
<Page.actionBar>
<ActionBar title="Edge to Edge tests" class="action-bar" />
</Page.actionBar>
<ScrollView>
<StackLayout class="p-20">
<Label text="{{ device }}" textWrap="true" color="#fca5a5" fontSize="13" marginBottom="4" />
<Label text="{{ windowInsets }}" textWrap="true" color="#fca5a5" fontSize="13" fontFamily="monospace" marginBottom="16" />

<Label text="Colour legend" color="#ffffff" fontWeight="bold" marginBottom="6" />
<GridLayout columns="auto, *" rows="auto, auto, auto, auto" marginBottom="20">
<StackLayout row="0" col="0" width="24" height="24" backgroundColor="#3f0d12" borderWidth="1" borderColor="#ffffff" margin="2" />
<Label row="0" col="1" text="backdrop - nothing reached here" color="#ffffff" marginLeft="8" verticalAlignment="center" />
<StackLayout row="1" col="0" width="24" height="24" backgroundColor="#166534" margin="2" />
<Label row="1" col="1" text="probe - the ring you see is its padding" color="#ffffff" marginLeft="8" verticalAlignment="center" />
<StackLayout row="2" col="0" width="24" height="24" backgroundColor="#1d4ed8" margin="2" />
<Label row="2" col="1" text="inner - what is left after the probe padded" color="#ffffff" marginLeft="8" verticalAlignment="center" />
<StackLayout row="3" col="0" width="24" height="24" backgroundColor="#b45309" margin="2" />
<Label row="3" col="1" text="core - third level, for the nested tests" color="#ffffff" marginLeft="8" verticalAlignment="center" />
</GridLayout>

<Button text="1. Edge matrix" tap="{{ open }}" id="ete/edges" class="btn btn-primary btn-view-demo" />
<Button text="2. Nested consume (dont-apply)" tap="{{ open }}" id="ete/nested" class="btn btn-primary btn-view-demo" />
<Button text="3. Keyboard insets" tap="{{ open }}" id="ete/ime" class="btn btn-primary btn-view-demo" />
<Button text="4. Lifecycle and reset" tap="{{ open }}" id="ete/lifecycle" class="btn btn-primary btn-view-demo" />
<Button text="5. Modals" tap="{{ open }}" id="ete/modals" class="btn btn-primary btn-view-demo" />
<Button text="6. System bars" tap="{{ open }}" id="ete/bars" class="btn btn-primary btn-view-demo" />
<Button text="7. Scrolling content" tap="{{ open }}" id="ete/scroll" class="btn btn-primary btn-view-demo" />
</StackLayout>
</ScrollView>
</Page>
56 changes: 56 additions & 0 deletions apps/toolbox/src/pages/ete/ime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { EventData, Frame, GridLayout, Observable, Page, TextField, Utils } from '@nativescript/core';
import { readEdges } from './shared';

class Ime extends Observable {
private ownPadding = 0;

constructor(private page: Page) {
super();
// Poll instead of waiting on a layout pass: the keyboard animates in and the
// interesting numbers are the ones after it settles.
const timer = setInterval(() => this.refresh(), 250);
this.page.on(Page.navigatedFromEvent, () => clearInterval(timer));
}

get probe(): GridLayout {
return this.page.getViewById<GridLayout>('probe');
}

/** Raising the app's own padding while the keyboard is up must not eat the keyboard gap. */
bumpPadding = () => {
this.ownPadding = this.ownPadding ? 0 : 24;
this.probe.padding = this.ownPadding;
setTimeout(() => this.refresh(), 60);
};

dismiss = () => {
this.page.getViewById<TextField>('field').dismissSoftInput();
};

back = () => {
Frame.topmost().goBack();
};

refresh() {
const readout = readEdges(this.probe);
this.set('padding', readout.padding);
this.set('edgeInsets', `edge ${readout.edgeInsets}`);
this.set('imeInsets', `ime ${readout.imeInsets}`);

const native = this.probe?.android as org.nativescript.widgets.LayoutBase;
if (!native?.getImeInsets) {
return;
}

// toDevicePixels can land on a half pixel; the view rounds it, so compare rounded.
const own = Math.round(Utils.layout.toDevicePixels(this.ownPadding));
const expected = own + Math.max(native.getEdgeInsets().bottom, native.getImeInsets().bottom);
const actual = native.getPaddingBottom();
this.set('verdict', actual === expected ? `PASS - bottom padding ${actual} = own ${own} + max(nav, ime)` : `FAIL - bottom padding ${actual}, expected ${expected}`);
}
}

export function navigatingTo(args: EventData) {
const page = <Page>args.object;
page.bindingContext = new Ime(page);
}
25 changes: 25 additions & 0 deletions apps/toolbox/src/pages/ete/ime.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" actionBarHidden="true" androidOverflowEdge="ignore" backgroundColor="#3f0d12">
<GridLayout id="probe" backgroundColor="#166534" androidOverflowEdge="none" rows="*, auto">
<ScrollView row="0" backgroundColor="#1d4ed8">
<StackLayout padding="12">
<Label text="Keyboard insets" color="#ffffff" fontWeight="bold" fontSize="17" />
<Label text="Focus the field. The probe pads by max(navigation bar, keyboard), so the blue panel should sit directly on top of the keyboard with no green showing through." color="#bfdbfe" fontSize="13" textWrap="true" />
<Label text="probe padding" color="#ffffff" fontWeight="bold" marginTop="10" />
<Label text="{{ padding }}" color="#bfdbfe" fontFamily="monospace" fontSize="13" textWrap="true" />
<Label text="getEdgeInsets / getImeInsets" color="#ffffff" fontWeight="bold" marginTop="10" />
<Label text="{{ edgeInsets }}" color="#bfdbfe" fontFamily="monospace" fontSize="13" textWrap="true" />
<Label text="{{ imeInsets }}" color="#bfdbfe" fontFamily="monospace" fontSize="13" textWrap="true" />
<Label text="{{ verdict }}" color="#fde68a" fontSize="14" fontWeight="bold" textWrap="true" marginTop="12" />
</StackLayout>
</ScrollView>

<StackLayout row="1" backgroundColor="#b45309" padding="10">
<TextField id="field" hint="tap to raise the keyboard" color="#ffffff" backgroundColor="#7c2d12" padding="10" />
<GridLayout columns="*, *, *" marginTop="6">
<Button col="0" text="setPadding 24" tap="{{ bumpPadding }}" color="#ffffff" backgroundColor="#7c2d12" />
<Button col="1" text="dismiss" tap="{{ dismiss }}" color="#ffffff" backgroundColor="#7c2d12" />
<Button col="2" text="back" tap="{{ back }}" color="#ffffff" backgroundColor="#7c2d12" />
</GridLayout>
</StackLayout>
</GridLayout>
</Page>
Loading
Loading