Skip to content

chore: headful baseline probe for #569 (do not merge) - #655

Closed
kdroidFilter wants to merge 7 commits into
nucleus-2.6from
ci/569-headful-baseline
Closed

chore: headful baseline probe for #569 (do not merge)#655
kdroidFilter wants to merge 7 commits into
nucleus-2.6from
ci/569-headful-baseline

Conversation

@kdroidFilter

Copy link
Copy Markdown
Collaborator

Temporary: runs tao-headful on the #569 branch as it stood before the review fixes, so the failures there can be told apart from regressions. Delete after reading.

#569)

`nativePopupLayers` made every Compose `Popup` a real OS window, but the
*decision* of where to put it stayed window-rooted, in two stacked ways.

The layers each built a work-area-sized `WindowInfo` so a popup could lay
out and flip against the display — and then `setContent` replayed the owner
window's composition locals over it, so `Popup.skiko.kt` read the window's
`containerSize` and clipped every popup back inside the window. The
intended design had never taken effect. The layers now re-provide
`LocalWindowInfo` inside the replayed locals.

With that in force the box is screen-sized but still rooted at the window's
content origin, so a `DropdownMenu` in a window near the bottom of the
display did not flip up — Compose saw a whole work area of room below the
anchor — and walked off the screen. Each layer now clamps its native frame
into the work area of the display it lands on, at the single point where it
pushes that frame: `popupScreenClampOffset`, fed the owner's content origin
on screen plus every display's work area. Only the native frame moves;
`boundsInWindow` stays what Compose believes, which is what hit-testing and
the surface content are expressed in. Re-clamped on every push, so an open
popup survives an owner drag, across monitors included.

Dialogs go through the same layers but must not follow the display:
`Dialog.skiko.kt` places at `containerSize.center`, so a window-owned
dialog centred on the screen would sit visibly off-centre and drift as the
window moved. Layers report the window size for dialogs and the work area
for popups, discriminated on `scrimColor` — only `Dialog.skiko.kt` writes
it, from `DialogAppearanceController.properties` during `DialogLayout`'s
composition, before `layer.Content { }` reads the container.

macOS needs the NSView's own origin on screen (a native title bar sits
between it and the window frame), hence `nativeGetContentRect`. Wayland
reports no geometry and is left unclamped: a popup there is a
`wl_subsurface` placed relative to the parent, with no global position.

Also exposes `nativePopupLayers` on `JewelDecoratedWindow`, which had no
such parameter at all — Jewel apps could not opt in. Jewel needs nothing
further: `DefaultPopupRenderer` delegates to `androidx.compose.ui.window.Popup`,
so its combo boxes, menus and tooltips flow through the fixed layers.

Tests: 18 unit cases on the clamp geometry, and 13 headful cases driving
real windows parked at real work-area edges — including one that reads the
popup HWND's rect back through Win32 and asserts it matches the reported
frame to the pixel, and two that pin the dialog contract. A new "Popups"
tab in nucleus-demo parks the window at any corner and opens menus
anchored at each window edge.
A Compose Dialog opened through nativePopupLayers had no scrim, clipped its
shadow and its appearance animation at the layout edge, slid diagonally
towards the display centre while scaling in, and stayed put when the window
was resized.

- Scrims: the owner window paints every layer's scrim after its content and
  each layer paints the scrims of the layers above it (PopupScrimRegistry,
  TaoSceneBundle.renderOverlay). A scrim change marks the owner scene
  visually dirty, so the Windows clean-frame present skip no longer eats the
  fade.
- Draw margin: the native surface extends 32 dp past boundsInWindow so
  shadows and the 10 dp slide-in are not clipped. Compose 1.12 renders a
  scene as one RenderNode drawable with unbounded bounds, so the R-tree
  cull-rect measurement upstream uses reports the whole canvas; a constant
  margin replaces it. The screen clamp is decided on the content rect; the
  interactive region stays the content (Windows content rect, macOS region
  hit-test, Linux press filter).
