diff --git a/Sources/TerminalUI/App.swift b/Sources/TerminalUI/App.swift index 5b014a3..1600d6d 100644 --- a/Sources/TerminalUI/App.swift +++ b/Sources/TerminalUI/App.swift @@ -32,7 +32,7 @@ extension App { output.write(AlternativeBuffer.on.control) output.write(CursorVisibility.off.control) - let canvas = Canvas(output) - body.render(in: canvas) + let canvas = AppCanvas(output: output) + body._render(in: canvas) } } diff --git a/Sources/TerminalUI/Environment/EnvironmentModifier.swift b/Sources/TerminalUI/Environment/EnvironmentModifier.swift index 66b5cde..e7d6408 100644 --- a/Sources/TerminalUI/Environment/EnvironmentModifier.swift +++ b/Sources/TerminalUI/Environment/EnvironmentModifier.swift @@ -5,19 +5,19 @@ extension View { _ keyPath: WritableKeyPath, _ value: Value ) -> some View { - modifier(EnvironmentModifier { $0[keyPath: keyPath] = value }) + EnvironmentView(content: self, keyPath: keyPath, value: value) } } -private struct EnvironmentModifier: ViewModifier { +private struct EnvironmentView: Builtin, View { - let modify: (inout EnvironmentValues) -> Void + let content: Content + let keyPath: WritableKeyPath + let value: Value - func body(content: Content) -> some View { - BuiltinView { canvas, environment in - var environment = environment - modify(&environment) - content.render(in: canvas, environment: environment) - } + func render(in canvas: any Canvas, environment: EnvironmentValues) { + var environment = environment + environment[keyPath: keyPath] = value + content._render(in: canvas, environment: environment) } } diff --git a/Sources/TerminalUI/Internal/Builtin.swift b/Sources/TerminalUI/Internal/Builtin.swift new file mode 100644 index 0000000..b8b0250 --- /dev/null +++ b/Sources/TerminalUI/Internal/Builtin.swift @@ -0,0 +1,10 @@ + +protocol Builtin { + func render(in canvas: any Canvas, environment: EnvironmentValues) +} + +extension Builtin { + public var body: Never { + fatalError("Body should never be called.") + } +} diff --git a/Sources/TerminalUI/Internal/BuiltinView.swift b/Sources/TerminalUI/Internal/BuiltinView.swift deleted file mode 100644 index 5546abb..0000000 --- a/Sources/TerminalUI/Internal/BuiltinView.swift +++ /dev/null @@ -1,20 +0,0 @@ - -struct BuiltinView { - - private let render: (Canvas, EnvironmentValues) -> Void - - init(render: @escaping (Canvas, EnvironmentValues) -> Void) { - self.render = render - } - - func render(in canvas: Canvas, environment: EnvironmentValues) { - render(canvas, environment) - } -} - -extension BuiltinView: View { - - var body: Never { - fatalError("BuiltinView body should never be called.") - } -} diff --git a/Sources/TerminalUI/Internal/Canvas.swift b/Sources/TerminalUI/Internal/Canvas.swift deleted file mode 100644 index 36c7b43..0000000 --- a/Sources/TerminalUI/Internal/Canvas.swift +++ /dev/null @@ -1,38 +0,0 @@ - -struct Canvas { - - private let draw: (Pixel, Position) -> Void - init(draw: @escaping (Pixel, Position) -> Void) { - self.draw = draw - } - - func draw(_ pixel: Pixel, at position: Position) { - draw(pixel, position) - } -} - -extension Canvas { - - init(_ output: some TextOutputStream) { - var output = output - self.init { pixel, position in - output.write(pixel.foreground.foreground) - output.write(pixel.background.background) - output.write(pixel.bold.controlSequence) - output.write(pixel.italic.controlSequence) - output.write(pixel.underline.controlSequence) - output.write(pixel.blinking.controlSequence) - output.write(pixel.inverse.controlSequence) - output.write(pixel.hidden.controlSequence) - output.write(pixel.strikethrough.controlSequence) - output.write(position.controlSequence) - output.write(pixel.content) - } - } -} - -extension TextOutputStream { - fileprivate mutating func write(_ character: Character) { - write(String(character)) - } -} diff --git a/Sources/TerminalUI/Rendering/AppCanvas.swift b/Sources/TerminalUI/Rendering/AppCanvas.swift new file mode 100644 index 0000000..097cf83 --- /dev/null +++ b/Sources/TerminalUI/Rendering/AppCanvas.swift @@ -0,0 +1,24 @@ + +struct AppCanvas: Canvas { + @Mutable var output: Output + + func draw(_ pixel: Pixel, at position: Position) { + output.write(pixel.foreground.foreground) + output.write(pixel.background.background) + output.write(pixel.bold.controlSequence) + output.write(pixel.italic.controlSequence) + output.write(pixel.underline.controlSequence) + output.write(pixel.blinking.controlSequence) + output.write(pixel.inverse.controlSequence) + output.write(pixel.hidden.controlSequence) + output.write(pixel.strikethrough.controlSequence) + output.write(position.controlSequence) + output.write(pixel.content) + } +} + +extension TextOutputStream { + fileprivate mutating func write(_ character: Character) { + write(String(character)) + } +} diff --git a/Sources/TerminalUI/Rendering/Canvas.swift b/Sources/TerminalUI/Rendering/Canvas.swift new file mode 100644 index 0000000..adae113 --- /dev/null +++ b/Sources/TerminalUI/Rendering/Canvas.swift @@ -0,0 +1,4 @@ + +protocol Canvas { + func draw(_ pixel: Pixel, at position: Position) +} diff --git a/Sources/TerminalUI/Internal/ControlSequence.swift b/Sources/TerminalUI/Rendering/ControlSequence.swift similarity index 100% rename from Sources/TerminalUI/Internal/ControlSequence.swift rename to Sources/TerminalUI/Rendering/ControlSequence.swift diff --git a/Sources/TerminalUI/Internal/FileHandleTextOutputStream.swift b/Sources/TerminalUI/Rendering/FileHandleTextOutputStream.swift similarity index 100% rename from Sources/TerminalUI/Internal/FileHandleTextOutputStream.swift rename to Sources/TerminalUI/Rendering/FileHandleTextOutputStream.swift diff --git a/Sources/TerminalUI/Internal/Pixel.swift b/Sources/TerminalUI/Rendering/Pixel.swift similarity index 100% rename from Sources/TerminalUI/Internal/Pixel.swift rename to Sources/TerminalUI/Rendering/Pixel.swift diff --git a/Sources/TerminalUI/Internal/Position.swift b/Sources/TerminalUI/Rendering/Position.swift similarity index 100% rename from Sources/TerminalUI/Internal/Position.swift rename to Sources/TerminalUI/Rendering/Position.swift diff --git a/Sources/TerminalUI/Internal/Size.swift b/Sources/TerminalUI/Rendering/Size.swift similarity index 100% rename from Sources/TerminalUI/Internal/Size.swift rename to Sources/TerminalUI/Rendering/Size.swift diff --git a/Sources/TerminalUI/View.swift b/Sources/TerminalUI/View.swift index cad5c5f..d16b89e 100644 --- a/Sources/TerminalUI/View.swift +++ b/Sources/TerminalUI/View.swift @@ -10,18 +10,18 @@ public protocol View { extension View { - func render(in canvas: Canvas) { - render(in: canvas, environment: EnvironmentValues()) + func _render(in canvas: any Canvas) { + _render(in: canvas, environment: EnvironmentValues()) } - func render(in canvas: Canvas, environment: EnvironmentValues) { + func _render(in canvas: any Canvas, environment: EnvironmentValues) { environment.install(on: self) - if let builtin = self as? BuiltinView { + if let builtin = self as? any Builtin { builtin.render(in: canvas, environment: environment) } else { - body.render(in: canvas, environment: environment) + body._render(in: canvas, environment: environment) } } } diff --git a/Sources/TerminalUI/ViewModifier.swift b/Sources/TerminalUI/ViewModifier.swift index e6c000e..f150ae4 100644 --- a/Sources/TerminalUI/ViewModifier.swift +++ b/Sources/TerminalUI/ViewModifier.swift @@ -22,17 +22,15 @@ public protocol ViewModifier { func body(content: Content) -> Body } -private struct ModifiedView: View { +private struct ModifiedView: Builtin, View { let content: Modifier.Content let modifier: Modifier - var body: some View { - BuiltinView { canvas, environment in - environment.install(on: modifier) - modifier - .body(content: content) - .render(in: canvas, environment: environment) - } + func render(in canvas: any Canvas, environment: EnvironmentValues) { + environment.install(on: modifier) + modifier + .body(content: content) + ._render(in: canvas, environment: environment) } } diff --git a/Sources/TerminalUI/Views/Text.swift b/Sources/TerminalUI/Views/Text.swift index c2bd082..c8685a6 100644 --- a/Sources/TerminalUI/Views/Text.swift +++ b/Sources/TerminalUI/Views/Text.swift @@ -1,32 +1,27 @@ -public struct Text { +public struct Text: Builtin, View { private let string: String public init(_ string: String) { self.string = string } -} - -extension Text: View { - public var body: some View { - BuiltinView { canvas, environment in - for (character, index) in zip(string, 1...) { - let pixel = Pixel( - character, - foreground: environment.foregroundColor, - background: environment.backgroundColor, - bold: environment.bold, - italic: environment.italic, - underline: environment.underline, - blinking: environment.blinking, - inverse: environment.inverse, - hidden: environment.hidden, - strikethrough: environment.strikethrough - ) - canvas.draw(pixel, at: Position(x: index, y: 0)) - } + func render(in canvas: any Canvas, environment: EnvironmentValues) { + for (character, index) in zip(string, 1...) { + let pixel = Pixel( + character, + foreground: environment.foregroundColor, + background: environment.backgroundColor, + bold: environment.bold, + italic: environment.italic, + underline: environment.underline, + blinking: environment.blinking, + inverse: environment.inverse, + hidden: environment.hidden, + strikethrough: environment.strikethrough + ) + canvas.draw(pixel, at: Position(x: index, y: 0)) } } } diff --git a/Sources/TerminalUITesting/View.expect.swift b/Sources/TerminalUITesting/View.expect.swift index acec37e..a79c6e1 100644 --- a/Sources/TerminalUITesting/View.expect.swift +++ b/Sources/TerminalUITesting/View.expect.swift @@ -1,12 +1,19 @@ @testable import TerminalUI import Testing +package struct TestCanvas: Canvas { + @Mutable package var pixels: [Position: Pixel] = [:] + package init() {} + package func draw(_ pixel: Pixel, at position: Position) { + pixels[position] = pixel + } +} + extension View { package func expect(_ expected: [Position: Pixel]) { - var pixels: [Position: Pixel] = [:] - let canvas = Canvas { pixels[$1] = $0 } - render(in: canvas) - #expect(pixels == expected) + let canvas = TestCanvas() + _render(in: canvas) + #expect(canvas.pixels == expected) } } diff --git a/Tests/TerminalUITests/Internal/CanvasTests.swift b/Tests/TerminalUITests/Internal/CanvasTests.swift index 8c9e0a6..27817be 100644 --- a/Tests/TerminalUITests/Internal/CanvasTests.swift +++ b/Tests/TerminalUITests/Internal/CanvasTests.swift @@ -8,7 +8,7 @@ struct CanvasTests { @Test("Drawing with default values") func defaultValues() { let stream = TestStream() - let canvas = Canvas(stream) + let canvas = AppCanvas(output: stream) canvas.draw(Pixel("a"), at: Position(x: 2, y: 1)) let controls = stream.output.split(separator: "\u{1b}") #expect(controls == [ diff --git a/Tests/TerminalUITests/Views/TextTests.swift b/Tests/TerminalUITests/Views/TextTests.swift index 2dcfb76..3e63013 100644 --- a/Tests/TerminalUITests/Views/TextTests.swift +++ b/Tests/TerminalUITests/Views/TextTests.swift @@ -8,12 +8,11 @@ struct TextTests { @Test("Text displays correctly") func displays() { - var pixels: [Position: Pixel] = [:] - let canvas = Canvas { pixels[$1] = $0 } + let canvas = TestCanvas() - Text("Hello").render(in: canvas) + Text("Hello")._render(in: canvas) - #expect(pixels == [ + #expect(canvas.pixels == [ Position(x: 1, y: 0): Pixel("H"), Position(x: 2, y: 0): Pixel("e"), Position(x: 3, y: 0): Pixel("l"),