Skip to content

Commit

Permalink
Simplify and improve layout system
Browse files Browse the repository at this point in the history
  • Loading branch information
aabewhite committed Aug 2, 2024
1 parent 4039456 commit 5c40d80
Show file tree
Hide file tree
Showing 11 changed files with 58 additions and 102 deletions.
2 changes: 1 addition & 1 deletion Sources/SkipUI/SkipUI/Color/Color.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public struct Color: ShapeStyle, Hashable, Sendable {

@Composable public override func ComposeContent(context: ComposeContext) {
let animatable = colorImpl().asAnimatable(context: context)
let modifier = context.modifier.background(animatable.value).fillSize(expandContainer: false)
let modifier = context.modifier.background(animatable.value).fillSize()
Box(modifier: modifier)
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/SkipUI/SkipUI/Components/Image.swift
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ public struct Image : View, Equatable {
switch resizingMode {
case .stretch:
let scale = contentScale(aspectRatio: aspectRatio, contentMode: contentMode)
let modifier = Modifier.fillSize(expandContainer: false)
let modifier = Modifier.fillSize()
androidx.compose.foundation.Image(painter: painter, contentDescription: nil, modifier: modifier, contentScale: scale, colorFilter: colorFilter)
default: // TODO: .tile
let modifier = Modifier.wrapContentSize(unbounded: true).size((painter.intrinsicSize.width / scale).dp, (painter.intrinsicSize.height / scale).dp)
Expand Down
4 changes: 2 additions & 2 deletions Sources/SkipUI/SkipUI/Components/Spacer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ public struct Spacer : View {
let fillModifier: Modifier
switch axis {
case .horizontal:
fillModifier = EnvironmentValues.shared._fillWidth?(true) ?? Modifier
fillModifier = EnvironmentValues.shared._fillWidth?() ?? Modifier
case .vertical:
fillModifier = EnvironmentValues.shared._fillHeight?(true) ?? Modifier
fillModifier = EnvironmentValues.shared._fillHeight?() ?? Modifier
case nil:
fillModifier = Modifier
}
Expand Down
73 changes: 21 additions & 52 deletions Sources/SkipUI/SkipUI/Compose/ComposeContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,43 +37,34 @@ import androidx.compose.ui.Modifier
@Composable public func ComposeContainer(axis: Axis? = nil, eraseAxis: Bool = false, scrollAxes: Axis.Set = [], modifier: Modifier = Modifier, fillWidth: Bool = false, fixedWidth: Bool = false, fillHeight: Bool = false, fixedHeight: Bool = false, then: Modifier = Modifier, content: @Composable (Modifier) -> Void) {
// Use remembered expansion values to recompose on change
let isFillWidth = remember { mutableStateOf(fillWidth) }
let isNonExpandingFillWidth = remember { mutableStateOf(false) }
let isFillHeight = remember { mutableStateOf(fillHeight) }
let isNonExpandingFillHeight = remember { mutableStateOf(false) }

// Create the correct modifier for the current values. We use IntrinsicSize.Max for non-expanding fills so that child
// views who want to take up available space without expanding this container can do so by calling `fillMaxWidth/Height`
//
// We have a special case when our content is framed, meaning at least one dimension is fixed. If a non-fixed dimension
// wants a non-expanding fill, we do not set IntrinsicSize.Max and use an expanding fill instead
// TODO: This special case works in practice thus far, but I do not fully understand it
// Create the correct modifier for the current values. We must use IntrinsicSize.Max for fills in a scroll direction
// because Compose's fillMax modifiers have no effect in the scroll direction. We can't use IntrinsicSize for scrolling
// containers, however
var modifier = modifier
let inheritedScrollAxes = EnvironmentValues.shared._scrollAxes
var totalScrollAxes = inheritedScrollAxes
if fixedWidth {
totalScrollAxes.remove(Axis.Set.horizontal)
} else {
if isFillWidth.value {
} else if isFillWidth.value {
if fillWidth {
modifier = modifier.fillWidth()
} else if inheritedScrollAxes.contains(Axis.Set.horizontal) {
modifier = modifier.width(IntrinsicSize.Max)
} else {
modifier = modifier.fillWidth()
} else if isNonExpandingFillWidth.value {
if !fixedHeight {
modifier = modifier.width(IntrinsicSize.Max)
} else {
modifier = modifier.fillWidth()
}
}
}
if fixedHeight {
totalScrollAxes.remove(Axis.Set.vertical)
} else {
if isFillHeight.value {
} else if isFillHeight.value {
if fillHeight {
modifier = modifier.fillHeight()
} else if inheritedScrollAxes.contains(Axis.Set.vertical) {
modifier = modifier.height(IntrinsicSize.Max)
} else {
modifier = modifier.fillHeight()
} else if isNonExpandingFillHeight.value {
if !fixedWidth {
modifier = modifier.height(IntrinsicSize.Max)
} else {
modifier = modifier.fillHeight()
}
}
}
totalScrollAxes.formUnion(scrollAxes)
Expand All @@ -96,43 +87,21 @@ import androidx.compose.ui.Modifier

// Set the 'fillWidth' and 'fillHeight' blocks to trigger a side effect to update our container's expansion state, which can
// cause it to recompose and recalculate its own modifier. We must use `SideEffect` or the recomposition never happens
$0.set_fillWidth { expandContainer in
let fillModifier = EnvironmentValues.shared._fillWidthModifier
let isExpanding = expandContainer || fillModifier != nil
if isExpanding && !isFillWidth.value {
$0.set_fillWidth {
if !isFillWidth.value {
SideEffect {
isFillWidth.value = true
}
}
if !isExpanding && !isNonExpandingFillWidth.value {
SideEffect {
isNonExpandingFillWidth.value = true
}
}
if let fillModifier, fillModifier != Modifier {
return fillModifier
} else {
return Modifier.fillMaxWidth()
}
return EnvironmentValues.shared._fillWidthModifier ?? Modifier.fillMaxWidth()
}
$0.set_fillHeight { expandContainer in
let fillModifier = EnvironmentValues.shared._fillHeightModifier
let isExpanding = expandContainer || fillModifier != nil
if isExpanding && !isFillHeight.value {
$0.set_fillHeight {
if !isFillHeight.value {
SideEffect {
isFillHeight.value = true
}
}
if !isExpanding && !isNonExpandingFillHeight.value {
SideEffect {
isNonExpandingFillHeight.value = true
}
}
if let fillModifier, fillModifier != Modifier {
return fillModifier
} else {
return Modifier.fillMaxHeight()
}
return EnvironmentValues.shared._fillHeightModifier ?? Modifier.fillMaxHeight()
}
} in: {
// Render the container content with the above environment setup
Expand Down
12 changes: 6 additions & 6 deletions Sources/SkipUI/SkipUI/Compose/ComposeExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,32 +20,32 @@ extension Modifier {
/// This is a replacement for `fillMaxWidth` designed for situations when the composable may be in an `HStack` and does not want to push other items out.
///
/// - Warning: Containers with child content should use the `ComposeContainer` composable instead.
@Composable public func fillWidth(expandContainer: Bool = true) -> Modifier {
@Composable public func fillWidth() -> Modifier {
guard let fill = EnvironmentValues.shared._fillWidth else {
return fillMaxWidth()
}
return then(fill(expandContainer))
return then(fill())
}

/// Fill available remaining height.
///
/// This is a replacement for `fillMaxHeight` designed for situations when the composable may be in an `VStack` and does not want to push other items out.
///
/// - Warning: Containers with child content should use the `ComposeContainer` composable instead.
@Composable public func fillHeight(expandContainer: Bool = true) -> Modifier {
@Composable public func fillHeight() -> Modifier {
guard let fill = EnvironmentValues.shared._fillHeight else {
return fillMaxHeight()
}
return then(fill(expandContainer))
return then(fill())
}

/// Fill available remaining size.
///
/// This is a replacement for `fillMaxSize` designed for situations when the composable may be in an `HStack` or `VStack` and does not want to push other items out.
///
/// - Warning: Containers with child content should use the `ComposeContainer` composable instead.
@Composable public func fillSize(expandContainer: Bool = true) -> Modifier {
return fillWidth(expandContainer: expandContainer).fillHeight(expandContainer: expandContainer)
@Composable public func fillSize() -> Modifier {
return fillWidth().fillHeight()
}

/// Add padding equivalent to the given safe area.
Expand Down
16 changes: 7 additions & 9 deletions Sources/SkipUI/SkipUI/Compose/ComposeLayouts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,25 +54,23 @@ import androidx.compose.ui.unit.dp
/// Compose a view with the given frame.
@Composable func FrameLayout(view: View, context: ComposeContext, minWidth: CGFloat?, idealWidth: CGFloat?, maxWidth: CGFloat?, minHeight: CGFloat?, idealHeight: CGFloat?, maxHeight: CGFloat?, alignment: Alignment) {
let scrollAxes = EnvironmentValues.shared._scrollAxes
var modifier = context.modifier
var thenModifier: Modifier = Modifier
if maxWidth == .infinity {
modifier = modifier.fillWidth(expandContainer: !scrollAxes.contains(Axis.Set.horizontal))
if let minWidth, minWidth > 0.0 {
modifier = modifier.requiredWidthIn(min: minWidth.dp)
thenModifier = thenModifier.requiredWidthIn(min: minWidth.dp)
}
} else if minWidth != nil || maxWidth != nil {
modifier = modifier.requiredWidthIn(min: minWidth != nil ? minWidth!.dp : Dp.Unspecified, max: maxWidth != nil ? maxWidth!.dp : Dp.Unspecified)
thenModifier = thenModifier.requiredWidthIn(min: minWidth != nil ? minWidth!.dp : Dp.Unspecified, max: maxWidth != nil ? maxWidth!.dp : Dp.Unspecified)
}
if maxHeight == .infinity {
modifier = modifier.fillHeight(expandContainer: !scrollAxes.contains(Axis.Set.vertical))
if let minHeight, minHeight > 0.0 {
modifier = modifier.requiredHeightIn(min: minHeight.dp)
thenModifier = thenModifier.requiredHeightIn(min: minHeight.dp)
}
} else if minHeight != nil || maxHeight != nil {
modifier = modifier.requiredHeightIn(min: minHeight != nil ? minHeight!.dp : Dp.Unspecified, max: maxHeight != nil ? maxHeight!.dp : Dp.Unspecified)
thenModifier = thenModifier.requiredHeightIn(min: minHeight != nil ? minHeight!.dp : Dp.Unspecified, max: maxHeight != nil ? maxHeight!.dp : Dp.Unspecified)
}
let isContainerView = view.strippingModifiers(perform: { $0 is HStack || $0 is VStack || $0 is ZStack })
ComposeContainer(modifier: modifier, fixedWidth: maxWidth != nil && maxWidth != Double.infinity, fixedHeight: maxHeight != nil && maxHeight != Double.infinity) { modifier in
ComposeContainer(modifier: context.modifier, fillWidth: maxWidth == Double.infinity, fixedWidth: maxWidth != nil && maxWidth != Double.infinity, fillHeight: maxHeight == Double.infinity, fixedHeight: maxHeight != nil && maxHeight != Double.infinity, then: thenModifier) { modifier in
// Apply the sizing modifier directly to containers, which would otherwise fit their size to their content instead
if isContainerView {
let contentContext = context.content(modifier: modifier)
Expand Down Expand Up @@ -232,7 +230,7 @@ import androidx.compose.ui.unit.dp

@Composable func PositionLayout(x: CGFloat, y: CGFloat, context: ComposeContext, target: @Composable (ComposeContext) -> Void) {
// SwiftUI expands to fill the available space and places within that
Box(modifier: context.modifier.fillSize(expandContainer: false)) {
Box(modifier: context.modifier.fillSize()) {
let density = LocalDensity.current
let xPx = with(density) { x.dp.roundToPx() }
let yPx = with(density) { y.dp.roundToPx() }
Expand Down
33 changes: 11 additions & 22 deletions Sources/SkipUI/SkipUI/Containers/ZStack.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,7 @@ public struct ZStack : View {
let contentContext = context.content()
ComposeContainer(eraseAxis: true, modifier: context.modifier) { modifier in
Box(modifier: modifier, contentAlignment: alignment.asComposeAlignment()) {
EnvironmentValues.shared.setValues {
$0.set_fillWidthModifier(Modifier)
$0.set_fillHeightModifier(Modifier)
} in: {
views.forEach { $0.Compose(context: contentContext) }
}
views.forEach { $0.Compose(context: contentContext) }
}
}
} else {
Expand All @@ -74,23 +69,17 @@ public struct ZStack : View {
arguments.rememberedNewIds.clear()
}
Box(contentAlignment: alignment.asComposeAlignment()) {
EnvironmentValues.shared.setValues {
// The ComposeContainer uses the presence of these modifiers to influence container expansion behavior
$0.set_fillWidthModifier(Modifier)
$0.set_fillHeightModifier(Modifier)
} in: {
for view in state {
let id = arguments.idMap(view)
var modifier: Modifier = Modifier
if let animation, arguments.newIds.contains(id) || arguments.rememberedNewIds.contains(id) || !arguments.ids.contains(id) {
let transition = TransitionModifierView.transition(for: view) ?? OpacityTransition.shared
let spec = animation.asAnimationSpec()
let enter = transition.asEnterTransition(spec: spec)
let exit = transition.asExitTransition(spec: spec)
modifier = modifier.animateEnterExit(enter: enter, exit: exit)
}
view.Compose(context: context.content(modifier: modifier))
for view in state {
let id = arguments.idMap(view)
var modifier: Modifier = Modifier
if let animation, arguments.newIds.contains(id) || arguments.rememberedNewIds.contains(id) || !arguments.ids.contains(id) {
let transition = TransitionModifierView.transition(for: view) ?? OpacityTransition.shared
let spec = animation.asAnimationSpec()
let enter = transition.asEnterTransition(spec: spec)
let exit = transition.asExitTransition(spec: spec)
modifier = modifier.animateEnterExit(enter: enter, exit: exit)
}
view.Compose(context: context.content(modifier: modifier))
}
}
}, label: "ZStack")
Expand Down
8 changes: 4 additions & 4 deletions Sources/SkipUI/SkipUI/Environment/EnvironmentValues.swift
Original file line number Diff line number Diff line change
Expand Up @@ -414,13 +414,13 @@ extension EnvironmentValues {
set { setBuiltinValue(key: "_contentPadding", value: newValue, defaultValue: { EdgeInsets() }) }
}

var _fillHeight: (@Composable (Bool) -> Modifier)? {
get { builtinValue(key: "_fillHeight", defaultValue: { nil }) as! (@Composable (Bool) -> Modifier)? }
var _fillHeight: (@Composable () -> Modifier)? {
get { builtinValue(key: "_fillHeight", defaultValue: { nil }) as! (@Composable () -> Modifier)? }
set { setBuiltinValue(key: "_fillHeight", value: newValue, defaultValue: { nil }) }
}

var _fillWidth: (@Composable (Bool) -> Modifier)? {
get { builtinValue(key: "_fillWidth", defaultValue: { nil }) as! (@Composable (Bool) -> Modifier)? }
var _fillWidth: (@Composable () -> Modifier)? {
get { builtinValue(key: "_fillWidth", defaultValue: { nil }) as! (@Composable () -> Modifier)? }
set { setBuiltinValue(key: "_fillWidth", value: newValue, defaultValue: { nil }) }
}

Expand Down
6 changes: 3 additions & 3 deletions Sources/SkipUI/SkipUI/Graphics/Gradient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ public struct LinearGradient : ShapeStyle, Sendable {

#if SKIP
@Composable public override func ComposeContent(context: ComposeContext) {
let modifier = context.modifier.background(asBrush(opacity: 1.0, animationContext: nil)!).fillSize(expandContainer: false)
let modifier = context.modifier.background(asBrush(opacity: 1.0, animationContext: nil)!).fillSize()
Box(modifier: modifier)
}

Expand Down Expand Up @@ -180,7 +180,7 @@ public struct EllipticalGradient : ShapeStyle, Sendable {
#if SKIP
@Composable public override func ComposeContent(context: ComposeContext) {
// Trick to scale our (circular) radial brush into an ellipse when this gradient is used as a view
BoxWithConstraints(modifier: context.modifier.fillSize(expandContainer = false).clipToBounds()) {
BoxWithConstraints(modifier: context.modifier.fillSize().clipToBounds()) {
let aspectRatio = maxWidth / maxHeight
let modifier = Modifier.fillMaxSize().scale(max(aspectRatio, Float(1.0)), max(Float(1.0) / aspectRatio, Float(1.0))).background(asBrush(opacity = 1.0, animationContext: nil)!!)
Box(modifier: modifier)
Expand Down Expand Up @@ -239,7 +239,7 @@ public struct RadialGradient : ShapeStyle, Sendable {

#if SKIP
@Composable public override func ComposeContent(context: ComposeContext) {
let modifier = context.modifier.background(asBrush(opacity: 1.0, animationContext: nil)!).fillSize(expandContainer: false)
let modifier = context.modifier.background(asBrush(opacity: 1.0, animationContext: nil)!).fillSize()
Box(modifier: modifier)
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/SkipUI/SkipUI/Layout/GeometryReader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public struct GeometryReader : View {
#if SKIP
@Composable public override func ComposeContent(context: ComposeContext) {
let rememberedGlobalFramePx = remember { mutableStateOf<Rect?>(nil) }
Box(modifier: context.modifier.fillSize(expandContainer: false).onGloballyPositioned {
Box(modifier: context.modifier.fillSize().onGloballyPositioned {
rememberedGlobalFramePx.value = $0.boundsInRoot()
}) {
if let globalFramePx = rememberedGlobalFramePx.value {
Expand Down
2 changes: 1 addition & 1 deletion Tests/SkipUITests/ImageTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ final class ImageTests: XCSnapshotTestCase {
let imageVector = ImageVector.vectorResource(id: drawableID)
let painter = rememberVectorPainter(imageVector)
//Icon(imageVector: imageVector, contentDescription: "demo icon", modifier: Modifier.fillSize(expandContainer: false))
androidx.compose.foundation.Image(painter: painter, contentDescription: "demo icon", modifier: Modifier.fillSize(expandContainer: false), contentScale: ContentScale.Fit, colorFilter: ColorFilter.tint(tintColor))
androidx.compose.foundation.Image(painter: painter, contentDescription: "demo icon", modifier: Modifier.fillSize(), contentScale: ContentScale.Fit, colorFilter: ColorFilter.tint(tintColor))
}
.background(Color.black)
.foregroundStyle(Color.white)
Expand Down

0 comments on commit 5c40d80

Please sign in to comment.