This commit is contained in:
Carson Katri 2023-02-05 21:12:30 +00:00 committed by GitHub
commit ba2a98f9b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1422 additions and 66 deletions

View File

@ -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 {}

View File

@ -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?> {

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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 }
}

View File

@ -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) }
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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 {}

View File

@ -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
}
}

View File

@ -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()
}
}

View File

@ -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 }
}

View File

@ -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 }
}

View File

@ -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
)
}
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
}
}

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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;")
"""]
}
}

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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