Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default class EntryAbility extends UIAbility {

onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
const scenarioId = want.parameters?.['openbitfunDesignPreview'];
if (scenarioId === 'history-scroll' || scenarioId === 'durable-timeline' || scenarioId === 'composer-submit' || scenarioId === 'catalog-refresh' || scenarioId === 'catalog-refresh-dark' || scenarioId === 'model-role-selector' || scenarioId === 'model-role-selector-dark' || scenarioId === 'model-role-selector-wide' || scenarioId === 'subagent-process' || scenarioId === 'subagent-process-dark' || scenarioId === 'file-visibility' || scenarioId === 'file-visibility-dark' || scenarioId === 'immersive-bottom' || scenarioId === 'immersive-bottom-dark' || scenarioId === 'immersive-bottom-wide' || scenarioId === 'immersive-bottom-drawer' || scenarioId === 'immersive-bottom-settings' || scenarioId === 'immersive-bottom-settings-dark' || scenarioId === 'immersive-bottom-cover' || scenarioId === 'immersive-bottom-miniapps' || scenarioId === 'immersive-bottom-wide-shell' || scenarioId === 'immersive-bottom-tools' || scenarioId === 'immersive-bottom-tools-editor' || scenarioId === 'immersive-bottom-tools-file-action' || scenarioId === 'immersive-bottom-view-settings' || scenarioId === 'immersive-bottom-connect' || scenarioId === 'composer-overflow' || scenarioId === 'composer-overflow-empty' || scenarioId === 'composer-overflow-dark' || scenarioId === 'composer-overflow-wide' || scenarioId === 'composer-overflow-expanded' || scenarioId === 'composer-overflow-expanded-dark' || scenarioId === 'device-selector' || scenarioId === 'device-selector-dark' || scenarioId === 'welcome-home' || scenarioId === 'connected-conversation' ||
if (scenarioId === 'history-scroll' || scenarioId === 'durable-timeline' || scenarioId === 'composer-submit' || scenarioId === 'catalog-refresh' || scenarioId === 'catalog-refresh-dark' || scenarioId === 'model-role-selector' || scenarioId === 'model-role-selector-dark' || scenarioId === 'model-role-selector-wide' || scenarioId === 'subagent-process' || scenarioId === 'subagent-process-dark' || scenarioId === 'file-visibility' || scenarioId === 'file-visibility-dark' || scenarioId === 'immersive-bottom' || scenarioId === 'immersive-bottom-dark' || scenarioId === 'immersive-bottom-wide' || scenarioId === 'immersive-bottom-drawer' || scenarioId === 'immersive-bottom-drawer-dark' || scenarioId === 'immersive-bottom-settings' || scenarioId === 'immersive-bottom-settings-dark' || scenarioId === 'immersive-bottom-cover' || scenarioId === 'immersive-bottom-miniapps' || scenarioId === 'immersive-bottom-wide-shell' || scenarioId === 'immersive-bottom-tools' || scenarioId === 'immersive-bottom-tools-editor' || scenarioId === 'immersive-bottom-tools-file-action' || scenarioId === 'immersive-bottom-view-settings' || scenarioId === 'immersive-bottom-connect' || scenarioId === 'composer-overflow' || scenarioId === 'composer-overflow-empty' || scenarioId === 'composer-overflow-dark' || scenarioId === 'composer-overflow-wide' || scenarioId === 'composer-overflow-expanded' || scenarioId === 'composer-overflow-expanded-dark' || scenarioId === 'device-selector' || scenarioId === 'device-selector-dark' || scenarioId === 'welcome-home' || scenarioId === 'connected-conversation' ||
scenarioId === 'streaming-dark' ||
scenarioId === 'reconnecting-wide' || scenarioId === 'narrow-multiline' ||
scenarioId === 'fold-context' || scenarioId === 'long-reading' ||
Expand All @@ -25,7 +25,7 @@ export default class EntryAbility extends UIAbility {
AppStorage.setOrCreate('scenarioId', scenarioId);
}
try {
const colorMode = scenarioId === 'cold-start-home-dark' || scenarioId === 'catalog-refresh-dark' || scenarioId === 'model-role-selector-dark' || scenarioId === 'subagent-process-dark' || scenarioId === 'file-visibility-dark' || scenarioId === 'immersive-bottom-dark' || scenarioId === 'immersive-bottom-settings-dark' || scenarioId === 'composer-overflow-dark' || scenarioId === 'composer-overflow-expanded-dark' || scenarioId === 'device-selector-dark' || scenarioId === 'streaming-dark' || scenarioId === 'interaction-mailbox-dark'
const colorMode = scenarioId === 'cold-start-home-dark' || scenarioId === 'catalog-refresh-dark' || scenarioId === 'model-role-selector-dark' || scenarioId === 'subagent-process-dark' || scenarioId === 'file-visibility-dark' || scenarioId === 'immersive-bottom-dark' || scenarioId === 'immersive-bottom-drawer-dark' || scenarioId === 'immersive-bottom-settings-dark' || scenarioId === 'composer-overflow-dark' || scenarioId === 'composer-overflow-expanded-dark' || scenarioId === 'device-selector-dark' || scenarioId === 'streaming-dark' || scenarioId === 'interaction-mailbox-dark'
? ConfigurationConstant.ColorMode.COLOR_MODE_DARK
: scenarioId === 'cold-start-home' || scenarioId === 'catalog-refresh' || scenarioId === 'model-role-selector' || scenarioId === 'model-role-selector-wide' || scenarioId === 'subagent-process' || scenarioId === 'file-visibility' || scenarioId === 'immersive-bottom' || scenarioId === 'immersive-bottom-wide' || scenarioId === 'immersive-bottom-drawer' || scenarioId === 'immersive-bottom-settings' || scenarioId === 'immersive-bottom-cover' || scenarioId === 'immersive-bottom-miniapps' || scenarioId === 'immersive-bottom-wide-shell' || scenarioId === 'immersive-bottom-tools' || scenarioId === 'immersive-bottom-tools-editor' || scenarioId === 'immersive-bottom-tools-file-action' || scenarioId === 'immersive-bottom-view-settings' || scenarioId === 'immersive-bottom-connect' || scenarioId === 'composer-overflow' || scenarioId === 'composer-overflow-empty' || scenarioId === 'composer-overflow-wide' || scenarioId === 'composer-overflow-expanded' || scenarioId === 'device-selector' || scenarioId === 'connected-conversation' || scenarioId === 'reconnecting-wide'
? ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT
Expand Down Expand Up @@ -109,7 +109,10 @@ export default class EntryAbility extends UIAbility {
/**
* The immersive shell is not verifiable from a screenshot alone: whether the
* window covers the status bar and how tall the bottom strip is are device
* facts, so state them once per launch.
* facts, so state them once per launch. The camera cutout is stated in full —
* visibility and all four rectangles — because it is the one avoid area that
* moves to a different edge when the device rotates, and a device without a
* cutout reports zeroes here rather than staying silent about it.
*/
private logWindowInsets(mainWindow: window.Window): void {
try {
Expand All @@ -119,11 +122,37 @@ export default class EntryAbility extends UIAbility {
'Window insets statusBar=%{public}s navigation=%{public}s windowRect=%{public}s',
String(system.topRect.height), String(navigation.bottomRect.height),
JSON.stringify(mainWindow.getWindowProperties().windowRect));
const cutout = mainWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_CUTOUT);
hilog.info(DOMAIN, TAG,
'Window cutout visible=%{public}s top=%{public}s bottom=%{public}s left=%{public}s right=%{public}s',
String(cutout.visible), String(cutout.topRect.height), String(cutout.bottomRect.height),
String(cutout.leftRect.width), String(cutout.rightRect.width));
} catch (err) {
hilog.error(DOMAIN, TAG, 'Failed to read window insets. Cause: %{public}s', JSON.stringify(err));
}
}

/**
* The content colour of both system bars, for the whole window and the whole
* app.
*
* This is one value for one window, and it is derived from one thing only: the
* colour mode. There is no per-page override here, so any page whose top band
* is dark — a media viewer, an immersive title, a hero the design decides to
* paint dark — puts the light-mode ink on a dark band and the clock and the
* status icons stop being readable against it. Every top band the app ships
* today is a theme-following page colour (`PAGE_BG`, `SIDEBAR_BG`,
* `PAGE_BG_OVERLAY`, `WELCOME_PAGE_BG`, `CARD`), which is why the single value
* is correct as it stands; `immersive-bottom.test.cjs` guards that list as a
* tripwire.
*
* So: a new page that paints its own dark top band is not finished here. It has
* to come back to this method — either by declaring its band against the
* colour mode the way the others do, or by giving the app a per-page override
* (the platform's `HdsNavigation.systemBarStyle` is the page-level route, and
* the platform also warns against mixing it with the window-level calls used
* here). Either way the band and the content colour are one decision, not two.
*/
private updateSystemBars(colorMode?: ConfigurationConstant.ColorMode): void {
if (!this.mainWindow) {
return;
Expand All @@ -132,16 +161,14 @@ export default class EntryAbility extends UIAbility {
const content = dark ? MobileDesignColors.ink.dark : MobileDesignColors.ink.light;
try {
this.mainWindow.setWindowSystemBarProperties({
// Both bars are transparent, because the window is laid out full-screen
// and the strips they occupy are the page's own edges: the drawer's fill
// and the chat page's blurred header band are what should show there.
// An opaque status bar painted the page colour over both, which clipped
// the sidebar's title, and it also sat over the pull-down notification
// centre's frosted background as a white strip instead of blending into
// it. `statusBarContentColor` is what keeps the clock and the icons
// legible against whatever the page puts behind them.
statusBarColor: MobileDesignColors.transparent.light,
navigationBarColor: MobileDesignColors.transparent.light,
// The window is laid out full-screen above (`setWindowLayoutFullScreen(true)`),
// so it already runs in the immersive layout, and the status bar and the
// navigation area are transparent by platform default: both strips carry
// the page's own background through. The page therefore owns what shows
// there — the drawer's fill and the chat page's blurred header band — and
// any background colour set here would repaint the page's edge instead.
// The content colours are the only thing left to state: they keep the
// clock and the icons legible against whatever the page puts behind them.
statusBarContentColor: content,
navigationBarContentColor: content
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,17 @@ export struct AppShell {
.width('100%')
.height('100%')
.backgroundColor(SIDEBAR_BG)
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
// The cutout is an avoid area of its own, so the floor claims it too: a
// notch the framework would otherwise leave bare has to end up under the
// same fill the rest of the window is. The floor is the window itself —
// full width and full height — so it names all four edges.
// `expandSafeArea` is per edge and takes effect only where this
// component's boundary meets that edge's safe-area boundary, so claiming
// the top strip with `TOP` says nothing about the side strip a rotated
// cutout lands on: without `START`/`END` the fill stops at the inset and
// the page colour shows beside it.
.expandSafeArea([SafeAreaType.SYSTEM, SafeAreaType.CUTOUT],
[SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM, SafeAreaEdge.START, SafeAreaEdge.END])
.hitTestBehavior(HitTestMode.None)
.opacity((this.allowSidebar && this.shellState.showSidebar) ? 1 : 0)
// Matched to the content card rather than to the sidebar panel: the
Expand Down Expand Up @@ -148,7 +158,13 @@ export struct AppShell {
private CompactLoginCover() {
Stack({ alignContent: Alignment.Bottom }) {
Column().width('100%').height('100%').backgroundColor(SCRIM)
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
// The scrim is the cover's own background over the whole shell, so a bare
// notch would read as a hole in the dimming: it claims the cutout too.
// Like the floor it is the whole shell, so it names the same four edges:
// a rotated cutout sits on a side edge, and the scrim has to reach it or
// the dimming has a hole in it.
.expandSafeArea([SafeAreaType.SYSTEM, SafeAreaType.CUTOUT],
[SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM, SafeAreaEdge.START, SafeAreaEdge.END])
.onClick(() => { this.shellState.showSettings = false; })
Scroll() {
Column() { this.settings() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,12 @@ export struct AppSidebar {
// row is what has to keep clear of the status bar: the fill still reaches
// the top of the window, and the title, the search field and the session
// rows start below the strip.
.padding({ left: 20, right: 20, top: this.insets.top })
//
// Its side edges are the screen's too, and a rotated device moves the camera
// cutout onto one of them: the wider of the design's gutter and the cutout
// is what the panel reserves, so the 20 the design asks for survives on a
// device that claims no side strip at all.
.padding({ left: this.insets.leftPadding(20), right: this.insets.rightPadding(20), top: this.insets.top })
.backgroundColor(SIDEBAR_BG)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +184,26 @@ export struct ConversationView {
.width('100%')
// Carried by the band that owns the background, not by the wrapper: the
// band is what has to reach the status bar, and its measured height is
// what the transcript borrows as its content start offset.
.padding({ top: this.topSafeInset })
// what the transcript borrows as its content start offset. Its sides are
// the window's wherever this page *is* the window — the compact layout,
// and the wide compositions that put it flush against an edge — so it
// reserves the side strips as well; the header's own gutter lives inside
// it and is not restated here.
//
// In the usual wide layout this page is the detail pane instead, and then
// only its right edge is the window's: the left one is the master pane's.
// A layout dump of that posture reads the left strip as a 21.14vp indent
// on a band that never touches that edge (see the note on `FlowPlaceholder`
// in RemoteSurfaceHost — the padding model cannot drop one side per edge,
// and dropping both would put the compact header back under the cutout).
.padding({ left: this.insets.leftPadding(0), right: this.insets.rightPadding(0), top: this.topSafeInset })
.backgroundColor(PAGE_BG_OVERLAY)
.backgroundBlurStyle(BlurStyle.Thin)
// Named so a layout dump can be read against the pixels, the way the
// bottom fade is: this band's own box is what says where the page's top
// edge sits, and the side padding it carries is only visible in a dump as
// an offset between the box and its first child.
.id('conversation-top-band')
this.TopEdgeFade()
}
.width('100%')
Expand All @@ -206,8 +222,19 @@ export struct ConversationView {
// The composer keeps the distance from the screen edge it had while the page
// area ended above the navigation bar, and the layer's own box grows by that
// strip: the gradient below is the layer's background, so it fills the strip
// and the transcript behind it reaches the bottom of the screen.
.padding({ bottom: this.bottomSafeInset })
// and the transcript behind it reaches the bottom of the screen. Wherever the
// layer's sides are the window's — the compact layout, and the wide
// compositions that put the page flush against an edge — they are the
// layer's own business as well: the composer's gutter is inside it, and on a
// rotated device the camera cutout would otherwise sit over the first control
// of the row. In the usual wide layout the layer is the detail pane's width
// instead, so only its right edge is the window's and the left strip is an
// indent nothing needs; see the note on `TopOverlay` above.
.padding({
left: this.insets.leftPadding(0),
right: this.insets.rightPadding(0),
bottom: this.bottomSafeInset
})
// The fade covers the whole layer, not just a strip above it: the transcript
// runs behind the composer, so anything short of that leaves a line of text
// sitting crisp and legible beside the pill after it has already faded out
Expand All @@ -221,8 +248,19 @@ export struct ConversationView {
// into it. Where the immersive layout already puts the page area there the
// two overlap and nothing extra is drawn; on a window that still ends above
// the navigation bar this is what keeps the strip from being bare page
// colour under the fade.
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
// colour under the fade. The cutout is a separate avoid area, so the fade
// claims it as well: a device that rotated its camera onto an edge the fade
// reaches would otherwise show the page colour beside the gradient. Both of
// its sides are named because `expandSafeArea` is per edge and only takes
// effect where this box's own boundary meets that edge's safe-area
// boundary: a side the compact layer owns expands, and the same claim on the
// detail pane's left side — where the master pane is in between — draws
// nothing. `TOP` is deliberately absent: this layer is only as tall as the
// status line and the composer and sits at the bottom of the pane, so its
// top edge is nowhere near the window's top avoid area and naming it would
// claim a strip the fade never touches.
.expandSafeArea([SafeAreaType.SYSTEM, SafeAreaType.CUTOUT],
[SafeAreaEdge.START, SafeAreaEdge.END, SafeAreaEdge.BOTTOM])
// Named so a layout dump can be read against the pixels: the fade's own box
// is what says where the chat page's bottom edge sits and how far the
// transcript can scroll, and neither is visible in the composer's bounds.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,15 @@ export struct FilePreviewSurface {
// The header band is the surface's top edge in both hosts — the compact
// full-page overlay and the wide preview pane — so it carries the status bar
// strip itself: the band's fill still reaches the top of the window and the
// back control and title sit below the clock and the indicators.
.padding({ left: 8, right: 8, top: 8 + this.insets.top, bottom: 8 })
// back control and title sit below the clock and the indicators. It spans
// the window horizontally, so it carries the side strips the same way: on a
// rotated device the back control is what the camera cutout would cover.
.padding({
left: this.insets.leftPadding(8),
right: this.insets.rightPadding(8),
top: 8 + this.insets.top,
bottom: 8
})
.alignItems(VerticalAlign.Center)
.border({ width: { bottom: 1 }, color: LINE })
.backgroundColor(PAGE_BG)
Expand Down
Loading
Loading