Skip to content

Commit

Permalink
1.2.0
Browse files Browse the repository at this point in the history
nathantannar4 committed Apr 17, 2024
1 parent 54fd495 commit 52f22b9
Showing 19 changed files with 2,365 additions and 28 deletions.
20 changes: 20 additions & 0 deletions Sources/Turbocharger/Sources/Core/IdentifiableBox.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// Copyright (c) Nathan Tannar
//

import SwiftUI

@frozen
public struct IdentifiableBox<Value, ID: Hashable>: Identifiable {
public var value: Value
public var keyPath: KeyPath<Value, ID>

public var id: ID { value[keyPath: keyPath] }

@inlinable
public init(_ value: Value, id keyPath: KeyPath<Value, ID>) {
self.value = value
self.keyPath = keyPath
}
}

47 changes: 47 additions & 0 deletions Sources/Turbocharger/Sources/Extensions/Alignment+Extensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// Copyright (c) Nathan Tannar
//

import SwiftUI

extension NSTextAlignment {
public init(
alignment: HorizontalAlignment,
layoutDirection: LayoutDirection
) {
switch alignment {
case .center:
self.init(alignment: TextAlignment.center, layoutDirection: layoutDirection)
case .trailing:
self.init(alignment: TextAlignment.trailing, layoutDirection: layoutDirection)
default:
self.init(alignment: TextAlignment.leading, layoutDirection: layoutDirection)
}
}

public init(
alignment: TextAlignment,
layoutDirection: LayoutDirection
) {
switch alignment {
case .center:
self = .center
default:
switch layoutDirection {
case .rightToLeft:
if alignment == .leading {
self = .right
} else {
self = .left
}
default:
if alignment == .leading {
self = .left
} else {
self = .right
}
}
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//
// Copyright (c) Nathan Tannar
//

import SwiftUI

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
extension AnyShapeStyle {

public var color: Color? {
func resolve(provider: Any) -> Color? {
let className = String(describing: type(of: provider))
if className.hasPrefix("ColorBox") {
guard MemoryLayout<Color>.size == MemoryLayout<AnyObject>.size else {
return nil
}
let color = unsafeBitCast(provider as AnyObject, to: Color.self)
return color
} else if className.hasPrefix("GradientBox") {
guard
let provider = Mirror(reflecting: provider).descendant("base", "color", "provider"),
let resolved = resolve(provider: provider)
else {
return nil
}
return resolved
} else {
return nil
}
}

guard
let box = Mirror(reflecting: self).descendant("storage", "box"),
let resolved = resolve(provider: box)
else {
return nil
}
return resolved
}
}

// MARK: - Previews

@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)
struct AnyShapeStyle_Previews: PreviewProvider {
static var previews: some View {
VStack {
HStack {
Rectangle()
.fill(AnyShapeStyle(Color.red).color ?? .clear)

Rectangle()
.fill(Color.red)
}

HStack {
Rectangle()
.fill(AnyShapeStyle(Color.red.gradient).color ?? .clear)

Rectangle()
.fill(Color.red.gradient)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
//
// Copyright (c) Nathan Tannar
//

import SwiftUI

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
extension AttributedString {

#if os(iOS) || os(tvOS) || os(watchOS)
public func toUIKit(
in environment: EnvironmentValues = EnvironmentValues()
) -> AttributedString {
var result = self
for run in result.runs {
result[run.range].setAttributes(run.attributes.toUIKit(in: environment))
}
return result
}
#elseif os(macOS)
public func toAppKit(
in environment: EnvironmentValues = EnvironmentValues()
) -> AttributedString {
var result = self
for run in result.runs {
result[run.range].setAttributes(run.attributes.toAppKit(in: environment))
}
return result
}
#endif
}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
extension AttributeContainer {

#if os(iOS) || os(tvOS) || os(watchOS)
public func toUIKit(
in environment: EnvironmentValues = EnvironmentValues()
) -> AttributeContainer {
var attributes = self
if let font = attributes.swiftUI.font, attributes.uiKit.font == nil {
attributes.uiKit.font = font.toUIFont()
}
if let foregroundColor = attributes.swiftUI.foregroundColor, attributes.uiKit.foregroundColor == nil {
attributes.uiKit.foregroundColor = foregroundColor.toUIColor()
}
if let backgroundColor = attributes.swiftUI.backgroundColor, attributes.uiKit.backgroundColor == nil {
attributes.uiKit.backgroundColor = backgroundColor.toUIColor()
}
if let strikethroughStyle = attributes.swiftUI.strikethroughStyle {
attributes.uiKit.strikethroughStyle = NSUnderlineStyle(strikethroughStyle)
let color = Mirror(reflecting: strikethroughStyle).descendant("color") as? Color
if let color {
attributes.uiKit.strikethroughColor = color.toUIColor()
}
}
if let underlineStyle = attributes.swiftUI.underlineStyle {
attributes.uiKit.underlineStyle = NSUnderlineStyle(underlineStyle)
let color = Mirror(reflecting: underlineStyle).descendant("color") as? Color
if let color {
attributes.uiKit.underlineColor = color.toUIColor()
}
}
if let kern = attributes.swiftUI.kern {
attributes.uiKit.kern = kern
}
if let tracking = attributes.swiftUI.tracking {
attributes.uiKit.tracking = tracking
}
if let baselineOffset = attributes.swiftUI.baselineOffset {
attributes.uiKit.baselineOffset = baselineOffset
}
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = environment.lineSpacing
attributes.uiKit.paragraphStyle = paragraphStyle
return attributes
}
#elseif os(macOS)
public func toAppKit(
in environment: EnvironmentValues = EnvironmentValues()
) -> AttributeContainer {
var attributes = self
if let font = attributes.swiftUI.font, attributes.appKit.font == nil {
attributes.appKit.font = font.toNSFont()
}
if let foregroundColor = attributes.swiftUI.foregroundColor, attributes.appKit.foregroundColor == nil {
attributes.appKit.foregroundColor = foregroundColor.toNSColor()
}
if let backgroundColor = attributes.swiftUI.backgroundColor, attributes.appKit.backgroundColor == nil {
attributes.appKit.backgroundColor = backgroundColor.toNSColor()
}
if let strikethroughStyle = attributes.swiftUI.strikethroughStyle {
attributes.appKit.strikethroughStyle = NSUnderlineStyle(strikethroughStyle)
let color = Mirror(reflecting: strikethroughStyle).descendant("color") as? Color
if let color {
attributes.appKit.strikethroughColor = color.toNSColor()
}
}
if let underlineStyle = attributes.swiftUI.underlineStyle {
attributes.appKit.underlineStyle = NSUnderlineStyle(underlineStyle)
let color = Mirror(reflecting: underlineStyle).descendant("color") as? Color
if let color {
attributes.appKit.underlineColor = color.toNSColor()
}
}
if let kern = attributes.swiftUI.kern {
attributes.appKit.kern = kern
}
if let tracking = attributes.swiftUI.tracking {
attributes.appKit.tracking = tracking
}
if let baselineOffset = attributes.swiftUI.baselineOffset {
attributes.appKit.baselineOffset = baselineOffset
}
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = environment.lineSpacing
attributes.appKit.paragraphStyle = paragraphStyle
return attributes
}
#endif
}

extension NSParagraphStyle: @unchecked Sendable { }

#if os(macOS)
extension NSFont: @unchecked Sendable { }
#endif
84 changes: 84 additions & 0 deletions Sources/Turbocharger/Sources/Extensions/Color+Extensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//
// Copyright (c) Nathan Tannar
//

import SwiftUI

extension Color {

@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
public func toCGColor() -> CGColor {
if let cgColor = cgColor {
return cgColor
} else {
return PlatformRepresentable(self).cgColor
}
}

#if os(iOS) || os(tvOS) || os(watchOS)
@available(iOS 14.0, tvOS 14.0, watchOS 7.0, *)
public func toUIColor() -> UIColor {
toPlatformValue()
}
#endif

#if os(macOS)
@available(macOS 11.0, *)
public func toNSColor() -> NSColor {
toPlatformValue()
}
#endif

#if os(macOS)
typealias PlatformRepresentable = NSColor
#elseif os(iOS) || os(tvOS) || os(watchOS)
typealias PlatformRepresentable = UIColor
#endif
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
private func toPlatformValue() -> PlatformRepresentable {
func resolve(provider: Any) -> PlatformRepresentable {
let className = String(describing: type(of: provider))
switch className {
case "OpacityColor":
let mirror = Mirror(reflecting: provider)
guard
let opacity = mirror.descendant("opacity") as? Double,
let base = mirror.descendant("base")
else {
return PlatformRepresentable(self)
}
let color = resolve(provider: base)
return color.withAlphaComponent(opacity)

case "NamedColor":
let mirror = Mirror(reflecting: provider)
guard
let name = mirror.descendant("name") as? String
else {
return PlatformRepresentable(self)
}
let bundle = mirror.descendant("bundle") as? Bundle
#if os(iOS) || os(tvOS)
return UIColor { traits in
UIColor(named: name, in: bundle, compatibleWith: traits) ?? UIColor(self)
}
#elseif os(watchOS)
return UIColor(named: name) ?? UIColor(self)
#else
return NSColor(named: name, bundle: bundle) ?? NSColor(self)
#endif
default:
return provider as? PlatformRepresentable ?? PlatformRepresentable(self)
}
}

// Need to extract the UIColor since because SwiftUI's UIColor init
// from a Color does not work for dynamic colors when set on UIView's
guard let base = Mirror(reflecting: self).descendant("provider", "base") else {
return PlatformRepresentable(self)
}
return resolve(provider: base)
}
#endif
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// Copyright (c) Nathan Tannar
//

import SwiftUI

#if os(macOS)
extension NSEdgeInsets {
public init(
edgeInsets: EdgeInsets,
layoutDirection: LayoutDirection
) {
self.init(
top: edgeInsets.top,
left: layoutDirection == .leftToRight ? edgeInsets.leading : edgeInsets.trailing,
bottom: edgeInsets.bottom,
right: layoutDirection == .leftToRight ? edgeInsets.trailing : edgeInsets.leading
)
}
}
#else
extension UIEdgeInsets {
public init(
edgeInsets: EdgeInsets,
layoutDirection: LayoutDirection
) {
self.init(
top: edgeInsets.top,
left: layoutDirection == .leftToRight ? edgeInsets.leading : edgeInsets.trailing,
bottom: edgeInsets.bottom,
right: layoutDirection == .leftToRight ? edgeInsets.trailing : edgeInsets.leading
)
}
}
#endif
179 changes: 179 additions & 0 deletions Sources/Turbocharger/Sources/Extensions/Environment+Extensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
//
// Copyright (c) Nathan Tannar
//

import SwiftUI
import Engine

/// Accessors to internal keys ``Engine.EnvironmentKeyVisitor``
extension EnvironmentValues {

/// The value for the ``.labelsHidden(_)`` modifier
public var labelsHidden: Bool {
self["LabelsHiddenKey", default: false]
}

/// The value for the ``.foregroundStyle(_)``/``.foregroundColor(_)`` modifier
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
public var foregroundStyle: AnyShapeStyle {
self["ForegroundStyleKey", default: AnyShapeStyle(.foreground)]
}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
public var foregroundColor: Color? {
foregroundStyle.color
}

/// The value for the ``.tint(_)`` modifier
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
public var tint: AnyShapeStyle {
self["TintKey", default: AnyShapeStyle(.tint)]
}

/// The value for the ``.accentColor(_)`` modifier
public var accentColor: Color {
self["AccentColorKey", default: Color.accentColor]
}

/// The value for the ``.underlineStyle(_)`` modifier
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
public var underlineStyle: Text.LineStyle? {
self["UnderlineStyleKey"]
}

/// The value for the ``.strikethrough(_)`` modifier
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
public var strikethroughStyle: Text.LineStyle? {
self["StrikethroughStyleKey"]
}

/// The value for the ``.kerning(_)`` modifier
@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)
public var kerning: CGFloat {
self["DefaultKerningKey", default: 0]
}

/// The value for the ``.tracking(_)`` modifier
@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)
public var tracking: CGFloat {
self["DefaultTrackingKey", default: 0]
}

/// The value for the ``.baselineOffset(_)`` modifier
@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)
public var baselineOffset: CGFloat {
self["DefaultBaselineOffsetKey", default: 0]
}

/// The value for the ``.lineLimit(_, reservesSpace: Bool)`` modifier
@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)
public var lowerLineLimit: Int? {
self["DefaultBaselineOffsetKey"]
}

/// The value for the ``.textScale(_)`` modifier
@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *)
public var textScale: Text.Scale {
self["TextScaleKey", default: Text.Scale.default]
}

/// The value for the ``.imageScale(_)`` modifier
@available(iOS 13.0, macOS 11.0, tvOS 13.0, watchOS 6.0, *)
public var imageScale: Image.Scale? {
self["ImageScaleKey"]
}

#if os(iOS) || os(tvOS) || os(watchOS)
/// The value for the ``.textInputAutocapitalization(_)`` modifier
@available(iOS 15.0, tvOS 15.0, watchOS 8.0, *)
@available(macOS, unavailable)
public var textInputAutocapitalization: TextInputAutocapitalization {
self["TextInputAutocapitalizationKey", default: TextInputAutocapitalization.never]
}
#endif

#if os(iOS) || os(tvOS)
/// The value for the ``.textContentType(_)`` modifier
@available(iOS 13.0, tvOS 13.0, *)
@available(macOS, unavailable)
@available(watchOS, unavailable)
public var textContentType: UITextContentType? {
self["TextContentTypeKey"]
}
#endif
}

// MARK: - Previews

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
struct EnvironmentValues_Previews: PreviewProvider {
static var previews: some View {
VStack {
EnvironmentValuePreview(keyPath: \.labelsHidden) {
TextField("Label", text: .constant(""))
.fixedSize()
} content: { isHidden in
Text(isHidden.description)
}
.labelsHidden()

EnvironmentValuePreview(keyPath: \.foregroundStyle) {
Text("Hello, World")
} content: { foregroundStyle in
Circle()
.fill(foregroundStyle)
.fixedSize()
}
.foregroundStyle(.red)

EnvironmentValuePreview(keyPath: \.tint) {
Button("Action") { }
} content: { foregroundStyle in
Circle()
.fill(foregroundStyle)
.fixedSize()
}
.tint(.purple)

EnvironmentValuePreview(keyPath: \.minimumScaleFactor) {
Text("Hello, World")
.frame(width: 50)
} content: { minimumScaleFactor in
Text(minimumScaleFactor.description)
}
.lineLimit(1)
.minimumScaleFactor(0.5)
}
}

struct EnvironmentValuePreview<Value, Source: View, Content: View>: View {

var keyPath: KeyPath<EnvironmentValues, Value>
var source: Source
var content: (Value) -> Content

init(
keyPath: KeyPath<EnvironmentValues, Value>,
@ViewBuilder source: () -> Source,
@ViewBuilder content: @escaping (Value) -> Content
) {
self.keyPath = keyPath
self.source = source()
self.content = content
}

var body: some View {
HStack {
source

Divider()
.fixedSize()

EnvironmentValueReader(keyPath) { value in
content(value)
}
}
}
}
}

455 changes: 455 additions & 0 deletions Sources/Turbocharger/Sources/Extensions/Font+Extensions.swift

Large diffs are not rendered by default.

150 changes: 150 additions & 0 deletions Sources/Turbocharger/Sources/Extensions/Image+Extensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
//
// Copyright (c) Nathan Tannar
//

import SwiftUI

extension Image {

#if os(macOS)
typealias PlatformRepresentable = NSImage
#elseif os(iOS) || os(tvOS) || os(watchOS)
typealias PlatformRepresentable = UIImage
#endif

#if os(iOS) || os(tvOS) || os(watchOS)
public func toUIImage(
in environment: EnvironmentValues = EnvironmentValues()
) -> UIImage? {
toPlatformValue(in: environment)
}
#endif

#if os(macOS)
public func toNSImage(
in environment: EnvironmentValues = EnvironmentValues()
) -> NSImage? {
toPlatformValue(in: environment)
}
#endif

fileprivate func toPlatformValue(
in environment: EnvironmentValues
) -> PlatformRepresentable? {
ImageProvider(for: self)?.resolved(in: environment)
}
}

private enum ImageProvider {

case system(String)
case named(String, Bundle?)
case cg(CGImage, CGFloat, Image.Orientation)
case image(Image.PlatformRepresentable)

init?(for image: Image) {
guard let base = Mirror(reflecting: image).descendant("provider", "base") else {
return nil
}

let className = String(describing: type(of: base))
let mirror = Mirror(reflecting: base)
switch className {
case "NamedImageProvider":
guard let name = mirror.descendant("name") as? String else {
return nil
}
if let location = mirror.descendant("location") {
if String(describing: location) == "system" {
self = .system(name)
} else {
let bundle = mirror.descendant("location", "bundle")
self = .named(name, bundle as? Bundle)
}
} else {
self = .named(name, nil)
}

case "\(Image.PlatformRepresentable.self)":
guard let image = base as? Image.PlatformRepresentable else {
return nil
}
self = .image(image)

case "CGImageProvider":
guard
let image = mirror.descendant("image"),
let scale = mirror.descendant("scale") as? CGFloat,
let orientation = mirror.descendant("orientation") as? Image.Orientation
else {
return nil
}
self = .cg(image as! CGImage, scale, orientation)

default:
return nil
}
}

func resolved(in environment: EnvironmentValues) -> Image.PlatformRepresentable? {
switch self {
case .system(let name):
#if os(iOS) || os(tvOS) || os(watchOS)
let scale: UIImage.SymbolScale = {
guard let scale = environment.imageScale else { return .unspecified }
switch scale {
case .small: return .small
case .medium: return .medium
case .large: return .large
@unknown default:
return .unspecified
}
}()
let config = environment.font?.toUIFont().map {
UIImage.SymbolConfiguration(
font: $0,
scale: scale
)
} ?? UIImage.SymbolConfiguration(scale: scale)
return UIImage(
systemName: name,
withConfiguration: config
)
#elseif os(macOS)
if #available(macOS 11.0, *) {
return NSImage(systemSymbolName: name, accessibilityDescription: nil)
}
return nil
#endif
case let .named(name, bundle):
#if os(iOS) || os(tvOS) || os(watchOS)
return UIImage(named: name, in: bundle, with: nil)
#elseif os(macOS)
if #available(macOS 14.0, *), let bundle {
return NSImage(resource: ImageResource(name: name, bundle: bundle))
}
return NSImage(named: name)
#endif
case let .image(image):
return image
case let .cg(image, scale, orientation):
#if os(iOS) || os(tvOS) || os(watchOS)
let orientation: UIImage.Orientation = {
switch orientation {
case .down: return .down
case .downMirrored: return .downMirrored
case .left: return .left
case .leftMirrored: return .leftMirrored
case .right: return .right
case .rightMirrored: return .rightMirrored
case .up: return .up
case .upMirrored: return .upMirrored
}
}()
return UIImage(cgImage: image, scale: scale, orientation: orientation)
#elseif os(macOS)
return NSImage(cgImage: image, size: .zero)
#endif
}
}
}
224 changes: 224 additions & 0 deletions Sources/Turbocharger/Sources/Extensions/Text+Extensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
//
// Copyright (c) Nathan Tannar
//

