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
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ fun DecoratedDialogScope.DialogTitleBarImpl(
gradientStartColor: Color = Color.Unspecified,
style: TitleBarStyle = LocalTitleBarStyle.current,
controlButtonsDirection: LayoutDirection = LocalLayoutDirection.current,
layoutPolicy: TitleBarLayoutPolicy = TitleBarLayoutPolicy.Default,
applyTitleBar: (Dp, DecoratedWindowState) -> PaddingValues,
onPlace: (() -> Unit)? = null,
backgroundContent: @Composable () -> Unit = {},
Expand All @@ -30,10 +31,36 @@ fun DecoratedDialogScope.DialogTitleBarImpl(
gradientStartColor = gradientStartColor,
style = style,
controlButtonsDirection = controlButtonsDirection,
layoutPolicy = layoutPolicy,
applyTitleBar = applyTitleBar,
onPlace = onPlace,
backgroundContent = backgroundContent,
) { _ ->
content(dialogState)
}
}

@Suppress("FunctionNaming", "LongParameterList")
@Composable
fun DecoratedDialogScope.DialogTitleBarImpl(
modifier: Modifier = Modifier,
gradientStartColor: Color = Color.Unspecified,
style: TitleBarStyle = LocalTitleBarStyle.current,
controlButtonsDirection: LayoutDirection = LocalLayoutDirection.current,
applyTitleBar: (Dp, DecoratedWindowState) -> PaddingValues,
onPlace: (() -> Unit)? = null,
backgroundContent: @Composable () -> Unit = {},
content: @Composable TitleBarScope.(DecoratedDialogState) -> Unit,
) {
DialogTitleBarImpl(
modifier = modifier,
gradientStartColor = gradientStartColor,
style = style,
controlButtonsDirection = controlButtonsDirection,
layoutPolicy = TitleBarLayoutPolicy.Default,
applyTitleBar = applyTitleBar,
onPlace = onPlace,
backgroundContent = backgroundContent,
content = content,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,30 +24,23 @@ import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasurePolicy
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.ParentDataModifierNode
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.offset
import io.github.kdroidfilter.nucleus.core.runtime.Platform
import io.github.kdroidfilter.nucleus.window.styling.LocalTitleBarStyle
import io.github.kdroidfilter.nucleus.window.styling.TitleBarStyle
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.isActive
import java.awt.Window
import kotlin.math.max

private const val GRADIENT_MIDPOINT = 0.5f

Expand All @@ -70,6 +63,7 @@ fun GenericTitleBarImpl(
gradientStartColor: Color = Color.Unspecified,
style: TitleBarStyle = LocalTitleBarStyle.current,
controlButtonsDirection: LayoutDirection = LocalLayoutDirection.current,
layoutPolicy: TitleBarLayoutPolicy = TitleBarLayoutPolicy.Default,
applyTitleBar: (Dp, DecoratedWindowState) -> PaddingValues,
onPlace: (() -> Unit)? = null,
backgroundContent: @Composable () -> Unit = {},
Expand Down Expand Up @@ -128,16 +122,24 @@ fun GenericTitleBarImpl(
scope.content(state)
}
},
modifier = Modifier.fillMaxSize(),
modifier = Modifier.fillMaxSize().onPlaced { onPlace?.invoke() },
measurePolicy =
rememberTitleBarMeasurePolicy(window, state, applyTitleBar, controlButtonsDirection, onPlace),
rememberTitleBarMeasurePolicy(
window = window,
state = state,
applyTitleBar = applyTitleBar,
controlButtonsDirection = controlButtonsDirection,
layoutPolicy = layoutPolicy,
),
)
}
}

