Merge e7051e631b
into e0d8e9db46
This commit is contained in:
commit
ba2a98f9b3
|
@ -19,8 +19,9 @@
|
|||
/// 1. `View.makeMountedView`
|
||||
/// 2. `MountedHostView.update` when reconciling
|
||||
///
|
||||
protocol EnvironmentReader {
|
||||
mutating func setContent(from values: EnvironmentValues)
|
||||
@_spi(TokamakCore)
|
||||
public protocol _EnvironmentReader {
|
||||
mutating func _setContent(from values: EnvironmentValues)
|
||||
}
|
||||
|
||||
@propertyWrapper
|
||||
|
@ -37,7 +38,7 @@ public struct Environment<Value>: DynamicProperty {
|
|||
self.keyPath = keyPath
|
||||
}
|
||||
|
||||
mutating func setContent(from values: EnvironmentValues) {
|
||||
public mutating func _setContent(from values: EnvironmentValues) {
|
||||
content = .value(values[keyPath: keyPath])
|
||||
}
|
||||
|
||||
|
@ -52,4 +53,5 @@ public struct Environment<Value>: DynamicProperty {
|
|||
}
|
||||
}
|
||||
|
||||
extension Environment: EnvironmentReader {}
|
||||
@_spi(TokamakCore)
|
||||
extension Environment: _EnvironmentReader {}
|
||||
|
|
|
@ -40,7 +40,7 @@ public struct EnvironmentObject<ObjectType>: DynamicProperty
|
|||
var _store: ObjectType?
|
||||
var _seed: Int = 0
|
||||
|
||||
mutating func setContent(from values: EnvironmentValues) {
|
||||
public mutating func _setContent(from values: EnvironmentValues) {
|
||||
_store = values[ObjectIdentifier(ObjectType.self)]
|
||||
}
|
||||
|
||||
|
@ -65,7 +65,10 @@ public struct EnvironmentObject<ObjectType>: DynamicProperty
|
|||
public init() {}
|
||||
}
|
||||
|
||||
extension EnvironmentObject: ObservedProperty, EnvironmentReader {}
|
||||
extension EnvironmentObject: ObservedProperty {}
|
||||
|
||||
@_spi(TokamakCore)
|
||||
extension EnvironmentObject: _EnvironmentReader {}
|
||||
|
||||
extension ObservableObject {
|
||||
static var environmentStore: WritableKeyPath<EnvironmentValues, Self?> {
|
||||
|
|
|
@ -22,7 +22,7 @@ public protocol EnvironmentalModifier: ViewModifier {
|
|||
static var _requiresMainThread: Bool { get }
|
||||
}
|
||||
|
||||
private struct EnvironmentalModifierResolver<M>: ViewModifier, EnvironmentReader
|
||||
private struct EnvironmentalModifierResolver<M>: ViewModifier, _EnvironmentReader
|
||||
where M: EnvironmentalModifier
|
||||
{
|
||||
let modifier: M
|
||||
|
@ -32,7 +32,7 @@ private struct EnvironmentalModifierResolver<M>: ViewModifier, EnvironmentReader
|
|||
content.modifier(resolved)
|
||||
}
|
||||
|
||||
mutating func setContent(from values: EnvironmentValues) {
|
||||
mutating func _setContent(from values: EnvironmentValues) {
|
||||
resolved = modifier.resolve(in: values)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -309,8 +309,8 @@ public extension FiberReconciler {
|
|||
storage.getter = { box.value }
|
||||
value = storage
|
||||
// Read from the environment.
|
||||
} else if var environmentReader = value as? EnvironmentReader {
|
||||
environmentReader.setContent(from: environment)
|
||||
} else if var environmentReader = value as? _EnvironmentReader {
|
||||
environmentReader._setContent(from: environment)
|
||||
value = environmentReader
|
||||
}
|
||||
// Subscribe to observable properties.
|
||||
|
@ -322,8 +322,8 @@ public extension FiberReconciler {
|
|||
}
|
||||
property.set(value: value, on: &content)
|
||||
}
|
||||
if var environmentReader = content as? EnvironmentReader {
|
||||
environmentReader.setContent(from: environment)
|
||||
if var environmentReader = content as? _EnvironmentReader {
|
||||
environmentReader._setContent(from: environment)
|
||||
content = environmentReader
|
||||
}
|
||||
}
|
||||
|
|
|
@ -127,7 +127,8 @@ extension EnvironmentValues {
|
|||
}
|
||||
}
|
||||
|
||||
var measureText: (Text, ProposedViewSize, EnvironmentValues) -> CGSize {
|
||||
@_spi(TokamakCore)
|
||||
public var measureText: (Text, ProposedViewSize, EnvironmentValues) -> CGSize {
|
||||
get { self[MeasureTextKey.self] }
|
||||
set { self[MeasureTextKey.self] = newValue }
|
||||
}
|
||||
|
|
|
@ -0,0 +1,277 @@
|
|||
// Copyright 2022 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 7/6/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@_spi(TokamakCore)
|
||||
public struct _LazyGridLayoutCache {
|
||||
var resolvedItems = [ResolvedItem]()
|
||||
var mainAxisSizes = [CGFloat]()
|
||||
struct ResolvedItem {
|
||||
let size: CGFloat
|
||||
let item: GridItem
|
||||
}
|
||||
}
|
||||
|
||||
@_spi(TokamakCore)
|
||||
public protocol _LazyGridLayout: Layout where Cache == _LazyGridLayoutCache {
|
||||
static var axis: Axis { get }
|
||||
var items: [GridItem] { get }
|
||||
var _alignment: Alignment { get }
|
||||
var spacing: CGFloat? { get }
|
||||
}
|
||||
|
||||
public extension _LazyGridLayout {
|
||||
internal var mainAxis: WritableKeyPath<CGSize, CGFloat> {
|
||||
switch Self.axis {
|
||||
case .horizontal:
|
||||
return \.width
|
||||
case .vertical:
|
||||
return \.height
|
||||
}
|
||||
}
|
||||
|
||||
internal var crossAxis: WritableKeyPath<CGSize, CGFloat> {
|
||||
switch Self.axis {
|
||||
case .horizontal:
|
||||
return \.height
|
||||
case .vertical:
|
||||
return \.width
|
||||
}
|
||||
}
|
||||
|
||||
func makeCache(subviews: Subviews) -> Cache {
|
||||
.init()
|
||||
}
|
||||
|
||||
func sizeThatFits(
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Subviews,
|
||||
cache: inout Cache
|
||||
) -> CGSize {
|
||||
cache.resolvedItems.removeAll()
|
||||
|
||||
var reservedFixedSpace = CGFloat.zero
|
||||
for (index, item) in items.enumerated() {
|
||||
if case .adaptive = item.size {
|
||||
continue
|
||||
}
|
||||
if index < items.count - 1 {
|
||||
reservedFixedSpace += item.spacing ?? 8
|
||||
}
|
||||
if case let .fixed(fixed) = item.size {
|
||||
reservedFixedSpace += fixed
|
||||
}
|
||||
}
|
||||
|
||||
let proposal = proposal.replacingUnspecifiedDimensions()
|
||||
|
||||
var remainingSize = proposal[keyPath: crossAxis] - reservedFixedSpace
|
||||
|
||||
let flexibleItems = items.filter {
|
||||
if case .fixed = $0.size {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
var remainingItems = flexibleItems.count
|
||||
|
||||
for item in items {
|
||||
switch item.size {
|
||||
case let .flexible(minimum, maximum):
|
||||
let divviedSpace = remainingSize / CGFloat(remainingItems)
|
||||
let size = max(minimum, min(maximum, divviedSpace))
|
||||
remainingSize -= size
|
||||
cache.resolvedItems.append(.init(size: size, item: item))
|
||||
remainingItems -= 1
|
||||
case let .fixed(size):
|
||||
cache.resolvedItems.append(.init(size: size, item: item))
|
||||
case let .adaptive(minimum, maximum):
|
||||
let divviedSpace = remainingSize / CGFloat(remainingItems)
|
||||
var remaining = divviedSpace
|
||||
var fitCount = 0
|
||||
while true {
|
||||
if fitCount != 0 {
|
||||
remaining -= item.spacing ?? 8
|
||||
}
|
||||
if remaining - minimum < 0 {
|
||||
break
|
||||
}
|
||||
remaining -= minimum
|
||||
fitCount += 1
|
||||
}
|
||||
let fitSize = min(
|
||||
max(
|
||||
(divviedSpace - ((item.spacing ?? 8) * CGFloat(fitCount - 1))) / CGFloat(fitCount),
|
||||
minimum
|
||||
),
|
||||
maximum
|
||||
)
|
||||
for _ in 0..<fitCount {
|
||||
remainingSize -= fitSize
|
||||
cache.resolvedItems.append(.init(size: fitSize, item: item))
|
||||
}
|
||||
remainingItems -= 1
|
||||
}
|
||||
}
|
||||
|
||||
var mainAxisSize = CGFloat.zero
|
||||
var maxMainAxisSize = CGFloat.zero
|
||||
var mainAxisSpacing = CGFloat.zero
|
||||
for (index, subview) in subviews.enumerated() {
|
||||
let itemIndex = index % cache.resolvedItems.count
|
||||
let itemSize = cache.resolvedItems[itemIndex].size
|
||||
let size = subview.sizeThatFits(.init(
|
||||
width: Self.axis == .vertical ? itemSize : nil,
|
||||
height: Self.axis == .horizontal ? itemSize : nil
|
||||
))
|
||||
if size[keyPath: mainAxis] > maxMainAxisSize {
|
||||
maxMainAxisSize = size[keyPath: mainAxis]
|
||||
}
|
||||
if subviews.indices.contains(index + cache.resolvedItems.count) {
|
||||
if let spacing = spacing {
|
||||
mainAxisSpacing = spacing
|
||||
} else {
|
||||
let spacing = subview.spacing.distance(
|
||||
to: subviews[index + cache.resolvedItems.count].spacing,
|
||||
along: .vertical
|
||||
)
|
||||
if spacing > mainAxisSpacing {
|
||||
mainAxisSpacing = spacing
|
||||
}
|
||||
}
|
||||
}
|
||||
if itemIndex == cache.resolvedItems.count - 1 {
|
||||
cache.mainAxisSizes.append(maxMainAxisSize)
|
||||
mainAxisSize += maxMainAxisSize + mainAxisSpacing
|
||||
maxMainAxisSize = .zero
|
||||
mainAxisSpacing = .zero
|
||||
}
|
||||
}
|
||||
cache.mainAxisSizes.append(maxMainAxisSize)
|
||||
mainAxisSize += maxMainAxisSize
|
||||
|
||||
var result = proposal
|
||||
result[keyPath: mainAxis] = mainAxisSize
|
||||
return result
|
||||
}
|
||||
|
||||
func placeSubviews(
|
||||
in bounds: CGRect,
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Subviews,
|
||||
cache: inout Cache
|
||||
) {
|
||||
let contentSize = cache.resolvedItems.enumerated().reduce(into: .zero) {
|
||||
$0 += $1.element
|
||||
.size + ($1.offset == cache.resolvedItems.count - 1 ? 0 : $1.element.item.spacing ?? 8)
|
||||
}
|
||||
|
||||
var offset = CGSize.zero
|
||||
let origin = CGSize(width: bounds.width, height: bounds.height)
|
||||
let contentAlignmentID: AlignmentID.Type
|
||||
switch Self.axis {
|
||||
case .horizontal:
|
||||
contentAlignmentID = _alignment.vertical.id
|
||||
case .vertical:
|
||||
contentAlignmentID = _alignment.horizontal.id
|
||||
}
|
||||
let startOffset = contentAlignmentID.defaultValue(
|
||||
in: .init(size: origin, alignmentGuides: [:])
|
||||
) - contentAlignmentID.defaultValue(
|
||||
in: .init(size: .init(width: contentSize, height: contentSize), alignmentGuides: [:])
|
||||
)
|
||||
offset[keyPath: crossAxis] = startOffset
|
||||
var mainAxisSpacing = CGFloat.zero
|
||||
for (index, subview) in subviews.enumerated() {
|
||||
let itemIndex = index % cache.resolvedItems.count
|
||||
let mainAxisIndex = index / cache.resolvedItems.count
|
||||
|
||||
if itemIndex == 0 {
|
||||
offset[keyPath: crossAxis] = startOffset
|
||||
mainAxisSpacing = .zero
|
||||
}
|
||||
|
||||
var proposal = CGSize.zero
|
||||
proposal[keyPath: mainAxis] = cache.mainAxisSizes[mainAxisIndex]
|
||||
proposal[keyPath: crossAxis] = cache.resolvedItems[itemIndex].size
|
||||
|
||||
let dimensions = subview.dimensions(in: .init(proposal))
|
||||
|
||||
var position = offset
|
||||
|
||||
position.width += cache.resolvedItems[itemIndex].item.alignment.horizontal.id
|
||||
.defaultValue(in: .init(
|
||||
size: proposal,
|
||||
alignmentGuides: [:]
|
||||
))
|
||||
position.height += cache.resolvedItems[itemIndex].item.alignment.vertical.id
|
||||
.defaultValue(in: .init(
|
||||
size: proposal,
|
||||
alignmentGuides: [:]
|
||||
))
|
||||
|
||||
position.width -= dimensions[
|
||||
cache.resolvedItems[itemIndex].item.alignment.horizontal
|
||||
]
|
||||
position.height -= dimensions[
|
||||
cache.resolvedItems[itemIndex].item.alignment.vertical
|
||||
]
|
||||
|
||||
subview.place(
|
||||
at: .init(
|
||||
x: bounds.minX + position.width,
|
||||
y: bounds.minY + position.height
|
||||
),
|
||||
proposal: .init(proposal)
|
||||
)
|
||||
|
||||
offset[keyPath: crossAxis] += cache.resolvedItems[itemIndex].size
|
||||
offset[keyPath: crossAxis] += cache.resolvedItems[itemIndex].item.spacing ?? 8
|
||||
|
||||
if spacing == nil && subviews.indices.contains(index + cache.resolvedItems.count) {
|
||||
let spacing = subview.spacing.distance(
|
||||
to: subviews[index + cache.resolvedItems.count].spacing,
|
||||
along: Self.axis
|
||||
)
|
||||
if spacing > mainAxisSpacing {
|
||||
mainAxisSpacing = spacing
|
||||
}
|
||||
}
|
||||
if itemIndex == cache.resolvedItems.count - 1 {
|
||||
offset[keyPath: mainAxis] += cache.mainAxisSizes[mainAxisIndex] + (
|
||||
spacing ?? mainAxisSpacing
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@_spi(TokamakCore)
|
||||
extension LazyVGrid: _LazyGridLayout {
|
||||
public static var axis: Axis { .vertical }
|
||||
public var items: [GridItem] { columns }
|
||||
public var _alignment: Alignment { .init(horizontal: alignment, vertical: .center) }
|
||||
}
|
||||
|
||||
@_spi(TokamakCore)
|
||||
extension LazyHGrid: _LazyGridLayout {
|
||||
public static var axis: Axis { .horizontal }
|
||||
public var items: [GridItem] { rows }
|
||||
public var _alignment: Alignment { .init(horizontal: .center, vertical: alignment) }
|
||||
}
|
|
@ -37,9 +37,10 @@ extension ModifiedContent: ModifierContainer {
|
|||
var environmentModifier: _EnvironmentModifier? { modifier as? _EnvironmentModifier }
|
||||
}
|
||||
|
||||
extension ModifiedContent: EnvironmentReader where Modifier: EnvironmentReader {
|
||||
mutating func setContent(from values: EnvironmentValues) {
|
||||
modifier.setContent(from: values)
|
||||
@_spi(TokamakCore)
|
||||
extension ModifiedContent: _EnvironmentReader where Modifier: _EnvironmentReader {
|
||||
public mutating func _setContent(from values: EnvironmentValues) {
|
||||
modifier._setContent(from: values)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ public struct _BackgroundLayout<Content, Background>: _PrimitiveView
|
|||
}
|
||||
}
|
||||
|
||||
public struct _BackgroundModifier<Background>: ViewModifier, EnvironmentReader
|
||||
public struct _BackgroundModifier<Background>: ViewModifier
|
||||
where Background: View
|
||||
{
|
||||
public var environment: EnvironmentValues!
|
||||
|
@ -58,11 +58,14 @@ public struct _BackgroundModifier<Background>: ViewModifier, EnvironmentReader
|
|||
)
|
||||
}
|
||||
|
||||
mutating func setContent(from values: EnvironmentValues) {
|
||||
public mutating func _setContent(from values: EnvironmentValues) {
|
||||
environment = values
|
||||
}
|
||||
}
|
||||
|
||||
@_spi(TokamakCore)
|
||||
extension _BackgroundModifier: _EnvironmentReader {}
|
||||
|
||||
extension _BackgroundModifier: Equatable where Background: Equatable {
|
||||
public static func == (
|
||||
lhs: _BackgroundModifier<Background>,
|
||||
|
@ -90,7 +93,7 @@ public extension View {
|
|||
}
|
||||
|
||||
@frozen
|
||||
public struct _BackgroundShapeModifier<Style, Bounds>: ViewModifier, EnvironmentReader
|
||||
public struct _BackgroundShapeModifier<Style, Bounds>: ViewModifier
|
||||
where Style: ShapeStyle, Bounds: Shape
|
||||
{
|
||||
public var environment: EnvironmentValues!
|
||||
|
@ -111,11 +114,14 @@ public struct _BackgroundShapeModifier<Style, Bounds>: ViewModifier, Environment
|
|||
.background(shape.fill(style, style: fillStyle))
|
||||
}
|
||||
|
||||
public mutating func setContent(from values: EnvironmentValues) {
|
||||
public mutating func _setContent(from values: EnvironmentValues) {
|
||||
environment = values
|
||||
}
|
||||
}
|
||||
|
||||
@_spi(TokamakCore)
|
||||
extension _BackgroundShapeModifier: _EnvironmentReader {}
|
||||
|
||||
public extension View {
|
||||
@inlinable
|
||||
func background<S, T>(
|
||||
|
@ -149,7 +155,7 @@ public struct _OverlayLayout<Content, Overlay>: _PrimitiveView
|
|||
}
|
||||
}
|
||||
|
||||
public struct _OverlayModifier<Overlay>: ViewModifier, EnvironmentReader
|
||||
public struct _OverlayModifier<Overlay>: ViewModifier
|
||||
where Overlay: View
|
||||
{
|
||||
public var environment: EnvironmentValues!
|
||||
|
@ -169,11 +175,14 @@ public struct _OverlayModifier<Overlay>: ViewModifier, EnvironmentReader
|
|||
)
|
||||
}
|
||||
|
||||
mutating func setContent(from values: EnvironmentValues) {
|
||||
public mutating func _setContent(from values: EnvironmentValues) {
|
||||
environment = values
|
||||
}
|
||||
}
|
||||
|
||||
@_spi(TokamakCore)
|
||||
extension _OverlayModifier: _EnvironmentReader {}
|
||||
|
||||
extension _OverlayModifier: Equatable where Overlay: Equatable {
|
||||
public static func == (lhs: _OverlayModifier<Overlay>, rhs: _OverlayModifier<Overlay>) -> Bool {
|
||||
lhs.overlay == rhs.overlay
|
||||
|
|
|
@ -246,16 +246,16 @@ extension EnvironmentValues {
|
|||
for dynamicProp in info.properties.filter({ $0.type is DynamicProperty.Type }) {
|
||||
guard let propInfo = typeInfo(of: dynamicProp.type) else { return }
|
||||
var propWrapper = dynamicProp.get(from: element) as! DynamicProperty
|
||||
for prop in propInfo.properties.filter({ $0.type is EnvironmentReader.Type }) {
|
||||
var wrapper = prop.get(from: propWrapper) as! EnvironmentReader
|
||||
wrapper.setContent(from: self)
|
||||
for prop in propInfo.properties.filter({ $0.type is _EnvironmentReader.Type }) {
|
||||
var wrapper = prop.get(from: propWrapper) as! _EnvironmentReader
|
||||
wrapper._setContent(from: self)
|
||||
prop.set(value: wrapper, on: &propWrapper)
|
||||
}
|
||||
dynamicProp.set(value: propWrapper, on: &element)
|
||||
}
|
||||
for prop in info.properties.filter({ $0.type is EnvironmentReader.Type }) {
|
||||
var wrapper = prop.get(from: element) as! EnvironmentReader
|
||||
wrapper.setContent(from: self)
|
||||
for prop in info.properties.filter({ $0.type is _EnvironmentReader.Type }) {
|
||||
var wrapper = prop.get(from: element) as! _EnvironmentReader
|
||||
wrapper._setContent(from: self)
|
||||
prop.set(value: wrapper, on: &element)
|
||||
}
|
||||
// swiftlint:enable force_cast
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public struct ContainerRelativeShape: Shape, EnvironmentReader {
|
||||
public struct ContainerRelativeShape: Shape {
|
||||
var containerShape: (CGRect, GeometryProxy) -> Path? = { _, _ in nil }
|
||||
|
||||
public func path(in rect: CGRect) -> Path {
|
||||
|
@ -26,11 +26,14 @@ public struct ContainerRelativeShape: Shape, EnvironmentReader {
|
|||
|
||||
public init() {}
|
||||
|
||||
public mutating func setContent(from values: EnvironmentValues) {
|
||||
public mutating func _setContent(from values: EnvironmentValues) {
|
||||
containerShape = values._containerShape
|
||||
}
|
||||
}
|
||||
|
||||
@_spi(TokamakCore)
|
||||
extension ContainerRelativeShape: _EnvironmentReader {}
|
||||
|
||||
extension ContainerRelativeShape: InsettableShape {
|
||||
@inlinable
|
||||
public func inset(by amount: CGFloat) -> some InsettableShape {
|
||||
|
|
|
@ -57,8 +57,7 @@ public extension View {
|
|||
}
|
||||
|
||||
@frozen
|
||||
public struct _BackgroundStyleModifier<Style>: ViewModifier, _EnvironmentModifier,
|
||||
EnvironmentReader
|
||||
public struct _BackgroundStyleModifier<Style>: ViewModifier, _EnvironmentModifier
|
||||
where Style: ShapeStyle
|
||||
{
|
||||
public var environment: EnvironmentValues!
|
||||
|
@ -70,7 +69,8 @@ public struct _BackgroundStyleModifier<Style>: ViewModifier, _EnvironmentModifie
|
|||
}
|
||||
|
||||
public typealias Body = Never
|
||||
public mutating func setContent(from values: EnvironmentValues) {
|
||||
|
||||
public mutating func _setContent(from values: EnvironmentValues) {
|
||||
environment = values
|
||||
}
|
||||
|
||||
|
@ -85,3 +85,6 @@ public struct _BackgroundStyleModifier<Style>: ViewModifier, _EnvironmentModifie
|
|||
public extension ShapeStyle where Self == BackgroundStyle {
|
||||
static var background: Self { .init() }
|
||||
}
|
||||
|
||||
@_spi(TokamakCore)
|
||||
extension _BackgroundStyleModifier: _EnvironmentReader {}
|
||||
|
|
|
@ -25,7 +25,7 @@ public struct GridItem {
|
|||
}
|
||||
|
||||
public var size: GridItem.Size
|
||||
public var spacing: CGFloat
|
||||
public var spacing: CGFloat?
|
||||
public var alignment: Alignment
|
||||
|
||||
public init(
|
||||
|
@ -34,7 +34,7 @@ public struct GridItem {
|
|||
alignment: Alignment? = nil
|
||||
) {
|
||||
self.size = size
|
||||
self.spacing = spacing ?? 4
|
||||
self.spacing = spacing
|
||||
self.alignment = alignment ?? .center
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,389 @@
|
|||
// Copyright 2020 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 7/1/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@usableFromInline
|
||||
enum GridRowKey: LayoutValueKey {
|
||||
@usableFromInline
|
||||
static let defaultValue: GridRowID? = nil
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
enum GridCellColumns: LayoutValueKey {
|
||||
@usableFromInline
|
||||
static let defaultValue: Int = 1
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
enum GridCellAnchor: LayoutValueKey {
|
||||
@usableFromInline
|
||||
static let defaultValue: UnitPoint? = nil
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
enum GridColumnAlignment: LayoutValueKey {
|
||||
@usableFromInline
|
||||
static let defaultValue: HorizontalAlignment? = nil
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
enum GridCellUnsizedAxes: LayoutValueKey {
|
||||
@usableFromInline
|
||||
static let defaultValue: Axis.Set = []
|
||||
}
|
||||
|
||||
public extension View {
|
||||
@inlinable
|
||||
func gridCellColumns(_ count: Int) -> some View {
|
||||
layoutValue(key: GridCellColumns.self, value: count)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
func gridCellAnchor(_ anchor: UnitPoint) -> some View {
|
||||
layoutValue(key: GridCellAnchor.self, value: anchor)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
func gridColumnAlignment(_ guide: HorizontalAlignment) -> some View {
|
||||
layoutValue(key: GridColumnAlignment.self, value: guide)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
func gridCellUnsizedAxes(_ axes: Axis.Set) -> some View {
|
||||
layoutValue(key: GridCellUnsizedAxes.self, value: axes)
|
||||
}
|
||||
}
|
||||
|
||||
@usableFromInline final class GridRowID: CustomDebugStringConvertible {
|
||||
@usableFromInline
|
||||
let alignment: VerticalAlignment?
|
||||
|
||||
@usableFromInline
|
||||
init(alignment: VerticalAlignment?) {
|
||||
self.alignment = alignment
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
var debugDescription: String {
|
||||
"\(ObjectIdentifier(self))"
|
||||
}
|
||||
}
|
||||
|
||||
@frozen
|
||||
public struct GridRow<Content: View>: View {
|
||||
@usableFromInline
|
||||
let id: GridRowID
|
||||
@usableFromInline
|
||||
let content: Content
|
||||
|
||||
@inlinable
|
||||
public init(alignment: VerticalAlignment? = nil, @ViewBuilder content: () -> Content) {
|
||||
id = .init(alignment: alignment)
|
||||
self.content = content()
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
content
|
||||
.layoutValue(key: GridRowKey.self, value: id)
|
||||
}
|
||||
}
|
||||
|
||||
@frozen
|
||||
public struct Grid<Content>: View, Layout where Content: View {
|
||||
@usableFromInline
|
||||
let alignment: Alignment
|
||||
@usableFromInline
|
||||
let horizontalSpacing: CGFloat?
|
||||
@usableFromInline
|
||||
let verticalSpacing: CGFloat?
|
||||
@usableFromInline
|
||||
let content: Content
|
||||
|
||||
@inlinable
|
||||
public init(
|
||||
alignment: Alignment = .center,
|
||||
horizontalSpacing: CGFloat? = nil,
|
||||
verticalSpacing: CGFloat? = nil,
|
||||
@ViewBuilder content: () -> Content
|
||||
) {
|
||||
self.alignment = alignment
|
||||
self.horizontalSpacing = horizontalSpacing
|
||||
self.verticalSpacing = verticalSpacing
|
||||
self.content = content()
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
LayoutView(layout: self, content: content)
|
||||
}
|
||||
|
||||
public struct Cache {
|
||||
struct Row {
|
||||
let id: GridRowID?
|
||||
var columns: [Column]
|
||||
|
||||
struct Column {
|
||||
let subview: LayoutSubview
|
||||
var span: Int
|
||||
let flexibility: (width: Bool, height: Bool)
|
||||
}
|
||||
}
|
||||
|
||||
var rows = [Row]()
|
||||
var columnWidths = [Int: CGFloat]()
|
||||
var rowHeights = [CGFloat]()
|
||||
var horizontalSpacing = [Int: CGFloat]()
|
||||
var verticalSpacing = [Int: CGFloat]()
|
||||
var columnAlignments = [Int: HorizontalAlignment]()
|
||||
}
|
||||
|
||||
public func makeCache(subviews: Subviews) -> Cache {
|
||||
subviews.reduce(into: Cache()) { partialResult, subview in
|
||||
let id = subview[GridRowKey.self]
|
||||
let size = subview.sizeThatFits(ProposedViewSize.infinity)
|
||||
let column = Cache.Row.Column(
|
||||
subview: subview,
|
||||
span: subview[GridCellColumns.self],
|
||||
flexibility: (size.width == .infinity, size.height == .infinity)
|
||||
)
|
||||
if id != nil && id === partialResult.rows.last?.id {
|
||||
partialResult.rows[partialResult.rows.count - 1].columns.append(column)
|
||||
} else {
|
||||
partialResult.rows.append(Cache.Row(id: id, columns: [column]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func sizeThatFits(
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Subviews,
|
||||
cache: inout Cache
|
||||
) -> CGSize {
|
||||
cache.columnWidths.removeAll()
|
||||
cache.rowHeights.removeAll()
|
||||
|
||||
let rows = cache.rows.count
|
||||
var columns = cache.rows
|
||||
.map { $0.columns.reduce(into: 0) { $0 += $1.span } }
|
||||
.max() ?? 0
|
||||
|
||||
let proposal = proposal.replacingUnspecifiedDimensions()
|
||||
|
||||
for (rowIndex, row) in cache.rows.enumerated() {
|
||||
guard row.id == nil else { continue }
|
||||
for columnIndex in 0..<row.columns.count {
|
||||
cache.rows[rowIndex].columns[columnIndex].span = columns
|
||||
}
|
||||
}
|
||||
|
||||
for (rowIndex, row) in cache.rows.enumerated() {
|
||||
var spannedColumns = 0
|
||||
for (columnIndex, column) in row.columns.enumerated() {
|
||||
if row.columns.indices.contains(columnIndex + 1) {
|
||||
if let overrideSpacing = horizontalSpacing {
|
||||
cache.horizontalSpacing[spannedColumns + (column.span) - 1] = overrideSpacing
|
||||
} else {
|
||||
let distance = column.subview.spacing.distance(
|
||||
to: row.columns[columnIndex + 1].subview.spacing,
|
||||
along: .horizontal
|
||||
)
|
||||
if distance > cache
|
||||
.horizontalSpacing[spannedColumns + column.span - 1, default: .zero]
|
||||
{
|
||||
cache.horizontalSpacing[spannedColumns + column.span - 1] = distance
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cache.rows.indices.contains(rowIndex + 1),
|
||||
cache.rows[rowIndex + 1].columns.indices.contains(columnIndex)
|
||||
{
|
||||
if let overrideSpacing = verticalSpacing {
|
||||
cache.verticalSpacing[rowIndex] = overrideSpacing
|
||||
} else {
|
||||
let distance = column.subview.spacing.distance(
|
||||
to: cache.rows[rowIndex + 1].columns[columnIndex].subview.spacing,
|
||||
along: .vertical
|
||||
)
|
||||
if distance > cache.verticalSpacing[rowIndex, default: .zero] {
|
||||
cache.verticalSpacing[rowIndex] = distance
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
spannedColumns += column.span
|
||||
}
|
||||
if spannedColumns > columns {
|
||||
columns = spannedColumns
|
||||
}
|
||||
}
|
||||
|
||||
let totalHorizontalSpacing = cache.horizontalSpacing.reduce(into: .zero) { $0 += $1.value }
|
||||
let totalVerticalSpacing = cache.verticalSpacing.reduce(into: .zero) { $0 += $1.value }
|
||||
|
||||
let divviedWidth = (proposal.width - totalHorizontalSpacing) / CGFloat(columns)
|
||||
let divviedHeight = (proposal.height - totalVerticalSpacing) / CGFloat(rows)
|
||||
|
||||
var flexRows = 0
|
||||
var flexHeight = (proposal.height - totalVerticalSpacing)
|
||||
for row in cache.rows {
|
||||
var maxHeight: CGFloat = .zero
|
||||
var rowHeight = divviedHeight
|
||||
var spannedColumns = 0
|
||||
var hasFlexItems = false
|
||||
for column in row.columns {
|
||||
guard !column.flexibility.width && !column.flexibility.height else {
|
||||
hasFlexItems = true
|
||||
spannedColumns += column.span
|
||||
continue
|
||||
}
|
||||
let spacing = ((spannedColumns - 1)..<(spannedColumns + column.span - 2))
|
||||
.reduce(into: .zero) { $0 += cache.horizontalSpacing[$1, default: .zero] }
|
||||
let size = column.subview.dimensions(in: .init(
|
||||
width: divviedWidth * CGFloat(column.span) + spacing,
|
||||
height: rowHeight
|
||||
))
|
||||
let columnAlignment = column.subview[GridColumnAlignment.self]
|
||||
for columnIndex in spannedColumns..<(spannedColumns + column.span) {
|
||||
if (size.width / CGFloat(column.span)) > cache.columnWidths[columnIndex, default: .zero] {
|
||||
cache.columnWidths[columnIndex] = size.width / CGFloat(column.span)
|
||||
}
|
||||
if let columnAlignment = columnAlignment {
|
||||
cache.columnAlignments[columnIndex] = columnAlignment
|
||||
}
|
||||
}
|
||||
if size.height > rowHeight {
|
||||
rowHeight = size.height
|
||||
}
|
||||
if size.height > maxHeight {
|
||||
maxHeight = size.height
|
||||
}
|
||||
spannedColumns += column.span
|
||||
}
|
||||
cache.rowHeights.append(maxHeight)
|
||||
flexHeight -= maxHeight
|
||||
if hasFlexItems {
|
||||
flexRows += 1
|
||||
}
|
||||
}
|
||||
|
||||
flexHeight /= CGFloat(flexRows)
|
||||
|
||||
var height = CGFloat.zero
|
||||
let flexWidth = divviedWidth
|
||||
|
||||
for (rowIndex, row) in cache.rows.enumerated() {
|
||||
let rowHeight = flexHeight + cache.rowHeights[rowIndex]
|
||||
var spannedColumns = 0
|
||||
for column in row.columns {
|
||||
guard column.flexibility.width || column.flexibility.height else {
|
||||
spannedColumns += column.span
|
||||
continue
|
||||
}
|
||||
let unsizedAxes = column.subview[GridCellUnsizedAxes.self]
|
||||
let size = column.subview.dimensions(in: .init(
|
||||
width: unsizedAxes.contains(.horizontal)
|
||||
? (spannedColumns..<(spannedColumns + column.span))
|
||||
.reduce(into: .zero) { $0 += cache.columnWidths[$1, default: .zero] }
|
||||
: flexWidth * CGFloat(column.span),
|
||||
height: unsizedAxes.contains(.vertical)
|
||||
? cache.rowHeights[rowIndex]
|
||||
: rowHeight
|
||||
))
|
||||
let eachColumnWidth = size.width / CGFloat(column.span)
|
||||
for columnIndex in spannedColumns..<(spannedColumns + column.span) {
|
||||
if eachColumnWidth > cache.columnWidths[columnIndex, default: .zero] {
|
||||
cache.columnWidths[columnIndex] = eachColumnWidth
|
||||
}
|
||||
}
|
||||
if size.height > cache.rowHeights[rowIndex] {
|
||||
cache.rowHeights[rowIndex] = size.height
|
||||
}
|
||||
spannedColumns += column.span
|
||||
}
|
||||
height += cache.rowHeights[rowIndex]
|
||||
}
|
||||
|
||||
return .init(
|
||||
width: cache.columnWidths.values
|
||||
.reduce(into: CGFloat.zero) { $0 += $1 } + totalHorizontalSpacing,
|
||||
height: height + totalVerticalSpacing
|
||||
)
|
||||
}
|
||||
|
||||
public func placeSubviews(
|
||||
in bounds: CGRect,
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Subviews,
|
||||
cache: inout Cache
|
||||
) {
|
||||
var y = bounds.minY
|
||||
for (rowIndex, row) in cache.rows.enumerated() {
|
||||
var x = bounds.minX
|
||||
let rowHeight = cache.rowHeights[rowIndex]
|
||||
var spannedColumns = 0
|
||||
|
||||
for column in row.columns {
|
||||
let spacing = (spannedColumns..<(spannedColumns + column.span - 1))
|
||||
.reduce(into: .zero) { $0 += cache.horizontalSpacing[$1, default: .zero] }
|
||||
let width = (spannedColumns..<(spannedColumns + column.span))
|
||||
.reduce(into: .zero) { $0 += cache.columnWidths[$1, default: .zero] } + spacing
|
||||
let proposal = ProposedViewSize(
|
||||
width: width,
|
||||
height: rowHeight
|
||||
)
|
||||
let dimensions = column.subview.dimensions(in: proposal)
|
||||
let anchor = column.subview[GridCellAnchor.self] ?? UnitPoint(
|
||||
x: dimensions.width == 0
|
||||
? 0
|
||||
: dimensions[
|
||||
cache.columnAlignments[spannedColumns, default: alignment.horizontal]
|
||||
] / dimensions.width,
|
||||
y: dimensions.height == 0
|
||||
? 0
|
||||
: dimensions[row.id?.alignment ?? alignment.vertical] / dimensions.height
|
||||
)
|
||||
column.subview.place(
|
||||
at: .init(
|
||||
x: x + (width * anchor.x) - (dimensions.width * anchor.x),
|
||||
y: y + (rowHeight * anchor.y) - (dimensions.height * anchor.y)
|
||||
),
|
||||
proposal: proposal
|
||||
)
|
||||
for spannedColumnIndex in spannedColumns..<(spannedColumns + column.span) {
|
||||
x += cache.columnWidths[spannedColumnIndex, default: .zero]
|
||||
x += cache.horizontalSpacing[spannedColumnIndex, default: .zero]
|
||||
}
|
||||
spannedColumns += column.span
|
||||
}
|
||||
y += rowHeight + cache.verticalSpacing[rowIndex, default: .zero]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension Grid where Content == EmptyView {
|
||||
init(
|
||||
alignment: Alignment = .center,
|
||||
horizontalSpacing: CGFloat? = nil,
|
||||
verticalSpacing: CGFloat? = nil
|
||||
) {
|
||||
self.alignment = alignment
|
||||
self.horizontalSpacing = horizontalSpacing
|
||||
self.verticalSpacing = verticalSpacing
|
||||
content = EmptyView()
|
||||
}
|
||||
}
|
|
@ -20,7 +20,8 @@ import Foundation
|
|||
public struct LazyHGrid<Content>: _PrimitiveView where Content: View {
|
||||
let rows: [GridItem]
|
||||
let alignment: VerticalAlignment
|
||||
let spacing: CGFloat
|
||||
@_spi(TokamakCore)
|
||||
public let spacing: CGFloat?
|
||||
let pinnedViews: PinnedScrollableViews
|
||||
let content: Content
|
||||
|
||||
|
@ -33,7 +34,7 @@ public struct LazyHGrid<Content>: _PrimitiveView where Content: View {
|
|||
) {
|
||||
self.rows = rows
|
||||
self.alignment = alignment
|
||||
self.spacing = spacing ?? 8
|
||||
self.spacing = spacing
|
||||
self.pinnedViews = pinnedViews
|
||||
self.content = content()
|
||||
}
|
||||
|
@ -46,5 +47,5 @@ public struct _LazyHGridProxy<Content> where Content: View {
|
|||
|
||||
public var rows: [GridItem] { subject.rows }
|
||||
public var content: Content { subject.content }
|
||||
public var spacing: CGFloat { subject.spacing }
|
||||
public var spacing: CGFloat { subject.spacing ?? 8 }
|
||||
}
|
||||
|
|
|
@ -20,7 +20,8 @@ import Foundation
|
|||
public struct LazyVGrid<Content>: _PrimitiveView where Content: View {
|
||||
let columns: [GridItem]
|
||||
let alignment: HorizontalAlignment
|
||||
let spacing: CGFloat
|
||||
@_spi(TokamakCore)
|
||||
public let spacing: CGFloat?
|
||||
let pinnedViews: PinnedScrollableViews
|
||||
let content: Content
|
||||
|
||||
|
@ -33,7 +34,7 @@ public struct LazyVGrid<Content>: _PrimitiveView where Content: View {
|
|||
) {
|
||||
self.columns = columns
|
||||
self.alignment = alignment
|
||||
self.spacing = spacing ?? 8
|
||||
self.spacing = spacing
|
||||
self.pinnedViews = pinnedViews
|
||||
self.content = content()
|
||||
}
|
||||
|
@ -46,5 +47,5 @@ public struct _LazyVGridProxy<Content> where Content: View {
|
|||
|
||||
public var columns: [GridItem] { subject.columns }
|
||||
public var content: Content { subject.content }
|
||||
public var spacing: CGFloat { subject.spacing }
|
||||
public var spacing: CGFloat? { subject.spacing }
|
||||
}
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
// Created by Carson Katri on 06/29/2020.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// A scrollable view along a given axis.
|
||||
///
|
||||
/// By default, your app will overflow without the ability to scroll. Embed it in a `ScrollView`
|
||||
|
@ -49,6 +51,10 @@ public struct ScrollView<Content>: _PrimitiveView where Content: View {
|
|||
self.showsIndicators = showsIndicators
|
||||
self.content = content()
|
||||
}
|
||||
|
||||
public func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
|
||||
visitor.visit(content)
|
||||
}
|
||||
}
|
||||
|
||||
extension ScrollView: ParentView {
|
||||
|
@ -67,3 +73,59 @@ public struct PinnedScrollableViews: OptionSet {
|
|||
public static let sectionHeaders: Self = .init(rawValue: 1 << 0)
|
||||
public static let sectionFooters: Self = .init(rawValue: 1 << 1)
|
||||
}
|
||||
|
||||
extension ScrollView: Layout {
|
||||
public func sizeThatFits(
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Subviews,
|
||||
cache: inout ()
|
||||
) -> CGSize {
|
||||
let proposal = proposal.replacingUnspecifiedDimensions()
|
||||
if axes.isEmpty {
|
||||
return proposal
|
||||
}
|
||||
let contentProposal = ProposedViewSize(
|
||||
width: axes.contains(.horizontal) ? nil : proposal.width,
|
||||
height: axes.contains(.vertical) ? nil : proposal.height
|
||||
)
|
||||
let contentSize = subviews.reduce(into: CGSize.zero) {
|
||||
let size = $1.sizeThatFits(contentProposal)
|
||||
if size.width > $0.width {
|
||||
$0.width = size.width
|
||||
}
|
||||
if size.height > $0.height {
|
||||
$0.height = size.height
|
||||
}
|
||||
}
|
||||
return .init(
|
||||
width: axes.contains(.horizontal) ? proposal.width : contentSize.width,
|
||||
height: axes.contains(.vertical) ? proposal.height : contentSize.height
|
||||
)
|
||||
}
|
||||
|
||||
public func placeSubviews(
|
||||
in bounds: CGRect,
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Subviews,
|
||||
cache: inout ()
|
||||
) {
|
||||
let contentProposal = ProposedViewSize(
|
||||
width: axes.contains(.horizontal) ? nil : proposal.width,
|
||||
height: axes.contains(.vertical) ? nil : proposal.height
|
||||
)
|
||||
for subview in subviews {
|
||||
if axes.contains(.horizontal) && axes.contains(.vertical) {
|
||||
subview.place(
|
||||
at: .init(x: bounds.midX, y: bounds.midY),
|
||||
anchor: .center,
|
||||
proposal: contentProposal
|
||||
)
|
||||
} else {
|
||||
subview.place(
|
||||
at: bounds.origin,
|
||||
proposal: contentProposal
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,7 +36,14 @@ public struct Text: _PrimitiveView, Equatable {
|
|||
let modifiers: [_Modifier]
|
||||
|
||||
@Environment(\.self)
|
||||
var environment
|
||||
var _environment: EnvironmentValues
|
||||
|
||||
@_spi(TokamakCore)
|
||||
public var environmentOverride: EnvironmentValues?
|
||||
|
||||
var environment: EnvironmentValues {
|
||||
environmentOverride ?? _environment
|
||||
}
|
||||
|
||||
public static func == (lhs: Text, rhs: Text) -> Bool {
|
||||
lhs.storage == rhs.storage
|
||||
|
|
|
@ -83,6 +83,7 @@ public struct _TextFieldProxy<Label: View> {
|
|||
public var onCommit: () -> () { subject.onCommit }
|
||||
public var onEditingChanged: (Bool) -> () { subject.onEditingChanged }
|
||||
public var textFieldStyle: _AnyTextFieldStyle { subject.environment.textFieldStyle }
|
||||
public var environment: EnvironmentValues { subject.environment }
|
||||
public var foregroundColor: AnyColorBox.ResolvedValue? {
|
||||
guard let foregroundColor = subject.environment.foregroundColor else {
|
||||
return nil
|
||||
|
|
|
@ -176,6 +176,9 @@ public typealias Toggle = TokamakCore.Toggle
|
|||
public typealias VStack = TokamakCore.VStack
|
||||
public typealias ZStack = TokamakCore.ZStack
|
||||
|
||||
public typealias Grid = TokamakCore.Grid
|
||||
public typealias GridRow = TokamakCore.GridRow
|
||||
|
||||
// MARK: Special Views
|
||||
|
||||
public typealias View = TokamakCore.View
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
|
||||
import JavaScriptKit
|
||||
import TokamakCore
|
||||
|
||||
@_spi(TokamakStaticHTML)
|
||||
import TokamakStaticHTML
|
||||
|
||||
public typealias HTML = TokamakStaticHTML.HTML
|
||||
|
@ -32,6 +34,7 @@ public struct DynamicHTML<Content>: View, AnyDynamicHTML {
|
|||
public let attributes: [HTMLAttribute: String]
|
||||
public let listeners: [String: Listener]
|
||||
let content: Content
|
||||
let visitContent: (ViewVisitor) -> ()
|
||||
|
||||
fileprivate let cachedInnerHTML: String?
|
||||
|
||||
|
@ -43,6 +46,10 @@ public struct DynamicHTML<Content>: View, AnyDynamicHTML {
|
|||
public var body: Never {
|
||||
neverBody("HTML")
|
||||
}
|
||||
|
||||
public func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
|
||||
visitContent(visitor)
|
||||
}
|
||||
}
|
||||
|
||||
public extension DynamicHTML where Content: StringProtocol {
|
||||
|
@ -57,6 +64,7 @@ public extension DynamicHTML where Content: StringProtocol {
|
|||
self.listeners = listeners
|
||||
self.content = content
|
||||
cachedInnerHTML = String(content)
|
||||
visitContent = { _ in }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,13 +73,14 @@ extension DynamicHTML: ParentView where Content: View {
|
|||
_ tag: String,
|
||||
_ attributes: [HTMLAttribute: String] = [:],
|
||||
listeners: [String: Listener] = [:],
|
||||
@ViewBuilder content: () -> Content
|
||||
@ViewBuilder content: @escaping () -> Content
|
||||
) {
|
||||
self.tag = tag
|
||||
self.attributes = attributes
|
||||
self.listeners = listeners
|
||||
self.content = content()
|
||||
cachedInnerHTML = nil
|
||||
visitContent = { $0.visit(content()) }
|
||||
}
|
||||
|
||||
@_spi(TokamakCore)
|
||||
|
@ -89,3 +98,10 @@ public extension DynamicHTML where Content == EmptyView {
|
|||
self = DynamicHTML(tag, attributes, listeners: listeners) { EmptyView() }
|
||||
}
|
||||
}
|
||||
|
||||
@_spi(TokamakStaticHTML)
|
||||
extension DynamicHTML: HTMLConvertible {
|
||||
public func attributes(useDynamicLayout: Bool) -> [HTMLAttribute: String] {
|
||||
attributes
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,12 @@
|
|||
// Created by Jed Fox on 06/28/2020.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@_spi(TokamakCore)
|
||||
import TokamakCore
|
||||
|
||||
@_spi(TokamakStaticHTML)
|
||||
import TokamakStaticHTML
|
||||
|
||||
extension TextField: DOMPrimitive where Label == Text {
|
||||
|
@ -39,19 +44,18 @@ extension TextField: DOMPrimitive where Label == Text {
|
|||
}
|
||||
}
|
||||
|
||||
var renderedBody: AnyView {
|
||||
var attributes: [HTMLAttribute: String] {
|
||||
let proxy = _TextFieldProxy(self)
|
||||
|
||||
return AnyView(DynamicHTML("input", [
|
||||
return [
|
||||
"type": proxy.textFieldStyle is RoundedBorderTextFieldStyle ? "search" : "text",
|
||||
.value: proxy.textBinding.wrappedValue,
|
||||
"placeholder": _TextProxy(proxy.label).rawText,
|
||||
"style": """
|
||||
\(css(for: proxy.textFieldStyle)) \
|
||||
\(proxy.foregroundColor.map { "color: \($0.cssValue);" } ?? "")
|
||||
""",
|
||||
"class": className(for: proxy.textFieldStyle),
|
||||
], listeners: [
|
||||
]
|
||||
}
|
||||
|
||||
var listeners: [String: Listener] {
|
||||
let proxy = _TextFieldProxy(self)
|
||||
return [
|
||||
"focus": { _ in proxy.onEditingChanged(true) },
|
||||
"blur": { _ in proxy.onEditingChanged(false) },
|
||||
"keypress": { event in if event.key == "Enter" { proxy.onCommit() } },
|
||||
|
@ -60,6 +64,75 @@ extension TextField: DOMPrimitive where Label == Text {
|
|||
proxy.textBinding.wrappedValue = newValue
|
||||
}
|
||||
},
|
||||
]))
|
||||
]
|
||||
}
|
||||
|
||||
var renderedBody: AnyView {
|
||||
let proxy = _TextFieldProxy(self)
|
||||
return AnyView(DynamicHTML(
|
||||
"input",
|
||||
attributes.merging([
|
||||
"style": """
|
||||
\(css(for: proxy.textFieldStyle)) \
|
||||
\(proxy.foregroundColor.map { "color: \($0.cssValue);" } ?? "")
|
||||
""",
|
||||
"class": className(for: proxy.textFieldStyle),
|
||||
], uniquingKeysWith: { $1 }),
|
||||
listeners: listeners
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@_spi(TokamakStaticHTML)
|
||||
extension TextField: HTMLConvertible, DOMNodeConvertible, Layout, _AnyLayout, Animatable
|
||||
where Label == Text
|
||||
{
|
||||
public typealias AnimatableData = EmptyAnimatableData
|
||||
public var tag: String { "input" }
|
||||
public func attributes(useDynamicLayout: Bool) -> [HTMLAttribute: String] {
|
||||
if useDynamicLayout {
|
||||
return attributes
|
||||
.merging(["style": "padding: 0; border: none;"], uniquingKeysWith: { $0 + $1 })
|
||||
} else {
|
||||
return attributes
|
||||
}
|
||||
}
|
||||
|
||||
public func sizeThatFits(
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Subviews,
|
||||
cache: inout ()
|
||||
) -> CGSize {
|
||||
let proxy = _TextFieldProxy(self)
|
||||
var content = Text(proxy.textBinding.wrappedValue)
|
||||
content.environmentOverride = proxy.environment
|
||||
let contentSize = proxy.environment.measureText(
|
||||
content,
|
||||
proposal,
|
||||
proxy.environment
|
||||
)
|
||||
var label = proxy.label
|
||||
label.environmentOverride = proxy.environment
|
||||
let labelSize = proxy.environment.measureText(
|
||||
label,
|
||||
proposal,
|
||||
proxy.environment
|
||||
)
|
||||
let proposal = proposal.replacingUnspecifiedDimensions()
|
||||
return .init(
|
||||
width: max(proposal.width, max(contentSize.width, labelSize.width)),
|
||||
height: max(contentSize.height, labelSize.height)
|
||||
)
|
||||
}
|
||||
|
||||
public func placeSubviews(
|
||||
in bounds: CGRect,
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Subviews,
|
||||
cache: inout ()
|
||||
) {
|
||||
for subview in subviews {
|
||||
subview.place(at: bounds.origin, proposal: proposal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -94,6 +94,9 @@ public typealias VStack = TokamakCore.VStack
|
|||
public typealias ZStack = TokamakCore.ZStack
|
||||
public typealias Link = TokamakCore.Link
|
||||
|
||||
public typealias Grid = TokamakCore.Grid
|
||||
public typealias GridRow = TokamakCore.GridRow
|
||||
|
||||
// MARK: Special Views
|
||||
|
||||
public typealias View = TokamakCore.View
|
||||
|
|
|
@ -26,3 +26,17 @@ extension Link: _HTMLPrimitive {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
@_spi(TokamakStaticHTML)
|
||||
extension Link: HTMLConvertible {
|
||||
public var tag: String { "a" }
|
||||
public func attributes(useDynamicLayout: Bool) -> [HTMLAttribute: String] {
|
||||
["href": _LinkProxy(self).destination.absoluteString, "class": "_tokamak-link"]
|
||||
}
|
||||
|
||||
public func primitiveVisitor<V>(useDynamicLayout: Bool) -> ((V) -> ())? where V: ViewVisitor {
|
||||
{
|
||||
$0.visit(_LinkProxy(self).label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,8 +36,7 @@ extension LazyHGrid: _HTMLPrimitive {
|
|||
_LazyHGridProxy(self).rows.last
|
||||
}
|
||||
|
||||
@_spi(TokamakStaticHTML)
|
||||
public var renderedBody: AnyView {
|
||||
var styles: String {
|
||||
var styles = """
|
||||
display: grid;
|
||||
grid-template-rows: \(_LazyHGridProxy(self)
|
||||
|
@ -55,8 +54,28 @@ extension LazyHGrid: _HTMLPrimitive {
|
|||
styles += "align-items: \(lastRow.alignment.vertical.cssValue);"
|
||||
}
|
||||
styles += "grid-gap: \(_LazyHGridProxy(self).spacing)px;"
|
||||
return AnyView(HTML("div", ["style": styles]) {
|
||||
return styles
|
||||
}
|
||||
|
||||
@_spi(TokamakStaticHTML)
|
||||
public var renderedBody: AnyView {
|
||||
AnyView(HTML("div", ["style": styles]) {
|
||||
_LazyHGridProxy(self).content
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@_spi(TokamakStaticHTML)
|
||||
extension LazyHGrid: HTMLConvertible {
|
||||
public var tag: String { "div" }
|
||||
public func attributes(useDynamicLayout: Bool) -> [HTMLAttribute: String] {
|
||||
guard !useDynamicLayout else { return [:] }
|
||||
return ["style": styles]
|
||||
}
|
||||
|
||||
public func primitiveVisitor<V>(useDynamicLayout: Bool) -> ((V) -> ())? where V: ViewVisitor {
|
||||
{
|
||||
$0.visit(_LazyHGridProxy(self).content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,8 +36,7 @@ extension LazyVGrid: _HTMLPrimitive {
|
|||
_LazyVGridProxy(self).columns.last
|
||||
}
|
||||
|
||||
@_spi(TokamakStaticHTML)
|
||||
public var renderedBody: AnyView {
|
||||
var styles: String {
|
||||
var styles = """
|
||||
display: grid;
|
||||
grid-template-columns: \(_LazyVGridProxy(self)
|
||||
|
@ -54,9 +53,29 @@ extension LazyVGrid: _HTMLPrimitive {
|
|||
styles += "justify-items: \(lastCol.alignment.horizontal.cssValue);"
|
||||
styles += "align-items: \(lastCol.alignment.vertical.cssValue);"
|
||||
}
|
||||
styles += "grid-gap: \(_LazyVGridProxy(self).spacing)px;"
|
||||
return AnyView(HTML("div", ["style": styles]) {
|
||||
styles += "grid-gap: \(_LazyVGridProxy(self).spacing ?? 8)px;"
|
||||
return styles
|
||||
}
|
||||
|
||||
@_spi(TokamakStaticHTML)
|
||||
public var renderedBody: AnyView {
|
||||
AnyView(HTML("div", ["style": styles]) {
|
||||
_LazyVGridProxy(self).content
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@_spi(TokamakStaticHTML)
|
||||
extension LazyVGrid: HTMLConvertible {
|
||||
public var tag: String { "div" }
|
||||
public func attributes(useDynamicLayout: Bool) -> [HTMLAttribute: String] {
|
||||
guard !useDynamicLayout else { return [:] }
|
||||
return ["style": styles]
|
||||
}
|
||||
|
||||
public func primitiveVisitor<V>(useDynamicLayout: Bool) -> ((V) -> ())? where V: ViewVisitor {
|
||||
{
|
||||
$0.visit(_LazyVGridProxy(self).content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,3 +46,15 @@ extension ScrollView: _HTMLPrimitive, SpacerContainer {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
@_spi(TokamakStaticHTML)
|
||||
extension ScrollView: HTMLConvertible {
|
||||
public var tag: String { "div" }
|
||||
|
||||
public func attributes(useDynamicLayout: Bool) -> [HTMLAttribute: String] {
|
||||
["style": """
|
||||
\(axes.contains(.horizontal) ? "overflow-x: auto; width: 100%;" : "overflow-x: hidden;")
|
||||
\(axes.contains(.vertical) ? "overflow-y: auto; height: 100%;" : "overflow-y: hidden;")
|
||||
"""]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ benchmark("render Text (StackReconciler)") {
|
|||
}
|
||||
|
||||
benchmark("render Text (FiberReconciler)") {
|
||||
StaticHTMLFiberRenderer().render(Text("Hello, world!"))
|
||||
_ = StaticHTMLFiberRenderer().render(Text("Hello, world!"))
|
||||
}
|
||||
|
||||
benchmark("render ForEach(100) (StackReconciler)") {
|
||||
|
@ -56,7 +56,7 @@ benchmark("render ForEach(100) (StackReconciler)") {
|
|||
}
|
||||
|
||||
benchmark("render ForEach(100) (FiberReconciler)") {
|
||||
StaticHTMLFiberRenderer().render(ForEach(1..<100) { Text("\($0)") })
|
||||
_ = StaticHTMLFiberRenderer().render(ForEach(1..<100) { Text("\($0)") })
|
||||
}
|
||||
|
||||
benchmark("render ForEach(1000) (StackReconciler)") {
|
||||
|
@ -64,7 +64,7 @@ benchmark("render ForEach(1000) (StackReconciler)") {
|
|||
}
|
||||
|
||||
benchmark("render ForEach(1000) (FiberReconciler)") {
|
||||
StaticHTMLFiberRenderer().render(ForEach(1..<1000) { Text("\($0)") })
|
||||
_ = StaticHTMLFiberRenderer().render(ForEach(1..<1000) { Text("\($0)") })
|
||||
}
|
||||
|
||||
struct RecursiveView: View {
|
||||
|
@ -88,7 +88,7 @@ benchmark("render RecursiveView(1000) (StackReconciler)") {
|
|||
}
|
||||
|
||||
benchmark("render RecursiveView(1000) (FiberReconciler)") {
|
||||
StaticHTMLFiberRenderer().render(RecursiveView(1000))
|
||||
_ = StaticHTMLFiberRenderer().render(RecursiveView(1000))
|
||||
}
|
||||
|
||||
Benchmark.main()
|
||||
|
|
|
@ -0,0 +1,285 @@
|
|||
// Copyright 2022 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 2/4/23.
|
||||
//
|
||||
|
||||
#if os(macOS) && swift(>=5.7)
|
||||
import SwiftUI
|
||||
import TokamakStaticHTML
|
||||
import XCTest
|
||||
|
||||
final class GridTests: XCTestCase {
|
||||
@available(macOS 13.0, *)
|
||||
func testGrid() async {
|
||||
await compare(size: .init(width: 500, height: 500)) {
|
||||
SwiftUI.Grid(alignment: .bottomTrailing, horizontalSpacing: 10, verticalSpacing: 10) {
|
||||
GridRow(alignment: .top) {
|
||||
SwiftUI.Rectangle()
|
||||
.fill(Color(white: 0))
|
||||
SwiftUI.Rectangle()
|
||||
.fill(Color(white: 0.25))
|
||||
SwiftUI.Rectangle()
|
||||
.fill(Color(white: 0.4))
|
||||
}
|
||||
GridRow(alignment: .top) {
|
||||
SwiftUI.Rectangle()
|
||||
.fill(Color(white: 0))
|
||||
SwiftUI.Rectangle()
|
||||
.fill(Color(white: 0.25))
|
||||
SwiftUI.Rectangle()
|
||||
.fill(Color(white: 0.4))
|
||||
SwiftUI.Rectangle()
|
||||
.fill(Color(white: 0.6))
|
||||
}
|
||||
}
|
||||
} to: {
|
||||
TokamakStaticHTML.Grid(
|
||||
alignment: .bottomTrailing,
|
||||
horizontalSpacing: 10,
|
||||
verticalSpacing: 10
|
||||
) {
|
||||
GridRow(alignment: .top) {
|
||||
Rectangle()
|
||||
.fill(Color(white: 0))
|
||||
Rectangle()
|
||||
.fill(Color(white: 0.25))
|
||||
Rectangle()
|
||||
.fill(Color(white: 0.4))
|
||||
}
|
||||
GridRow(alignment: .top) {
|
||||
Rectangle()
|
||||
.fill(Color(white: 0))
|
||||
Rectangle()
|
||||
.fill(Color(white: 0.25))
|
||||
Rectangle()
|
||||
.fill(Color(white: 0.4))
|
||||
Rectangle()
|
||||
.fill(Color(white: 0.6))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 13.0, *)
|
||||
func testGridCellColumns() async {
|
||||
await compare(size: .init(width: 500, height: 500)) {
|
||||
SwiftUI.Grid(alignment: .bottomTrailing, horizontalSpacing: 10, verticalSpacing: 10) {
|
||||
GridRow(alignment: .top) {
|
||||
SwiftUI.Rectangle()
|
||||
.fill(Color(white: 0))
|
||||
SwiftUI.Rectangle()
|
||||
.fill(Color(white: 0.25))
|
||||
.gridCellColumns(2)
|
||||
SwiftUI.Rectangle()
|
||||
.fill(Color(white: 0.4))
|
||||
}
|
||||
GridRow(alignment: .top) {
|
||||
SwiftUI.Rectangle()
|
||||
.fill(Color(white: 0))
|
||||
SwiftUI.Rectangle()
|
||||
.fill(Color(white: 0.25))
|
||||
SwiftUI.Rectangle()
|
||||
.fill(Color(white: 0.4))
|
||||
SwiftUI.Rectangle()
|
||||
.fill(Color(white: 0.6))
|
||||
}
|
||||
}
|
||||
} to: {
|
||||
TokamakStaticHTML.Grid(
|
||||
alignment: .bottomTrailing,
|
||||
horizontalSpacing: 10,
|
||||
verticalSpacing: 10
|
||||
) {
|
||||
GridRow(alignment: .top) {
|
||||
Rectangle()
|
||||
.fill(Color(white: 0))
|
||||
Rectangle()
|
||||
.fill(Color(white: 0.25))
|
||||
.gridCellColumns(2)
|
||||
Rectangle()
|
||||
.fill(Color(white: 0.4))
|
||||
}
|
||||
GridRow(alignment: .top) {
|
||||
Rectangle()
|
||||
.fill(Color(white: 0))
|
||||
Rectangle()
|
||||
.fill(Color(white: 0.25))
|
||||
Rectangle()
|
||||
.fill(Color(white: 0.4))
|
||||
Rectangle()
|
||||
.fill(Color(white: 0.6))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 13.0, *)
|
||||
func testGridColumnAlignment() async {
|
||||
await compare(size: .init(width: 500, height: 500)) {
|
||||
SwiftUI.Grid(alignment: .bottomTrailing, horizontalSpacing: 10, verticalSpacing: 10) {
|
||||
GridRow(alignment: .top) {
|
||||
SwiftUI.Rectangle()
|
||||
.fill(Color(white: 0))
|
||||
SwiftUI.Rectangle()
|
||||
.fill(Color(white: 0.25))
|
||||
.frame(width: 15)
|
||||
.gridColumnAlignment(.trailing)
|
||||
SwiftUI.Rectangle()
|
||||
.fill(Color(white: 0.4))
|
||||
}
|
||||
GridRow(alignment: .top) {
|
||||
SwiftUI.Rectangle()
|
||||
.fill(Color(white: 0))
|
||||
SwiftUI.Rectangle()
|
||||
.fill(Color(white: 0.25))
|
||||
SwiftUI.Rectangle()
|
||||
.fill(Color(white: 0.4))
|
||||
}
|
||||
}
|
||||
} to: {
|
||||
TokamakStaticHTML.Grid(
|
||||
alignment: .bottomTrailing,
|
||||
horizontalSpacing: 10,
|
||||
verticalSpacing: 10
|
||||
) {
|
||||
GridRow(alignment: .top) {
|
||||
Rectangle()
|
||||
.fill(Color(white: 0))
|
||||
Rectangle()
|
||||
.fill(Color(white: 0.25))
|
||||
.frame(width: 15)
|
||||
.gridColumnAlignment(.trailing)
|
||||
Rectangle()
|
||||
.fill(Color(white: 0.4))
|
||||
}
|
||||
GridRow(alignment: .top) {
|
||||
Rectangle()
|
||||
.fill(Color(white: 0))
|
||||
Rectangle()
|
||||
.fill(Color(white: 0.25))
|
||||
Rectangle()
|
||||
.fill(Color(white: 0.4))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 13.0, *)
|
||||
func testGridCellAnchor() async {
|
||||
await compare(size: .init(width: 500, height: 500)) {
|
||||
SwiftUI.Grid(alignment: .topLeading, horizontalSpacing: 10, verticalSpacing: 10) {
|
||||
GridRow(alignment: .top) {
|
||||
SwiftUI.Rectangle()
|
||||
.fill(Color(white: 0))
|
||||
.frame(width: 15, height: 15)
|
||||
.gridCellAnchor(.bottomTrailing)
|
||||
SwiftUI.Rectangle()
|
||||
.fill(Color(white: 0))
|
||||
SwiftUI.Rectangle()
|
||||
.fill(Color(white: 0))
|
||||
}
|
||||
GridRow(alignment: .top) {
|
||||
SwiftUI.Rectangle()
|
||||
.fill(Color(white: 0))
|
||||
SwiftUI.Rectangle()
|
||||
.fill(Color(white: 0))
|
||||
SwiftUI.Rectangle()
|
||||
.fill(Color(white: 0))
|
||||
.frame(width: 15, height: 15)
|
||||
.gridCellAnchor(.leading)
|
||||
}
|
||||
}
|
||||
} to: {
|
||||
TokamakStaticHTML.Grid(alignment: .topLeading, horizontalSpacing: 10, verticalSpacing: 10) {
|
||||
GridRow(alignment: .top) {
|
||||
Rectangle()
|
||||
.fill(Color(white: 0))
|
||||
.frame(width: 15, height: 15)
|
||||
.gridCellAnchor(.bottomTrailing)
|
||||
Rectangle()
|
||||
.fill(Color(white: 0))
|
||||
Rectangle()
|
||||
.fill(Color(white: 0))
|
||||
}
|
||||
GridRow(alignment: .top) {
|
||||
Rectangle()
|
||||
.fill(Color(white: 0))
|
||||
Rectangle()
|
||||
.fill(Color(white: 0))
|
||||
Rectangle()
|
||||
.fill(Color(white: 0))
|
||||
.frame(width: 15, height: 15)
|
||||
.gridCellAnchor(.leading)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 13.0, *)
|
||||
func testGridCellUnsizedAxes() async {
|
||||
await compare(size: .init(width: 500, height: 500)) {
|
||||
SwiftUI.Grid(alignment: .bottomTrailing, horizontalSpacing: 10, verticalSpacing: 10) {
|
||||
GridRow(alignment: .top) {
|
||||
SwiftUI.Rectangle()
|
||||
.fill(Color(white: 0))
|
||||
.frame(width: 100, height: 100)
|
||||
SwiftUI.Rectangle()
|
||||
.fill(Color(white: 0.25))
|
||||
.frame(width: 100, height: 100)
|
||||
}
|
||||
Rectangle()
|
||||
.fill(Color(white: 0))
|
||||
.frame(height: 5)
|
||||
.gridCellUnsizedAxes(.horizontal)
|
||||
GridRow(alignment: .top) {
|
||||
SwiftUI.Rectangle()
|
||||
.fill(Color(white: 0))
|
||||
.frame(width: 100, height: 100)
|
||||
SwiftUI.Rectangle()
|
||||
.fill(Color(white: 0.25))
|
||||
.frame(width: 100, height: 100)
|
||||
}
|
||||
}
|
||||
} to: {
|
||||
TokamakStaticHTML.Grid(
|
||||
alignment: .bottomTrailing,
|
||||
horizontalSpacing: 10,
|
||||
verticalSpacing: 10
|
||||
) {
|
||||
GridRow(alignment: .top) {
|
||||
Rectangle()
|
||||
.fill(Color(white: 0))
|
||||
.frame(width: 100, height: 100)
|
||||
Rectangle()
|
||||
.fill(Color(white: 0.25))
|
||||
.frame(width: 100, height: 100)
|
||||
}
|
||||
Rectangle()
|
||||
.fill(Color(white: 0))
|
||||
.frame(height: 5)
|
||||
.gridCellUnsizedAxes(.horizontal)
|
||||
GridRow(alignment: .top) {
|
||||
Rectangle()
|
||||
.fill(Color(white: 0))
|
||||
.frame(width: 100, height: 100)
|
||||
Rectangle()
|
||||
.fill(Color(white: 0.25))
|
||||
.frame(width: 100, height: 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,112 @@
|
|||
// Copyright 2022 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 2/4/23.
|
||||
//
|
||||
|
||||
#if os(macOS)
|
||||
import SwiftUI
|
||||
import TokamakStaticHTML
|
||||
import XCTest
|
||||
|
||||
final class LazyGridTests: XCTestCase {
|
||||
@available(macOS 13.0, *)
|
||||
func testLazyVGrid() async {
|
||||
await compare(size: .init(width: 500, height: 500)) {
|
||||
SwiftUI.LazyVGrid(columns: [.init(.adaptive(minimum: 100))], spacing: 10) {
|
||||
ForEach(0..<10) { _ in
|
||||
Rectangle()
|
||||
.fill(Color(white: 0))
|
||||
.frame(height: 100)
|
||||
}
|
||||
}
|
||||
} to: {
|
||||
TokamakStaticHTML.LazyVGrid(columns: [.init(.adaptive(minimum: 100))], spacing: 10) {
|
||||
ForEach(0..<10) { _ in
|
||||
Rectangle()
|
||||
.fill(Color(white: 0))
|
||||
.frame(height: 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
await compare(size: .init(width: 500, height: 500)) {
|
||||
SwiftUI.LazyVGrid(
|
||||
columns: [.init(.fixed(100)), .init(.fixed(100), spacing: 0), .init(.flexible())],
|
||||
spacing: 10
|
||||
) {
|
||||
ForEach(0..<10) { _ in
|
||||
Rectangle()
|
||||
.fill(Color(white: 0))
|
||||
.frame(height: 100)
|
||||
}
|
||||
}
|
||||
} to: {
|
||||
TokamakStaticHTML.LazyVGrid(
|
||||
columns: [.init(.fixed(100)), .init(.fixed(100), spacing: 0), .init(.flexible())],
|
||||
spacing: 10
|
||||
) {
|
||||
ForEach(0..<10) { _ in
|
||||
Rectangle()
|
||||
.fill(Color(white: 0))
|
||||
.frame(height: 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 13.0, *)
|
||||
func testLazyHGrid() async {
|
||||
await compare(size: .init(width: 500, height: 500)) {
|
||||
SwiftUI.LazyHGrid(rows: [.init(.adaptive(minimum: 100))], spacing: 10) {
|
||||
ForEach(0..<10) { _ in
|
||||
Rectangle()
|
||||
.fill(Color(white: 0))
|
||||
.frame(width: 100)
|
||||
}
|
||||
}
|
||||
} to: {
|
||||
TokamakStaticHTML.LazyHGrid(rows: [.init(.adaptive(minimum: 100))], spacing: 10) {
|
||||
ForEach(0..<10) { _ in
|
||||
Rectangle()
|
||||
.fill(Color(white: 0))
|
||||
.frame(width: 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
await compare(size: .init(width: 500, height: 500)) {
|
||||
SwiftUI.LazyHGrid(
|
||||
rows: [.init(.fixed(100)), .init(.fixed(100), spacing: 0), .init(.flexible())],
|
||||
spacing: 10
|
||||
) {
|
||||
ForEach(0..<10) { _ in
|
||||
Rectangle()
|
||||
.fill(Color(white: 0))
|
||||
.frame(width: 100)
|
||||
}
|
||||
}
|
||||
} to: {
|
||||
TokamakStaticHTML.LazyHGrid(
|
||||
rows: [.init(.fixed(100)), .init(.fixed(100), spacing: 0), .init(.flexible())],
|
||||
spacing: 10
|
||||
) {
|
||||
ForEach(0..<10) { _ in
|
||||
Rectangle()
|
||||
.fill(Color(white: 0))
|
||||
.frame(width: 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,40 @@
|
|||
// Copyright 2022 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 2/4/23.
|
||||
//
|
||||
|
||||
#if os(macOS)
|
||||
import SwiftUI
|
||||
import TokamakStaticHTML
|
||||
import XCTest
|
||||
|
||||
final class LinkTests: XCTestCase {
|
||||
func testLink() async {
|
||||
await compare(size: .init(width: 500, height: 500)) {
|
||||
SwiftUI.Link(destination: URL(string: "https://tokamak.dev")!) {
|
||||
Rectangle()
|
||||
.fill(Color(white: 0))
|
||||
.frame(width: 250, height: 100)
|
||||
}
|
||||
} to: {
|
||||
TokamakStaticHTML.Link(destination: URL(string: "https://tokamak.dev")!) {
|
||||
Rectangle()
|
||||
.fill(TokamakStaticHTML.Color(white: 0))
|
||||
.frame(width: 250, height: 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -115,12 +115,12 @@ final class VisualRenderingTests: XCTestCase {
|
|||
|
||||
let matchA = verifySnapshot(
|
||||
matching: gradient,
|
||||
as: .image(size: size),
|
||||
as: .image(precision: 0.9, size: size),
|
||||
testName: "\(#function)A"
|
||||
)
|
||||
let matchB = verifySnapshot(
|
||||
matching: gradient,
|
||||
as: .image(size: size),
|
||||
as: .image(precision: 0.9, size: size),
|
||||
testName: "\(#function)B"
|
||||
)
|
||||
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.1 KiB |
Loading…
Reference in New Issue