diff --git a/src/ComposeNet.Compose/ComposeBridges.cs b/src/ComposeNet.Compose/ComposeBridges.cs
index 48cea820..b9728f8b 100644
--- a/src/ComposeNet.Compose/ComposeBridges.cs
+++ b/src/ComposeNet.Compose/ComposeBridges.cs
@@ -259,11 +259,19 @@ internal static IntPtr ModifierClipRoundedCorners(IntPtr modifier, float dp)
public static partial void Text(
string text,
IModifier? modifier,
+ long? color,
Sp? fontSize,
+ FontStyle? fontStyle,
FontWeight? fontWeight,
+ FontFamily? fontFamily,
Sp? letterSpacing,
TextDecoration? decoration,
+ TextAlign? align,
Sp? lineHeight,
+ TextOverflow? overflow,
+ bool? softWrap,
+ int? maxLines,
+ int? minLines,
IComposer composer);
// androidx.compose.material3.ButtonKt.Button
@@ -605,8 +613,24 @@ public static partial void IconPainter(
Signature = TextFieldStringSig,
Defaults = typeof(TextFieldDefault))]
[ComposeFacade]
- public static partial void TextField(string value, [Callback(typeof(string))] IFunction1 onValueChange,
- IModifier? modifier, IComposer composer);
+ public static partial void TextField(
+ string value,
+ [Callback(typeof(string))] IFunction1 onValueChange,
+ IModifier? modifier,
+ bool? enabled,
+ bool? readOnly,
+ IFunction2? label,
+ IFunction2? placeholder,
+ IFunction2? leadingIcon,
+ IFunction2? trailingIcon,
+ IFunction2? prefix,
+ IFunction2? suffix,
+ IFunction2? supportingText,
+ bool? isError,
+ bool? singleLine,
+ int? maxLines,
+ int? minLines,
+ IComposer composer);
[ComposeBridge(
Class = "androidx/compose/material3/OutlinedTextFieldKt",
@@ -614,8 +638,24 @@ public static partial void TextField(string value, [Callback(typeof(string))] IF
Signature = TextFieldStringSig,
Defaults = typeof(TextFieldDefault))]
[ComposeFacade]
- public static partial void OutlinedTextField(string value, [Callback(typeof(string))] IFunction1 onValueChange,
- IModifier? modifier, IComposer composer);
+ public static partial void OutlinedTextField(
+ string value,
+ [Callback(typeof(string))] IFunction1 onValueChange,
+ IModifier? modifier,
+ bool? enabled,
+ bool? readOnly,
+ IFunction2? label,
+ IFunction2? placeholder,
+ IFunction2? leadingIcon,
+ IFunction2? trailingIcon,
+ IFunction2? prefix,
+ IFunction2? suffix,
+ IFunction2? supportingText,
+ bool? isError,
+ bool? singleLine,
+ int? maxLines,
+ int? minLines,
+ IComposer composer);
// androidx.compose.material3.SecureTextFieldKt.{SecureTextField,OutlinedSecureTextField}-XvU6IwQ.
// Both overloads have identical 23-user-param signatures: the
diff --git a/src/ComposeNet.Compose/FontFamily.cs b/src/ComposeNet.Compose/FontFamily.cs
new file mode 100644
index 00000000..2ea83f04
--- /dev/null
+++ b/src/ComposeNet.Compose/FontFamily.cs
@@ -0,0 +1,83 @@
+using Android.Runtime;
+
+namespace ComposeNet;
+
+///
+/// C# wrapper around androidx.compose.ui.text.font.FontFamily.
+/// Compose's FontFamily is a real Kotlin class, but Compose
+/// 1.11.2.1's ui-text-android package is shipped as a
+/// Java-library-only stub with zero exported types — so the .NET
+/// binding doesn't expose it yet. We subclass
+/// directly and resolve the Kotlin Companion instances via JNI;
+/// the bridge generator's reference-type code path
+/// (x is null ? IntPtr.Zero : x.Handle) passes the handle through
+/// to the JNI L slot.
+///
+/// Will swap to bound AndroidX.Compose.UI.Text.Font.FontFamily
+/// once
+/// ships and we adopt the next Xamarin.AndroidX.Compose.UI.Text.Android
+/// release.
+///
+public sealed class FontFamily : Java.Lang.Object
+{
+ FontFamily(IntPtr handle, JniHandleOwnership transfer)
+ : base(handle, transfer) { }
+
+ // FontFamily.Companion exposes generic-font-family getters:
+ // FontFamily$Companion.getDefault() : FontFamily
+ // FontFamily$Companion.getSansSerif() : FontFamily
+ // FontFamily$Companion.getSerif() : FontFamily
+ // FontFamily$Companion.getMonospace() : FontFamily
+ // FontFamily$Companion.getCursive() : FontFamily
+ static IntPtr s_companion;
+ static unsafe IntPtr Companion()
+ {
+ if (s_companion == IntPtr.Zero)
+ {
+ IntPtr fontFamilyCls = JNIEnv.FindClass("androidx/compose/ui/text/font/FontFamily");
+ IntPtr fid = JNIEnv.GetStaticFieldID(fontFamilyCls, "Companion", "Landroidx/compose/ui/text/font/FontFamily$Companion;");
+ IntPtr local = JNIEnv.GetStaticObjectField(fontFamilyCls, fid);
+ s_companion = JNIEnv.NewGlobalRef(local);
+ JNIEnv.DeleteLocalRef(local);
+ }
+ return s_companion;
+ }
+
+ static FontFamily Resolve(string getterName, string returnDescriptor)
+ {
+ // FontFamily$Companion getters return concrete subtypes:
+ // getDefault() -> SystemFontFamily
+ // getSansSerif() -> GenericFontFamily
+ // getSerif() -> GenericFontFamily
+ // getMonospace() -> GenericFontFamily
+ // getCursive() -> GenericFontFamily
+ // All of these extend FontFamily, so we wrap as our base type.
+ IntPtr cls = JNIEnv.FindClass("androidx/compose/ui/text/font/FontFamily$Companion");
+ IntPtr mid = JNIEnv.GetMethodID(cls, getterName, $"(){returnDescriptor}");
+ IntPtr companion = Companion();
+ IntPtr value = JNIEnv.CallObjectMethod(companion, mid);
+ return new FontFamily(value, JniHandleOwnership.TransferLocalRef);
+ }
+
+ const string SystemFontFamilyDescriptor = "Landroidx/compose/ui/text/font/SystemFontFamily;";
+ const string GenericFontFamilyDescriptor = "Landroidx/compose/ui/text/font/GenericFontFamily;";
+
+ static FontFamily? s_default, s_sansSerif, s_serif, s_monospace, s_cursive;
+
+ ///
+ /// FontFamily.Default — the platform's system font family.
+ ///
+ public static FontFamily Default => s_default ??= Resolve("getDefault", SystemFontFamilyDescriptor);
+
+ /// Generic sans-serif family (FontFamily.SansSerif).
+ public static FontFamily SansSerif => s_sansSerif ??= Resolve("getSansSerif", GenericFontFamilyDescriptor);
+
+ /// Generic serif family (FontFamily.Serif).
+ public static FontFamily Serif => s_serif ??= Resolve("getSerif", GenericFontFamilyDescriptor);
+
+ /// Generic monospace family (FontFamily.Monospace).
+ public static FontFamily Monospace => s_monospace ??= Resolve("getMonospace", GenericFontFamilyDescriptor);
+
+ /// Generic cursive family (FontFamily.Cursive).
+ public static FontFamily Cursive => s_cursive ??= Resolve("getCursive", GenericFontFamilyDescriptor);
+}
diff --git a/src/ComposeNet.Compose/FontStyle.cs b/src/ComposeNet.Compose/FontStyle.cs
new file mode 100644
index 00000000..4dd6df7f
--- /dev/null
+++ b/src/ComposeNet.Compose/FontStyle.cs
@@ -0,0 +1,75 @@
+using Android.Runtime;
+
+namespace ComposeNet;
+
+///
+/// C# wrapper around androidx.compose.ui.text.font.FontStyle.
+/// In Kotlin source, FontStyle is a @JvmInline value class
+/// wrapping an Int, but every @Composable function in
+/// Material 3 declares it as nullable (fontStyle: FontStyle?
+/// = null) — so at the JNI boundary it travels as a boxed
+/// androidx/compose/ui/text/font/FontStyle; reference, not a
+/// packed int. The bridge generator's reference-type path
+/// passes the handle through to the JNI L slot.
+///
+/// Same trick as : call the mangled
+/// Companion.getNormal-_-LCdwA()I for the packed int, then
+/// route through static FontStyle.box-impl(I)LFontStyle;.
+///
+/// Will swap to bound AndroidX.Compose.UI.Text.Font.FontStyle
+/// once
+/// ships.
+///
+public sealed class FontStyle : Java.Lang.Object
+{
+ FontStyle(IntPtr handle, JniHandleOwnership transfer)
+ : base(handle, transfer) { }
+
+ static IntPtr s_companion;
+ static IntPtr s_box;
+
+ static unsafe IntPtr Companion()
+ {
+ if (s_companion == IntPtr.Zero)
+ {
+ IntPtr cls = JNIEnv.FindClass("androidx/compose/ui/text/font/FontStyle");
+ IntPtr fid = JNIEnv.GetStaticFieldID(cls, "Companion", "Landroidx/compose/ui/text/font/FontStyle$Companion;");
+ IntPtr local = JNIEnv.GetStaticObjectField(cls, fid);
+ s_companion = JNIEnv.NewGlobalRef(local);
+ JNIEnv.DeleteLocalRef(local);
+ }
+ return s_companion;
+ }
+
+ static IntPtr BoxMethod()
+ {
+ if (s_box == IntPtr.Zero)
+ {
+ IntPtr cls = JNIEnv.FindClass("androidx/compose/ui/text/font/FontStyle");
+ s_box = JNIEnv.GetStaticMethodID(cls, "box-impl", "(I)Landroidx/compose/ui/text/font/FontStyle;");
+ }
+ return s_box;
+ }
+
+ static unsafe FontStyle Resolve(string mangledGetter)
+ {
+ IntPtr companionCls = JNIEnv.FindClass("androidx/compose/ui/text/font/FontStyle$Companion");
+ IntPtr getter = JNIEnv.GetMethodID(companionCls, mangledGetter, "()I");
+ int packed = JNIEnv.CallIntMethod(Companion(), getter);
+
+ IntPtr cls = JNIEnv.FindClass("androidx/compose/ui/text/font/FontStyle");
+ JValue* args = stackalloc JValue[1];
+ args[0] = new JValue(packed);
+ IntPtr boxed = JNIEnv.CallStaticObjectMethod(cls, BoxMethod(), args);
+ return new FontStyle(boxed, JniHandleOwnership.TransferLocalRef);
+ }
+
+ static FontStyle? s_normal, s_italic;
+
+ /// Upright glyphs (the default).
+ public static FontStyle Normal => s_normal ??= Resolve("getNormal-_-LCdwA");
+
+ /// Italic / slanted glyphs.
+ public static FontStyle Italic => s_italic ??= Resolve("getItalic-_-LCdwA");
+}
+
diff --git a/src/ComposeNet.Compose/PublicAPI.Unshipped.txt b/src/ComposeNet.Compose/PublicAPI.Unshipped.txt
index 88521ead..9c96527f 100644
--- a/src/ComposeNet.Compose/PublicAPI.Unshipped.txt
+++ b/src/ComposeNet.Compose/PublicAPI.Unshipped.txt
@@ -1,7 +1,7 @@
#nullable enable
-~override ComposeNet.TextAlign.Equals(object obj) -> bool
-~override ComposeNet.TextAlign.ToString() -> string
+~override ComposeNet.TextOverflow.Equals(object obj) -> bool
+~override ComposeNet.TextOverflow.ToString() -> string
ComposeNet.AlertDialog
ComposeNet.AlertDialog.AlertDialog(System.Action! onDismissRequest) -> void
ComposeNet.AlertDialog.ConfirmButton.get -> ComposeNet.ComposableNode?
@@ -221,6 +221,8 @@ ComposeNet.FlexibleBottomAppBar
ComposeNet.FlexibleBottomAppBar.FlexibleBottomAppBar() -> void
ComposeNet.FloatingActionButton
ComposeNet.FloatingActionButton.FloatingActionButton(System.Action! onClick) -> void
+ComposeNet.FontFamily
+ComposeNet.FontStyle
ComposeNet.FontWeight
ComposeNet.GridCells
ComposeNet.HorizontalCenteredHeroCarousel
@@ -463,8 +465,34 @@ ComposeNet.OutlinedSecureTextField.SupportingText.set -> void
ComposeNet.OutlinedSecureTextField.TrailingIcon.get -> ComposeNet.ComposableNode?
ComposeNet.OutlinedSecureTextField.TrailingIcon.set -> void
ComposeNet.OutlinedTextField
+ComposeNet.OutlinedTextField.Enabled.get -> bool?
+ComposeNet.OutlinedTextField.Enabled.set -> void
+ComposeNet.OutlinedTextField.IsError.get -> bool?
+ComposeNet.OutlinedTextField.IsError.set -> void
+ComposeNet.OutlinedTextField.Label.get -> ComposeNet.ComposableNode?
+ComposeNet.OutlinedTextField.Label.set -> void
+ComposeNet.OutlinedTextField.LeadingIcon.get -> ComposeNet.ComposableNode?
+ComposeNet.OutlinedTextField.LeadingIcon.set -> void
+ComposeNet.OutlinedTextField.MaxLines.get -> int?
+ComposeNet.OutlinedTextField.MaxLines.set -> void
+ComposeNet.OutlinedTextField.MinLines.get -> int?
+ComposeNet.OutlinedTextField.MinLines.set -> void
ComposeNet.OutlinedTextField.OutlinedTextField(ComposeNet.MutableState! state) -> void
ComposeNet.OutlinedTextField.OutlinedTextField(string! value, System.Action! onValueChange) -> void
+ComposeNet.OutlinedTextField.Placeholder.get -> ComposeNet.ComposableNode?
+ComposeNet.OutlinedTextField.Placeholder.set -> void
+ComposeNet.OutlinedTextField.Prefix.get -> ComposeNet.ComposableNode?
+ComposeNet.OutlinedTextField.Prefix.set -> void
+ComposeNet.OutlinedTextField.ReadOnly.get -> bool?
+ComposeNet.OutlinedTextField.ReadOnly.set -> void
+ComposeNet.OutlinedTextField.SingleLine.get -> bool?
+ComposeNet.OutlinedTextField.SingleLine.set -> void
+ComposeNet.OutlinedTextField.Suffix.get -> ComposeNet.ComposableNode?
+ComposeNet.OutlinedTextField.Suffix.set -> void
+ComposeNet.OutlinedTextField.SupportingText.get -> ComposeNet.ComposableNode?
+ComposeNet.OutlinedTextField.SupportingText.set -> void
+ComposeNet.OutlinedTextField.TrailingIcon.get -> ComposeNet.ComposableNode?
+ComposeNet.OutlinedTextField.TrailingIcon.set -> void
ComposeNet.PermanentDrawerSheet
ComposeNet.PermanentDrawerSheet.ContainerColor.get -> long
ComposeNet.PermanentDrawerSheet.ContainerColor.set -> void
@@ -612,30 +640,73 @@ ComposeNet.Tab.Text.set -> void
ComposeNet.TabRow
ComposeNet.TabRow.TabRow(int selectedTabIndex) -> void
ComposeNet.Text
+ComposeNet.Text.Align.get -> ComposeNet.TextAlign?
+ComposeNet.Text.Align.set -> void
+ComposeNet.Text.Color.get -> long?
+ComposeNet.Text.Color.set -> void
ComposeNet.Text.Decoration.get -> ComposeNet.TextDecoration?
ComposeNet.Text.Decoration.set -> void
+ComposeNet.Text.FontFamily.get -> ComposeNet.FontFamily?
+ComposeNet.Text.FontFamily.set -> void
ComposeNet.Text.FontSize.get -> ComposeNet.Sp?
ComposeNet.Text.FontSize.set -> void
+ComposeNet.Text.FontStyle.get -> ComposeNet.FontStyle?
+ComposeNet.Text.FontStyle.set -> void
ComposeNet.Text.FontWeight.get -> ComposeNet.FontWeight?
ComposeNet.Text.FontWeight.set -> void
ComposeNet.Text.LetterSpacing.get -> ComposeNet.Sp?
ComposeNet.Text.LetterSpacing.set -> void
ComposeNet.Text.LineHeight.get -> ComposeNet.Sp?
ComposeNet.Text.LineHeight.set -> void
+ComposeNet.Text.MaxLines.get -> int?
+ComposeNet.Text.MaxLines.set -> void
+ComposeNet.Text.MinLines.get -> int?
+ComposeNet.Text.MinLines.set -> void
+ComposeNet.Text.Overflow.get -> ComposeNet.TextOverflow?
+ComposeNet.Text.Overflow.set -> void
+ComposeNet.Text.SoftWrap.get -> bool?
+ComposeNet.Text.SoftWrap.set -> void
ComposeNet.Text.Text(string! text) -> void
ComposeNet.TextAlign
-ComposeNet.TextAlign.Deconstruct(out int Value) -> void
-ComposeNet.TextAlign.Equals(ComposeNet.TextAlign other) -> bool
-ComposeNet.TextAlign.TextAlign() -> void
-ComposeNet.TextAlign.TextAlign(int Value) -> void
-ComposeNet.TextAlign.Value.get -> int
-ComposeNet.TextAlign.Value.init -> void
ComposeNet.TextButton
ComposeNet.TextButton.TextButton(System.Action! onClick) -> void
ComposeNet.TextDecoration
ComposeNet.TextField
+ComposeNet.TextField.Enabled.get -> bool?
+ComposeNet.TextField.Enabled.set -> void
+ComposeNet.TextField.IsError.get -> bool?
+ComposeNet.TextField.IsError.set -> void
+ComposeNet.TextField.Label.get -> ComposeNet.ComposableNode?
+ComposeNet.TextField.Label.set -> void
+ComposeNet.TextField.LeadingIcon.get -> ComposeNet.ComposableNode?
+ComposeNet.TextField.LeadingIcon.set -> void
+ComposeNet.TextField.MaxLines.get -> int?
+ComposeNet.TextField.MaxLines.set -> void
+ComposeNet.TextField.MinLines.get -> int?
+ComposeNet.TextField.MinLines.set -> void
+ComposeNet.TextField.Placeholder.get -> ComposeNet.ComposableNode?
+ComposeNet.TextField.Placeholder.set -> void
+ComposeNet.TextField.Prefix.get -> ComposeNet.ComposableNode?
+ComposeNet.TextField.Prefix.set -> void
+ComposeNet.TextField.ReadOnly.get -> bool?
+ComposeNet.TextField.ReadOnly.set -> void
+ComposeNet.TextField.SingleLine.get -> bool?
+ComposeNet.TextField.SingleLine.set -> void
+ComposeNet.TextField.Suffix.get -> ComposeNet.ComposableNode?
+ComposeNet.TextField.Suffix.set -> void
+ComposeNet.TextField.SupportingText.get -> ComposeNet.ComposableNode?
+ComposeNet.TextField.SupportingText.set -> void
ComposeNet.TextField.TextField(ComposeNet.MutableState! state) -> void
ComposeNet.TextField.TextField(string! value, System.Action! onValueChange) -> void
+ComposeNet.TextField.TrailingIcon.get -> ComposeNet.ComposableNode?
+ComposeNet.TextField.TrailingIcon.set -> void
+ComposeNet.TextOverflow
+ComposeNet.TextOverflow.Deconstruct(out int Value) -> void
+ComposeNet.TextOverflow.Equals(ComposeNet.TextOverflow other) -> bool
+ComposeNet.TextOverflow.TextOverflow() -> void
+ComposeNet.TextOverflow.TextOverflow(int Value) -> void
+ComposeNet.TextOverflow.Value.get -> int
+ComposeNet.TextOverflow.Value.init -> void
ComposeNet.TimePicker
ComposeNet.TimePicker.TimePicker(ComposeNet.TimePickerState? state = null) -> void
ComposeNet.TimePickerDialog
@@ -703,7 +774,7 @@ override ComposeNet.MutableState.ToString() -> string!
override ComposeNet.Sp.Equals(object? obj) -> bool
override ComposeNet.Sp.GetHashCode() -> int
override ComposeNet.Sp.ToString() -> string!
-override ComposeNet.TextAlign.GetHashCode() -> int
+override ComposeNet.TextOverflow.GetHashCode() -> int
static ComposeNet.Arrangement.Bottom.get -> ComposeNet.Arrangement!
static ComposeNet.Arrangement.Center.get -> ComposeNet.Arrangement!
static ComposeNet.Arrangement.End.get -> ComposeNet.Arrangement!
@@ -720,6 +791,13 @@ static ComposeNet.Dp.operator !=(ComposeNet.Dp left, ComposeNet.Dp right) -> boo
static ComposeNet.Dp.operator ==(ComposeNet.Dp left, ComposeNet.Dp right) -> bool
static ComposeNet.Dp.Pack(ComposeNet.Dp? value) -> float
static ComposeNet.Dp.Zero.get -> ComposeNet.Dp
+static ComposeNet.FontFamily.Cursive.get -> ComposeNet.FontFamily!
+static ComposeNet.FontFamily.Default.get -> ComposeNet.FontFamily!
+static ComposeNet.FontFamily.Monospace.get -> ComposeNet.FontFamily!
+static ComposeNet.FontFamily.SansSerif.get -> ComposeNet.FontFamily!
+static ComposeNet.FontFamily.Serif.get -> ComposeNet.FontFamily!
+static ComposeNet.FontStyle.Italic.get -> ComposeNet.FontStyle!
+static ComposeNet.FontStyle.Normal.get -> ComposeNet.FontStyle!
static ComposeNet.FontWeight.Black.get -> ComposeNet.FontWeight!
static ComposeNet.FontWeight.Bold.get -> ComposeNet.FontWeight!
static ComposeNet.FontWeight.ExtraBold.get -> ComposeNet.FontWeight!
@@ -740,18 +818,23 @@ static ComposeNet.Sp.operator !=(ComposeNet.Sp left, ComposeNet.Sp right) -> boo
static ComposeNet.Sp.operator ==(ComposeNet.Sp left, ComposeNet.Sp right) -> bool
static ComposeNet.Sp.Pack(ComposeNet.Sp? value) -> long
static ComposeNet.Sp.Zero.get -> ComposeNet.Sp
-static ComposeNet.TextAlign.Center.get -> ComposeNet.TextAlign
-static ComposeNet.TextAlign.End.get -> ComposeNet.TextAlign
-static ComposeNet.TextAlign.Justify.get -> ComposeNet.TextAlign
-static ComposeNet.TextAlign.Left.get -> ComposeNet.TextAlign
-static ComposeNet.TextAlign.operator !=(ComposeNet.TextAlign left, ComposeNet.TextAlign right) -> bool
-static ComposeNet.TextAlign.operator ==(ComposeNet.TextAlign left, ComposeNet.TextAlign right) -> bool
-static ComposeNet.TextAlign.Pack(ComposeNet.TextAlign? value) -> int
-static ComposeNet.TextAlign.Right.get -> ComposeNet.TextAlign
-static ComposeNet.TextAlign.Start.get -> ComposeNet.TextAlign
-static ComposeNet.TextAlign.Unspecified.get -> ComposeNet.TextAlign
+static ComposeNet.TextAlign.Center.get -> ComposeNet.TextAlign!
+static ComposeNet.TextAlign.End.get -> ComposeNet.TextAlign!
+static ComposeNet.TextAlign.Justify.get -> ComposeNet.TextAlign!
+static ComposeNet.TextAlign.Left.get -> ComposeNet.TextAlign!
+static ComposeNet.TextAlign.Right.get -> ComposeNet.TextAlign!
+static ComposeNet.TextAlign.Start.get -> ComposeNet.TextAlign!
+static ComposeNet.TextAlign.Unspecified.get -> ComposeNet.TextAlign!
static ComposeNet.TextDecoration.LineThrough.get -> ComposeNet.TextDecoration!
static ComposeNet.TextDecoration.None.get -> ComposeNet.TextDecoration!
static ComposeNet.TextDecoration.Underline.get -> ComposeNet.TextDecoration!
+static ComposeNet.TextOverflow.Clip.get -> ComposeNet.TextOverflow
+static ComposeNet.TextOverflow.Ellipsis.get -> ComposeNet.TextOverflow
+static ComposeNet.TextOverflow.MiddleEllipsis.get -> ComposeNet.TextOverflow
+static ComposeNet.TextOverflow.operator !=(ComposeNet.TextOverflow left, ComposeNet.TextOverflow right) -> bool
+static ComposeNet.TextOverflow.operator ==(ComposeNet.TextOverflow left, ComposeNet.TextOverflow right) -> bool
+static ComposeNet.TextOverflow.Pack(ComposeNet.TextOverflow? value) -> int
+static ComposeNet.TextOverflow.StartEllipsis.get -> ComposeNet.TextOverflow
+static ComposeNet.TextOverflow.Visible.get -> ComposeNet.TextOverflow
virtual ComposeNet.MutableState.Value.get -> T
virtual ComposeNet.MutableState.Value.set -> void
diff --git a/src/ComposeNet.Compose/TextAlign.cs b/src/ComposeNet.Compose/TextAlign.cs
index 5d3d5be1..ec01f1bf 100644
--- a/src/ComposeNet.Compose/TextAlign.cs
+++ b/src/ComposeNet.Compose/TextAlign.cs
@@ -1,49 +1,94 @@
+using Android.Runtime;
+
namespace ComposeNet;
///
-/// C# mirror of Kotlin's androidx.compose.ui.text.style.TextAlign
-/// — a @JvmInline value class wrapping an Int. The bridge
-/// generator lowers TextAlign? to the underlying int JNI
-/// slot.
+/// C# wrapper around androidx.compose.ui.text.style.TextAlign.
+/// In Kotlin source, TextAlign is a @JvmInline value class
+/// wrapping an Int, but every @Composable function in
+/// Material 3 declares it as nullable (textAlign: TextAlign?
+/// = null) — so at the JNI boundary it travels as a boxed
+/// androidx/compose/ui/text/style/TextAlign; reference, not a
+/// packed int. The bridge generator's reference-type path
+/// passes the handle through to the JNI L slot.
+///
+/// To produce that boxed reference the constants here call the
+/// inline-class's mangled Companion.getCenter-e0LSkKk()I
+/// (and friends) for the packed int, then route it through the
+/// synthesized static TextAlign.box-impl(I)LTextAlign; to wrap.
///
-/// Values mirror the Kotlin TextAlign.Companion constants:
-///
-/// - = 1
-/// - = 2
-/// - = 3
-/// - = 4
-/// - = 5
-/// - = 6
-///
+/// Will swap to bound AndroidX.Compose.UI.Text.Style.TextAlign
+/// once
+/// ships and we adopt the next Xamarin.AndroidX.Compose.UI.Text.Android
+/// release.
///
-public readonly record struct TextAlign(int Value)
+public sealed class TextAlign : Java.Lang.Object
{
+ TextAlign(IntPtr handle, JniHandleOwnership transfer)
+ : base(handle, transfer) { }
+
+ static IntPtr s_companion;
+ static IntPtr s_box;
+
+ static unsafe IntPtr Companion()
+ {
+ if (s_companion == IntPtr.Zero)
+ {
+ IntPtr cls = JNIEnv.FindClass("androidx/compose/ui/text/style/TextAlign");
+ IntPtr fid = JNIEnv.GetStaticFieldID(cls, "Companion", "Landroidx/compose/ui/text/style/TextAlign$Companion;");
+ IntPtr local = JNIEnv.GetStaticObjectField(cls, fid);
+ s_companion = JNIEnv.NewGlobalRef(local);
+ JNIEnv.DeleteLocalRef(local);
+ }
+ return s_companion;
+ }
+
+ static IntPtr BoxMethod()
+ {
+ if (s_box == IntPtr.Zero)
+ {
+ IntPtr cls = JNIEnv.FindClass("androidx/compose/ui/text/style/TextAlign");
+ s_box = JNIEnv.GetStaticMethodID(cls, "box-impl", "(I)Landroidx/compose/ui/text/style/TextAlign;");
+ }
+ return s_box;
+ }
+
+ static unsafe TextAlign Resolve(string mangledGetter)
+ {
+ // 1. Companion.()I returns the packed int.
+ IntPtr companionCls = JNIEnv.FindClass("androidx/compose/ui/text/style/TextAlign$Companion");
+ IntPtr getter = JNIEnv.GetMethodID(companionCls, mangledGetter, "()I");
+ int packed = JNIEnv.CallIntMethod(Companion(), getter);
+
+ // 2. TextAlign.box-impl(int) -> Landroidx/.../TextAlign;
+ IntPtr cls = JNIEnv.FindClass("androidx/compose/ui/text/style/TextAlign");
+ JValue* args = stackalloc JValue[1];
+ args[0] = new JValue(packed);
+ IntPtr boxed = JNIEnv.CallStaticObjectMethod(cls, BoxMethod(), args);
+ return new TextAlign(boxed, JniHandleOwnership.TransferLocalRef);
+ }
+
+ static TextAlign? s_left, s_right, s_center, s_justify, s_start, s_end, s_unspecified;
+
/// Align text to the left edge of the container.
- public static TextAlign Left => new(1);
+ public static TextAlign Left => s_left ??= Resolve("getLeft-e0LSkKk");
/// Align text to the right edge of the container.
- public static TextAlign Right => new(2);
+ public static TextAlign Right => s_right ??= Resolve("getRight-e0LSkKk");
/// Center text within the container.
- public static TextAlign Center => new(3);
+ public static TextAlign Center => s_center ??= Resolve("getCenter-e0LSkKk");
/// Stretch lines to fill the container width.
- public static TextAlign Justify => new(4);
+ public static TextAlign Justify => s_justify ??= Resolve("getJustify-e0LSkKk");
/// Align text to the layout-direction start edge.
- public static TextAlign Start => new(5);
+ public static TextAlign Start => s_start ??= Resolve("getStart-e0LSkKk");
/// Align text to the layout-direction end edge.
- public static TextAlign End => new(6);
-
- /// Kotlin's TextAlign.Unspecified sentinel.
- public static TextAlign Unspecified => new(int.MinValue);
-
- ///
- /// Pack a nullable into the raw int
- /// the JNI slot expects. null → 0; the auto-mask
- /// leaves the $default bit set so Kotlin's real default
- /// applies.
- ///
- public static int Pack(TextAlign? value) => value?.Value ?? 0;
+ public static TextAlign End => s_end ??= Resolve("getEnd-e0LSkKk");
+
+ /// The unspecified-alignment sentinel value (Kotlin's default for nullable slots).
+ public static TextAlign Unspecified => s_unspecified ??= Resolve("getUnspecified-e0LSkKk");
}
+
diff --git a/src/ComposeNet.Compose/TextOverflow.cs b/src/ComposeNet.Compose/TextOverflow.cs
new file mode 100644
index 00000000..a2386969
--- /dev/null
+++ b/src/ComposeNet.Compose/TextOverflow.cs
@@ -0,0 +1,42 @@
+namespace ComposeNet;
+
+///
+/// C# mirror of Kotlin's androidx.compose.ui.text.style.TextOverflow
+/// — a @JvmInline value class wrapping an Int. The bridge
+/// generator lowers TextOverflow? to the underlying int
+/// JNI slot.
+///
+/// Values mirror the Kotlin TextOverflow.Companion constants:
+///
+/// - = 1 — truncate at the container edge.
+/// - = 2 — replace overflow with "…".
+/// - = 3 — render past the container bounds.
+/// - = 4 — leading ellipsis.
+/// - = 5 — middle ellipsis.
+///
+///
+public readonly record struct TextOverflow(int Value)
+{
+ /// Truncate the text at the edge of the container.
+ public static TextOverflow Clip => new(1);
+
+ /// Replace the overflowing text with an ellipsis (default for single-line).
+ public static TextOverflow Ellipsis => new(2);
+
+ /// Render the text outside the container bounds (no clipping).
+ public static TextOverflow Visible => new(3);
+
+ /// Place the ellipsis at the start of the text.
+ public static TextOverflow StartEllipsis => new(4);
+
+ /// Place the ellipsis in the middle of the text.
+ public static TextOverflow MiddleEllipsis => new(5);
+
+ ///
+ /// Pack a nullable into the raw
+ /// int the JNI slot expects. null → 0; the
+ /// auto-mask leaves the $default bit set so Kotlin's real
+ /// default applies.
+ ///
+ public static int Pack(TextOverflow? value) => value?.Value ?? 0;
+}
diff --git a/src/ComposeNet.Sample/MainActivity.cs b/src/ComposeNet.Sample/MainActivity.cs
index 832ed2c4..9002206c 100644
--- a/src/ComposeNet.Sample/MainActivity.cs
+++ b/src/ComposeNet.Sample/MainActivity.cs
@@ -180,6 +180,64 @@ protected override void OnCreate(Bundle? savedInstanceState)
LineHeight = 22,
Modifier = Modifier.Companion.Padding(8),
},
+ // Issue #58: text styling additions — color, italic /
+ // family, alignment, overflow, line clamping. Each
+ // property flows through the [ComposeFacade] /
+ // [ComposeBridge] generators: Color/MaxLines/MinLines/
+ // SoftWrap use the new nullable-primitive path,
+ // FontStyle/FontFamily/TextAlign use the nullable
+ // reference-wrapper path, and TextOverflow uses the
+ // packed @JvmInline value-class path.
+ new Text("Issue #58 text styling:")
+ {
+ Modifier = Modifier.Companion.Padding(top: 8, bottom: 4, start: 0, end: 0),
+ FontWeight = ComposeNet.FontWeight.Bold,
+ },
+ new Text("Italic serif red, centered")
+ {
+ Color = ColorKt.Color(red: 0xC6, green: 0x28, blue: 0x28, alpha: 0xFF),
+ FontStyle = ComposeNet.FontStyle.Italic,
+ FontFamily = ComposeNet.FontFamily.Serif,
+ Align = ComposeNet.TextAlign.Center,
+ Modifier = Modifier.Companion.FillMaxWidth(),
+ },
+ new Text("Monospace, end-aligned")
+ {
+ FontFamily = ComposeNet.FontFamily.Monospace,
+ Align = ComposeNet.TextAlign.End,
+ Modifier = Modifier.Companion.FillMaxWidth(),
+ },
+ new Text("This long line should clip with an ellipsis because we cap it at MaxLines=1 and force overflow.")
+ {
+ MaxLines = 1,
+ Overflow = ComposeNet.TextOverflow.Ellipsis,
+ SoftWrap = false,
+ },
+ new Text("This paragraph wraps to at most two lines and uses a non-default minLines so the slot reserves vertical space even when the content is shorter than the maximum allowed.")
+ {
+ MaxLines = 2,
+ MinLines = 2,
+ Overflow = ComposeNet.TextOverflow.Ellipsis,
+ },
+ // TextField with new slots: leading/trailing icons,
+ // label, supporting text, prefix, suffix.
+ new TextField(name)
+ {
+ Label = new Text("Your name"),
+ Placeholder = new Text("Type something…"),
+ LeadingIcon = new Text("👤"),
+ TrailingIcon = new Text("✎"),
+ SupportingText = new Text("Powered by issue #58 slots"),
+ SingleLine = true,
+ },
+ new OutlinedTextField(name)
+ {
+ Label = new Text("Outlined variant"),
+ Prefix = new Text("@"),
+ Suffix = new Text(".dev"),
+ SupportingText = new Text($"len={name.Value.Length}"),
+ SingleLine = true,
+ },
// Phase 2 modifier demo — clickable rounded chip painted with
// Background + Border + Clip; tapping it increments the counter.
new Text($"Phase 2 modifiers (tap me): {count}")
diff --git a/src/ComposeNet.SourceGenerators.Tests/BridgeGeneratorTests.cs b/src/ComposeNet.SourceGenerators.Tests/BridgeGeneratorTests.cs
index e58528d7..eb057108 100644
--- a/src/ComposeNet.SourceGenerators.Tests/BridgeGeneratorTests.cs
+++ b/src/ComposeNet.SourceGenerators.Tests/BridgeGeneratorTests.cs
@@ -78,13 +78,16 @@ public readonly record struct Em(float Value)
{
public static long Pack(Em? value) => default;
}
- public readonly record struct TextAlign(int Value)
- {
- public static int Pack(TextAlign? value) => value?.Value ?? 0;
- }
public sealed class Shape : Java.Lang.Object { }
public sealed class FontWeight : Java.Lang.Object { }
+ public sealed class FontStyle : Java.Lang.Object { }
+ public sealed class FontFamily : Java.Lang.Object { }
+ public sealed class TextAlign : Java.Lang.Object { }
public sealed class TextDecoration : Java.Lang.Object { }
+ public readonly record struct TextOverflow(int Value)
+ {
+ public static int Pack(TextOverflow? value) => value?.Value ?? 0;
+ }
}
""";
@@ -1132,15 +1135,19 @@ public static partial class ComposeBridges
}
[Fact]
- public void ValueType_SpTextAlign_LowerCorrectly()
+ public void ValueType_SpTextOverflow_LowerCorrectly()
{
- // @Composable bridge: (Sp fontSize, TextAlign align, Composer).
+ // @Composable bridge: (Sp fontSize, TextOverflow overflow, Composer).
// JNI: 2 user slots (J/I) + Composer (L) + 1 $changed (I) + 1 $default (I).
+ // Sp lowers to packed long; TextOverflow lowers to packed int —
+ // both are non-nullable in their real-world Compose @Composable
+ // declarations, so they travel as primitives rather than boxed
+ // references.
var code = """
using ComposeNet;
using AndroidX.Compose.Runtime;
- [assembly: ComposeDefaults("FontDefault", "fontSize", "align")]
+ [assembly: ComposeDefaults("FontDefault", "fontSize", "overflow")]
namespace ComposeNet
{
@@ -1151,7 +1158,7 @@ public static partial class ComposeBridges
JvmName = "f",
Signature = "(JILandroidx/compose/runtime/Composer;II)V",
Defaults = typeof(FontDefault))]
- public static partial void F(Sp? fontSize, TextAlign? align, IComposer composer);
+ public static partial void F(Sp? fontSize, TextOverflow? overflow, IComposer composer);
}
}
""";
@@ -1160,7 +1167,7 @@ public static partial class ComposeBridges
Assert.Empty(diags.Where(d => d.Severity == DiagnosticSeverity.Error));
Assert.NotNull(emitted);
Assert.Contains("global::ComposeNet.Sp.Pack(fontSize)", emitted);
- Assert.Contains("global::ComposeNet.TextAlign.Pack(align)", emitted);
+ Assert.Contains("global::ComposeNet.TextOverflow.Pack(overflow)", emitted);
}
[Fact]
@@ -1253,4 +1260,75 @@ public static partial class ComposeBridges
Assert.Contains("weight is null ? global::System.IntPtr.Zero : ((global::Java.Lang.Object)weight).Handle", emitted);
Assert.Contains("if (weight is not null) defaults &= ~(int)global::ComposeNet.WeightedDefault.Weight", emitted);
}
+
+ [Fact]
+ public void NullablePrimitive_BoolIntLong_LowerToZeroDefaults()
+ {
+ // bool? / int? / long? params surface as nullable so the caller
+ // can leave the Kotlin default in place (auto-mask bit stays
+ // set). When the caller supplies a value, the mask bit clears
+ // and the value is passed through to the JNI primitive slot.
+ var code = """
+ using ComposeNet;
+ using AndroidX.Compose.Runtime;
+
+ [assembly: ComposeDefaults("PrimDefault",
+ "softWrap", "maxLines", "color")]
+
+ namespace ComposeNet
+ {
+ public static partial class ComposeBridges
+ {
+ [ComposeBridge(
+ Class = "x/Y",
+ JvmName = "f",
+ Signature = "(ZIJLandroidx/compose/runtime/Composer;II)V",
+ Defaults = typeof(PrimDefault))]
+ public static partial void F(bool? softWrap, int? maxLines, long? color, IComposer composer);
+ }
+ }
+ """;
+
+ var (_, diags, emitted) = Run(code);
+ Assert.Empty(diags.Where(d => d.Severity == DiagnosticSeverity.Error));
+ Assert.NotNull(emitted);
+ // Lowering: pass underlying primitive with null → zero literal.
+ Assert.Contains("(softWrap ?? false)", emitted);
+ Assert.Contains("(maxLines ?? 0)", emitted);
+ Assert.Contains("(color ?? 0L)", emitted);
+ // Auto-mask: clear the bit only when a value was supplied.
+ Assert.Contains("if (softWrap is not null) defaults &= ~(int)global::ComposeNet.PrimDefault.SoftWrap", emitted);
+ Assert.Contains("if (maxLines is not null) defaults &= ~(int)global::ComposeNet.PrimDefault.MaxLines", emitted);
+ Assert.Contains("if (color is not null) defaults &= ~(int)global::ComposeNet.PrimDefault.Color", emitted);
+ }
+
+ [Fact]
+ public void NullablePrimitive_FloatDouble_LowerCorrectly()
+ {
+ var code = """
+ using ComposeNet;
+ using AndroidX.Compose.Runtime;
+
+ [assembly: ComposeDefaults("FloatDefault", "ratio", "scale")]
+
+ namespace ComposeNet
+ {
+ public static partial class ComposeBridges
+ {
+ [ComposeBridge(
+ Class = "x/Y",
+ JvmName = "f",
+ Signature = "(FDLandroidx/compose/runtime/Composer;II)V",
+ Defaults = typeof(FloatDefault))]
+ public static partial void F(float? ratio, double? scale, IComposer composer);
+ }
+ }
+ """;
+
+ var (_, diags, emitted) = Run(code);
+ Assert.Empty(diags.Where(d => d.Severity == DiagnosticSeverity.Error));
+ Assert.NotNull(emitted);
+ Assert.Contains("(ratio ?? 0f)", emitted);
+ Assert.Contains("(scale ?? 0d)", emitted);
+ }
}
diff --git a/src/ComposeNet.SourceGenerators.Tests/FacadeGeneratorTests.cs b/src/ComposeNet.SourceGenerators.Tests/FacadeGeneratorTests.cs
index 0208426b..4deb40bc 100644
--- a/src/ComposeNet.SourceGenerators.Tests/FacadeGeneratorTests.cs
+++ b/src/ComposeNet.SourceGenerators.Tests/FacadeGeneratorTests.cs
@@ -104,13 +104,16 @@ public readonly struct Em
public float Value { get; }
public static long Pack(Em? e) => 0L;
}
- public readonly struct TextAlign
+ public readonly struct TextOverflow
{
- public TextAlign(int v) { Value = v; }
+ public TextOverflow(int v) { Value = v; }
public int Value { get; }
- public static int Pack(TextAlign? a) => 0;
+ public static int Pack(TextOverflow? a) => 0;
}
public class FontWeight : Java.Lang.Object { }
+ public class FontStyle : Java.Lang.Object { }
+ public class FontFamily : Java.Lang.Object { }
+ public class TextAlign : Java.Lang.Object { }
public class TextDecoration : Java.Lang.Object { }
public class Shape : Java.Lang.Object { }
public abstract class ComposableNode
@@ -1562,8 +1565,11 @@ public static partial void Text(
}
[Fact]
- public void OptionalValue_TextAlignAndDpAreClassifiedAsValueTypes()
+ public void OptionalValue_TextOverflowAndDpAreClassifiedAsValueTypes()
{
+ // TextOverflow is a non-nullable @JvmInline value class in
+ // Compose source — it travels as packed `I`. Dp is also packed
+ // (`F`). Both surface as nullable auto-properties.
var code = """
using AndroidX.Compose.Runtime;
using AndroidX.Compose.UI;
@@ -1571,7 +1577,7 @@ public void OptionalValue_TextAlignAndDpAreClassifiedAsValueTypes()
using Kotlin.Jvm.Functions;
[assembly: ComposeDefaults("FooDefault",
- "!a", "align", "size")]
+ "!a", "overflow", "size")]
namespace ComposeNet
{
@@ -1581,7 +1587,7 @@ public static partial class ComposeBridges
Signature="(Ljava/lang/String;IFLandroidx/compose/runtime/Composer;II)V",
Defaults=typeof(FooDefault))]
[ComposeFacade]
- public static partial void Foo(string a, TextAlign? align, Dp? size, IComposer composer);
+ public static partial void Foo(string a, TextOverflow? overflow, Dp? size, IComposer composer);
}
}
""";
@@ -1589,7 +1595,7 @@ public static partial class ComposeBridges
var (output, diags, emitted) = Run(code, "Foo");
Assert.Empty(diags.Where(d => d.Severity == DiagnosticSeverity.Error));
Assert.NotNull(emitted);
- Assert.Contains("public global::ComposeNet.TextAlign? Align { get; set; }", emitted);
+ Assert.Contains("public global::ComposeNet.TextOverflow? Overflow { get; set; }", emitted);
Assert.Contains("public global::ComposeNet.Dp? Size { get; set; }", emitted);
var errors = output.GetDiagnostics().Where(d => d.Severity == DiagnosticSeverity.Error).ToArray();
@@ -1639,4 +1645,54 @@ public static partial void Bar(
var errors = output.GetDiagnostics().Where(d => d.Severity == DiagnosticSeverity.Error).ToArray();
Assert.Empty(errors);
}
+
+ [Fact]
+ public void OptionalValue_NullablePrimitivesEmitNullableAutoProperties()
+ {
+ // bool? / int? / long? params surface as Optional auto-properties
+ // — null leaves the Kotlin default in place via the auto-mask
+ // bit, a value clears the bit and lowers to the JNI primitive
+ // slot.
+ var code = """
+ using AndroidX.Compose.Runtime;
+ using AndroidX.Compose.UI;
+ using ComposeNet;
+ using Kotlin.Jvm.Functions;
+
+ [assembly: ComposeDefaults("PrimDefault",
+ "!text", "modifier", "softWrap", "maxLines", "color")]
+
+ namespace ComposeNet
+ {
+ public static partial class ComposeBridges
+ {
+ [ComposeBridge(Class="x/Y", JvmName="F",
+ Signature="(Ljava/lang/String;Landroidx/compose/ui/Modifier;ZIJLandroidx/compose/runtime/Composer;II)V",
+ Defaults=typeof(PrimDefault))]
+ [ComposeFacade]
+ public static partial void F(
+ string text, IModifier? modifier,
+ bool? softWrap, int? maxLines, long? color,
+ IComposer composer);
+ }
+ }
+ """;
+
+ var (output, diags, emitted) = Run(code, "F");
+ Assert.Empty(diags.Where(d => d.Severity == DiagnosticSeverity.Error));
+ Assert.NotNull(emitted);
+ // Each nullable primitive surfaces as an auto-property.
+ Assert.Contains("public bool? SoftWrap { get; set; }", emitted);
+ Assert.Contains("public int? MaxLines { get; set; }", emitted);
+ Assert.Contains("public long? Color { get; set; }", emitted);
+ // The bridge call passes the property names through.
+ Assert.Contains("global::ComposeNet.ComposeBridges.F(_text, BuildModifier(), SoftWrap, MaxLines, Color, composer);", emitted);
+ // Not surfaced as ctor parameters.
+ Assert.DoesNotContain("bool? softWrap", emitted);
+ Assert.DoesNotContain("int? maxLines", emitted);
+ Assert.DoesNotContain("long? color", emitted);
+
+ var errors = output.GetDiagnostics().Where(d => d.Severity == DiagnosticSeverity.Error).ToArray();
+ Assert.Empty(errors);
+ }
}
diff --git a/src/ComposeNet.SourceGenerators/ComposeBridgeGenerator.cs b/src/ComposeNet.SourceGenerators/ComposeBridgeGenerator.cs
index c693b629..6be62284 100644
--- a/src/ComposeNet.SourceGenerators/ComposeBridgeGenerator.cs
+++ b/src/ComposeNet.SourceGenerators/ComposeBridgeGenerator.cs
@@ -532,7 +532,7 @@ static string Emit(
var member = PascalCase(p.Name);
bool nullableValueType = ComposeValueTypes.TryGet(p.Type, out _, out _);
- if (p.NullableAnnotation == NullableAnnotation.Annotated || p.Type.IsReferenceType || nullableValueType || IsNullableIntPtr(p.Type))
+ if (p.NullableAnnotation == NullableAnnotation.Annotated || p.Type.IsReferenceType || nullableValueType || IsNullableIntPtr(p.Type) || IsNullablePrimitive(p.Type))
{
sb.Append(" if (").Append(EscapeIdent(p.Name)).Append(" is not null) defaults &= ~(int)global::ComposeNet.")
.Append(enumName).Append('.').Append(member).AppendLine(";");
@@ -710,6 +710,16 @@ static void EmitUserArgValue(StringBuilder sb, IParameterSymbol p, JniType sigTy
sb.Append('(').Append(p.Name).Append(" ?? global::System.IntPtr.Zero)");
return;
}
+ if (IsNullablePrimitive(p.Type))
+ {
+ // `bool? / int? / long? / float? / double?` → null means
+ // "let the Kotlin default kick in"; pass the zero literal
+ // in that slot. The auto-mask logic clears the matching
+ // `$default` bit only when the caller supplied a value.
+ var defaultLit = NullablePrimitiveZeroLiteral(p.Type);
+ sb.Append('(').Append(EscapeIdent(p.Name)).Append(" ?? ").Append(defaultLit).Append(')');
+ return;
+ }
if (ComposeValueTypes.TryGet(p.Type, out _, out var info))
{
// Recognized Compose @JvmInline value class. The lowering
@@ -757,7 +767,8 @@ or SpecialType.System_IntPtr
or SpecialType.System_String)
return false;
if (IsNullableIntPtr(p.Type)) return false;
- // Recognized Compose value types (Color/Dp/Sp/Em/TextAlign) lower
+ if (IsNullablePrimitive(p.Type)) return false;
+ // Recognized Compose value types (Dp/Sp/Em/TextOverflow) lower
// to JNI primitives — no managed handle to keep alive.
if (ComposeValueTypes.TryGet(p.Type, out _, out _)) return false;
return true;
@@ -769,6 +780,48 @@ t is INamedTypeSymbol n &&
n.TypeArguments.Length == 1 &&
n.TypeArguments[0].SpecialType == SpecialType.System_IntPtr;
+ ///
+ /// True for parameters typed as Nullable<T> where
+ /// T is one of the primitive JNI-friendly value types
+ /// (bool, int, long, float,
+ /// double). These let a bridge expose an "optional" Compose
+ /// param: null leaves the $default bit set so Kotlin's
+ /// real default applies; a value clears the bit and is passed
+ /// through to the JNI primitive slot.
+ ///
+ static bool IsNullablePrimitive(ITypeSymbol t)
+ {
+ if (t is not INamedTypeSymbol n) return false;
+ if (n.OriginalDefinition.SpecialType != SpecialType.System_Nullable_T) return false;
+ if (n.TypeArguments.Length != 1) return false;
+ return n.TypeArguments[0].SpecialType is
+ SpecialType.System_Boolean
+ or SpecialType.System_Int32
+ or SpecialType.System_Int64
+ or SpecialType.System_Single
+ or SpecialType.System_Double;
+ }
+
+ ///
+ /// Zero-literal expression for the underlying primitive of a
+ /// type. Used as the
+ /// ?? default fallback in
+ /// when the caller passes null.
+ ///
+ static string NullablePrimitiveZeroLiteral(ITypeSymbol t)
+ {
+ var inner = ((INamedTypeSymbol)t).TypeArguments[0].SpecialType;
+ return inner switch
+ {
+ SpecialType.System_Boolean => "false",
+ SpecialType.System_Int32 => "0",
+ SpecialType.System_Int64 => "0L",
+ SpecialType.System_Single => "0f",
+ SpecialType.System_Double => "0d",
+ _ => "default",
+ };
+ }
+
static bool IsModifierType(ITypeSymbol t)
{
if (t is INamedTypeSymbol n)
diff --git a/src/ComposeNet.SourceGenerators/ComposeFacadeGenerator.cs b/src/ComposeNet.SourceGenerators/ComposeFacadeGenerator.cs
index 8749fa2d..3350330b 100644
--- a/src/ComposeNet.SourceGenerators/ComposeFacadeGenerator.cs
+++ b/src/ComposeNet.SourceGenerators/ComposeFacadeGenerator.cs
@@ -934,16 +934,24 @@ or SpecialType.System_Single
///
/// True for parameters typed as Nullable<T> where T
/// is a recognized Compose @JvmInline value class (Dp/Sp/Em/
- /// TextAlign), or for nullable reference-typed wrappers in
- /// . Both shapes surface as
- /// auto-properties on
- /// the generated facade. Non-nullable reference wrappers do not
- /// qualify — emitting a nullable auto-property for them would
- /// pass null to a non-nullable bridge parameter.
+ /// TextOverflow), for nullable reference-typed wrappers in
+ /// (FontWeight/FontStyle/
+ /// FontFamily/TextAlign/TextDecoration/Shape), or for
+ /// Nullable<T> where T is a JNI-friendly
+ /// primitive (bool, int, long, float,
+ /// double) — the "optional Compose primitive" shape:
+ /// null → leave the $default bit set; a value clears
+ /// the bit and lowers to the primitive JNI slot. All three shapes
+ /// surface as
+ /// auto-properties on the generated facade. Non-nullable reference
+ /// wrappers do not qualify — emitting a nullable auto-property for
+ /// them would pass null to a non-nullable bridge parameter.
///
static bool IsOptionalValueType(ITypeSymbol type, NullableAnnotation annotation)
{
if (ComposeValueTypes.TryGet(type, out _, out _)) return true;
+ // Nullable: bool?, int?, long?, float?, double?.
+ if (IsNullablePrimitive(type)) return true;
// Nullable reference-type wrapper: T? where T is recognized.
// The C# nullable-annotation flow gives us the underlying type
// directly (no Nullable wrapping) for reference types, so
@@ -955,6 +963,26 @@ static bool IsOptionalValueType(ITypeSymbol type, NullableAnnotation annotation)
return false;
}
+ ///
+ /// Mirror of ComposeBridgeGenerator.IsNullablePrimitive —
+ /// see that helper for the rationale. Kept private here because
+ /// the bridge generator's copy lives in a separate translation
+ /// unit and the registry of "primitive Nullable types" is small
+ /// and stable.
+ ///
+ static bool IsNullablePrimitive(ITypeSymbol t)
+ {
+ if (t is not INamedTypeSymbol n) return false;
+ if (n.OriginalDefinition.SpecialType != SpecialType.System_Nullable_T) return false;
+ if (n.TypeArguments.Length != 1) return false;
+ return n.TypeArguments[0].SpecialType is
+ SpecialType.System_Boolean
+ or SpecialType.System_Int32
+ or SpecialType.System_Int64
+ or SpecialType.System_Single
+ or SpecialType.System_Double;
+ }
+
static bool IsKnownScopeKind(Compilation compilation, string scope)
{
var enumType = compilation.GetTypeByMetadataName("ComposeNet.ScopeKind");
diff --git a/src/ComposeNet.SourceGenerators/ComposeReferenceTypes.cs b/src/ComposeNet.SourceGenerators/ComposeReferenceTypes.cs
index d386f2c7..fc83e59f 100644
--- a/src/ComposeNet.SourceGenerators/ComposeReferenceTypes.cs
+++ b/src/ComposeNet.SourceGenerators/ComposeReferenceTypes.cs
@@ -31,6 +31,9 @@ internal static class ComposeReferenceTypes
public static readonly IReadOnlyCollection Recognized = new HashSet
{
"ComposeNet.FontWeight",
+ "ComposeNet.FontFamily",
+ "ComposeNet.FontStyle",
+ "ComposeNet.TextAlign",
"ComposeNet.TextDecoration",
"ComposeNet.Shape",
};
diff --git a/src/ComposeNet.SourceGenerators/ComposeValueTypes.cs b/src/ComposeNet.SourceGenerators/ComposeValueTypes.cs
index 56f98ae4..91f1bbf6 100644
--- a/src/ComposeNet.SourceGenerators/ComposeValueTypes.cs
+++ b/src/ComposeNet.SourceGenerators/ComposeValueTypes.cs
@@ -41,9 +41,13 @@ internal static class ComposeValueTypes
["ComposeNet.Sp"] =
('J', "global::ComposeNet.Sp.Pack({0})"),
- // androidx.compose.ui.text.style.TextAlign → JNI int.
- ["ComposeNet.TextAlign"] =
- ('I', "global::ComposeNet.TextAlign.Pack({0})"),
+ // androidx.compose.ui.text.style.TextOverflow → JNI int.
+ // (Compose declares TextOverflow as non-nullable in
+ // @Composable signatures, so it lowers as packed `I` rather
+ // than the boxed `L` reference seen for nullable inline
+ // classes like TextAlign / FontStyle.)
+ ["ComposeNet.TextOverflow"] =
+ ('I', "global::ComposeNet.TextOverflow.Pack({0})"),
// androidx.compose.ui.graphics.Color is bound by
// Xamarin.AndroidX.Compose.UI.Graphics 1.11.2.1, but Kotlin's