Skip to content

Bind ModalBottomSheet, BottomSheetScaffold, date/time pickers, and Tooltip#9

Merged
jonathanpeppers merged 4 commits into
mainfrom
jonathanpeppers/bind-dialogs-sheets-followups
Jun 3, 2026
Merged

Bind ModalBottomSheet, BottomSheetScaffold, date/time pickers, and Tooltip#9
jonathanpeppers merged 4 commits into
mainfrom
jonathanpeppers/bind-dialogs-sheets-followups

Conversation

@jonathanpeppers

@jonathanpeppers jonathanpeppers commented Jun 3, 2026

Copy link
Copy Markdown
Owner

Follow-up to #6 — completes the user-facing pieces of #3 by binding the remaining Material 3 dialogs, sheets, pickers, and Tooltip. Rebased onto current main, which picks up the declarative [assembly: ComposeDefaults(...)] source generator from #8 and uses it for every new $default enum.

Composables added

Composable Kt class State holder
ModalBottomSheet ModalBottomSheet_androidKt SheetState via direct C# call to ModalBottomSheetKt.RememberModalBottomSheetState (NOT stripped)
BottomSheetScaffold BottomSheetScaffoldKt BottomSheetScaffoldState via direct C# call to BottomSheetScaffoldKt.RememberBottomSheetScaffoldState (NOT stripped)
DatePickerDialog DatePickerDialog_androidKt DatePickerState via JNI rememberDatePickerState-EU0dCGE
DatePicker DatePickerKt (uses DatePickerState)
TimePicker TimePickerKt TimePickerState via JNI rememberTimePickerState
TimePickerDialog TimePickerDialogKt (uses TimePickerState)
Tooltip (TooltipBox) TooltipKt TooltipState via JNI rememberTooltipState + TooltipDefaults.INSTANCE.rememberPlainTooltipPositionProvider-kHDZbjc

RememberModalBottomSheetState and RememberBottomSheetScaffoldState are present in the bound assembly, so the facades call them directly through C# instead of going through JNI — per the upstream binding shape noted in #6's description.

Intentionally NOT bound

#3 also listed BasicTooltipBox and BasicEdgeToEdgeDialog. Both live in androidx.compose.material3.internal — they're the foundation primitives that the public Tooltip / Dialog / AlertDialog / DatePickerDialog / TimePickerDialog / TooltipBox wrappers are built on top of, and there is no user-facing scenario where they're preferable to the public composables we already bind. (Confirmed empirically: BasicEdgeToEdgeDialog rendered as a fullscreen overlay with no way to dismiss it from the sample.) Skipped on purpose, with comments in Composables.cs explaining why.

Pattern

Each facade mirrors AlertDialog from #6 — slot composables exposed as named properties on the facade, optional slots tracked via the standard Compose $default bitmask. The 6 new bitmask enums are generated via the declarative attribute pattern from #8:

[assembly: ComposeDefaults("ModalBottomSheetDefault",
    "!onDismissRequest", "sheetState", "!modifier", ..., "dragHandle", "!content")]

!param consumes a bit position without emitting an enum member, covering the params the caller always provides.

JNI plumbing lives in ComposeBridges.cs: 7 composable bridges + 4 state-holder/helper bridges, with full mangled descriptors documented inline so future readers can verify against the AAR via javap.

Sample usage

// ModalBottomSheet — SheetState comes from the bound C# RememberModalBottomSheetState (no JNI).
new ModalBottomSheet(onDismissRequest: () => showSheet.Value = false)
{
    new Column
    {
        new Text("Modal bottom sheet"),
        new Text("Drag down or tap outside to dismiss."),
        new Button(onClick: () => showSheet.Value = false) { new Text("Hide") },
    },
}

// DatePickerDialog with a DatePicker as its body slot.
new DatePickerDialog(onDismissRequest: () => showDate.Value = false)
{
    ConfirmButton = new Button(onClick: () => showDate.Value = false) { new Text("OK") },
    DismissButton = new Button(onClick: () => showDate.Value = false) { new Text("Cancel") },
    Body          = new DatePicker(),
}