- Dialog scene size: a dialog's root Layout fills the layer scene's
  constraints and carries the appearance GraphicsLayer, so its scale pivots
  on the scene centre. Dialog layers now run their inner scene at the owner
  window size, popups keep the work area.
- Resize: the dialog container size is read from the owner's snapshot-backed
  WindowInfo, so the dialog re-centres when the window is resized.

DialogAppearanceHeadfulCases films both layer modes with java.awt.Robot and
compares slide-in, scrim ramp and settle time; PopupFrameRecord gains the
content frame next to the inflated native frame.
The appearance film now also records the hide animation and counts grabs
that repeat the previous frame during either animation — dropped frames
show up as a stall count the native layer must not exceed. The owner
window carries forty rows of text so its per-frame present costs something.
The first-visible check is one-sided: the native surface legitimately shows
its first frame before the owner's next present.
…t fades out (#569)

Dialog.skiko.kt's disappearance swaps the layer's content for an empty
Layout that replays the recorded picture, and Compose then reports a
zero-size boundsInWindow at the window centre for the whole fade-out. An
in-scene layer draws into the window canvas and does not care; the native
surface followed the bounds and shrank to a 32 dp square around a point, so
a closing dialog collapsed and vanished instead of fading out. Each layer
now sizes and places its surface on the last non-empty bounds.

The appearance film gains the Material 3 AlertDialog nucleus-demo opens,
a warm-up before filming, a duration-based comparison, and the smallest
height the dialog spanned while fading out.
…face

The Windows and Linux context menu flyouts drew inside the window's render
target whenever the window ran without nativePopupLayers, so a menu opened
near an edge was clipped by the window like any in-scene popup. An
OS-looking menu has to leave the window like the menus it imitates, and the
application's choice for its other popups must not decide that.

nativePopupLayers is a whole-scene switch (platform vs canvas layers), so a
per-popup opt-in needs the seam Compose 1.12 actually uses: Popup picks its
layer through LocalComposeSceneContext. A friend-package Java accessor
reaches that internal local without reflection; NativePopupLayers { } then
provides, for its subtree only, the window scene's own context with
createLayer routed to the window's native popup layer factory — the same
factory attach() uses when nativePopupLayers is on. The context menu
representation wraps the Windows and Linux flyouts in it; macOS stays on
NSMenu.
…tates

Three things the menu got wrong on a Linux desktop, found by driving a real
right click against a nested GNOME Shell and reading back both screenshots
and the app's own trace (scripts/context-menu-wayland-e2e.py, with the
fixture it drives in nucleus-application's tests).

A menu opened near the bottom of the screen was cut off. On native Wayland a
client cannot know where its own window is, so it cannot keep a popup on
screen by itself — the X11 clamp of #569 has nothing to work with there. The
popup layer now maps as an xdg_popup instead of a wl_subsurface and lets the
compositor place it: it flips above the pointer when there is no room below
and slides along an edge, which is what GTK's own menus do. The tao patch
carries the anchor point, the surface size and the shadow margins in one
request, because GDK builds the positioner from the window's geometry as it
stands at map time — a popup still sized 1x1 there asks the compositor to
constrain a 1x1 rectangle and is never flipped. One popup per parent takes
that path (an xdg_popup must be its parent's topmost popup); a dialog keeps
the subsurface, since it belongs to its window rather than to the display.

A second right click only closed the menu instead of moving it. The press
that dismisses a popup is delivered to the scene in the same turn as the
dismissal, so Compose's contextMenuOpenDetector — disabled while the menu is
open — was still disabled when the press arrived, and the press did nothing.
The host now recomposes and re-lays-out the scene between the two, so the
detector is listening again by the time it sees the press.

The menu also appeared a beat late: the layer painted its first frame only on
the owner window's next redraw, though that first render is what measures the
content and puts the popup on screen at all. It renders as soon as its GPU
side is up. Measured against the fixture's trace, press to first present is
now 40 ms steady, 108 ms for the first menu of a session.
@kdroidFilter
kdroidFilter deleted the ci/569-headful-baseline branch September 6, 2026 06:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant