Repository navigation
feat(core): expose foldable display features - #121
Merged
Merged
Conversation
houko
force-pushed
the
feat/foldable-geometry
branch
from
November 15, 2026 02:26
e20040e to
e95afb3
Compare
houko
force-pushed
the
feat/foldable-geometry
branch
from
November 15, 2026 03:44
e95afb3 to
e190ebf
Compare
houko
force-pushed
the
feat/foldable-geometry
branch
from
November 15, 2026 12:20
e190ebf to
5437746
Compare
houko
force-pushed
the
feat/foldable-geometry
branch
from
November 15, 2026 14:32
5437746 to
37b4b15
Compare
…l bar's edge on iOS 27.1
The device's window extensions send their first report of the folds only after the window has drawn its first frame, so an app cold-launched half open sent START without them, built and presented its first tree with one region and split it in a RESIZE a frame or more later. On a device that folds, by the folds the extensions list from vendor level 6 or by a hinge angle sensor before it, the view now holds the app's start until that first report, 300 ms at most from the first frame or 3 s from its first size for a window that does not draw. Devices without the extensions, and those that do not fold, start at once as before.
houko
force-pushed
the
feat/foldable-geometry
branch
from
November 15, 2026 14:34
37b4b15 to
e74054b
Compare
Collaborator
Author
|
Reviewed. Rebased onto main after #122 and #249. The Java in On a Pixel 3 XL, START still follows the view's first size within about 10 ms on cold launches, rotation reports one region with the safe area following the notch, and the clock and zone match the device's. One related gap that exists on main and is not introduced here: Android sends a rotation's safe area before its new size, so for a few milliseconds the engine sees the new safe area with the old window size. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the foldable design in
docs/architecture/foldable.mdfor #49, with the answers to its decisions the user confirmed on 2026-11-15. This replaces the branch's earlier model of a fold, hinge, cutout and barKindwith a flat, half-folded or folded posture sent throughSetEnvironment, which the design rejected.What it does
internal/drivergainsGeometry: usableRegions, each with a frame, a safe area and a reserved rectangle, andObstructions, each with a frame, an orientation and separateOccluding,SeparatingandState(HingeUnknown,HingeClosed,HingeHalfOpen,HingeOpen), plus an angle where the platform reports one.driver.Dividederives the regions from the obstructions at a size: it clips frames to the surface, drops frames outside it, and orders regions and obstructions by position.SetEnvironment.Engine.Resize,Engine.ResizeAndWaitandTree.Resizetake it with the size, andengine.Options.Geometrygives it to the first commit withOptions.Size, so an app started on a half-open foldable builds its first tree with both regions. Merged resizes keep the newest size and geometry together, andTree.Resizesets the size and the geometry in one batch, so a new geometry at the same size still applies.core.Env.Geometryexposes it as a signal that changes only when the geometry, or the safe area inside its regions, changes. It returns a copy. Core lays nothing out by it. System bars and cutouts stay inSafeArea.androidx.window.extensionsby reflection (Folds.java), at vendor API levels 1 and 2+, from API 30. The library takes one listener per context, so allNsgoViews of an activity share one listener that fans each report out to them. A report is sent at the view's next layout, so it goes with the size the view gets for the window it was measured at, and a view that attaches again sends the latest report itself. Frames are clipped to the view in pixels, so an embedded view offset in its window keeps a fold that crosses it. The runtime sends the features withSTARTandRESIZE, and sends none while the window's bounds differ from the ones the latest report was measured at. The extension sends its first report only after the window has drawn its first frame, so on a device that folds (a fold in the extension'sgetSupportedWindowFeaturesfrom vendor level 6, else a hinge angle sensor) the view holds the app's start until that report: at most 300 ms from the first frame, or 3 s from the view's first size for a window that never draws, after which it starts without the folds and aRESIZEbrings them. Devices without the extension, and devices that do not fold, start at once. The Go side derives the occluding and separating properties as Jetpack'sHardwareFoldingFeaturedoes.classes.dexandclasses.jarare rebuilt.nsgo builddeclares<uses-library android:name="androidx.window.extensions" android:required="false"/>in the APK, App Bundle or-embedAAR only for an app that readscore.Env.Geometry: its executable must keep the method's name, and one of its packages outside NSGo's internal ones must select aGeometrythat is not an imported package's. The second check is there because reflection by non-constant names, astsxuses, keeps every exported method, so every TSX app's executable names the method. No other app declares the library.reservedRegionsOfKind:regions inlayoutSubviewsand when the engine starts, so they go with the size it reports and with the first commit. It adds aUIHingeInteractionwhose updates set the state and the angle and lay the view out again. It also reports the trait collection'sverticalBarEdgeasEnvironment.VerticalBarEdge/Env.VerticalBarEdge. The backend checks classes and selectors first, so older systems report one region.CHANGELOG.mdis untouched, as main freezes it until 0.1.0.Review findings addressed
Environmentwas no longer comparable. Main'sEnvironment.Equalalready covers this, and the geometry is no longer inEnvironment.Geometry,Region,Obstruction,HingeState,VerticalBarEdge) and its constants are now aliased in core, andTestPublicAPINamesNoInternalTypepasses.Environment.DisplaysandEqual: the branch is rebuilt on main and keeps main'sEqual.Geometry.Equal, the stored value is cloned on the way in and out, and tests check that unrelated environment changes do not rerun readers.CHANGELOG.mduntil 0.1.0.Foldswas still unset. Registration no longer calls the views, and the view sends its size with the latest report once it is listening.NsgoViewin an activity got no reports, because the extension ignores a second listener for the same context. There is now one listener per activity, shared by its views.STARTcarried. The engine now starts with it, on iOS too.STARTbefore the extension's first report, which arrives after the window's first frame, so it built its first tree with one region and split it in aRESIZE54-315 ms later. The view now holds the start for that report on devices that fold (see above).aarManifesttakes both the overlay flag and the libraries, the binary manifest keeps both the overlay service anduses-library, and the overlay surfaces'Resizecalls (Android runtime andnsgotest) pass an empty geometry, since an overlay window over other apps has no fold.Recordingservice and theuses-libraryfor the window extensions,docs/architecture/tool.mdkeeps both paragraphs' sentences, and ARCHITECTURE.md drops both resolved open questions. feat(android): implement long-running microphone recording #122'sTestFrameworkAttributeIDschecks every attribute ID the manifest encoder writes, so it now listsandroid:required(0x0101028e,android.R.attr.required). The Java inFolds.javaandNsgoView.javamerged without changes to this branch's hunks. The half-open cold-launch checks on thepixel_foldemulator below therefore still describe this code.classes.dexandclasses.jarare rebuilt at each commit that changes Java and rebuild byte for byte from that commit's sources.Validation
After the rebase onto #122, at each of the six commits:
go tool nsgo check -target all(vet for every target,GOOS=androidincluded),go tool nsgo testofinternal/driver,core,internal/engine,internal/platform/...,nsgotestandcmd/nsgo/..., andgo tool nsgo test -run 'Layer|Skills|Doc' .. On a Pixel 3 XL (API 31, no window extensions), a probe app with a log of the view's first size andSTART: 9-11 ms from the first size toSTARTover 6 cold launches (one outlier at 28 ms under host load), against 8-13 ms for the same probe built on #122's head; rotation reports one region at 411x846 and 846x411;time.Nowreads the device's clock andAsia/Tokyozone.Before the rebase:
go testof every touched package (cmd/nsgo,cmd/nsgo/apk,core,internal/driver,internal/engine,internal/platform/android,internal/platform/ios,internal/platform/macos,internal/platform/miniprogram,internal/platform/watchos,nsgotest) andgo test -run 'Layer|Skills|Doc' .go vet ./...for darwin,GOOS=ios,GOOS=android GOARCH=arm64,GOOS=linux,GOOS=windowsandGOOS=js GOARCH=wasmclasses.dexandclasses.jarrebuild byte for byte from the committed JavaTestStartLaysOutWithItsGeometry(engine),TestAndroidLibrariesandTestFileSelectsGeometry(tool)Device checks, with a probe app that draws each region and logs every geometry it reads:
pixel_foldemulator, API 35: 6 cold launches half open with the final build (16 counting earlier iterations of the fix), each withSTARTcarrying the fold and the first tree built with two regions; screen recordings of 4 of them show the first frame presented after the splash already split. The same probe built from the previous head sentSTARTwithout the fold and split 155-229 ms later. Flat reports one region with a non-separating open fold; half open, rotation (horizontal fold), back, fold (outer display, one region) and unfold all report as before. A cold launch on the folded outer display starts as soon as the first report (no folds) arrives. With the extension's reports ignored, the app starts 300 ms after the first frame with one region.STARTis unchanged, 7-14 ms before and 7-18 ms after over 6 launches each, the process spawn either way.Device checks still needed:
-embedhost: twoNsgoViews in one activity both report the hinge, detaching one leaves the other reporting, removing a view and adding it back at the same size keeps two regions, and a view placed below a toolbar keeps a vertical fold.