diff --git a/src/ComposeNet.Compose/ComposableLambdas.cs b/src/ComposeNet.Compose/ComposableLambdas.cs index 77b45c61..50643d5b 100644 --- a/src/ComposeNet.Compose/ComposableLambdas.cs +++ b/src/ComposeNet.Compose/ComposableLambdas.cs @@ -58,20 +58,59 @@ internal sealed class ComposableLambda2 : Java.Lang.Object, IFunction2 // Function3 — Column/Row/Box/Button content. // p0 = scope (RowScope/ColumnScope), p1 = composer, p2 = $changed. -// The scope receiver is ignored here; users can't access it from the -// tree-style API anyway (modifiers like .weight live on Modifier -// extensions, which are a Tier 2 problem). +// +// Two ctors: +// * Action<IComposer> (the original): scope is discarded — +// used everywhere children don't need to know it (Column, Box, Button). +// * Action<IntPtr, IComposer>: receives the raw scope handle, +// used by container composables whose children are extension-receiver +// composables (RowScope.NavigationBarItem). The scope is +// published via so the child Render +// can read it. [Register("composenet/compose/ComposableLambda3")] internal sealed class ComposableLambda3 : Java.Lang.Object, IFunction3 { - readonly System.Action _body; - public ComposableLambda3(System.Action body) => _body = body; + readonly System.Action _body; + + public ComposableLambda3(System.Action body) + : this((_, c) => body(c)) { } + + public ComposableLambda3(System.Action body) => _body = body; public Java.Lang.Object? Invoke(Java.Lang.Object? p0, Java.Lang.Object? p1, Java.Lang.Object? p2) { System.ArgumentNullException.ThrowIfNull(p1); var composer = Android.Runtime.Extensions.JavaCast(p1); - _body(composer); + _body(p0?.Handle ?? IntPtr.Zero, composer); return null; } } + +// Thread-static stash of the current Compose receiver scope (RowScope / +// ColumnScope) handle. Set by container composables that consume a +// scope-receiver Function3 content lambda; read by *Item composables +// whose underlying Kotlin static method takes the scope as its first +// argument. Composition runs synchronously on a single thread, so a +// [ThreadStatic] is sufficient — and a struct-disposable scope guard +// keeps push/pop balanced even when child Render throws. +internal static class RenderContext +{ + [System.ThreadStatic] + static IntPtr s_scope; + + public static IntPtr CurrentScope => s_scope; + + public static ScopeFrame PushScope(IntPtr scope) + { + var prev = s_scope; + s_scope = scope; + return new ScopeFrame(prev); + } + + internal readonly struct ScopeFrame : System.IDisposable + { + readonly IntPtr _previous; + public ScopeFrame(IntPtr previous) => _previous = previous; + public void Dispose() => s_scope = _previous; + } +} diff --git a/src/ComposeNet.Compose/Composables.cs b/src/ComposeNet.Compose/Composables.cs index 57191987..ee7eca5b 100644 --- a/src/ComposeNet.Compose/Composables.cs +++ b/src/ComposeNet.Compose/Composables.cs @@ -283,3 +283,329 @@ internal override void Render(IComposer composer) composer: composer); } } + +// ---- Card ---- + +/// +/// Material 3 non-clickable Card — a tonal surface with rounded +/// corners that lays its children out as a Column. Children are added +/// via collection-initializer syntax: +/// +/// new Card { new Text("Title"), new Text("Subtitle") } +/// +/// +public sealed class Card : ComposableContainer +{ + internal override void Render(IComposer composer) + { + var content = new ComposableLambda3(c => RenderChildren(c)); + ComposeBridges.Card(content, composer); + } +} + +// ---- Chip family ---- + +/// +/// Material 3 AssistChip. is required; +/// and are optional +/// slots: +/// +/// new AssistChip(onClick: ...) { Label = new Text("Filter") } +/// +/// +public sealed class AssistChip : ComposableNode +{ + readonly System.Action _onClick; + public AssistChip(System.Action onClick) => _onClick = onClick; + + /// Required: chip text. + public ComposableNode? Label { get; set; } + + /// Optional: leading slot (e.g. icon). + public ComposableNode? LeadingIcon { get; set; } + + /// Optional: trailing slot. + public ComposableNode? TrailingIcon { get; set; } + + internal override void Render(IComposer composer) + { + if (Label is null) + throw new System.InvalidOperationException( + "AssistChip.Label is required (the Kotlin parameter has no default)."); + + var click = new ComposableLambda0(_onClick); + var label = new ComposableLambda2(c => Label.Render(c)); + ComposableLambda2? leading = LeadingIcon is null ? null : new ComposableLambda2(c => LeadingIcon.Render(c)); + ComposableLambda2? trailing = TrailingIcon is null ? null : new ComposableLambda2(c => TrailingIcon.Render(c)); + + int defaults = (int)AssistChipDefault.All; + if (leading is not null) defaults &= ~(int)AssistChipDefault.LeadingIcon; + if (trailing is not null) defaults &= ~(int)AssistChipDefault.TrailingIcon; + + ComposeBridges.AssistChip(click, label, leading, trailing, defaults, composer); + } +} + +/// +/// Material 3 FilterChip. Renders as either selected or unselected; +/// the onClick handler typically toggles the bound boolean state. +/// +public sealed class FilterChip : ComposableNode +{ + readonly bool _selected; + readonly System.Action _onClick; + + public FilterChip(bool selected, System.Action onClick) + { + _selected = selected; + _onClick = onClick; + } + + /// Required: chip text. + public ComposableNode? Label { get; set; } + + /// Optional: leading slot (typically the check / unselected icon). + public ComposableNode? LeadingIcon { get; set; } + + /// Optional: trailing slot. + public ComposableNode? TrailingIcon { get; set; } + + internal override void Render(IComposer composer) + { + if (Label is null) + throw new System.InvalidOperationException( + "FilterChip.Label is required (the Kotlin parameter has no default)."); + + var click = new ComposableLambda0(_onClick); + var label = new ComposableLambda2(c => Label.Render(c)); + ComposableLambda2? leading = LeadingIcon is null ? null : new ComposableLambda2(c => LeadingIcon.Render(c)); + ComposableLambda2? trailing = TrailingIcon is null ? null : new ComposableLambda2(c => TrailingIcon.Render(c)); + + int defaults = (int)FilterChipDefault.All; + if (leading is not null) defaults &= ~(int)FilterChipDefault.LeadingIcon; + if (trailing is not null) defaults &= ~(int)FilterChipDefault.TrailingIcon; + + ComposeBridges.FilterChip(_selected, click, label, leading, trailing, defaults, composer); + } +} + +/// +/// Material 3 InputChip. Adds an slot in +/// addition to the leading/trailing slots common to the chip family. +/// +public sealed class InputChip : ComposableNode +{ + readonly bool _selected; + readonly System.Action _onClick; + + public InputChip(bool selected, System.Action onClick) + { + _selected = selected; + _onClick = onClick; + } + + /// Required: chip text. + public ComposableNode? Label { get; set; } + + public ComposableNode? LeadingIcon { get; set; } + public ComposableNode? Avatar { get; set; } + public ComposableNode? TrailingIcon { get; set; } + + internal override void Render(IComposer composer) + { + if (Label is null) + throw new System.InvalidOperationException( + "InputChip.Label is required (the Kotlin parameter has no default)."); + + var click = new ComposableLambda0(_onClick); + var label = new ComposableLambda2(c => Label.Render(c)); + ComposableLambda2? leading = LeadingIcon is null ? null : new ComposableLambda2(c => LeadingIcon.Render(c)); + ComposableLambda2? avatar = Avatar is null ? null : new ComposableLambda2(c => Avatar.Render(c)); + ComposableLambda2? trailing = TrailingIcon is null ? null : new ComposableLambda2(c => TrailingIcon.Render(c)); + + int defaults = (int)InputChipDefault.All; + if (leading is not null) defaults &= ~(int)InputChipDefault.LeadingIcon; + if (avatar is not null) defaults &= ~(int)InputChipDefault.Avatar; + if (trailing is not null) defaults &= ~(int)InputChipDefault.TrailingIcon; + + ComposeBridges.InputChip(_selected, click, label, leading, avatar, trailing, defaults, composer); + } +} + +/// +/// Material 3 SuggestionChip. Single-icon variant — only an +/// slot is exposed (vs. AssistChip's leading + trailing). +/// +public sealed class SuggestionChip : ComposableNode +{ + readonly System.Action _onClick; + public SuggestionChip(System.Action onClick) => _onClick = onClick; + + /// Required: chip text. + public ComposableNode? Label { get; set; } + + /// Optional: leading slot. + public ComposableNode? Icon { get; set; } + + internal override void Render(IComposer composer) + { + if (Label is null) + throw new System.InvalidOperationException( + "SuggestionChip.Label is required (the Kotlin parameter has no default)."); + + var click = new ComposableLambda0(_onClick); + var label = new ComposableLambda2(c => Label.Render(c)); + ComposableLambda2? icon = Icon is null ? null : new ComposableLambda2(c => Icon.Render(c)); + + int defaults = (int)SuggestionChipDefault.All; + if (icon is not null) defaults &= ~(int)SuggestionChipDefault.Icon; + + ComposeBridges.SuggestionChip(click, label, icon, defaults, composer); + } +} + +// ---- NavigationBar / NavigationBarItem ---- + +/// +/// Material 3 NavigationBar. Container for +/// children laid out horizontally: +/// +/// new NavigationBar +/// { +/// new NavigationBarItem(selected: tab == 0, onClick: () => tab.Value = 0) +/// { +/// Icon = new Text("🏠"), Label = new Text("Home"), +/// }, +/// new NavigationBarItem(selected: tab == 1, onClick: () => tab.Value = 1) +/// { +/// Icon = new Text("⚙"), Label = new Text("Settings"), +/// }, +/// } +/// +/// +public sealed class NavigationBar : ComposableContainer +{ + internal override void Render(IComposer composer) + { + // Capture the RowScope receiver (p0 of the Function3) and publish + // it so child NavigationBarItems can pass it to their underlying + // RowScope-extension static. + var content = new ComposableLambda3((scope, c) => + { + using var _ = RenderContext.PushScope(scope); + RenderChildren(c); + }); + ComposeBridges.NavigationBar(content, composer); + } +} + +/// +/// Material 3 NavigationBarItem. Must be a child of +/// — the Kotlin static method takes a +/// RowScope extension receiver, which the parent +/// publishes via RenderContext. +/// is required; is optional. +/// +public sealed class NavigationBarItem : ComposableNode +{ + readonly bool _selected; + readonly System.Action _onClick; + + public NavigationBarItem(bool selected, System.Action onClick) + { + _selected = selected; + _onClick = onClick; + } + + /// Required: item icon. + public ComposableNode? Icon { get; set; } + + /// Optional: item label. + public ComposableNode? Label { get; set; } + + internal override void Render(IComposer composer) + { + if (Icon is null) + throw new System.InvalidOperationException( + "NavigationBarItem.Icon is required (the Kotlin parameter has no default)."); + + var click = new ComposableLambda0(_onClick); + var icon = new ComposableLambda2(c => Icon.Render(c)); + ComposableLambda2? label = Label is null ? null : new ComposableLambda2(c => Label.Render(c)); + + int defaults = (int)NavigationBarItemDefault.All; + if (label is not null) defaults &= ~(int)NavigationBarItemDefault.Label; + + ComposeBridges.NavigationBarItem( + rowScope: RenderContext.CurrentScope, + selected: _selected, + onClick: click, + icon: icon, + label: label, + defaults: defaults, + composer: composer); + } +} + +// ---- NavigationRail / NavigationRailItem ---- + +/// +/// Material 3 NavigationRail. Vertical analog of +/// . Children are s: +/// +/// new NavigationRail +/// { +/// new NavigationRailItem(selected: tab == 0, onClick: ...) { Icon = ..., Label = ... }, +/// new NavigationRailItem(selected: tab == 1, onClick: ...) { Icon = ..., Label = ... }, +/// } +/// +/// +public sealed class NavigationRail : ComposableContainer +{ + internal override void Render(IComposer composer) + { + // NavigationRailItem (unlike NavigationBarItem) is a top-level + // static, not a ColumnScope extension — so we don't need to + // publish the scope. Children can render directly. + var content = new ComposableLambda3(c => RenderChildren(c)); + ComposeBridges.NavigationRail(content, composer); + } +} + +/// +/// Material 3 NavigationRailItem. Used inside +/// . +/// +public sealed class NavigationRailItem : ComposableNode +{ + readonly bool _selected; + readonly System.Action _onClick; + + public NavigationRailItem(bool selected, System.Action onClick) + { + _selected = selected; + _onClick = onClick; + } + + /// Required: item icon. + public ComposableNode? Icon { get; set; } + + /// Optional: item label. + public ComposableNode? Label { get; set; } + + internal override void Render(IComposer composer) + { + if (Icon is null) + throw new System.InvalidOperationException( + "NavigationRailItem.Icon is required (the Kotlin parameter has no default)."); + + var click = new ComposableLambda0(_onClick); + var icon = new ComposableLambda2(c => Icon.Render(c)); + ComposableLambda2? label = Label is null ? null : new ComposableLambda2(c => Label.Render(c)); + + int defaults = (int)NavigationRailItemDefault.All; + if (label is not null) defaults &= ~(int)NavigationRailItemDefault.Label; + + ComposeBridges.NavigationRailItem(_selected, click, icon, label, defaults, composer); + } +} diff --git a/src/ComposeNet.Compose/ComposeBridges.cs b/src/ComposeNet.Compose/ComposeBridges.cs index fe6ae626..70713235 100644 --- a/src/ComposeNet.Compose/ComposeBridges.cs +++ b/src/ComposeNet.Compose/ComposeBridges.cs @@ -365,4 +365,414 @@ static unsafe void InvokeTextField(IntPtr cls, IntPtr method, string value, IFun JNIEnv.DeleteLocalRef(valueRef); } } + + // androidx.compose.material3.CardKt.Card (non-clickable): + // (modifier, shape, colors, elevation, border, content, + // composer, $changed, $default) + // 6 user params, only bit 5 (content) provided. + const string CardSig = + "(Landroidx/compose/ui/Modifier;Landroidx/compose/ui/graphics/Shape;" + + "Landroidx/compose/material3/CardColors;Landroidx/compose/material3/CardElevation;" + + "Landroidx/compose/foundation/BorderStroke;" + + "Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V"; + + static IntPtr s_cardClass; + static IntPtr s_cardMethod; + + public static unsafe void Card(IFunction3 content, IComposer composer) + { + if (s_cardClass == IntPtr.Zero) + { + s_cardClass = JNIEnv.FindClass("androidx/compose/material3/CardKt"); + s_cardMethod = JNIEnv.GetStaticMethodID(s_cardClass, "Card", CardSig); + } + + JValue* args = stackalloc JValue[9]; + args[0] = new JValue(IntPtr.Zero); // modifier + args[1] = new JValue(IntPtr.Zero); // shape + args[2] = new JValue(IntPtr.Zero); // colors + args[3] = new JValue(IntPtr.Zero); // elevation + args[4] = new JValue(IntPtr.Zero); // border + args[5] = new JValue(((Java.Lang.Object)content).Handle); + args[6] = new JValue(((Java.Lang.Object)composer).Handle); + args[7] = new JValue(0); + args[8] = new JValue((int)CardDefault.All); + JNIEnv.CallStaticVoidMethod(s_cardClass, s_cardMethod, args); + } + + // androidx.compose.material3.ChipKt.AssistChip: + // (onClick, label, modifier, enabled, leadingIcon, trailingIcon, + // shape, colors, elevation, border, interactionSource, + // composer, $changed, $changed1, $default) + // 11 user params; bit 0 (onClick), bit 1 (label) always provided. + // bits 4 (leadingIcon) + 5 (trailingIcon) toggled per-call. + const string AssistChipSig = + "(Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;" + + "Landroidx/compose/ui/Modifier;Z" + + "Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;" + + "Landroidx/compose/ui/graphics/Shape;" + + "Landroidx/compose/material3/ChipColors;Landroidx/compose/material3/ChipElevation;" + + "Landroidx/compose/foundation/BorderStroke;" + + "Landroidx/compose/foundation/interaction/MutableInteractionSource;" + + "Landroidx/compose/runtime/Composer;III)V"; + + static IntPtr s_assistChipClass; + static IntPtr s_assistChipMethod; + + public static unsafe void AssistChip( + IFunction0 onClick, + IFunction2 label, + IFunction2? leadingIcon, + IFunction2? trailingIcon, + int defaults, + IComposer composer) + { + if (s_assistChipClass == IntPtr.Zero) + { + s_assistChipClass = JNIEnv.FindClass("androidx/compose/material3/ChipKt"); + s_assistChipMethod = JNIEnv.GetStaticMethodID(s_assistChipClass, "AssistChip", AssistChipSig); + } + + JValue* args = stackalloc JValue[15]; + args[0] = new JValue(((Java.Lang.Object)onClick).Handle); + args[1] = new JValue(((Java.Lang.Object)label).Handle); + args[2] = new JValue(IntPtr.Zero); // modifier + args[3] = new JValue(true); // enabled + args[4] = new JValue(leadingIcon is null ? IntPtr.Zero : ((Java.Lang.Object)leadingIcon).Handle); + args[5] = new JValue(trailingIcon is null ? IntPtr.Zero : ((Java.Lang.Object)trailingIcon).Handle); + args[6] = new JValue(IntPtr.Zero); // shape + args[7] = new JValue(IntPtr.Zero); // colors + args[8] = new JValue(IntPtr.Zero); // elevation + args[9] = new JValue(IntPtr.Zero); // border + args[10] = new JValue(IntPtr.Zero); // interactionSource + args[11] = new JValue(((Java.Lang.Object)composer).Handle); + args[12] = new JValue(0); // $changed + args[13] = new JValue(0); // $changed1 + args[14] = new JValue(defaults); // $default + JNIEnv.CallStaticVoidMethod(s_assistChipClass, s_assistChipMethod, args); + } + + // androidx.compose.material3.ChipKt.FilterChip: + // (selected, onClick, label, modifier, enabled, leadingIcon, trailingIcon, + // shape, colors, elevation, border, interactionSource, + // composer, $changed, $changed1, $default) + // 12 user params; bits 0 (selected), 1 (onClick), 2 (label) always provided. + const string FilterChipSig = + "(ZLkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;" + + "Landroidx/compose/ui/Modifier;Z" + + "Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;" + + "Landroidx/compose/ui/graphics/Shape;" + + "Landroidx/compose/material3/SelectableChipColors;" + + "Landroidx/compose/material3/SelectableChipElevation;" + + "Landroidx/compose/foundation/BorderStroke;" + + "Landroidx/compose/foundation/interaction/MutableInteractionSource;" + + "Landroidx/compose/runtime/Composer;III)V"; + + static IntPtr s_filterChipClass; + static IntPtr s_filterChipMethod; + + public static unsafe void FilterChip( + bool selected, + IFunction0 onClick, + IFunction2 label, + IFunction2? leadingIcon, + IFunction2? trailingIcon, + int defaults, + IComposer composer) + { + if (s_filterChipClass == IntPtr.Zero) + { + s_filterChipClass = JNIEnv.FindClass("androidx/compose/material3/ChipKt"); + s_filterChipMethod = JNIEnv.GetStaticMethodID(s_filterChipClass, "FilterChip", FilterChipSig); + } + + JValue* args = stackalloc JValue[16]; + args[0] = new JValue(selected); + args[1] = new JValue(((Java.Lang.Object)onClick).Handle); + args[2] = new JValue(((Java.Lang.Object)label).Handle); + args[3] = new JValue(IntPtr.Zero); // modifier + args[4] = new JValue(true); // enabled + args[5] = new JValue(leadingIcon is null ? IntPtr.Zero : ((Java.Lang.Object)leadingIcon).Handle); + args[6] = new JValue(trailingIcon is null ? IntPtr.Zero : ((Java.Lang.Object)trailingIcon).Handle); + args[7] = new JValue(IntPtr.Zero); // shape + args[8] = new JValue(IntPtr.Zero); // colors + args[9] = new JValue(IntPtr.Zero); // elevation + args[10] = new JValue(IntPtr.Zero); // border + args[11] = new JValue(IntPtr.Zero); // interactionSource + args[12] = new JValue(((Java.Lang.Object)composer).Handle); + args[13] = new JValue(0); + args[14] = new JValue(0); + args[15] = new JValue(defaults); + JNIEnv.CallStaticVoidMethod(s_filterChipClass, s_filterChipMethod, args); + } + + // androidx.compose.material3.ChipKt.InputChip: + // (selected, onClick, label, modifier, enabled, leadingIcon, avatar, + // trailingIcon, shape, colors, elevation, border, interactionSource, + // composer, $changed, $changed1, $default) + // 13 user params; bits 0/1/2 always provided. + const string InputChipSig = + "(ZLkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;" + + "Landroidx/compose/ui/Modifier;Z" + + "Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;" + + "Lkotlin/jvm/functions/Function2;Landroidx/compose/ui/graphics/Shape;" + + "Landroidx/compose/material3/SelectableChipColors;" + + "Landroidx/compose/material3/SelectableChipElevation;" + + "Landroidx/compose/foundation/BorderStroke;" + + "Landroidx/compose/foundation/interaction/MutableInteractionSource;" + + "Landroidx/compose/runtime/Composer;III)V"; + + static IntPtr s_inputChipClass; + static IntPtr s_inputChipMethod; + + public static unsafe void InputChip( + bool selected, + IFunction0 onClick, + IFunction2 label, + IFunction2? leadingIcon, + IFunction2? avatar, + IFunction2? trailingIcon, + int defaults, + IComposer composer) + { + if (s_inputChipClass == IntPtr.Zero) + { + s_inputChipClass = JNIEnv.FindClass("androidx/compose/material3/ChipKt"); + s_inputChipMethod = JNIEnv.GetStaticMethodID(s_inputChipClass, "InputChip", InputChipSig); + } + + JValue* args = stackalloc JValue[17]; + args[0] = new JValue(selected); + args[1] = new JValue(((Java.Lang.Object)onClick).Handle); + args[2] = new JValue(((Java.Lang.Object)label).Handle); + args[3] = new JValue(IntPtr.Zero); // modifier + args[4] = new JValue(true); // enabled + args[5] = new JValue(leadingIcon is null ? IntPtr.Zero : ((Java.Lang.Object)leadingIcon).Handle); + args[6] = new JValue(avatar is null ? IntPtr.Zero : ((Java.Lang.Object)avatar).Handle); + args[7] = new JValue(trailingIcon is null ? IntPtr.Zero : ((Java.Lang.Object)trailingIcon).Handle); + args[8] = new JValue(IntPtr.Zero); // shape + args[9] = new JValue(IntPtr.Zero); // colors + args[10] = new JValue(IntPtr.Zero); // elevation + args[11] = new JValue(IntPtr.Zero); // border + args[12] = new JValue(IntPtr.Zero); // interactionSource + args[13] = new JValue(((Java.Lang.Object)composer).Handle); + args[14] = new JValue(0); + args[15] = new JValue(0); + args[16] = new JValue(defaults); + JNIEnv.CallStaticVoidMethod(s_inputChipClass, s_inputChipMethod, args); + } + + // androidx.compose.material3.ChipKt.SuggestionChip: + // (onClick, label, modifier, enabled, icon, shape, colors, elevation, + // border, interactionSource, composer, $changed, $default) + // 10 user params; bits 0 (onClick), 1 (label) always provided. + const string SuggestionChipSig = + "(Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;" + + "Landroidx/compose/ui/Modifier;Z" + + "Lkotlin/jvm/functions/Function2;Landroidx/compose/ui/graphics/Shape;" + + "Landroidx/compose/material3/ChipColors;Landroidx/compose/material3/ChipElevation;" + + "Landroidx/compose/foundation/BorderStroke;" + + "Landroidx/compose/foundation/interaction/MutableInteractionSource;" + + "Landroidx/compose/runtime/Composer;II)V"; + + static IntPtr s_suggestionChipClass; + static IntPtr s_suggestionChipMethod; + + public static unsafe void SuggestionChip( + IFunction0 onClick, + IFunction2 label, + IFunction2? icon, + int defaults, + IComposer composer) + { + if (s_suggestionChipClass == IntPtr.Zero) + { + s_suggestionChipClass = JNIEnv.FindClass("androidx/compose/material3/ChipKt"); + s_suggestionChipMethod = JNIEnv.GetStaticMethodID(s_suggestionChipClass, "SuggestionChip", SuggestionChipSig); + } + + JValue* args = stackalloc JValue[13]; + args[0] = new JValue(((Java.Lang.Object)onClick).Handle); + args[1] = new JValue(((Java.Lang.Object)label).Handle); + args[2] = new JValue(IntPtr.Zero); // modifier + args[3] = new JValue(true); // enabled + args[4] = new JValue(icon is null ? IntPtr.Zero : ((Java.Lang.Object)icon).Handle); + args[5] = new JValue(IntPtr.Zero); // shape + args[6] = new JValue(IntPtr.Zero); // colors + args[7] = new JValue(IntPtr.Zero); // elevation + args[8] = new JValue(IntPtr.Zero); // border + args[9] = new JValue(IntPtr.Zero); // interactionSource + args[10] = new JValue(((Java.Lang.Object)composer).Handle); + args[11] = new JValue(0); + args[12] = new JValue(defaults); + JNIEnv.CallStaticVoidMethod(s_suggestionChipClass, s_suggestionChipMethod, args); + } + + // androidx.compose.material3.NavigationBarKt.NavigationBar-HsRjFd4: + // (modifier, containerColor, contentColor, tonalElevation, windowInsets, + // content, composer, $changed, $default) + // 6 user params; only bit 5 (content) provided. + const string NavigationBarSig = + "(Landroidx/compose/ui/Modifier;JJF" + + "Landroidx/compose/foundation/layout/WindowInsets;" + + "Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V"; + + static IntPtr s_navBarClass; + static IntPtr s_navBarMethod; + + public static unsafe void NavigationBar(IFunction3 content, IComposer composer) + { + if (s_navBarClass == IntPtr.Zero) + { + s_navBarClass = JNIEnv.FindClass("androidx/compose/material3/NavigationBarKt"); + s_navBarMethod = JNIEnv.GetStaticMethodID(s_navBarClass, "NavigationBar-HsRjFd4", NavigationBarSig); + } + + JValue* args = stackalloc JValue[9]; + args[0] = new JValue(IntPtr.Zero); // modifier + args[1] = new JValue(0L); // containerColor + args[2] = new JValue(0L); // contentColor + args[3] = new JValue(0f); // tonalElevation + args[4] = new JValue(IntPtr.Zero); // windowInsets + args[5] = new JValue(((Java.Lang.Object)content).Handle); + args[6] = new JValue(((Java.Lang.Object)composer).Handle); + args[7] = new JValue(0); + args[8] = new JValue((int)NavigationBarDefault.All); + JNIEnv.CallStaticVoidMethod(s_navBarClass, s_navBarMethod, args); + } + + // androidx.compose.material3.NavigationBarKt.NavigationBarItem: + // (RowScope, selected, onClick, icon, modifier, enabled, label, + // alwaysShowLabel, colors, interactionSource, composer, $changed, $default) + // RowScope is the Kotlin extension receiver (first param). 9 user params + // (after the receiver); bits 0 (selected), 1 (onClick), 2 (icon) provided. + const string NavigationBarItemSig = + "(Landroidx/compose/foundation/layout/RowScope;Z" + + "Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;" + + "Landroidx/compose/ui/Modifier;Z" + + "Lkotlin/jvm/functions/Function2;Z" + + "Landroidx/compose/material3/NavigationBarItemColors;" + + "Landroidx/compose/foundation/interaction/MutableInteractionSource;" + + "Landroidx/compose/runtime/Composer;II)V"; + + static IntPtr s_navBarItemClass; + static IntPtr s_navBarItemMethod; + + public static unsafe void NavigationBarItem( + IntPtr rowScope, + bool selected, + IFunction0 onClick, + IFunction2 icon, + IFunction2? label, + int defaults, + IComposer composer) + { + if (s_navBarItemClass == IntPtr.Zero) + { + s_navBarItemClass = JNIEnv.FindClass("androidx/compose/material3/NavigationBarKt"); + s_navBarItemMethod = JNIEnv.GetStaticMethodID(s_navBarItemClass, "NavigationBarItem", NavigationBarItemSig); + } + + if (rowScope == IntPtr.Zero) + throw new System.InvalidOperationException( + "NavigationBarItem must be a child of NavigationBar (no RowScope receiver in scope)."); + + JValue* args = stackalloc JValue[13]; + args[0] = new JValue(rowScope); + args[1] = new JValue(selected); + args[2] = new JValue(((Java.Lang.Object)onClick).Handle); + args[3] = new JValue(((Java.Lang.Object)icon).Handle); + args[4] = new JValue(IntPtr.Zero); // modifier + args[5] = new JValue(true); // enabled + args[6] = new JValue(label is null ? IntPtr.Zero : ((Java.Lang.Object)label).Handle); + args[7] = new JValue(true); // alwaysShowLabel + args[8] = new JValue(IntPtr.Zero); // colors + args[9] = new JValue(IntPtr.Zero); // interactionSource + args[10] = new JValue(((Java.Lang.Object)composer).Handle); + args[11] = new JValue(0); + args[12] = new JValue(defaults); + JNIEnv.CallStaticVoidMethod(s_navBarItemClass, s_navBarItemMethod, args); + } + + // androidx.compose.material3.NavigationRailKt.NavigationRail-qi6gXK8: + // (modifier, containerColor, contentColor, header, windowInsets, + // content, composer, $changed, $default) + // 6 user params; only bit 5 (content) provided. (`header` is a slot we + // don't surface — defaulted via bit 3.) + const string NavigationRailSig = + "(Landroidx/compose/ui/Modifier;JJ" + + "Lkotlin/jvm/functions/Function3;" + + "Landroidx/compose/foundation/layout/WindowInsets;" + + "Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V"; + + static IntPtr s_navRailClass; + static IntPtr s_navRailMethod; + + public static unsafe void NavigationRail(IFunction3 content, IComposer composer) + { + if (s_navRailClass == IntPtr.Zero) + { + s_navRailClass = JNIEnv.FindClass("androidx/compose/material3/NavigationRailKt"); + s_navRailMethod = JNIEnv.GetStaticMethodID(s_navRailClass, "NavigationRail-qi6gXK8", NavigationRailSig); + } + + JValue* args = stackalloc JValue[9]; + args[0] = new JValue(IntPtr.Zero); // modifier + args[1] = new JValue(0L); // containerColor + args[2] = new JValue(0L); // contentColor + args[3] = new JValue(IntPtr.Zero); // header + args[4] = new JValue(IntPtr.Zero); // windowInsets + args[5] = new JValue(((Java.Lang.Object)content).Handle); + args[6] = new JValue(((Java.Lang.Object)composer).Handle); + args[7] = new JValue(0); + args[8] = new JValue((int)NavigationRailDefault.All); + JNIEnv.CallStaticVoidMethod(s_navRailClass, s_navRailMethod, args); + } + + // androidx.compose.material3.NavigationRailKt.NavigationRailItem: + // (selected, onClick, icon, modifier, enabled, label, alwaysShowLabel, + // colors, interactionSource, composer, $changed, $default) + // 9 user params (no scope receiver despite parent NavigationRail + // exposing a ColumnScope content lambda — NavigationRailItem is a + // top-level static, not a ColumnScope extension). + const string NavigationRailItemSig = + "(ZLkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;" + + "Landroidx/compose/ui/Modifier;Z" + + "Lkotlin/jvm/functions/Function2;Z" + + "Landroidx/compose/material3/NavigationRailItemColors;" + + "Landroidx/compose/foundation/interaction/MutableInteractionSource;" + + "Landroidx/compose/runtime/Composer;II)V"; + + static IntPtr s_navRailItemClass; + static IntPtr s_navRailItemMethod; + + public static unsafe void NavigationRailItem( + bool selected, + IFunction0 onClick, + IFunction2 icon, + IFunction2? label, + int defaults, + IComposer composer) + { + if (s_navRailItemClass == IntPtr.Zero) + { + s_navRailItemClass = JNIEnv.FindClass("androidx/compose/material3/NavigationRailKt"); + s_navRailItemMethod = JNIEnv.GetStaticMethodID(s_navRailItemClass, "NavigationRailItem", NavigationRailItemSig); + } + + JValue* args = stackalloc JValue[12]; + args[0] = new JValue(selected); + args[1] = new JValue(((Java.Lang.Object)onClick).Handle); + args[2] = new JValue(((Java.Lang.Object)icon).Handle); + args[3] = new JValue(IntPtr.Zero); // modifier + args[4] = new JValue(true); // enabled + args[5] = new JValue(label is null ? IntPtr.Zero : ((Java.Lang.Object)label).Handle); + args[6] = new JValue(true); // alwaysShowLabel + args[7] = new JValue(IntPtr.Zero); // colors + args[8] = new JValue(IntPtr.Zero); // interactionSource + args[9] = new JValue(((Java.Lang.Object)composer).Handle); + args[10] = new JValue(0); + args[11] = new JValue(defaults); + JNIEnv.CallStaticVoidMethod(s_navRailItemClass, s_navRailItemMethod, args); + } } diff --git a/src/ComposeNet.Compose/ComposeDefaults.cs b/src/ComposeNet.Compose/ComposeDefaults.cs index da052678..8a396381 100644 --- a/src/ComposeNet.Compose/ComposeDefaults.cs +++ b/src/ComposeNet.Compose/ComposeDefaults.cs @@ -9,15 +9,17 @@ // attribute below — the binder exposes those Kt classes, so the // generator can read parameter names off the longest overload. // -// The other seven (Button, Text, IconButton, FloatingActionButton, -// Surface, AlertDialog, TextField/OutlinedTextField) are *also* -// generated, but from the declarative `[ComposeDefaults]` overload. -// Their Kotlin overloads with the trailing $default param are stripped -// from the binding (mangled JVM names like `Text--4IGK_g` from inline -// classes such as `Color`/`TextUnit`/`Dp`), so there is no IMethodSymbol -// for the generator to introspect — we hand it the Kotlin parameter -// names instead. Names prefixed with `!` consume a bit position but -// don't emit an enum member (e.g. params the caller always provides). +// The other enums are *also* generated, but from the declarative +// `[ComposeDefaults]` overload. Their Kotlin overloads with the trailing +// $default param are stripped from the binding (mangled JVM names like +// `Text--4IGK_g` from inline classes such as `Color`/`TextUnit`/`Dp`), +// so there is no IMethodSymbol for the generator to introspect — we +// hand it the Kotlin parameter names instead. Names prefixed with `!` +// consume a bit position but don't emit an enum member (e.g. params +// the caller always provides). For extension-receiver functions +// (NavigationBarItem takes a RowScope receiver), the receiver is NOT +// part of the $default bitmask — start the name list at the first +// user-facing parameter. // // When dotnet/java-interop#1440 lands and exposes the inline-class // overloads, the declarative attributes can be replaced with @@ -82,3 +84,63 @@ "keyboardOptions", "keyboardActions", "singleLine", "maxLines", "minLines", "interactionSource", "shape", "colors")] +// androidx.compose.material3.CardKt.Card (non-clickable): 6 user params, +// bit 5 = content provided. +[assembly: ComposeDefaults("CardDefault", + "modifier", "shape", "colors", "elevation", "border", "!content")] + +// androidx.compose.material3.ChipKt.AssistChip: 11 user params, +// bit 0 = onClick, bit 1 = label (both always provided). +// Optional slot bits 4 (LeadingIcon) and 5 (TrailingIcon) are toggled +// per-call by AssistChip.Render. +[assembly: ComposeDefaults("AssistChipDefault", + "!onClick", "!label", "modifier", "enabled", "leadingIcon", "trailingIcon", + "shape", "colors", "elevation", "border", "interactionSource")] + +// androidx.compose.material3.ChipKt.FilterChip: 12 user params, +// bits 0 = selected, 1 = onClick, 2 = label (all always provided). +[assembly: ComposeDefaults("FilterChipDefault", + "!selected", "!onClick", "!label", "modifier", "enabled", + "leadingIcon", "trailingIcon", "shape", "colors", "elevation", + "border", "interactionSource")] + +// androidx.compose.material3.ChipKt.InputChip: 13 user params, +// bits 0 = selected, 1 = onClick, 2 = label (all always provided). +[assembly: ComposeDefaults("InputChipDefault", + "!selected", "!onClick", "!label", "modifier", "enabled", + "leadingIcon", "avatar", "trailingIcon", "shape", "colors", + "elevation", "border", "interactionSource")] + +// androidx.compose.material3.ChipKt.SuggestionChip: 10 user params, +// bit 0 = onClick, bit 1 = label (both always provided). +[assembly: ComposeDefaults("SuggestionChipDefault", + "!onClick", "!label", "modifier", "enabled", "icon", + "shape", "colors", "elevation", "border", "interactionSource")] + +// androidx.compose.material3.NavigationBarKt.NavigationBar-HsRjFd4: +// 6 user params, bit 5 = content provided. +[assembly: ComposeDefaults("NavigationBarDefault", + "modifier", "containerColor", "contentColor", "tonalElevation", + "windowInsets", "!content")] + +// androidx.compose.material3.NavigationBarKt.NavigationBarItem: 9 user +// params after the RowScope receiver (the receiver is not part of the +// $default bitmask). Bits 0 = selected, 1 = onClick, 2 = icon (all +// always provided). The optional Label slot is toggled by +// NavigationBarItem.Render. +[assembly: ComposeDefaults("NavigationBarItemDefault", + "!selected", "!onClick", "!icon", "modifier", "enabled", "label", + "alwaysShowLabel", "colors", "interactionSource")] + +// androidx.compose.material3.NavigationRailKt.NavigationRail-qi6gXK8: +// 6 user params, bit 5 = content provided. +[assembly: ComposeDefaults("NavigationRailDefault", + "modifier", "containerColor", "contentColor", "header", + "windowInsets", "!content")] + +// androidx.compose.material3.NavigationRailKt.NavigationRailItem: +// 9 user params; bits 0 = selected, 1 = onClick, 2 = icon +// (all always provided). +[assembly: ComposeDefaults("NavigationRailItemDefault", + "!selected", "!onClick", "!icon", "modifier", "enabled", "label", + "alwaysShowLabel", "colors", "interactionSource")] diff --git a/src/ComposeNet.Sample/MainActivity.cs b/src/ComposeNet.Sample/MainActivity.cs index 130843cb..c226b22b 100644 --- a/src/ComposeNet.Sample/MainActivity.cs +++ b/src/ComposeNet.Sample/MainActivity.cs @@ -14,6 +14,8 @@ protected override void OnCreate(Bundle? savedInstanceState) 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)); return new MaterialTheme { new Surface @@ -32,6 +34,36 @@ protected override void OnCreate(Bundle? savedInstanceState) }, 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"), + }, + new NavigationBar + { + new NavigationBarItem(selected: tab.Value == 0, onClick: () => tab.Value = 0) + { + Icon = new Text("🏠"), + Label = new Text("Home"), + }, + new NavigationBarItem(selected: tab.Value == 1, onClick: () => tab.Value = 1) + { + Icon = new Text("⚙"), + Label = new Text("Settings"), + }, + }, new FloatingActionButton(onClick: () => showDlg.Value = true) { new Text("✕"),