@Suppress("FunctionNaming")
@Suppress("FunctionNaming", "LongParameterList")
@Composable
fun DecoratedWindowScope.TitleBarImpl(
fun GenericTitleBarImpl(
window: Window,
state: DecoratedWindowState,
modifier: Modifier = Modifier,
gradientStartColor: Color = Color.Unspecified,
style: TitleBarStyle = LocalTitleBarStyle.current,
Expand All @@ -154,147 +156,66 @@ fun DecoratedWindowScope.TitleBarImpl(
gradientStartColor = gradientStartColor,
style = style,
controlButtonsDirection = controlButtonsDirection,
layoutPolicy = TitleBarLayoutPolicy.Default,
applyTitleBar = applyTitleBar,
onPlace = onPlace,
backgroundContent = backgroundContent,
content = content,
)
}

class TitleBarMeasurePolicy(
private val window: Window,
private val state: DecoratedWindowState,
private val applyTitleBar: (Dp, DecoratedWindowState) -> PaddingValues,
private val controlButtonsDirection: LayoutDirection,
private val onPlace: (() -> Unit)? = null,
) : MeasurePolicy {
@Suppress("CyclomaticComplexMethod", "LongMethod")
override fun MeasureScope.measure(
measurables: List<Measurable>,
constraints: Constraints,
): MeasureResult {
if (measurables.isEmpty()) {
return layout(width = constraints.minWidth, height = constraints.minHeight) {}
}

var maxSpaceVertically = constraints.minHeight
val contentConstraints = constraints.copy(minWidth = 0, minHeight = 0)

// Two-pass measurement: End items are measured independently so they
// don't reduce the available width for Start/Center items. This keeps
// behaviour consistent with JBR where native caption buttons reserve
// space via padding insets rather than Compose item measurement.
val endMeasurables = mutableListOf<Pair<Measurable, Placeable>>()
val otherMeasurables = mutableListOf<Pair<Measurable, Placeable>>()

// Pass 1 – measure End-aligned items among themselves
var endOccupied = 0
for (it in measurables) {
val alignment = (it.parentData as? TitleBarChildDataNode)?.horizontalAlignment
if (alignment != Alignment.End) continue
val placeable = it.measure(contentConstraints.offset(horizontal = -endOccupied))
endOccupied += placeable.width
maxSpaceVertically = max(maxSpaceVertically, placeable.height)
endMeasurables += it to placeable
}

// Pass 2 – measure non-End items with full available width
var otherOccupied = 0
@Suppress("LoopWithTooManyJumpStatements")
for (it in measurables) {
val alignment = (it.parentData as? TitleBarChildDataNode)?.horizontalAlignment
if (alignment == Alignment.End) continue
val placeable = it.measure(contentConstraints.offset(horizontal = -otherOccupied))
if (constraints.maxWidth < otherOccupied + placeable.width) break
otherOccupied += placeable.width
maxSpaceVertically = max(maxSpaceVertically, placeable.height)
otherMeasurables += it to placeable
}

val measuredPlaceable = endMeasurables + otherMeasurables
val boxHeight = maxSpaceVertically

val contentPadding = applyTitleBar(boxHeight.toDp(), state)

// Use Ltr to get absolute left/right insets.
val leftInset = contentPadding.calculateLeftPadding(LayoutDirection.Ltr).roundToPx()
val rightInset = contentPadding.calculateRightPadding(LayoutDirection.Ltr).roundToPx()

val occupiedSpaceHorizontally = endOccupied + otherOccupied + leftInset + rightInset
val boxWidth = maxOf(constraints.minWidth, occupiedSpaceHorizontally)

return layout(boxWidth, boxHeight) {
onPlace?.invoke()

val placeableGroups =
measuredPlaceable.groupBy { (measurable, _) ->
(measurable.parentData as? TitleBarChildDataNode)?.horizontalAlignment
?: Alignment.CenterHorizontally
}

val contentIsRtl = layoutDirection == LayoutDirection.Rtl
val controlsOnRight = controlButtonsDirection == LayoutDirection.Ltr

// Absolute occupied-space tracking for each side
var leftUsed = leftInset
var rightUsed = rightInset

// End items (control buttons) first β€” they claim the extreme edge
// before Start items, so they always stay at their designated side
// even when content and controls share the same edge.
placeableGroups[Alignment.End]?.forEach { (_, placeable) ->
val y = Alignment.CenterVertically.align(placeable.height, boxHeight)
if (controlsOnRight) {
placeable.place(boxWidth - rightUsed - placeable.width, y)
rightUsed += placeable.width
} else {
placeable.place(leftUsed, y)
leftUsed += placeable.width
}
}

// Start items: leading edge of the content direction
placeableGroups[Alignment.Start]?.forEach { (_, placeable) ->
val y = Alignment.CenterVertically.align(placeable.height, boxHeight)
if (contentIsRtl) {
placeable.place(boxWidth - rightUsed - placeable.width, y)
rightUsed += placeable.width
} else {
placeable.place(leftUsed, y)
leftUsed += placeable.width
}
}

// Center items: clamped between occupied edges
val centerPlaceable = placeableGroups[Alignment.CenterHorizontally].orEmpty()
val requiredCenterSpace = centerPlaceable.sumOf { it.second.width }
val minX = leftUsed
val maxX = boxWidth - rightUsed - requiredCenterSpace
var centerX = (boxWidth - requiredCenterSpace) / 2

if (minX <= maxX) {
centerX = centerX.coerceIn(minX, maxX)
centerPlaceable.forEach { (_, placeable) ->
val y = Alignment.CenterVertically.align(placeable.height, boxHeight)
placeable.place(centerX, y)
centerX += placeable.width
}
}
}
}
@Suppress("FunctionNaming")
@Composable
fun DecoratedWindowScope.TitleBarImpl(
modifier: Modifier = Modifier,
gradientStartColor: Color = Color.Unspecified,
style: TitleBarStyle = LocalTitleBarStyle.current,
controlButtonsDirection: LayoutDirection = LocalLayoutDirection.current,
layoutPolicy: TitleBarLayoutPolicy = TitleBarLayoutPolicy.Default,
applyTitleBar: (Dp, DecoratedWindowState) -> PaddingValues,
onPlace: (() -> Unit)? = null,
backgroundContent: @Composable () -> Unit = {},
content: @Composable TitleBarScope.(DecoratedWindowState) -> Unit,
) {
GenericTitleBarImpl(
window = window,
state = state,
modifier = modifier,
gradientStartColor = gradientStartColor,
style = style,
controlButtonsDirection = controlButtonsDirection,
layoutPolicy = layoutPolicy,
applyTitleBar = applyTitleBar,
onPlace = onPlace,
backgroundContent = backgroundContent,
content = content,
)
}

@Suppress("FunctionNaming")
@Composable
fun rememberTitleBarMeasurePolicy(
window: Window,
state: DecoratedWindowState,
applyTitleBar: (Dp, DecoratedWindowState) -> PaddingValues,
fun DecoratedWindowScope.TitleBarImpl(
modifier: Modifier = Modifier,
gradientStartColor: Color = Color.Unspecified,
style: TitleBarStyle = LocalTitleBarStyle.current,
controlButtonsDirection: LayoutDirection = LocalLayoutDirection.current,
applyTitleBar: (Dp, DecoratedWindowState) -> PaddingValues,
onPlace: (() -> Unit)? = null,
): MeasurePolicy =
remember(window, state, applyTitleBar, controlButtonsDirection, onPlace) {
TitleBarMeasurePolicy(window, state, applyTitleBar, controlButtonsDirection, onPlace)
}
backgroundContent: @Composable () -> Unit = {},
content: @Composable TitleBarScope.(DecoratedWindowState) -> Unit,
) {
TitleBarImpl(
modifier = modifier,
gradientStartColor = gradientStartColor,
style = style,
controlButtonsDirection = controlButtonsDirection,
layoutPolicy = TitleBarLayoutPolicy.Default,
applyTitleBar = applyTitleBar,
onPlace = onPlace,
backgroundContent = backgroundContent,
content = content,
)
}

@Stable
interface TitleBarScope {
Expand All @@ -309,7 +230,7 @@ interface TitleBarScope {
* fullscreen on non-notch screens.
*
* Standard [clickable][androidx.compose.foundation.clickable] requires a
* complete Press β†’ Release (tap) gesture. On some JDK/macOS combinations,
* complete Press -> Release (tap) gesture. On some JDK/macOS combinations,
* the system injects phantom pointer-exit events between Press and Release
* in fullscreen, which cancels the tap gesture and prevents `onClick` from
* firing.
Expand Down
Loading
Loading