Migrate reconciler and TestRenderer to View API (#107)

* Migrate reconciler and AppKit renderer to View API

* Fix building tests, cleanup unused code

* Remove more unused code

* Remove TokamakAppKit, add ParentView/GroupView

* Remove unused code, make testMount pass

* Cleanup more unused code

* Add ValueStorage protocol

* Add getter/setter internal properties on State

* Fix more State test assertions

* Fix all the reconciler tests 🎉
This commit is contained in:
Max Desiatov 2020-04-11 18:21:04 +01:00 committed by GitHub
parent 3d714964e6
commit 9407dd0674
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
134 changed files with 572 additions and 5029 deletions

View File

@ -6,4 +6,5 @@
--operatorfunc nospace
--ifdef noindent
--stripunusedargs closure-only
--disable andOperator
--disable andOperator
--swiftversion 5.2

View File

@ -18,7 +18,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_: UIApplication, didFinishLaunchingWithOptions _: Options?)
-> Bool {
// Override point for customization after application launch.
return true
true
}
func applicationWillResignActive(_: UIApplication) {

View File

@ -30,7 +30,7 @@ enum ElementaryParticles: String, CaseIterable {
}
extension ElementaryParticles: CustomStringConvertible {
var description: String { return rawValue.localizedCapitalized }
var description: String { rawValue.localizedCapitalized }
}
private struct Cells: CellProvider {
@ -39,7 +39,7 @@ private struct Cells: CellProvider {
item: ElementaryParticles,
path: CellPath
) -> AnyNode {
return Label.node(.init(
Label.node(.init(
Style(
[CenterY.equal(to: .parent),
Height.equal(to: 44),
@ -59,7 +59,7 @@ struct CollectionExample: PureLeafComponent {
typealias Props = Null
static func render(props: Props) -> AnyNode {
return CollectionView<Cells>.node(.init(
CollectionView<Cells>.node(.init(
Style(
Edges.equal(to: .parent, inset: 20),
backgroundColor: .white

View File

@ -13,7 +13,7 @@ class ScrollDelegate: NSObject, UIScrollViewDelegate {
var view: UIView?
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return view
view
}
}

View File

@ -15,7 +15,7 @@ private struct Cells: CellProvider {
item: AppRoute,
path: CellPath
) -> AnyNode {
return Label.node(.init(
Label.node(.init(
Style(
[CenterY.equal(to: .parent),
Height.equal(to: 44),
@ -37,7 +37,7 @@ struct List: PureLeafComponent {
}
static func render(props: Props) -> AnyNode {
return ListView<Cells>.node(.init(
ListView<Cells>.node(.init(
Style([
Edges.equal(to: .parent),
]),

View File

@ -15,7 +15,7 @@ struct NavigationModal: PureLeafComponent {
}
static func render(props: Props) -> AnyNode {
return props.isPresented.value ?
props.isPresented.value ?
ModalPresenter.node(
NavigationPresenter<ModalRouter>.node(
.init(
@ -36,7 +36,7 @@ struct SimpleModal: PureLeafComponent {
}
static func render(props: Props) -> AnyNode {
return props.isPresented.value ? ModalPresenter.node(
props.isPresented.value ? ModalPresenter.node(
Animation.node(
Null(),
Button.node(.init(

View File

@ -13,7 +13,7 @@ struct ScrollViewExample: LeafComponent {
typealias Props = Null
static func render(props: Props, hooks: Hooks) -> AnyNode {
return View.node(
View.node(
.init(Style(Edges.equal(to: .safeArea))),
ScrollView.node(
.init(Style(Edges.equal(to: .parent))),

View File

@ -14,7 +14,7 @@ struct ListProvider: CellProvider {
typealias Model = Int
static func cell(props _: Null, item: Int, path _: CellPath) -> AnyNode {
return Label.node(.init(
Label.node(.init(
Style(
[CenterY.equal(to: .parent),
Height.equal(to: 44),

View File

@ -13,17 +13,16 @@ struct ThrobberExample: PureLeafComponent {
typealias Props = Null
static func render(props: Null) -> AnyNode {
return
View.node(
.init(Style(
Edges.equal(to: .parent),
backgroundColor: .black
)),
Throbber.node(.init(
Style(Edges.equal(to: .parent)),
isAnimating: true,
variety: .whiteLarge
))
)
View.node(
.init(Style(
Edges.equal(to: .parent),
backgroundColor: .black
)),
Throbber.node(.init(
Style(Edges.equal(to: .parent)),
isAnimating: true,
variety: .whiteLarge
))
)
}
}

View File

@ -31,7 +31,7 @@ enum AppRoute: String, CaseIterable {
}
extension AppRoute: CustomStringConvertible {
var description: String { return rawValue.localizedCapitalized }
var description: String { rawValue.localizedCapitalized }
}
struct Router: NavigationRouter {

View File

@ -13,7 +13,7 @@ import UIKit
final class ViewController: TokamakViewController {
override var node: AnyNode {
return NavigationPresenter<Router>.node(.init(initial: .list))
NavigationPresenter<Router>.node(.init(initial: .list))
}
}

View File

@ -13,7 +13,7 @@ import TokamakDemo
final class ViewController: TokamakViewController {
override var node: AnyNode {
return View.node(
View.node(
.init(Style([
Edges.equal(to: .parent),
Width.equal(to: 200),

16
Package.resolved Normal file
View File

@ -0,0 +1,16 @@
{
"object": {
"pins": [
{
"package": "Runtime",
"repositoryURL": "https://github.com/wickwirew/Runtime.git",
"state": {
"branch": "master",
"revision": "0a059a96c49b4e8bca085476bc8dc2061921c5b6",
"version": null
}
}
]
},
"version": 1
}

View File

@ -16,10 +16,6 @@ let package = Package(
name: "Tokamak",
targets: ["Tokamak"]
),
.library(
name: "TokamakAppKit",
targets: ["TokamakAppKit"]
),
.library(
name: "TokamakTestRenderer",
targets: ["TokamakTestRenderer"]
@ -28,6 +24,7 @@ let package = Package(
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
.package(url: "https://github.com/wickwirew/Runtime.git", .branch("master")),
],
targets: [
// Targets are the basic building blocks of a package. A target can define
@ -36,23 +33,19 @@ let package = Package(
// in packages which this package depends on.
.target(
name: "Tokamak",
dependencies: []
dependencies: ["Runtime"]
),
.target(
name: "TokamakDemo",
dependencies: ["Tokamak"]
),
.target(
name: "TokamakAppKit",
dependencies: ["Tokamak"]
),
.target(
name: "TokamakTestRenderer",
dependencies: ["Tokamak"]
),
.testTarget(
name: "TokamakTests",
dependencies: ["TokamakTestRenderer"]
dependencies: ["TokamakDemo", "TokamakTestRenderer"]
),
]
)

View File

@ -26,6 +26,6 @@ public struct AnyEquatable: Equatable {
}
public static func ==(lhs: AnyEquatable, rhs: AnyEquatable) -> Bool {
return lhs.equals(rhs.value) || rhs.equals(lhs.value)
lhs.equals(rhs.value) || rhs.equals(lhs.value)
}
}

View File

@ -1,165 +0,0 @@
//
// Node.swift
// Tokamak
//
// Created by Max Desiatov on 30/11/2018.
//
public struct AnyNode: Equatable {
// Equatable can't be automatically derived for `type` property?
public static func ==(lhs: AnyNode, rhs: AnyNode) -> Bool {
return
lhs.type == rhs.type &&
lhs.children == rhs.children &&
lhs.props == rhs.props &&
lhs.ref === rhs.ref
}
public let props: AnyEquatable
public let children: AnyEquatable
let type: ComponentType
public let ref: AnyObject?
public func isSubtypeOf<T>(_: T.Type) -> Bool {
return type.host is T.Type || type.composite is T.Type
}
public func isSubtypeOf<T, U>(_: T.Type, or: U.Type) -> Bool {
return isSubtypeOf(T.self) || isSubtypeOf(U.self)
}
public func isSubtypeOf<T, U, V>(
_: T.Type,
or _: U.Type,
or _: V.Type
) -> Bool {
return isSubtypeOf(T.self) || isSubtypeOf(U.self) || isSubtypeOf(V.self)
}
}
extension Null {
public static func node() -> AnyNode {
return AnyNode(
props: AnyEquatable(Null()),
children: AnyEquatable(Null()),
type: .null,
ref: nil
)
}
}
extension Component where Children == Null {
public static func node(_ props: Props) -> AnyNode {
return node(props, Null())
}
}
extension Component where Props == Null, Children == Null {
public static func node() -> AnyNode {
return node(Null(), Null())
}
}
extension Component where Props: Default, Props.DefaultValue == Props {
public static func node(_ children: Children) -> AnyNode {
return node(Props.defaultValue, children)
}
}
extension Component where Children == [AnyNode] {
public static func node(_ props: Props,
_ child: AnyNode) -> AnyNode {
return node(props, [child])
}
public static func node(_ props: Props) -> AnyNode {
return node(props, [])
}
}
extension Component where Props == Null, Children == [AnyNode] {
public static func node() -> AnyNode {
return node(Null(), [])
}
}
extension Component where Props: Default, Props.DefaultValue == Props,
Children == [AnyNode] {
public static func node(_ child: AnyNode) -> AnyNode {
return node(Props.defaultValue, [child])
}
public static func node() -> AnyNode {
return node(Props.defaultValue, [])
}
}
extension HostComponent {
public static func node(_ props: Props, _ children: Children) -> AnyNode {
return AnyNode(
props: AnyEquatable(props),
children: AnyEquatable(children),
type: .host(self),
ref: nil
)
}
}
extension CompositeComponent {
public static func node(
_ props: Props,
_ children: Children
) -> AnyNode {
return AnyNode(
props: AnyEquatable(props),
children: AnyEquatable(children),
type: .composite(self),
ref: nil
)
}
}
extension RefComponent {
public static func node(
_ props: Props,
_ children: Children,
ref: Ref<RefTarget?>
) -> AnyNode {
return AnyNode(
props: AnyEquatable(props),
children: AnyEquatable(children),
type: .host(self),
ref: ref
)
}
}
extension RefComponent where Children == [AnyNode] {
public static func node(
_ props: Props,
_ child: AnyNode,
ref: Ref<RefTarget?>
) -> AnyNode {
return node(props, [child], ref: ref)
}
public static func node(_ props: Props, ref: Ref<RefTarget?>) -> AnyNode {
return node(props, [], ref: ref)
}
}
extension RefComponent where Children == Null {
public static func node(_ props: Props, ref: Ref<RefTarget?>) -> AnyNode {
return node(props, Null(), ref: ref)
}
}
extension RefComponent where Props: Default, Props.DefaultValue == Props {
public static func node(
_ children: Children,
ref: Ref<RefTarget?>
) -> AnyNode {
return node(Props.defaultValue, children, ref: ref)
}
}

View File

@ -23,7 +23,7 @@ typealias Updater<T> = (inout T) -> ()
public var projectedValue: Binding<Value> { self }
init(get: @escaping () -> Value, set: @escaping (Value) -> ()) {
public init(get: @escaping () -> Value, set: @escaping (Value) -> ()) {
self.get = get
self.set = set
}
@ -39,13 +39,6 @@ typealias Updater<T> = (inout T) -> ()
}
}
// FIXME: absent in the reference interface
extension Binding: Equatable where Value: Equatable {
public static func ==(lhs: Binding<Value>, rhs: Binding<Value>) -> Bool {
lhs.wrappedValue == rhs.wrappedValue
}
}
extension Hooks {
/** Allows a component to have its own state and to be updated when the state
changes. Returns a very simple state container, which on initial call of

View File

@ -0,0 +1,5 @@
//
// Created by Max Desiatov on 08/04/2020.
//
public typealias CGFloat = Double

View File

@ -1,151 +0,0 @@
//
// Component.swift
// Tokamak
//
// Created by Max Desiatov on 07/10/2018.
//
/// Type-erased version of `HostComponent` to work around [PAT restrictions](
/// http://www.russbishop.net/swift-associated-types). Users
/// of Tokamak shouldn't ever need to conform to this protocol directly, use
/// `HostComponent` instead.
public protocol AnyHostComponent {}
public protocol HostComponent: AnyHostComponent, Component {}
public protocol AnyRefComponent {
static func update(ref: AnyObject, with value: Any)
}
public protocol RefComponent: AnyRefComponent, HostComponent {
associatedtype RefTarget
}
extension AnyRefComponent where Self: RefComponent {
public static func update(ref: AnyObject, with value: Any) {
guard let ref = ref as? Ref<RefTarget?>,
let value = value as? RefTarget else {
assertionFailure("failed to cast objects passed to \(#function)")
return
}
ref.value = value
}
}
/// Type-erased version of `CompositeComponent` to work around
/// [PAT restrictions](http://www.russbishop.net/swift-associated-types). Users
/// of Tokamak shouldn't ever need to conform to this protocol directly, use
/// `CompositeComponent` instead.
public protocol AnyCompositeComponent {
static func render(
props: AnyEquatable,
children: AnyEquatable,
hooks: Hooks
) -> AnyNode
}
public protocol Component {
associatedtype Props: Equatable
associatedtype Children: Equatable
static func node(
_ props: Props,
_ children: Children
) -> AnyNode
}
public protocol CompositeComponent: AnyCompositeComponent, Component {
static func render(
props: Props,
children: Children,
hooks: Hooks
) -> AnyNode
}
public extension CompositeComponent {
/// Default implementation of `AnyCompositeComponent` that delegates to
/// `render` requirement in `CompositeComponent` PAT.
static func render(
props: AnyEquatable,
children: AnyEquatable,
hooks: Hooks
) -> AnyNode {
guard let props = props.value as? Props,
let children = children.value as? Children else {
fatalError(
"""
invalid types of `props` and `children` arguments passed to \(#function)
"""
)
}
return render(props: props, children: children, hooks: hooks)
}
}
public protocol PureComponent: CompositeComponent {
static func render(props: Props, children: Children) -> AnyNode
}
public extension PureComponent {
static func render(
props: Props,
children: Children,
hooks: Hooks
) -> AnyNode {
return render(props: props, children: children)
}
}
public protocol LeafComponent: CompositeComponent where Children == Null {
static func render(props: Props, hooks: Hooks) -> AnyNode
}
public extension LeafComponent {
static func render(
props: Props,
children _: Children,
hooks: Hooks
) -> AnyNode {
return render(props: props, hooks: hooks)
}
}
public protocol PureLeafComponent: LeafComponent {
static func render(props: Props) -> AnyNode
}
public extension PureLeafComponent {
static func render(props: Props, hooks: Hooks) -> AnyNode {
return render(props: props)
}
}
enum ComponentType: Equatable {
static func ==(lhs: ComponentType, rhs: ComponentType) -> Bool {
switch (lhs, rhs) {
case let (.host(ltype), .host(rtype)):
return ltype == rtype
case let (.composite(ltype), .composite(rtype)):
return ltype == rtype
default:
return false
}
}
case host(AnyHostComponent.Type)
case composite(AnyCompositeComponent.Type)
case null
var composite: AnyCompositeComponent.Type? {
guard case let .composite(type) = self else { return nil }
return type
}
var host: AnyHostComponent.Type? {
guard case let .host(type) = self else { return nil }
return type
}
}

View File

@ -1,15 +0,0 @@
//
// Alert.swift
// Tokamak
//
// Created by Max Desiatov on 31/12/2018.
//
struct Alert: HostComponent {
struct Props: Equatable {
let title: String?
let message: String?
}
typealias Children = [AnyNode]
}

View File

@ -1,23 +0,0 @@
//
// Animated.swift
// Tokamak
//
// Created by Max Desiatov on 02/01/2019.
//
protocol Animatable: Component {}
struct Animated<T: Component, U: Equatable>: HostComponent {
public struct Props: Equatable {
public struct AnimationCurve {}
public let duration: Second
public let initial: T.Props
public let isRunning: Bool
public let isReversed: Bool
public let keyPath: KeyPath<T.Props, U>
public let target: U
}
public typealias Children = T.Children
}

View File

@ -1,47 +0,0 @@
//
// Button.swift
// Tokamak
//
// Created by Max Desiatov on 02/12/2018.
//
public struct Button: HostComponent {
public struct Props: Equatable, ControlProps, StyleProps, Default {
public static var defaultValue: Props {
return Props()
}
public let handlers: [Event: Handler<()>]
public let style: Style?
public let titleColor: Color?
public let isEnabled: Bool
public let text: String
public init(
_ style: Style? = nil,
handlers: [Event: Handler<()>] = [:],
isEnabled: Bool = true,
onPress: Handler<()>? = nil,
text: String = "",
titleColor: Color? = nil
) {
var handlers = handlers
if let onPress = onPress {
handlers[.touchUpInside] = onPress
}
self.handlers = handlers
self.style = style
self.titleColor = titleColor
self.isEnabled = isEnabled
self.text = text
}
}
public typealias Children = [AnyNode]
}
extension Button.Props: ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
self.init(text: value)
}
}

View File

@ -1,74 +0,0 @@
//
// CollectionView.swift
// Tokamak
//
// Created by Matvii Hodovaniuk on 3/6/19.
//
public struct CollectionView<T: CellProvider>: HostComponent {
public struct Props: Equatable, StyleProps {
public let cellProps: T.Props
public let sections: T.Sections
public let onSelect: Handler<CellPath>?
public let style: Style?
public let scrollOptions: ScrollOptions?
public init(
_ style: Style? = nil,
cellProps: T.Props,
onSelect: Handler<CellPath>? = nil,
scrollOptions: ScrollOptions? = nil,
singleSection: T.Sections.Element
) {
self.cellProps = cellProps
sections = T.Sections.single(section: singleSection)
self.onSelect = onSelect
self.style = style
self.scrollOptions = scrollOptions
}
public init(
_ style: Style? = nil,
cellProps: T.Props,
sections: T.Sections,
onSelect: Handler<CellPath>? = nil,
scrollOptions: ScrollOptions? = nil
) {
self.cellProps = cellProps
self.sections = sections
self.onSelect = onSelect
self.style = style
self.scrollOptions = scrollOptions
}
}
public typealias Children = Null
}
extension CollectionView.Props where T.Props == Null {
public init(
_ style: Style? = nil,
sections: T.Sections,
onSelect: Handler<CellPath>? = nil,
scrollOptions: ScrollOptions? = nil
) {
cellProps = Null()
self.sections = sections
self.onSelect = onSelect
self.style = style
self.scrollOptions = scrollOptions
}
public init(
_ style: Style? = nil,
onSelect: Handler<CellPath>? = nil,
singleSection: T.Sections.Element,
scrollOptions: ScrollOptions? = nil
) {
cellProps = Null()
sections = T.Sections.single(section: singleSection)
self.onSelect = onSelect
self.style = style
self.scrollOptions = scrollOptions
}
}

View File

@ -1,38 +0,0 @@
//
// DatePicker.swift
// Tokamak
//
// Created by Matvii Hodovaniuk on 2/4/19.
//
import Foundation
public struct DatePicker: HostComponent {
public struct Props: Equatable, ControlProps, StyleProps,
ValueControlProps {
public let handlers: EventHandlers
public let style: Style?
public let value: Date
public let valueHandler: Handler<Date>?
public let isEnabled: Bool
public let isAnimated: Bool
public init(
_ style: Style? = nil,
handlers: EventHandlers = [:],
isAnimated: Bool = true,
isEnabled: Bool = true,
value: Date,
valueHandler: Handler<Date>? = nil
) {
self.handlers = handlers
self.style = style
self.value = value
self.valueHandler = valueHandler
self.isEnabled = isEnabled
self.isAnimated = isAnimated
}
}
public typealias Children = Null
}

View File

@ -1,23 +0,0 @@
//
// ImageView.swift
// Tokamak
//
// Created by Matvii Hodovaniuk on 2/17/19.
//
public struct ImageView: HostComponent {
public typealias Children = [AnyNode]
public struct Props: Equatable, StyleProps {
public let image: Image
public let style: Style?
public init(
_ style: Style? = nil,
image: Image
) {
self.image = image
self.style = style
}
}
}

View File

@ -1,45 +0,0 @@
//
// Label.swift
// Tokamak
//
// Created by Max Desiatov on 02/12/2018.
//
public struct Label: HostComponent {
public struct Props: Equatable, StyleProps, Default {
public static var defaultValue: Props {
return Props()
}
public let alignment: TextAlignment
public let lineBreakMode: LineBreakMode
public let numberOfLines: Int
public let style: Style?
public let text: String
public let textColor: Color?
public init(
_ style: Style? = nil,
alignment: TextAlignment = .natural,
lineBreakMode: LineBreakMode = .truncateTail,
numberOfLines: Int = 1,
text: String = "",
textColor: Color? = nil
) {
self.alignment = alignment
self.lineBreakMode = lineBreakMode
self.numberOfLines = numberOfLines
self.text = text
self.textColor = textColor
self.style = style
}
}
public typealias Children = [AnyNode]
}
extension Label.Props: ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
self.init(text: value)
}
}

View File

@ -1,140 +0,0 @@
//
// ListView.swift
// Tokamak
//
// Created by Max Desiatov on 21/01/2019.
//
public struct CellPath {
public let section: Int
public let item: Int
public init(section: Int, item: Int) {
self.section = section
self.item = item
}
}
/** A collection of sections with random access to its elements and
an ability to create a new `SectionedModel` with a single section. The most
basic `SectionedModel` is an array of arrays `[[T]]`, where every element
in the top level array is a section `[T]` and every element `T` is a row/item
in this section.
*/
public protocol SectionedModel: RandomAccessCollection
where Element: RandomAccessCollection, Index == Int, Element.Index == Int {
static func single(section: Element) -> Self
}
extension Array: SectionedModel
where Element: RandomAccessCollection, Element.Index == Int {
public static func single(section: Element) -> [Element] {
return [section]
}
}
public protocol IdentifiedCellProvider {
associatedtype Props: Equatable
associatedtype Identifier: RawRepresentable, CaseIterable
where Identifier.RawValue == String
associatedtype Model: Equatable
associatedtype Sections: SectionedModel & Equatable = [[Model]] where
Sections.Element.Element == Model
static func cell(
props: Props,
item: Model,
path: CellPath
) -> (Identifier, AnyNode)
}
public enum SingleIdentifier: String, CaseIterable {
case single
}
public protocol CellProvider: IdentifiedCellProvider
where Identifier == SingleIdentifier {
static func cell(
props: Props,
item: Model,
path: CellPath
) -> AnyNode
}
extension CellProvider {
public static func cell(
props: Props,
item: Model,
path: CellPath
) -> (Identifier, AnyNode) {
return (.single, cell(props: props, item: item, path: path))
}
}
public struct ListView<T: CellProvider>: HostComponent {
public struct Props: Equatable, StyleProps {
public let cellProps: T.Props
public let model: T.Sections
public let onSelect: Handler<CellPath>?
public let style: Style?
public let scrollOptions: ScrollOptions?
public init(
_ style: Style? = nil,
cellProps: T.Props,
onSelect: Handler<CellPath>? = nil,
scrollOptions: ScrollOptions? = nil,
singleSection: T.Sections.Element
) {
self.cellProps = cellProps
model = T.Sections.single(section: singleSection)
self.onSelect = onSelect
self.style = style
self.scrollOptions = scrollOptions
}
public init(
_ style: Style? = nil,
cellProps: T.Props,
model: T.Sections,
onSelect: Handler<CellPath>? = nil,
scrollOptions: ScrollOptions? = nil
) {
self.cellProps = cellProps
self.model = model
self.onSelect = onSelect
self.style = style
self.scrollOptions = scrollOptions
}
}
public typealias Children = Null
}
extension ListView.Props where T.Props == Null {
public init(
_ style: Style? = nil,
model: T.Sections,
onSelect: Handler<CellPath>? = nil,
scrollOptions: ScrollOptions? = nil
) {
cellProps = Null()
self.model = model
self.onSelect = onSelect
self.style = style
self.scrollOptions = scrollOptions
}
public init(
_ style: Style? = nil,
onSelect: Handler<CellPath>? = nil,
scrollOptions: ScrollOptions? = nil,
singleSection: T.Sections.Element
) {
cellProps = Null()
model = T.Sections.single(section: singleSection)
self.onSelect = onSelect
self.style = style
self.scrollOptions = scrollOptions
}
}

View File

@ -1,18 +0,0 @@
//
// NavigationController.swift
// Tokamak
//
// Created by Max Desiatov on 06/01/2019.
//
public struct NavigationController: HostComponent {
public typealias Children = [AnyNode]
public struct Props: Equatable {
public let hidesBarsWhenKeyboardAppears: Bool?
public let popAnimated: Bool
public let prefersLargeTitles: Bool
public let pushAnimated: Bool
public let onPop: Handler<()>
}
}

View File

@ -1,59 +0,0 @@
//
// ScrollView.swift
// Tokamak
//
// Created by Matvii Hodovaniuk on 2/28/19.
//
public struct ScrollView: HostComponent {
public struct Props: Equatable, StyleProps, Default {
public static var defaultValue: Props {
return Props()
}
public let style: Style?
public let scrollOptions: ScrollOptions
public init(
_ style: Style? = nil,
alwaysBounceHorizontal: Bool = false,
alwaysBounceVertical: Bool = false,
bounces: Bool = true,
bouncesZoom: Bool = true,
contentInset: ScrollOptions.EdgeInsets = ScrollOptions.EdgeInsets(),
indicatorStyle: ScrollOptions.IndicatorStyle = .default,
isDirectionalLockEnabled: Bool = false,
isPagingEnabled: Bool = false,
isScrollEnabled: Bool = true,
maximumZoomScale: Float = 1.0,
minimumZoomScale: Float = 1.0,
scrollIndicatorInsets: ScrollOptions.EdgeInsets = ScrollOptions.EdgeInsets(),
scrollsToTop: Bool = true,
showsHorizontalScrollIndicator: Bool = true,
showsVerticalScrollIndicator: Bool = true,
zoomScale: Float = 1.0
) {
self.style = style
scrollOptions = ScrollOptions(
alwaysBounceHorizontal: alwaysBounceHorizontal,
alwaysBounceVertical: alwaysBounceVertical,
bounces: bounces,
bouncesZoom: bouncesZoom,
contentInset: contentInset,
indicatorStyle: indicatorStyle,
isDirectionalLockEnabled: isDirectionalLockEnabled,
isPagingEnabled: isPagingEnabled,
isScrollEnabled: isScrollEnabled,
maximumZoomScale: maximumZoomScale,
minimumZoomScale: minimumZoomScale,
scrollIndicatorInsets: scrollIndicatorInsets,
scrollsToTop: scrollsToTop,
showsHorizontalScrollIndicator: showsHorizontalScrollIndicator,
showsVerticalScrollIndicator: showsVerticalScrollIndicator,
zoomScale: zoomScale
)
}
}
public typealias Children = AnyNode
}

View File

@ -1,33 +0,0 @@
//
// SegmentedControl.swift
// Tokamak
//
// Created by Max Desiatov on 05/01/2019.
//
public struct SegmentedControl: HostComponent {
public struct Props: Equatable, ControlProps, StyleProps,
ValueControlProps {
public let handlers: EventHandlers
public let style: Style?
public let value: Int
public let valueHandler: Handler<Int>?
public let isEnabled: Bool
public init(
_ style: Style? = nil,
handlers: EventHandlers = [:],
isEnabled: Bool = true,
value: Int,
valueHandler: Handler<Int>? = nil
) {
self.handlers = handlers
self.style = style
self.value = value
self.valueHandler = valueHandler
self.isEnabled = isEnabled
}
}
public typealias Children = [String]
}

View File

@ -1,33 +0,0 @@
//
// Slider.swift
// Tokamak
//
// Created by Max Desiatov on 29/12/2018.
//
public struct Slider: HostComponent {
public struct Props: Equatable, ControlProps, StyleProps,
ValueControlProps {
public let handlers: EventHandlers
public let style: Style?
public let value: Float
public let valueHandler: Handler<Float>?
public let isEnabled: Bool
public init(
_ style: Style? = nil,
handlers: EventHandlers = [:],
isEnabled: Bool = true,
value: Float,
valueHandler: Handler<Float>? = nil
) {
self.handlers = handlers
self.style = style
self.value = value
self.valueHandler = valueHandler
self.isEnabled = isEnabled
}
}
public typealias Children = Null
}

View File

@ -1,101 +0,0 @@
//
// StackView.swift
// Tokamak
//
// Created by Max Desiatov on 02/12/2018.
//
public struct StackView: HostComponent {
public struct Props: Equatable, StyleProps, Default {
public static var defaultValue: Props {
return Props()
}
public enum Alignment {
case fill
case center
case leading
case trailing
case top
case bottom
}
public enum Axis {
case horizontal
case vertical
}
public enum Distribution {
case fill
case fillEqually
case fillProportionally
case equalSpacing
}
public let alignment: Alignment
public let axis: Axis
public let distribution: Distribution
public let spacing: Double
/// not exposing style: UIStackView is a non-rendering subclass of UIView
/// https://useyourloaf.com/blog/stack-view-background-color/
public let style: Style?
public init(
_ frame: Rectangle,
alignment: Alignment = .fill,
axis: Axis = .horizontal,
distribution: Distribution = .fill,
spacing: Double = 0
) {
self.alignment = alignment
self.axis = axis
self.distribution = distribution
style = Style(frame)
self.spacing = spacing
}
public init(
_ constraint: Constraint,
alignment: Alignment = .fill,
axis: Axis = .horizontal,
distribution: Distribution = .fill,
spacing: Double = 0
) {
self.alignment = alignment
self.axis = axis
self.distribution = distribution
style = Style(constraint)
self.spacing = spacing
}
public init(
_ constraints: [Constraint],
alignment: Alignment = .fill,
axis: Axis = .horizontal,
distribution: Distribution = .fill,
spacing: Double = 0
) {
self.alignment = alignment
self.axis = axis
self.distribution = distribution
style = Style(constraints)
self.spacing = spacing
}
public init(
alignment: Alignment = .fill,
axis: Axis = .horizontal,
distribution: Distribution = .fill,
spacing: Double = 0
) {
self.alignment = alignment
self.axis = axis
self.distribution = distribution
style = nil
self.spacing = spacing
}
}
public typealias Children = [AnyNode]
}

View File

@ -1,48 +0,0 @@
//
// Stepper.swift
// Tokamak
//
// Created by Matvii Hodovaniuk on 1/21/19.
//
public struct Stepper: HostComponent {
public struct Props: Equatable, ControlProps, StyleProps,
ValueControlProps {
public let autorepeat: Bool
public let handlers: EventHandlers
public let isEnabled: Bool
public let maximumValue: Double
public let minimumValue: Double
public let stepValue: Double
public let value: Double
public let valueHandler: Handler<Double>?
public let wraps: Bool
public let style: Style?
public init(
_ style: Style? = nil,
autorepeat: Bool = true,
handlers: EventHandlers = [:],
isEnabled: Bool = true,
maximumValue: Double = 100.0,
minimumValue: Double = 0.0,
stepValue: Double = 1.0,
value: Double,
valueHandler: Handler<Double>? = nil,
wraps: Bool = false
) {
self.autorepeat = autorepeat
self.handlers = handlers
self.isEnabled = isEnabled
self.maximumValue = maximumValue
self.minimumValue = minimumValue
self.stepValue = stepValue
self.value = value
self.valueHandler = valueHandler
self.wraps = wraps
self.style = style
}
}
public typealias Children = Null
}

View File

@ -1,42 +0,0 @@
//
// Switch.swift
// Tokamak
//
// Created by Matvii Hodovaniuk on 1/21/19.
//
public struct Switch: HostComponent {
public struct Props: Equatable, ControlProps, StyleProps,
ValueControlProps {
public let handlers: EventHandlers
public let style: Style?
public let value: Bool
public let valueHandler: Handler<Bool>?
public let isEnabled: Bool
public let isAnimated: Bool
public let onImage: Image?
public let offImage: Image?
public init(
_ style: Style? = nil,
handlers: EventHandlers = [:],
isAnimated: Bool = true,
isEnabled: Bool = true,
offImage: Image? = nil,
onImage: Image? = nil,
value: Bool,
valueHandler: Handler<Bool>? = nil
) {
self.handlers = handlers
self.style = style
self.value = value
self.valueHandler = valueHandler
self.isEnabled = isEnabled
self.isAnimated = isAnimated
self.offImage = offImage
self.onImage = onImage
}
}
public typealias Children = Null
}

View File

@ -1,147 +0,0 @@
//
// TextField.swift
// Tokamak
//
// Created by Matvii Hodovaniuk on 2/22/19.
//
public struct TextField: HostComponent {
public struct Props: Equatable, ControlProps, StyleProps,
ValueControlProps {
public enum ViewMode {
case never
case whileEditing
case unlessEditing
case always
}
public enum TextAutocapitalizationType {
case none
case words
case sentences
case allCharacters
}
public enum TextAutocorrectionType {
case `default`
case no
case yes
}
public enum TextSpellCheckingType {
case `default`
case no
case yes
}
public enum KeyboardType {
case `default`
case asciiCapable
case numbersAndPunctuation
case URL
case numberPad
case phonePad
case namePhonePad
case emailAddress
case decimalPad
case twitter
case webSearch
case asciiCapableNumberPad
}
public enum KeyboardAppearance {
case `default`
case dark
case light
}
public enum ReturnKeyType {
case `default`
case go
case google
case join
case next
case route
case search
case send
case yahoo
case done
case emergencyCall
case `continue`
}
public enum BorderStyle {
case none
case line
case bezel
case roundedRect
}
public let autocapitalizationType: TextAutocapitalizationType
public let autocorrectionType: TextAutocorrectionType
public let borderStyle: BorderStyle
public let clearButtonMode: ViewMode
public let clearsOnBeginEditing: Bool
public let clearsOnInsertion: Bool
public let handlers: EventHandlers
public let style: Style?
public let value: String
public let valueHandler: Handler<String>?
public let isEnabled: Bool
public let isSecureTextEntry: Bool
public let keyboardAppearance: KeyboardAppearance
public let keyboardType: KeyboardType
public let placeholder: String?
public let returnKeyType: ReturnKeyType
public let spellCheckingType: TextSpellCheckingType
public let textAlignment: TextAlignment
public let textColor: Color
public let isAnimated: Bool
public init(
_ style: Style? = nil,
autocapitalizationType: TextAutocapitalizationType = .sentences,
autocorrectionType: TextAutocorrectionType = .default,
borderStyle: BorderStyle = .bezel,
clearButtonMode: ViewMode = .never,
clearsOnBeginEditing: Bool = false,
clearsOnInsertion: Bool = false,
handlers: EventHandlers = [:],
isAnimated: Bool = true,
isEnabled: Bool = true,
isSecureTextEntry: Bool = false,
keyboardAppearance: KeyboardAppearance = .default,
keyboardType: KeyboardType = .default,
placeholder: String? = nil,
returnKeyType: ReturnKeyType = .default,
spellCheckingType: TextSpellCheckingType = .default,
textAlignment: TextAlignment = .natural,
textColor: Color = .black,
value: String,
valueHandler: Handler<String>? = nil
) {
self.autocapitalizationType = autocapitalizationType
self.autocorrectionType = autocorrectionType
self.borderStyle = borderStyle
self.clearButtonMode = clearButtonMode
self.clearsOnBeginEditing = clearsOnBeginEditing
self.clearsOnInsertion = clearsOnInsertion
self.handlers = handlers
self.style = style
self.value = value
self.valueHandler = valueHandler
self.isSecureTextEntry = isSecureTextEntry
self.isEnabled = isEnabled
self.keyboardAppearance = keyboardAppearance
self.keyboardType = keyboardType
self.placeholder = placeholder
self.returnKeyType = returnKeyType
self.spellCheckingType = spellCheckingType
self.textAlignment = textAlignment
self.textColor = textColor
self.isAnimated = isAnimated
}
}
public typealias Children = Null
}

View File

@ -1,41 +0,0 @@
//
// TextView.swift
// Tokamak
//
// Created by Matvii Hodovaniuk on 3/27/19.
//
public struct TextView: HostComponent {
public struct Props: Equatable, StyleProps, ValueControlProps {
public let style: Style?
public let allowsEditingTextAttributes: Bool
public let isEditable: Bool
public let textAlignment: TextAlignment
public let textColor: Color?
public let scrollOptions: ScrollOptions?
public let value: String
public let valueHandler: Handler<String>?
public init(
_ style: Style? = nil,
allowsEditingTextAttributes: Bool = false,
isEditable: Bool = true,
scrollOptions: ScrollOptions? = nil,
textAlignment: TextAlignment = .natural,
textColor: Color? = nil,
value: String = "",
valueHandler: Handler<String>? = nil
) {
self.style = style
self.allowsEditingTextAttributes = allowsEditingTextAttributes
self.isEditable = isEditable
self.textAlignment = textAlignment
self.textColor = textColor
self.scrollOptions = scrollOptions
self.value = value
self.valueHandler = valueHandler
}
}
public typealias Children = Null
}

View File

@ -1,39 +0,0 @@
//
// Throbber.swift
// Tokamak
//
// Created by Max Desiatov on 20/03/2019.
//
public struct Throbber: HostComponent {
public struct Props: Equatable, StyleProps {
public enum Variety {
case whiteLarge
case white
case gray
}
/// Value of this property overrides the value of `variety`
public let color: Color?
public let isAnimating: Bool
public let hidesWhenStopped: Bool
public let style: Style?
public let variety: Variety
public init(
_ style: Style? = nil,
color: Color? = nil,
isAnimating: Bool = false,
hidesWhenStopped: Bool = true,
variety: Variety = .gray
) {
self.style = style
self.color = color
self.isAnimating = isAnimating
self.hidesWhenStopped = hidesWhenStopped
self.variety = variety
}
}
public typealias Children = Null
}

View File

@ -1,22 +0,0 @@
//
// View.swift
// Tokamak
//
// Created by Max Desiatov on 29/12/2018.
//
public struct ViewComponent: HostComponent {
public typealias Children = [AnyNode]
public struct Props: Equatable, StyleProps, Default {
public static var defaultValue: Props {
return Props()
}
public let style: Style?
public init(_ style: Style? = nil) {
self.style = style
}
}
}

View File

@ -1,72 +0,0 @@
//
// ModalPresenter.swift
// Tokamak
//
// Created by Max Desiatov on 31/12/2018.
//
public struct ModalPresenter: HostComponent {
public struct Props: Equatable, Default {
public static var defaultValue: Props {
return Props()
}
public enum PresentationStyle {
/** Renderer targets up in the tree of nodes owning the presenter node are
removed after the presentation completes.
*/
case fullScreen
case pageSheet
case formSheet
/** Renderer targets up in the tree of nodes owning the presenter node are
removed after the presentation completes.
*/
case currentContext
/** Renderer targets up in the tree of nodes owning the presenter node are
not removed from the target hierarchy when the presentation finishes.
So if the presented component does not fill the screen with opaque
content, the underlying content shows through.
*/
case overCurrentContext
/** Renderer targets up in the tree of nodes owning the presenter node are
not removed from the target hierarchy when the presentation finishes.
So if the presented component does not fill the screen with opaque
content, the underlying content shows through.
*/
case overFullScreen
case blurOverFullScreen
case popover
}
public enum TransitionStyle {
case coverVertical
case flipHorizontal
case crossDissolve
case partialCurl
}
public let dismissAnimated: Bool
public let presentAnimated: Bool
public let presentationStyle: PresentationStyle?
public let transitionStyle: TransitionStyle?
public init(
dismissAnimated: Bool = true,
presentAnimated: Bool = true,
presentationStyle: PresentationStyle? = nil,
transitionStyle: TransitionStyle? = nil
) {
self.dismissAnimated = dismissAnimated
self.presentAnimated = presentAnimated
self.presentationStyle = presentationStyle
self.transitionStyle = transitionStyle
}
}
public typealias Children = AnyNode
}

View File

@ -1,27 +0,0 @@
//
// NavigationItem.swift
// TokamakUIKit
//
// Created by Max Desiatov on 04/02/2019.
//
public struct NavigationItem: HostComponent {
public struct Props: Equatable {
/// The mode to use when displaying the title of the navigation bar.
public enum TitleMode {
case automatic
case large
case standard
}
public let title: String?
public let titleMode: TitleMode
public init(title: String? = nil, titleMode: TitleMode = .automatic) {
self.title = title
self.titleMode = titleMode
}
}
public typealias Children = AnyNode
}

View File

@ -1,88 +0,0 @@
//
// NavigationPresenter.swift
// Tokamak
//
// Created by Max Desiatov on 31/12/2018.
//
public protocol NavigationRouter {
associatedtype Props: Equatable
associatedtype Route: Equatable
static func route(
props: Props,
route: Route,
push: @escaping (Route) -> (),
pop: @escaping () -> (),
hooks: Hooks
) -> AnyNode
}
public struct NavigationPresenter<T: NavigationRouter>: LeafComponent {
public struct Props: Equatable {
public let hidesBarsWhenKeyboardAppears: Bool?
public let initial: T.Route
public let popAnimated: Bool
public let prefersLargeTitles: Bool
public let pushAnimated: Bool
public let routerProps: T.Props
public init(
hidesBarsWhenKeyboardAppears: Bool? = nil,
initial: T.Route,
popAnimated: Bool = true,
prefersLargeTitles: Bool = false,
pushAnimated: Bool = true,
routerProps: T.Props
) {
self.hidesBarsWhenKeyboardAppears = hidesBarsWhenKeyboardAppears
self.initial = initial
self.popAnimated = popAnimated
self.prefersLargeTitles = prefersLargeTitles
self.pushAnimated = pushAnimated
self.routerProps = routerProps
}
}
public static func render(props: Props, hooks: Hooks) -> AnyNode {
let stack = hooks.state([props.initial])
let pop = { () -> () in stack.wrappedValue.remove(at: stack.wrappedValue.count - 1) }
return NavigationController.node(
.init(
hidesBarsWhenKeyboardAppears: props.hidesBarsWhenKeyboardAppears,
popAnimated: props.popAnimated,
prefersLargeTitles: props.prefersLargeTitles,
pushAnimated: props.pushAnimated,
onPop: Handler(pop)
),
stack.wrappedValue.map {
T.route(
props: props.routerProps,
route: $0,
push: { stack.wrappedValue += [$0] },
pop: pop,
hooks: hooks
)
}
)
}
}
extension NavigationPresenter.Props where T.Props == Null {
public init(
hidesBarsWhenKeyboardAppears: Bool? = nil,
initial: T.Route,
popAnimated: Bool = true,
prefersLargeTitles: Bool = false,
pushAnimated: Bool = true
) {
self.hidesBarsWhenKeyboardAppears = hidesBarsWhenKeyboardAppears
self.initial = initial
self.popAnimated = popAnimated
self.prefersLargeTitles = prefersLargeTitles
self.pushAnimated = pushAnimated
routerProps = Null()
}
}

View File

@ -1,32 +0,0 @@
//
// TabItem.swift
// Tokamak
//
// Created by Max Desiatov on 04/02/2019.
//
public struct TabItem: HostComponent {
public struct Props: Equatable {
public let badgeColor: Color?
public let badgeValue: String?
public let image: Image?
public let selectedImage: Image?
public let title: String?
public init(
badgeColor: Color? = nil,
badgeValue: String? = nil,
image: Image? = nil,
selectedImage: Image? = nil,
title: String? = nil
) {
self.badgeColor = badgeColor
self.badgeValue = badgeValue
self.image = image
self.selectedImage = selectedImage
self.title = title
}
}
public typealias Children = AnyNode
}

View File

@ -1,23 +0,0 @@
//
// TabPresenter.swift
// Tokamak
//
// Created by Matvii Hodovaniuk on 3/13/19.
//
public struct TabPresenter: HostComponent {
public typealias Children = [AnyNode]
public struct Props: Equatable {
public let isAnimated: Bool
public let selectedIndex: Binding<Int>?
public init(
isAnimated: Bool = false,
selectedIndex: Binding<Int>? = nil
) {
self.isAnimated = isAnimated
self.selectedIndex = selectedIndex
}
}
}

View File

@ -1,34 +0,0 @@
//
// Accessibility.swift
// Tokamak
//
// Created by Matvii Hodovaniuk on 2/21/19.
//
public struct Accessibility: Equatable {
public let identifier: String?
public let language: String?
public let label: String?
public let hint: String?
public let value: String?
public let elementsHidden: Bool
public let isModal: Bool
public init(
elementsHidden: Bool = false,
hint: String? = nil,
isModal: Bool = false,
label: String? = "",
language: String? = nil,
value: String? = "",
identifier: String? = ""
) {
self.elementsHidden = elementsHidden
self.hint = hint
self.isModal = isModal
self.label = label
self.language = language
self.value = value
self.identifier = identifier
}
}

View File

@ -1,29 +0,0 @@
//
// Bottom.swift
// Tokamak
//
// Created by Max Desiatov on 02/02/2019.
//
public struct Bottom: Equatable {
public let target: Constraint.SafeAreaTarget
public let constant: Double
public static func equal(
to target: Constraint.SafeAreaTarget,
constant: Double = 0
) -> Constraint {
return .bottom(Bottom(
target: target, constant: constant
))
}
public static func equal(
to target: Constraint.Target,
constant: Double = 0
) -> Constraint {
return .bottom(Bottom(
target: .external(target), constant: constant
))
}
}

View File

@ -1,18 +0,0 @@
//
// Center.swift
// Tokamak
//
// Created by Max Desiatov on 02/02/2019.
//
public struct Center: Equatable {
public let target: Constraint.Target
public let constant: Double
public static func equal(
to target: Constraint.Target,
constant: Double = 0
) -> Constraint {
return .center(Center(target: target, constant: constant))
}
}

View File

@ -1,29 +0,0 @@
//
// CenterX.swift
// Tokamak
//
// Created by Max Desiatov on 02/02/2019.
//
public struct CenterX: Equatable {
public let target: Constraint.SafeAreaTarget
public let constant: Double
public static func equal(
to target: Constraint.SafeAreaTarget,
constant: Double = 0
) -> Constraint {
return .centerX(CenterX(
target: target, constant: constant
))
}
public static func equal(
to target: Constraint.Target,
constant: Double = 0
) -> Constraint {
return .centerX(CenterX(
target: .external(target), constant: constant
))
}
}

View File

@ -1,30 +0,0 @@
//
// CenterY.swift
// Tokamak
//
// Created by Max Desiatov on 02/02/2019.
//
public struct CenterY: Equatable {
public let target: Constraint.SafeAreaTarget
public let constant: Double
public static func equal(
to target: Constraint.SafeAreaTarget,
constant: Double = 0,
multiplier: Double = 1
) -> Constraint {
return .centerY(CenterY(
target: target, constant: constant
))
}
public static func equal(
to target: Constraint.Target,
constant: Double = 0
) -> Constraint {
return .centerY(CenterY(
target: .external(target), constant: constant
))
}
}

View File

@ -1,42 +0,0 @@
//
// Constraint.swift
// Tokamak
//
// Created by Max Desiatov on 16/10/2018.
//
public enum Constraint: Equatable {
public enum SafeAreaTarget: Equatable {
case external(Target)
case safeArea
}
public enum Target {
case next
case parent
}
public enum OwnTarget: Equatable {
case external(Target)
case own
}
case firstBaseline(FirstBaseline)
case lastBaseline(LastBaseline)
case center(Center)
case centerX(CenterX)
case centerY(CenterY)
case width(Width)
case height(Height)
case size(SizeConstraint)
case edges(Edges)
case leading(Leading)
case trailing(Trailing)
case left(Left)
case right(Right)
case top(Top)
case bottom(Bottom)
}

View File

@ -1,43 +0,0 @@
//
// Edges.swift
// Tokamak
//
// Created by Max Desiatov on 02/02/2019.
//
public struct Edges: Equatable {
public let target: Constraint.SafeAreaTarget
public let insets: Insets
public static func equal(
to target: Constraint.SafeAreaTarget,
insets: Insets = .zero
) -> Constraint {
return .edges(Edges(target: target, insets: insets))
}
public static func equal(
to target: Constraint.Target,
insets: Insets = .zero
) -> Constraint {
return .edges(Edges(target: .external(target), insets: insets))
}
public static func equal(
to target: Constraint.Target,
inset: Double
) -> Constraint {
return .edges(Edges(target: .external(target), insets: Insets(
top: inset, bottom: inset, left: inset, right: inset
)))
}
public static func equal(
to target: Constraint.SafeAreaTarget,
inset: Double
) -> Constraint {
return .edges(Edges(target: target, insets: Insets(
top: inset, bottom: inset, left: inset, right: inset
)))
}
}

View File

@ -1,22 +0,0 @@
//
// FirstBaseline.swift
// Tokamak
//
// Created by Max Desiatov on 02/02/2019.
//
public struct FirstBaseline: Equatable {
public let target: Constraint.Target
public let constant: Double
public let multiplier: Double
public static func equal(
to target: Constraint.Target,
constant: Double = 0,
multiplier: Double = 1
) -> Constraint {
return .firstBaseline(FirstBaseline(
target: target, constant: constant, multiplier: multiplier
))
}
}

View File

@ -1,38 +0,0 @@
//
// Height.swift
// Tokamak
//
// Created by Max Desiatov on 02/02/2019.
//
public struct Height: Equatable {
public let target: Constraint.OwnTarget
public let constant: Double
public let multiplier: Double
public static func equal(
to target: Constraint.Target,
constant: Double = 0,
multiplier: Double = 1
) -> Constraint {
return .height(Height(
target: .external(target), constant: constant, multiplier: multiplier
))
}
public static func equal(
to target: Constraint.OwnTarget,
constant: Double = 0,
multiplier: Double = 1
) -> Constraint {
return .height(Height(
target: target, constant: constant, multiplier: multiplier
))
}
public static func equal(to constant: Double) -> Constraint {
return .height(Height(
target: .own, constant: constant, multiplier: 0
))
}
}

View File

@ -1,22 +0,0 @@
//
// LastBaseline.swift
// Tokamak
//
// Created by Max Desiatov on 02/02/2019.
//
public struct LastBaseline: Equatable {
public let target: Constraint.Target
public let constant: Double
public let multiplier: Double
public static func equal(
to target: Constraint.Target,
constant: Double = 0,
multiplier: Double = 1
) -> Constraint {
return .lastBaseline(LastBaseline(
target: target, constant: constant, multiplier: multiplier
))
}
}

View File

@ -1,29 +0,0 @@
//
// Leading.swift
// Tokamak
//
// Created by Max Desiatov on 02/02/2019.
//
public struct Leading: Equatable {
public let target: Constraint.SafeAreaTarget
public let constant: Double
public static func equal(
to target: Constraint.SafeAreaTarget,
constant: Double = 0
) -> Constraint {
return .leading(Leading(
target: target, constant: constant
))
}
public static func equal(
to target: Constraint.Target,
constant: Double = 0
) -> Constraint {
return .leading(Leading(
target: .external(target), constant: constant
))
}
}

View File

@ -1,29 +0,0 @@
//
// Left.swift
// Tokamak
//
// Created by Max Desiatov on 02/02/2019.
//
public struct Left: Equatable {
public let target: Constraint.SafeAreaTarget
public let constant: Double
public static func equal(
to target: Constraint.SafeAreaTarget,
constant: Double = 0
) -> Constraint {
return .left(Left(
target: target, constant: constant
))
}
public static func equal(
to target: Constraint.Target,
constant: Double = 0
) -> Constraint {
return .left(Left(
target: .external(target), constant: constant
))
}
}

View File

@ -1,29 +0,0 @@
//
// Right.swift
// Tokamak
//
// Created by Max Desiatov on 02/02/2019.
//
public struct Right: Equatable {
public let target: Constraint.SafeAreaTarget
public let constant: Double
public static func equal(
to target: Constraint.SafeAreaTarget,
constant: Double = 0
) -> Constraint {
return .right(Right(
target: target, constant: constant
))
}
public static func equal(
to target: Constraint.Target,
constant: Double = 0
) -> Constraint {
return .right(Right(
target: .external(target), constant: constant
))
}
}

View File

@ -1,24 +0,0 @@
//
// SizeConstraint.swift
// Tokamak
//
// Created by Max Desiatov on 02/02/2019.
//
public enum SizeConstraint: Equatable {
case constant(Size)
case multiplier(Constraint.Target, Double)
}
extension Size {
static func equal(to size: Size) -> SizeConstraint {
return .constant(size)
}
static func equal(
to target: Constraint.Target,
multiplier: Double = 1.0
) -> SizeConstraint {
return .multiplier(target, multiplier)
}
}

View File

@ -1,29 +0,0 @@
//
// Top.swift
// Tokamak
//
// Created by Max Desiatov on 02/02/2019.
//
public struct Top: Equatable {
public let target: Constraint.SafeAreaTarget
public let constant: Double
public static func equal(
to target: Constraint.SafeAreaTarget,
constant: Double = 0
) -> Constraint {
return .top(Top(
target: target, constant: constant
))
}
public static func equal(
to target: Constraint.Target,
constant: Double = 0
) -> Constraint {
return .top(Top(
target: .external(target), constant: constant
))
}
}

View File

@ -1,29 +0,0 @@
//
// Trailing.swift
// Tokamak
//
// Created by Max Desiatov on 02/02/2019.
//
public struct Trailing: Equatable {
public let target: Constraint.SafeAreaTarget
public let constant: Double
public static func equal(
to target: Constraint.SafeAreaTarget,
constant: Double = 0
) -> Constraint {
return .trailing(Trailing(
target: target, constant: constant
))
}
public static func equal(
to target: Constraint.Target,
constant: Double = 0
) -> Constraint {
return .trailing(Trailing(
target: .external(target), constant: constant
))
}
}

View File

@ -1,38 +0,0 @@
//
// Width.swift
// Tokamak
//
// Created by Max Desiatov on 02/02/2019.
//
public struct Width: Equatable {
public let target: Constraint.OwnTarget
public let constant: Double
public let multiplier: Double
public static func equal(
to target: Constraint.Target,
constant: Double = 0,
multiplier: Double = 1
) -> Constraint {
return .width(Width(
target: .external(target), constant: constant, multiplier: multiplier
))
}
public static func equal(
to target: Constraint.OwnTarget,
constant: Double = 0,
multiplier: Double = 1
) -> Constraint {
return .width(Width(
target: target, constant: constant, multiplier: multiplier
))
}
public static func equal(to constant: Double) -> Constraint {
return .width(Width(
target: .own, constant: constant, multiplier: 0
))
}
}

View File

@ -1,46 +0,0 @@
//
// Event.swift
// Tokamak
//
// Created by Max Desiatov on 04/12/2018.
//
public enum Event: CaseIterable, Hashable {
case touchDown
case touchDownRepeat
case touchDragInside
case touchDragOutside
case touchDragEnter
case touchDragExit
case touchUpInside
case touchUpOutside
case touchCancel
case valueChanged
case editingDidBegin
case editingChanged
case editingDidEnd
case editingDidEndOnExit
case allTouchEvents
case allEditingEvents
case allEvents
// FIXME: provisional stability, taken from Marzipan preview symbols
// https://www.highcaffeinecontent.com/blog/20190302-Making-Marzipan-Apps-Sing
case hoverEnter
case hoverExit
case contextualMenu
}
public typealias EventHandlers = [Event: Handler<()>]
public protocol ControlProps {
var handlers: EventHandlers { get }
var isEnabled: Bool { get }
}
public protocol ValueControlProps {
associatedtype Value
var value: Value { get }
var valueHandler: Handler<Value>? { get }
}

View File

@ -1,78 +0,0 @@
//
// Image.swift
// Tokamak
//
// Created by Max Desiatov on 26/02/2019.
//
import Foundation
public struct Image: Equatable {
public enum ContentMode {
case scaleToFill
case scaleAspectFit
case scaleAspectFill
case center
case top
case bottom
case left
case right
case topLeft
case topRight
case bottomLeft
case bottomRight
}
public enum RenderingMode {
case automatic
case alwaysOriginal
case alwaysTemplate
}
public enum Source: Equatable {
case name(String)
case data(Data)
}
public let contentMode: ContentMode
/// when changed initializes new image with given data or name
public let source: Source
/// when changed creates new image with `withRenderingMode`
public let renderingMode: RenderingMode
/// equivalent to `flipsForRightToLeftLayoutDirection` in UIKit
public let flipsForRTL: Bool
/// when changed initializes new image with given scale
public let scale: Double
public init(
contentMode: ContentMode = .scaleToFill,
flipsForRTL: Bool = false,
name: String,
renderingMode: RenderingMode = .automatic,
scale: Double = 1.0
) {
source = .name(name)
self.contentMode = contentMode
self.renderingMode = renderingMode
self.scale = scale
self.flipsForRTL = flipsForRTL
}
public init(
contentMode: ContentMode = .scaleToFill,
data: Data,
flipsForRTL: Bool = false,
renderingMode: RenderingMode = .automatic,
scale: Double = 1.0
) {
source = .data(data)
self.contentMode = contentMode
self.renderingMode = renderingMode
self.scale = scale
self.flipsForRTL = flipsForRTL
}
}

View File

@ -1,24 +0,0 @@
//
// Insets.swift
// TokamakUIKit
//
// Created by Max Desiatov on 17/01/2019.
//
public struct Insets: Equatable {
public let top: Double
public let bottom: Double
public let left: Double
public let right: Double
public init(top: Double, bottom: Double, left: Double, right: Double) {
self.top = top
self.bottom = bottom
self.left = left
self.right = right
}
public static var zero: Insets {
return .init(top: 0, bottom: 0, left: 0, right: 0)
}
}

View File

@ -1,90 +0,0 @@
//
// ScrollOptions.swift
// Tokamak
//
// Created by Matvii Hodovaniuk on 3/27/19.
//
public struct ScrollOptions: Equatable, Default {
public static var defaultValue: ScrollOptions {
return ScrollOptions()
}
public struct EdgeInsets: Equatable {
public let bottom: Float
public let left: Float
public let right: Float
public let top: Float
public init(
top: Float = 0,
left: Float = 0,
bottom: Float = 0,
right: Float = 0
) {
self.top = top
self.left = left
self.bottom = bottom
self.right = right
}
}
public enum IndicatorStyle {
case `default`
case black
case white
}
public let alwaysBounceHorizontal: Bool
public let alwaysBounceVertical: Bool
public let bounces: Bool
public let bouncesZoom: Bool
public let contentInset: EdgeInsets
public let indicatorStyle: IndicatorStyle
public let isDirectionalLockEnabled: Bool
public let isPagingEnabled: Bool
public let isScrollEnabled: Bool
public let maximumZoomScale: Float
public let minimumZoomScale: Float
public let scrollIndicatorInsets: EdgeInsets
public let scrollsToTop: Bool
public let showsHorizontalScrollIndicator: Bool
public let showsVerticalScrollIndicator: Bool
public let zoomScale: Float
public init(
alwaysBounceHorizontal: Bool = false,
alwaysBounceVertical: Bool = false,
bounces: Bool = true,
bouncesZoom: Bool = true,
contentInset: EdgeInsets = EdgeInsets(),
indicatorStyle: IndicatorStyle = .default,
isDirectionalLockEnabled: Bool = false,
isPagingEnabled: Bool = false,
isScrollEnabled: Bool = true,
maximumZoomScale: Float = 1.0,
minimumZoomScale: Float = 1.0,
scrollIndicatorInsets: EdgeInsets = EdgeInsets(),
scrollsToTop: Bool = true,
showsHorizontalScrollIndicator: Bool = true,
showsVerticalScrollIndicator: Bool = true,
zoomScale: Float = 1.0
) {
self.alwaysBounceHorizontal = alwaysBounceHorizontal
self.alwaysBounceVertical = alwaysBounceVertical
self.bounces = bounces
self.bouncesZoom = bouncesZoom
self.contentInset = contentInset
self.isDirectionalLockEnabled = isDirectionalLockEnabled
self.isPagingEnabled = isPagingEnabled
self.indicatorStyle = indicatorStyle
self.isScrollEnabled = isScrollEnabled
self.maximumZoomScale = maximumZoomScale
self.minimumZoomScale = minimumZoomScale
self.scrollIndicatorInsets = scrollIndicatorInsets
self.scrollsToTop = scrollsToTop
self.showsVerticalScrollIndicator = showsVerticalScrollIndicator
self.showsHorizontalScrollIndicator = showsHorizontalScrollIndicator
self.zoomScale = zoomScale
}
}

View File

@ -1,18 +0,0 @@
//
// Second.swift
// Tokamak
//
// Created by Max Desiatov on 05/01/2019.
//
public struct Second: ExpressibleByFloatLiteral, Equatable {
public let value: Double
public init(floatLiteral value: Double) {
self.value = value
}
public init(_ value: Double) {
self.value = value
}
}

View File

@ -1,172 +0,0 @@
//
// Style.swift
// Tokamak
//
// Created by Max Desiatov on 31/12/2018.
//
public protocol StyleProps {
var style: Style? { get }
}
public enum Layout: Equatable {
case frame(Rectangle)
case constraints([Constraint])
}
public struct Style: Equatable {
public let accessibility: Accessibility?
public let allowsEdgeAntialiasing: Bool?
public let allowsGroupOpacity: Bool?
public let backgroundColor: Color?
public let borderColor: Color?
public let borderWidth: Float?
public let cornerRadius: Float?
public let isDoubleSided: Bool?
public let isHidden: Bool?
public let masksToBounds: Bool?
public let opacity: Float?
public let shadowColor: Color?
public let shadowOpacity: Float?
public let shadowRadius: Float?
public let layout: Layout?
public init(
accessibility: Accessibility? = nil,
allowsEdgeAntialiasing: Bool? = false,
allowsGroupOpacity: Bool? = true,
backgroundColor: Color? = nil,
borderColor: Color? = nil,
borderWidth: Float? = 0.0,
cornerRadius: Float? = 0.0,
isDoubleSided: Bool? = true,
isHidden: Bool? = nil,
masksToBounds: Bool? = false,
opacity: Float? = 1.0,
shadowColor: Color? = nil,
shadowOpacity: Float? = 0.0,
shadowRadius: Float? = 3.0
) {
self.accessibility = accessibility
self.allowsEdgeAntialiasing = allowsEdgeAntialiasing
self.allowsGroupOpacity = allowsGroupOpacity
self.backgroundColor = backgroundColor
self.borderColor = borderColor
self.borderWidth = borderWidth
self.cornerRadius = cornerRadius
self.isDoubleSided = isDoubleSided
self.isHidden = isHidden
self.masksToBounds = masksToBounds
self.opacity = opacity
self.shadowColor = shadowColor
self.shadowOpacity = shadowOpacity
self.shadowRadius = shadowRadius
layout = nil
}
public init(
_ frame: Rectangle,
accessibility: Accessibility? = nil,
allowsEdgeAntialiasing: Bool? = false,
allowsGroupOpacity: Bool? = true,
backgroundColor: Color? = nil,
borderColor: Color? = nil,
borderWidth: Float? = 0.0,
cornerRadius: Float? = 0.0,
isDoubleSided: Bool? = true,
isHidden: Bool? = nil,
masksToBounds: Bool? = false,
opacity: Float? = 1.0,
shadowColor: Color? = nil,
shadowOpacity: Float? = 0.0,
shadowRadius: Float? = 3.0
) {
self.accessibility = accessibility
self.allowsEdgeAntialiasing = allowsEdgeAntialiasing
self.allowsGroupOpacity = allowsGroupOpacity
self.backgroundColor = backgroundColor
self.borderColor = borderColor
self.borderWidth = borderWidth
self.cornerRadius = cornerRadius
self.isDoubleSided = isDoubleSided
self.isHidden = isHidden
self.masksToBounds = masksToBounds
self.opacity = opacity
self.shadowColor = shadowColor
self.shadowOpacity = shadowOpacity
self.shadowRadius = shadowRadius
layout = .frame(frame)
}
public init(
_ constraint: Constraint,
accessibility: Accessibility? = nil,
allowsEdgeAntialiasing: Bool? = false,
allowsGroupOpacity: Bool? = true,
backgroundColor: Color? = nil,
borderColor: Color? = nil,
borderWidth: Float? = 0.0,
cornerRadius: Float? = 0.0,
isDoubleSided: Bool? = true,
isHidden: Bool? = nil,
masksToBounds: Bool? = false,
opacity: Float? = 1.0,
shadowColor: Color? = nil,
shadowOpacity: Float? = 0.0,
shadowRadius: Float? = 3.0
) {
self.accessibility = accessibility
self.allowsEdgeAntialiasing = allowsEdgeAntialiasing
self.allowsGroupOpacity = allowsGroupOpacity
self.backgroundColor = backgroundColor
self.borderColor = borderColor
self.borderWidth = borderWidth
self.cornerRadius = cornerRadius
self.isDoubleSided = isDoubleSided
self.isHidden = isHidden
self.masksToBounds = masksToBounds
self.opacity = opacity
self.shadowColor = shadowColor
self.shadowOpacity = shadowOpacity
self.shadowRadius = shadowRadius
layout = .constraints([constraint])
}
public init(
_ constraints: [Constraint],
accessibility: Accessibility? = nil,
allowsEdgeAntialiasing: Bool? = false,
allowsGroupOpacity: Bool? = true,
backgroundColor: Color? = nil,
borderColor: Color? = nil,
borderWidth: Float? = 0.0,
cornerRadius: Float? = 0.0,
isDoubleSided: Bool? = true,
isHidden: Bool? = nil,
masksToBounds: Bool? = false,
opacity: Float? = 1.0,
shadowColor: Color? = nil,
shadowOpacity: Float? = 0.0,
shadowRadius: Float? = 3.0
) {
self.accessibility = accessibility
self.allowsEdgeAntialiasing = allowsEdgeAntialiasing
self.allowsGroupOpacity = allowsGroupOpacity
self.backgroundColor = backgroundColor
self.borderColor = borderColor
self.borderWidth = borderWidth
self.cornerRadius = cornerRadius
self.isDoubleSided = isDoubleSided
self.isHidden = isHidden
self.masksToBounds = masksToBounds
self.opacity = opacity
self.shadowColor = shadowColor
self.shadowOpacity = shadowOpacity
self.shadowRadius = shadowRadius
layout = .constraints(constraints)
}
}

View File

@ -1,65 +0,0 @@
//
// Effect.swift
// Tokamak
//
// Created by Max Desiatov on 10/02/2019.
//
extension Hooks {
/// Schedule an effect to be executed on every call to `render`.
public func effect(closure: @escaping () -> ()) {
scheduleEffect(nil) { closure(); return nil }
}
/** Schedule an effect to be executed on every call to `render`. The effect
closure should return a cleanup closure to be executed before the next
call to `render` or when a component is unmounted.
*/
public func finalizedEffect(closure: @escaping () -> () -> ()) {
scheduleEffect(nil, closure)
}
/** Schedule an effect to be executed on calls to `render` when `observed`
value has changed from the previous call to `render`.
You can use this overload of `effect` to control when exactly the effect
`closure` is executed. For example, always pass `Null()` as `observed` so
that the effect is executed only once on the initial rendering.
Another use case is an effect that schedules a repeated timer with a specific
interval. You wouldn't want to reschedule the timer on every call to
component's `render` if the interval hasn't changed. Pass the interval as
`observed`, which will be compared to the previous value and trigger effect
execution (updating the timer interval or creating a new timer) when the
interval has changed.
*/
public func effect<T: Equatable>(_ observed: T, closure: @escaping () -> ()) {
scheduleEffect(AnyEquatable(observed)) { closure(); return nil }
}
/** Schedule an effect to be executed on calls to `render` when `observed`
value has changed from the previous call to `render`. The effect
closure should return a cleanup closure to be executed before the next
call to `render` or when a component is unmounted.
You can use this overload of `effect` to control when exactly the effect
`closure` is executed with additional cleanup. For example, always pass
`Null()` as `observed` so that the effect is executed only once on the
initial rendering.
Another use case is an effect that subscribes to updates on a user model
fetched from the network and you need to correctly unsubscribe from updates
when a component is unmounted. You wouldn't to unsubscribe and resubscribe
every time a component rerendered if an observed user ID hasn't changed.
Pass the ID as `observed`, which will be compared to the previous value and
trigger effect execution (unsubscribing from updates on old user ID and
subscribing for new user ID) when the ID has changed.
*/
public func finalizedEffect<T>(
_ observed: T,
closure: @escaping () -> () -> ()
)
where T: Equatable {
scheduleEffect(AnyEquatable(observed), closure)
}
}

View File

@ -11,16 +11,6 @@ typealias Effect = () -> Finalizer
protocol HookedComponent: AnyObject {
/// State cells of this component indexed by order of `hooks.state` calls
var state: [Any] { get set }
/// Effect cells of this component indexed by order of `hooks.effect` calls
var effects: [(observed: AnyEquatable?, Effect)] { get set }
/// Finalizer cells of this component received from `Effect` evaluation.
/// Indices in this array exactly match indices in `effects` array.
var effectFinalizers: [Finalizer] { get set }
/// Ref cells of this component indexed by order of `hooks.ref` calls
var refs: [AnyObject] { get set }
}
/** Functions implemented directly in this class are parts of internal
@ -40,11 +30,6 @@ public final class Hooks {
private var stateIndex = 0
private var effectIndex = 0
private(set) var scheduledEffects = Set<Int>()
private var refIndex = 0
init(
component: HookedComponent,
queueState: @escaping (
@ -74,57 +59,4 @@ public final class Hooks {
// swiftlint:disable:next force_cast
return ({ component.state[boundIndex] as! T }, stateIndex)
}
/** Schedules effect exection with the current reconciler accessed via
`component`.
*/
func scheduleEffect(
_ observed: AnyEquatable?,
_ effect: @escaping Effect
) {
defer { effectIndex += 1 }
guard let component = component else {
assertionFailure("hooks.effect should only be called within `render`")
return
}
if component.effects.count > effectIndex {
guard component.effects[effectIndex].0 != observed else { return }
component.effects[effectIndex].0 = observed
component.effects[effectIndex].1 = effect
scheduledEffects.insert(effectIndex)
} else {
component.effects.append((observed, effect))
scheduledEffects.insert(effectIndex)
}
}
/** For a given initial value return a current ref
(initialized from `initial` if current was absent)
*/
func ref<T>(_ initial: Ref<T>) -> Ref<T> {
defer { refIndex += 1 }
guard let component = component else {
assertionFailure("hooks.ref should only be called within `render`")
return initial
}
if component.refs.count > refIndex {
guard let result = component.refs[refIndex] as? Ref<T> else {
assertionFailure(
"""
unexpected ref type during rendering, possible Rules of Hooks violation
"""
)
return initial
}
return result
} else {
component.refs.append(initial)
return initial
}
}
}

View File

@ -1,24 +0,0 @@
//
// Ref.swift
// Tokamak
//
// Created by Max Desiatov on 09/02/2019.
//
public final class Ref<T> {
public var value: T
init(_ value: T) {
self.value = value
}
}
extension Hooks {
public func ref<T>(type: T.Type) -> Ref<T?> {
return ref(Ref(nil))
}
public func ref<T>(_ initial: T) -> Ref<T> {
return ref(Ref(initial))
}
}

View File

@ -6,10 +6,12 @@
//
public class MountedComponent<R: Renderer> {
public internal(set) var node: AnyNode
public internal(set) var node: AnyView
public let viewType: Any.Type
init(_ node: AnyNode) {
init(_ node: AnyView) {
self.node = node
viewType = node.type
}
func mount(with reconciler: StackReconciler<R>) {
@ -25,16 +27,25 @@ public class MountedComponent<R: Renderer> {
}
}
extension AnyNode {
extension View {
func makeMountedComponent<R: Renderer>(_ parentTarget: R.TargetType)
-> MountedComponent<R> {
switch type {
case let .host(type):
return MountedHostComponent(self, type, parentTarget)
case let .composite(type):
return MountedCompositeComponent(self, type, parentTarget)
case .null:
return MountedNull(self)
if let anyView = self as? AnyView {
if anyView.type == EmptyView.self {
return MountedNull(anyView)
} else if anyView.bodyType == Never.self {
return MountedHostComponent(anyView, parentTarget)
} else {
return MountedCompositeComponent(anyView, parentTarget)
}
}
if self is EmptyView {
return MountedNull(AnyView(self))
} else if Body.self is Never.Type {
return MountedHostComponent(AnyView(self), parentTarget)
} else {
return MountedCompositeComponent(AnyView(self), parentTarget)
}
}
}

View File

@ -6,12 +6,13 @@
//
import Dispatch
import Runtime
final class MountedCompositeComponent<R: Renderer>: MountedComponent<R>,
HookedComponent, Hashable {
static func ==(lhs: MountedCompositeComponent<R>,
rhs: MountedCompositeComponent<R>) -> Bool {
return lhs === rhs
lhs === rhs
}
func hash(into hasher: inout Hasher) {
@ -20,22 +21,12 @@ final class MountedCompositeComponent<R: Renderer>: MountedComponent<R>,
private var mountedChildren = [MountedComponent<R>]()
private let parentTarget: R.TargetType
let type: AnyCompositeComponent.Type
// HookedComponent implementation
/// There's no easy way to downcast elements of `[Any]` to `T`
/// and apply `inout` updater without creating copies, working around
/// that with pointers.
var state = [Any]()
var effects = [(observed: AnyEquatable?, Effect)]()
var effectFinalizers = [Finalizer]()
var refs = [AnyObject]()
init(_ node: AnyNode,
_ type: AnyCompositeComponent.Type,
init(_ node: AnyView,
_ parentTarget: R.TargetType) {
self.type = type
self.parentTarget = parentTarget
super.init(node)
@ -44,24 +35,17 @@ final class MountedCompositeComponent<R: Renderer>: MountedComponent<R>,
override func mount(with reconciler: StackReconciler<R>) {
let renderedNode = reconciler.render(component: self)
let child: MountedComponent<R> =
renderedNode.makeMountedComponent(parentTarget)
let child: MountedComponent<R> = renderedNode.makeMountedComponent(parentTarget)
mountedChildren = [child]
child.mount(with: reconciler)
}
override func unmount(with reconciler: StackReconciler<R>) {
mountedChildren.forEach { $0.unmount(with: reconciler) }
DispatchQueue.main.async {
for f in self.effectFinalizers {
f?()
}
}
}
override func update(with reconciler: StackReconciler<R>) {
// FIXME: for now without fragments mounted composite components have only
// FIXME: for now without fragments (groups?) mounted composite components have only
// a single element in `mountedChildren`, but this will change when
// fragments are implemented and this switch should be rewritten to compare
// all elements in `mountedChildren`
@ -75,16 +59,21 @@ final class MountedCompositeComponent<R: Renderer>: MountedComponent<R>,
// some mounted children
case let (wrapper?, renderedNode):
// new node is the same type as existing child, checking props/children
if wrapper.node.type == renderedNode.type,
wrapper.node.props != renderedNode.props ||
wrapper.node.children != renderedNode.children {
wrapper.node = renderedNode
let renderedNodeType = (renderedNode as? AnyView)?.type ?? type(of: renderedNode)
// FIXME: no idea if using `mangledName` is reliable, but seems to be the only way to get
// a name of a type constructor in runtime. Should definitely check if these are different
// across modules, otherwise can cause problems with views with same names in different
// modules.
// new node is the same type as existing child
// swiftlint:disable:next force_try
if try! wrapper.node.typeConstructorName == typeInfo(of: renderedNodeType).mangledName {
wrapper.node = AnyView(renderedNode)
wrapper.update(with: reconciler)
} else
// new node is of different type, complete rerender, i.e. unmount old
// wrapper, then mount a new one with new node
if wrapper.node.type != renderedNode.type {
} else {
// new node is of different type, complete rerender, i.e. unmount old
// wrapper, then mount a new one with new node
wrapper.unmount(with: reconciler)
let child: MountedComponent<R> =

View File

@ -23,12 +23,8 @@ public final class MountedHostComponent<R: Renderer>: MountedComponent<R> {
*/
private var target: R.TargetType?
public let type: AnyHostComponent.Type
init(_ node: AnyNode,
_ type: AnyHostComponent.Type,
init(_ node: AnyView,
_ parentTarget: R.TargetType) {
self.type = type
self.parentTarget = parentTarget
super.init(node)
@ -44,21 +40,10 @@ public final class MountedHostComponent<R: Renderer>: MountedComponent<R> {
reconciler.renderer?.update(target: target, with: self)
switch node.children.value {
case let nodes as [AnyNode]:
mountedChildren = nodes.map { $0.makeMountedComponent(target) }
mountedChildren.forEach { $0.mount(with: reconciler) }
guard !node.children.isEmpty else { return }
case let node as AnyNode:
let child: MountedComponent<R> = node.makeMountedComponent(target)
mountedChildren = [child]
child.mount(with: reconciler)
default:
// child type that can't be rendered, but still makes sense
// (e.g. `String`)
()
}
mountedChildren = node.children.map { $0.makeMountedComponent(target) }
mountedChildren.forEach { $0.mount(with: reconciler) }
}
override func unmount(with reconciler: StackReconciler<R>) {
@ -80,93 +65,64 @@ public final class MountedHostComponent<R: Renderer>: MountedComponent<R> {
reconciler.renderer?.update(target: target,
with: self)
switch node.children.value {
case var nodes as [AnyNode]:
switch (mountedChildren.isEmpty, nodes.isEmpty) {
// if existing children present and new children array is empty
// then unmount all existing children
case (false, true):
mountedChildren.forEach { $0.unmount(with: reconciler) }
mountedChildren = []
var nodes = node.children
// if no existing children then mount all new children
case (true, false):
mountedChildren = nodes.map { $0.makeMountedComponent(target) }
mountedChildren.forEach { $0.mount(with: reconciler) }
switch (mountedChildren.isEmpty, nodes.isEmpty) {
// if existing children present and new children array is empty
// then unmount all existing children
case (false, true):
mountedChildren.forEach { $0.unmount(with: reconciler) }
mountedChildren = []
// if both arrays have items then reconcile by types and keys
case (false, false):
var newChildren = [MountedComponent<R>]()
// if no existing children then mount all new children
case (true, false):
mountedChildren = nodes.map { $0.makeMountedComponent(target) }
mountedChildren.forEach { $0.mount(with: reconciler) }
// iterate through every `mountedChildren` element and compare with
// a corresponding `nodes` element, remount if type differs, otherwise
// run simple update
while let child = mountedChildren.first, let node = nodes.first {
let newChild: MountedComponent<R>
if node.type == mountedChildren[0].node.type {
// not sure if comparison of `props` between `child.node` and
// `node` are more computationally expensive than plainly
// updating props on a target. `children` comparison between
// `child.node` and `node` runs implicitly within this loop.
// From functionality perspective this should work and we need
// benchmarks before this can be optimised.
child.node = node
child.update(with: reconciler)
newChild = child
} else {
child.unmount(with: reconciler)
newChild = node.makeMountedComponent(target)
newChild.mount(with: reconciler)
}
newChildren.append(newChild)
mountedChildren.removeFirst()
nodes.removeFirst()
}
// if both arrays have items then reconcile by types and keys
case (false, false):
var newChildren = [MountedComponent<R>]()
// more mounted components left than nodes were to be rendered:
// unmount remaining `mountedChildren`
if !mountedChildren.isEmpty {
for child in mountedChildren {
child.unmount(with: reconciler)
}
} else {
// more nodes left than children were mounted,
// mount remaining nodes
for node in nodes {
let newChild: MountedComponent<R> =
node.makeMountedComponent(target)
newChild.mount(with: reconciler)
newChildren.append(newChild)
}
}
mountedChildren = newChildren
// both arrays are empty, nothing to reconcile
case (true, true):
()
}
case let node as AnyNode:
if let child = mountedChildren.first {
if child.node.type == node.type {
// iterate through every `mountedChildren` element and compare with
// a corresponding `nodes` element, remount if type differs, otherwise
// run simple update
while let child = mountedChildren.first, let node = nodes.first {
let newChild: MountedComponent<R>
if node.typeConstructorName == mountedChildren[0].node.typeConstructorName {
child.node = node
child.update(with: reconciler)
newChild = child
} else {
child.unmount(with: reconciler)
let child: MountedComponent<R> = node.makeMountedComponent(target)
child.mount(with: reconciler)
mountedChildren = [child]
newChild = node.makeMountedComponent(target)
newChild.mount(with: reconciler)
}
} else {
let child: MountedComponent<R> = node.makeMountedComponent(target)
child.mount(with: reconciler)
mountedChildren = [child]
newChildren.append(newChild)
mountedChildren.removeFirst()
nodes.removeFirst()
}
// child type that can't be rendered, but still makes sense as a child
// (e.g. `String`)
default:
// more mounted components left than nodes were to be rendered:
// unmount remaining `mountedChildren`
if !mountedChildren.isEmpty {
for child in mountedChildren {
child.unmount(with: reconciler)
}
} else {
// more nodes left than children were mounted,
// mount remaining nodes
for node in nodes {
let newChild: MountedComponent<R> =
node.makeMountedComponent(target)
newChild.mount(with: reconciler)
newChildren.append(newChild)
}
}
mountedChildren = newChildren
// both arrays are empty, nothing to reconcile
case (true, true):
()
}
}

View File

@ -49,7 +49,7 @@ extension Color: ExpressibleByIntegerLiteral {
extension Color {
public init?(hex: String) {
let cArray = Array(hex.replacingOccurrences(of: "#", with: ""))
let cArray = Array(hex.count > 6 ? String(hex.dropFirst()) : hex)
guard cArray.count == 6 else { return nil }

View File

@ -15,7 +15,7 @@ public struct Point: Equatable {
}
public static var zero: Point {
return Point(x: 0, y: 0)
Point(x: 0, y: 0)
}
}
@ -29,7 +29,7 @@ public struct Size: Equatable {
}
public static var zero: Size {
return Size(width: 0, height: 0)
Size(width: 0, height: 0)
}
}
@ -43,6 +43,6 @@ public struct Rectangle: Equatable {
}
public static var zero: Rectangle {
return Rectangle(.zero, .zero)
Rectangle(.zero, .zero)
}
}

View File

@ -34,9 +34,6 @@ public protocol Renderer: AnyObject {
instance.
- parameter component: Type of the host component that renders to the
newly created target.
- parameter props: Props used to configure the new target.
- parameter children: Children of the rendered host component for the new
target.
- returns: The newly created target.
*/
func mountTarget(
@ -49,13 +46,6 @@ public protocol Renderer: AnyObject {
- parameter target: Existing target instance to be updated.
- parameter component: Type of the host component that renders to the
updated target.
- parameter props: Props used to configure the existing target. This props
value can be different from props passed on previous
updates or on target creation. The props value is wrapped
with `AnyEquatable` for type-erasure purposes.
- parameter children: Children used to configure the existing target. These
children can be different from children passed on
previous updates or on target creation.
*/
func update(
target: TargetType,
@ -78,7 +68,7 @@ public protocol Renderer: AnyObject {
}
extension Renderer {
public func mount(with node: AnyNode, to parent: TargetType) -> Mounted {
public func mount(with node: AnyView, to parent: TargetType) -> Mounted {
let result: Mounted = node.makeMountedComponent(parent)
if let reconciler = reconciler {
result.mount(with: reconciler)
@ -86,7 +76,7 @@ extension Renderer {
return result
}
public func update(component: Mounted, with node: AnyNode) {
public func update(component: Mounted, with node: AnyView) {
component.node = node
if let reconciler = reconciler {
component.update(with: reconciler)

View File

@ -6,6 +6,7 @@
//
import Dispatch
import Runtime
public final class StackReconciler<R: Renderer> {
private var queuedRerenders = Set<MountedCompositeComponent<R>>()
@ -14,7 +15,7 @@ public final class StackReconciler<R: Renderer> {
private let rootComponent: MountedComponent<R>
private(set) weak var renderer: R?
public init(node: AnyNode, target: R.TargetType, renderer: R) {
public init<V: View>(node: V, target: R.TargetType, renderer: R) {
self.renderer = renderer
rootTarget = target
@ -23,9 +24,11 @@ public final class StackReconciler<R: Renderer> {
rootComponent.mount(with: self)
}
func queue(updater: (inout Any) -> (),
for component: MountedCompositeComponent<R>,
id: Int) {
func queueUpdate(
for component: MountedCompositeComponent<R>,
id: Int,
updater: (inout Any) -> ()
) {
let scheduleReconcile = queuedRerenders.isEmpty
updater(&component.state[id])
@ -46,37 +49,34 @@ public final class StackReconciler<R: Renderer> {
queuedRerenders.removeAll()
}
func render(component: MountedCompositeComponent<R>) -> AnyNode {
// Avoiding an indirect reference cycle here: this closure can be
// owned by callbacks owned by node's target, which is strongly referenced
// by the reconciler.
let hooks = Hooks(
component: component
) { [weak self, weak component] id, updater in
guard let component = component else { return }
self?.queue(updater: updater, for: component, id: id)
}
func render(component: MountedCompositeComponent<R>) -> some View {
// swiftlint:disable force_try
let info = try! typeInfo(of: component.node.type)
let stateProperties = info.properties.filter { $0.type is ValueStorage.Type }
let result = component.type.render(
props: component.node.props,
children: component.node.children,
hooks: hooks
)
for (id, stateProperty) in stateProperties.enumerated() {
// `ValueStorage` properties were already filtered out, so safe to assume the value's type
// swiftlint:disable:next force_cast
var state = try! stateProperty.get(from: component.node.view) as! ValueStorage
DispatchQueue.main.async {
for i in hooks.scheduledEffects {
if component.effectFinalizers.count > i {
component.effectFinalizers[i]?()
component.effectFinalizers[i] = component.effects[i].1()
} else {
component.effectFinalizers.append(component.effects[i].1())
}
if component.state.count == id {
component.state.append(state.anyInitialValue)
}
}
// clean up `component` reference to enable assertions when hooks are called
// outside of `render`
hooks.component = nil
state.getter = { component.state[id] }
// Avoiding an indirect reference cycle here: this closure can be
// owned by callbacks owned by node's target, which is strongly referenced
// by the reconciler.
state.setter = { [weak self, weak component] newValue in
guard let component = component else { return }
self?.queueUpdate(for: component, id: id) { $0 = newValue }
}
try! stateProperty.set(value: state, on: &component.node.view)
}
// swiftlint:enable force_try
let result = component.node.bodyClosure(component.node.view)
return result
}

View File

@ -0,0 +1,41 @@
//
// Created by Max Desiatov on 08/04/2020.
//
protocol ValueStorage {
var getter: (() -> Any)? { get set }
var setter: ((Any) -> ())? { get set }
var anyInitialValue: Any { get }
}
@propertyWrapper public struct State<Value> {
private let initialValue: Value
var anyInitialValue: Any { initialValue }
var getter: (() -> Any)?
var setter: ((Any) -> ())?
public init(wrappedValue value: Value) {
initialValue = value
}
public var wrappedValue: Value {
get { getter?() as? Value ?? initialValue }
nonmutating set { setter?(newValue) }
}
public var projectedValue: Binding<Value> {
guard let getter = getter, let setter = setter else {
fatalError("\(#function) not available outside of `body`")
}
// swiftlint:disable:next force_cast
return .init(get: { getter() as! Value }, set: { setter($0) })
}
}
extension State: ValueStorage {}
extension State where Value: ExpressibleByNilLiteral {
@inlinable public init() { self.init(wrappedValue: nil) }
}

View File

@ -6,9 +6,9 @@
//
open class Target {
public internal(set) var node: AnyNode
public internal(set) var node: AnyView
public init(node: AnyNode) {
self.node = node
public init<V: View>(node: V) {
self.node = AnyView(node)
}
}

View File

@ -1,60 +0,0 @@
//
// Unique.swift
// Tokamak
//
// Created by Max Desiatov on 06/11/2018.
//
/// Typealias for closures returning no value and wrapped with `Unique`.
public typealias Handler<T> = Unique<(T) -> ()>
/// Classes have identity even when they have no content. `UniqueReference`
/// defines `Equatable` as an identity comparison.
class UniqueReference {}
extension UniqueReference: Equatable {
public static func ==(lhs: UniqueReference, rhs: UniqueReference) -> Bool {
return lhs === rhs
}
}
/** `Unique` works around the fact that `ObjectIdentifier` can't take
closures as arguments despite closures being reference types and having
identity. You can pass any value or reference to `Unique.init`, which will
create a new identity. `Unique` implements `Equatable`, but will return
`false` on equality comparison of different identities (including closures)
Example:
```swift
let x = Unique(5)
let y = Unique(5)
let z = x
x == y // is `false`
x == z // is `true`
let closure1 = Unique { 5 }
let closure2 = Unique { 5 }
let closure3 = closure1
closure1 == closure2 // is `false`
closure3 == closure1 // is `true`
```
*/
public struct Unique<T> {
private let id = UniqueReference()
/// Unpacked value stored within `Unique` container.
public let value: T
/// Create a new `Unique` container.
public init(_ value: T) {
self.value = value
}
}
extension Unique: Equatable {
public static func ==(lhs: Unique<T>, rhs: Unique<T>) -> Bool {
return lhs.id == rhs.id
}
}

View File

@ -1,12 +0,0 @@
//
// View.swift
//
//
// Created by Max Desiatov on 07/04/2020.
//
public protocol View {
associatedtype Body: View
var body: Self.Body { get }
}

View File

@ -0,0 +1,46 @@
//
// Created by Max Desiatov on 08/04/2020.
//
import Runtime
public struct AnyView: View {
let type: Any.Type
let typeConstructorName: String
let bodyType: Any.Type
var view: Any
// needs to take a fresh version of `view` as an argument, otherwise it captures the old view value
let bodyClosure: (Any) -> AnyView
public init<V>(_ view: V) where V: View {
if let anyView = view as? AnyView {
type = anyView.type
typeConstructorName = anyView.typeConstructorName
bodyType = anyView.bodyType
self.view = anyView.view
bodyClosure = anyView.bodyClosure
} else {
type = V.self
// FIXME: no idea if using `mangledName` is reliable, but seems to be the only way to get
// a name of a type constructor in runtime. Should definitely check if these are different
// across modules, otherwise can cause problems with views with same names in different
// modules.
// swiftlint:disable:next force_try
typeConstructorName = try! typeInfo(of: type).mangledName
bodyType = V.Body.self
self.view = view
// swiftlint:disable:next force_cast
bodyClosure = { AnyView(($0 as! V).body) }
}
}
}
extension AnyView: ParentView {
var children: [AnyView] {
(view as? ParentView)?.children ?? []
}
}

View File

@ -0,0 +1,25 @@
//
// Button.swift
// Tokamak
//
// Created by Max Desiatov on 02/12/2018.
//
public struct Button<Label>: View where Label: View {
// FIXME: should be internal
public let label: Label
public let action: () -> ()
public init(action: @escaping () -> (), @ViewBuilder label: () -> Label) {
self.label = label()
self.action = action
}
}
extension Button where Label == Text {
public init<S>(_ title: S, action: @escaping () -> ()) where S: StringProtocol {
self.init(action: action) {
Text(title)
}
}
}

View File

@ -0,0 +1,28 @@
//
// Created by Max Desiatov on 08/04/2020.
//
public enum VerticalAlignment: Equatable {
case top
case center
case bottom
}
public struct HStack<Content>: View where Content: View {
// FIXME: should be internal
public let alignment: VerticalAlignment
public let spacing: CGFloat?
public let content: Content
public init(alignment: VerticalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> Content) {
self.alignment = alignment
self.spacing = spacing
self.content = content()
}
}
extension HStack: ParentView {
var children: [AnyView] {
(content as? GroupView)?.children ?? [AnyView(content)]
}
}

View File

@ -0,0 +1,16 @@
//
// Created by Max Desiatov on 08/04/2020.
//
public struct Text: View {
// FIXME: should be internal
public let content: String
public init(verbatim content: String) {
self.content = content
}
public init<S>(_ content: S) where S: StringProtocol {
self.content = String(content)
}
}

View File

@ -0,0 +1,21 @@
//
// Created by Max Desiatov on 08/04/2020.
//
public struct TupleView<T>: View {
public let value: T
let children: [AnyView]
public init(_ value: T) {
self.value = value
children = []
}
init<T1: View, T2: View>(_ v1: T1, _ v2: T2) where T == (T1, T2) {
value = (v1, v2)
children = [AnyView(v1), AnyView(v2)]
}
}
extension TupleView: GroupView {}

View File

@ -0,0 +1,28 @@
//
// View.swift
//
//
// Created by Max Desiatov on 07/04/2020.
//
public protocol View {
associatedtype Body: View
var body: Self.Body { get }
}
extension Never: View {
public typealias Body = Never
}
public extension View where Body == Never {
var body: Never { fatalError() }
}
/// A `View` type that renders with subviews, usually specified in the `Content` type argument
protocol ParentView {
var children: [AnyView] { get }
}
/// A `View` type that is not rendered, but "flattened" rendering all its children instead.
protocol GroupView: ParentView {}

View File

@ -0,0 +1,98 @@
//
// Created by Max Desiatov on 08/04/2020.
//
public struct EmptyView: View {
@inlinable public init() {}
}
// swiftlint:disable:next type_name
public enum _ConditionalContent<TrueBranch, FalseBranch>: View
where TrueBranch: View, FalseBranch: View {
case trueBranch(TrueBranch)
case falseBranch(FalseBranch)
}
@_functionBuilder public struct ViewBuilder {
public static func buildBlock() -> EmptyView { EmptyView() }
public static func buildBlock<Content>(
_ content: Content
) -> Content where Content: View {
content
}
public static func buildIf<Content>(_ content: Content?) -> Content? where Content: View {
content
}
public static func buildEither<TrueContent, FalseContent>(
first: TrueContent
) -> _ConditionalContent<TrueContent, FalseContent> where TrueContent: View, FalseContent: View {
.trueBranch(first)
}
public static func buildEither<TrueContent, FalseContent>(
second: FalseContent
) -> _ConditionalContent<TrueContent, FalseContent> where TrueContent: View, FalseContent: View {
.falseBranch(second)
}
}
// swiftlint:disable line_length
// swiftlint:disable large_tuple
// swiftlint:disable function_parameter_count
extension ViewBuilder {
public static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> TupleView<(C0, C1)> where C0: View, C1: View {
TupleView(c0, c1)
}
}
extension ViewBuilder {
public static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2) -> TupleView<(C0, C1, C2)> where C0: View, C1: View, C2: View {
TupleView((c0, c1, c2))
}
}
extension ViewBuilder {
public static func buildBlock<C0, C1, C2, C3>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3) -> TupleView<(C0, C1, C2, C3)> where C0: View, C1: View, C2: View, C3: View {
TupleView((c0, c1, c2, c3))
}
}
extension ViewBuilder {
public static func buildBlock<C0, C1, C2, C3, C4>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4) -> TupleView<(C0, C1, C2, C3, C4)> where C0: View, C1: View, C2: View, C3: View, C4: View {
TupleView((c0, c1, c2, c3, c4))
}
}
extension ViewBuilder {
public static func buildBlock<C0, C1, C2, C3, C4, C5>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5) -> TupleView<(C0, C1, C2, C3, C4, C5)> where C0: View, C1: View, C2: View, C3: View, C4: View, C5: View {
TupleView((c0, c1, c2, c3, c4, c5))
}
}
extension ViewBuilder {
public static func buildBlock<C0, C1, C2, C3, C4, C5, C6>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6) -> TupleView<(C0, C1, C2, C3, C4, C5, C6)> where C0: View, C1: View, C2: View, C3: View, C4: View, C5: View, C6: View {
TupleView((c0, c1, c2, c3, c4, c5, c6))
}
}
extension ViewBuilder {
public static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7)> where C0: View, C1: View, C2: View, C3: View, C4: View, C5: View, C6: View, C7: View {
TupleView((c0, c1, c2, c3, c4, c5, c6, c7))
}
}
extension ViewBuilder {
public static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8)> where C0: View, C1: View, C2: View, C3: View, C4: View, C5: View, C6: View, C7: View, C8: View {
TupleView((c0, c1, c2, c3, c4, c5, c6, c7, c8))
}
}
extension ViewBuilder {
public static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8, _ c9: C9) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9)> where C0: View, C1: View, C2: View, C3: View, C4: View, C5: View, C6: View, C7: View, C8: View, C9: View {
TupleView((c0, c1, c2, c3, c4, c5, c6, c7, c8, c9))
}
}

View File

@ -1,111 +0,0 @@
//
// AppKitRenderer.swift
// TokamakAppKit
//
// Created by Max Desiatov on 02/12/2018.
//
import AppKit
import Tokamak
// FIXME: working around "Couldn't lookup symbols: protocol witness table"
// compiler bug
// let _modalPresenterWitnessTableHack: NSHostComponent.Type =
// ModalPresenter.self
// let _stackControllerWitnessTableHack: NSHostComponent.Type =
// NavigationController.self
// let _navigationItemWitnessTableHack: NSHostComponent.Type =
// NavigationItem.self
// let _listViewWitnessTableHack: NSHostComponent.Type =
// ListView<HackyProvider>.self
// let _collectionViewWitnessTableHack: NSHostComponent.Type =
// CollectionView<HackyProvider>.self
struct HackyProvider: CellProvider {
static func cell(
props: Props, item: Int, path: CellPath
) -> AnyNode {
return Null.node()
}
typealias Props = Null
typealias Model = Int
}
class NSTarget: Target {
var viewController: NSViewController {
fatalError("\(#function) should be overriden in NSTarget subclass")
}
var refTarget: Any {
fatalError("\(#function) should be overriden in NSTarget subclass")
}
}
/// UIKitRenderer is an implementation of `Renderer` with UIKit as a target.
final class AppKitRenderer: Renderer {
private(set) var reconciler: StackReconciler<AppKitRenderer>?
private weak var rootViewController: NSViewController!
init(_ node: AnyNode, rootViewController: NSViewController) {
self.rootViewController = rootViewController
reconciler = StackReconciler(
node: node,
target: ViewBox(rootViewController.view, rootViewController, node),
renderer: self
)
}
private func typeAssertionFailure(for type: AnyHostComponent.Type) {
assertionFailure("""
component type \(type) not supported by AppKitRenderer
""")
}
func mountTarget(
to parent: NSTarget,
with component: AppKitRenderer.MountedHost
) -> NSTarget? {
guard let rendererComponent = component.type as? NSHostComponent.Type else {
typeAssertionFailure(for: component.type)
return nil
}
return rendererComponent.mountTarget(to: parent,
component: component,
self)
}
func update(
target: NSTarget,
with component: AppKitRenderer.MountedHost
) {
guard let rendererComponent = component.type as? NSHostComponent.Type else {
typeAssertionFailure(for: component.type)
return
}
rendererComponent.update(target: target,
node: component.node)
guard
let componentType = component.type as? AnyRefComponent.Type,
let anyRef = component.node.ref else { return }
componentType.update(ref: anyRef, with: target.refTarget)
}
func unmount(
target: NSTarget,
from parent: NSTarget,
with component: AppKitRenderer.MountedHost,
completion: @escaping () -> ()
) {
guard let rendererComponent = component.type as? NSHostComponent.Type else {
typeAssertionFailure(for: component.type)
return
}
rendererComponent.unmount(target: target, completion: completion)
}
}

View File

@ -1,27 +0,0 @@
//
// ContainerViewController.swift
// TokamakUIKit
//
// Created by Max Desiatov on 01/01/2019.
//
import AppKit
import Tokamak
final class ContainerViewController: NSViewController {
private let contained: NSView
init(contained: NSView) {
self.contained = contained
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
view.addSubview(contained)
}
}

View File

@ -1,104 +0,0 @@
//
// ControlBox.swift
// TokamakAppKit
//
// Created by Max Desiatov on 01/01/2019.
//
import AppKit
import Tokamak
final class Action {
private let handler: (()) -> ()
init(_ handler: @escaping (()) -> ()) {
self.handler = handler
}
@objc func perform() {
handler(())
}
}
let actionSelector = #selector(Action.perform)
/// Wraps Objective-C target/action pattern used by `UIControl` with a swifty
/// closure-based API.
class ControlBox<T: NSControl & Default>: ViewBox<T> {
private var handlersMask: NSEvent.EventTypeMask = []
private var handlers = [Event: Handler<()>]()
private var action: Action!
// this delegate stays as a constant and doesn't create a reference cycle
// swiftlint:disable:next weak_delegate
let delegate = TextEditingDelegate()
override init(
_ view: T,
_ viewController: NSViewController,
_ node: AnyNode
) {
super.init(view, viewController, node)
delegate.callback = { [weak self] in
self?.handlers[$0]?.value(())
}
action = Action { [weak self] in
guard let eventMask = NSApp.currentEvent?.associatedEventsMask else {
return
}
let events = eventMask.elements().compactMap { Event($0) }
for e in events {
self?.handlers[e]?.value(())
}
}
view.target = action
view.action = actionSelector
}
func bind(handlers: [Event: Handler<()>]) {
self.handlers = handlers
}
}
/// `OptionSet` enumeration for easy conversion from `EventTypeMask` to `Event`
extension OptionSet where RawValue: FixedWidthInteger {
func elements() -> AnySequence<Self> {
var remainingBits = rawValue
var bitMask: RawValue = 1
return AnySequence {
AnyIterator {
while remainingBits != 0 {
defer { bitMask = bitMask &* 2 }
if remainingBits & bitMask != 0 {
remainingBits = remainingBits & ~bitMask
return Self(rawValue: bitMask)
}
}
return nil
}
}
}
}
/** Helper class that work around the fact that `ControlBox` isn't a subclass
of `NSObject`, which is required by `NSControlTextEditingDelegate`
*/
class TextEditingDelegate: NSObject, NSControlTextEditingDelegate {
fileprivate var callback: ((Event) -> ())?
func controlTextDidBeginEditing(_: Notification) {
callback?(.editingDidBegin)
}
func controlTextDidChange(_ obj: Notification) {
callback?(.editingChanged)
}
func controlTextDidEndEditing(_ obj: Notification) {
callback?(.editingDidEnd)
}
}

View File

@ -1,26 +0,0 @@
//
// ViewBox.swift
// TokamakAppKit
//
// Created by Max Desiatov on 29/12/2018.
//
import AppKit
import Tokamak
class ViewBox<T: NSView>: ViewControllerBox<NSViewController> {
let view: T
/// Array of constraints installed from props that configured this view
var constraints = [NSLayoutConstraint]()
init(_ view: T, _ viewController: NSViewController, _ node: AnyNode) {
self.view = view
super.init(viewController, node)
}
override var refTarget: Any {
return view
}
}

View File

@ -1,26 +0,0 @@
//
// ViewControllerBox.swift
// TokamakAppKit
//
// Created by Max Desiatov on 11/01/2019.
//
import AppKit
import Tokamak
class ViewControllerBox<T: NSViewController>: NSTarget {
let containerViewController: T
init(_ viewController: T, _ node: AnyNode) {
containerViewController = viewController
super.init(node: node)
}
override var viewController: NSViewController {
return containerViewController
}
override var refTarget: Any {
return containerViewController
}
}

View File

@ -1,35 +0,0 @@
//
// Button.swift
// TokamakUIKit
//
// Created by Max Desiatov on 29/12/2018.
//
import AppKit
import Tokamak
final class TokamakButton: NSButton, Default {
/// This property can't be defined in a `UIButton` extension
/// as a plain `UIView` needs a different implementation of `defaultValue`
/// and subclass extensions can't override extensions of a parent class.
static var defaultValue: TokamakButton {
let result = TokamakButton()
result.bezelStyle = .rounded
return result
}
}
extension Button: NSControlComponent {
typealias Target = TokamakButton
public typealias RefTarget = NSButton
static func update(control box: ControlBox<TokamakButton>,
_ props: Button.Props,
_ children: [AnyNode]) {
let control = box.view
control.contentTintColor = props.titleColor.flatMap { NSColor($0) }
control.title = props.text
}
}

View File

@ -1,54 +0,0 @@
//
// Label.swift
// TokamakUIKit
//
// Created by Max Desiatov on 29/12/2018.
//
import AppKit
import Tokamak
final class TokamakLabel: NSTextView, Default {
static var defaultValue: TokamakLabel {
let result = TokamakLabel()
result.isEditable = false
result.backgroundColor = .clear
return result
}
}
extension NSTextAlignment {
public init(_ alignment: TextAlignment) {
switch alignment {
case .left:
self = .left
case .right:
self = .right
case .center:
self = .center
case .justified:
self = .justified
case .natural:
self = .natural
}
}
}
extension Label: NSViewComponent {
public typealias RefTarget = NSTextView
static func update(view box: ViewBox<TokamakLabel>,
_ props: Label.Props,
_ children: [AnyNode]) {
let view = box.view
view.alignment = NSTextAlignment(props.alignment)
view.textContainer?.maximumNumberOfLines = props.numberOfLines
view.textContainer?.lineBreakMode = NSLineBreakMode(props.lineBreakMode)
if let textColor = props.textColor {
view.textColor = NSColor(textColor)
} else {
view.textColor = .textColor
}
view.string = props.text
}
}

View File

@ -1,74 +0,0 @@
//
// StackView.swift
// TokamakUIKit
//
// Created by Max Desiatov on 29/12/2018.
//
import AppKit
import Tokamak
final class TokamakStackView: NSStackView, Default {
static var defaultValue: TokamakStackView {
return TokamakStackView()
}
}
extension NSLayoutConstraint.Attribute {
public init?(_ alignment: StackView.Props.Alignment) {
switch alignment {
case .top:
self = .top
case .bottom:
self = .bottom
case .leading:
self = .leading
case .trailing:
self = .trailing
case .fill, .center:
return nil
}
}
}
extension NSUserInterfaceLayoutOrientation {
public init(_ axis: StackView.Props.Axis) {
switch axis {
case .horizontal:
self = .horizontal
case .vertical:
self = .vertical
}
}
}
extension NSStackView.Distribution {
public init(_ distribution: StackView.Props.Distribution) {
switch distribution {
case .fill:
self = .fill
case .fillEqually:
self = .fillEqually
case .fillProportionally:
self = .fillProportionally
case .equalSpacing:
self = .equalSpacing
}
}
}
extension StackView: NSViewComponent {
public typealias RefTarget = NSStackView
static func update(view box: ViewBox<TokamakStackView>,
_ props: StackView.Props,
_: [AnyNode]) {
let view = box.view
NSLayoutConstraint.Attribute(props.alignment).flatMap {
view.alignment = $0
}
view.orientation = NSUserInterfaceLayoutOrientation(props.axis)
view.distribution = NSStackView.Distribution(props.distribution)
view.spacing = CGFloat(props.spacing)
}
}

View File

@ -1,23 +0,0 @@
//
// View.swift
// TokamakUIKit
//
// Created by Max Desiatov on 31/12/2018.
//
import AppKit
import Tokamak
final class TokamakView: NSView, Default {
public static var defaultValue: TokamakView {
return TokamakView()
}
}
extension ViewComponent: NSViewComponent {
public typealias RefTarget = NSView
static func update(view: ViewBox<TokamakView>,
_ props: ViewComponent.Props,
_: [AnyNode]) {}
}

View File

@ -1,26 +0,0 @@
//
// Color.swift
// TokamakAppKit
//
// Created by Max Desiatov on 14/12/2018.
//
import AppKit
import Tokamak
extension NSColor {
public convenience init(_ color: Color) {
switch color.space {
case .sRGB:
self.init(red: CGFloat(color.red),
green: CGFloat(color.green),
blue: CGFloat(color.blue),
alpha: CGFloat(color.alpha))
case .displayP3:
self.init(displayP3Red: CGFloat(color.red),
green: CGFloat(color.green),
blue: CGFloat(color.blue),
alpha: CGFloat(color.alpha))
}
}
}

View File

@ -1,39 +0,0 @@
//
// BaselineConstraint.swift
// TokamakAppKit
//
// Created by Max Desiatov on 03/02/2019.
//
import AppKit
import Tokamak
protocol BaselineConstraint {
var firstAnchor: KeyPath<NSView, NSLayoutYAxisAnchor> { get }
var secondAnchor: KeyPath<NSView, NSLayoutYAxisAnchor> { get }
var target: Constraint.Target { get }
var constant: Double { get }
}
extension BaselineConstraint {
func constraint(
current: NSView,
parent: NSView?,
next: NSView?
) -> [NSLayoutConstraint] {
let secondView: NSView?
switch target {
case .next:
secondView = next
case .parent:
secondView = parent
}
guard let second = secondView?[keyPath: secondAnchor] else { return [] }
return [current[keyPath: firstAnchor].constraint(
equalTo: second,
constant: CGFloat(constant)
)]
}
}

Some files were not shown because too many files have changed in this diff Show More