diff --git a/Directory.Build.targets b/Directory.Build.targets index d42a4023..fb04912f 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -22,6 +22,8 @@ + + @@ -30,12 +32,14 @@ - - + + + + diff --git a/samples/JetNews/README.md b/samples/JetNews/README.md index 228b893b..82f8a9c5 100644 --- a/samples/JetNews/README.md +++ b/samples/JetNews/README.md @@ -62,7 +62,7 @@ feature. | Upstream feature | Tracking issue | |-----------------------------------------------------------------|----------------| -| Adaptive list-detail two-pane layout on wider devices | [#143](https://github.com/jonathanpeppers/Microsoft.AndroidX.Compose/issues/143) (WindowSizeClass), plus a SharedTransitionLayout / ListDetailScene binding | +| Adaptive list-detail two-pane layout on wider devices | `SharedTransitionLayout` / `ListDetailScene` binding (the `WindowSizeClass` read itself shipped in [#143](https://github.com/jonathanpeppers/Microsoft.AndroidX.Compose/issues/143) — see `composer.CurrentWindowAdaptiveInfo()`) | | Real hero PNGs in card / article (currently a solid `Box` filled with a per-post `HeroColor`) | [#145](https://github.com/jonathanpeppers/Microsoft.AndroidX.Compose/issues/145) — `ContentScale.Crop` on the `Image` facade; without it small vector hero images letterbox | | Inline-run paragraph styling (Link / Bold / Italic / Code spans inside one paragraph) | [#141](https://github.com/jonathanpeppers/Microsoft.AndroidX.Compose/issues/141) — `AnnotatedString` + `SpanStyle` | | Top-bar elevation / collapse on scroll (`pinnedScrollBehavior`, `enterAlwaysScrollBehavior`) | [#142](https://github.com/jonathanpeppers/Microsoft.AndroidX.Compose/issues/142) — `Modifier.nestedScroll` + `TopAppBarDefaults` | diff --git a/samples/README.md b/samples/README.md index b2ba4283..0365135b 100644 --- a/samples/README.md +++ b/samples/README.md @@ -46,7 +46,6 @@ that needs the same primitive. | [#20](https://github.com/jonathanpeppers/Microsoft.AndroidX.Compose/issues/20) | Edge-to-edge bootstrapping | Status/nav-bar overlap on every sample. | | [#141](https://github.com/jonathanpeppers/Microsoft.AndroidX.Compose/issues/141) | `AnnotatedString` + `SpanStyle` for inline-run text styling | Link / Bold / Italic / Code spans inside paragraphs in **JetNews** article reader. | | [#142](https://github.com/jonathanpeppers/Microsoft.AndroidX.Compose/issues/142) | `Modifier.nestedScroll` + `TopAppBarDefaults` scroll behaviors | Top-bar elevation / collapse on scroll in **JetNews**, **Reply**, **Jetcaster**. | -| [#143](https://github.com/jonathanpeppers/Microsoft.AndroidX.Compose/issues/143) | WindowSizeClass / `currentWindowAdaptiveInfo` | Adaptive layouts; blocks **Reply** entirely and the **JetNews** list-detail screen. | | [#144](https://github.com/jonathanpeppers/Microsoft.AndroidX.Compose/issues/144) | Custom `Layout {}` primitive — Measurable / Placeable / MeasureScope | `InterestsAdaptiveContentLayout` in **JetNews**, custom carousels in **Jetsnack**, asymmetric chat bubbles in **Jetchat**. | | [#145](https://github.com/jonathanpeppers/Microsoft.AndroidX.Compose/issues/145) | `ContentScale` + `Alignment` slots on the `Image` facade | Hero images on cards in **JetNews** (currently solid-color `Box` placeholders). | | [#146](https://github.com/jonathanpeppers/Microsoft.AndroidX.Compose/issues/146) | `stringResource(id)` lookup | Localizable UI strings in every sample (all currently inline literals). | @@ -66,7 +65,8 @@ Closed gaps that previously appeared here (now usable in samples): **#63** Modifier surface (Background/Border/Clickable/Size/Width/Height/AspectRatio/Offset/Alpha/Clip/Weight + scroll + focus + semantics + Draggable), **#65** Compose value types (`Color`/`Dp`/`Sp`/`FontWeight`/`TextAlign`), **#70** Row/Column `Arrangement`, -**#140** `DrawerState.open()` / `close()` suspend bridges. +**#140** `DrawerState.open()` / `close()` suspend bridges, +**#143** `WindowSizeClass` predicates + `currentWindowAdaptiveInfo()` extension (NavigationSuiteScaffold, SharedTransitionLayout, and ListDetailScene bindings still missing — see [#168](https://github.com/jonathanpeppers/Microsoft.AndroidX.Compose/issues/168) for the fold-aware TwoPane piece). ## Attribution diff --git a/samples/Reply/README.md b/samples/Reply/README.md index f1053e2e..5ad188c3 100644 --- a/samples/Reply/README.md +++ b/samples/Reply/README.md @@ -50,8 +50,8 @@ links back to the tracking issues. | Upstream feature | Status | Tracking issue | |------------------|--------|----------------| -| `NavigationSuiteScaffold` + `WindowSizeClass` (compact → medium → expanded switchover) | dropped — pinned to bottom nav | [#143](https://github.com/jonathanpeppers/Microsoft.AndroidX.Compose/issues/143) | -| `NavigationRail` / `PermanentNavigationDrawer` / `ModalNavigationDrawer` content for medium and expanded sizes | dropped — bottom nav only | [#143](https://github.com/jonathanpeppers/Microsoft.AndroidX.Compose/issues/143) | +| `NavigationSuiteScaffold` (compact → medium → expanded switchover) | dropped — pinned to bottom nav | `Xamarin.AndroidX.Compose.Material3.Adaptive.NavigationSuite` not yet referenced; would also need [#163](https://github.com/jonathanpeppers/Microsoft.AndroidX.Compose/issues/163). The size-class read itself ([#143](https://github.com/jonathanpeppers/Microsoft.AndroidX.Compose/issues/143)) shipped — see `composer.CurrentWindowAdaptiveInfo()`. | +| `NavigationRail` / `PermanentNavigationDrawer` / `ModalNavigationDrawer` content for medium and expanded sizes | dropped — bottom nav only | [#163](https://github.com/jonathanpeppers/Microsoft.AndroidX.Compose/issues/163) (drawer-row facade) — branching on `WindowSizeClass` is unblocked by [#143](https://github.com/jonathanpeppers/Microsoft.AndroidX.Compose/issues/143). | | `accompanist.adaptive.TwoPane` + `WindowLayoutInfo`/`FoldingFeature` (list-detail with fold avoidance) | dropped — single-pane | [#168](https://github.com/jonathanpeppers/Microsoft.AndroidX.Compose/issues/168) | | `NavigationDrawerItem` rows inside `ModalDrawerSheet` | not used (no drawer in single-pane port) | [#163](https://github.com/jonathanpeppers/Microsoft.AndroidX.Compose/issues/163) | | `BackHandler {}` to collapse multi-select / detail | dropped — system back falls through to the navigator | [#166](https://github.com/jonathanpeppers/Microsoft.AndroidX.Compose/issues/166) | diff --git a/samples/Reply/ReplyApp.cs b/samples/Reply/ReplyApp.cs index fa4cbdc8..9d9b9a34 100644 --- a/samples/Reply/ReplyApp.cs +++ b/samples/Reply/ReplyApp.cs @@ -8,8 +8,13 @@ namespace AndroidX.Compose.Samples.Reply; /// /// Upstream Reply uses NavigationSuiteScaffoldLayout + /// WindowSizeClass to pick between bottom nav (compact), nav -/// rail (medium), and permanent drawer (expanded). That adaptive -/// scaffold isn't bound yet (#143), so this port pins to bottom nav. +/// rail (medium), and permanent drawer (expanded). The +/// WindowSizeClass read itself is now available (issue #143 — +/// see composer.CurrentWindowAdaptiveInfo()), but +/// NavigationSuiteScaffold lives in the +/// Xamarin.AndroidX.Compose.Material3.Adaptive.NavigationSuite +/// package which isn't referenced yet, so this port still pins to +/// bottom nav. /// public static class ReplyApp { diff --git a/src/Microsoft.AndroidX.Compose.Gallery/Demos/LocalsMisc/WindowSizeClassDemo.cs b/src/Microsoft.AndroidX.Compose.Gallery/Demos/LocalsMisc/WindowSizeClassDemo.cs new file mode 100644 index 00000000..de9e6813 --- /dev/null +++ b/src/Microsoft.AndroidX.Compose.Gallery/Demos/LocalsMisc/WindowSizeClassDemo.cs @@ -0,0 +1,62 @@ +using AndroidX.Compose.Runtime; +using AndroidX.Compose.Gallery.Registry; +using AndroidX.Window.Core.Layout; + +namespace AndroidX.Compose.Gallery.Demos.LocalsMisc; + +/// +/// composer.CurrentWindowAdaptiveInfo() — live readout of the upstream +/// + Posture, plus a phone / tablet / desktop +/// indicator that flips at the Material 3 standard breakpoints. Rotate the +/// device or resize the split-screen window to see all values update. +/// +public static class WindowSizeClassDemo +{ + /// Registry entry exposed via . + public static Demo Demo => new( + Id: "locals-window-size-class", + CategoryId: "locals-misc", + Title: "WindowAdaptiveInfo & WindowSizeClass", + Description: "composer.CurrentWindowAdaptiveInfo() — live size class predicates + posture for adaptive layouts. Rotate to flip the Phone/Tablet/Desktop label.", + Build: _ => new Column + { + new SizeClassReadout(), + }); + + sealed class SizeClassReadout : ComposableNode + { + public override void Render(IComposer composer) + { + var info = composer.CurrentWindowAdaptiveInfo(); + var size = info.WindowSizeClass; + var posture = info.WindowPosture; + + new Column + { + new Text($"MinWidthDp = {size.MinWidthDp}"), + new Text($"MinHeightDp = {size.MinHeightDp}"), + new Text(""), + new Text("Width predicates at standard breakpoints:"), + new Text($" IsWidthAtLeastBreakpoint(600) = {size.IsWidthAtLeastBreakpoint(WindowSizeClass.WidthDpMediumLowerBound)}"), + new Text($" IsWidthAtLeastBreakpoint(840) = {size.IsWidthAtLeastBreakpoint(WindowSizeClass.WidthDpExpandedLowerBound)}"), + new Text("Height predicates at standard breakpoints:"), + new Text($" IsHeightAtLeastBreakpoint(480) = {size.IsHeightAtLeastBreakpoint(WindowSizeClass.HeightDpMediumLowerBound)}"), + new Text($" IsHeightAtLeastBreakpoint(900) = {size.IsHeightAtLeastBreakpoint(WindowSizeClass.HeightDpExpandedLowerBound)}"), + new Text(""), + new Text($"Layout class: {DeviceLabel(size)}"), + new Text(""), + new Text("Posture (foldable / hinge state):"), + new Text($" IsTabletop = {posture.IsTabletop}"), + new Text($" Hinge count = {posture.HingeList.Count}"), + }.Render(composer); + } + + // Process larger breakpoints first — IsWidthAtLeastBreakpoint is order- + // dependent: a 1200dp window matches the 600dp breakpoint too. Upstream + // docs explicitly call out this larger→smaller ordering. + static string DeviceLabel(WindowSizeClass size) => + size.IsWidthAtLeastBreakpoint(WindowSizeClass.WidthDpExpandedLowerBound) ? "🖥️ Desktop (Expanded)" : + size.IsWidthAtLeastBreakpoint(WindowSizeClass.WidthDpMediumLowerBound) ? "📟 Tablet (Medium)" : + "📱 Phone (Compact)"; + } +} diff --git a/src/Microsoft.AndroidX.Compose.Gallery/Registry/Catalog.cs b/src/Microsoft.AndroidX.Compose.Gallery/Registry/Catalog.cs index b7cb6444..70ea7e83 100644 --- a/src/Microsoft.AndroidX.Compose.Gallery/Registry/Catalog.cs +++ b/src/Microsoft.AndroidX.Compose.Gallery/Registry/Catalog.cs @@ -172,6 +172,7 @@ public static class Catalog D.LocalsMisc.BuiltInCompositionLocalsDemo.Demo, D.LocalsMisc.CustomCompositionLocalDemo.Demo, D.LocalsMisc.ComposeViewInteropDemo.Demo, + D.LocalsMisc.WindowSizeClassDemo.Demo, D.LocalsMisc.CircularProgressIndicatorDemo.Demo, D.LocalsMisc.LinearProgressIndicatorDemo.Demo, D.LocalsMisc.ImageDemo.Demo, diff --git a/src/Microsoft.AndroidX.Compose/ComposeExtensions.CurrentWindowAdaptiveInfo.cs b/src/Microsoft.AndroidX.Compose/ComposeExtensions.CurrentWindowAdaptiveInfo.cs new file mode 100644 index 00000000..4838d165 --- /dev/null +++ b/src/Microsoft.AndroidX.Compose/ComposeExtensions.CurrentWindowAdaptiveInfo.cs @@ -0,0 +1,59 @@ +using AndroidX.Compose.Material3.Adaptive; +using AndroidX.Compose.Runtime; + +namespace AndroidX.Compose; + +public static partial class ComposeExtensions +{ + /// + /// C# parity of Kotlin's currentWindowAdaptiveInfo() @Composable + /// from androidx.compose.material3.adaptive. Returns the live + /// for the host window — exposes both the + /// upstream + /// (with IsWidthAtLeastBreakpoint / IsHeightAtLeastBreakpoint + /// + the standard 600 / 840 / 480 / 900 dp breakpoint constants) and the + /// describing foldable / tabletop state. + /// + /// Use this to branch between phone / tablet / desktop layouts inside + /// a composable: + /// + /// + /// var info = composer.CurrentWindowAdaptiveInfo(); + /// if (info.WindowSizeClass.IsWidthAtLeastBreakpoint( + /// AndroidX.Window.Core.Layout.WindowSizeClass.WidthDpMediumLowerBound)) + /// return ListDetailScreen.Build(...); + /// else + /// return PhoneScreen.Build(...); + /// + /// + /// Re-reads on every recomposition Compose drives, so layout code + /// that branches off the returned reacts + /// naturally to rotation, multi-window resize, fold / unfold, and + /// activity-embedded surface changes. + /// + /// The composer for the active composition. + /// + /// When true, snaps width buckets at the additional 1200 dp + /// (large) and 1600 dp (extra-large) breakpoints in addition to the + /// standard 600 / 840 bounds. Mirrors the Kotlin parameter + /// of the same name; defaults to false for parity with the + /// no-argument Kotlin call site. + /// + /// + /// The bound Kotlin would have returned — + /// not a managed wrapper. Pass it straight to any binding API that takes + /// one (e.g. NavigationSuiteScaffold). + /// + public static WindowAdaptiveInfo CurrentWindowAdaptiveInfo( + this IComposer composer, + bool supportLargeAndXLargeWidth = false) + { + ArgumentNullException.ThrowIfNull(composer); + + return WindowAdaptiveInfoKt.CurrentWindowAdaptiveInfo( + supportLargeAndXLargeWidth, + composer, + p2: 0, + _changed: 0); + } +} diff --git a/src/Microsoft.AndroidX.Compose/Microsoft.AndroidX.Compose.csproj b/src/Microsoft.AndroidX.Compose/Microsoft.AndroidX.Compose.csproj index 0e7b0d8b..58263bd2 100644 --- a/src/Microsoft.AndroidX.Compose/Microsoft.AndroidX.Compose.csproj +++ b/src/Microsoft.AndroidX.Compose/Microsoft.AndroidX.Compose.csproj @@ -34,6 +34,8 @@ + + @@ -58,6 +60,8 @@ + + diff --git a/src/Microsoft.AndroidX.Compose/PublicAPI.Unshipped.txt b/src/Microsoft.AndroidX.Compose/PublicAPI.Unshipped.txt index d276e26a..8108b0bc 100644 --- a/src/Microsoft.AndroidX.Compose/PublicAPI.Unshipped.txt +++ b/src/Microsoft.AndroidX.Compose/PublicAPI.Unshipped.txt @@ -1414,6 +1414,7 @@ static AndroidX.Compose.ComposeExtensions.CollectAsStateWithLifecycle(this Xa static AndroidX.Compose.ComposeExtensions.CollectAsStateWithLifecycle(this Xamarin.KotlinX.Coroutines.Flow.IStateFlow! stateFlow, AndroidX.Compose.Runtime.IComposer! composer) -> AndroidX.Compose.CollectedState! static AndroidX.Compose.ComposeExtensions.ColorResource(this AndroidX.Compose.Runtime.IComposer! composer, int id) -> long static AndroidX.Compose.ComposeExtensions.ColorScheme(this AndroidX.Compose.Runtime.IComposer! composer) -> AndroidX.Compose.Material3.ColorScheme! +static AndroidX.Compose.ComposeExtensions.CurrentWindowAdaptiveInfo(this AndroidX.Compose.Runtime.IComposer! composer, bool supportLargeAndXLargeWidth = false) -> AndroidX.Compose.Material3.Adaptive.WindowAdaptiveInfo! static AndroidX.Compose.ComposeExtensions.DerivedStateOf(System.Func! calculation) -> AndroidX.Compose.DerivedState! static AndroidX.Compose.ComposeExtensions.DerivedStateOf(this AndroidX.Compose.Runtime.IComposer! composer, System.Func! calculation) -> AndroidX.Compose.DerivedState! static AndroidX.Compose.ComposeExtensions.DimensionResource(this AndroidX.Compose.Runtime.IComposer! composer, int id) -> AndroidX.Compose.Dp