import SwiftUI

@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
extension Text {

/// Transforms the `Text` to a `String`, using the environment to resolve localized
/// string keys if necessary.
@inlinable
public func resolve(in environment: EnvironmentValues) -> String {
_resolveText(in: environment)
}

/// Transforms the `Text` to a `AttributedString`, using the environment to resolve localized
/// string keys if necessary.
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
public func resolveAttributed(in environment: EnvironmentValues) -> AttributedString {
guard MemoryLayout<Text>.size == MemoryLayout<Text.Layout>.size else {
return AttributedString(resolve(in: environment))
}
return _resolveAttributed(in: environment)
}
}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
extension Text {
private enum Storage {
case verbatim(String)
case anyTextStorage(AnyObject)
}

private enum Modifier {
case color(Color?)
case font(Font?)
case italic
case weight(Font.Weight?)
case kerning(CGFloat)
case tracking(CGFloat)
case baseline(CGFloat)
case rounded
case anyTextModifier(AnyObject)
}

private struct Environment {
var font: Font?
var fontWeight: Font.Weight?
var fontWidth: CGFloat?
var foregroundColor: Color?
var underlineStyle: Text.LineStyle?
var strikethroughStyle: Text.LineStyle?
var kerning: CGFloat?
var tracking: CGFloat?
var baselineOffset: CGFloat?
var isItalic: Bool = false
var isBold: Bool = false
var isMonospaced: Bool = false
var environment: EnvironmentValues

init(environment: EnvironmentValues) {
self.environment = environment
}

var attributes: AttributeContainer {
var attributes = AttributeContainer()
attributes.swiftUI.font = {
var font = font ?? environment.font
if let fontWeight {
font = font?.weight(fontWeight)
}
if #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *), let fontWidth {
font = font?.width(.init(fontWidth))
}
if isItalic {
font = font?.italic()
}
if isBold {
font = font?.bold()
}
if isMonospaced {
font = font?.monospaced()
}
return font
}()
attributes.swiftUI.foregroundColor = foregroundColor ?? environment.foregroundColor
attributes.swiftUI.underlineStyle = underlineStyle ?? environment.underlineStyle
attributes.swiftUI.strikethroughStyle = strikethroughStyle ?? environment.strikethroughStyle
if #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) {
attributes.kern = kerning ?? environment.kerning
attributes.tracking = tracking ?? environment.tracking
attributes.baselineOffset = baselineOffset ?? environment.baselineOffset
} else {
attributes.swiftUI.kern = kerning
attributes.swiftUI.tracking = tracking
attributes.swiftUI.baselineOffset = baselineOffset
}
return attributes
}
}

