diff --git a/src/ComposeNet.Compose/Composables.cs b/src/ComposeNet.Compose/Composables.cs
index ee7eca5b..48e1c938 100644
--- a/src/ComposeNet.Compose/Composables.cs
+++ b/src/ComposeNet.Compose/Composables.cs
@@ -1,9 +1,9 @@
using System.Collections;
using System.Collections.Generic;
+using Android.Runtime;
using AndroidX.Compose.Foundation.Layout;
using AndroidX.Compose.Material3;
using AndroidX.Compose.Runtime;
-
namespace ComposeNet;
// ---- Leaf: Text ----
@@ -609,3 +609,341 @@ internal override void Render(IComposer composer)
ComposeBridges.NavigationRailItem(_selected, click, icon, label, defaults, composer);
}
}
+// ---- ModalBottomSheet ----
+
+///
+/// Material 3 ModalBottomSheet. Opens a modal sheet anchored to
+/// the bottom of the screen.
+///
+/// The SheetState is created inside via
+/// — that
+/// builder is NOT stripped from the binding, so we call it directly on
+/// the bound C# method instead of going through JNI. Use the visibility
+/// pattern from : gate the entire instance on
+/// a of .
+///
+///
+/// var show = Remember(() => new MutableState<bool>(false));
+/// show.Value
+/// ? new ModalBottomSheet(onDismissRequest: () => show.Value = false)
+/// {
+/// new Column { new Text("Sheet contents") },
+/// }
+/// : null
+///
+///
+public sealed class ModalBottomSheet : ComposableContainer
+{
+ readonly System.Action _onDismissRequest;
+ public ModalBottomSheet(System.Action onDismissRequest) => _onDismissRequest = onDismissRequest;
+
+ /// Optional drag handle drawn at the top of the sheet.
+ public ComposableNode? DragHandle { get; set; }
+
+ internal override void Render(IComposer composer)
+ {
+ // Bound C# call — RememberModalBottomSheetState is NOT stripped.
+ // p3 is the (renamed) Composer parameter; _changed = 3 means bits
+ // 0 and 1 (skipPartiallyExpanded, confirmValueChange) are
+ // defaulted by Compose.
+ var sheetState = ModalBottomSheetKt.RememberModalBottomSheetState(
+ skipPartiallyExpanded: false,
+ confirmValueChange: null,
+ _composer: composer,
+ p3: 0,
+ _changed: 3);
+
+ var onDismiss = new ComposableLambda0(_onDismissRequest);
+ var content = new ComposableLambda3(c => RenderChildren(c));
+ ComposableLambda2? dragHandle = DragHandle is null ? null
+ : new ComposableLambda2(c => DragHandle.Render(c));
+
+ int defaults = (int)ModalBottomSheetDefault.All;
+ if (dragHandle is not null) defaults &= ~(int)ModalBottomSheetDefault.DragHandle;
+
+ ComposeBridges.ModalBottomSheet(
+ onDismissRequest: onDismiss,
+ sheetState: ((Java.Lang.Object)sheetState).Handle,
+ dragHandle: dragHandle,
+ content: content,
+ defaults: defaults,
+ composer: composer);
+ }
+}
+
+// ---- BottomSheetScaffold ----
+
+///
+/// Material 3 BottomSheetScaffold. Hosts a persistent bottom
+/// sheet alongside a primary content area, plus optional top bar and
+/// snackbar slots.
+///
+///
+/// is called directly on the C# binding (NOT stripped), threading the
+/// composer through.
+///
+public sealed class BottomSheetScaffold : ComposableContainer
+{
+ /// Required: the persistent bottom-sheet content.
+ public ComposableNode? SheetContent { get; set; }
+
+ /// Optional: the sheet's drag handle.
+ public ComposableNode? SheetDragHandle { get; set; }
+
+ /// Optional: persistent top bar above the main content.
+ public ComposableNode? TopBar { get; set; }
+
+ internal override void Render(IComposer composer)
+ {
+ if (SheetContent is null)
+ throw new System.InvalidOperationException(
+ "BottomSheetScaffold.SheetContent is required.");
+
+ // Bound C# call — RememberBottomSheetScaffoldState is NOT stripped.
+ var scaffoldState = BottomSheetScaffoldKt.RememberBottomSheetScaffoldState(
+ bottomSheetState: null,
+ snackbarHostState: null,
+ _composer: composer,
+ p3: 0,
+ _changed: 3);
+
+ var sheet = new ComposableLambda3(c => SheetContent.Render(c));
+ var content = new ComposableLambda3(c => RenderChildren(c));
+
+ ComposableLambda2? dragHandle = SheetDragHandle is null ? null
+ : new ComposableLambda2(c => SheetDragHandle.Render(c));
+ ComposableLambda2? topBar = TopBar is null ? null
+ : new ComposableLambda2(c => TopBar.Render(c));
+
+ int defaults = (int)BottomSheetScaffoldDefault.All;
+ if (dragHandle is not null) defaults &= ~(int)BottomSheetScaffoldDefault.SheetDragHandle;
+ if (topBar is not null) defaults &= ~(int)BottomSheetScaffoldDefault.TopBar;
+
+ ComposeBridges.BottomSheetScaffold(
+ sheetContent: sheet,
+ scaffoldState: ((Java.Lang.Object)scaffoldState).Handle,
+ sheetDragHandle: dragHandle,
+ topBar: topBar,
+ snackbarHost: null,
+ content: content,
+ defaults: defaults,
+ composer: composer);
+ }
+}
+
+// ---- DatePickerDialog ----
+
+///
+/// Material 3 DatePickerDialog. The DatePickerState
+/// builder (rememberDatePickerState-EU0dCGE) is mangled and
+/// stripped from the binding, so we resolve it through JNI inside the
+/// facade. Place a
+/// (or any composable) inside .
+///
+public sealed class DatePickerDialog : ComposableNode
+{
+ readonly System.Action _onDismissRequest;
+ public DatePickerDialog(System.Action onDismissRequest) => _onDismissRequest = onDismissRequest;
+
+ /// Required: the affirmative button (no Kotlin default).
+ public ComposableNode? ConfirmButton { get; set; }
+
+ /// Optional: secondary button.
+ public ComposableNode? DismissButton { get; set; }
+
+ /// Required: dialog body — typically a .
+ public ComposableNode? Body { get; set; }
+
+ internal override void Render(IComposer composer)
+ {
+ if (ConfirmButton is null)
+ throw new System.InvalidOperationException(
+ "DatePickerDialog.ConfirmButton is required.");
+ if (Body is null)
+ throw new System.InvalidOperationException(
+ "DatePickerDialog.Body is required (the dialog's content slot).");
+
+ var onDismiss = new ComposableLambda0(_onDismissRequest);
+ var confirm = new ComposableLambda2(c => ConfirmButton.Render(c));
+ var content = new ComposableLambda3(c => Body.Render(c));
+ ComposableLambda2? dismiss = DismissButton is null ? null
+ : new ComposableLambda2(c => DismissButton.Render(c));
+
+ int defaults = (int)DatePickerDialogDefault.All;
+ if (dismiss is not null) defaults &= ~(int)DatePickerDialogDefault.DismissButton;
+
+ ComposeBridges.DatePickerDialog(
+ onDismissRequest: onDismiss,
+ confirmButton: confirm,
+ dismissButton: dismiss,
+ content: content,
+ defaults: defaults,
+ composer: composer);
+ }
+}
+
+// ---- DatePicker (the inline picker control used inside DatePickerDialog) ----
+
+///
+/// Material 3 DatePicker. The Kotlin DatePickerKt.DatePicker
+/// composable IS exposed by the binding, but its $default bitmask
+/// param isn't user-visible (the binding drops the trailing
+/// $default parameter on @Composable functions), so we go through
+/// raw JNI to set it. Place inside 's body.
+/// Pass an explicit to read the
+/// selection from a button callback; if none is supplied a fresh state
+/// is created internally and the selection is unobservable.
+///
+public sealed class DatePicker : ComposableNode
+{
+ readonly DatePickerState? _state;
+ public DatePicker(DatePickerState? state = null) => _state = state;
+
+ internal override void Render(IComposer composer)
+ {
+ var stateHandle = ComposeBridges.RememberDatePickerState(composer);
+ if (_state is not null && _state.Jvm is null)
+ _state.Jvm = Java.Lang.Object.GetObject(stateHandle, JniHandleOwnership.DoNotTransfer)!;
+ ComposeBridges.DatePicker(stateHandle, (int)DatePickerDefault.All, composer);
+ }
+}
+
+// ---- TimePicker ----
+
+///
+/// Material 3 TimePicker. Resolves TimePickerState via
+/// raw JNI (rememberTimePickerState takes a
+/// so it requires the composer-aware bridge). Pass an explicit
+/// to read the picked
+/// hour/minute from a button callback.
+///
+public sealed class TimePicker : ComposableNode
+{
+ readonly TimePickerState _state;
+ public TimePicker(TimePickerState? state = null) => _state = state ?? new TimePickerState();
+
+ internal override void Render(IComposer composer)
+ {
+ var stateHandle = ComposeBridges.RememberTimePickerState(_state.InitialHour, _state.InitialMinute, _state.InitialIs24Hour, composer);
+ if (_state.Jvm is null)
+ _state.Jvm = Java.Lang.Object.GetObject(stateHandle, JniHandleOwnership.DoNotTransfer)!;
+ ComposeBridges.TimePicker(stateHandle, (int)TimePickerDefault.All, composer);
+ }
+}
+
+// ---- TimePickerDialog ----
+
+///
+/// Material 3 TimePickerDialog. Both ConfirmButton and
+/// DismissButton are required by Compose;
+/// and are optional. Place a
+/// in the body.
+///
+public sealed class TimePickerDialog : ComposableNode
+{
+ readonly System.Action _onDismissRequest;
+ public TimePickerDialog(System.Action onDismissRequest) => _onDismissRequest = onDismissRequest;
+
+ public ComposableNode? ConfirmButton { get; set; }
+ public ComposableNode? DismissButton { get; set; }
+ public ComposableNode? Title { get; set; }
+ public ComposableNode? ModeToggleButton { get; set; }
+ /// Required: dialog body — typically a .
+ public ComposableNode? Body { get; set; }
+
+ internal override void Render(IComposer composer)
+ {
+ if (ConfirmButton is null || DismissButton is null)
+ throw new System.InvalidOperationException(
+ "TimePickerDialog.ConfirmButton and DismissButton are both required.");
+ if (Body is null)
+ throw new System.InvalidOperationException(
+ "TimePickerDialog.Body is required (the dialog's content slot).");
+
+ var onDismiss = new ComposableLambda0(_onDismissRequest);
+ var confirm = new ComposableLambda2(c => ConfirmButton.Render(c));
+ var dismiss = new ComposableLambda2(c => DismissButton.Render(c));
+ var content = new ComposableLambda3(c => Body.Render(c));
+ ComposableLambda2? title = Title is null ? null
+ : new ComposableLambda2(c => Title.Render(c));
+ ComposableLambda2? toggle = ModeToggleButton is null ? null
+ : new ComposableLambda2(c => ModeToggleButton.Render(c));
+
+ int defaults = (int)TimePickerDialogDefault.All;
+ if (title is not null) defaults &= ~(int)TimePickerDialogDefault.Title;
+ if (toggle is not null) defaults &= ~(int)TimePickerDialogDefault.ModeToggleButton;
+
+ ComposeBridges.TimePickerDialog(
+ onDismissRequest: onDismiss,
+ confirmButton: confirm,
+ dismissButton: dismiss,
+ title: title,
+ modeToggleButton: toggle,
+ content: content,
+ defaults: defaults,
+ composer: composer);
+ }
+}
+
+// ---- Tooltip (TooltipBox + plain styled tooltip) ----
+
+///
+/// Material 3 TooltipBox with a plain tooltip popup. The popup
+/// position provider comes from TooltipDefaults.rememberPlainTooltipPositionProvider;
+/// TooltipState from rememberTooltipState. Both are
+/// resolved via JNI inside .
+///
+/// Use named-property syntax: is the popup body shown
+/// on long-press / hover, is the always-visible
+/// content the popup attaches to.
+///
+public sealed class Tooltip : ComposableNode
+{
+ readonly bool _isPersistent;
+ public Tooltip(bool isPersistent = false) => _isPersistent = isPersistent;
+
+ /// Required: the popup body shown on long-press / hover.
+ public ComposableNode? Tip { get; set; }
+
+ /// Required: the always-visible anchor the tooltip attaches to.
+ public ComposableNode? Anchor { get; set; }
+
+ internal override void Render(IComposer composer)
+ {
+ if (Tip is null || Anchor is null)
+ throw new System.InvalidOperationException(
+ "Tooltip requires both Tip (popup body) and Anchor (visible content).");
+
+ var positionProvider = ComposeBridges.RememberPlainTooltipPositionProvider(composer);
+ var stateHandle = ComposeBridges.RememberTooltipState(_isPersistent, composer);
+
+ var tooltip = new ComposableLambda3(c => Tip.Render(c));
+ var anchor = new ComposableLambda2(c => Anchor.Render(c));
+
+ ComposeBridges.TooltipBox(
+ positionProvider: positionProvider,
+ tooltip: tooltip,
+ state: stateHandle,
+ content: anchor,
+ defaults: (int)TooltipBoxDefault.All,
+ composer: composer);
+ }
+}
+
+// ---- (BasicTooltip intentionally not bound) ----
+//
+// `androidx.compose.material3.internal.BasicTooltipKt.BasicTooltipBox`
+// is the foundation primitive that the public `TooltipBox` is built on
+// top of. There's no user-facing scenario where it's preferable to
+// `Tooltip` (the public wrapper we already bind), so it's skipped on
+// purpose for the same reason as `BasicEdgeToEdgeDialog`.
+
+// ---- (BasicEdgeToEdgeDialog intentionally not bound) ----
+//
+// `androidx.compose.material3.internal.BasicEdgeToEdgeDialog` is an
+// internal implementation primitive that upstream `Dialog`,
+// `AlertDialog`, `DatePickerDialog`, and `TimePickerDialog` are built
+// on top of. It draws content edge-to-edge with no scrim, no insets,
+// and no built-in dismiss handling — there is no user-facing scenario
+// where it's preferable to one of the public dialog wrappers we
+// already bind. Skipped on purpose.
\ No newline at end of file
diff --git a/src/ComposeNet.Compose/ComposeBridges.cs b/src/ComposeNet.Compose/ComposeBridges.cs
index c23dcc60..3ce13dd0 100644
--- a/src/ComposeNet.Compose/ComposeBridges.cs
+++ b/src/ComposeNet.Compose/ComposeBridges.cs
@@ -373,6 +373,551 @@ public static unsafe void AlertDialog(
}
}
+ // androidx.compose.material3.ModalBottomSheet_androidKt.ModalBottomSheet-dYc4hso(
+ // onDismissRequest, modifier, sheetState, sheetMaxWidth, shape,
+ // containerColor, contentColor, tonalElevation, scrimColor, dragHandle,
+ // windowInsets, properties, content, composer, $changed, $changed1, $default)
+ //
+ // 13 user params; bits 0 (onDismissRequest), 2 (sheetState), 12 (content)
+ // are always provided. dragHandle (bit 9) is the only optional slot.
+ const string ModalBottomSheetSig =
+ "(Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;" +
+ "Landroidx/compose/material3/SheetState;FLandroidx/compose/ui/graphics/Shape;JJFJ" +
+ "Lkotlin/jvm/functions/Function2;Landroidx/compose/foundation/layout/WindowInsets;" +
+ "Landroidx/compose/material3/ModalBottomSheetProperties;" +
+ "Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V";
+
+ static IntPtr s_modalBottomSheetClass;
+ static IntPtr s_modalBottomSheetMethod;
+
+ public static unsafe void ModalBottomSheet(
+ IFunction0 onDismissRequest,
+ IntPtr sheetState,
+ IFunction2? dragHandle,
+ IFunction3 content,
+ int defaults,
+ IComposer composer)
+ {
+ if (s_modalBottomSheetClass == IntPtr.Zero)
+ {
+ s_modalBottomSheetClass = JNIEnv.FindClass("androidx/compose/material3/ModalBottomSheet_androidKt");
+ s_modalBottomSheetMethod = JNIEnv.GetStaticMethodID(s_modalBottomSheetClass, "ModalBottomSheet-dYc4hso", ModalBottomSheetSig);
+ }
+
+ JValue* args = stackalloc JValue[17];
+ args[0] = new JValue(((Java.Lang.Object)onDismissRequest).Handle);
+ args[1] = new JValue(IntPtr.Zero); // modifier
+ args[2] = new JValue(sheetState);
+ args[3] = new JValue(0f); // sheetMaxWidth
+ args[4] = new JValue(IntPtr.Zero); // shape
+ args[5] = new JValue(0L); // containerColor
+ args[6] = new JValue(0L); // contentColor
+ args[7] = new JValue(0f); // tonalElevation
+ args[8] = new JValue(0L); // scrimColor
+ args[9] = new JValue(dragHandle is null ? IntPtr.Zero : ((Java.Lang.Object)dragHandle).Handle);
+ args[10] = new JValue(IntPtr.Zero); // windowInsets
+ args[11] = new JValue(IntPtr.Zero); // properties
+ args[12] = new JValue(((Java.Lang.Object)content).Handle);
+ args[13] = new JValue(((Java.Lang.Object)composer).Handle);
+ args[14] = new JValue(0); // $changed
+ args[15] = new JValue(0); // $changed1
+ args[16] = new JValue(defaults); // $default
+ try
+ {
+ JNIEnv.CallStaticVoidMethod(s_modalBottomSheetClass, s_modalBottomSheetMethod, args);
+ }
+ finally
+ {
+ GC.KeepAlive(onDismissRequest);
+ GC.KeepAlive(dragHandle);
+ GC.KeepAlive(content);
+ GC.KeepAlive(composer);
+ }
+ }
+
+ // androidx.compose.material3.BottomSheetScaffoldKt.BottomSheetScaffold-sdMYb0k(
+ // sheetContent, modifier, scaffoldState, sheetPeekHeight, sheetMaxWidth,
+ // sheetShape, sheetContainerColor, sheetContentColor, sheetTonalElevation,
+ // sheetShadowElevation, sheetDragHandle, sheetSwipeEnabled, topBar,
+ // snackbarHost, containerColor, contentColor, content, composer,
+ // $changed, $changed1, $default)
+ //
+ // 17 user params; provided bits: 0 (sheetContent), 2 (scaffoldState),
+ // 16 (content). Optional slots: 10 (sheetDragHandle), 12 (topBar),
+ // 13 (snackbarHost).
+ const string BottomSheetScaffoldSig =
+ "(Lkotlin/jvm/functions/Function3;Landroidx/compose/ui/Modifier;" +
+ "Landroidx/compose/material3/BottomSheetScaffoldState;FF" +
+ "Landroidx/compose/ui/graphics/Shape;JJFF" +
+ "Lkotlin/jvm/functions/Function2;Z" +
+ "Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;JJ" +
+ "Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V";
+
+ static IntPtr s_bottomSheetScaffoldClass;
+ static IntPtr s_bottomSheetScaffoldMethod;
+
+ public static unsafe void BottomSheetScaffold(
+ IFunction3 sheetContent,
+ IntPtr scaffoldState,
+ IFunction2? sheetDragHandle,
+ IFunction2? topBar,
+ IFunction3? snackbarHost,
+ IFunction3 content,
+ int defaults,
+ IComposer composer)
+ {
+ if (s_bottomSheetScaffoldClass == IntPtr.Zero)
+ {
+ s_bottomSheetScaffoldClass = JNIEnv.FindClass("androidx/compose/material3/BottomSheetScaffoldKt");
+ s_bottomSheetScaffoldMethod = JNIEnv.GetStaticMethodID(s_bottomSheetScaffoldClass, "BottomSheetScaffold-sdMYb0k", BottomSheetScaffoldSig);
+ }
+
+ JValue* args = stackalloc JValue[21];
+ args[0] = new JValue(((Java.Lang.Object)sheetContent).Handle);
+ args[1] = new JValue(IntPtr.Zero); // modifier
+ args[2] = new JValue(scaffoldState);
+ args[3] = new JValue(0f); // sheetPeekHeight
+ args[4] = new JValue(0f); // sheetMaxWidth
+ args[5] = new JValue(IntPtr.Zero); // sheetShape
+ args[6] = new JValue(0L); // sheetContainerColor
+ args[7] = new JValue(0L); // sheetContentColor
+ args[8] = new JValue(0f); // sheetTonalElevation
+ args[9] = new JValue(0f); // sheetShadowElevation
+ args[10] = new JValue(sheetDragHandle is null ? IntPtr.Zero : ((Java.Lang.Object)sheetDragHandle).Handle);
+ args[11] = new JValue(true); // sheetSwipeEnabled
+ args[12] = new JValue(topBar is null ? IntPtr.Zero : ((Java.Lang.Object)topBar).Handle);
+ args[13] = new JValue(snackbarHost is null ? IntPtr.Zero : ((Java.Lang.Object)snackbarHost).Handle);
+ args[14] = new JValue(0L); // containerColor
+ args[15] = new JValue(0L); // contentColor
+ args[16] = new JValue(((Java.Lang.Object)content).Handle);
+ args[17] = new JValue(((Java.Lang.Object)composer).Handle);
+ args[18] = new JValue(0); // $changed
+ args[19] = new JValue(0); // $changed1
+ args[20] = new JValue(defaults); // $default
+ try
+ {
+ JNIEnv.CallStaticVoidMethod(s_bottomSheetScaffoldClass, s_bottomSheetScaffoldMethod, args);
+ }
+ finally
+ {
+ GC.KeepAlive(sheetContent);
+ GC.KeepAlive(sheetDragHandle);
+ GC.KeepAlive(topBar);
+ GC.KeepAlive(snackbarHost);
+ GC.KeepAlive(content);
+ GC.KeepAlive(composer);
+ }
+ }
+
+ // androidx.compose.material3.DatePickerDialog_androidKt.DatePickerDialog-GmEhDVc(
+ // onDismissRequest, confirmButton, modifier, dismissButton, shape,
+ // tonalElevation, colors, properties, content, composer, $changed, $default)
+ //
+ // 9 user params; bits 0 (onDismissRequest), 1 (confirmButton),
+ // 8 (content) always provided. dismissButton (bit 3) is the only
+ // optional Function2 slot.
+ const string DatePickerDialogSig =
+ "(Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;" +
+ "Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;" +
+ "Landroidx/compose/ui/graphics/Shape;F" +
+ "Landroidx/compose/material3/DatePickerColors;" +
+ "Landroidx/compose/ui/window/DialogProperties;" +
+ "Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V";
+
+ static IntPtr s_datePickerDialogClass;
+ static IntPtr s_datePickerDialogMethod;
+
+ public static unsafe void DatePickerDialog(
+ IFunction0 onDismissRequest,
+ IFunction2 confirmButton,
+ IFunction2? dismissButton,
+ IFunction3 content,
+ int defaults,
+ IComposer composer)
+ {
+ if (s_datePickerDialogClass == IntPtr.Zero)
+ {
+ s_datePickerDialogClass = JNIEnv.FindClass("androidx/compose/material3/DatePickerDialog_androidKt");
+ s_datePickerDialogMethod = JNIEnv.GetStaticMethodID(s_datePickerDialogClass, "DatePickerDialog-GmEhDVc", DatePickerDialogSig);
+ }
+
+ JValue* args = stackalloc JValue[12];
+ args[0] = new JValue(((Java.Lang.Object)onDismissRequest).Handle);
+ args[1] = new JValue(((Java.Lang.Object)confirmButton).Handle);
+ args[2] = new JValue(IntPtr.Zero); // modifier
+ args[3] = new JValue(dismissButton is null ? IntPtr.Zero : ((Java.Lang.Object)dismissButton).Handle);
+ args[4] = new JValue(IntPtr.Zero); // shape
+ args[5] = new JValue(0f); // tonalElevation
+ args[6] = new JValue(IntPtr.Zero); // colors
+ args[7] = new JValue(IntPtr.Zero); // properties
+ args[8] = new JValue(((Java.Lang.Object)content).Handle);
+ args[9] = new JValue(((Java.Lang.Object)composer).Handle);
+ args[10] = new JValue(0); // $changed
+ args[11] = new JValue(defaults); // $default
+ try
+ {
+ JNIEnv.CallStaticVoidMethod(s_datePickerDialogClass, s_datePickerDialogMethod, args);
+ }
+ finally
+ {
+ GC.KeepAlive(onDismissRequest);
+ GC.KeepAlive(confirmButton);
+ GC.KeepAlive(dismissButton);
+ GC.KeepAlive(content);
+ GC.KeepAlive(composer);
+ }
+ }
+
+ // androidx.compose.material3.TimePickerKt.TimePicker-mT9BvqQ(
+ // state, modifier, colors, layoutType, composer, $changed, $default)
+ //
+ // 4 user params; bit 0 (state) always provided.
+ const string TimePickerSig =
+ "(Landroidx/compose/material3/TimePickerState;Landroidx/compose/ui/Modifier;" +
+ "Landroidx/compose/material3/TimePickerColors;ILandroidx/compose/runtime/Composer;II)V";
+
+ static IntPtr s_timePickerClass;
+ static IntPtr s_timePickerMethod;
+
+ public static unsafe void TimePicker(IntPtr state, int defaults, IComposer composer)
+ {
+ if (s_timePickerClass == IntPtr.Zero)
+ {
+ s_timePickerClass = JNIEnv.FindClass("androidx/compose/material3/TimePickerKt");
+ s_timePickerMethod = JNIEnv.GetStaticMethodID(s_timePickerClass, "TimePicker-mT9BvqQ", TimePickerSig);
+ }
+
+ JValue* args = stackalloc JValue[7];
+ args[0] = new JValue(state);
+ args[1] = new JValue(IntPtr.Zero); // modifier
+ args[2] = new JValue(IntPtr.Zero); // colors
+ args[3] = new JValue(0); // layoutType
+ args[4] = new JValue(((Java.Lang.Object)composer).Handle);
+ args[5] = new JValue(0); // $changed
+ args[6] = new JValue(defaults); // $default
+ try
+ {
+ JNIEnv.CallStaticVoidMethod(s_timePickerClass, s_timePickerMethod, args);
+ }
+ finally
+ {
+ GC.KeepAlive(composer);
+ }
+ }
+
+ // androidx.compose.material3.TimePickerDialogKt.TimePickerDialog-FItCLgY(
+ // onDismissRequest, confirmButton, dismissButton, modifier, properties,
+ // title, modeToggleButton, shape, containerColor, content, composer,
+ // $changed, $default)
+ //
+ // 10 user params. confirmButton (bit 1), dismissButton (bit 2), and
+ // content (bit 9) are required slots. title (bit 5) and
+ // modeToggleButton (bit 6) are optional.
+ const string TimePickerDialogSig =
+ "(Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;" +
+ "Lkotlin/jvm/functions/Function2;Landroidx/compose/ui/Modifier;" +
+ "Landroidx/compose/ui/window/DialogProperties;" +
+ "Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;" +
+ "Landroidx/compose/ui/graphics/Shape;J" +
+ "Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V";
+
+ static IntPtr s_timePickerDialogClass;
+ static IntPtr s_timePickerDialogMethod;
+
+ public static unsafe void TimePickerDialog(
+ IFunction0 onDismissRequest,
+ IFunction2 confirmButton,
+ IFunction2 dismissButton,
+ IFunction2? title,
+ IFunction2? modeToggleButton,
+ IFunction3 content,
+ int defaults,
+ IComposer composer)
+ {
+ if (s_timePickerDialogClass == IntPtr.Zero)
+ {
+ s_timePickerDialogClass = JNIEnv.FindClass("androidx/compose/material3/TimePickerDialogKt");
+ s_timePickerDialogMethod = JNIEnv.GetStaticMethodID(s_timePickerDialogClass, "TimePickerDialog-FItCLgY", TimePickerDialogSig);
+ }
+
+ JValue* args = stackalloc JValue[13];
+ args[0] = new JValue(((Java.Lang.Object)onDismissRequest).Handle);
+ args[1] = new JValue(((Java.Lang.Object)confirmButton).Handle);
+ args[2] = new JValue(((Java.Lang.Object)dismissButton).Handle);
+ args[3] = new JValue(IntPtr.Zero); // modifier
+ args[4] = new JValue(IntPtr.Zero); // properties
+ args[5] = new JValue(title is null ? IntPtr.Zero : ((Java.Lang.Object)title).Handle);
+ args[6] = new JValue(modeToggleButton is null ? IntPtr.Zero : ((Java.Lang.Object)modeToggleButton).Handle);
+ args[7] = new JValue(IntPtr.Zero); // shape
+ args[8] = new JValue(0L); // containerColor
+ args[9] = new JValue(((Java.Lang.Object)content).Handle);
+ args[10] = new JValue(((Java.Lang.Object)composer).Handle);
+ args[11] = new JValue(0); // $changed
+ args[12] = new JValue(defaults); // $default
+ try
+ {
+ JNIEnv.CallStaticVoidMethod(s_timePickerDialogClass, s_timePickerDialogMethod, args);
+ }
+ finally
+ {
+ GC.KeepAlive(onDismissRequest);
+ GC.KeepAlive(confirmButton);
+ GC.KeepAlive(dismissButton);
+ GC.KeepAlive(title);
+ GC.KeepAlive(modeToggleButton);
+ GC.KeepAlive(content);
+ GC.KeepAlive(composer);
+ }
+ }
+
+ // androidx.compose.material3.TooltipKt.TooltipBox (7-user-param overload):
+ // (positionProvider, tooltip, state, modifier, focusable, enableUserInput,
+ // content, composer, $changed, $default)
+ //
+ // Bits 0 (positionProvider), 1 (tooltip), 2 (state), 6 (content) provided.
+ const string TooltipBoxSig =
+ "(Landroidx/compose/ui/window/PopupPositionProvider;" +
+ "Lkotlin/jvm/functions/Function3;" +
+ "Landroidx/compose/material3/TooltipState;" +
+ "Landroidx/compose/ui/Modifier;ZZ" +
+ "Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V";
+
+ static IntPtr s_tooltipBoxClass;
+ static IntPtr s_tooltipBoxMethod;
+
+ public static unsafe void TooltipBox(
+ IntPtr positionProvider,
+ IFunction3 tooltip,
+ IntPtr state,
+ IFunction2 content,
+ int defaults,
+ IComposer composer)
+ {
+ if (s_tooltipBoxClass == IntPtr.Zero)
+ {
+ s_tooltipBoxClass = JNIEnv.FindClass("androidx/compose/material3/TooltipKt");
+ s_tooltipBoxMethod = JNIEnv.GetStaticMethodID(s_tooltipBoxClass, "TooltipBox", TooltipBoxSig);
+ }
+
+ JValue* args = stackalloc JValue[10];
+ args[0] = new JValue(positionProvider);
+ args[1] = new JValue(((Java.Lang.Object)tooltip).Handle);
+ args[2] = new JValue(state);
+ args[3] = new JValue(IntPtr.Zero); // modifier
+ args[4] = new JValue(true); // focusable
+ args[5] = new JValue(true); // enableUserInput
+ args[6] = new JValue(((Java.Lang.Object)content).Handle);
+ args[7] = new JValue(((Java.Lang.Object)composer).Handle);
+ args[8] = new JValue(0); // $changed
+ args[9] = new JValue(defaults); // $default
+ try
+ {
+ JNIEnv.CallStaticVoidMethod(s_tooltipBoxClass, s_tooltipBoxMethod, args);
+ }
+ finally
+ {
+ GC.KeepAlive(tooltip);
+ GC.KeepAlive(content);
+ GC.KeepAlive(composer);
+ }
+ }
+
+ // androidx.compose.material3.DatePickerKt.DatePicker(
+ // state, modifier, dateFormatter, colors, title, headline,
+ // showModeToggle, requestFocus, composer, $changed, $default)
+ //
+ // 8 user params; bit 0 (state) provided. requestFocus is the
+ // optional focus requester (defaultable).
+ const string DatePickerSig =
+ "(Landroidx/compose/material3/DatePickerState;Landroidx/compose/ui/Modifier;" +
+ "Landroidx/compose/material3/DatePickerFormatter;" +
+ "Landroidx/compose/material3/DatePickerColors;" +
+ "Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Z" +
+ "Landroidx/compose/ui/focus/FocusRequester;" +
+ "Landroidx/compose/runtime/Composer;II)V";
+
+ static IntPtr s_datePickerClass;
+ static IntPtr s_datePickerMethod;
+
+ public static unsafe void DatePicker(IntPtr state, int defaults, IComposer composer)
+ {
+ if (s_datePickerClass == IntPtr.Zero)
+ {
+ s_datePickerClass = JNIEnv.FindClass("androidx/compose/material3/DatePickerKt");
+ s_datePickerMethod = JNIEnv.GetStaticMethodID(s_datePickerClass, "DatePicker", DatePickerSig);
+ }
+
+ JValue* args = stackalloc JValue[11];
+ args[0] = new JValue(state);
+ args[1] = new JValue(IntPtr.Zero); // modifier
+ args[2] = new JValue(IntPtr.Zero); // dateFormatter
+ args[3] = new JValue(IntPtr.Zero); // colors
+ args[4] = new JValue(IntPtr.Zero); // title
+ args[5] = new JValue(IntPtr.Zero); // headline
+ args[6] = new JValue(true); // showModeToggle
+ args[7] = new JValue(IntPtr.Zero); // requestFocus
+ args[8] = new JValue(((Java.Lang.Object)composer).Handle);
+ args[9] = new JValue(0); // $changed
+ args[10] = new JValue(defaults); // $default
+ try
+ {
+ JNIEnv.CallStaticVoidMethod(s_datePickerClass, s_datePickerMethod, args);
+ }
+ finally
+ {
+ GC.KeepAlive(composer);
+ }
+ }
+
+ // ---- State-holder bridges ----
+ //
+ // Every `remember*State` builder is itself @Composable so it takes a
+ // trailing Composer + $changed + $default. The dotnet/android-libraries
+ // binding generator strips the mangled overloads (DatePickerState's
+ // `-EU0dCGE`), so we go through raw JNI for those. We always return
+ // the IntPtr — the caller threads it back into a composable bridge
+ // as a JValue argument without ever materialising a managed wrapper.
+
+ // androidx.compose.material3.DatePickerKt.rememberDatePickerState-EU0dCGE(
+ // initialSelectedDateMillis, initialDisplayedMonthMillis, yearRange,
+ // initialDisplayMode, selectableDates, composer, $changed, $default)
+ const string RememberDatePickerStateSig =
+ "(Ljava/lang/Long;Ljava/lang/Long;Lkotlin/ranges/IntRange;I" +
+ "Landroidx/compose/material3/SelectableDates;" +
+ "Landroidx/compose/runtime/Composer;II)Landroidx/compose/material3/DatePickerState;";
+
+ static IntPtr s_rememberDatePickerStateClass;
+ static IntPtr s_rememberDatePickerStateMethod;
+
+ public static unsafe IntPtr RememberDatePickerState(IComposer composer)
+ {
+ if (s_rememberDatePickerStateClass == IntPtr.Zero)
+ {
+ s_rememberDatePickerStateClass = JNIEnv.FindClass("androidx/compose/material3/DatePickerKt");
+ s_rememberDatePickerStateMethod = JNIEnv.GetStaticMethodID(s_rememberDatePickerStateClass, "rememberDatePickerState-EU0dCGE", RememberDatePickerStateSig);
+ }
+
+ JValue* args = stackalloc JValue[8];
+ args[0] = new JValue(IntPtr.Zero); // initialSelectedDateMillis
+ args[1] = new JValue(IntPtr.Zero); // initialDisplayedMonthMillis
+ args[2] = new JValue(IntPtr.Zero); // yearRange
+ args[3] = new JValue(0); // initialDisplayMode
+ args[4] = new JValue(IntPtr.Zero); // selectableDates
+ args[5] = new JValue(((Java.Lang.Object)composer).Handle);
+ args[6] = new JValue(0); // $changed
+ args[7] = new JValue(0b11111); // $default — all 5 user params defaulted
+ try
+ {
+ return JNIEnv.CallStaticObjectMethod(s_rememberDatePickerStateClass, s_rememberDatePickerStateMethod, args);
+ }
+ finally
+ {
+ GC.KeepAlive(composer);
+ }
+ }
+
+ // androidx.compose.material3.TimePickerKt.rememberTimePickerState(
+ // initialHour, initialMinute, is24Hour, composer, $changed, $default)
+ const string RememberTimePickerStateSig =
+ "(IIZLandroidx/compose/runtime/Composer;II)Landroidx/compose/material3/TimePickerState;";
+
+ static IntPtr s_rememberTimePickerStateClass;
+ static IntPtr s_rememberTimePickerStateMethod;
+
+ public static unsafe IntPtr RememberTimePickerState(int initialHour, int initialMinute, bool is24Hour, IComposer composer)
+ {
+ if (s_rememberTimePickerStateClass == IntPtr.Zero)
+ {
+ s_rememberTimePickerStateClass = JNIEnv.FindClass("androidx/compose/material3/TimePickerKt");
+ s_rememberTimePickerStateMethod = JNIEnv.GetStaticMethodID(s_rememberTimePickerStateClass, "rememberTimePickerState", RememberTimePickerStateSig);
+ }
+
+ JValue* args = stackalloc JValue[6];
+ args[0] = new JValue(initialHour);
+ args[1] = new JValue(initialMinute);
+ args[2] = new JValue(is24Hour);
+ args[3] = new JValue(((Java.Lang.Object)composer).Handle);
+ args[4] = new JValue(0); // $changed
+ args[5] = new JValue(0); // $default — all 3 provided
+ try
+ {
+ return JNIEnv.CallStaticObjectMethod(s_rememberTimePickerStateClass, s_rememberTimePickerStateMethod, args);
+ }
+ finally
+ {
+ GC.KeepAlive(composer);
+ }
+ }
+
+ // androidx.compose.material3.TooltipKt.rememberTooltipState(
+ // initialIsVisible, isPersistent, mutatorMutex, composer, $changed, $default)
+ const string RememberTooltipStateSig =
+ "(ZZLandroidx/compose/foundation/MutatorMutex;Landroidx/compose/runtime/Composer;II)Landroidx/compose/material3/TooltipState;";
+
+ static IntPtr s_rememberTooltipStateClass;
+ static IntPtr s_rememberTooltipStateMethod;
+
+ public static unsafe IntPtr RememberTooltipState(bool isPersistent, IComposer composer)
+ {
+ if (s_rememberTooltipStateClass == IntPtr.Zero)
+ {
+ s_rememberTooltipStateClass = JNIEnv.FindClass("androidx/compose/material3/TooltipKt");
+ s_rememberTooltipStateMethod = JNIEnv.GetStaticMethodID(s_rememberTooltipStateClass, "rememberTooltipState", RememberTooltipStateSig);
+ }
+
+ JValue* args = stackalloc JValue[6];
+ args[0] = new JValue(false); // initialIsVisible
+ args[1] = new JValue(isPersistent);
+ args[2] = new JValue(IntPtr.Zero); // mutatorMutex
+ args[3] = new JValue(((Java.Lang.Object)composer).Handle);
+ args[4] = new JValue(0); // $changed
+ args[5] = new JValue(0b101); // $default — bits 0 and 2 (initialIsVisible, mutatorMutex)
+ try
+ {
+ return JNIEnv.CallStaticObjectMethod(s_rememberTooltipStateClass, s_rememberTooltipStateMethod, args);
+ }
+ finally
+ {
+ GC.KeepAlive(composer);
+ }
+ }
+
+ // androidx.compose.material3.TooltipDefaults.INSTANCE.rememberPlainTooltipPositionProvider-kHDZbjc(
+ // spacingBetweenTooltipAndAnchor, composer, $changed, $default)
+ //
+ // Instance method on the Kotlin object singleton. We resolve INSTANCE
+ // once and reuse it across calls.
+ const string RememberPlainTooltipPositionProviderSig =
+ "(FLandroidx/compose/runtime/Composer;II)Landroidx/compose/ui/window/PopupPositionProvider;";
+
+ static IntPtr s_tooltipDefaultsInstance;
+ static IntPtr s_rememberPlainTooltipPositionProviderMethod;
+
+ public static unsafe IntPtr RememberPlainTooltipPositionProvider(IComposer composer)
+ {
+ if (s_tooltipDefaultsInstance == IntPtr.Zero)
+ {
+ IntPtr cls = JNIEnv.FindClass("androidx/compose/material3/TooltipDefaults");
+ IntPtr instanceFid = JNIEnv.GetStaticFieldID(cls, "INSTANCE", "Landroidx/compose/material3/TooltipDefaults;");
+ s_tooltipDefaultsInstance = JNIEnv.NewGlobalRef(JNIEnv.GetStaticObjectField(cls, instanceFid));
+ s_rememberPlainTooltipPositionProviderMethod = JNIEnv.GetMethodID(cls, "rememberPlainTooltipPositionProvider-kHDZbjc", RememberPlainTooltipPositionProviderSig);
+ }
+
+ JValue* args = stackalloc JValue[4];
+ args[0] = new JValue(0f); // spacing
+ args[1] = new JValue(((Java.Lang.Object)composer).Handle);
+ args[2] = new JValue(0);
+ args[3] = new JValue(1); // $default — spacing defaulted
+ try
+ {
+ return JNIEnv.CallObjectMethod(s_tooltipDefaultsInstance, s_rememberPlainTooltipPositionProviderMethod, args);
+ }
+ finally
+ {
+ GC.KeepAlive(composer);
+ }
+ }
+
static unsafe void InvokeTextField(IntPtr cls, IntPtr method, string value, IFunction1 onValueChange, IComposer composer, int defaults)
{
IntPtr valueRef = JNIEnv.NewString(value);
diff --git a/src/ComposeNet.Compose/ComposeDefaults.cs b/src/ComposeNet.Compose/ComposeDefaults.cs
index 8a396381..297babd3 100644
--- a/src/ComposeNet.Compose/ComposeDefaults.cs
+++ b/src/ComposeNet.Compose/ComposeDefaults.cs
@@ -74,6 +74,52 @@
"icon", "title", "text", "shape", "containerColor", "iconContentColor",
"titleContentColor", "textContentColor", "tonalElevation", "properties")]
+// androidx.compose.material3.ModalBottomSheet_androidKt.ModalBottomSheet-dYc4hso:
+// 13 user params; bits 0 (onDismissRequest), 2 (sheetState), 12 (content) always provided.
+[assembly: ComposeDefaults("ModalBottomSheetDefault",
+ "!onDismissRequest", "modifier", "!sheetState", "sheetMaxWidth", "shape",
+ "containerColor", "contentColor", "tonalElevation", "scrimColor", "dragHandle",
+ "windowInsets", "properties", "!content")]
+
+// androidx.compose.material3.BottomSheetScaffoldKt.BottomSheetScaffold-sdMYb0k:
+// 17 user params; bits 0 (sheetContent), 2 (scaffoldState), 16 (content) always provided.
+[assembly: ComposeDefaults("BottomSheetScaffoldDefault",
+ "!sheetContent", "modifier", "!scaffoldState", "sheetPeekHeight", "sheetMaxWidth",
+ "sheetShape", "sheetContainerColor", "sheetContentColor", "sheetTonalElevation",
+ "sheetShadowElevation", "sheetDragHandle", "sheetSwipeEnabled", "topBar",
+ "snackbarHost", "containerColor", "contentColor", "!content")]
+
+// androidx.compose.material3.DatePickerDialog_androidKt.DatePickerDialog-GmEhDVc:
+// 9 user params; bits 0 (onDismissRequest), 1 (confirmButton), 8 (content) always provided.
+[assembly: ComposeDefaults("DatePickerDialogDefault",
+ "!onDismissRequest", "!confirmButton", "modifier", "dismissButton", "shape",
+ "tonalElevation", "colors", "properties", "!content")]
+
+// androidx.compose.material3.DatePickerKt.DatePicker:
+// 8 user params; bit 0 (state) always provided.
+[assembly: ComposeDefaults("DatePickerDefault",
+ "!state", "modifier", "dateFormatter", "colors", "title",
+ "headline", "showModeToggle", "requestFocus")]
+
+// androidx.compose.material3.TimePickerKt.TimePicker-mT9BvqQ:
+// 4 user params; bit 0 (state) always provided.
+[assembly: ComposeDefaults("TimePickerDefault",
+ "!state", "modifier", "colors", "layoutType")]
+
+// androidx.compose.material3.TimePickerDialogKt.TimePickerDialog-FItCLgY:
+// 10 user params; bits 0 (onDismissRequest), 1 (confirmButton),
+// 2 (dismissButton), 9 (content) always provided.
+[assembly: ComposeDefaults("TimePickerDialogDefault",
+ "!onDismissRequest", "!confirmButton", "!dismissButton", "modifier", "properties",
+ "title", "modeToggleButton", "shape", "containerColor", "!content")]
+
+// androidx.compose.material3.TooltipKt.TooltipBox (7-param overload):
+// 7 user params; bits 0 (positionProvider), 1 (tooltip), 2 (state),
+// 6 (content) always provided.
+[assembly: ComposeDefaults("TooltipBoxDefault",
+ "!positionProvider", "!tooltip", "!state", "modifier", "focusable",
+ "enableUserInput", "!content")]
+
// androidx.compose.material3.TextFieldKt.TextField (String overload) AND
// OutlinedTextFieldKt.OutlinedTextField (String overload): 23 user params,
// bit 0 = value, bit 1 = onValueChange (both provided).
diff --git a/src/ComposeNet.Compose/PickerState.cs b/src/ComposeNet.Compose/PickerState.cs
new file mode 100644
index 00000000..f4b59c68
--- /dev/null
+++ b/src/ComposeNet.Compose/PickerState.cs
@@ -0,0 +1,130 @@
+using Android.Runtime;
+using AndroidX.Compose.Material3;
+
+namespace ComposeNet;
+
+// ---- DatePickerState ----
+
+///
+/// Caller-supplied state holder for /
+/// . The underlying JVM
+/// androidx.compose.material3.DatePickerState is created lazily
+/// the first time a bound to this state is
+/// rendered; reads/writes to before
+/// that point are no-ops/fallbacks.
+///
+///
+/// Typical usage — Remember a state instance, pass it to a
+/// , and read the picked value from the
+/// dialog's ConfirmButton.OnClick:
+///
+/// var pickerState = Remember(() => new DatePickerState());
+///
+/// new DatePickerDialog(onDismissRequest: ...)
+/// {
+/// ConfirmButton = new Button(onClick: () =>
+/// {
+/// var ms = pickerState.SelectedDateMillis;
+/// // ms is the picked date as Unix epoch milliseconds (UTC).
+/// })
+/// { new Text("OK") },
+/// Body = new DatePicker(pickerState),
+/// }
+///
+///
+public sealed class DatePickerState
+{
+ internal IDatePickerState? Jvm;
+
+ ///
+ /// The currently selected date as Unix epoch milliseconds (UTC), or
+ /// null if no date is selected. Mirrors Kotlin's
+ /// DatePickerState.selectedDateMillis: Long?. Returns
+ /// null until the first render binds
+ /// this state to the JVM picker.
+ ///
+ public long? SelectedDateMillis
+ {
+ get => Jvm?.SelectedDateMillis?.LongValue();
+ set
+ {
+ if (Jvm is not null)
+ Jvm.SelectedDateMillis = value is long ms ? Java.Lang.Long.ValueOf(ms) : null;
+ }
+ }
+
+ ///
+ /// First-of-month milliseconds for the month currently shown by the
+ /// picker. Mirrors Kotlin's DatePickerState.displayedMonthMillis.
+ /// Returns 0 until the state is bound.
+ ///
+ public long DisplayedMonthMillis
+ {
+ get => Jvm?.DisplayedMonthMillis ?? 0L;
+ set { if (Jvm is not null) Jvm.DisplayedMonthMillis = value; }
+ }
+}
+
+// ---- TimePickerState ----
+
+///
+/// Caller-supplied state holder for /
+/// . Mirrors Kotlin's
+/// TimePickerState: /
+/// expose the live values; reflects the format.
+///
+///
+/// Typical usage — Remember a state instance with the desired
+/// initial values, pass it to a , and read the
+/// picked time from the dialog's ConfirmButton.OnClick:
+///
+/// var pickerState = Remember(() => new TimePickerState(initialHour: 9, initialMinute: 30));
+///
+/// new TimePickerDialog(onDismissRequest: ...)
+/// {
+/// ConfirmButton = new Button(onClick: () =>
+/// {
+/// var h = pickerState.Hour;
+/// var m = pickerState.Minute;
+/// })
+/// { new Text("OK") },
+/// Body = new TimePicker(pickerState),
+/// }
+///
+///
+public sealed class TimePickerState
+{
+ internal ITimePickerState? Jvm;
+
+ internal int InitialHour { get; }
+ internal int InitialMinute { get; }
+ internal bool InitialIs24Hour { get; }
+
+ public TimePickerState(int initialHour = 12, int initialMinute = 0, bool is24Hour = true)
+ {
+ InitialHour = initialHour;
+ InitialMinute = initialMinute;
+ InitialIs24Hour = is24Hour;
+ }
+
+ /// Currently displayed hour (0–23). Falls back to the
+ /// constructor's initialHour until bound.
+ public int Hour
+ {
+ get => Jvm?.Hour ?? InitialHour;
+ set { if (Jvm is not null) Jvm.Hour = value; }
+ }
+
+ /// Currently displayed minute (0–59). Falls back to the
+ /// constructor's initialMinute until bound.
+ public int Minute
+ {
+ get => Jvm?.Minute ?? InitialMinute;
+ set { if (Jvm is not null) Jvm.Minute = value; }
+ }
+
+ /// Whether the picker is in 24-hour mode (vs. 12-hour with
+ /// AM/PM). Falls back to the constructor's is24Hour until
+ /// bound.
+ public bool Is24Hour => Jvm?.Is24hour() ?? InitialIs24Hour;
+}
diff --git a/src/ComposeNet.Sample/MainActivity.cs b/src/ComposeNet.Sample/MainActivity.cs
index c226b22b..10bd4a52 100644
--- a/src/ComposeNet.Sample/MainActivity.cs
+++ b/src/ComposeNet.Sample/MainActivity.cs
@@ -1,4 +1,5 @@
using Android.OS;
+using AndroidX.Compose.Material3;
using ComposeNet;
namespace ComposeNet.Sample;
@@ -11,81 +12,165 @@ protected override void OnCreate(Bundle? savedInstanceState)
base.OnCreate(savedInstanceState);
SetContent(() =>
{
- var count = Remember(() => new MutableNumberState(0));
- var name = Remember(() => new MutableState(""));
- var showDlg = Remember(() => new MutableState(false));
- var liked = Remember(() => new MutableState(false));
- var tab = Remember(() => new MutableNumberState(0));
+ var count = Remember(() => new MutableNumberState(0));
+ var name = Remember(() => new MutableState(""));
+ var liked = Remember(() => new MutableState(false));
+ var tab = Remember(() => new MutableNumberState(0));
+ var showAlert = Remember(() => new MutableState(false));
+ var showSheet = Remember(() => new MutableState(false));
+ var showDate = Remember(() => new MutableState(false));
+ var showTime = Remember(() => new MutableState(false));
+ var pickedDate = Remember(() => new MutableState("(none)"));
+ var pickedTime = Remember(() => new MutableState("(none)"));
+ var dateState = Remember(() => new DatePickerState());
+ var timeState = Remember(() => new TimePickerState(initialHour: 9, initialMinute: 30));
+
+ // Per-tab content. Only the current tab's column is added to
+ // the screen — keeps the sample short enough to fit on one
+ // phone-sized scroll area.
+ ComposableNode tabContent = tab.Value switch
+ {
+ 0 => new Column
+ {
+ new Text("Hello from .NET"),
+ new Text($"Count: {count}"),
+ new Button(onClick: () => count++) { new Text("Tap to increment") },
+ new IconButton(onClick: () => count--) { new Text("−") },
+ new OutlinedTextField(name),
+ new Text($"Hi {(string.IsNullOrEmpty(name.Value) ? "stranger" : name.Value)}"),
+ new Card
+ {
+ new Text("Inside a Card"),
+ new Text($"Counter snapshot: {count}"),
+ },
+ },
+ 1 => new Column
+ {
+ new Text("Chips, FAB, tooltip"),
+ new AssistChip(onClick: () => count++)
+ {
+ Label = new Text("Assist (+1)"),
+ },
+ new FilterChip(selected: liked.Value, onClick: () => liked.Value = !liked.Value)
+ {
+ Label = new Text(liked.Value ? "Liked" : "Like"),
+ },
+ new SuggestionChip(onClick: () => count.Value = 0)
+ {
+ Label = new Text("Reset"),
+ },
+ new Tooltip
+ {
+ Tip = new Surface { new Text("Helpful hint") },
+ Anchor = new Button(onClick: () => count++) { new Text("Long-press me") },
+ },
+ new FloatingActionButton(onClick: () => showAlert.Value = true)
+ {
+ new Text("✕"),
+ },
+ },
+ _ => new Column
+ {
+ new Text("Dialogs and sheets"),
+ new Button(onClick: () => showSheet.Value = true) { new Text("Modal bottom sheet") },
+ new Button(onClick: () => showDate.Value = true) { new Text("Date picker dialog") },
+ new Button(onClick: () => showTime.Value = true) { new Text("Time picker dialog") },
+ new Text($"Picked date: {pickedDate}"),
+ new Text($"Picked time: {pickedTime}"),
+ },
+ };
+
return new MaterialTheme
{
new Surface
{
new Column
{
- new Text("Hello from .NET"),
- new Text($"Count: {count}"),
- new Button(onClick: () => count++)
- {
- new Text("Tap to increment"),
- },
- new IconButton(onClick: () => count--)
- {
- new Text("−"),
- },
- new OutlinedTextField(name),
- new Text($"Hi {(string.IsNullOrEmpty(name.Value) ? "stranger" : name.Value)}"),
- new Card
- {
- new Text("Inside a Card"),
- new Text($"Counter snapshot: {count}"),
- },
- new AssistChip(onClick: () => count++)
- {
- Label = new Text("Assist (+1)"),
- },
- new FilterChip(selected: liked.Value, onClick: () => liked.Value = !liked.Value)
- {
- Label = new Text(liked.Value ? "Liked" : "Like"),
- },
- new SuggestionChip(onClick: () => count.Value = 0)
- {
- Label = new Text("Reset"),
- },
+ tabContent,
+
+ // Bottom navigation switches between the three tabs above.
new NavigationBar
{
new NavigationBarItem(selected: tab.Value == 0, onClick: () => tab.Value = 0)
{
- Icon = new Text("🏠"),
- Label = new Text("Home"),
+ Icon = new Text("🔢"),
+ Label = new Text("Basics"),
},
new NavigationBarItem(selected: tab.Value == 1, onClick: () => tab.Value = 1)
{
- Icon = new Text("⚙"),
- Label = new Text("Settings"),
+ Icon = new Text("👍"),
+ Label = new Text("Buttons"),
+ },
+ new NavigationBarItem(selected: tab.Value == 2, onClick: () => tab.Value = 2)
+ {
+ Icon = new Text("📅"),
+ Label = new Text("Pickers"),
},
},
- new FloatingActionButton(onClick: () => showDlg.Value = true)
- {
- new Text("✕"),
- },
- showDlg.Value
- ? new AlertDialog(onDismissRequest: () => showDlg.Value = false)
+
+ // --- Overlays: rendered at the root regardless of tab,
+ // so a dialog opened on Tab 1 still works after switching. ---
+ showAlert.Value
+ ? new AlertDialog(onDismissRequest: () => showAlert.Value = false)
{
Title = new Text("Reset counter?"),
Text = new Text("This will set the counter back to zero."),
- ConfirmButton = new Button(onClick: () => { count.Value = 0; showDlg.Value = false; })
+ ConfirmButton = new Button(onClick: () => { count.Value = 0; showAlert.Value = false; })
{
new Text("Reset"),
},
- DismissButton = new Button(onClick: () => showDlg.Value = false)
+ DismissButton = new Button(onClick: () => showAlert.Value = false)
{
new Text("Cancel"),
},
}
: null,
+
+ // ModalBottomSheet — SheetState comes from the bound C#
+ // RememberModalBottomSheetState (NOT stripped, no JNI).
+ showSheet.Value
+ ? 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") },
+ },
+ }
+ : null,
+
+ showDate.Value
+ ? new DatePickerDialog(onDismissRequest: () => showDate.Value = false)
+ {
+ ConfirmButton = new Button(onClick: () =>
+ {
+ pickedDate.Value = dateState.SelectedDateMillis is long ms ? ms.ToString() : "(none)";
+ showDate.Value = false;
+ })
+ { new Text("OK") },
+ DismissButton = new Button(onClick: () => showDate.Value = false) { new Text("Cancel") },
+ Body = new DatePicker(dateState),
+ }
+ : null,
+
+ showTime.Value
+ ? new TimePickerDialog(onDismissRequest: () => showTime.Value = false)
+ {
+ Title = new Text("Pick a time"),
+ ConfirmButton = new Button(onClick: () =>
+ {
+ pickedTime.Value = $"{timeState.Hour:D2}:{timeState.Minute:D2}";
+ showTime.Value = false;
+ })
+ { new Text("OK") },
+ DismissButton = new Button(onClick: () => showTime.Value = false) { new Text("Cancel") },
+ Body = new TimePicker(timeState),
+ }
+ : null,
},
},
};
});
}
-}
+}
\ No newline at end of file