Skip to content

Commit

Permalink
Factored out Rendering logic to setup for Client, Server transport.
Browse files Browse the repository at this point in the history
  • Loading branch information
zaneenders committed Jul 3, 2024
1 parent 4c19309 commit db62294
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 111 deletions.
1 change: 0 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ let package = Package(
from: "510.1.0"),
// View documentation locally with the following command
// swift package --disable-sandbox preview-documentation --target ChromaShell
// swift package --disable-sandbox preview-documentation --target Chroma
.package(
url: "https://github.com/apple/swift-docc-plugin.git",
from: "1.3.0"),
Expand Down
6 changes: 5 additions & 1 deletion Sources/ChromaShell/InteractionLoop.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ struct InteractionLoop: ~Copyable {
private let renderer: RenderObserver

init(_ block: some Block) {
self.renderer = RenderObserver(block)
let size = Terminal.size()
self.renderer = RenderObserver(
block, size.x, size.y, TerminalRenderer.self)
Terminal.setup()
}

Expand All @@ -31,6 +33,8 @@ struct InteractionLoop: ~Copyable {
guard let code = AsciiKeyCode.decode(keyboard: byte) else {
continue
}
let size = Terminal.size()
await renderer.updateSize(size.x, size.y)
switch await renderer.mode {
case .input:
switch code {
Expand Down
15 changes: 5 additions & 10 deletions Sources/ChromaShell/Rendering/Pipeline.swift
Original file line number Diff line number Diff line change
@@ -1,27 +1,22 @@
/// The pipeline used to display the graph and apply user interaction updates.
extension Block {
func pipeline(_ path: SelectedStateNode?) -> SelectedStateNode {
func pipeline(_ path: SelectedStateNode?) -> (
SelectedStateNode, VisibleNode
) {
var pathCopy = path
let size = Terminal.size()
let ascii = self.readBlockTree(.vertical)
let visible = self.readBlockTree(.vertical)
.flattenTuplesAndComposed()
.mergeArraysIntoGroups()
.wrapWithGroup()
.flattenSimilarGroups()
.createPath()
.mergeState(with: &pathCopy)
.computeVisible(size.x, size.y)
.drawVisible(size.x, size.y).0
ChromaFrame(ascii, .default, .default).render()
return pathCopy!
return (pathCopy!, visible)
}
}

enum Mode {
case normal
case input
}

extension SelectedStateNode {
/// applies the given command to the current selected state node.
/// Not sure if this is right or not honestly. But current approach isn't
Expand Down
42 changes: 39 additions & 3 deletions Sources/ChromaShell/Rendering/RenderObserver.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Observation

/// The Commands that can be sent to update the state of the ``RenderObserver``.
enum Command {
case `in`
case `out`
Expand All @@ -10,6 +11,13 @@ enum Command {
case unsafeInput(String)
}

/// The Modes in which RenderObserver can be in. This effects how input is
/// interpreted
enum Mode {
case normal
case input
}

/// This is going to be the core of what makes ChromaShell actually work as it
/// survive where the translation and updates to the given Block are processed
/// and passed to the terminal. This may get moved out of rendering and into
Expand All @@ -19,13 +27,39 @@ actor RenderObserver {
// Holds the current state between render passes. Mostly updated via
// commands like up, down, left, right, in, out.
private var graphState: SelectedStateNode? = nil
/// Displays the current ``Mode`` that the RenderObserver is in.
private(set) var mode: Mode = .normal
private var renderer: any Renderer.Type
private var x: Int
private var y: Int

init(_ block: some Block) {
/// The block provided will in a sense be the source of truth for the state
/// of the system and will not we swapped out for other versions during
/// run time. The other parameters will be update and used for each frame
/// update. Well x and y are provided hear it is good calling convention to
/// pass x and y before each command.
/// - Parameters:
/// - block: The Visual state of the system
/// - x: Initial x coordinate to render with.
/// - y: Initial y coordinate to render with.
/// - renderer: A type that will be constructed with the current
/// ``VisibleNode`` then render called with the current x and y values.
/// Which if updated before the command call will not change between there
/// and when they are passed to render.
init(_ block: some Block, _ x: Int, _ y: Int, _ renderer: any Renderer.Type)
{
self.x = x
self.y = y
self.renderer = renderer
self.block = block
}

/// Signal the update to rerender.
func updateSize(_ x: Int, _ y: Int) {
self.x = x
self.y = y
}

/// Signal the update to rerendered.
func startObservation() {
withObservationTracking {
render()
Expand All @@ -52,6 +86,8 @@ actor RenderObserver {

/// Draw to the screen, put the available data on the terminal.
func render() {
self.graphState = self.block.pipeline(self.graphState)
let (state, visible) = self.block.pipeline(self.graphState)
self.graphState = state
renderer.init(visible).render(x, y)
}
}
8 changes: 8 additions & 0 deletions Sources/ChromaShell/Rendering/Renderer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/// This type is responsible for output to the display of the current setup.
protocol Renderer {
/// This will be called per frame so keep the work between initialization
/// and render light.
init(_ graph: VisibleNode)
/// Outputs the actual image to the display.
func render(_ x: Int, _ y: Int)
}
108 changes: 108 additions & 0 deletions Sources/ChromaShell/Rendering/TerminalRenderer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
struct TerminalRenderer: Renderer {
let graph: VisibleNode

init(_ graph: VisibleNode) {
self.graph = graph
}

func render(_ x: Int, _ y: Int) {
let ascii = self.graph.drawVisible(x, y).0
ChromaFrame(ascii, .default, .default).render()
}
}

struct Consumed: Equatable {
let x: Int
let y: Int
}

extension VisibleNode {
/// Draw / render the VisibleNode consuming the max width and height given.
func drawVisible(_ width: Int, _ height: Int) -> (ANSIString, Consumed) {
switch self {
case let .entry(s):
return (s, Consumed(x: s.count, y: 1))
case let .button(l):
return (l, Consumed(x: l.count, y: 1))
case let .text(t):
return (t, Consumed(x: t.count, y: 1))
case let .group(orientation, children):
var xt = 0
var yt = 0
var out = ""
for child in children {
// TODO check if we can add
switch orientation {
/*
Hmm we need to add all the widths together then consume the renaming space?
*/
case .horizontal:
let (s, c) = child.drawVisible(width - xt, height)
yt = c.y
xt += c.x
out += s
case .vertical:
let (s, c) = child.drawVisible(width, height - yt)
yt += c.y
xt = c.x
out += s
}
}
switch orientation {
case .horizontal:
return consumeWidth(
out, needed: xt, available: width, height: yt)
case .vertical:
return consumeHeight(
out, needed: yt, available: height, width: xt)
}
case let .selected(child): // Apply Style?
let (s, c) = child.drawVisible(width, height)
return (_wrap(s, .black, .pink), c)
}
}
}

/// Consumes the given height
private func consumeHeight(
_ text: String, needed: Int, available height: Int, width: Int
) -> (ANSIString, Consumed) {
let half = (height - needed) / 2
let spacer = Array(repeating: " ", count: width).joined()
let spacing = Array(repeating: spacer, count: half)
let top = spacing
var bump = ""
let bottom = spacing
var yt = top.count + bump.count + needed + bottom.count
if yt != width {
yt += 1
bump += spacer
}
let out =
top.joined(separator: "") + bump + text
+ bottom.joined(separator: "")
return (out, Consumed(x: width, y: yt))
}

/// Consumes the given width
private func consumeWidth(
_ text: String, needed: Int, available width: Int, height: Int
)
-> (ANSIString, Consumed)
{
/*
BUG This only wraps one row not multiple. Maybe I need some notion of Row.
*/
let half = (width - needed) / 2
let spacing = Array(repeating: " ", count: half).joined()
let left = spacing
var bump = ""
let right = spacing
var xt = left.count + bump.count + needed + right.count
if xt != width {
xt += 1
bump += " "
}
let out = left + bump + text + right
return (out, Consumed(x: xt, y: height))
}
96 changes: 0 additions & 96 deletions Sources/ChromaShell/Rendering/VisibleNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,99 +54,3 @@ extension VisibleNode {
}
}
}

struct Consumed: Equatable {
let x: Int
let y: Int
}

extension VisibleNode {
/// Draw / render the VisibleNode consuming the max width and height given.
func drawVisible(_ width: Int, _ height: Int) -> (ANSIString, Consumed) {
switch self {
case let .entry(s):
return (s, Consumed(x: s.count, y: 1))
case let .button(l):
return (l, Consumed(x: l.count, y: 1))
case let .text(t):
return (t, Consumed(x: t.count, y: 1))
case let .group(orientation, children):
var xt = 0
var yt = 0
var out = ""
for child in children {
// TODO check if we can add
switch orientation {
/*
Hmm we need to add all the widths together then consume the renaming space?
*/
case .horizontal:
let (s, c) = child.drawVisible(width - xt, height)
yt = c.y
xt += c.x
out += s
case .vertical:
let (s, c) = child.drawVisible(width, height - yt)
yt += c.y
xt = c.x
out += s
}
}
switch orientation {
case .horizontal:
return consumeWidth(
out, needed: xt, available: width, height: yt)
case .vertical:
return consumeHeight(
out, needed: yt, available: height, width: xt)
}
case let .selected(child): // Apply Style?
let (s, c) = child.drawVisible(width, height)
return (_wrap(s, .black, .pink), c)
}
}
}

/// Consumes the given height
private func consumeHeight(
_ text: String, needed: Int, available height: Int, width: Int
) -> (ANSIString, Consumed) {
let half = (height - needed) / 2
let spacer = Array(repeating: " ", count: width).joined()
let spacing = Array(repeating: spacer, count: half)
let top = spacing
var bump = ""
let bottom = spacing
var yt = top.count + bump.count + needed + bottom.count
if yt != width {
yt += 1
bump += spacer
}
let out =
top.joined(separator: "") + bump + text
+ bottom.joined(separator: "")
return (out, Consumed(x: width, y: yt))
}

/// Consumes the given width
private func consumeWidth(
_ text: String, needed: Int, available width: Int, height: Int
)
-> (ANSIString, Consumed)
{
/*
BUG This only wraps one row not multiple. Maybe I need some notion of Row.
*/
let half = (width - needed) / 2
let spacing = Array(repeating: " ", count: half).joined()
let left = spacing
var bump = ""
let right = spacing
var xt = left.count + bump.count + needed + right.count
if xt != width {
xt += 1
bump += " "
}
let out = left + bump + text + right
return (out, Consumed(x: xt, y: height))
}

0 comments on commit db62294

Please sign in to comment.