private struct Layout {
var storage: Storage
var modifiers: [Modifier]
}

private var layout: Text.Layout {
unsafeBitCast(self, to: Text.Layout.self)
}

func _resolveAttributed(in environment: EnvironmentValues) -> AttributedString {
let environment = Environment(environment: environment)
return _resolveAttributed(in: environment)
}

private func _resolveAttributed(in environment: Environment) -> AttributedString {
var environment = environment
for modifier in layout.modifiers.reversed() {
switch modifier {
case .color(let color):
environment.foregroundColor = color
case .font(let font):
environment.font = font
case .italic:
environment.isItalic = true
case .weight(let weight):
environment.fontWeight = weight
case .kerning(let kerning):
environment.kerning = kerning
case .tracking(let tracking):
environment.tracking = tracking
case .baseline(let baseline):
environment.baselineOffset = baseline
case .rounded:
break
case .anyTextModifier(let modifier):
let mirror = Mirror(reflecting: modifier)
let className = String(describing: type(of: modifier))
switch className {
case "TextWidthModifier":
if #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *),
let width = mirror.descendant("width") as? CGFloat
{
environment.fontWidth = width
}
case "TextDesignModifier":
if let design = mirror.descendant("design") as? Font.Design {
environment.isMonospaced = design == .monospaced
}
case "UnderlineTextModifier":
if let lineStyle = mirror.descendant("lineStyle") as? Text.LineStyle {
environment.underlineStyle = lineStyle
}
case "BoldTextModifier":
let isActive = (mirror.descendant("isActive") as? Bool) ?? true
environment.isBold = isActive
default:
break
}
}
}
switch layout.storage {
case .verbatim(let text):
return AttributedString(
text,
attributes: environment.attributes
)
case .anyTextStorage(let storage):
return resolve(storage: storage, environment: environment)
}
}

