Skip to content
Open
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
29 changes: 24 additions & 5 deletions Sources/SkipUI/SkipUI/Compose/ComposeLayouts.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
// Copyright 2023–2025 Skip
// SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception
#if SKIP
import android.util.Log
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
Expand Down Expand Up @@ -142,9 +144,9 @@ private func flexibleLayoutFloat(_ value: CGFloat?) -> Float? {
}

/// Layout the given view to ignore the given safe areas.
@Composable func IgnoresSafeAreaLayout(content: Renderable, context: ComposeContext, expandInto: Edge.Set) {
@Composable func IgnoresSafeAreaLayout(content: Renderable, context: ComposeContext, expandInto: Edge.Set, logTag: String = "") {
ComposeContainer(modifier: context.modifier) { modifier in
IgnoresSafeAreaLayout(expandInto: expandInto, modifier: modifier) { _, _ in
IgnoresSafeAreaLayout(expandInto: expandInto, checkEdges: expandInto, modifier: modifier, logTag: logTag) { _, _ in
content.Render(context.content())
}
}
Expand All @@ -156,12 +158,22 @@ private func flexibleLayoutFloat(_ value: CGFloat?) -> Float? {
/// the given closure as a pixel rect.
/// - Parameter checkEdges: Which edges to check to see if we're against a safe area. Any matching edges will be
/// passed to the given closure.
@Composable func IgnoresSafeAreaLayout(expandInto: Edge.Set, checkEdges: Edge.Set = [], modifier: Modifier = Modifier, target: @Composable (IntRect, Edge.Set) -> Void) {
/// - Parameter logTag: When non-empty, emits Android ``Log`` lines with tag `SkipUI.ISAL.<logTag>` (e.g. filter logcat `SkipUI.ISAL.List`).
@Composable func IgnoresSafeAreaLayout(expandInto: Edge.Set, checkEdges: Edge.Set = [], modifier: Modifier = Modifier, logTag: String = "", target: @Composable (IntRect, Edge.Set) -> Void) {
guard let safeArea = EnvironmentValues.shared._safeArea else {
if !logTag.isEmpty {
Log.d("SkipUI.ISAL.\(logTag)", "no SafeArea in environment; skipping expansion")
}
target(IntRect.Zero, [])
return
}

if !logTag.isEmpty {
LaunchedEffect(logTag, expandInto.rawValue, checkEdges.rawValue) {
Log.d("SkipUI.ISAL.\(logTag)", "init expandInto=\(expandInto) checkEdges=\(checkEdges) edgesState(initial)=\(checkEdges)")
}
}

// Note: We only allow edges we're interested in to affect our internal state and output. This is critical
// for reducing recompositions, especially during e.g. navigation animations. We also match our internal
// state to our output to ensure we aren't re-calling the target block when output hasn't changed
Expand Down Expand Up @@ -207,8 +219,15 @@ private func flexibleLayoutFloat(_ value: CGFloat?) -> Float? {
return ComposeResult.ok
} in: {
Layout(modifier: modifier.onGloballyPositionedInWindow {
let edges = adjacentSafeAreaEdges(bounds: $0, safeArea: safeArea, isRTL: isRTL, checkEdges: expandInto.union(checkEdges))
edgesState.value = edges
let probeEdges = expandInto.union(checkEdges)
let newEdges = adjacentSafeAreaEdges(bounds: $0, safeArea: safeArea, isRTL: isRTL, checkEdges: probeEdges)
if !logTag.isEmpty {
let previous = edgesState.value
if newEdges != previous {
Log.d("SkipUI.ISAL.\(logTag)", "onGloballyPositionedInWindow adjacentEdges \(previous) -> \(newEdges) (probeEdges=\(probeEdges))")
}
}
edgesState.value = newEdges
}, content: {
let expansion = IntRect(top: expansionTop, left: expansionLeft, bottom: expansionBottom, right: expansionRight)
target(expansion, edges.intersection(checkEdges))
Expand Down
2 changes: 1 addition & 1 deletion Sources/SkipUI/SkipUI/Containers/LazyVGrid.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public struct LazyVGrid: View, Renderable {
let renderables = content.EvaluateLazyItems(level: 0, context: context)
let itemCollector = remember { mutableStateOf(LazyItemCollector()) }
ComposeContainer(axis: .vertical, scrollAxes: scrollAxes, modifier: context.modifier, fillWidth: true) { modifier in
IgnoresSafeAreaLayout(expandInto: [], checkEdges: [.bottom], modifier: modifier) { _, safeAreaEdges in
IgnoresSafeAreaLayout(expandInto: [], checkEdges: [.bottom], modifier: modifier, logTag: "LazyVGrid") { _, safeAreaEdges in
// Integrate with our scroll-to-top and ScrollViewReader
let gridState = rememberLazyGridState(initialFirstVisibleItemIndex = isSearchable ? 1 : 0)
let flingBehavior = scrollTargetBehavior is ViewAlignedScrollTargetBehavior ? rememberSnapFlingBehavior(gridState, SnapPosition.Start) : ScrollableDefaults.flingBehavior()
Expand Down
2 changes: 1 addition & 1 deletion Sources/SkipUI/SkipUI/Containers/LazyVStack.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public struct LazyVStack : View, Renderable {
let renderables = content.EvaluateLazyItems(level: 0, context: context)
let itemCollector = remember { mutableStateOf(LazyItemCollector()) }
ComposeContainer(axis: .vertical, scrollAxes: scrollAxes, modifier: context.modifier, fillWidth: true) { modifier in
IgnoresSafeAreaLayout(expandInto: [], checkEdges: [.bottom], modifier: modifier) { _, safeAreaEdges in
IgnoresSafeAreaLayout(expandInto: [], checkEdges: [.bottom], modifier: modifier, logTag: "LazyVStack") { _, safeAreaEdges in
// Integrate with our scroll-to-top and ScrollViewReader
let listState = rememberLazyListState(initialFirstVisibleItemIndex = isSearchable ? 1 : 0)
let flingBehavior = scrollTargetBehavior is ViewAlignedScrollTargetBehavior ? rememberSnapFlingBehavior(listState, SnapPosition.Start) : ScrollableDefaults.flingBehavior()
Expand Down
2 changes: 1 addition & 1 deletion Sources/SkipUI/SkipUI/Containers/List.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ public final class List : View, Renderable {
var ignoresSafeAreaEdges: Edge.Set = [.top, .bottom]
ignoresSafeAreaEdges.formIntersection(safeArea?.absoluteSystemBarEdges ?? [])
ComposeContainer(scrollAxes: .vertical, modifier: context.modifier, fillWidth: true, fillHeight: true, then: Modifier.background(BackgroundColor(styling: styling, isItem: false))) { modifier in
IgnoresSafeAreaLayout(expandInto: ignoresSafeAreaEdges, checkEdges: [.bottom], modifier: modifier) { safeAreaExpansion, safeAreaEdges in
IgnoresSafeAreaLayout(expandInto: ignoresSafeAreaEdges, checkEdges: [.bottom], modifier: modifier, logTag: "List") { safeAreaExpansion, safeAreaEdges in
var containerModifier: Modifier
let refreshing = remember { mutableStateOf(false) }
let refreshAction = EnvironmentValues.shared.refresh
Expand Down
2 changes: 1 addition & 1 deletion Sources/SkipUI/SkipUI/Containers/Navigation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ public struct NavigationStack : View, Renderable {
// When we layout, only extend into safe areas that are due to system bars, not into any app chrome
var ignoresSafeAreaEdges: Edge.Set = [.top, .bottom]
ignoresSafeAreaEdges.formIntersection(safeArea?.absoluteSystemBarEdges ?? [])
IgnoresSafeAreaLayout(expandInto: ignoresSafeAreaEdges) { _, _ in
IgnoresSafeAreaLayout(expandInto: ignoresSafeAreaEdges, checkEdges: ignoresSafeAreaEdges, logTag: "NavigationStack") { _, _ in
ComposeContainer(modifier: context.modifier, fillWidth: true, fillHeight: true) { modifier in
let isRTL = EnvironmentValues.shared.layoutDirection == LayoutDirection.rightToLeft
NavHost(navController: navController, startDestination: Navigator.rootRoute, modifier: modifier) {
Expand Down
2 changes: 1 addition & 1 deletion Sources/SkipUI/SkipUI/Containers/ScrollView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public struct ScrollView : View, Renderable {

let contentContext = context.content()
ComposeContainer(scrollAxes: effectiveScrollAxes, modifier: context.modifier, fillWidth: axes.contains(.horizontal), fillHeight: axes.contains(.vertical)) { modifier in
IgnoresSafeAreaLayout(expandInto: [], checkEdges: [.bottom], modifier: modifier) { _, safeAreaEdges in
IgnoresSafeAreaLayout(expandInto: [], checkEdges: [.bottom], modifier: modifier, logTag: "ScrollView") { _, safeAreaEdges in
var containerModifier: Modifier = Modifier
if wantsVerticalScroll {
containerModifier = containerModifier.fillMaxHeight()
Expand Down
2 changes: 1 addition & 1 deletion Sources/SkipUI/SkipUI/Containers/TabView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ public struct TabView : View, Renderable {
// tab switches
var ignoresSafeAreaEdges: Edge.Set = [.bottom, .top]
ignoresSafeAreaEdges.formIntersection(safeArea?.absoluteSystemBarEdges ?? [])
IgnoresSafeAreaLayout(expandInto: ignoresSafeAreaEdges) { _, _ in
IgnoresSafeAreaLayout(expandInto: ignoresSafeAreaEdges, checkEdges: ignoresSafeAreaEdges, logTag: "TabView") { _, _ in
ComposeContainer(modifier: context.modifier, fillWidth: true, fillHeight: true) { modifier in
// Don't use a Scaffold: it clips content beyond its bounds and prevents .ignoresSafeArea modifiers from working
Column(modifier: modifier.background(Color.background.colorImpl())) {
Expand Down
2 changes: 1 addition & 1 deletion Sources/SkipUI/SkipUI/Containers/Table.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public final class Table<ObjectType, ID> : View, Renderable where ObjectType: Id
ignoresSafeAreaEdges.formIntersection(safeArea?.absoluteSystemBarEdges ?? [])
let itemContext = context.content()
ComposeContainer(scrollAxes: .vertical, modifier: context.modifier, fillWidth: true, fillHeight: true) { modifier in
IgnoresSafeAreaLayout(expandInto: ignoresSafeAreaEdges, modifier: modifier) { safeAreaExpansion, _ in
IgnoresSafeAreaLayout(expandInto: ignoresSafeAreaEdges, modifier: modifier, logTag: "Table") { safeAreaExpansion, _ in
let density = LocalDensity.current
let headerSafeAreaHeight = with(density) { safeAreaExpansion.top.toDp() }
let footerSafeAreaHeight = with(density) { safeAreaExpansion.bottom.toDp() }
Expand Down