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
1 change: 1 addition & 0 deletions compose-rust/android-kotlin/shared/ComposeRustHost.kt
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,7 @@ private fun ComposeRustTree(
chainOf = { asset -> (host.table.assets.asset(asset) as? Asset.Font)?.chain },
brushOf = { asset -> (host.table.assets.asset(asset) as? Asset.Brush)?.brush },
windowBackdrop = asked && platformBacksWindowWithMaterial(),
overlay = host.table.overlayTokens,
)
// The animations the Host handed over are played here, on this side's frame clock, and
// the Host is not called while they play unless one asked for events. The loop waits
Expand Down
9 changes: 9 additions & 0 deletions compose-rust/android-kotlin/shared/DesignSystem.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import dev.darkpyonix.composerust.protocol.DesignTokens
import androidx.compose.animation.core.FiniteAnimationSpec
import dev.darkpyonix.composerust.protocol.MaterialRole
import dev.darkpyonix.composerust.protocol.MotionRole
import dev.darkpyonix.composerust.protocol.OverlayTokens
import dev.darkpyonix.composerust.protocol.Paint
import dev.darkpyonix.composerust.protocol.ShapeRole
import dev.darkpyonix.composerust.protocol.SpaceRole
Expand Down Expand Up @@ -180,6 +181,11 @@ class ResolvedTheme(
* given is enough to find the rest of the stack: see [chainFor].
*/
val fontChains: List<FontChain> = emptyList(),

/**
* Application-level overlay theme tokens for Dialog, Menu, Dropdown and Sheet.
*/
val overlay: OverlayTokens? = null,
) {
/**
* The stack [family] is the first family of, or null where it is not one.
Expand Down Expand Up @@ -1638,6 +1644,8 @@ fun resolveTheme(
* family lacks in the families after it.
*/
chainOf: (Int) -> FontChain? = { null },
/** Application-level overlay theme tokens for Dialog, Menu, Dropdown and Sheet. */
overlay: OverlayTokens? = null,
): ResolvedTheme {
val system = when {
theme == null -> adaptiveSystem(platform, DesignSystem.Material3)
Expand Down Expand Up @@ -1684,6 +1692,7 @@ fun resolveTheme(
platform,
application,
chains,
overlay,
)
}

Expand Down
50 changes: 46 additions & 4 deletions compose-rust/android-kotlin/shared/HostContainer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import dev.darkpyonix.composerust.protocol.TypeRole
import dev.darkpyonix.composerust.ui.textStyle
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
Expand Down Expand Up @@ -45,12 +46,20 @@ import dev.darkpyonix.composerust.ui.resolvedShape
import dev.darkpyonix.composerust.ui.weightOf
import dev.darkpyonix.composerust.design.liftsOffThePage

/** True when the Host set a shape modifier of its own, which replaces the default shape. */
internal fun Node.setsOwnShape(): Boolean =
modifiers.any {
it is ProtocolModifier.Shape ||
it is ProtocolModifier.ShapeRole ||
it is ProtocolModifier.CornerEach
}

/**
* The corner this container is drawn with: the one the Host asked for if it asked, and the
* design system's corner for this kind of container otherwise.
*/
internal fun Node.containerShape(style: ContainerStyle, theme: ResolvedTheme): Shape =
if (modifiers.any { it is ProtocolModifier.Shape || it is ProtocolModifier.ShapeRole || it is ProtocolModifier.CornerEach }) {
if (setsOwnShape()) {
modifiers.resolvedShape(theme)
} else {
style.shape
Expand Down Expand Up @@ -98,12 +107,34 @@ internal fun Modifier.containerDecoration(
theme: ResolvedTheme,
/** True for a menu, a dialog or a tooltip: a sheet over the page rather than in it. */
overlay: Boolean = false,
role: ContainerRole? = null,
): Modifier {
val shape = node.containerShape(style, theme)
val raised = if (node.setsOwnElevation()) {
val shape = if (node.setsOwnShape()) {
node.containerShape(style, theme)
} else if (overlay) {
when (role) {
ContainerRole.Dialog -> theme.overlay?.dialogCornerRadius?.takeIf { it > 0 }?.let { RoundedCornerShape(it.dp) }
ContainerRole.Menu -> theme.overlay?.popupCornerRadius?.takeIf { it > 0 }?.let { RoundedCornerShape(it.dp) }
else -> null
} ?: style.shape
} else {
style.shape
}
val effectiveElevation = if (node.setsOwnElevation()) {
0.dp
} else if (overlay) {
when (role) {
ContainerRole.Dialog -> theme.overlay?.dialogElevation?.takeIf { it > 0 }?.let { it.dp }
ContainerRole.Menu -> theme.overlay?.popupElevation?.takeIf { it > 0 }?.let { it.dp }
else -> null
} ?: style.elevation
} else {
style.elevation
}
val raised = if (node.setsOwnElevation() || effectiveElevation.value <= 0f) {
this
} else {
theme.rules.elevation(this, style.elevation, shape, theme)
theme.rules.elevation(this, effectiveElevation, shape, theme)
}
val material = style.material
// A Host that named its own Background has already had it applied, so painting the
Expand Down Expand Up @@ -139,6 +170,17 @@ internal fun Modifier.containerDecoration(
.then(
if (node.setsOwnPadding()) {
Modifier
} else if (overlay) {
val customPadding = when (role) {
ContainerRole.Dialog -> theme.overlay?.dialogPadding?.takeIf { it > 0 }?.let { it.dp }
ContainerRole.Menu -> theme.overlay?.popupPadding?.takeIf { it > 0 }?.let { it.dp }
else -> null
}
if (customPadding != null) {
Modifier.padding(customPadding)
} else {
Modifier.padding(horizontal = style.horizontalPadding, vertical = style.verticalPadding)
}
} else {
Modifier.padding(horizontal = style.horizontalPadding, vertical = style.verticalPadding)
},
Expand Down
32 changes: 28 additions & 4 deletions compose-rust/android-kotlin/shared/HostDropdown.kt
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,29 @@ private fun OpeningList(
)
}
if (open) {
val popupShape = if (node.setsOwnShape()) {
node.containerShape(style, theme)
} else {
theme.overlay?.popupCornerRadius?.takeIf { it > 0 }?.let { androidx.compose.foundation.shape.RoundedCornerShape(it.dp) } ?: style.shape
}
val popupElevation = if (node.setsOwnElevation()) {
0.dp
} else {
theme.overlay?.popupElevation?.takeIf { it > 0 }?.let { it.dp } ?: style.elevation
}
val popupRaised = if (node.setsOwnElevation() || popupElevation.value <= 0f) {
Modifier
} else {
theme.rules.elevation(Modifier, popupElevation, popupShape, theme)
}
val explicitPopupPad = theme.overlay?.popupPadding
val popupPadding = if (node.setsOwnPadding()) {
Modifier
} else if (explicitPopupPad != null && explicitPopupPad > 0) {
Modifier.padding(explicitPopupPad.dp)
} else {
Modifier.padding(vertical = theme.space(SpaceRole.Xs))
}
Popup(
popupPositionProvider = BelowAnchorPositionProvider,
onDismissRequest = { open = false },
Expand All @@ -205,10 +228,11 @@ private fun OpeningList(
Column(
Modifier
.testTag(dropdownPopupTestTag(node.id))
.clip(style.shape)
.background(style.container, style.shape)
.border(style.borderWidth, style.borderColor, style.shape)
.padding(vertical = theme.space(SpaceRole.Xs)),
.then(popupRaised)
.clip(popupShape)
.background(style.container, popupShape)
.border(style.borderWidth, style.borderColor, popupShape)
.then(popupPadding),
) {
options.forEachIndexed { index, childId ->
key(childId) {
Expand Down
11 changes: 7 additions & 4 deletions compose-rust/android-kotlin/shared/HostOverlay.kt
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import dev.darkpyonix.composerust.ui.node.Node
import dev.darkpyonix.composerust.ui.node.NodeTable
import dev.darkpyonix.composerust.ui.node.RenderNode
import dev.darkpyonix.composerust.ui.node.nodeTestTag
import dev.darkpyonix.composerust.ui.paintProp
import dev.darkpyonix.composerust.ui.textStyle
import dev.darkpyonix.composerust.design.LocalGlassBackdrop
import androidx.compose.runtime.CompositionLocalProvider
Expand Down Expand Up @@ -194,6 +195,9 @@ internal fun HostDialog(
val open = rememberOverlayOpen(node)
if (!open.value) return
val style = theme.rules.container(ContainerRole.Dialog, theme)
val scrim = node.paintProp(PropertyKind.Scrim)?.let { theme.color(it) }
?: theme.overlay?.scrim?.takeIf { it != 0L }?.let { androidx.compose.ui.graphics.Color(it.toULong()) }
?: style.scrim
val surfaceRequester = remember(node.id) { FocusRequester() }
val onEscape = { dismiss(node, dispatcher) }
Popup(
Expand All @@ -206,14 +210,13 @@ internal fun HostDialog(
) {
ModalOverlayFocus(node.id, surfaceRequester, onEscape)
Box(
Modifier.fillMaxSize().background(style.scrim),
Modifier.fillMaxSize().background(scrim).testTag(dialogTestTag(node.id)),
contentAlignment = Alignment.Center,
) {
Column(
modifier
.testTag(dialogTestTag(node.id))
.modalOverlaySurface(node.id, surfaceRequester)
.containerDecoration(node, style, theme, overlay = true),
.containerDecoration(node, style, theme, overlay = true, role = ContainerRole.Dialog),
) {
node.children.forEach { childId ->
key(childId) { RenderNode(childId, table, dispatcher) }
Expand Down Expand Up @@ -265,7 +268,7 @@ internal fun HostMenu(
Column(
Modifier
.testTag(menuPopupTestTag(node.id))
.containerDecoration(node, style, theme, overlay = true),
.containerDecoration(node, style, theme, overlay = true, role = ContainerRole.Menu),
) {
entries.forEach { childId ->
key(childId) { RenderNode(childId, table, dispatcher) }
Expand Down
75 changes: 66 additions & 9 deletions compose-rust/android-kotlin/shared/HostSheet.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,15 @@ import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties
import dev.darkpyonix.composerust.design.ResolvedTheme
import dev.darkpyonix.composerust.design.SheetEdge
import dev.darkpyonix.composerust.protocol.PropertyKind
import dev.darkpyonix.composerust.protocol.ShapeRole
import dev.darkpyonix.composerust.runtime.EventDispatcher
import dev.darkpyonix.composerust.runtime.LocalWindowSizeClass
import dev.darkpyonix.composerust.ui.node.Node
import dev.darkpyonix.composerust.ui.node.NodeTable
import dev.darkpyonix.composerust.ui.node.RenderNode
import dev.darkpyonix.composerust.ui.node.nodeTestTag
import dev.darkpyonix.composerust.ui.paintProp
import kotlin.math.roundToInt

/** How far a sheet has to be pulled towards its edge before letting go closes it. */
Expand Down Expand Up @@ -84,6 +86,65 @@ internal fun HostSheet(
val surfaceRequester = remember(node.id) { FocusRequester() }
val onEscape = { dismiss(node, dispatcher) }

val scrim = node.paintProp(PropertyKind.Scrim)?.let { theme.color(it) }
?: theme.overlay?.scrim?.takeIf { it != 0L }?.let { androidx.compose.ui.graphics.Color(it.toULong()) }
?: style.scrim

val shape = if (node.setsOwnShape()) {
node.containerShape(
dev.darkpyonix.composerust.design.ContainerStyle(
container = style.container,
content = androidx.compose.ui.graphics.Color.Unspecified,
shape = style.shape,
elevation = style.elevation,
borderWidth = style.borderWidth,
borderColor = style.borderColor,
horizontalPadding = 0.dp,
verticalPadding = 0.dp,
separator = null,
scrim = style.scrim,
typeRole = dev.darkpyonix.composerust.protocol.TypeRole.Body,
),
theme,
)
} else {
theme.overlay?.sheetCornerRadius?.takeIf { it > 0 }?.let {
if (bottom) {
androidx.compose.foundation.shape.RoundedCornerShape(it.dp, it.dp, 0.dp, 0.dp)
} else {
androidx.compose.foundation.shape.RoundedCornerShape(it.dp, 0.dp, 0.dp, it.dp)
}
} ?: style.shape
}

val sheetElevation = if (node.setsOwnElevation()) {
0.dp
} else {
theme.overlay?.sheetElevation?.takeIf { it > 0 }?.let { it.dp } ?: style.elevation
}
val raised = if (node.setsOwnElevation() || sheetElevation.value <= 0f) {
Modifier
} else {
theme.rules.elevation(Modifier, sheetElevation, shape, theme)
}

val containerBg = if (node.setsOwnBackground()) Modifier else Modifier.background(style.container)

val outline = if (style.borderWidth.value > 0f) {
Modifier.border(style.borderWidth, style.borderColor, shape)
} else {
Modifier
}

val explicitSheetPad = theme.overlay?.sheetPadding
val sheetPadding = if (node.setsOwnPadding()) {
Modifier
} else if (explicitSheetPad != null && explicitSheetPad > 0) {
Modifier.padding(explicitSheetPad.dp)
} else {
Modifier.padding(style.padding)
}

Popup(
alignment = Alignment.Center,
properties = PopupProperties(focusable = true, dismissOnBackPress = false),
Expand All @@ -94,7 +155,7 @@ internal fun HostSheet(
Modifier
.testTag(sheetScrimTestTag(node.id))
.fillMaxSize()
.background(style.scrim)
.background(scrim)
// Pressing what the sheet covers is the other way of asking to close it,
// and it gets the same answer: one dismissal. A raw tap detector rather
// than `clickable`, because a scrim is not a button and saying it is one
Expand All @@ -107,11 +168,6 @@ internal fun HostSheet(
} else {
Modifier.fillMaxHeight().fillMaxWidth(style.widthFraction)
}
val outline = if (style.borderWidth.value > 0f) {
Modifier.border(style.borderWidth, style.borderColor, style.shape)
} else {
Modifier
}
// Two boxes rather than one: the outer is the sheet itself, which is what is
// dragged and what the design system sized, and the inner carries whatever
// Modifiers the Host put on the node, which belong inside its surface.
Expand All @@ -127,8 +183,9 @@ internal fun HostSheet(
IntOffset(travel.roundToInt(), 0)
}
}
.clip(style.shape)
.background(style.container)
.then(raised)
.clip(shape)
.then(containerBg)
.then(outline)
.draggable(
orientation = if (bottom) Orientation.Vertical else Orientation.Horizontal,
Expand All @@ -143,7 +200,7 @@ internal fun HostSheet(
// swallowed here rather than reaching the scrim behind.
.pointerInput(Unit) { detectTapGestures { } },
) {
Column(modifier.fillMaxWidth().padding(style.padding)) {
Column(modifier.fillMaxWidth().then(sheetPadding)) {
style.handle?.let { handle ->
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
Box(
Expand Down
11 changes: 11 additions & 0 deletions compose-rust/android-kotlin/shared/NodeTable.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import androidx.compose.runtime.setValue
import dev.darkpyonix.composerust.foundation.ChromeInsets
import dev.darkpyonix.composerust.foundation.MessageQueue
import dev.darkpyonix.composerust.protocol.Mutation
import dev.darkpyonix.composerust.protocol.OverlayTokens
import dev.darkpyonix.composerust.protocol.PropertyKind
import dev.darkpyonix.composerust.protocol.PropertyValue
import dev.darkpyonix.composerust.protocol.Theme
Expand Down Expand Up @@ -187,6 +188,10 @@ class NodeTable {
var theme: Theme? by mutableStateOf(null)
private set

/** Null until the Host sends an overlay tokens record. */
var overlayTokens: OverlayTokens? by mutableStateOf(null)
private set

/** Null until the Host sends its first `SetWindow` record. */
var window: Window? by mutableStateOf(null)
private set
Expand Down Expand Up @@ -320,6 +325,7 @@ class NodeTable {
fail(TableError.INVALID_PALETTE, "theme palette: $problem")
}
}
is Mutation.SetOverlayTokens -> overlayTokens = mutation.overlay
// Read before there is a window to apply it to. The platform layer asks for
// this out of the first batch and builds the window from it, so by the time
// the batch is applied the window already looks the way it says. Keeping it
Expand Down Expand Up @@ -974,6 +980,11 @@ class NodeTable {
widget == WidgetKind.Menu ||
// A sheet is an overlay on the same terms as a dialog.
widget == WidgetKind.Sheet

// A custom scrim colour for modal and popup overlays.
PropertyKind.Scrim -> widget == WidgetKind.Dialog ||
widget == WidgetKind.Menu ||
widget == WidgetKind.Sheet
// A Dropdown's selection is a position in its own children, the same thing
// a tab strip's selection is.
PropertyKind.SelectedIndex ->
Expand Down
Loading
Loading