Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 30 additions & 15 deletions platform/android/lui/src/main/kotlin/dev/lui/LuiNodeView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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")),
Expand All @@ -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,
Expand Down Expand Up @@ -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()) {
Expand All @@ -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())
}
}
}
Expand Down
1 change: 1 addition & 0 deletions platform/android/lui/src/main/kotlin/dev/lui/LuiStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
36 changes: 36 additions & 0 deletions platform/android/lui/src/test/kotlin/dev/lui/LuiRowStretchTest.kt
Original file line number Diff line number Diff line change
@@ -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() } }
}
}
52 changes: 52 additions & 0 deletions platform/android/lui/src/test/kotlin/dev/lui/LuiStoreTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion platform/apple/Sources/LUIAppleBackend/LUIDrawerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Loading
Loading