// TimePickerDialog with a TimePicker as its body slot.
new TimePickerDialog(onDismissRequest: () => showTime.Value = false)
{
    Title         = new Text("Pick a time"),
    ConfirmButton = new Button(onClick: () => showTime.Value = false) { new Text("OK") },
    DismissButton = new Button(onClick: () => showTime.Value = false) { new Text("Cancel") },
    Body          = new TimePicker(),
}

// Tooltip — long-press the anchor to show the tooltip popup.
new Tooltip
{
    Tip    = new Surface { new Text("Helpful hint") },
    Anchor = new Button(onClick: () => count++) { new Text("Long-press me") },
}

BottomSheetScaffold is bound but not exercised in the sample — the simpler ModalBottomSheet covers the smoke-test path; the scaffold variant is available for callers that need a persistent sheet.

Verification

  • dotnet build src/ComposeNet.Compose/ComposeNet.Compose.csproj — clean (the source generator from Generate ComposeDefaults enums via declarative attribute #8 emits the 6 new enums correctly).
  • dotnet build src/ComposeNet.Sample/ComposeNet.Sample.csproj -t:Install — clean.
  • App launches on emulator-5554, no FATAL / AndroidRuntime errors in the app pid; each new dialog/sheet/picker can be opened and dismissed from the sample's button row.

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

@jonathanpeppers jonathanpeppers force-pushed the jonathanpeppers/bind-dialogs-sheets-followups branch from 94cb662 to 4c3e205 Compare June 3, 2026 17:56
@jonathanpeppers jonathanpeppers changed the title Bind ModalBottomSheet, BottomSheetScaffold, date/time pickers, tooltips, and BasicEdgeToEdgeDialog Bind ModalBottomSheet, BottomSheetScaffold, date/time pickers, and tooltips Jun 3, 2026
@jonathanpeppers jonathanpeppers force-pushed the jonathanpeppers/bind-dialogs-sheets-followups branch from 4c3e205 to c951c4d Compare June 3, 2026 18:03
@jonathanpeppers jonathanpeppers changed the title Bind ModalBottomSheet, BottomSheetScaffold, date/time pickers, and tooltips Bind ModalBottomSheet, BottomSheetScaffold, date/time pickers, and Tooltip Jun 3, 2026
@jonathanpeppers jonathanpeppers requested a review from Copilot June 3, 2026 18:18

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 expands the ComposeNet Material 3 facade to cover additional “dialogs and sheets” UI primitives (sheets, picker dialogs, pickers, and tooltip), using the existing slot-property + $default bitmask pattern and adding the necessary JNI bridges where bindings are stripped/incomplete.

Changes:

  • Added new public facades in Composables.cs for ModalBottomSheet, BottomSheetScaffold, DatePickerDialog/DatePicker, TimePickerDialog/TimePicker, and Tooltip (TooltipBox wrapper).
  • Added new declarative [assembly: ComposeDefaults(...)] entries for the newly bridged composables’ $default masks.
  • Added multiple new raw-JNI bridge methods in ComposeBridges.cs (composables + remember* state holders) and updated the sample to exercise the new UI.

Reviewed changes

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

File Description
src/ComposeNet.Sample/MainActivity.cs Updates the sample UI to open/dismiss the newly added dialogs/sheets/pickers and demonstrate tooltip behavior.
src/ComposeNet.Compose/ComposeDefaults.cs Adds new declarative ComposeDefaults definitions for the new composables’ $default bitmasks.
src/ComposeNet.Compose/ComposeBridges.cs Introduces new JNI bridge entry points for stripped composables and state-holder remember* helpers.
src/ComposeNet.Compose/Composables.cs Adds the new public facade node types and wires them to the bridges/defaults pattern.

Comment thread src/ComposeNet.Compose/ComposeDefaults.cs
Comment thread src/ComposeNet.Compose/ComposeBridges.cs Outdated
Comment thread src/ComposeNet.Compose/Composables.cs
Comment thread src/ComposeNet.Compose/Composables.cs
@jonathanpeppers jonathanpeppers force-pushed the jonathanpeppers/bind-dialogs-sheets-followups branch from c951c4d to f63f797 Compare June 3, 2026 18:31
jonathanpeppers added a commit that referenced this pull request Jun 3, 2026
…ckerState

PR #9 review (#9 (review)):

1. Generate a $default enum for DatePickerKt.DatePicker via the
   declarative ComposeDefaults attribute, replacing the hard-coded
   0b11111110 magic number in ComposeBridges.DatePicker. The bridge now
   takes an `int defaults` parameter the same way every other composable
   bridge does, and DatePicker.Render passes (int)DatePickerDefault.All.

2. Expose DatePickerState/TimePickerState wrappers so callers can read
   the picked date/time from a button callback. Each new wrapper holds
   the JVM state interface (IDatePickerState / ITimePickerState — both
   bound, no extra JNI needed) and exposes Kotlin-faithful properties:

     DatePickerState.SelectedDateMillis        long?  (Unix epoch UTC)
     DatePickerState.DisplayedMonthMillis      long
     TimePickerState.Hour                      int
     TimePickerState.Minute                    int
     TimePickerState.Is24Hour                  bool

   DatePicker / TimePicker now accept an optional state argument; if
   omitted (or for TimePicker, with default initial values), a fresh
   state is created internally and the selection is unobservable, just
   like the previous behaviour.

   Sample (MainActivity.cs) demonstrates the read-back pattern:
   tapping the time picker dialog's OK button now displays "09:30"
   (the initial values) in the trigger row's status text, and the
   date picker's confirm callback reads SelectedDateMillis from its
   bound state.

Verified on emulator-5554:
  - dotnet build src/ComposeNet.Compose — clean (generator emits
    DatePickerDefault correctly).
  - Sample installs and launches with no FATAL/AndroidRuntime errors.
  - UI-automation drive: open date dialog, OK -> "Picked date: (none)"
    (null SelectedDateMillis when nothing selected, no crash).
    Open time dialog, OK -> "Picked time: 09:30" (reads initial values
    via the bound ITimePickerState).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers and others added 3 commits June 3, 2026 14:10
…oltip

Follow-up to PR #6 — completes the user-facing pieces of issue #3 by
binding the remaining Material 3 dialogs, sheets, pickers, and the
Tooltip composable. Mirrors PR #6's named-slot facade + $default
bitmask pattern, with raw JNI bridges for any composable or
remember*State builder stripped from the binding.

Composables added (in src/ComposeNet.Compose/Composables.cs):
  - ModalBottomSheet (state via direct C# call to RememberModalBottomSheetState)
  - BottomSheetScaffold (state via direct C# call to RememberBottomSheetScaffoldState)
  - DatePickerDialog + DatePicker (state via JNI rememberDatePickerState-EU0dCGE)
  - TimePicker + TimePickerDialog (state via JNI rememberTimePickerState)
  - Tooltip (TooltipBox) with TooltipDefaults.rememberPlainTooltipPositionProvider

Issue #3 also listed BasicTooltipBox and BasicEdgeToEdgeDialog. Both
live in androidx.compose.material3.internal — they are foundation
primitives that the public Tooltip / Dialog / AlertDialog wrappers
are built on top of, and there is no user-facing scenario where
they're preferable to the public composables we already bind.
Skipped on purpose, with comments in Composables.cs explaining why.

JNI plumbing: 7 composable bridges + 4 state-holder/helper bridges
in ComposeBridges.cs, with full mangled descriptors documented inline
so future readers can verify against the AAR via javap.

Each facade has a corresponding [Flags] $default enum generated via
the new declarative `[assembly: ComposeDefaults("FooDefault", ...)]`
form introduced in PR #8 — names prefixed with `!` consume a bit
position but emit no enum member, covering the params the caller
always provides.

Sample (src/ComposeNet.Sample/MainActivity.cs) gains a button row
that toggles each new dialog/sheet via MutableState<bool>, plus an
inline Tooltip anchor so the popup can be exercised by long-pressing
on the emulator.

Build + smoke-tested on emulator-5554: app launches cleanly, no
NoSuchMethodError / UnsatisfiedLinkError / NPE in the app pid.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ckerState

PR #9 review (#9 (review)):

1. Generate a $default enum for DatePickerKt.DatePicker via the
   declarative ComposeDefaults attribute, replacing the hard-coded
   0b11111110 magic number in ComposeBridges.DatePicker. The bridge now
   takes an `int defaults` parameter the same way every other composable
   bridge does, and DatePicker.Render passes (int)DatePickerDefault.All.

2. Expose DatePickerState/TimePickerState wrappers so callers can read
   the picked date/time from a button callback. Each new wrapper holds
   the JVM state interface (IDatePickerState / ITimePickerState — both
   bound, no extra JNI needed) and exposes Kotlin-faithful properties:

     DatePickerState.SelectedDateMillis        long?  (Unix epoch UTC)
     DatePickerState.DisplayedMonthMillis      long
     TimePickerState.Hour                      int
     TimePickerState.Minute                    int
     TimePickerState.Is24Hour                  bool

   DatePicker / TimePicker now accept an optional state argument; if
   omitted (or for TimePicker, with default initial values), a fresh
   state is created internally and the selection is unobservable, just
   like the previous behaviour.

   Sample (MainActivity.cs) demonstrates the read-back pattern:
   tapping the time picker dialog's OK button now displays "09:30"
   (the initial values) in the trigger row's status text, and the
   date picker's confirm callback reads SelectedDateMillis from its
   bound state.

Verified on emulator-5554:
  - dotnet build src/ComposeNet.Compose — clean (generator emits
    DatePickerDefault correctly).
  - Sample installs and launches with no FATAL/AndroidRuntime errors.
  - UI-automation drive: open date dialog, OK -> "Picked date: (none)"
    (null SelectedDateMillis when nothing selected, no crash).
    Open time dialog, OK -> "Picked time: 09:30" (reads initial values
    via the bound ITimePickerState).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Mirror the pattern from PR #11 (bfb108d) — wrap each
JNIEnv.CallStatic*Method invocation in try/finally and
GC.KeepAlive every managed wrapper whose .Handle was read
into a JValue. Once the handle is in the JValue the JIT
can consider the wrapper dead, and a GC mid-JNI-call would
finalize it and invalidate the handle.

Applied to: ModalBottomSheet, BottomSheetScaffold,
DatePickerDialog, DatePicker, TimePicker, TimePickerDialog,
TooltipBox, RememberDatePickerState, RememberTimePickerState,
RememberTooltipState, RememberPlainTooltipPositionProvider.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The sample had grown past one screen — buttons, chips, FAB, dialog
triggers, picker triggers and a tooltip were all stacked in one
Column. Split them across three tabs driven by the existing 'tab'
MutableNumberState:

  - Basics: Text, Button, IconButton, OutlinedTextField, Card
  - Buttons: chips, Tooltip, FloatingActionButton
  - Pickers: ModalBottomSheet/DatePicker/TimePicker triggers + readouts

NavigationBar at the bottom of the Column switches tabs. Overlays
(AlertDialog, ModalBottomSheet, DatePickerDialog, TimePickerDialog)
are still rendered at the root regardless of tab so a dialog opened
from the FAB on the Buttons tab still works after switching.

Note: the NavigationBar isn't pinned to the bottom edge — that
needs Scaffold (issue #12).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jonathanpeppers jonathanpeppers merged commit 1b53239 into main Jun 3, 2026
@jonathanpeppers jonathanpeppers deleted the jonathanpeppers/bind-dialogs-sheets-followups branch June 4, 2026 16:51
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