private func resolve(
storage: Any,
environment: Environment
) -> AttributedString {
let className = String(describing: type(of: storage))
switch className {
case "ConcatenatedTextStorage":
let mirror = Mirror(reflecting: storage)
guard
let first = mirror.descendant("first") as? Text,
let second = mirror.descendant("second") as? Text
else {
fallthrough
}
return first._resolveAttributed(in: environment) + second._resolveAttributed(in: environment)

case "AttachmentTextStorage":
guard let image = Mirror(reflecting: storage).descendant("image") as? Image else {
fallthrough
}
var attributedString = AttributedString(stringLiteral: " ")
#if os(iOS) || os(tvOS) || os(watchOS)
if let image = image.toUIImage() {
if #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *),
let baselineOffset = environment.baselineOffset,
baselineOffset != 0
{
attributedString.attachment = NSTextAttachment(
image: image.withBaselineOffset(fromBottom: baselineOffset)
)
} else {
attributedString.attachment = NSTextAttachment(
image: image
)
}
}
#endif
return attributedString

default:
return AttributedString(
resolve(in: environment.environment),
attributes: environment.attributes
)
}
}
}

#if os(iOS) || os(tvOS) || os(watchOS)
extension NSTextAttachment: @unchecked Sendable { }
#endif
761 changes: 761 additions & 0 deletions Sources/Turbocharger/Sources/View/CollectionView.swift

