├── .gitignore ├── Tests ├── LinuxMain.swift └── terminal-uiTests │ ├── XCTestManifests.swift │ ├── GeometryTests.swift │ ├── RegressionTests.swift │ ├── VStackTests.swift │ ├── ZStackTests.swift │ ├── TextTests.swift │ └── TestingContexts.swift ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── Sources ├── TerminalUI │ ├── Helpers.swift │ ├── GeometryReader.swift │ ├── Views.swift │ ├── RenderingContext.swift │ ├── Padding.swift │ ├── Border.swift │ ├── Color.swift │ ├── FixedFrame.swift │ ├── Text.swift │ ├── OverlayBackground.swift │ ├── ZStack.swift │ ├── Lowlevel.swift │ ├── FlexibleFrame.swift │ ├── Geometry.swift │ ├── VStack.swift │ ├── HStack.swift │ └── Alignment.swift └── sample │ └── main.swift ├── LICENSE ├── Package.swift └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import terminal_uiTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += terminal_uiTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Tests/terminal-uiTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(terminal_uiTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Tests/terminal-uiTests/GeometryTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import TerminalUI 3 | 4 | final class GeometryTests: XCTestCase { 5 | func testExample() { 6 | let x0 = Rect(origin: .zero, size: Size(width: 10, height: 5)) 7 | let x1 = Rect(origin: .zero, size: Size(width: 20, height: 3)) 8 | XCTAssertEqual(x0.union(x1), Rect(origin: .zero, size: Size(width: 20, height: 5))) 9 | 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Tests/terminal-uiTests/RegressionTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import TerminalUI 3 | 4 | final class RegressionTests: XCTestCase { 5 | func testExample() { 6 | var sample: some BuiltinView { 7 | Text("Hello") 8 | .padding() 9 | .border() 10 | .padding() 11 | .border() 12 | } 13 | let size = sample.size(for: ProposedSize(width: 20, height: 5)) 14 | sample.render(context: TestContext(), size: size) 15 | // this used to crash. 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/TerminalUI/Helpers.swift: -------------------------------------------------------------------------------- 1 | extension Array { 2 | // expectes the array to be sorted by groupId 3 | func group(by groupId: (Element) -> A) -> [[Element]] { 4 | guard !isEmpty else { return [] } 5 | var groups: [[Element]] = [] 6 | var currentGroup: [Element] = [self[0]] 7 | for element in dropFirst() { 8 | if groupId(currentGroup[0]) == groupId(element) { 9 | currentGroup.append(element) 10 | } else { 11 | groups.append(currentGroup) 12 | currentGroup = [element] 13 | } 14 | } 15 | groups.append(currentGroup) 16 | return groups 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/terminal-uiTests/VStackTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Chris Eidhof on 10.05.21. 6 | // 7 | 8 | @testable import TerminalUI 9 | import XCTest 10 | 11 | 12 | 13 | final class VStackTests: XCTestCase { 14 | func testExample() { 15 | let c = TestContext() 16 | let rootView = 17 | VStack(children: [ 18 | Text("Hello"), 19 | Text("World") 20 | ]) 21 | let s = rootView.size(for: ProposedSize(width: 20, height: 5)) 22 | XCTAssertEqual(s, Size(width: 5, height: 2)) 23 | rootView.render(context: c, size: s) 24 | let result = """ 25 | translateBy Point(x: 0, y: 0) 26 | write Hello 27 | translateBy Point(x: 0, y: 1) 28 | write World 29 | 30 | """ 31 | XCTAssertEqual(result, c.log) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/TerminalUI/GeometryReader.swift: -------------------------------------------------------------------------------- 1 | public struct GeometryReader: BuiltinView { 2 | public init(alignment: Alignment = Alignment.topLeading, content: @escaping (Size) -> Content) { 3 | self.alignment = alignment 4 | self.content = content 5 | } 6 | 7 | var alignment = Alignment.topLeading 8 | let content: (Size) -> Content 9 | 10 | public func render(context: RenderingContext, size: Size) { 11 | let child = content(size) 12 | let childSize = child.size(for: ProposedSize(size)) 13 | let parentPoint = alignment.point(for: size) 14 | let childPoint = alignment.point(for: childSize) 15 | var c = context 16 | c.translateBy(Point(x: parentPoint.x-childPoint.x, y: parentPoint.y-childPoint.y)) 17 | child.render(context: c, size: childSize) 18 | } 19 | 20 | public func size(for proposed: ProposedSize) -> Size { 21 | return proposed.orDefault 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Tests/terminal-uiTests/ZStackTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import TerminalUI 3 | 4 | final class ZStackTests: XCTestCase { 5 | var context = TestContext() 6 | 7 | override func setUp() { 8 | context = TestContext() 9 | } 10 | 11 | func testSimple() { 12 | let t = ZStack(children: [Text("Hello")]) 13 | let size = t.size(for: ProposedSize(width: nil, height: nil)) 14 | XCTAssertEqual(size, Size(width: 5, height: 1)) 15 | 16 | } 17 | 18 | func testDouble() { 19 | let t = ZStack(alignment: .center, children: [ 20 | Text("x\n---\nx"), 21 | Text("+") 22 | ]) 23 | let size = t.size(for: ProposedSize(width: nil, height: nil)) 24 | XCTAssertEqual(size, Size(width: 3, height: 3)) 25 | let buffer = ArrayBuffer(size: size) 26 | let c = ArrayContext(buffer: buffer) 27 | t.render(context: c, size: size) 28 | XCTAssertEqual(buffer.string, "x \n-+-\nx ") 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/TerminalUI/Views.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Chris Eidhof on 06.05.21. 6 | // 7 | 8 | public typealias Width = Int 9 | public typealias Height = Int 10 | 11 | public protocol BuiltinView { 12 | func size(for proposed: ProposedSize) -> Size 13 | func render(context: RenderingContext, size: Size) 14 | } 15 | 16 | extension BuiltinView { 17 | public func padding(_ amount: Int = 1) -> some BuiltinView { 18 | Padding(content: self, insets: EdgeInsets(value: amount)) 19 | } 20 | 21 | public func padding(_ insets: EdgeInsets) -> some BuiltinView { 22 | Padding(content: self, insets: insets) 23 | } 24 | 25 | public func border(style: BorderStyle = .default) -> some BuiltinView { 26 | self.padding(1).background(Border(style: style)) 27 | } 28 | 29 | public func frame(width: Int? = nil, height: Int? = nil, alignment: Alignment = .center) -> some BuiltinView { 30 | FixedFrame(width: width, height: height, alignment: alignment, content: self) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/TerminalUI/RenderingContext.swift: -------------------------------------------------------------------------------- 1 | public struct TTYRenderingContext: RenderingContext { 2 | var origin: Point = .zero 3 | public var foregroundColor: Color? = nil 4 | public var backgroundColor: Color? = nil 5 | 6 | mutating public func translateBy(_ point: Point) { 7 | origin.x += point.x 8 | origin.y += point.y 9 | } 10 | 11 | public func write(_ s: S) { 12 | setColor(foregroundColor: foregroundColor, backgroundColor: backgroundColor) 13 | move(to: origin) 14 | _write(String(s)) 15 | } 16 | } 17 | 18 | // Concrete implementations have to have value semantics: it should be possible to make a copy by doing `var c = previousContext` and `c` and `previousContext` should be separate instances. In other words, conforming types need to be a struct. 19 | public protocol RenderingContext { 20 | var foregroundColor: Color? { get set } 21 | var backgroundColor: Color? { get set } 22 | mutating func translateBy(_ point: Point) 23 | func write(_ s: S) 24 | } 25 | -------------------------------------------------------------------------------- /Sources/TerminalUI/Padding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Chris Eidhof on 06.05.21. 6 | // 7 | 8 | struct Padding: BuiltinView { 9 | var content: Content 10 | var insets: EdgeInsets = EdgeInsets(value: 1) 11 | 12 | func childSize(for size: Size) -> Size { 13 | var childProposal = size 14 | childProposal.width -= (insets.leading + insets.trailing) 15 | childProposal.height -= (insets.top + insets.bottom) 16 | return childProposal 17 | } 18 | 19 | func size(for proposed: ProposedSize) -> Size { 20 | var result = content.size(for: ProposedSize(childSize(for: proposed.orDefault))) 21 | result.width += (insets.leading + insets.trailing) 22 | result.height += (insets.top + insets.bottom) 23 | return result 24 | } 25 | 26 | func render(context: RenderingContext, size: Size) { 27 | var c = context 28 | c.translateBy(Point(x: insets.leading, y: insets.top)) 29 | content.render(context: c, size: childSize(for: size)) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 objc.io 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "TerminalUI", 8 | products: [ 9 | // Products define the executables and libraries a package produces, and make them visible to other packages. 10 | .library( 11 | name: "TerminalUI", 12 | targets: ["TerminalUI"]), 13 | ], 14 | dependencies: [ 15 | // Dependencies declare other packages that this package depends on. 16 | // .package(url: /* package url */, from: "1.0.0"), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 20 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 21 | .target( 22 | name: "TerminalUI", 23 | dependencies: [], 24 | path: "Sources/TerminalUI"), 25 | .target(name: "sample", dependencies: ["TerminalUI"]), 26 | .testTarget( 27 | name: "terminal-uiTests", 28 | dependencies: ["TerminalUI"]), 29 | ] 30 | ) 31 | -------------------------------------------------------------------------------- /Sources/sample/main.swift: -------------------------------------------------------------------------------- 1 | import TerminalUI 2 | 3 | var horizontal: some BuiltinView { 4 | HStack(children: [ 5 | Text("Hello") 6 | .padding() 7 | .foregroundColor(.blue) 8 | .border() 9 | .padding() 10 | .backgroundColor(.red), 11 | Text("Testing A Longer Text") 12 | .frame(maxWidth: .max) 13 | .border() 14 | .padding(), 15 | ZStack(alignment: .center, children: [ 16 | Text("Test").padding().border(), 17 | Text("+").backgroundColor(.green) 18 | ]), 19 | Text(["This", "is", "a", "truncation", "test"].joined(separator: "\n")) // todo: fix this crash 20 | ], alignment: .bottom) 21 | .overlay(Text("[x]", color: .red), alignment: .topTrailing) 22 | .overlay(GeometryReader(alignment: .bottom) { size in 23 | Text("\(size.width)⨉\(size.height)") 24 | }) 25 | .border() 26 | } 27 | 28 | func rootView(_ char: Int32?) -> some BuiltinView { 29 | var alignment: HorizontalAlignment = .center 30 | if char == 108 { alignment = .leading } 31 | if char == 114 { alignment = .trailing } 32 | return VStack(alignment: alignment, children: [ 33 | Text("Hello"), 34 | Text("\(char ?? 0)"), 35 | Text("Sunny World") 36 | .border(style: .ascii) 37 | 38 | ]).padding() 39 | .border() 40 | } 41 | 42 | 43 | run(rootView) 44 | -------------------------------------------------------------------------------- /Sources/TerminalUI/Border.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Chris Eidhof on 06.05.21. 6 | // 7 | 8 | public struct BorderStyle { 9 | 10 | public var topLeft = "┌" 11 | public var topRight = "┐" 12 | public var horizontal = "─" 13 | public var vertical = "│" 14 | public var bottomLeft = "└" 15 | public var bottomRight = "┘" 16 | 17 | public static let `default` = BorderStyle() 18 | 19 | public static let ascii = BorderStyle(topLeft: "+", topRight: "+", horizontal: "-", vertical: "|", bottomLeft: "+", bottomRight: "+") 20 | } 21 | 22 | struct Border: BuiltinView { 23 | var style = BorderStyle() 24 | let width = 1 25 | 26 | func size(for proposed: ProposedSize) -> Size { 27 | return proposed.orDefault 28 | } 29 | 30 | func render(context: RenderingContext, size: Size) { 31 | guard size.width > 1, size.height > 1 else { return } 32 | let topLine = style.topLeft + String(repeating: style.horizontal, count: size.width-2) + style.topRight 33 | context.write(topLine) 34 | var c = context 35 | let vertical = style.vertical + String(repeating: " ", count: size.width-2) + style.vertical 36 | if size.height > 2 { 37 | for _ in 1...size.height-2 { 38 | c.translateBy(.init(x: 0, y: 1)) 39 | c.write(vertical) 40 | } 41 | } 42 | c.translateBy(.init(x: 0, y: 1)) 43 | let bottomLine = style.bottomLeft + String(repeating: style.horizontal, count: size.width-2) + style.bottomRight 44 | c.write(bottomLine) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/TerminalUI/Color.swift: -------------------------------------------------------------------------------- 1 | // See https://en.wikipedia.org/wiki/ANSI_escape_code 2 | 3 | public enum Color: Int, RawRepresentable, CaseIterable { 4 | case black = 30 5 | case red = 31 6 | case green = 32 7 | case yellow = 33 8 | case blue = 34 9 | case magenta = 35 10 | case cyan = 36 11 | case white = 37 12 | case brightBlack = 90 13 | case brightRed = 91 14 | case brightGreen = 92 15 | case brightYellow = 93 16 | case brightBlue = 94 17 | case brightMagenta = 95 18 | case brightCyan = 96 19 | case brightWhite = 97 20 | } 21 | 22 | extension Color { 23 | var foreground: Int { 24 | rawValue 25 | } 26 | 27 | var background: Int { 28 | rawValue + 10 29 | } 30 | } 31 | 32 | struct ModifyContext: BuiltinView { 33 | var content: Content 34 | var modify: (inout RenderingContext) -> () 35 | 36 | func size(for proposed: ProposedSize) -> Size { 37 | content.size(for: proposed) 38 | } 39 | 40 | func render(context: RenderingContext, size: Size) { 41 | var c = context 42 | modify(&c) 43 | content.render(context: c, size: size) 44 | } 45 | 46 | } 47 | 48 | extension BuiltinView { 49 | public func foregroundColor(_ color: Color? = nil) -> some BuiltinView { 50 | ModifyContext(content: self, modify: { c in 51 | c.foregroundColor = color 52 | }) 53 | } 54 | 55 | public func backgroundColor(_ color: Color? = nil) -> some BuiltinView { 56 | ModifyContext(content: self, modify: { c in 57 | c.backgroundColor = color 58 | }) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/TerminalUI/FixedFrame.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Chris Eidhof on 06.05.21. 6 | // 7 | 8 | struct FixedFrame: BuiltinView { 9 | var width: Width? 10 | var height: Width? 11 | var alignment: Alignment 12 | var content: Content 13 | 14 | func size(for proposed: ProposedSize) -> Size { 15 | let childSize = content.size(for: ProposedSize(width: width ?? proposed.width, height: height ?? proposed.height)) 16 | return Size(width: width ?? childSize.width, height: height ?? childSize.height) 17 | } 18 | 19 | func render(context: RenderingContext, size: Size) { 20 | let childSize = content.size(for: ProposedSize(size)) 21 | let t = translation(for: content, in: size, childSize: childSize, alignment: alignment) 22 | var c = context 23 | c.translateBy(t) 24 | content.render(context: c, size: childSize) 25 | } 26 | } 27 | 28 | extension BuiltinView { 29 | func translation(for childView: BuiltinView, in parentSize: Size, childSize: Size, alignment: Alignment) -> Point { 30 | let parentPoint = alignment.point(for: parentSize) 31 | let childPoint = alignment.point(for: childSize) 32 | return Point(x: parentPoint.x-childPoint.x, y: parentPoint.y-childPoint.y) 33 | } 34 | 35 | func translation(for sibling: V, in size: Size, siblingSize: Size, alignment: Alignment) -> Point { 36 | let selfPoint = alignment.point(for: size) 37 | let childPoint = alignment.point(for: siblingSize) 38 | return Point(x: selfPoint.x-childPoint.x, y: selfPoint.y-childPoint.y) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Tests/terminal-uiTests/TextTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import TerminalUI 3 | 4 | final class TextTests: XCTestCase { 5 | var context = TestContext() 6 | 7 | override func setUp() { 8 | context = TestContext() 9 | } 10 | 11 | func testExample() { 12 | let t = Text("Hello") 13 | let size = t.size(for: ProposedSize(width: nil, height: nil)) 14 | XCTAssertEqual(size, Size(width: 5, height: 1)) 15 | 16 | } 17 | 18 | func testTruncation() { 19 | let t = Text("Hello World") 20 | let size = t.size(for: ProposedSize(width: 5, height: 1)) 21 | XCTAssertEqual(size, Size(width: 5, height: 1)) 22 | t.render(context: context, size: size) 23 | XCTAssertEqual(context.log, "write Hell…\n") 24 | } 25 | 26 | func testTruncation2() { 27 | let t = Text("Hello") 28 | let size = t.size(for: ProposedSize(width: 5, height: 1)) 29 | t.render(context: context, size: size) 30 | XCTAssertEqual(context.log, "write Hello\n") 31 | } 32 | 33 | func testLineTruncation2() { 34 | let t = Text("Hello\nWorld") 35 | let size = t.size(for: ProposedSize(width: 5, height: 1)) 36 | t.render(context: context, size: size) 37 | XCTAssertEqual(context.log, "write Hell…\n") 38 | } 39 | 40 | func testLineTruncation3() { 41 | let t = Text("This is a truncation test".split(separator: " ").joined(separator: "\n")) 42 | let size = t.size(for: ProposedSize(width: 20, height: 1)) 43 | t.render(context: context, size: size) 44 | XCTAssertEqual(context.log, "write This…\n") 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/terminal-uiTests/TestingContexts.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Chris Eidhof on 10.05.21. 6 | // 7 | 8 | import TerminalUI 9 | 10 | class TestContext: RenderingContext { 11 | var log: String = "" 12 | 13 | var foregroundColor: Color? { 14 | didSet { 15 | print("Set foreground color \(String(describing: foregroundColor))", to: &log) 16 | } 17 | } 18 | 19 | var backgroundColor: Color? { 20 | didSet { 21 | print("Set background color \(String(describing: foregroundColor))", to: &log) 22 | } 23 | } 24 | 25 | func translateBy(_ point: Point) { 26 | print("translateBy \(point)", to: &log) 27 | 28 | } 29 | 30 | func write(_ s: S) where S : StringProtocol { 31 | print("write \(String(s))", to: &log) 32 | } 33 | 34 | 35 | } 36 | 37 | final class ArrayBuffer { 38 | var result: [[Character]] 39 | 40 | var string: String { 41 | result.map { String($0) }.joined(separator: "\n") 42 | } 43 | 44 | init(size: Size) { 45 | self.result = Array(repeating: Array(repeating: " ", count: size.width), count: size.height) 46 | } 47 | } 48 | 49 | struct ArrayContext: RenderingContext { 50 | var buffer: ArrayBuffer 51 | var point = Point.zero 52 | 53 | init(buffer: ArrayBuffer) { 54 | self.buffer = buffer 55 | } 56 | var foregroundColor: Color? 57 | var backgroundColor: Color? 58 | 59 | mutating func translateBy(_ point: Point) { 60 | self.point.x += point.x 61 | self.point.y += point.y 62 | } 63 | 64 | func write(_ s: S) where S : StringProtocol { 65 | assert(!s.contains(where: { $0.isNewline })) 66 | buffer.result[point.y][point.x.. Size { 13 | // todo actually fit ourselves? 14 | let lines = self.lines 15 | let width = lines.map { $0.count }.max() ?? 0 16 | let height = lines.count 17 | return Size(width: min(proposed.width ?? .max, width), height: min(proposed.height ?? .max, height)) 18 | } 19 | 20 | public func render(context: RenderingContext, size: Size) { 21 | let l = lines 22 | guard !l.isEmpty, size.height > 0 else { return } 23 | var c = context 24 | for i in 0.. 0 { 26 | c.translateBy(.init(x: 0, y: 1)) 27 | } 28 | let line = lines[i] 29 | let lastLineBeforeVerticalTruncation = i + 1 == size.height && i + 1 < l.endIndex 30 | let truncated = line.truncate(to: size.width) 31 | if lastLineBeforeVerticalTruncation { 32 | if truncated.count >= size.width { 33 | c.write(truncated.dropLast() + "…") 34 | } else { 35 | c.write(truncated + "…") 36 | } 37 | } else { 38 | c.write(truncated) 39 | } 40 | } 41 | } 42 | } 43 | 44 | extension StringProtocol { 45 | func truncate(to: Width) -> String { 46 | guard to > 0 else { return "" } 47 | if count <= to { return String(self) } 48 | return self.prefix(to-1) + "…" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/TerminalUI/OverlayBackground.swift: -------------------------------------------------------------------------------- 1 | 2 | struct Overlay: BuiltinView { 3 | let content: Content 4 | let overlay: O 5 | let alignment: Alignment 6 | 7 | func render(context: RenderingContext, size: Size) { 8 | content.render(context: context, size: size) 9 | let childSize = overlay.size(for: ProposedSize(size)) 10 | var c = context 11 | let t = content.translation(for: overlay, in: size, siblingSize: childSize, alignment: alignment) 12 | c.translateBy(t) 13 | overlay.render(context: c, size: childSize) 14 | } 15 | 16 | func size(for proposed: ProposedSize) -> Size { 17 | content.size(for: proposed) 18 | } 19 | } 20 | 21 | struct Background: BuiltinView { 22 | let content: Content 23 | let background: O 24 | let alignment: Alignment 25 | 26 | func render(context: RenderingContext, size: Size) { 27 | let childSize = background.size(for: ProposedSize(size)) 28 | var c = context 29 | let t = content.translation(for: background, in: size, siblingSize: childSize, alignment: alignment) 30 | c.translateBy(t) 31 | 32 | background.render(context: c, size: childSize) 33 | content.render(context: context, size: size) 34 | } 35 | 36 | func size(for proposed: ProposedSize) -> Size { 37 | content.size(for: proposed) 38 | } 39 | } 40 | 41 | extension BuiltinView { 42 | public func overlay(_ other: O, alignment: Alignment = .center) -> some BuiltinView { 43 | Overlay(content: self, overlay: other, alignment: alignment) 44 | } 45 | 46 | public func background(_ other: O, alignment: Alignment = .center) -> some BuiltinView { 47 | Background(content: self, background: other, alignment: alignment) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/TerminalUI/ZStack.swift: -------------------------------------------------------------------------------- 1 | public struct ZStack: BuiltinView { 2 | public init(alignment: Alignment = .center, children: [BuiltinView]) { 3 | self.alignment = alignment 4 | self.children = children 5 | } 6 | 7 | var alignment: Alignment = .center 8 | var children: [BuiltinView] 9 | @LayoutState var rects: [Rect] = [] 10 | 11 | public func render(context: RenderingContext, size: Size) { 12 | assert(rects.count == children.count) 13 | let selfPoint = alignment.point(for: size) 14 | let actualRects: [Rect] = rects.map { 15 | var copy = $0 16 | copy.origin.x += selfPoint.x 17 | copy.origin.y += selfPoint.y 18 | return copy 19 | } 20 | let minX = actualRects.map { $0.minX }.min() ?? 0 21 | let maxY = actualRects.map { $0.maxY }.max() ?? size.height 22 | var c = context 23 | c.translateBy(Point(x: -minX, y: size.height-maxY)) 24 | for i in children.indices { 25 | let child = children[i] 26 | var childC = c 27 | childC.translateBy(Point(x: actualRects[i].origin.x, y: actualRects[i].origin.y)) 28 | child.render(context: childC, size: rects[i].size) 29 | } 30 | } 31 | 32 | 33 | public func size(for proposed: ProposedSize) -> Size { 34 | layout(proposed: proposed) 35 | guard let f = rects.first else { return .zero } 36 | let union = rects.dropFirst().reduce(f, { $0.union($1) }) 37 | let size = union.size 38 | layout(proposed: ProposedSize(size)) 39 | return size 40 | } 41 | 42 | func layout(proposed: ProposedSize) { 43 | rects = Array(repeating: .zero, count: children.count) 44 | for i in children.indices { 45 | let child = children[i] 46 | let childSize = child.size(for: proposed) 47 | let point = alignment.point(for: childSize) 48 | rects[i] = Rect(origin: Point(x: -point.x, y: -point.y), size: childSize) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # terminal-ui 2 | 3 | A way to build [TUI](https://en.wikipedia.org/wiki/Text-based_user_interface) apps with a layout system and API that's similar to SwiftUI. 4 | 5 | This was just a proof of concept. [SwiftTUI](https://github.com/rensbreur/SwiftTUI) looks a lot more complete and supported. 6 | 7 | We reimplemented parts of the SwiftUI layout system in the Swift Talk series [SwiftUI Layout Explained](https://talk.objc.io/collections/swiftui-layout-explained). This tries to stay as close to that as possible. 8 | 9 | A specific goal is to keep the layout behavior as close to SwiftUI as possible. So most SwiftUI programs should "just work" here as well. 10 | 11 | Implementation Status 12 | 13 | - Views (in random order) 14 | - [x] Alignment 15 | - [x] GeometryReader 16 | - [x] Border 17 | - [x] Color (so far we only support 16 colors) 18 | - [x] FixedFrame 19 | - [x] FlexibleFrame 20 | - [x] HStack 21 | - [x] Overlay 22 | - [x] Background 23 | - [x] ZStack 24 | - [x] Text 25 | - [x] Padding (In progress) 26 | - [ ] Add tests 27 | - [x] VStack 28 | - [ ] Progress 29 | - [ ] ScrollView 30 | - [ ] List 31 | - [ ] Button 32 | - [ ] Switch 33 | - [ ] Custom Alignment Guides 34 | - [ ] Layout Priorities 35 | - More ideas (lower priority) 36 | - [ ] Tree View (similar to outlines) 37 | - [ ] Menus 38 | - [ ] Navigation? (Not sure if this is a good idea, but could be fun to try) 39 | - State/Lifecycle/Interactivity 40 | - [ ] Custom `View` structures (I think we can build this on top of `BuiltinView`) 41 | - [ ] Environment (should be easy) 42 | - [ ] Preferences 43 | - [ ] State/Binding/ObservedObject 44 | - [ ] Focus (we should have some way for a control to be in focus, and ) 45 | - [ ] Interaction (nothing is interactive yet) 46 | - [ ] Animations 47 | - [ ] ... 48 | 49 | Similar Projects: 50 | 51 | - https://github.com/migueldeicaza/TermKit 52 | - https://github.com/migueldeicaza/gui.cs 53 | 54 | Inspiration: 55 | 56 | - https://github.com/aristocratos/bashtop 57 | - https://github.com/rothgar/awesome-tuis 58 | - ... 59 | -------------------------------------------------------------------------------- /Sources/TerminalUI/Lowlevel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Chris Eidhof on 06.05.21. 6 | // 7 | 8 | import Darwin 9 | 10 | // Taken from https://stackoverflow.com/questions/49748507/listening-to-stdin-in-swift 11 | func enableRawMode(file: Int32) -> termios { 12 | var raw: termios = .init() 13 | tcgetattr(file, &raw) 14 | let original = raw 15 | raw.c_lflag &= ~(UInt(ECHO | ICANON)) 16 | tcsetattr(file, TCSAFLUSH, &raw); 17 | return original 18 | } 19 | 20 | 21 | func clearScreen() { 22 | _write("\u{1b}[2J") 23 | } 24 | 25 | func move(to: Point) { 26 | _write("\u{1b}[\(to.y+1);\(to.x+1)H") 27 | } 28 | 29 | 30 | func setColor(foregroundColor: Color?, backgroundColor: Color?) { 31 | _write("\u{1b}[0m") // todo this is unnecessarily expensive 32 | if let f = foregroundColor { 33 | _write("\u{1b}[\(f.foreground)m") 34 | } else { 35 | } 36 | if let b = backgroundColor { 37 | _write("\u{1b}[\(b.background)m") 38 | } 39 | } 40 | 41 | func _write(_ str: String, fd: Int32 = STDOUT_FILENO) { 42 | _ = str.withCString { str in 43 | write(fd, str, strlen(str)) 44 | } 45 | } 46 | 47 | func winsz_handler(sig: Int32) { 48 | render(char: nil) 49 | } 50 | 51 | var current: ((Int32?) -> BuiltinView)? = nil 52 | 53 | public func render(char: Int32?) { 54 | var w: winsize = .init() 55 | _ = ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) 56 | let size: Size = Size(width: Int(w.ws_col), height: Int(w.ws_row)) 57 | clearScreen() 58 | move(to: Point(x: 1, y: 1)) 59 | let implicit = current!(char) 60 | let s = implicit.size(for: ProposedSize(size)) 61 | implicit.render(context: TTYRenderingContext(), size: s) 62 | 63 | } 64 | 65 | public func run(_ rootView: @escaping (Int32?) -> V) { 66 | current = { c in rootView(c).frame(maxWidth: .max, maxHeight: .max) } 67 | _ = enableRawMode(file: STDOUT_FILENO) 68 | render(char: nil) 69 | signal(SIGWINCH, winsz_handler) 70 | _ = enableRawMode(file: STDIN_FILENO) 71 | var c = getchar() 72 | while true { 73 | render(char: c) 74 | c = getchar() 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/TerminalUI/FlexibleFrame.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Chris Eidhof on 06.05.21. 6 | // 7 | 8 | struct FlexibleFrame: BuiltinView { 9 | var minWidth: Width? 10 | var idealWidth: Width? 11 | var maxWidth: Width? 12 | var minHeight: Height? 13 | var idealHeight: Height? 14 | var maxHeight: Height? 15 | var alignment: Alignment 16 | var content: Content 17 | 18 | func size(for p: ProposedSize) -> Size { 19 | var proposed = ProposedSize(width: p.width ?? idealWidth, height: p.height ?? idealHeight).orDefault 20 | if let min = minWidth, min > proposed.width { 21 | proposed.width = min 22 | } 23 | if let max = maxWidth, max < proposed.width { 24 | proposed.width = max 25 | } 26 | if let min = minHeight, min > proposed.height { 27 | proposed.height = min 28 | } 29 | if let max = maxHeight, max < proposed.height { 30 | proposed.height = max 31 | } 32 | var result = content.size(for: ProposedSize(proposed)) 33 | if let m = minWidth { 34 | result.width = max(m, min(result.width, proposed.width)) 35 | } 36 | if let m = maxWidth { 37 | result.width = min(m, max(result.width, proposed.width)) 38 | } 39 | if let m = minHeight { 40 | result.height = max(m, min(result.height, proposed.height)) 41 | } 42 | if let m = maxHeight { 43 | result.height = min(m, max(result.height, proposed.height)) 44 | } 45 | return result 46 | } 47 | 48 | func render(context: RenderingContext, size: Size) { 49 | var c = context 50 | let childSize = content.size(for: ProposedSize(size)) 51 | let t = translation(for: content, in: size, childSize: childSize, alignment: alignment) 52 | c.translateBy(t) 53 | content.render(context: c, size: childSize) 54 | } 55 | } 56 | 57 | extension BuiltinView { 58 | public func frame(minWidth: Width? = nil, idealWidth: Width? = nil, maxWidth: Width? = nil, minHeight: Height? = nil, idealHeight: Height? = nil, maxHeight: Height? = nil, alignment: Alignment = .center) -> some BuiltinView { 59 | FlexibleFrame(minWidth: minWidth, idealWidth: idealWidth, maxWidth: maxWidth, minHeight: minHeight, idealHeight: idealHeight, maxHeight: maxHeight, alignment: alignment, content: self) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/TerminalUI/Geometry.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Chris Eidhof on 07.05.21. 6 | // 7 | 8 | public struct ProposedSize: Equatable { 9 | public var width: Width? 10 | public var height: Height? 11 | } 12 | 13 | extension ProposedSize { 14 | var orDefault: Size { 15 | return Size(width: width ?? 1, height: height ?? 1) 16 | } 17 | } 18 | 19 | public struct Size: Equatable { 20 | public var width: Width 21 | public var height: Height 22 | 23 | public static let zero = Size(width: 0, height: 0) 24 | } 25 | 26 | public struct Point: Equatable { 27 | public var x: Int 28 | public var y: Int 29 | 30 | static public let zero = Point(x: 0, y: 0) 31 | } 32 | 33 | public struct Rect: Equatable { 34 | public var origin: Point 35 | public var size: Size 36 | 37 | static public let zero = Rect(origin: .zero, size: .zero) 38 | } 39 | 40 | extension ProposedSize { 41 | public init(_ size: Size) { 42 | self.width = size.width 43 | self.height = size.height 44 | } 45 | } 46 | 47 | public struct EdgeInsets: Equatable { 48 | public init(leading: Width, trailing: Width, top: Height, bottom: Height) { 49 | self.leading = leading 50 | self.trailing = trailing 51 | self.top = top 52 | self.bottom = bottom 53 | } 54 | 55 | public init(value: Width) { 56 | self.leading = value 57 | self.trailing = value 58 | self.top = value 59 | self.bottom = value 60 | } 61 | 62 | public var leading: Width 63 | public var trailing: Width 64 | public var top: Height 65 | public var bottom: Height 66 | } 67 | 68 | extension Rect { 69 | public var minX: Int { 70 | size.width > 0 ? origin.x : origin.x - size.width 71 | } 72 | 73 | public var minY: Int { 74 | size.height > 0 ? origin.y : origin.y - size.height 75 | } 76 | 77 | public var maxX: Int { 78 | origin.x + size.width 79 | } 80 | 81 | public var maxY: Int { 82 | origin.y + size.height 83 | } 84 | 85 | public var standardized: Rect { 86 | self // todo 87 | } 88 | 89 | public func union(_ other: Rect) -> Rect { 90 | let s = standardized 91 | let o = other.standardized 92 | let minX = min(s.minX, o.minX) 93 | let maxX = max(s.maxX, o.maxX) 94 | let minY = min(s.minY, o.minY) 95 | let maxY = max(s.maxY, o.maxY) 96 | return Rect(origin: Point(x: minX, y: minY), size: Size(width: maxX-minX, height: maxY-minY)) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Sources/TerminalUI/VStack.swift: -------------------------------------------------------------------------------- 1 | // TODO: this is duplicated between HStack and here, except for the axis. We should abstract away that code? 2 | 3 | public struct VStack: BuiltinView { 4 | var children: [BuiltinView] 5 | var alignment: HorizontalAlignment = .center 6 | let spacing: Height? = 0 7 | @LayoutState var sizes: [Size] = [] 8 | 9 | public init(alignment: HorizontalAlignment = .center, children: [BuiltinView]) { 10 | self.children = children 11 | self.alignment = alignment 12 | } 13 | 14 | 15 | public func render(context: RenderingContext, size: Size) { 16 | let stackX = alignment.alignmentID.defaultValue(in: size) 17 | var currentY: Height = 0 18 | for idx in children.indices { 19 | let child = children[idx] 20 | let childSize = sizes[idx] 21 | let childX = alignment.alignmentID.defaultValue(in: childSize) 22 | var c = context 23 | c.translateBy(Point(x: stackX-childX, y: currentY)) 24 | child.render(context: c, size: childSize) 25 | currentY += childSize.height 26 | } 27 | } 28 | 29 | public func size(for proposed: ProposedSize) -> Size { 30 | layout(proposed: proposed) 31 | let width: Width = sizes.reduce(0) { max($0, $1.width) } 32 | let height: Height = sizes.reduce(0) { $0 + $1.height } 33 | return Size(width: width, height: height) 34 | } 35 | 36 | func layout(proposed: ProposedSize) { 37 | let flexibility: [LayoutInfo] = children.indices.map { idx in 38 | let child = children[idx] 39 | let lower = child.size(for: ProposedSize(width: proposed.width, height: 0)).height 40 | let upper = child.size(for: ProposedSize(width: proposed.width, height: .max)).height 41 | return LayoutInfo(min: lower, max: upper, idx: idx, priority: 1) 42 | }.sorted() 43 | var groups = flexibility.group(by: \.priority) 44 | var sizes: [Size] = Array(repeating: .zero, count: children.count) 45 | let allMinHeights = flexibility.map(\.min).reduce(0,+) 46 | var remainingHeight = proposed.height! - allMinHeights // TODO force unwrap 47 | 48 | while !groups.isEmpty { 49 | let group = groups.removeFirst() 50 | remainingHeight += group.map(\.min).reduce(0,+) 51 | 52 | var remainingIndices = group.map { $0.idx } 53 | while !remainingIndices.isEmpty { 54 | let height = remainingHeight / remainingIndices.count 55 | let idx = remainingIndices.removeFirst() 56 | let child = children[idx] 57 | let size = child.size(for: ProposedSize(width: proposed.width, height: height)) 58 | sizes[idx] = size 59 | remainingHeight -= size.height 60 | if remainingHeight < 0 { remainingHeight = 0 } 61 | } 62 | } 63 | self.sizes = sizes 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Sources/TerminalUI/HStack.swift: -------------------------------------------------------------------------------- 1 | @propertyWrapper 2 | final class LayoutState { 3 | var wrappedValue: A 4 | init(wrappedValue: A) { 5 | self.wrappedValue = wrappedValue 6 | } 7 | } 8 | 9 | struct LayoutInfo: Comparable { 10 | var min: Width 11 | var max: Height 12 | var idx: Int 13 | var priority: Double 14 | 15 | static func <(_ l: LayoutInfo, _ r: LayoutInfo) -> Bool { 16 | if l.priority > r.priority { return true } 17 | if r.priority > l.priority { return false } 18 | return l.flexibility < r.flexibility 19 | } 20 | 21 | var flexibility: Int { 22 | max - min 23 | } 24 | } 25 | 26 | // TODO: this is duplicated between HStack and here, except for the axis. We should abstract away that code? 27 | 28 | public struct HStack: BuiltinView { 29 | var children: [BuiltinView] 30 | var alignment: VerticalAlignment = .center 31 | let spacing: Width? = 0 32 | @LayoutState var sizes: [Size] = [] 33 | 34 | public init(children: [BuiltinView], alignment: VerticalAlignment = .center) { 35 | self.children = children 36 | self.alignment = alignment 37 | } 38 | 39 | 40 | public func render(context: RenderingContext, size: Size) { 41 | let stackY = alignment.alignmentID.defaultValue(in: size) 42 | var currentX: Width = 0 43 | for idx in children.indices { 44 | let child = children[idx] 45 | let childSize = sizes[idx] 46 | let childY = alignment.alignmentID.defaultValue(in: childSize) 47 | var c = context 48 | c.translateBy(Point(x: currentX, y: stackY-childY)) 49 | child.render(context: c, size: childSize) 50 | currentX += childSize.width 51 | } 52 | } 53 | 54 | public func size(for proposed: ProposedSize) -> Size { 55 | layout(proposed: proposed) 56 | let width: Width = sizes.reduce(0) { $0 + $1.width } 57 | let height: Height = sizes.reduce(0) { max($0, $1.height) } 58 | return Size(width: width, height: height) 59 | } 60 | 61 | func layout(proposed: ProposedSize) { 62 | let flexibility: [LayoutInfo] = children.indices.map { idx in 63 | let child = children[idx] 64 | let lower = child.size(for: ProposedSize(width: 0, height: proposed.height)).width 65 | let upper = child.size(for: ProposedSize(width: .max, height: proposed.height)).width 66 | return LayoutInfo(min: lower, max: upper, idx: idx, priority: 1) 67 | }.sorted() 68 | var groups = flexibility.group(by: \.priority) 69 | var sizes: [Size] = Array(repeating: .zero, count: children.count) 70 | let allMinWidths = flexibility.map(\.min).reduce(0,+) 71 | var remainingWidth = proposed.width! - allMinWidths // TODO force unwrap 72 | 73 | while !groups.isEmpty { 74 | let group = groups.removeFirst() 75 | remainingWidth += group.map(\.min).reduce(0,+) 76 | 77 | var remainingIndices = group.map { $0.idx } 78 | while !remainingIndices.isEmpty { 79 | let width = remainingWidth / remainingIndices.count 80 | let idx = remainingIndices.removeFirst() 81 | let child = children[idx] 82 | let size = child.size(for: ProposedSize(width: width, height: proposed.height)) 83 | sizes[idx] = size 84 | remainingWidth -= size.width 85 | if remainingWidth < 0 { remainingWidth = 0 } 86 | } 87 | } 88 | self.sizes = sizes 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/TerminalUI/Alignment.swift: -------------------------------------------------------------------------------- 1 | public struct Alignment { 2 | public var horizontal: HorizontalAlignment 3 | public var vertical: VerticalAlignment 4 | 5 | public static let center = Self(horizontal: .center, vertical: .center) 6 | public static let leading = Self(horizontal: .leading, vertical: .center) 7 | public static let trailing = Self(horizontal: .trailing, vertical: .center) 8 | public static let top = Self(horizontal: .center , vertical: .top) 9 | public static let topLeading = Self(horizontal: .leading, vertical: .top) 10 | public static let topTrailing = Self(horizontal: .trailing, vertical: .top) 11 | public static let bottom = Self(horizontal: .center , vertical: .bottom) 12 | public static let bottomLeading = Self(horizontal: .leading, vertical: .bottom) 13 | public static let bottomTrailing = Self(horizontal: .trailing, vertical: .bottom) 14 | } 15 | 16 | public struct HorizontalAlignment { 17 | public var alignmentID: AlignmentID.Type 18 | public var builtin: Bool 19 | public static let leading = Self(alignmentID: HLeading.self, builtin: true) 20 | public static let center = Self(alignmentID: HCenter.self, builtin: true) 21 | public static let trailing = Self(alignmentID: HTrailing.self, builtin: true) 22 | } 23 | 24 | // We don't allow custom alignments for now 25 | extension HorizontalAlignment { 26 | fileprivate init(alignmentID: AlignmentID.Type) { 27 | self.init(alignmentID: alignmentID, builtin: false) 28 | } 29 | } 30 | 31 | extension VerticalAlignment { 32 | fileprivate init(alignmentID: AlignmentID.Type) { 33 | self.init(alignmentID: alignmentID, builtin: false) 34 | } 35 | } 36 | 37 | public struct VerticalAlignment { 38 | public var alignmentID: AlignmentID.Type 39 | public var builtin: Bool 40 | 41 | public static let top = Self(alignmentID: VTop.self, builtin: true) 42 | public static let center = Self(alignmentID: VCenter.self, builtin: true) 43 | public static let bottom = Self(alignmentID: VBottom.self, builtin: true) 44 | } 45 | 46 | public protocol AlignmentID { 47 | static func defaultValue(in context: Size) -> Int 48 | } 49 | 50 | enum VTop: AlignmentID { 51 | static func defaultValue(in context: Size) -> Int { 52 | 0 53 | } 54 | } 55 | 56 | enum VCenter: AlignmentID { 57 | static func defaultValue(in context: Size) -> Int { context.height/2 } 58 | } 59 | 60 | enum VBottom: AlignmentID { 61 | static func defaultValue(in context: Size) -> Int { 62 | context.height 63 | } 64 | } 65 | 66 | enum HLeading: AlignmentID { 67 | static func defaultValue(in context: Size) -> Int { 0 } 68 | } 69 | 70 | enum HCenter: AlignmentID { 71 | static func defaultValue(in context: Size) -> Int { context.width/2 } 72 | } 73 | 74 | enum HTrailing: AlignmentID { 75 | static func defaultValue(in context: Size) -> Int { context.width } 76 | } 77 | 78 | extension Alignment { 79 | public func point(for size: Size) -> Point { 80 | let x = horizontal.alignmentID.defaultValue(in: size) 81 | let y = vertical.alignmentID.defaultValue(in: size) 82 | return Point(x: x, y: y) 83 | } 84 | } 85 | 86 | //struct CustomHAlignmentGuide: BuiltinView, BuiltinView { 87 | // var content: Content 88 | // var alignment: HorizontalAlignment 89 | // var computeValue: (Size) -> Width 90 | // 91 | // var layoutPriority: Double { 92 | // content._layoutPriority 93 | // } 94 | // 95 | // func render(context: RenderingContext, size: Size) { 96 | // content._render(context: context, size: size) 97 | // } 98 | // func size(proposed: ProposedSize) -> Size { 99 | // content._size(proposed: proposed) 100 | // } 101 | // func customAlignment(for alignment: HorizontalAlignment, in size: Size) -> Width? { 102 | // if alignment.alignmentID == self.alignment.alignmentID { 103 | // return computeValue(size) 104 | // } 105 | // return content._customAlignment(for: alignment, in: size) 106 | // } 107 | // var swiftUI: some View { 108 | // content.swiftUI.alignmentGuide(alignment.swiftUI, computeValue: { 109 | // computeValue(Size(width: $0.width, height: $0.height)) 110 | // }) 111 | // } 112 | //} 113 | // 114 | //extension BuiltinView { 115 | // func alignmentGuide(for alignment: HorizontalAlignment, computeValue: @escaping (Size) -> Width) -> some BuiltinView { 116 | // CustomHAlignmentGuide(content: self, alignment: alignment, computeValue: computeValue) 117 | // } 118 | //} 119 | --------------------------------------------------------------------------------