Skip to content

Commit

Permalink
Model Canvas and Builtin views as protocols
Browse files Browse the repository at this point in the history
  • Loading branch information
danielctull committed Dec 14, 2024
2 parents 0b33ff6 + 0c128f6 commit 0a0b620
Show file tree
Hide file tree
Showing 18 changed files with 91 additions and 112 deletions.
4 changes: 2 additions & 2 deletions Sources/TerminalUI/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
18 changes: 9 additions & 9 deletions Sources/TerminalUI/Environment/EnvironmentModifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,19 @@ extension View {
_ keyPath: WritableKeyPath<EnvironmentValues, Value>,
_ value: Value
) -> some View {
modifier(EnvironmentModifier { $0[keyPath: keyPath] = value })
EnvironmentView(content: self, keyPath: keyPath, value: value)
}
}

private struct EnvironmentModifier<Content: View>: ViewModifier {
private struct EnvironmentView<Content: View, Value>: Builtin, View {

let modify: (inout EnvironmentValues) -> Void
let content: Content
let keyPath: WritableKeyPath<EnvironmentValues, Value>
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)
}
}
10 changes: 10 additions & 0 deletions Sources/TerminalUI/Internal/Builtin.swift
Original file line number Diff line number Diff line change
@@ -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.")
}
}
20 changes: 0 additions & 20 deletions Sources/TerminalUI/Internal/BuiltinView.swift

This file was deleted.

38 changes: 0 additions & 38 deletions Sources/TerminalUI/Internal/Canvas.swift

This file was deleted.

24 changes: 24 additions & 0 deletions Sources/TerminalUI/Rendering/AppCanvas.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@

struct AppCanvas<Output: TextOutputStream>: 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))
}
}
4 changes: 4 additions & 0 deletions Sources/TerminalUI/Rendering/Canvas.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

protocol Canvas {
func draw(_ pixel: Pixel, at position: Position)
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
10 changes: 5 additions & 5 deletions Sources/TerminalUI/View.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
14 changes: 6 additions & 8 deletions Sources/TerminalUI/ViewModifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,15 @@ public protocol ViewModifier {
func body(content: Content) -> Body
}

private struct ModifiedView<Modifier: ViewModifier>: View {
private struct ModifiedView<Modifier: ViewModifier>: 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)
}
}
37 changes: 16 additions & 21 deletions Sources/TerminalUI/Views/Text.swift
Original file line number Diff line number Diff line change
@@ -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))
}
}
}
15 changes: 11 additions & 4 deletions Sources/TerminalUITesting/View.expect.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
2 changes: 1 addition & 1 deletion Tests/TerminalUITests/Internal/CanvasTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 == [
Expand Down
7 changes: 3 additions & 4 deletions Tests/TerminalUITests/Views/TextTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down

0 comments on commit 0a0b620

Please sign in to comment.