Large diffs are not rendered by default.

136 changes: 136 additions & 0 deletions Sources/Turbocharger/Sources/View/CollectionViewRepresentable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
//
// Copyright (c) Nathan Tannar
//

import SwiftUI
import Engine

@available(iOS 14.0, *)
@available(macOS, unavailable)
@available(tvOS, unavailable)
@available(watchOS, unavailable)
public protocol CollectionViewLayout: DynamicProperty {

#if os(iOS)
associatedtype UICollectionViewType: UICollectionView

func makeUICollectionView(
context: Context,
options: CollectionViewLayoutOptions
) -> UICollectionViewType

func updateUICollectionView(
_ collectionView: UICollectionViewType,
context: Context
)

func updateUICollectionViewCell(
_ collectionView: UICollectionViewType,
cell: UICollectionViewCell,
kind: HostingConfigurationKind,
indexPath: IndexPath
)
#endif

typealias Context = CollectionViewLayoutContext

}

#if os(iOS)
@available(iOS 14.0, *)
extension CollectionViewLayout {
public func updateUICollectionViewCell(
_ collectionView: UICollectionViewType,
cell: UICollectionViewCell,
kind: HostingConfigurationKind,
indexPath: IndexPath
) { }
}
#endif

@available(iOS 14.0, *)
@available(macOS, unavailable)
@available(tvOS, unavailable)
@available(watchOS, unavailable)
public struct CollectionViewLayoutContext {
public var environment: EnvironmentValues
public var transaction: Transaction
}

