Skip to content

Bind containers and lists: Card, Chips, NavigationBar, NavigationRail#7

Merged
jonathanpeppers merged 1 commit into
mainfrom
jonathanpeppers/containers-and-lists
Jun 3, 2026
Merged

Bind containers and lists: Card, Chips, NavigationBar, NavigationRail#7
jonathanpeppers merged 1 commit into
mainfrom
jonathanpeppers/containers-and-lists

Conversation

@jonathanpeppers

@jonathanpeppers jonathanpeppers commented Jun 3, 2026

Copy link
Copy Markdown
Owner

Adds JNI bridges + tree-style facade for the next batch of Material 3 composables stripped from Xamarin.AndroidX.Compose.Material3Android 1.4.0.3.

Closes part of #2.

Composables landed

Composable JVM target Notes
Card (non-clickable) CardKt.Card (no mangle) ColumnScope Function3 content via collection-init
AssistChip ChipKt.AssistChip Label required; LeadingIcon/TrailingIcon optional slots
FilterChip ChipKt.FilterChip + selected bool ctor param
InputChip ChipKt.InputChip + Avatar slot
SuggestionChip ChipKt.SuggestionChip single Icon slot
NavigationBar + NavigationBarItem NavigationBarKt.NavigationBar-HsRjFd4 / NavigationBarItem RowScope receiver threaded via RenderContext
NavigationRail + NavigationRailItem NavigationRailKt.NavigationRail-qi6gXK8 / NavigationRailItem RailItem is top-level static, no scope plumbing

Scope-receiver plumbing

NavigationBarItem is a Kotlin RowScope extension static — the JVM signature begins with Landroidx/compose/foundation/layout/RowScope;. The receiver only exists inside the parent NavigationBar's content Function3 (p0 of the lambda). To pass it down to children:

  • ComposableLambda3 grew a second ctor Action<IntPtr scope, IComposer> that captures p0?.Handle and hands it to the body. The original Action<IComposer> ctor is preserved (delegates to the new one, discarding scope) so existing callers (Column, Box, Button, Card) need no change.
  • RenderContext is a [ThreadStatic] stash with a struct-disposable push/pop guard. NavigationBar.Render pushes the scope before RenderChildren; NavigationBarItem.Render reads RenderContext.CurrentScope and passes it as the first JNI arg.

NavigationRailItem does not take an extension receiver in the bytecode (despite NavigationRail exposing a ColumnScope content Function3), so it renders directly without consulting RenderContext.

Defaults bookkeeping

