Skip to content
Merged
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
19 changes: 15 additions & 4 deletions platform/android/lui/src/main/kotlin/dev/lui/LuiNodeView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.PrimaryScrollableTabRow
import androidx.compose.material3.Tab
import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.material3.ModalBottomSheet
Expand Down Expand Up @@ -590,8 +591,14 @@ private fun LuiNodeContent(
if (children.isNotEmpty()) {
val selectedIndex = children.indexOfFirst { backend.node(it)?.propBool("selected") == true }
.coerceAtLeast(0)
PrimaryTabRow(selectedTabIndex = selectedIndex, modifier = modifier) {
children.forEachNode { LuiNodeView(backend, it) }
if (node.propString("style-class").orEmpty().split(' ').contains("scrollable")) {
PrimaryScrollableTabRow(selectedTabIndex = selectedIndex, modifier = modifier, edgePadding = 0.dp) {
children.forEachNode { LuiNodeView(backend, it) }
}
} else {
PrimaryTabRow(selectedTabIndex = selectedIndex, modifier = modifier) {
children.forEachNode { LuiNodeView(backend, it) }
}
}
}
}
Expand Down Expand Up @@ -1217,7 +1224,7 @@ private fun LuiTextControl(
}

val fieldModifier = modifier.then(
if (grouped || borderless || formField) Modifier.fillMaxWidth() else Modifier.width(240.dp),
if (grouped || borderless || formField || search) Modifier.fillMaxWidth() else Modifier.width(240.dp),
).focusRequester(focusRequester)
val fieldTextStyle = (if (borderless) MaterialTheme.typography.bodyLarge
else MaterialTheme.typography.bodyMedium).copy(
Expand Down Expand Up @@ -2188,7 +2195,11 @@ private fun LuiModalSurface(backend: LuiBackend, node: LuiNodeLike, id: Long) {
)
LuiNodeKind.sheet -> ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = node.prop("detents")?.stringValue
?.split(",")?.map { it.trim() }?.contains("medium") != true,
),
containerColor = LocalLuiSemanticColors.current["background"] ?: MaterialTheme.colorScheme.surface,
) {
Column(Modifier.fillMaxWidth().heightIn(max = node.prop("height")?.intValue?.dp ?: Dp.Infinity)) {
if (node.text().isNotEmpty()) {
Expand Down
49 changes: 49 additions & 0 deletions platform/android/lui/src/test/kotlin/dev/lui/LuiScreenshotTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -237,4 +237,53 @@ class LuiScreenshotTest {

@Test
fun controlsDark() = controlsScreen(dark = true)

private fun scrollableTabs(dark: Boolean) {
val backend = backendFor("""{"generation":1,"ops":[
{"op":"create-node","id":1,"kind":"root"},
{"op":"create-node","id":2,"kind":"column"},
{"op":"set-prop","id":2,"property":"width","value":220},
{"op":"insert-child","parent":1,"child":2,"index":0},
{"op":"create-node","id":3,"kind":"tabs"},
{"op":"set-prop","id":3,"property":"style-class","value":"scrollable"},
{"op":"insert-child","parent":2,"child":3,"index":0},
{"op":"create-node","id":4,"kind":"button"},
{"op":"set-prop","id":4,"property":"text","value":"List"},
{"op":"set-prop","id":4,"property":"press-enabled","value":true},
{"op":"insert-child","parent":3,"child":4,"index":0},
{"op":"create-node","id":5,"kind":"button"},
{"op":"set-prop","id":5,"property":"text","value":"Table"},
{"op":"set-prop","id":5,"property":"selected","value":true},
{"op":"set-prop","id":5,"property":"press-enabled","value":true},
{"op":"insert-child","parent":3,"child":5,"index":1},
{"op":"create-node","id":6,"kind":"button"},
{"op":"set-prop","id":6,"property":"text","value":"Gallery"},
{"op":"set-prop","id":6,"property":"press-enabled","value":true},
{"op":"insert-child","parent":3,"child":6,"index":2},
{"op":"create-node","id":7,"kind":"button"},
{"op":"set-prop","id":7,"property":"text","value":"Projects"},
{"op":"set-prop","id":7,"property":"press-enabled","value":true},
{"op":"insert-child","parent":3,"child":7,"index":3}
]}""")
snapshot(if (dark) "scrollable-tabs-dark" else "scrollable-tabs-light", backend, dark)
}

@Test fun scrollableTabsLight() = scrollableTabs(dark = false)
@Test fun scrollableTabsDark() = scrollableTabs(dark = true)

private fun searchFieldWidth(dark: Boolean) {
val backend = backendFor("""{"generation":1,"ops":[
{"op":"create-node","id":1,"kind":"root"},
{"op":"create-node","id":2,"kind":"column"},
{"op":"set-prop","id":2,"property":"width","value":320},
{"op":"insert-child","parent":1,"child":2,"index":0},
{"op":"create-node","id":3,"kind":"search-field"},
{"op":"set-prop","id":3,"property":"placeholder","value":"Search values"},
{"op":"insert-child","parent":2,"child":3,"index":0}
]}""")
snapshot(if (dark) "search-width-dark" else "search-width-light", backend, dark)
}

@Test fun searchFieldWidthLight() = searchFieldWidth(dark = false)
@Test fun searchFieldWidthDark() = searchFieldWidth(dark = true)
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 31 additions & 10 deletions platform/apple/Sources/LUIAppleBackend/LUISwiftUIRoot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2849,7 +2849,10 @@ private struct LUIModalSurfaceContent: View {

var body: some View {
surfaceContent
.environment(\.luiSemanticColors, effectiveSemanticColors)
.preferredColorScheme(effectiveColorScheme)
.modifier(LUIModalPresentationStyle(model: model))
.presentationBackground(modalBackground)
.sheet(item: Binding(
get: { backend.modalPresentation.nestedSheets[model.id] },
set: { value in
Expand All @@ -2862,7 +2865,7 @@ private struct LUIModalSurfaceContent: View {
LUIModalSurfaceContent(model: presentation.model, backend: backend)
.luiSheetScope(
semanticColors: effectiveSemanticColors,
colorScheme: colorScheme
colorScheme: effectiveColorScheme
)
}
}
Expand All @@ -2871,18 +2874,31 @@ private struct LUIModalSurfaceContent: View {
/// ancestors never reach it through the environment — walk the chain and
/// apply each ancestor's commit-decoded tokens the same way
/// `LUIThemeScopeModifier` does (outermost-first, inner wins).
private var effectiveSemanticColors: [String: Color] {
private var themeAncestors: [LUINodeModel] {
var ancestors: [LUINodeModel] = []
var cursor = model.parent
while let parentID = cursor,
let parent = backend.model(id: parentID) {
ancestors.append(parent)
cursor = parent.parent
while let parentID = cursor {
if let parent = backend.model(id: parentID) {
ancestors.append(parent)
cursor = parent.parent
} else {
cursor = backend.extensionModel(id: parentID)?.parent
}
}
return ancestors.reversed()
}

private var effectiveColorScheme: ColorScheme {
themeAncestors.reduce(colorScheme) { $1.derived.themeScope.scheme ?? $0 }
}

private var effectiveSemanticColors: [String: Color] {
var colors = semanticColors
for ancestor in ancestors.reversed() {
var scheme = colorScheme
for ancestor in themeAncestors {
scheme = ancestor.derived.themeScope.scheme ?? scheme
colors.merge(
ancestor.derived.themeScope.colors(dark: colorScheme == .dark)
ancestor.derived.themeScope.colors(dark: scheme == .dark)
) { _, new in new }
}
return colors
Expand Down Expand Up @@ -2936,7 +2952,7 @@ private struct LUIModalSurfaceContent: View {
if LUIModalBackgroundPolicy.usesGroupedSystemBackground(
styleClass: model.derived.styleClass
) {
return modalGroupedSystemBackground
return effectiveSemanticColors["background"] ?? modalGroupedSystemBackground
}
return LUIModalBackgroundPolicy.color(
semanticColors: effectiveSemanticColors,
Expand Down Expand Up @@ -6711,6 +6727,7 @@ private struct LUIListView: View {
.modifier(LUIListRowSeparatorModifier(
visibility: backend.model(id: childID)?.separatorVisibility
))
.environment(\.luiIsNativeFormRow, true)
.listRowBackground(semanticColors["surface"])
}
}
Expand Down Expand Up @@ -7447,7 +7464,11 @@ private struct LUITextControlView: View {
.submitLabel(model.property(.submitOnEnter)?.boolValue == true ? .send : .return)
} else if model.kind == .searchField {
#if os(iOS)
iosSearchFieldRow.textFieldStyle(.roundedBorder)
iosSearchFieldRow
.textFieldStyle(.plain)
.padding(.horizontal, 10)
.padding(.vertical, 8)
.background(.quaternary, in: RoundedRectangle(cornerRadius: 10))
#else
HStack(spacing: 6) {
Image(systemName: "magnifyingglass")
Expand Down
4 changes: 2 additions & 2 deletions src/lui_elements.ml
Original file line number Diff line number Diff line change
Expand Up @@ -438,8 +438,8 @@ let dynamic mount : t =
node
| None ->
(* A dynamic rendered as the branch of another dynamic has no anchor of
its own: mount under a transparent stack so it still has a home. *)
let anchor = Lui_ui.stack context in
its own: mount under a transparent box supported by every backend. *)
let anchor = Lui_ui.box context in
Lui_ui.string_property context anchor DisplayValue "contents";
ignore (mount context anchor);
anchor
Expand Down
8 changes: 8 additions & 0 deletions test/test_lui.ml
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,14 @@ let test_dyn_nested_bare () =
flush_app app;
Alcotest.(check bool) "nested dyn mounts text" true
(creates_text (all_ops ()));
let kinds = Hashtbl.create 8 in
List.iter (function
| Lui_protocol.CreateNode (id, kind) -> Hashtbl.replace kinds id kind
| Lui_protocol.SetProp (id, Lui_protocol.DisplayValue, _) ->
Alcotest.(check bool) "transparent anchors support native display properties" true
(List.mem (Hashtbl.find kinds id)
[Lui_protocol.Box; Lui_protocol.Column; Lui_protocol.Row])
| _ -> ()) (all_ops ());
batches := [];
ignore (Lui_app.send app (`Inner "updated"));
flush_app app;
Expand Down
Loading