├── .swift-version ├── Sources ├── HolyCow │ └── main.swift └── SwiftWebUI │ ├── Misc │ ├── FakeCompactImplementations.swift │ └── DebugSwitches.swift │ ├── Values │ ├── ContentMode.swift │ ├── UserInterfaceSizeClass.swift │ ├── StaticMember.swift │ ├── Shadow.swift │ ├── ContentSizeCategory.swift │ ├── NavigationBarItem.swift │ ├── EditMode.swift │ ├── LocalizedStringKey.swift │ ├── ImagePaint.swift │ ├── VerticalAlignment.swift │ ├── HorizontalAlignment.swift │ ├── EdgeInsets.swift │ ├── Length.swift │ ├── Alignment.swift │ ├── Color.swift │ ├── Font.swift │ └── Formatter.swift │ ├── Views │ ├── Shapes │ │ ├── README.md │ │ ├── Shape.swift │ │ ├── ShapeView.swift │ │ ├── ShapeStyle.swift │ │ ├── Rectangle.swift │ │ ├── RoundedRectangle.swift │ │ ├── SizedShape.swift │ │ ├── Path.swift │ │ └── UXKit.swift │ ├── View.swift │ ├── Navigation │ │ ├── NavigationSidebar.swift │ │ ├── NavigationContent.swift │ │ ├── NavigationContext.swift │ │ ├── NavigationLink.swift │ │ └── NavigationView.swift │ ├── Images │ │ ├── ColorView.swift │ │ ├── CGImage.swift │ │ ├── SystemImage.swift │ │ └── Image.swift │ ├── Generic │ │ ├── DynamicViewContent.swift │ │ ├── NeverView.swift │ │ ├── EmptyView.swift │ │ ├── Group.swift │ │ ├── ForEach.swift │ │ ├── ConditionalContent.swift │ │ ├── AnyView.swift │ │ └── TapAction.swift │ ├── Forms │ │ ├── SecureField.swift │ │ ├── Form.swift │ │ ├── SUIButtonStyle.swift │ │ ├── SUIToggleStyle.swift │ │ ├── SUITextFieldStyle.swift │ │ ├── Stepper.swift │ │ ├── HTMLPopUpPickerStyle.swift │ │ └── HTMLRadioPickerStyle.swift │ ├── SemanticUI │ │ ├── SUIFlag.swift │ │ ├── SUISegment.swift │ │ ├── SUILabel.swift │ │ └── SUICard.swift │ ├── Layout │ │ ├── ScrollView.swift │ │ ├── Divider.swift │ │ ├── Spacer.swift │ │ ├── HStack.swift │ │ ├── VStack.swift │ │ ├── SwitchView.swift │ │ └── Section.swift │ ├── TabbedView │ │ ├── TabItemLabel.swift │ │ └── SUITabView.swift │ ├── HTML │ │ ├── HTML.swift │ │ └── HTMLContainer.swift │ └── Unsplash │ │ ├── Unsplash.swift │ │ └── UnsplashSource.swift │ ├── Modifiers │ ├── ViewTag.swift │ ├── TraitWritingModifier.swift │ ├── ResizableModifier.swift │ ├── VisibilityModifiers.swift │ ├── ShadowModifier.swift │ ├── ViewModifier.swift │ ├── EnvironmentObjectWritingModifier.swift │ ├── EnvironmentKeyWritingModifier.swift │ ├── FrameModifier.swift │ ├── BackgroundModifier.swift │ ├── PaddingModifier.swift │ ├── RelativeSizeModifiers.swift │ └── ModifiedContent.swift │ ├── WASMHosting │ ├── DispatchQueuePolyfill.swift │ ├── JSONPolyfill.swift │ └── NetworkingPolyfill.swift │ ├── ViewHosting │ ├── NIOSessionIDGeneration.swift │ ├── HTMLStringHelpers.swift │ ├── CSSStyles.swift │ ├── Serve.swift │ └── HTMLChange.swift │ ├── VirtualDOM │ ├── HTMLWrappingActionNode.swift │ ├── Generic │ │ ├── GroupNode.swift │ │ ├── NotImplementedViewNode.swift │ │ ├── EmptyNode.swift │ │ ├── HTMLSwitchNode.swift │ │ ├── ResolvedConditionNode.swift │ │ └── HTMLClickContainerNode.swift │ ├── HTMLLeafNode.swift │ ├── Images │ │ ├── CGImageNode.swift │ │ └── HTMLColorNode.swift │ ├── Layout │ │ ├── HTMLScrollNode.swift │ │ ├── FlexBoxSpacerNode.swift │ │ ├── HTMLShadowNode.swift │ │ ├── HTMLFrameNode.swift │ │ ├── HTMLBackgroundNode.swift │ │ ├── SUIListNode.swift │ │ ├── SUIListItemNode.swift │ │ └── HTMLPaddingNode.swift │ ├── HTML │ │ └── HTMLOutputNode.swift │ ├── HTMLWrappingNode.swift │ ├── Forms │ │ ├── SUIButtonNode.swift │ │ ├── HTMLSelectNode.swift │ │ ├── HTMLOptionNode.swift │ │ ├── HTMLRadioNode.swift │ │ └── SUIInputNode.swift │ ├── TreeBuilding │ │ └── SUIListBuilder.swift │ └── TabbedView │ │ ├── SUITabItemNode.swift │ │ ├── SUITabSegmentNode.swift │ │ └── SUITabContainerNode.swift │ ├── Properties │ ├── ObservableObject.swift │ ├── BindingConvertible.swift │ ├── Identifiable.swift │ ├── SelectionManager.swift │ ├── DynamicViewProperty.swift │ ├── State.swift │ ├── EnvironmentObject.swift │ └── ObservedObject.swift │ └── Environment │ ├── Environment.swift │ └── EnvironmentValues.swift ├── docs └── wasmgif.gif ├── .swiftpm └── xcode │ └── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── CONTRIBUTING.md ├── .travis.d ├── before-install.sh └── install.sh ├── .travis.yml ├── Makefile ├── Package.swift ├── .gitignore └── README.md /.swift-version: -------------------------------------------------------------------------------- 1 | wasm-DEVELOPMENT-SNAPSHOT-2020-05-24-a 2 | -------------------------------------------------------------------------------- /Sources/HolyCow/main.swift: -------------------------------------------------------------------------------- 1 | import SwiftWebUI 2 | 3 | SwiftWebUI.serve(Text("Holy Cow!")) 4 | -------------------------------------------------------------------------------- /docs/wasmgif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carson-katri/SwiftWebUI/HEAD/docs/wasmgif.gif -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Legal 2 | 3 | By submitting a pull request, you represent that you have the right to license 4 | your contribution to ZeeZide and the community, and agree by submitting the patch 5 | that your contributions are licensed under the Apache 2.0 license. 6 | 7 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Misc/FakeCompactImplementations.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FakeCompactImplementations.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 05.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | protocol PreviewProvider {} 10 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Values/ContentMode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentMode.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 10.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public enum ContentMode: Hashable { 10 | case fit 11 | case fill 12 | } 13 | 14 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Views/Shapes/README.md: -------------------------------------------------------------------------------- 1 | # Shapes 2 | 3 | Not quite sure what to do with those. 4 | 5 | Some shapes can be represented in CSS. Probably all in SVG. Need to figure out 6 | what one wants to happen. 7 | 8 | The shapes we support could generate a
for now? E.g. a Rectangle/RoundedRect. 9 | 10 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Values/UserInterfaceSizeClass.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserInterfaceSizeClass.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 22.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public enum UserInterfaceSizeClass: Hashable { 10 | case compact, regular 11 | } 12 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Views/View.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 05.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public protocol View { 10 | 11 | associatedtype Body : View 12 | 13 | var body : Self.Body { get } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Values/StaticMember.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StaticMember.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 18.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public struct StaticMember { 10 | // This is used for stuff like ListStyle 11 | public var base: Base 12 | } 13 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Values/Shadow.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | public struct Shadow: Hashable { 4 | public var color: Color, radius: Length, x: Length, y: Length 5 | } 6 | 7 | extension Shadow: CSSStyleValue { 8 | 9 | public var cssStringValue: String { 10 | return "\(radius.cssStringValue) \(x.cssStringValue) \(y.cssStringValue) \(color.cssStringValue)" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Values/ContentSizeCategory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentSizeCategory.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 10.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public enum ContentSizeCategory: Equatable, Hashable { 10 | case medium 11 | case extraSmall, small 12 | case large, extraLarge, extraExtraLarge, extraExtraExtraLarge 13 | } 14 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Views/Shapes/Shape.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Shape.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 24.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public protocol Shape: View, Equatable { 10 | func path(in rect: UXRect) -> Path 11 | } 12 | 13 | public protocol InsettableShape: Shape { 14 | associatedtype InsetShape : Shape 15 | func inset(by amount: Length) -> InsetShape 16 | } 17 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Views/Navigation/NavigationSidebar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationSidebar.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 22.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | struct NavigationSidebar: View { 10 | 11 | let root : Root 12 | 13 | var body: some View { 14 | HTMLContainer(classes: [ "swiftui-nav-sidebar" ]) { 15 | root 16 | } 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Values/NavigationBarItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationBarItem.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 23.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public struct NavigationBarItem { 10 | public enum TitleDisplayMode: Hashable { 11 | case automatic, inline, large 12 | } 13 | 14 | let view : AnyView 15 | let displayMode : NavigationBarItem.TitleDisplayMode 16 | } 17 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Modifiers/ViewTag.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewTag.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 18.06.19. 6 | // Copyright © 2019-2020 Helge Heß. All rights reserved. 7 | // 8 | 9 | public extension View { 10 | 11 | func tag(_ tag: V) -> some View { 12 | return modifier(TraitWritingModifier(content: ViewTag(value: tag))) 13 | } 14 | } 15 | 16 | public struct ViewTag: Trait { 17 | let value : Value 18 | } 19 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Values/EditMode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EditMode.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 22.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public enum EditMode: Hashable { 10 | 11 | case inactive, transient, active 12 | 13 | public var isEditing: Bool { 14 | switch self { 15 | case .inactive: return false 16 | case .transient, .active: return true 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/WASMHosting/DispatchQueuePolyfill.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DispatchQueuePolyfill.swift 3 | // 4 | // 5 | // Created by Carson Katri on 5/29/20. 6 | // 7 | 8 | public class DispatchQueue { 9 | public static let main = DispatchQueue() 10 | 11 | public func async(execute: () -> Void) { 12 | execute() 13 | } 14 | } 15 | 16 | public extension Error { 17 | var localizedDescription: String { 18 | String(describing: self) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Views/Images/ColorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorView.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 17.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | extension Color: View { 10 | public typealias Body = Never 11 | 12 | } 13 | 14 | extension Color: TreeBuildingView { 15 | 16 | func buildTree(in context: TreeStateContext) -> HTMLTreeNode { 17 | return HTMLColorNode(elementID: context.currentElementID, color: self) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Modifiers/TraitWritingModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TraitWritingModifier.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 18.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public struct TraitWritingModifier: ViewModifier { 10 | 11 | let content : Content 12 | 13 | public func push(to context: TreeStateContext) { 14 | context.setTrait(content) 15 | } 16 | public func pop(from context: TreeStateContext) { 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Views/Generic/DynamicViewContent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DynamicViewContent.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 11.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public protocol DynamicViewContent : View { 10 | // repeatable content 11 | 12 | associatedtype Data : Collection 13 | 14 | var data: Self.Data { get } 15 | } 16 | 17 | // Note: This has stuff like onDelete, onMove, onInsert, 18 | // which return `_TraitWritingModifier`s. 19 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Views/Images/CGImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGImage.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 22.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | #if canImport(CoreGraphics) 10 | 11 | import class CoreGraphics.CGImage 12 | 13 | public extension Image { 14 | 15 | init(_ cgImage: CGImage, scale: Length? = nil, label: Text? = nil) { 16 | self.storage = .cgImage(cgImage, scale: scale, label: label) 17 | } 18 | 19 | } 20 | 21 | #endif // canImport(CoreGraphics) 22 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Values/LocalizedStringKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalizedStringKey.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 20.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public struct LocalizedStringKey: Equatable, ExpressibleByStringInterpolation { 10 | 11 | let value : String 12 | 13 | public init(_ value: String) { 14 | self.value = value 15 | } 16 | public init(stringLiteral value: String) { 17 | self.value = value 18 | } 19 | 20 | // support the interpolation stuff 21 | } 22 | 23 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/ViewHosting/NIOSessionIDGeneration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NIOSessionIDGeneration.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 19.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | #if canImport(Foundation) && canImport(NIO) 10 | import struct Foundation.UUID 11 | 12 | extension NIOHostingSession { 13 | static func createSessionID() -> String { 14 | // As discussed in https://github.com/swiftwebui/SwiftWebUI/issues/4 15 | return UUID().uuidString 16 | } 17 | } 18 | #endif 19 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Views/Forms/SecureField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SecureField.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 23.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public struct SecureField: View { 10 | 11 | public let body : TextField 12 | 13 | public init(_ text: Binding, placeholder: Text? = nil, 14 | onCommit: (() -> Void)? = nil) 15 | { 16 | body = TextField(secureText: text, placeholder: placeholder, 17 | onCommit: onCommit) 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Views/SemanticUI/SUIFlag.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SUIFlag.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 26.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public struct SUIFlag: View { 10 | 11 | let clazz : String 12 | 13 | public init(code: String) { 14 | clazz = code.lowercased() 15 | } 16 | public init(_ name: String) { 17 | clazz = name.lowercased() 18 | } 19 | 20 | public var body: some View { 21 | HTMLContainer("i", classes: [ clazz, "flag" ]) { 22 | EmptyView() 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Views/Navigation/NavigationContent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationContent.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 22.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | struct NavigationContent: View { 10 | 11 | @EnvironmentObject private var navigationContext: NavigationContext 12 | 13 | var body: some View { 14 | HTMLContainer(classes: [ "swiftui-nav-content" ], 15 | styles: [ .flexGrow: 5 ]) 16 | { 17 | SwitchView(navigationContext.activeTargetView) 18 | .padding(.fontSize(0.5)) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Values/ImagePaint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImagePaint.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 24.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public struct ImagePaint: Equatable { 10 | 11 | public var image : Image 12 | public var sourceRect : UXRect 13 | public var scale : Length 14 | 15 | public init(image: Image, 16 | sourceRect: UXRect = UXRect(x: 0, y: 0, width: 1, height: 1), 17 | scale: Length = 1) 18 | { 19 | self.image = image 20 | self.sourceRect = sourceRect 21 | self.scale = scale 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Views/Generic/NeverView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NeverView.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 05.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | extension Never : View { 10 | 11 | public var body : Never { fatalError("no body in Never") } 12 | 13 | } 14 | 15 | extension View where Body == Never { 16 | public var body : Never { fatalError("no body in \(type(of: self))") } 17 | } 18 | 19 | extension Never: TreeBuildingView { 20 | func buildTree(in context: TreeStateContext) -> HTMLTreeNode { 21 | return EmptyNode(elementID: context.currentElementID) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.travis.d/before-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ "$TRAVIS_OS_NAME" == "Linux" ]]; then 4 | sudo apt-get install -y wget \ 5 | clang-3.6 libc6-dev make git libicu52 libicu-dev \ 6 | git autoconf libtool pkg-config \ 7 | libblocksruntime-dev \ 8 | libkqueue-dev \ 9 | libpthread-workqueue-dev \ 10 | systemtap-sdt-dev \ 11 | libbsd-dev libbsd0 libbsd0-dbg \ 12 | curl libcurl4-openssl-dev \ 13 | libedit-dev \ 14 | python2.7 python2.7-dev \ 15 | libxml2 16 | 17 | sudo update-alternatives --quiet --install /usr/bin/clang clang /usr/bin/clang-3.6 100 18 | sudo update-alternatives --quiet --install /usr/bin/clang++ clang++ /usr/bin/clang++-3.6 100 19 | fi 20 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Views/Generic/EmptyView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmptyView.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 05.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public struct EmptyView : View { 10 | public typealias Body = Never 11 | public init() {} 12 | } 13 | 14 | extension HTMLTreeBuilder { 15 | func buildTree(for view: EmptyView, in context: TreeStateContext) 16 | -> HTMLTreeNode 17 | { 18 | return EmptyNode(elementID: context.currentElementID) 19 | } 20 | } 21 | 22 | extension EmptyView: TreeBuildingView { 23 | func buildTree(in context: TreeStateContext) -> HTMLTreeNode { 24 | return context.currentBuilder.buildTree(for: self, in: context) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Views/Shapes/ShapeView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShapeView.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 24.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public struct ShapeView: View { 10 | public typealias Body = Never 11 | 12 | public var shape : Content 13 | public var style : Style 14 | } 15 | 16 | #if false // @available(*, unavailable) 17 | extension HTMLTreeBuilder { 18 | 19 | func buildTree(for view: ShapeView, 20 | in context: TreeStateContext) -> HTMLTreeNode 21 | { 22 | // TODO: Render a DIV? 23 | return NotImplementedViewNode() 24 | } 25 | 26 | } 27 | #endif 28 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/VirtualDOM/HTMLWrappingActionNode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTMLWrappingActionNode.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 23.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public protocol HTMLWrappingActionNode: HTMLWrappingNode { 10 | var isEnabled : Bool { get } 11 | var action : () -> Void { get } 12 | } 13 | 14 | extension HTMLWrappingActionNode { 15 | 16 | func invoke(_ webID: [ String ], in context: TreeStateContext) throws { 17 | guard elementID.isContainedInWebID(webID) else { return } 18 | if elementID.count == webID.count { 19 | action() 20 | } 21 | else { 22 | try content.invoke(webID, in: context) 23 | } 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Views/Shapes/ShapeStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShapeStyle.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 24.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public protocol ShapeStyle { 10 | } 11 | extension ShapeStyle { 12 | public typealias Member = StaticMember 13 | } 14 | 15 | extension Color: ShapeStyle { 16 | } 17 | 18 | extension ImagePaint: ShapeStyle { 19 | 20 | } 21 | 22 | #if false // for those?? 23 | extension StaticMember where Base : ShapeStyle { 24 | public static var color : Color.Member { 25 | return Color.Member(base: Color.black) 26 | } 27 | } 28 | #endif 29 | 30 | public struct ForegroundStyle: ShapeStyle { 31 | // No idea what this is. Just a type for matching? 32 | } 33 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Views/Generic/Group.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Group.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 16.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public struct Group : View { 10 | 11 | public typealias Body = Never 12 | 13 | let content : Content 14 | 15 | public init(@ViewBuilder content: () -> Content) { 16 | self.content = content() 17 | } 18 | } 19 | 20 | extension Group: CustomStringConvertible { 21 | public var description: String { return "" } 22 | } 23 | 24 | extension Group: TreeBuildingView { 25 | func buildTree(in context: TreeStateContext) -> HTMLTreeNode { 26 | return context.currentBuilder.buildTree(for: self, in: context) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.travis.d/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # our path is: 4 | # /home/travis/build/NozeIO/Noze.io/ 5 | 6 | if ! test -z "$SWIFT_SNAPSHOT_NAME"; then 7 | # Install Swift 8 | wget "${SWIFT_SNAPSHOT_NAME}" 9 | 10 | TARBALL="`ls swift-*.tar.gz`" 11 | echo "Tarball: $TARBALL" 12 | 13 | TARPATH="$PWD/$TARBALL" 14 | 15 | cd $HOME # expand Swift tarball in $HOME 16 | tar zx --strip 1 --file=$TARPATH 17 | pwd 18 | 19 | export PATH="$PWD/usr/bin:$PATH" 20 | which swift 21 | 22 | if [ `which swift` ]; then 23 | echo "Installed Swift: `which swift`" 24 | else 25 | echo "Failed to install Swift?" 26 | exit 42 27 | fi 28 | fi 29 | 30 | swift --version 31 | 32 | 33 | # Environment 34 | 35 | TT_SWIFT_BINARY=`which swift` 36 | 37 | echo "${TT_SWIFT_BINARY}" 38 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Views/Forms/Form.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Form.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 26.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public struct Form : View { 10 | // In our case a Form is just CSS styling, isn't it? 11 | // The CSS for this would appreciate some love ;-) Using a VStack for now. 12 | 13 | let content : VStack 14 | 15 | public init(@ViewBuilder content: () -> Content) { 16 | self.content = 17 | VStack(alignment: .stretch, content: content) 18 | } 19 | 20 | public var body: some View { 21 | return HTMLContainer("form", classes: [ "swiftui-form", "ui", "form" ]) { 22 | content 23 | .relativeWidth(1) 24 | } 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Modifiers/ResizableModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ResizableModifier.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Carson Katri on 6/13/20. 6 | // 7 | 8 | public extension Image { 9 | func resizable() -> ModifiedContent { 10 | return modifier(ResizableModifier(image: self)) 11 | } 12 | } 13 | 14 | public struct ResizableModifier: ViewModifier { 15 | let image: Image 16 | 17 | public func buildTree(for view: T, in context: TreeStateContext) 18 | -> HTMLTreeNode 19 | { 20 | return HTMLImageNode(elementID : context.currentElementID, 21 | storage : image.storage, 22 | scale : context.environment.imageScale, 23 | resizable : true) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/VirtualDOM/Generic/GroupNode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GroupNode.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 23.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | struct GroupNode : HTMLWrappingNode { 10 | 11 | let elementID : ElementID 12 | let content : HTMLTreeNode // type erased, hm 13 | 14 | func nodeByApplyingNewContent(_ newContent: HTMLTreeNode) -> Self { 15 | return GroupNode(elementID: elementID, content: newContent) 16 | } 17 | 18 | var isContentNode : Bool { return false } 19 | 20 | public func dump(nesting: Int) { 21 | let indent = String(repeating: " ", count: nesting) 22 | print("\(indent)") 23 | content.dump(nesting: nesting + 1) 24 | print("\(indent)") 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Views/Layout/ScrollView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScrollView.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 23.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public struct ScrollView : View, CustomStringConvertible { 10 | public typealias Body = Never 11 | 12 | // TODO: support the properties we can? 13 | 14 | let content: Content 15 | 16 | public init(@ViewBuilder content: () -> Content) { 17 | self.content = content() 18 | } 19 | 20 | public var description: String { 21 | return "" 22 | } 23 | } 24 | 25 | extension ScrollView: TreeBuildingView { 26 | func buildTree(in context: TreeStateContext) -> HTMLTreeNode { 27 | return context.currentBuilder.buildTree(for: self, in: context) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: generic 2 | 3 | notifications: 4 | slack: nozeio:LIFY1Jtkx0FRcLq3u1WliHRZ 5 | 6 | matrix: 7 | include: 8 | - os: Linux 9 | dist: xenial 10 | env: SWIFT_SNAPSHOT_NAME="https://swift.org/builds/swift-5.1-release/ubuntu1604/swift-5.1-RELEASE/swift-5.1-RELEASE-ubuntu16.04.tar.gz" 11 | sudo: required 12 | - os: Linux 13 | dist: xenial 14 | env: SWIFT_SNAPSHOT_NAME="https://swift.org/builds/swift-5.2-release/ubuntu1604/swift-5.2-RELEASE/swift-5.2-RELEASE-ubuntu16.04.tar.gz" 15 | sudo: required 16 | 17 | before_install: 18 | - ./.travis.d/before-install.sh 19 | 20 | install: 21 | - ./.travis.d/install.sh 22 | 23 | script: 24 | - export PATH="$HOME/usr/bin:$PATH" 25 | - swift build -c release 26 | - swift build -c debug 27 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Modifiers/VisibilityModifiers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VisibilityModifiers.swift 3 | // 4 | // 5 | // Created by Carson Katri on 5/28/20. 6 | // 7 | 8 | public extension View { 9 | func onAppear(perform action: (() -> Void)? = nil) -> some View { 10 | return modifier(OnLoadModifier(action: action ?? {})) 11 | } 12 | } 13 | 14 | struct OnLoadModifier: ViewModifier { 15 | let action: () -> Void 16 | 17 | func buildTree(for view: T, in context: TreeStateContext) -> HTMLTreeNode { 18 | // This is hacked in pretty poorly, but it works. 19 | // We really need a way to track this per-View. 20 | // Not sure how to go about that yet. 21 | if context.buildOrigin == .initial { 22 | action() 23 | } 24 | return context.currentBuilder.buildTree(for: view, in: context) 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Views/Layout/Divider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Divider.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 24.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public struct Divider : View, Equatable { 10 | 11 | public typealias Body = Never 12 | 13 | public init() {} 14 | } 15 | 16 | extension HTMLTreeBuilder { 17 | 18 | func buildTree(for view: Divider, in context: TreeStateContext) 19 | -> HTMLTreeNode 20 | { 21 | // TBD: Direction? 22 | return HTMLOutputNode(content: "
", 23 | escape: false) 24 | } 25 | } 26 | 27 | extension Divider: TreeBuildingView { 28 | func buildTree(in context: TreeStateContext) -> HTMLTreeNode { 29 | return context.currentBuilder.buildTree(for: self, in: context) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/VirtualDOM/Generic/NotImplementedViewNode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotImplementedView.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 08.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | class NotImplementedViewNode : HTMLLeafNode { 10 | 11 | var elementID: ElementID { return ElementID.noElementID } 12 | 13 | func generateHTML(into html: inout String) { 14 | assertionFailure("NOT IMPLEMENTED") 15 | } 16 | func generateChanges(from oldNode : HTMLTreeNode, 17 | into changeset : inout [ HTMLChange ], 18 | in context : TreeStateContext) 19 | { 20 | assertionFailure("NOT IMPLEMENTED") 21 | } 22 | 23 | func dump(nesting: Int) { 24 | let indent = String(repeating: " ", count: nesting) 25 | print("\(indent)NOT IMPLEMENTED!"); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Views/TabbedView/TabItemLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabItemLabel.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 18.06.19. 6 | // Copyright © 2019-2020 Helge Heß. All rights reserved. 7 | // 8 | 9 | public extension View { 10 | 11 | func tabItem(_ item: V) -> some View { 12 | // The official doesn't carry a type here, but just `AnyView?`. Hm. 13 | return modifier(TraitWritingModifier(content: 14 | TabItemLabel(value: AnyView(item)))) 15 | } 16 | 17 | @available(*, deprecated, renamed: "tabItem") 18 | func tabItemLabel(_ item: V) -> some View { 19 | return modifier(TraitWritingModifier(content: 20 | TabItemLabel(value: AnyView(item)))) 21 | } 22 | } 23 | 24 | public struct TabItemLabel: Trait { 25 | public typealias Value = AnyView 26 | let value : AnyView 27 | } 28 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Values/VerticalAlignment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VerticalAlignment.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 05.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public enum VerticalAlignment : Equatable { 10 | 11 | case top, center, bottom, firstTextBaseline, lastTextBaseline 12 | 13 | } 14 | 15 | extension VerticalAlignment { 16 | 17 | var flexBoxValue : String { 18 | 19 | switch self { 20 | case .top: return "flex-start" 21 | case .center: return "center" 22 | case .bottom: return "flex-end" 23 | case .firstTextBaseline, .lastTextBaseline: 24 | print("WARN: unsupported alignment property:", self) 25 | return "flex-start" 26 | } 27 | } 28 | } 29 | extension VerticalAlignment: CSSStyleValue { 30 | public var cssStringValue: String { return flexBoxValue } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Views/HTML/HTML.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTML.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 11.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public struct HTML : View, Equatable { 10 | 11 | public typealias Body = Never 12 | 13 | let content : String 14 | let escape : Bool 15 | 16 | public init(_ content: String, escape: Bool = false) { 17 | self.content = content 18 | self.escape = escape 19 | } 20 | 21 | } 22 | 23 | extension HTMLTreeBuilder { 24 | 25 | func buildTree(for view: HTML, in context: TreeStateContext) -> HTMLTreeNode 26 | { 27 | return HTMLOutputNode(content: view.content, escape: view.escape) 28 | } 29 | } 30 | 31 | extension HTML: TreeBuildingView { 32 | func buildTree(in context: TreeStateContext) -> HTMLTreeNode { 33 | return context.currentBuilder.buildTree(for: self, in: context) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Values/HorizontalAlignment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HorizontalAlignment.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 05.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public enum HorizontalAlignment : Equatable { 10 | 11 | case leading, center, trailing 12 | 13 | /// Hack: Tell a flexbox to stretch its contents. 14 | /// 15 | /// This should be automatic depending on the contents of the vstack. 16 | case stretch 17 | } 18 | 19 | extension HorizontalAlignment { 20 | 21 | var flexBoxValue : String { 22 | switch self { 23 | case .leading : return "flex-start" 24 | case .center : return "center" 25 | case .trailing : return "flex-end" 26 | case .stretch : return "stretch" 27 | } 28 | } 29 | } 30 | 31 | extension HorizontalAlignment: CSSStyleValue { 32 | public var cssStringValue: String { return flexBoxValue } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Views/Layout/Spacer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Spacer.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 08.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public struct Spacer : View, Equatable { 10 | 11 | public typealias Body = Never 12 | 13 | public let minLength: Length? 14 | 15 | public init(minLength: Length? = nil) { 16 | self.minLength = minLength 17 | } 18 | } 19 | 20 | extension HTMLTreeBuilder { 21 | func buildTree(for view: Spacer, in context: TreeStateContext) 22 | -> HTMLTreeNode 23 | { 24 | return FlexBoxSpacerNode(elementID: context.currentElementID, 25 | minLength: view.minLength) 26 | } 27 | } 28 | 29 | extension Spacer: TreeBuildingView { 30 | func buildTree(in context: TreeStateContext) -> HTMLTreeNode { 31 | return context.currentBuilder.buildTree(for: self, in: context) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Views/Shapes/Rectangle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Rectangle.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 24.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | @available(*, unavailable) 10 | public struct Rectangle: InsettableShape { 11 | 12 | public var body: some View { 13 | return ShapeView(shape: self, style: ForegroundStyle()) 14 | } 15 | 16 | public func path(in rect: UXRect) -> Path { 17 | return Path(rect) 18 | } 19 | public func inset(by amount: Length) -> some Shape { 20 | return Inset(amount: amount) 21 | } 22 | 23 | public struct Inset: Shape { 24 | 25 | public var amount : Length 26 | 27 | public var body: some View { 28 | return ShapeView(shape: self, style: ForegroundStyle()) 29 | } 30 | 31 | public func path(in rect: UXRect) -> Path { 32 | return Path(rect.insetBy(amount)) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Views/SemanticUI/SUISegment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SUISegment.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 17.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public struct SUISegment: View { 10 | // TODO: clean this up 11 | 12 | let styles : CSSStyles? 13 | let content : Content 14 | 15 | public init(width : Length? = .percent(100), 16 | height : Length? = nil, 17 | @ViewBuilder content: () -> Content) 18 | { 19 | // TODO: support relativeHeight 20 | var styles = CSSStyles() 21 | if let v = width { styles[.width] = v } 22 | if let v = height { styles[.height] = v } 23 | self.styles = styles.isEmpty ? nil : styles 24 | self.content = content() 25 | } 26 | 27 | public var body: some View { 28 | HTMLContainer(classes: ["ui", "segment"], styles: styles) { 29 | content 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Modifiers/ShadowModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | public extension View { 4 | func shadow(color: Color = .black, radius: Length, x: Length = 0, y: Length = 0) -> ModifiedContent 5 | { 6 | return modifier(ShadowModifier(value: Shadow(color: color, radius: radius, x: x, y: y))) 7 | } 8 | } 9 | 10 | public struct ShadowModifier: ViewModifier { 11 | public typealias Value = Shadow 12 | 13 | let value : Value 14 | 15 | public func buildTree(for view: T, in context: TreeStateContext) 16 | -> HTMLTreeNode 17 | { 18 | context.appendContentElementIDComponent() 19 | let child = context.currentBuilder.buildTree(for: view, in: context) 20 | context.deleteLastElementIDComponent() 21 | 22 | return HTMLShadowNode(elementID: context.currentElementID, 23 | value: value, content: child) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Properties/ObservableObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObservableObject.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 06.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | // FIXME: Combine requires 10.15, maybe provide a simple alternative 10 | #if canImport(Combine) 11 | import Combine 12 | #elseif canImport(OpenCombine) 13 | import OpenCombine 14 | #endif 15 | 16 | public protocol ObservableObject: AnyObject, DynamicViewProperty, Identifiable { 17 | 18 | associatedtype PublisherType : Publisher 19 | where Self.PublisherType.Failure == Never 20 | 21 | var didChange: Self.PublisherType { get } 22 | 23 | } 24 | 25 | public extension ObservableObject { 26 | 27 | subscript(keyPath: ReferenceWritableKeyPath) -> Binding { 28 | return Binding(getValue: { return self[keyPath: keyPath] }, 29 | setValue: { self[keyPath: keyPath] = $0 }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Modifiers/ViewModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewModifier.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 10.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public protocol ViewModifier { 10 | // We implement this, but it may work quite different to the original. 11 | 12 | // associatedtype Body : View 13 | //func body(content: Self.Content) -> Self.Body 14 | 15 | func push(to context: TreeStateContext) 16 | func pop (from context: TreeStateContext) 17 | 18 | func buildTree(for view: T, in context: TreeStateContext) 19 | -> HTMLTreeNode 20 | } 21 | 22 | extension ViewModifier { 23 | 24 | public func push(to context: TreeStateContext) {} 25 | public func pop (from context: TreeStateContext) {} 26 | 27 | public func buildTree(for view: T, in context: TreeStateContext) 28 | -> HTMLTreeNode 29 | { 30 | return context.currentBuilder.buildTree(for: view, in: context) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Modifiers/EnvironmentObjectWritingModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EnvironmentObjectWritingModifier.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 21.06.19. 6 | // Copyright © 2019-2020 Helge Heß. All rights reserved. 7 | // 8 | 9 | public extension View { 10 | 11 | func environmentObject(_ object: O) -> some View { 12 | return modifier(EnvironmentObjectWritingModifier(object)) 13 | } 14 | } 15 | 16 | public struct EnvironmentObjectWritingModifier 17 | : ViewModifier 18 | { 19 | private let object : O 20 | 21 | init(_ object: O) { 22 | self.object = object 23 | } 24 | 25 | public func push(to context: TreeStateContext) { 26 | var newEnvironment = context.environment 27 | newEnvironment[EnvironmentObjectKey.self] = object 28 | context.environmentStack.append(newEnvironment) 29 | } 30 | public func pop(from context: TreeStateContext) { 31 | context.environmentStack.removeLast() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/VirtualDOM/Generic/EmptyNode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmptyNode.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 10.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | struct EmptyNode: HTMLLeafNode { 10 | // Note: This still needs an ID, if used in an if-else ViewBuilder, the 11 | // changes will try to replace the ID with the other contents. 12 | 13 | let elementID: ElementID 14 | 15 | func generateHTML(into html: inout String) { 16 | html += "
") 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Views/Generic/ForEach.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ForEach.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 11.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public struct ForEach : DynamicViewContent 10 | where Data: RandomAccessCollection, Data.Element: Identifiable 11 | { 12 | // Aka WORepetition 13 | 14 | public typealias Body = Never 15 | 16 | public let data : Data 17 | 18 | let content : ( Data.Element ) -> Content 19 | 20 | public init(_ data: Data, 21 | content: @escaping ( Data.Element.IdentifiedValue ) -> Content) 22 | { 23 | self.init(data, content: { value in content(value.identifiedValue) }) 24 | } 25 | init(_ data: Data, content: @escaping ( Data.Element ) -> Content) { 26 | self.data = data 27 | self.content = content 28 | } 29 | } 30 | 31 | extension ForEach: TreeBuildingView { 32 | func buildTree(in context: TreeStateContext) -> HTMLTreeNode { 33 | return context.currentBuilder.buildTree(for: self, in: context) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/VirtualDOM/HTMLLeafNode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 20.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public protocol HTMLLeafNode: HTMLTreeNode { 10 | } 11 | 12 | extension HTMLLeafNode { 13 | 14 | func rewrite(using rewriter: ( HTMLTreeNode ) -> RewriteAction) 15 | -> HTMLTreeNode 16 | { 17 | switch rewriter(self) { 18 | case .replaceAndReturn(let newNode): return newNode 19 | case .recurse: return self 20 | } 21 | } 22 | 23 | func takeValue(_ webID: [ String ], value: String, 24 | in context: TreeStateContext) throws 25 | { 26 | throw WebInvocationError.inactiveElement(webID) 27 | } 28 | func invoke(_ webID: [ String ], in context: TreeStateContext) throws { 29 | throw WebInvocationError.inactiveElement(webID) 30 | } 31 | 32 | var children: [ HTMLTreeNode ] { return [] } 33 | func dump(nesting: Int) { 34 | let indent = String(repeating: " ", count: nesting) 35 | print("\(indent)<\(self)/>") 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/VirtualDOM/Images/CGImageNode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGImage.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 22.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | #if canImport(CoreGraphics) 10 | 11 | import struct Foundation.Data 12 | import class CoreGraphics.CGImage 13 | import ImageIO 14 | 15 | fileprivate let defaultImageDataCapacity = 32000 // TBD 16 | 17 | extension CGImage { 18 | 19 | // TBD: should that have scale data already? 20 | func generateData(type: String) -> Data? { 21 | guard let data = CFDataCreateMutable(nil, defaultImageDataCapacity) else { 22 | return nil 23 | } 24 | guard let destination = CGImageDestinationCreateWithData( 25 | data, type as CFString, 1, nil 26 | ) else { return nil } 27 | 28 | CGImageDestinationAddImage(destination, self, nil) 29 | guard CGImageDestinationFinalize(destination) else { 30 | print("could not render CGImage ...") 31 | return nil 32 | } 33 | return data as Data 34 | } 35 | 36 | } 37 | 38 | #endif // canImport(CoreGraphics) 39 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Views/Generic/ConditionalContent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConditionalContent.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 10.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public struct ConditionalContent : View 10 | where TrueContent : View, FalseContent : View 11 | { 12 | // When building, we only ever get one side, either True or False. 13 | // That means if the condition toggles, the full child tree won't 14 | // match up anymore? (unless they have an indentical structure?) 15 | public typealias Body = Never 16 | 17 | enum Content { 18 | case first(TrueContent) 19 | case second(FalseContent) 20 | } 21 | let content : Content 22 | 23 | init(first : TrueContent) { content = .first(first) } 24 | init(second : FalseContent) { content = .second(second) } 25 | 26 | } 27 | 28 | extension ConditionalContent: TreeBuildingView { 29 | func buildTree(in context: TreeStateContext) -> HTMLTreeNode { 30 | return context.currentBuilder.buildTree(for: self, in: context) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Views/Layout/HStack.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HStack.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 08.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public struct HStack : View, CustomStringConvertible { 10 | public typealias Body = Never 11 | 12 | let alignment : VerticalAlignment 13 | let spacing : Length? 14 | let content : Content 15 | 16 | public var description: String { 17 | if let spacing = spacing { 18 | return "" 19 | } 20 | else { 21 | return "" 22 | } 23 | } 24 | 25 | public init(alignment: VerticalAlignment = .center, 26 | spacing: Length? = nil, @ViewBuilder content: () -> Content) 27 | { 28 | self.alignment = alignment 29 | self.spacing = spacing 30 | self.content = content() 31 | } 32 | } 33 | 34 | extension HStack: TreeBuildingView { 35 | func buildTree(in context: TreeStateContext) -> HTMLTreeNode { 36 | return context.currentBuilder.buildTree(for: self, in: context) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Views/Layout/VStack.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VStack.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 05.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public struct VStack : View, CustomStringConvertible { 10 | public typealias Body = Never 11 | 12 | let alignment : HorizontalAlignment 13 | let spacing : Length? 14 | let content : Content 15 | 16 | public init(alignment: HorizontalAlignment = .center, 17 | spacing: Length? = nil, @ViewBuilder content: () -> Content) 18 | { 19 | self.alignment = alignment 20 | self.spacing = spacing 21 | self.content = content() 22 | } 23 | 24 | public var description: String { 25 | if let spacing = spacing { 26 | return "" 27 | } 28 | else { 29 | return "" 30 | } 31 | } 32 | } 33 | 34 | extension VStack: TreeBuildingView { 35 | func buildTree(in context: TreeStateContext) -> HTMLTreeNode { 36 | return context.currentBuilder.buildTree(for: self, in: context) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Views/Shapes/RoundedRectangle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RoundedRectangle.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 24.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | @available(*, unavailable) 10 | public struct RoundedRectangle: InsettableShape { 11 | 12 | public var cornerRadius: Length 13 | 14 | public var body: some View { 15 | return ShapeView(shape: self, style: ForegroundStyle()) 16 | } 17 | 18 | public func path(in rect: UXRect) -> Path { 19 | return Path(roundedRect: rect, cornerRadius: cornerRadius) 20 | } 21 | public func inset(by amount: Length) -> some Shape { 22 | return Inset(cornerRadius: cornerRadius, amount: amount) 23 | } 24 | 25 | public struct Inset: Shape { 26 | 27 | public var cornerRadius : Length 28 | public var amount : Length 29 | 30 | public var body: some View { 31 | return ShapeView(shape: self, style: ForegroundStyle()) 32 | } 33 | 34 | public func path(in rect: UXRect) -> Path { 35 | return Path(roundedRect: rect.insetBy(amount), cornerRadius: cornerRadius) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Views/Shapes/SizedShape.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SizedShape.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 24.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public extension Shape { 10 | 11 | func size(_ size: UXSize) -> SizedShape { 12 | return SizedShape(shape: self, size: size) 13 | } 14 | 15 | func size(width: Length, height: Length) -> SizedShape { 16 | // Hmmmm, this takes a Length. So UXSize should probably be based around 17 | // lengths? CSS would probably deal with that quite right. 18 | switch ( width, height ) { 19 | case ( .pixels(let w), .pixels(let h) ): 20 | return SizedShape(shape: self, size: UXSize(width: w, height: h)) 21 | default: 22 | fatalError("unsupported Length unit") 23 | } 24 | } 25 | } 26 | 27 | public struct SizedShape: Shape, Equatable { 28 | 29 | public var shape : S 30 | public var size : UXSize 31 | 32 | public func path(in rect: UXRect) -> Path { 33 | // No idea whether this is correct. 34 | return shape.path(in: UXRect(origin: rect.origin, size: size)) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Views/Navigation/NavigationContext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationContext.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 22.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | #if canImport(Combine) 10 | import Combine 11 | #elseif canImport(OpenCombine) 12 | import OpenCombine 13 | #endif 14 | 15 | final class NavigationContext: ObservableObject { 16 | 17 | private(set) var activeTargetView : AnyView { 18 | didSet { didChange.send(()) } 19 | } 20 | 21 | init(_ initialTargetView: AnyView) { 22 | self.activeTargetView = initialTargetView 23 | } 24 | init(_ initialTargetView: V) { 25 | self.activeTargetView = AnyView(initialTargetView) 26 | } 27 | 28 | func navigate(to view: V) { 29 | activeTargetView = AnyView(view) 30 | } 31 | func navigate(to view: AnyView) { 32 | activeTargetView = view 33 | } 34 | 35 | var didChange = PassthroughSubject() 36 | } 37 | 38 | extension NavigationContext: CustomStringConvertible { 39 | var description: String { 40 | return "" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/VirtualDOM/Layout/HTMLScrollNode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTMLScrollView.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 23.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | struct HTMLScrollNode : HTMLWrappingNode { 10 | 11 | let elementID : ElementID 12 | let content : HTMLTreeNode // type erased, hm 13 | 14 | func nodeByApplyingNewContent(_ newContent: HTMLTreeNode) -> Self { 15 | return HTMLScrollNode(elementID: elementID, content: newContent) 16 | } 17 | 18 | var containsSpacer: Bool { 19 | // I guess, we behave like a spacer and grab all available width? 20 | return false 21 | } 22 | 23 | func generateHTML(into html: inout String) { 24 | html += "
") 37 | content.dump(nesting: nesting + 1) 38 | print("\(indent)") 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Views/Generic/AnyView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyView.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 17.06.19. 6 | // Copyright © 2019-2020 Helge Heß. All rights reserved. 7 | // 8 | 9 | public struct AnyView : View { 10 | 11 | public typealias Body = Never 12 | 13 | #if DEBUG 14 | private let viewDescription : () -> String 15 | #endif 16 | let viewType : Any.Type 17 | var bodyBuild : ( TreeStateContext ) -> HTMLTreeNode 18 | 19 | public init(_ view: V) { 20 | #if DEBUG 21 | self.viewDescription = { return String(describing: view) } 22 | #endif 23 | self.viewType = V.self 24 | self.bodyBuild = { context in 25 | context.currentBuilder.buildTree(for: view, in: context) 26 | } 27 | } 28 | } 29 | 30 | extension AnyView: CustomStringConvertible { 31 | 32 | public var description: String { 33 | #if DEBUG 34 | return "" 35 | #else 36 | return "" 37 | #endif 38 | } 39 | } 40 | 41 | extension AnyView: TreeBuildingView { 42 | func buildTree(in context: TreeStateContext) -> HTMLTreeNode { 43 | return context.currentBuilder.buildTree(for: self, in: context) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/ViewHosting/HTMLStringHelpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WOHelpers.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 08.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | extension String { 10 | 11 | mutating func appendContentString(_ s: String) { 12 | append(s) 13 | } 14 | mutating func appendContentHTMLString(_ s: String) { 15 | append(s.htmlEscaped) 16 | } 17 | mutating func appendContentHTMLAttributeValue(_ s: String) { 18 | append(s.htmlEscaped) 19 | } 20 | 21 | mutating func appendAttribute(_ name: String, _ value: String?) { 22 | append(" ") 23 | appendContentString(name) // TODO: escape properly! 24 | if let value = value { 25 | append("=\"") 26 | appendContentHTMLAttributeValue(value) 27 | append("\"") 28 | } 29 | } 30 | } 31 | 32 | fileprivate let escapeMap : [ Character : String ] = [ 33 | "<" : "<", ">": ">", "&": "&", "\"": """ 34 | ] 35 | extension Character { 36 | var htmlEscaped : String { 37 | return escapeMap[self] ?? "\(self)" 38 | } 39 | } 40 | extension StringProtocol { 41 | var htmlEscaped : String { 42 | return map { escapeMap[$0] ?? String($0) }.reduce("", +) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Environment/Environment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Environment.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 10.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | @propertyWrapper 10 | public struct Environment: DynamicViewProperty { 11 | 12 | // (\EnvironmentValues.isEnabled) or just (\.isEnabled) 13 | let keyPath : KeyPath 14 | 15 | public init(_ keyPath: KeyPath) { 16 | self.keyPath = keyPath 17 | } 18 | 19 | // DynamicViewProperty 20 | // update => this updates the value from the environment I think 21 | // how do we receive the environment? Is that a global? 22 | 23 | private var _value: Value? 24 | 25 | public var wrappedValue: Value { 26 | guard let value = _value else { 27 | fatalError("you cannot access @Environment outside of `body`") 28 | } 29 | return value 30 | } 31 | 32 | public mutating func update() { 33 | guard let context = DynamicViewPropertyHelpers.currentContext else { 34 | assertionFailure("you cannot access @Environment outside of `body`") 35 | return 36 | } 37 | 38 | _value = context.environment[keyPath: keyPath] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Views/Navigation/NavigationLink.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationLink.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 22.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public struct NavigationLink: View { 10 | // TBD: What is NavigationDestinationLink? Same like a navcontext? 11 | 12 | @EnvironmentObject private var navigationContext: NavigationContext 13 | 14 | private let destination : Destination 15 | private let onTrigger : (() -> Bool)? 16 | private let content : Content 17 | 18 | public init(destination : Destination, 19 | onTrigger : (() -> Bool)? = nil, 20 | @ViewBuilder content: () -> Content) 21 | { 22 | self.destination = destination 23 | self.onTrigger = onTrigger 24 | self.content = content() 25 | } 26 | 27 | public var body: some View { 28 | Button(action: onClick, label: content) 29 | .relativeWidth(1.0) 30 | } 31 | 32 | func onClick() { 33 | if let onTrigger = onTrigger, !onTrigger() { return } 34 | navigationContext.navigate(to: destination) 35 | } 36 | } 37 | 38 | @available(*, deprecated, renamed: "NavigationLink") 39 | public typealias NavigationButton = NavigationLink 40 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Modifiers/EnvironmentKeyWritingModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EnvironmentKeyWritingModifier.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 10.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public extension View { 10 | 11 | func environment(_ keyPath: WritableKeyPath, 12 | _ value: V) 13 | -> ModifiedContent> 14 | { 15 | return modifier(EnvironmentKeyWritingModifier(keyPath, value)) 16 | } 17 | 18 | } 19 | 20 | public struct EnvironmentKeyWritingModifier: ViewModifier { 21 | 22 | typealias EnvironmentKeyPath = WritableKeyPath 23 | 24 | private let keyPath : EnvironmentKeyPath 25 | private let value : Value 26 | 27 | init(_ keyPath: EnvironmentKeyPath, _ value: Value) { 28 | self.keyPath = keyPath 29 | self.value = value 30 | } 31 | 32 | public func push(to context: TreeStateContext) { 33 | var newEnvironment = context.environment 34 | newEnvironment[keyPath: keyPath] = value 35 | context.environmentStack.append(newEnvironment) 36 | } 37 | public func pop(from context: TreeStateContext) { 38 | context.environmentStack.removeLast() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Modifiers/FrameModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FrameModifier.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 17.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public extension View { 10 | 11 | func frame(width: Length? = nil, height: Length? = nil, 12 | alignment: Alignment = .center) 13 | -> ModifiedContent 14 | { 15 | return modifier(FrameLayout(value: 16 | .init(width: width, height: height, alignment: alignment))) 17 | } 18 | } 19 | 20 | public struct FrameLayout: ViewModifier { 21 | 22 | public typealias Content = Value 23 | 24 | public struct Value: Equatable { 25 | let width : Length? 26 | let height : Length? 27 | let alignment : Alignment 28 | } 29 | 30 | let value : Value 31 | 32 | public func buildTree(for view: T, in context: TreeStateContext) 33 | -> HTMLTreeNode 34 | { 35 | context.appendContentElementIDComponent() 36 | let child = context.currentBuilder.buildTree(for: view, in: context) 37 | context.deleteLastElementIDComponent() 38 | 39 | return HTMLFrameNode(elementID: context.currentElementID, 40 | value: value, content: child) 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Views/Unsplash/Unsplash.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Unsplash.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 25.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | #if canImport(Foundation) 10 | extension Image { 11 | 12 | public init(_ source: UnsplashSource, label: Text? = nil) { 13 | self.storage = .url(source.url, label: label) 14 | } 15 | 16 | public static func unsplash(scope : UnsplashSource.Scope = .none, 17 | time : UnsplashSource.Time = .all, 18 | size : UXSize = UXSize(width: 640, height: 480), 19 | terms : [ String ]) -> Image 20 | { 21 | return Image(UnsplashSource( 22 | scope: scope, time: time, 23 | size: 24 | UnsplashSource.Size(width: Int(size.width), height: Int(size.height)), 25 | terms: terms) 26 | ) 27 | } 28 | public static func unsplash(scope : UnsplashSource.Scope = .none, 29 | time : UnsplashSource.Time = .all, 30 | size : UXSize = UXSize(width: 640, height: 480), 31 | _ terms : String...) -> Image 32 | { 33 | return unsplash(scope: scope, time: time, size: size, terms: terms) 34 | } 35 | } 36 | #endif 37 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Views/Shapes/Path.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Path.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 24.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public enum Path : Equatable { 10 | // TODO: Presumably we could support any kind of Path via SVG, and potentially 11 | // CSS. Which can also mask & clip using pathes. 12 | 13 | case rect (_ bounds: UXRect) 14 | case roundedRect(_ bounds: UXRect, cornerRadius: Length) 15 | 16 | public init(_ rect: UXRect) { 17 | self = .rect(rect) 18 | } 19 | public init(roundedRect bounds: UXRect, cornerRadius: Length) { 20 | self = .roundedRect(bounds, cornerRadius: cornerRadius) 21 | } 22 | 23 | public var isEmpty: Bool { return false } 24 | 25 | public var boundingRect: UXRect { 26 | switch self { 27 | case .rect (let bounds): return bounds 28 | case .roundedRect(let bounds, _): return bounds 29 | } 30 | } 31 | } 32 | 33 | extension Path: Shape { 34 | 35 | public func path(in rect: UXRect) -> Path { 36 | // TBD: Should that scale our rect values?? Or center? Or what? :-) 37 | switch self { 38 | case .rect: 39 | return Path(rect) 40 | case .roundedRect(_, let cornerRadius): 41 | return Path(roundedRect: rect, cornerRadius: cornerRadius) 42 | } 43 | } 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Misc/DebugSwitches.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DebugSwitches.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 25.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | #if DEBUG 10 | let debugComponentScopes = false 11 | let debugInvalidation = false 12 | let debugEnvironmentChanges = false 13 | let debugRequestPhases = false 14 | let debugDumpTrees = false 15 | let debugTraits = false 16 | #else 17 | let debugComponentScopes = false 18 | let debugInvalidation = false 19 | let debugEnvironmentChanges = false 20 | let debugRequestPhases = false 21 | let debugDumpTrees = false 22 | let debugTraits = false 23 | #endif 24 | 25 | 26 | 27 | extension ObservableObject { // can't extend AnyObject ... 28 | 29 | var pointerDescription: String { 30 | return ObjectIdentifier(self).shortRawPointerString 31 | } 32 | 33 | } 34 | 35 | extension ObjectIdentifier { 36 | 37 | var rawPointerString: String { 38 | let s = String(describing: self) 39 | return s.hasPrefix("ObjectIdentifier(") 40 | ? String(s.dropFirst(17).dropLast(1)) 41 | : s 42 | } 43 | var shortRawPointerString: String { 44 | #if canImport(Foundation) 45 | rawPointerString.replacingOccurrences(of: "0x0000000", with: "0x") 46 | #else 47 | return rawPointerString 48 | #endif 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile 2 | 3 | # local config 4 | SWIFT_BUILD=swift build 5 | SWIFT_CLEAN=swift package clean 6 | SWIFT_BUILD_DIR=.build 7 | 8 | # docker config 9 | SWIFT_BUILD_IMAGE="helje5/swift-dev:5.2.0" 10 | CONFIGURATION=release 11 | DOCKER_BUILD_DIR=".docker.build" 12 | SWIFT_DOCKER_BUILD_DIR="$(DOCKER_BUILD_DIR)/x86_64-unknown-linux/$(CONFIGURATION)" 13 | DOCKER_BUILD_PRODUCT="$(DOCKER_BUILD_DIR)/$(TOOL_NAME)" 14 | 15 | 16 | SWIFT_SOURCES=\ 17 | Sources/*/*/*.swift \ 18 | Sources/*/*/*/*.swift 19 | 20 | all: 21 | $(SWIFT_BUILD) 22 | 23 | clean : 24 | $(SWIFT_CLEAN) 25 | # We have a different definition of "clean", might be just German 26 | # pickyness. 27 | rm -rf $(SWIFT_BUILD_DIR) 28 | 29 | $(DOCKER_BUILD_PRODUCT): $(SWIFT_SOURCES) 30 | time docker run --rm \ 31 | -v "$(PWD):/src" \ 32 | -v "$(PWD)/$(DOCKER_BUILD_DIR):/src/.build" \ 33 | "$(SWIFT_BUILD_IMAGE)" \ 34 | bash -c 'cd /src && swift build -c $(CONFIGURATION)' 35 | ls -lah $(DOCKER_BUILD_PRODUCT) 36 | 37 | docker-all: $(DOCKER_BUILD_PRODUCT) 38 | 39 | docker-clean: 40 | rm -rf $(DOCKER_BUILD_PRODUCT) 41 | 42 | docker-distclean: 43 | rm -rf $(DOCKER_BUILD_DIR) 44 | 45 | distclean: clean docker-distclean 46 | 47 | docker-emacs: 48 | docker run --rm -it \ 49 | -v "$(PWD):/src" \ 50 | -v "$(PWD)/$(DOCKER_BUILD_DIR):/src/.build" \ 51 | "$(SWIFT_BUILD_IMAGE)" \ 52 | emacs /src 53 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Modifiers/BackgroundModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BackgroundModifier.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 10.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public extension View { 10 | 11 | func background(_ color: Color? = nil , cornerRadius: Length? = nil) 12 | -> ModifiedContent 13 | { 14 | return modifier(BackgroundModifier(value: ( color, cornerRadius ))) 15 | } 16 | } 17 | 18 | public struct BackgroundModifier: ViewModifier { 19 | // This is very different, we don't do the shape thing yet. 20 | // ShapeStyle => Shape into View 21 | // Shape => no accessible inits 22 | // Presumably Shape would be an SVG thing? But they also use it for 23 | // clipping etc. So maybe rather CSS 24 | // So: 25 | // We just have color and radius :-) 26 | 27 | public typealias Value = ( color: Color?, cornerRadius: Length? ) 28 | 29 | let value : Value 30 | 31 | 32 | public func buildTree(for view: T, in context: TreeStateContext) 33 | -> HTMLTreeNode 34 | { 35 | context.appendContentElementIDComponent() 36 | let child = context.currentBuilder.buildTree(for: view, in: context) 37 | context.deleteLastElementIDComponent() 38 | 39 | return HTMLBackgroundNode(elementID: context.currentElementID, 40 | value: value, content: child) 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/VirtualDOM/HTML/HTMLOutputNode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTMLOutputNode.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 23.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | struct HTMLOutputNode: HTMLLeafNode, Equatable { 10 | 11 | var elementID : ElementID { return ElementID.noElementID } 12 | let content : String 13 | let escape : Bool 14 | 15 | init(content: String, escape: Bool) { 16 | self.content = content 17 | self.escape = escape 18 | } 19 | 20 | func generateHTML(into html: inout String) { 21 | if escape { 22 | html.appendContentHTMLString(content) 23 | } 24 | else { 25 | html.appendContentString(content) 26 | } 27 | } 28 | 29 | func generateChanges(from oldNode : HTMLTreeNode, 30 | into changeset : inout [ HTMLChange ], 31 | in context : TreeStateContext) 32 | { 33 | guard let oldNode = sameType(oldNode, &changeset) else { return } 34 | 35 | if oldNode.content != content || oldNode.escape != escape { 36 | print("WARN: can't change raw HTML views:", self) 37 | } 38 | } 39 | 40 | // MARK: - Debugging 41 | 42 | public func dump(nesting: Int) { 43 | let indent = String(repeating: " ", count: nesting) 44 | if escape { print("\(indent)") } 45 | else { print("\(indent)") } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Views/Forms/SUIButtonStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SUIButtonStyle.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 22.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public struct SUIButtonStyle: ButtonStyle { 10 | 11 | public typealias Label = ButtonStyleLabel 12 | public typealias Member = StaticMember 13 | 14 | public func body(configuration: Button) -> Body { 15 | return Body(configuration: configuration) 16 | } 17 | 18 | public struct Body: View { 19 | public typealias Body = Never 20 | 21 | let configuration : Button 22 | 23 | } 24 | } 25 | 26 | extension HTMLTreeBuilder { 27 | 28 | func buildTree(for view: SUIButtonStyle.Body, in context: TreeStateContext) 29 | -> HTMLTreeNode 30 | { 31 | let childTree = _buildContent(view.configuration.label, in: context) 32 | return SUIButtonNode(elementID : context.currentElementID, 33 | isEnabled : context.environment.isEnabled, 34 | isActive : false, 35 | action : view.configuration.action, 36 | content : childTree) 37 | } 38 | 39 | } 40 | 41 | extension SUIButtonStyle.Body: TreeBuildingView { 42 | 43 | func buildTree(in context: TreeStateContext) -> HTMLTreeNode { 44 | return context.currentBuilder.buildTree(for: self, in: context) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Modifiers/PaddingModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Padding.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 10.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public extension View { 10 | 11 | func padding(_ insets: EdgeInsets) -> ModifiedContent { 12 | return modifier(PaddingLayout(value: .insets(insets))) 13 | } 14 | func padding(_ edges: Edge.Set = .all, _ length: Length? = nil) 15 | -> ModifiedContent 16 | { 17 | return modifier(PaddingLayout(value: .edges(edges, length))) 18 | } 19 | func padding(_ length: Length) -> ModifiedContent { 20 | return modifier(PaddingLayout(value: .insets(EdgeInsets(length)))) 21 | } 22 | } 23 | 24 | public struct PaddingLayout: ViewModifier { 25 | 26 | public typealias Content = Value 27 | 28 | public enum Value: Equatable { 29 | case insets(EdgeInsets) 30 | case edges(Edge.Set, Length?) 31 | } 32 | 33 | let value : Value 34 | 35 | public func buildTree(for view: T, in context: TreeStateContext) 36 | -> HTMLTreeNode 37 | { 38 | let child = context.currentBuilder._buildInLayoutContext(view, in: context) 39 | return HTMLPaddingNode(elementID: context.currentElementID, 40 | value: value, 41 | localLayoutInfo: context.localLayoutInfo, 42 | content: child) 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Views/TabbedView/SUITabView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SUITabView.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 18.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public typealias TabView = SUITabView 10 | 11 | public struct SUITabView: View { 12 | // TODO: this could have two modes, one which re-renders on switch and one 13 | // which always pregenerates all tab content. 14 | // plus: one which loads on demand ;-) 15 | 16 | let selection : Binding 17 | let content : Content 18 | 19 | public init(selection: Binding, 20 | @ViewBuilder content: () -> Content) 21 | { 22 | self.selection = selection 23 | self.content = content() 24 | } 25 | 26 | public var body: some View { return content } 27 | } 28 | 29 | extension TabView where SelectionValue == Int { 30 | 31 | public init(@ViewBuilder content: () -> Content) { 32 | // The default if no selection binding is provided 33 | var selectionHolder = 0 34 | self.selection = Binding(getValue: { return selectionHolder}, 35 | setValue: { selectionHolder = $0 }) 36 | self.content = content() 37 | } 38 | 39 | } 40 | 41 | extension SUITabView: TreeBuildingView { 42 | func buildTree(in context: TreeStateContext) -> HTMLTreeNode { 43 | return context.currentBuilder.buildTree(for: self, in: context) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Views/Generic/TapAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TapAction.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 23.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public extension View { 10 | 11 | func onTapGesture(count: Int = 1, _ action: @escaping () -> Void) 12 | -> TapActionView 13 | { 14 | assert(count < 2, "only supporting single-click") 15 | return TapActionView(count: count, action: action, content: self) 16 | } 17 | 18 | } 19 | 20 | public struct TapActionView: View { 21 | public typealias Body = Never 22 | let count : Int 23 | let action : () -> Void 24 | let content : Content 25 | } 26 | 27 | extension HTMLTreeBuilder { 28 | 29 | func buildTree(for view: TapActionView, 30 | in context: TreeStateContext) -> HTMLTreeNode 31 | { 32 | let childTree = _buildContent(view.content, in: context) 33 | return HTMLClickContainerNode(elementID: context.currentElementID, 34 | isEnabled: context.environment.isEnabled, 35 | isDouble: view.count == 2, 36 | action: view.action, content: childTree) 37 | } 38 | 39 | } 40 | 41 | extension TapActionView: TreeBuildingView { 42 | 43 | func buildTree(in context: TreeStateContext) -> HTMLTreeNode { 44 | return context.currentBuilder.buildTree(for: self, in: context) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Values/EdgeInsets.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EdgeInsets.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 09.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public enum Edge: Int8, CaseIterable, Hashable { 10 | 11 | case top, leading, bottom, trailing 12 | 13 | public struct Set: OptionSet, Hashable { 14 | public let rawValue : Int 15 | public init(rawValue: Int) { 16 | self.rawValue = rawValue 17 | } 18 | 19 | public static let bottom = Set(rawValue: 1 << 0) 20 | public static let top = Set(rawValue: 1 << 1) 21 | public static let leading = Set(rawValue: 1 << 2) 22 | public static let trailing = Set(rawValue: 1 << 3) 23 | public static let horizontal = Set(rawValue: 1 << 4) 24 | public static let vertical = Set(rawValue: 1 << 5) 25 | 26 | public static let all = Set(rawValue: 1 << 6) 27 | // TBD: is this all flags, or a special context? 28 | } 29 | } 30 | 31 | public struct EdgeInsets: Hashable { 32 | public var top: Length, leading: Length, bottom: Length, trailing: Length 33 | 34 | public init(_ length: Length) { 35 | top = length; leading = length; bottom = length; trailing = length 36 | } 37 | } 38 | 39 | extension EdgeInsets: CSSStyleValue { 40 | 41 | public var cssStringValue: String { // clockwise in CSS 42 | return "\(top.cssStringValue) \(trailing.cssStringValue) " 43 | + "\(bottom.cssStringValue) \(leading.cssStringValue)" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/WASMHosting/JSONPolyfill.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Carson Katri on 5/28/20. 6 | // 7 | 8 | import JavaScriptKit 9 | 10 | // This is just a wrapper around the JavaScript JSON object that converts the JSObjectRef to a Decodable type. 11 | public class JSONDecoder { 12 | public init() { 13 | 14 | } 15 | 16 | // This should be a minor hurdle if I can leverage the JavaScript JSON type 17 | public func decode(_ type: T.Type, from jsonString: String) throws -> T { 18 | guard let json = JSObjectRef.global.JSON.object?.parse?(jsonString) else { 19 | throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: [], debugDescription: "The given data did not contain a top-level value.")) 20 | } 21 | return try JSValueDecoder().decode(from: json) 22 | } 23 | 24 | public func decode(_ type: T.Type, from data: Data) throws -> T { 25 | guard let json = JSObjectRef.global.JSON.object?.parse?(data.stringValue) else { 26 | throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: [], debugDescription: "The given data did not contain a top-level value.")) 27 | } 28 | do { 29 | return try JSValueDecoder().decode(from: json) 30 | } catch { 31 | throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: [], debugDescription: "The given data did not contain a top-level value.")) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Properties/BindingConvertible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BindingConvertible.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 05.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | @dynamicMemberLookup public protocol BindingConvertible { 10 | // Kinda like objects being able to vend a WOAssociation 11 | // `State` is a BindingConvertible. 12 | 13 | associatedtype Value 14 | 15 | var binding : Binding { get } 16 | 17 | subscript(dynamicMember path: WritableKeyPath) 18 | -> Binding { get } 19 | } 20 | 21 | public extension BindingConvertible { 22 | 23 | subscript(dynamicMember path: WritableKeyPath) 24 | -> Binding 25 | { 26 | return Binding(getValue: { return self.binding.wrappedValue[keyPath: path] }, 27 | setValue: { self.binding.wrappedValue[keyPath: path] = $0 }) 28 | } 29 | } 30 | 31 | public extension BindingConvertible { 32 | 33 | func zip(with rhs: T) 34 | -> Binding< ( Self.Value, T.Value ) > 35 | { 36 | return Binding( 37 | getValue: { return ( self.binding.wrappedValue, rhs.binding.wrappedValue ) }, 38 | setValue: { ( newLHS, newRHS ) in 39 | self.binding.wrappedValue = newLHS 40 | rhs.binding.wrappedValue = newRHS 41 | } 42 | ) 43 | } 44 | 45 | } 46 | 47 | extension Binding : BindingConvertible { 48 | 49 | public var binding : Self { return self } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /Sources/SwiftWebUI/Views/Forms/SUIToggleStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SUIToggleStyle.swift 3 | // SwiftWebUI 4 | // 5 | // Created by Helge Heß on 20.06.19. 6 | // Copyright © 2019 Helge Heß. All rights reserved. 7 | // 8 | 9 | public struct SUIToggleStyle: ToggleStyle { 10 | 11 | public typealias Label = ToggleStyleLabel 12 | public typealias Member = StaticMember 13 | 14 | public func body(configuration: Toggle