diff --git a/platform/android/lui/src/main/kotlin/dev/lui/LuiKindRules.kt b/platform/android/lui/src/main/kotlin/dev/lui/LuiKindRules.kt index 3a3a8e37..9990dc42 100644 --- a/platform/android/lui/src/main/kotlin/dev/lui/LuiKindRules.kt +++ b/platform/android/lui/src/main/kotlin/dev/lui/LuiKindRules.kt @@ -944,6 +944,7 @@ internal object LuiKindRules { "text-decoration" -> textDecorationSupported(value.stringValue) "white-space" -> value.stringValue in setOf("normal", "nowrap", "pre-wrap") "text-overflow" -> value.stringValue in setOf("clip", "ellipsis") + "text-alignment" -> value.stringValue in textAlignments "user-select" -> value.stringValue in setOf("none", "text") "cursor" -> value.stringValue in setOf("default", "pointer", "text") "data-attrs" -> value.stringValue?.let { dataAttrsValueOk(it) } == true diff --git a/platform/android/lui/src/main/kotlin/dev/lui/LuiNodeView.kt b/platform/android/lui/src/main/kotlin/dev/lui/LuiNodeView.kt index 7b36e950..251fd6b9 100644 --- a/platform/android/lui/src/main/kotlin/dev/lui/LuiNodeView.kt +++ b/platform/android/lui/src/main/kotlin/dev/lui/LuiNodeView.kt @@ -264,10 +264,11 @@ private fun emitValueChange(backend: LuiBackend, node: LuiNodeLike, id: Long, va /** Applies `grow` to a child inside a Row. */ @Composable -private fun RowScope.GrowChildRow(backend: LuiBackend, childId: Long) { +private fun RowScope.GrowChildRow(backend: LuiBackend, childId: Long, stretch: Boolean = false) { val grow = backend.node(childId)?.prop("grow")?.numberValue ?: 0.0 - if (grow > 0) { - Box(Modifier.weight(grow.toFloat().coerceAtLeast(0.001f)), propagateMinConstraints = true) { + if (grow > 0 || stretch) { + val sizing = if (grow > 0) Modifier.weight(grow.toFloat().coerceAtLeast(0.001f)) else Modifier + Box(if (stretch) sizing.fillMaxHeight() else sizing, propagateMinConstraints = true) { LuiNodeView(backend, childId) } } else { @@ -309,7 +310,9 @@ private fun LuiNodeContent( LuiNodeKind.root -> children.firstOrNull()?.let { LuiNodeView(backend, it) } LuiNodeKind.row -> Row( - modifier = if (node.propString("main") == "space_between") modifier.fillMaxWidth() else modifier, + modifier = modifier + .then(if (node.propString("main") == "space_between") Modifier.fillMaxWidth() else Modifier) + .then(if (node.propString("cross") == "stretch") Modifier.height(IntrinsicSize.Min) else Modifier), horizontalArrangement = if (node.propString("main") == "space_between") Arrangement.SpaceBetween else Arrangement.spacedBy( gapOf(node).dp, alignment = mainHorizontalAlignment(node.propString("main")), @@ -320,7 +323,7 @@ private fun LuiNodeContent( else -> Alignment.CenterVertically }, ) { - children.forEachNode { childId -> GrowChildRow(backend, childId) } + children.forEachNode { childId -> GrowChildRow(backend, childId, node.propString("cross") == "stretch") } } LuiNodeKind.column, @@ -1487,6 +1490,7 @@ private fun LuiMenuTrigger(backend: LuiBackend, node: LuiNodeLike, id: Long, mod val iconSize = if (node.text().isEmpty()) 24.dp else 16.dp var open by remember(id) { mutableStateOf(false) } val triggerForeground = luiThemeColor(node.propString("foreground"), foreground = true) + val labelAlignment = node.propString("text-alignment") Box { if (icon != null && node.text().isEmpty()) { @@ -1509,20 +1513,31 @@ private fun LuiMenuTrigger(backend: LuiBackend, node: LuiNodeLike, id: Long, mod onClick = { open = !open }, shapes = ButtonDefaults.shapes(), enabled = enabled, - modifier = modifier, + modifier = if (labelAlignment != null) modifier.width(IntrinsicSize.Max) else modifier, + contentPadding = if (labelAlignment != null) androidx.compose.foundation.layout.PaddingValues(0.dp) + else ButtonDefaults.TextButtonContentPadding, colors = if (triggerForeground == null) androidx.compose.material3.ButtonDefaults.textButtonColors() else androidx.compose.material3.ButtonDefaults.textButtonColors(contentColor = triggerForeground), ) { - if (icon != null) { - Row( - horizontalArrangement = Arrangement.spacedBy(4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon(icon, null, Modifier.size(iconSize)) - if (node.text().isNotEmpty()) Text(node.text()) + Box( + modifier = if (labelAlignment != null) Modifier.fillMaxWidth() else Modifier, + contentAlignment = when (labelAlignment) { + "start" -> Alignment.CenterStart + "end" -> Alignment.CenterEnd + else -> Alignment.Center + }, + ) { + if (icon != null) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon(icon, null, Modifier.size(iconSize)) + if (node.text().isNotEmpty()) Text(node.text()) + } + } else { + Text(node.text()) } - } else { - Text(node.text()) } } } diff --git a/platform/android/lui/src/main/kotlin/dev/lui/LuiStore.kt b/platform/android/lui/src/main/kotlin/dev/lui/LuiStore.kt index 395cee84..9927fc73 100644 --- a/platform/android/lui/src/main/kotlin/dev/lui/LuiStore.kt +++ b/platform/android/lui/src/main/kotlin/dev/lui/LuiStore.kt @@ -409,6 +409,7 @@ class LuiRetainedTree(private val registry: LuiExtensionRegistry) { throw LuiBackendException("menu-item accepts only nested menu metadata") } if (parent.kind != LuiNodeKind.menuItem && + !LuiKindRules.isButtonKind(parent.kind) && LuiKindRules.isContextMenuLeafHost(parent.kind) && child.kind != LuiNodeKind.contextMenu ) { diff --git a/platform/android/lui/src/main/kotlin/dev/lui/LuiWireSchema.kt b/platform/android/lui/src/main/kotlin/dev/lui/LuiWireSchema.kt index 55bbefb5..c58cf6e0 100644 --- a/platform/android/lui/src/main/kotlin/dev/lui/LuiWireSchema.kt +++ b/platform/android/lui/src/main/kotlin/dev/lui/LuiWireSchema.kt @@ -314,7 +314,7 @@ object LuiSchemaMatrix { LuiNodeKind.toolbar to setOf(LuiProperty.height, LuiProperty.padding, LuiProperty.background, LuiProperty.borderWidth, LuiProperty.cornerRadius, LuiProperty.orientation, LuiProperty.accessibilityLabel, LuiProperty.gap, LuiProperty.styleClass, LuiProperty.dataAttrs, LuiProperty.placement, LuiProperty.pointerEnabled), LuiNodeKind.bottomTabs to setOf(LuiProperty.accessibilityLabel, LuiProperty.styleClass, LuiProperty.dataAttrs, LuiProperty.grow, LuiProperty.width, LuiProperty.height, LuiProperty.minWidth, LuiProperty.maxWidth, LuiProperty.minHeight, LuiProperty.maxHeight, LuiProperty.pointerEnabled), LuiNodeKind.bottomTab to setOf(LuiProperty.title, LuiProperty.icon, LuiProperty.selected, LuiProperty.enabled, LuiProperty.pressEnabled, LuiProperty.pointerEnabled), - LuiNodeKind.menuTrigger to setOf(LuiProperty.text, LuiProperty.icon, LuiProperty.accessibilityLabel, LuiProperty.enabled, LuiProperty.foreground, LuiProperty.styleClass, LuiProperty.dataAttrs, LuiProperty.pointerEnabled), + LuiNodeKind.menuTrigger to setOf(LuiProperty.text, LuiProperty.textAlignment, LuiProperty.icon, LuiProperty.accessibilityLabel, LuiProperty.enabled, LuiProperty.foreground, LuiProperty.styleClass, LuiProperty.dataAttrs, LuiProperty.pointerEnabled), LuiNodeKind.listSection to setOf(LuiProperty.key, LuiProperty.separator, LuiProperty.pointerEnabled), LuiNodeKind.swipeActions to setOf(), LuiNodeKind.swipeAction to setOf(LuiProperty.text, LuiProperty.icon, LuiProperty.variant, LuiProperty.edge, LuiProperty.enabled, LuiProperty.background, LuiProperty.pressEnabled, LuiProperty.pointerEnabled), diff --git a/platform/android/lui/src/test/kotlin/dev/lui/LuiRowStretchTest.kt b/platform/android/lui/src/test/kotlin/dev/lui/LuiRowStretchTest.kt new file mode 100644 index 00000000..ebf6e5b1 --- /dev/null +++ b/platform/android/lui/src/test/kotlin/dev/lui/LuiRowStretchTest.kt @@ -0,0 +1,36 @@ +package dev.lui + +import app.cash.paparazzi.DeviceConfig +import app.cash.paparazzi.Paparazzi +import org.junit.Rule +import org.junit.Test + +class LuiRowStretchTest { + @get:Rule val paparazzi = Paparazzi(deviceConfig = DeviceConfig.PIXEL_5.copy(softButtons = false)) + + @Test fun tableDividersAndGrowingCellsMatchRowHeight() { + val backend = LuiBackend() + check(backend.applyBatch("""{"generation":1,"ops":[ + {"op":"create-node","id":1,"kind":"root"}, + {"op":"create-node","id":2,"kind":"row"}, + {"op":"insert-child","parent":1,"child":2,"index":0}, + {"op":"set-prop","id":2,"property":"cross","value":"stretch"}, + {"op":"create-node","id":3,"kind":"text"}, + {"op":"insert-child","parent":2,"child":3,"index":0}, + {"op":"set-prop","id":3,"property":"grow","value":1}, + {"op":"set-prop","id":3,"property":"min-height","value":48}, + {"op":"set-prop","id":3,"property":"text","value":"Name"}, + {"op":"set-prop","id":3,"property":"background","value":"secondary"}, + {"op":"create-node","id":4,"kind":"divider"}, + {"op":"insert-child","parent":2,"child":4,"index":1}, + {"op":"set-prop","id":4,"property":"orientation","value":"vertical"}, + {"op":"create-node","id":5,"kind":"text"}, + {"op":"insert-child","parent":2,"child":5,"index":2}, + {"op":"set-prop","id":5,"property":"width","value":170}, + {"op":"set-prop","id":5,"property":"height","value":96}, + {"op":"set-prop","id":5,"property":"text","value":"A taller value cell"}, + {"op":"set-prop","id":5,"property":"background","value":"secondary"} + ]}""")) { backend.lastError.orEmpty() } + paparazzi.snapshot(name = "stretched-table-row") { LuiTheme { backend.Content() } } + } +} diff --git a/platform/android/lui/src/test/kotlin/dev/lui/LuiStoreTest.kt b/platform/android/lui/src/test/kotlin/dev/lui/LuiStoreTest.kt index 25110237..d1795b33 100644 --- a/platform/android/lui/src/test/kotlin/dev/lui/LuiStoreTest.kt +++ b/platform/android/lui/src/test/kotlin/dev/lui/LuiStoreTest.kt @@ -14,6 +14,58 @@ class LuiStoreTest { private fun batch(generation: Int, vararg ops: LuiPatchOp) = LuiPatchBatch(generation, ops.toList()) + @Test + fun `native menu accepts semantic label alignments and rejects invalid values`() { + fun apply(value: LuiWireValue) { + tree().apply(batch(1, + LuiPatchOp.CreateNode(1L, LuiNodeKind.root), + LuiPatchOp.CreateNode(2L, LuiNodeKind.menuTrigger), + LuiPatchOp.InsertChild(1L, 2L, 0), + LuiPatchOp.SetProp(2L, "text", LuiWireValue.Str("All")), + LuiPatchOp.SetProp(2L, "text-alignment", value), + )) + } + for (alignment in listOf("start", "center", "end")) apply(LuiWireValue.Str(alignment)) + assertThrows(LuiBackendException::class.java) { apply(LuiWireValue.Str("left")) } + assertThrows(LuiBackendException::class.java) { apply(LuiWireValue.IntValue(1)) } + } + + @Test + fun `buttons retain custom labels alongside context menu metadata`() { + for (kind in listOf(LuiNodeKind.button, LuiNodeKind.toggleButton)) { + val tree = tree() + tree.apply(batch(1, + LuiPatchOp.CreateNode(1L, LuiNodeKind.root), + LuiPatchOp.CreateNode(2L, kind), + LuiPatchOp.CreateNode(3L, LuiNodeKind.row), + LuiPatchOp.CreateNode(4L, LuiNodeKind.icon), + LuiPatchOp.CreateNode(5L, LuiNodeKind.text), + LuiPatchOp.CreateNode(6L, LuiNodeKind.contextMenu), + LuiPatchOp.InsertChild(1L, 2L, 0), + LuiPatchOp.InsertChild(2L, 3L, 0), + LuiPatchOp.InsertChild(3L, 4L, 0), + LuiPatchOp.InsertChild(3L, 5L, 1), + LuiPatchOp.InsertChild(2L, 6L, 1), + LuiPatchOp.SetProp(4L, "name", LuiWireValue.Str("app:disclosure-right")), + LuiPatchOp.SetProp(5L, "text", LuiWireValue.Str("Results")), + )) + assertEquals(listOf(3L, 6L), tree.nodes[2L]!!.children) + } + } + + @Test + fun `text fields still reject custom container children`() { + assertThrows(LuiBackendException::class.java) { + tree().apply(batch(1, + LuiPatchOp.CreateNode(1L, LuiNodeKind.root), + LuiPatchOp.CreateNode(2L, LuiNodeKind.textField), + LuiPatchOp.CreateNode(3L, LuiNodeKind.row), + LuiPatchOp.InsertChild(1L, 2L, 0), + LuiPatchOp.InsertChild(2L, 3L, 0), + )) + } + } + @Test fun `applies create insert set-prop and drop in order`() { val tree = tree() diff --git a/platform/android/lui/src/test/snapshots/images/dev.lui_LuiRowStretchTest_tableDividersAndGrowingCellsMatchRowHeight_stretched-table-row.png b/platform/android/lui/src/test/snapshots/images/dev.lui_LuiRowStretchTest_tableDividersAndGrowingCellsMatchRowHeight_stretched-table-row.png new file mode 100644 index 00000000..2c6c3783 Binary files /dev/null and b/platform/android/lui/src/test/snapshots/images/dev.lui_LuiRowStretchTest_tableDividersAndGrowingCellsMatchRowHeight_stretched-table-row.png differ diff --git a/platform/apple/Sources/LUIAppleBackend/LUIDrawerView.swift b/platform/apple/Sources/LUIAppleBackend/LUIDrawerView.swift index 49c83eac..212c57dd 100644 --- a/platform/apple/Sources/LUIAppleBackend/LUIDrawerView.swift +++ b/platform/apple/Sources/LUIAppleBackend/LUIDrawerView.swift @@ -9,7 +9,7 @@ enum LUIDrawerScrollInteractionPolicy { var candidate = view while let current = candidate { if let scroll = current as? UIScrollView, scroll.isScrollEnabled, - scroll.alwaysBounceHorizontal || scroll.contentSize.width + scroll.adjustedContentInset.left + scroll.contentSize.width + scroll.adjustedContentInset.left + scroll.adjustedContentInset.right > scroll.bounds.width + 1 { return true } diff --git a/platform/apple/Sources/LUIAppleBackend/LUISwiftUIRoot.swift b/platform/apple/Sources/LUIAppleBackend/LUISwiftUIRoot.swift index 5be63be2..28ce39c2 100644 --- a/platform/apple/Sources/LUIAppleBackend/LUISwiftUIRoot.swift +++ b/platform/apple/Sources/LUIAppleBackend/LUISwiftUIRoot.swift @@ -4572,6 +4572,12 @@ private struct LUIMenuTriggerView: View { return fixed == nil && maximum == nil ? 44 : nil } + private var labelAlignment: Alignment { + LUIButtonVisualPolicy.labelAlignment( + textAlignment: model.property(.textAlignment)?.stringValue ?? "center" + ) + } + var body: some View { #if os(iOS) if inHoistedToolbar { @@ -4616,12 +4622,14 @@ private struct LUIMenuTriggerView: View { .padding(.horizontal, capsuleCell && !iconOnly ? 12 : 0) .frame( maxWidth: capsuleCell && iconOnly ? .infinity : nil, - maxHeight: capsuleCell ? .infinity : nil + maxHeight: capsuleCell ? .infinity : nil, + alignment: labelAlignment ) #if os(iOS) .frame( width: sizingCell?.surfaceWidth.map(CGFloat.init), - height: sizingCell?.surfaceHeight.map(CGFloat.init) + height: sizingCell?.surfaceHeight.map(CGFloat.init), + alignment: labelAlignment ) .frame( minWidth: minimumExtent( @@ -4635,7 +4643,8 @@ private struct LUIMenuTriggerView: View { minimum: sizingCell?.surfaceMinHeight, maximum: sizingCell?.surfaceMaxHeight ), - maxHeight: sizingCell?.surfaceMaxHeight.map(CGFloat.init) + maxHeight: sizingCell?.surfaceMaxHeight.map(CGFloat.init), + alignment: labelAlignment ) #endif .contentShape(Rectangle()) @@ -6976,39 +6985,70 @@ private struct LUIVirtualListView: View { @State private var titleTracker = LUIScrollSectionTitleTracker() @State private var windowTracker = LUIVirtualWindowTracker() @State private var firstSectionHeights: [SlotID: CGFloat] = [:] + @State private var rowFrames = LUIVirtualRowFrames() + @State private var pinnedFrame: PinnedFrame? + @State private var pinnedSize: PinnedSize? + + private struct ContentSpace: Hashable { let nodeID: Int } + private struct PinnedFrame: Equatable { let id: SlotID; let frame: CGRect } + private struct PinnedSize: Equatable { let id: SlotID; let height: CGFloat } var body: some View { + let rows = sectionRows + let editor = rows.editor + let contentSpace = ContentSpace(nodeID: model.id) ScrollViewReader { proxy in ScrollView { LazyVStack( alignment: .leading, spacing: CGFloat(model.property(.gap)?.intValue ?? 0) ) { - let rows = sectionRows ForEach(LUIVirtualRowIndices(count: rows.endIndex, editor: rows.editor), id: \.id) { slot in // A concrete, single-view root keeps SwiftUI from asking // every virtual slot for its conditional view-list count. VStack(alignment: .leading, spacing: 0) { let row = rows[slot.position] - let rowView = LUIVirtualListRow(row: row, backend: backend, - tracker: windowTracker, coordinateSpace: titleCoordinateSpace, - viewportHeight: unobscuredHeight ?? 600, - onMeasure: { id, height in - if firstSectionHeights[id] != height { firstSectionHeights[id] = height } - }) - if tracksSectionTitles { - rowView.modifier(LUIScrollSectionFrameModifier( - index: row.index, title: row.title, - coordinateSpace: titleCoordinateSpace, - onVisibilityChange: { index, title in - titleTracker.update(index: index, title: title, startsSection: row.startsSection) - } - )) - } else { rowView } + if slot.position == editor?.position { + Color.clear + .frame(height: pinnedSize?.id == row.id ? pinnedSize!.height + : rowFrames.frames[row.id]?.height ?? row.estimatedHeight) + } else { virtualRow(row) } + } + .onGeometryChange(for: CGRect.self) { geometry in + geometry.frame(in: .named(contentSpace)) + } action: { frame in + let id = rows[slot.position].id + rowFrames.frames[id] = frame + if slot.position == editor?.position { + let next = PinnedFrame(id: id, frame: frame) + if pinnedFrame != next { pinnedFrame = next } + } } .id(slot.id) } } + .coordinateSpace(name: contentSpace) + .overlay(alignment: .topLeading) { + if let editor { + let row = rows[editor.position] + let frame = pinnedFrame?.id == row.id ? pinnedFrame?.frame : rowFrames.frames[row.id] + if row.nodeID != nil, let frame { + // A focused native control cannot live inside a lazy + // slot: offscreen eviction removes it from the window + // and resigns first responder. Keep one real row in + // this persistent overlay, reserving its slot above. + virtualRow(row) + .fixedSize(horizontal: false, vertical: true) + .onGeometryChange(for: PinnedSize.self) { + PinnedSize(id: row.id, height: $0.size.height) + } action: { size in + if pinnedSize != size { pinnedSize = size } + } + .offset(y: frame.minY) + .id(editor.source) + } + } + } .environment(\.luiUnobscuredScrollHeight, unobscuredHeight) } .onAppear { revealTarget(using: proxy) } @@ -7034,6 +7074,24 @@ private struct LUIVirtualListView: View { } } + @ViewBuilder private func virtualRow(_ row: SectionRow) -> some View { + let content = LUIVirtualListRow(row: row, backend: backend, + tracker: windowTracker, coordinateSpace: titleCoordinateSpace, + viewportHeight: unobscuredHeight ?? 600, + onMeasure: { id, height in + if firstSectionHeights[id] != height { firstSectionHeights[id] = height } + }) + if tracksSectionTitles { + content.modifier(LUIScrollSectionFrameModifier( + index: row.index, title: row.title, + coordinateSpace: titleCoordinateSpace, + onVisibilityChange: { index, title in + titleTracker.update(index: index, title: title, startsSection: row.startsSection) + } + )) + } else { content } + } + private struct ScrollRequest: Equatable { let key: String let position: Int @@ -7126,7 +7184,15 @@ private struct LUIVirtualListView: View { estimatedHeight: segment.padding, tracksSectionHeight: false) } if segment.padding > 0, offset > window.firstSectionCount { offset -= 1 } - let childOffset = offset - window.start + let childOffset: Int + if offset >= window.start, offset < window.start + window.length { + childOffset = offset - window.start + } else if offset == window.pinnedIndex { + // The focused row stays mounted after the bounded window. + childOffset = window.length + } else { + childOffset = -1 + } return SectionRow(id: SlotID(source: segment.source, offset: offset), nodeID: segment.children.indices.contains(childOffset) ? segment.children[childOffset] : nil, windowID: segment.source, windowIndex: offset, index: index, @@ -7175,12 +7241,17 @@ private struct LUIVirtualListView: View { } +private final class LUIVirtualRowFrames { + var frames: [LUIVirtualListView.SlotID: CGRect] = [:] +} + // Window sources expose their total size independently of their mounted // children. Stable slot identities let LazyVStack retain measured heights // while distant OCaml scopes and native node models are evicted. fileprivate struct LUIVirtualWindow { let count: Int let start: Int + let length: Int let extent: CGFloat let firstSectionCount: Int let firstSectionInset: CGFloat @@ -7215,6 +7286,8 @@ fileprivate struct LUIVirtualWindow { }.sorted { $0.index < $1.index } self.count = count self.start = start + length = min(max(0, count - start), max(0, + attributes["data-virtual-length"].flatMap(Int.init) ?? (count - start))) let pinned = attributes["data-pinned-index"].flatMap(Int.init) ?? -1 pinnedIndex = (0.. property = TextValue || property = InlineIconName + || property = TextAlignment || property = AccessibilityLabel || property = Enabled || property = ForegroundValue || property = StyleClass || property = PointerEnabled || property = DataAttrs diff --git a/src/lui_wire_schema.ml b/src/lui_wire_schema.ml index 06f9c963..b5fe7402 100644 --- a/src/lui_wire_schema.ml +++ b/src/lui_wire_schema.ml @@ -388,7 +388,7 @@ let kind_property_matrix kind = | Toolbar -> Some [ HeightValue; PaddingValue; BackgroundValue; BorderWidth; CornerRadius; OrientationValue; AccessibilityLabel; Gap; StyleClass; DataAttrs; PlacementValue; PointerEnabled ] | BottomTabs -> Some [ AccessibilityLabel; StyleClass; DataAttrs; GrowValue; WidthValue; HeightValue; MinWidth; MaxWidth; MinHeight; MaxHeight; PointerEnabled ] | BottomTab -> Some [ TitleValue; InlineIconName; Selected; Enabled; PressEnabled; PointerEnabled ] - | MenuTrigger -> Some [ TextValue; InlineIconName; AccessibilityLabel; Enabled; ForegroundValue; StyleClass; DataAttrs; PointerEnabled ] + | MenuTrigger -> Some [ TextValue; TextAlignment; InlineIconName; AccessibilityLabel; Enabled; ForegroundValue; StyleClass; DataAttrs; PointerEnabled ] | ListSection -> Some [ KeyValue; SeparatorValue; PointerEnabled ] | SwipeActions -> Some [ ] | SwipeAction -> Some [ TextValue; InlineIconName; VariantValue; EdgeValue; Enabled; BackgroundValue; PressEnabled; PointerEnabled ] diff --git a/test/test_lui.ml b/test/test_lui.ml index 00ee4670..31531614 100644 --- a/test/test_lui.ml +++ b/test/test_lui.ml @@ -1875,6 +1875,8 @@ let test_menu_trigger_rules () = (property_supported MenuTrigger TextValue); Alcotest.(check bool) "trigger allows icon" true (property_supported MenuTrigger InlineIconName); + Alcotest.(check bool) "trigger allows explicit text alignment" true + (property_supported MenuTrigger TextAlignment); Alcotest.(check bool) "trigger allows label" true (property_supported MenuTrigger AccessibilityLabel); Alcotest.(check bool) "trigger drops width" false