├── .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 |
--------------------------------------------------------------------------------