@available(iOS 14.0, *)
@available(macOS, unavailable)
@available(tvOS, unavailable)
@available(watchOS, unavailable)
public struct CollectionViewLayoutOptions: OptionSet {
public var rawValue: UInt8
public init(rawValue: UInt8) {
self.rawValue = rawValue
}

/// The `UICollectionViewLayout` should include a header
public static let header = CollectionViewLayoutOptions(rawValue: 1 << 0)

/// The `UICollectionViewLayout` should include a footer
public static let footer = CollectionViewLayoutOptions(rawValue: 1 << 1)
}

@available(iOS 14.0, *)
@available(macOS, unavailable)
@available(tvOS, unavailable)
@available(watchOS, unavailable)
struct CollectionViewLayoutOptionsKey: EnvironmentKey {
static let defaultValue = CollectionViewLayoutOptions()
}

@available(iOS 14.0, *)
@available(macOS, unavailable)
@available(tvOS, unavailable)
@available(watchOS, unavailable)
extension EnvironmentValues {
public var collectionViewLayoutOptions: CollectionViewLayoutOptions {
get { self[CollectionViewLayoutOptionsKey.self] }
set { self[CollectionViewLayoutOptionsKey.self] = newValue }
}
}

@available(iOS 14.0, *)
@available(macOS, unavailable)
@available(tvOS, unavailable)
@available(watchOS, unavailable)
@frozen
public struct CollectionViewListLayout: CollectionViewLayout {

@inlinable
public init() { }

#if os(iOS)
public func makeUICollectionView(
context: Context,
options: CollectionViewLayoutOptions
) -> UICollectionView {
var configuration = UICollectionLayoutListConfiguration(appearance: .plain)
configuration.headerMode = options.contains(.header) ? .supplementary : .none
configuration.footerMode = options.contains(.footer) ? .supplementary : .none
configuration.showsSeparators = false
configuration.backgroundColor = .clear
let layout = UICollectionViewCompositionalLayout.list(using: configuration)
let uiCollectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
uiCollectionView.clipsToBounds = false
uiCollectionView.keyboardDismissMode = .interactive
return uiCollectionView
}

public func updateUICollectionView(
_ collectionView: UICollectionView,
context: Context
) { }
#endif
}