Each new bridge gets a *Default [Flags] enum generated by ComposeNet.SourceGenerators from a declarative [assembly: ComposeDefaults("Name", "param1", "!param2", ...)] attribute in ComposeDefaults.cs (the new pattern from #8). The ! prefix consumes a bit but emits no enum member — used for params the caller always provides (!onClick, !label, !selected, !content, !icon).

For optional slots (LeadingIcon, TrailingIcon, Avatar, Icon, Label), the bits stay as enum members — bridges start with All and clear them per-call when the user supplies the slot, same pattern as AlertDialog.

Sample

MainActivity.cs exercises:

  • a Card containing a counter snapshot,
  • an AssistChip (+1), FilterChip (toggles its own label), SuggestionChip (resets counter),
  • a NavigationBar with two NavigationBarItems flipping a MutableNumberState<int> tab.

Build verified with dotnet build src/ComposeNet.Sample. Source-generator tests pass (10/10). Deployed to an emulator and confirmed all controls render correctly.

Deferred to follow-ups (still part of #2)

State-bearing composables that need new infrastructure (calling rememberDrawerState / rememberCarouselState from inside Render and exposing the resulting DrawerState / CarouselState to user code):

  • ModalNavigationDrawer / DismissibleNavigationDrawer / PermanentNavigationDrawer (need DrawerState + rememberDrawerState)
  • HorizontalUncontainedCarousel / HorizontalMultiBrowseCarousel (need CarouselState + rememberCarouselState + a new Function4 lambda for the CarouselItemScope extension content)

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the ComposeNet.Compose facade to cover additional Material 3 “containers and lists” composables that are stripped/mangled in the Xamarin.AndroidX.Compose.Material3Android bindings, by adding JNI bridges, $default bitmask enums, and tree-style C# wrapper nodes. It also updates the sample app to exercise the new APIs, and adds minimal infrastructure to thread an extension-receiver scope (RowScope) to child composables.

Changes:

  • Added JNI bridge implementations for Card, chip family (AssistChip, FilterChip, InputChip, SuggestionChip), NavigationBar/NavigationBarItem, and NavigationRail/NavigationRailItem.
  • Added declarative ComposeDefaults entries for the new composables’ $default bitmask enums.
  • Added scope-receiver plumbing via a new ComposableLambda3 overload + thread-static RenderContext, and updated the sample to demonstrate the new composables.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/ComposeNet.Sample/MainActivity.cs Adds sample UI usage for Card, chips, and navigation components.
src/ComposeNet.Compose/ComposeDefaults.cs Adds declarative $default enum definitions for the newly bridged composables (and clarifies receiver-bitmask notes).
src/ComposeNet.Compose/ComposeBridges.cs Adds cached JNI bridges and signatures for the new Material 3 composables, including receiver-based NavigationBarItem.
src/ComposeNet.Compose/Composables.cs Introduces new public facade nodes/containers (Card, chip family, navigation bar/rail types) that call into the bridges and manage defaults.
src/ComposeNet.Compose/ComposableLambdas.cs Extends ComposableLambda3 to optionally capture the receiver scope handle and introduces RenderContext for passing it to children.

Comment thread src/ComposeNet.Compose/ComposableLambdas.cs Outdated
Comment thread src/ComposeNet.Compose/ComposeBridges.cs Outdated
@jonathanpeppers jonathanpeppers force-pushed the jonathanpeppers/containers-and-lists branch from 998eb4b to 2e2a6ab Compare June 3, 2026 18:32
…#2)

Adds JNI bridges + tree-style facade for the next batch of Material 3
composables stripped from Xamarin.AndroidX.Compose.Material3Android 1.4.0.3:

* Card (non-clickable) — CardKt.Card
* AssistChip / FilterChip / InputChip / SuggestionChip — ChipKt.*
* NavigationBar / NavigationBarItem — NavigationBarKt.*
* NavigationRail / NavigationRailItem — NavigationRailKt.*

NavigationBarItem is a `RowScope` extension static, so the receiver has
to be threaded from the parent NavigationBar. ComposableLambda3 grew an
optional ctor that exposes the captured scope handle, and a thread-static
RenderContext stash lets *Item composables read it during their Render.
NavigationRailItem is a top-level static (despite NavigationRail using a
ColumnScope content lambda), so no scope plumbing is needed there.

Sample wired up to demonstrate Card / chips / NavigationBar; reset chip
zeroes the counter, like-toggle FilterChip flips its label, and the two
NavigationBarItems flip a `MutableIntState` tab index.

Defers to a follow-up (state-bearing — needs rememberDrawerState /
rememberCarouselState infrastructure):
* ModalNavigationDrawer / DismissibleNavigationDrawer / PermanentNavigationDrawer
* HorizontalUncontainedCarousel / HorizontalMultiBrowseCarousel

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jonathanpeppers jonathanpeppers force-pushed the jonathanpeppers/containers-and-lists branch from 2e2a6ab to 3f2fa0d Compare June 3, 2026 18:49
@jonathanpeppers jonathanpeppers changed the title Bind containers and lists: Card, Chips, NavigationBar, NavigationRail (#2) Bind containers and lists: Card, Chips, NavigationBar, NavigationRail Jun 3, 2026
@jonathanpeppers jonathanpeppers merged commit ecc5d1c into main Jun 3, 2026
jonathanpeppers added a commit that referenced this pull request Jun 3, 2026
Wrap every JNIEnv.CallStaticVoidMethod in ComposeBridges.cs in a try/finally that calls GC.KeepAlive on each managed parameter whose .Handle was read into a JValue (lambdas, the composer, optional slot wrappers).

This matches what dotnet/java-interop emits for bound members. Without it, once .Handle is read the JIT can consider the managed wrapper dead and a GC during the JNI call can finalize it and invalidate the underlying handle.

Also document the pattern as rule #7 under the JNI-bridges section of .github/copilot-instructions.md so future bridges get it right.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jonathanpeppers jonathanpeppers deleted the jonathanpeppers/containers-and-lists branch June 4, 2026 16:51
jonathanpeppers added a commit that referenced this pull request Jun 10, 2026
Brainstorm idea #7 — make the "insert a small gap" idiom one expression instead of three.

## Motivation

Inserting an 8-dp gap reads:

```csharp
// before
new Spacer(Modifier.Companion.Width(8))
new Spacer(Modifier.Companion.Height(8))
new Spacer(Modifier.Companion.Weight(1f, fill: true))

// after
Spacer.Width(8)
Spacer.Height(8)
Spacer.Weight(1f)
```

Mirrors the shape of `Modifier.Width`/`Height`/`Size`. Same lowering — each factory is `new Spacer(Modifier.Companion.X(...))` under the hood, no new properties on `Spacer`, no behavioural change.

## API

Five new statics on `AndroidX.Compose.Spacer`:

| Factory | Equivalent |
|---|---|
| `Spacer.Width(Dp)` | `new Spacer(Modifier.Width(dp))` |
| `Spacer.Height(Dp)` | `new Spacer(Modifier.Height(dp))` |
| `Spacer.Size(Dp)` | `new Spacer(Modifier.Size(dp))` |
| `Spacer.Size(Dp, Dp)` | `new Spacer(Modifier.Size(w, h))` |
| `Spacer.Weight(float, bool fill = true)` | `new Spacer(Modifier.Weight(w, fill))` |

All take `Dp`; the existing `implicit operator Dp(int)` lets callers write `Spacer.Width(8)` literally.

The two existing ctors (`Spacer()`, `Spacer(Modifier)`) are **kept** — one site (`JetchatDrawer.cs:32` `StatusBarsPadding`) still legitimately uses the modifier ctor and four sentinel `new Spacer()` calls live in pattern-match defaults. Not removing them; this PR is non-breaking despite the original suggestion's openness to breaking changes.

## Migrated call sites (24)

| File | W/H/S | Weight |
|---|---|---|
| samples/Jetchat/Conversation.cs | 7 | |
| samples/Jetchat/JetchatDrawer.cs | 1 | |
| samples/Jetchat/Profile.cs | 3 | |
| samples/Jetchat/RecordButton.cs | 2 | |
| samples/JetNews/HomeCards.cs | 1 | 1 |
| samples/JetNews/JetnewsDrawer.cs | 1 | |
| samples/JetNews/PostBody.cs | 1 | |
| samples/JetNews/PostScreen.cs | 2 | |
| samples/Reply/EmptyComingSoon.cs | 1 | |
| src/Microsoft.AndroidX.Compose.Gallery/Demos/Navigation/BackHandlerDemo.cs | 4 | |
| **Total** | **23** | **1** |

Out-of-scope (untouched): 4 sentinel `new Spacer()` arms in `HomeScreen.cs` / `PostScreen.cs`, plus 1 `new Spacer(Modifier.Companion.StatusBarsPadding())` in `JetchatDrawer.cs`.

## Coordination — Modifier-statics race

The Modifier-statics PR (`jonathanpeppers/modifier-static-factories`) hasn't merged. **Factory bodies use `Modifier.Companion.Width(dp)`** (existing instance method on `Modifier.Companion`), not `Modifier.Width(dp)` (the not-yet-merged static). Once that PR lands a trivial mechanical follow-up can strip `.Companion` from these five lines in `Spacer.cs`.

## Verified

- ✅ `dotnet test src/Microsoft.AndroidX.Compose.SourceGenerators.Tests` — 130/130 passing
- ✅ `dotnet build src/Microsoft.AndroidX.Compose` — 0 errors
- ✅ `dotnet build src/Microsoft.AndroidX.Compose.Gallery` — 0 errors, 0 warnings
- ✅ `dotnet build samples/{Jetchat,JetNews,Reply}` — all green

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

2 participants