///|
pub fn empty(style? : Style = Style::default()) -> Node {
apply_style(Empty, style)
}
///|
pub fn text(style? : Style = Style::default(), value : String) -> Node {
Text(value, style)
}
///|
pub fn fragment(
style? : Style = Style::default(),
children : Array[Node],
) -> Node {
apply_style(Fragment(children), style)
}
///|
pub fn vstack(
gap? : Int = 0,
style? : Style = Style::default(),
children : Array[Node],
) -> Node {
apply_style(VStack(children, gap), style)
}
///|
pub fn hstack(
gap? : Int = 0,
style? : Style = Style::default(),
children : Array[Node],
) -> Node {
apply_style(HStack(children, gap), style)
}
///|
pub fn pad(
edge~ : Edge,
style? : Style = Style::default(),
node : Node,
) -> Node {
apply_style(Pad(node, edge), style)
}
///|
pub fn border(
kind? : Border = Rounded,
style? : Style = Style::default(),
node : Node,
) -> Node {
apply_style(Bordered(node, kind, Style::default()), style)
}
///|
pub fn clip(
size~ : Size,
style? : Style = Style::default(),
node : Node,
) -> Node {
apply_style(Clip(node, size), style)
}
///|
pub fn sized(
width? : Int = -1,
height? : Int = -1,
style? : Style = Style::default(),
node : Node,
) -> Node {
apply_style(Sized(node, { width, height }), style)
}
///|
pub fn align(
horizontal? : Align = Start,
vertical? : Align = Start,
style? : Style = Style::default(),
node : Node,
) -> Node {
apply_style(Aligned(node, horizontal, vertical), style)
}
///|
pub fn place(
size~ : Size,
horizontal? : Align = Center,
vertical? : Align = Center,
style? : Style = Style::default(),
node : Node,
) -> Node {
apply_style(Placed(node, size, horizontal, vertical), style)
}
///|
pub fn join_horizontal(
gap? : Int = 0,
style? : Style = Style::default(),
children : Array[Node],
) -> Node {
hstack(gap~, style~) <| children
}
///|
pub fn join_vertical(
gap? : Int = 0,
style? : Style = Style::default(),
children : Array[Node],
) -> Node {
vstack(gap~, style~) <| children
}
///|
pub fn fill(style? : Style = Style::default(), value : String) -> Node {
Fill(value, style)
}
///|
fn apply_style(node : Node, style : Style) -> Node {
let mut out = node
let text_style = style_text(style)
if text_style != Style::default() {
out = Styled(out, text_style)
}
if style.padding != Edge::zero() {
out = Pad(out, style.padding)
}
if style.border != NoBorder {
out = Bordered(out, style.border, Style::default())
}
if style.align_set {
out = Aligned(out, style.align_horizontal, style.align_vertical)
}
if style.width >= 0 || style.height >= 0 {
out = Sized(out, { width: style.width, height: style.height })
}
if style.clip_width >= 0 && style.clip_height >= 0 {
out = Clip(out, { width: style.clip_width, height: style.clip_height })
}
out
}
///|
fn style_text(style : Style) -> Style {
{
..Style::default(),
fg: style.fg,
bg: style.bg,
bold: style.bold,
dim: style.dim,
italic: style.italic,
underline: style.underline,
blink: style.blink,
reverse: style.reverse,
strikethrough: style.strikethrough,
}
}
///|
pub fn render_plain(node : Node, size : Size) -> Array[String] {
let lines = render_lines(node, size, Style::default())
fit_lines(lines, size)
}
///|
fn render_lines(node : Node, size : Size, inherited : Style) -> Array[String] {
match node {
Empty => []
Text(value, _) => split_lines(value)
Fragment(children) => {
let out : Array[String] = []
for child in children {
for line in render_lines(child, size, inherited) {
out.push(line)
}
}
out
}
VStack(children, gap) => render_vstack(children, gap, size, inherited)
HStack(children, gap) => render_hstack(children, gap, size, inherited)
Pad(child, edge) => render_padded(child, edge, size, inherited)
Styled(child, style) => render_lines(child, size, inherited.merge(style))
Bordered(child, border, _) => render_border(child, border, size, inherited)
Clip(child, clip_size) =>
fit_lines(render_lines(child, clip_size, inherited), clip_size)
Sized(child, fixed) => render_sized(child, fixed, size, inherited)
Aligned(child, horizontal, vertical) =>
render_aligned(child, size, horizontal, vertical, inherited)
Placed(child, place_size, horizontal, vertical) =>
render_aligned(child, place_size, horizontal, vertical, inherited)
Fill(value, _) => {
let out : Array[String] = []
for _ in 0.. Array[String] {
let target = {
width: if fixed.width >= 0 {
fixed.width
} else {
available.width
},
height: if fixed.height >= 0 {
fixed.height
} else {
available.height
},
}
fit_block(render_lines(child, target, inherited), target)
}
///|
fn render_aligned(
child : Node,
size : Size,
horizontal : Align,
vertical : Align,
inherited : Style,
) -> Array[String] {
let raw = render_lines(child, size, inherited)
let lines : Array[String] = []
let limit = Int::min(raw.length(), size.height)
for index in 0.. Array[String] {
let out : Array[String] = []
for index, child in children {
if index > 0 {
for _ in 0.. Array[String] {
if children.is_empty() {
return []
}
let child_width = Int::max(
1,
(size.width - gap * (children.length() - 1)) / children.length(),
)
let rendered : Array[Array[String]] = []
let mut height = 0
for child in children {
let child_lines = fit_lines(
render_lines(
child,
{ width: child_width, height: size.height },
inherited,
),
{ width: child_width, height: size.height },
)
if child_lines.length() > height {
height = child_lines.length()
}
rendered.push(child_lines)
}
let out : Array[String] = []
let spacer = repeat_to_width(" ", gap)
for row in 0.. 0 {
line.write_string(spacer)
}
if row < child_lines.length() {
line.write_string(pad_right(child_lines[row], child_width))
} else {
line.write_string(repeat_to_width(" ", child_width))
}
}
out.push(line.to_string())
}
out
}
///|
fn render_padded(
child : Node,
edge : Edge,
size : Size,
inherited : Style,
) -> Array[String] {
let inner = {
width: Int::max(0, size.width - edge.left - edge.right),
height: Int::max(0, size.height - edge.top - edge.bottom),
}
let out : Array[String] = []
let blank = repeat_to_width(" ", size.width)
for _ in 0.. Array[String] {
if kind is NoBorder || size.width < 2 || size.height < 2 {
return render_lines(child, size, inherited)
}
let (tl, tr, bl, br, horizontal, vertical) = match kind {
Normal => ("┌", "┐", "└", "┘", "─", "│")
Rounded => ("╭", "╮", "╰", "╯", "─", "│")
Square => ("+", "+", "+", "+", "-", "|")
Double => ("╔", "╗", "╚", "╝", "═", "║")
Thick => ("┏", "┓", "┗", "┛", "━", "┃")
NoBorder => ("", "", "", "", "", "")
}
let inner = { width: size.width - 2, height: size.height - 2 }
let inner_lines = fit_lines(render_lines(child, inner, inherited), inner)
let out : Array[String] = []
out.push("\{tl}\{repeat_to_width(horizontal, inner.width)}\{tr}")
for line in inner_lines {
out.push("\{vertical}\{fit_line(line, inner.width)}\{vertical}")
}
while out.length() < size.height - 1 {
let blank = repeat_to_width(" ", inner.width)
out.push("\{vertical}\{blank}\{vertical}")
}
out.push("\{bl}\{repeat_to_width(horizontal, inner.width)}\{br}")
out
}
///|
fn split_lines(value : String) -> Array[String] {
let out : Array[String] = []
let mut start = 0
let mut index = 0
while index < value.length() {
if value[index] == '\n' {
out.push(value[start:index].to_owned())
start = index + 1
}
index += 1
}
out.push(value[start:].to_owned())
out
}
///|
pub fn fit_lines(lines : Array[String], size : Size) -> Array[String] {
let out : Array[String] = []
let limit = Int::min(lines.length(), size.height)
for index in 0.. Array[String] {
if width <= 0 {
return [""]
}
let out : Array[String] = []
for paragraph in split_lines(value) {
wrap_paragraph(paragraph, width, out)
}
out
}
///|
fn wrap_paragraph(value : String, width : Int, out : Array[String]) -> Unit {
if value == "" {
out.push("")
return
}
let mut line = ""
for word in split_words(value) {
let word_width = display_width(word)
if word_width > width {
if line != "" {
out.push(line)
line = ""
}
line = wrap_long_word(word, width, out)
} else if line == "" {
line = word
} else if display_width(line) + 1 + word_width <= width {
line = "\{line} \{word}"
} else {
out.push(line)
line = word
}
}
if line != "" {
out.push(line)
}
}
///|
fn wrap_long_word(value : String, width : Int, out : Array[String]) -> String {
let mut line = ""
let mut used = 0
for char in value {
let piece = char.to_string()
let piece_width = display_width(piece)
if used > 0 && used + piece_width > width {
out.push(line)
line = ""
used = 0
}
line = "\{line}\{piece}"
used += piece_width
}
line
}
///|
fn split_words(value : String) -> Array[String] {
let out : Array[String] = []
let mut start = 0
let mut index = 0
while index < value.length() {
if value[index] == ' ' {
if start < index {
out.push(value[start:index].to_owned())
}
start = index + 1
}
index += 1
}
if start < value.length() {
out.push(value[start:].to_owned())
}
out
}
///|
pub fn fit_block(lines : Array[String], size : Size) -> Array[String] {
let out = fit_lines(lines, size)
let blank = repeat_to_width(" ", size.width)
while out.length() < size.height {
out.push(blank)
}
out
}
///|
pub fn fit_line(line : String, width : Int) -> String {
if width <= 0 {
""
} else if display_width(line) <= width {
pad_right(line, width)
} else {
take_width(line, width)
}
}
///|
pub fn pad_right(line : String, width : Int) -> String {
let current = display_width(line)
if current >= width {
line
} else {
let padding = repeat_to_width(" ", width - current)
"\{line}\{padding}"
}
}
///|
pub fn align_line(line : String, width : Int, alignment : Align) -> String {
let clipped = if display_width(line) > width {
take_width(line, width)
} else {
line
}
let remaining = Int::max(0, width - display_width(clipped))
match alignment {
Start => {
let padding = repeat_to_width(" ", remaining)
"\{clipped}\{padding}"
}
Center => {
let left = remaining / 2
let left_padding = repeat_to_width(" ", left)
let right_padding = repeat_to_width(" ", remaining - left)
"\{left_padding}\{clipped}\{right_padding}"
}
End => {
let padding = repeat_to_width(" ", remaining)
"\{padding}\{clipped}"
}
}
}
///|
pub fn repeat_to_width(value : String, width : Int) -> String {
if width <= 0 || value == "" {
return ""
}
let out = StringBuilder::new()
let mut used = 0
while used < width {
out.write_string(value)
used += display_width(value)
}
take_width(out.to_string(), width)
}
///|
pub fn display_width(value : String) -> Int {
let mut width = 0
for char in value {
width += char_width(char)
}
width
}
///|
pub fn take_width(value : String, width : Int) -> String {
if width <= 0 {
return ""
}
let out = StringBuilder::new()
let mut used = 0
for char in value {
let width_of_char = char_width(char)
if used + width_of_char > width {
break
}
out.write_char(char)
used += width_of_char
}
out.to_string()
}
///|
fn char_width(char : Char) -> Int {
let code = char.to_int()
if (code >= 0x1100 && code <= 0x115f) ||
(code >= 0x2e80 && code <= 0xa4cf && !(code >= 0x2500 && code <= 0x259f)) ||
(code >= 0xac00 && code <= 0xd7a3) ||
(code >= 0xf900 && code <= 0xfaff) ||
(code >= 0xfe10 && code <= 0xfe6f) ||
(code >= 0xff00 && code <= 0xff60) ||
(code >= 0xffe0 && code <= 0xffe6) ||
(code >= 0x1f300 && code <= 0x1faff) {
2
} else {
1
}
}