@available(iOS 14.0, *)
@available(macOS, unavailable)
@available(tvOS, unavailable)
@available(watchOS, unavailable)
extension CollectionViewLayout where Self == CollectionViewListLayout {
public static var list: CollectionViewListLayout { .init() }
}
57 changes: 57 additions & 0 deletions Sources/Turbocharger/Sources/View/EnvironmentValueReader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//
// Copyright (c) Nathan Tannar
//

import SwiftUI

/// A view that reads an environment value to derive its content
@frozen
public struct EnvironmentValueReader<Value, Content: View>: View {

@usableFromInline
var value: Environment<Value>

@usableFromInline
var content: (Value) -> Content

@inlinable
public init(
_ keyPath: KeyPath<EnvironmentValues, Value>,
@ViewBuilder content: @escaping (Value) -> Content
) {
self.value = Environment(keyPath)
self.content = content
}

public var body: some View {
content(value.wrappedValue)
}
}

// MARK: - Preview

fileprivate extension EnvironmentValues {
var testFlag: Bool {
get { self[EnvironmentValueReader_Preview.TestFlagKey.self] }
set { self[EnvironmentValueReader_Preview.TestFlagKey.self] = newValue }
}
}

struct EnvironmentValueReader_Preview: PreviewProvider {
enum TestFlagKey: EnvironmentKey {
static let defaultValue: Bool = false
}

static var previews: some View {
VStack {
EnvironmentValueReader(\.testFlag) { flag in
Text(flag.description)
}

EnvironmentValueReader(\.testFlag) { flag in
Text(flag.description)
}
.environment(\.testFlag, true)
}
}
}
2 changes: 2 additions & 0 deletions Sources/Turbocharger/Sources/View/FlowStack.swift
Original file line number Diff line number Diff line change
@@ -212,7 +212,9 @@ struct FlowStack_Previews: PreviewProvider {
var body: some View {
VStack {
Text(width.rounded().description)
#if os(iOS) || os(macOS)
Slider(value: $width, in: 10...375)
#endif

FlowStack {
ScrollView {
15 changes: 0 additions & 15 deletions Sources/Turbocharger/Sources/View/FluidGradient.swift
Original file line number Diff line number Diff line change
@@ -361,21 +361,6 @@ extension CASpringAnimation {
}
}

extension Color {
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
func toCGColor() -> CGColor {
if let cgColor = cgColor {
return cgColor
} else {
#if os(macOS)
return NSColor(self).cgColor
#else
return UIColor(self).cgColor
#endif
}
}
}

extension UnitPoint {
static func random() -> UnitPoint {
UnitPoint(x: CGFloat.random(in: 0...1), y: CGFloat.random(in: 0...1))
22 changes: 20 additions & 2 deletions Sources/Turbocharger/Sources/View/LabeledView.swift
Original file line number Diff line number Diff line change
@@ -100,11 +100,22 @@ extension VerticalAlignment {
}

public struct DefaultLabeledViewStyle: LabeledViewStyle {

@Environment(\.labelsHidden) var labelsHidden

public init() { }

public func makeBody(configuration: LabeledViewStyleConfiguration) -> some View {
HStack(alignment: .label) {
configuration.label
if !labelsHidden {
configuration.label
}

configuration.content
.frame(maxWidth: .infinity, alignment: .trailing)
.frame(
maxWidth: labelsHidden ? nil : .infinity,
alignment: .trailing
)
}
}
}
@@ -140,6 +151,13 @@ struct LabeledViewStyle_Previews: PreviewProvider {
Text("Label")
}

LabeledView {
Text("Content")
} label: {
Text("Label")
}
.labelsHidden()

LabeledView {
Text("Content")
.font(.largeTitle)
2 changes: 2 additions & 0 deletions Sources/Turbocharger/Sources/View/MarqueeText.swift
Original file line number Diff line number Diff line change
@@ -151,7 +151,9 @@ struct MarqueeText_Previews: PreviewProvider {
var body: some View {
VStack(spacing: 12) {
Text(width.description)
#if os(iOS) || os(macOS)
Slider(value: $width, in: 100...300)
#endif
Button("Reset") { id += 1 }

MarqueeText(
12 changes: 1 addition & 11 deletions Sources/Turbocharger/Sources/View/TextReader.swift
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@
//

import SwiftUI
import Engine

/// A view that resolves `Text` with the current environment
@frozen
@@ -33,17 +34,6 @@ public struct TextReader<Content: View>: View {
}
}

@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
extension Text {

/// Transforms the `Text` to a `String`, using the environment to resolve localized
/// string keys if necessary.
@inlinable
public func resolve(in environment: EnvironmentValues) -> String {
_resolveText(in: environment)
}
}

// MARK: - Previews

@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)

0 comments on commit 52f22b9

Please sign in to comment.