///|
/// A Box represents a rectangular area of text data.
/// Each box has a consistent width across all rows.
struct Box {
data : Array[String]
}
///|
/// Vertical alignment options for box positioning.
pub(all) enum Vertical {
Top // Align to top edge
Center // Align to vertical center
Bottom // Align to bottom edge
}
///|
/// Horizontal alignment options for box positioning.
pub(all) enum Horizontal {
Left // Align to left edge
Center // Align to horizontal center
Right // Align to right edge
}
///|
/// Get the height (number of rows) of a box.
///
/// # Example
/// ```moonbit nocheck
/// let box = fill('*', 3, 5)
/// let _ = box.height() // returns 3
/// ```
pub fn Box::height(self : Box) -> Int {
self.data.length()
}
///|
/// Get the width (number of columns) of a box.
/// Returns 0 for empty boxes.
///
/// # Example
/// ```moonbit nocheck
/// let box = fill('*', 3, 5)
/// let _ = box.width() // returns 5
/// ```
pub fn Box::width(self : Box) -> Int {
if self.height() == 0 {
0
} else {
self.data[0].length()
}
}
///|
/// Get both dimensions of a box as (height, width).
///
/// # Example
/// ```moonbit nocheck
/// let box = fill('*', 3, 5)
/// let _ = box.dimensions() // returns (3, 5)
/// ```
pub fn Box::dimensions(self : Box) -> (Int, Int) {
(self.height(), self.width())
}
///|
/// Convert a box to a string for display.
///
/// # Example
/// ```moonbit nocheck
/// let box = fill('*', 2, 3)
/// let _ = box.to_string()
/// ```
pub impl Show for Box with to_string(self) {
let buf = @buffer.new()
self.output(buf)
buf.to_string()
}
///|
/// Output a box's content to a logger.
///
/// # Example
/// ```moonbit nocheck
/// let box = fill('*', 2, 3)
/// let logger = @buffer.new()
/// box.output(logger)
/// // Logs:
/// // ***
/// // ***
/// ```
pub impl Show for Box with output(self, logger) {
logger.write_string(self.data.join("\n"))
}
///|
/// Create a box filled with the specified character.
///
/// # Parameters
/// - `c`: The character to fill the box with
/// - `h`: Height (number of rows)
/// - `w`: Width (number of columns)
///
/// # Example
/// ```moonbit nocheck
/// let _star = fill('*', 3, 5)
/// // Creates:
/// // *****
/// // *****
/// // *****
/// ```
pub fn fill(c : Char, h : Int, w : Int) -> Box {
{ data: Array::make(h, String::repeat(c.to_string(), w)) }
}
///|
/// Create a 1×1 box containing a single character.
///
/// # Example
/// ```moonbit nocheck
/// let _star = singleton('*')
/// // Creates a single '*'
/// ```
pub fn singleton(c : Char) -> Box {
fill(c, 1, 1)
}
///|
/// Create a box filled with spaces.
///
/// # Parameters
/// - `h`: Height (number of rows)
/// - `w`: Width (number of columns)
///
/// # Example
/// ```moonbit nocheck
/// let _gap = space(2, 4)
/// // Creates 2 rows of 4 spaces each
/// ```
pub fn space(h : Int, w : Int) -> Box {
fill(' ', h, w)
}
///|
/// Create an empty box with zero dimensions.
/// Useful as an identity element for box combinations.
pub fn empty() -> Box {
fill(' ', 0, 0)
}
///|
/// Place two boxes side by side horizontally.
///
/// # Parameters
/// - `r`: The box to place to the right
/// - `align`: Vertical alignment (default: Center)
///
/// # Example
/// ```moonbit nocheck
/// let left = fill('L', 2, 3)
///
/// let right = fill('R', 3, 2)
///
/// let _combined = left.beside(right, align=Top)
/// ```
pub fn Box::beside(self : Box, r : Box, align? : Vertical = Center) -> Box {
guard self.width() != 0 else { r }
guard r.width() != 0 else { self }
let hl = self.heighten(r.height(), align~)
let hr = r.heighten(self.height(), align~)
{ data: hl.data.zip(hr.data).map(fn(s) { s.0 + s.1 }) }
}
///|
/// Stack two boxes vertically.
///
/// # Parameters
/// - `b`: The box to place below
/// - `align`: Horizontal alignment (default: Center)
///
/// # Example
/// ```moonbit nocheck
/// let top = fill('T', 1, 5)
///
/// let bottom = fill('B', 2, 3)
///
/// let _stacked = top.above(bottom, align=Left)
/// ```
pub fn Box::above(self : Box, b : Box, align? : Horizontal = Center) -> Box {
guard self.height() != 0 else { b }
guard b.height() != 0 else { self }
let wl = self.widen(b.width(), align~)
let wr = b.widen(self.width(), align~)
{ data: wl.data + wr.data }
}
///|
/// Overlay another box on top of this box, treating a character as transparent.
/// The overlay can be shifted by horizontal (`dx`) and vertical (`dy`) offsets.
///
/// # Parameters
/// - `overlay`: Box to draw over the base box
/// - `dx`: Horizontal offset relative to the base box (default: 0)
/// - `dy`: Vertical offset relative to the base box (default: 0)
/// - `transparent`: Character on the overlay that keeps the underlying content (default: space)
///
/// # Example
/// ```moonbit nocheck
/// let base = fill('.', 3, 5)
///
/// let marker = grid([
/// [singleton(' '), singleton('#')],
/// [singleton('#'), singleton('#')],
/// ])
///
/// let _combined = base.overlay(marker, dx=1, dy=1)
/// // Result:
/// // .....
/// // ..#..
/// // .##..
/// ```
pub fn Box::overlay(
self : Box,
overlay : Box,
dx? : Int = 0,
dy? : Int = 0,
transparent? : Char = ' ',
) -> Box {
guard overlay.height() != 0 && overlay.width() != 0 else { self }
let base_height = self.height()
let base_width = self.width()
let min_x = if dx < 0 { dx } else { 0 }
let min_y = if dy < 0 { dy } else { 0 }
let overlay_right = dx + overlay.width()
let overlay_bottom = dy + overlay.height()
let max_x = if base_width > overlay_right {
base_width
} else {
overlay_right
}
let max_y = if base_height > overlay_bottom {
base_height
} else {
overlay_bottom
}
let height = max_y - min_y
let width = max_x - min_x
let rows = []
for row = 0; row < height; row = row + 1 {
rows.push(Array::make(width, ' '))
}
fn paint(
dest : Array[Array[Char]],
src : Box,
offset_x : Int,
offset_y : Int,
transparent : Char,
skip_transparent : Bool,
) {
let h = src.height()
let w = src.width()
for y = 0; y < h; y = y + 1 {
let src_row = src.data[y].iter().to_array()
let dest_row = dest[offset_y + y]
for x = 0; x < w; x = x + 1 {
let ch = src_row[x]
if skip_transparent && ch == transparent {
continue
}
dest_row[offset_x + x] = ch
}
}
}
if base_height != 0 && base_width != 0 {
paint(rows, self, -min_x, -min_y, transparent, false)
}
paint(rows, overlay, dx - min_x, dy - min_y, transparent, true)
let result = []
for row = 0; row < height; row = row + 1 {
let buf = @buffer.new()
let chars = rows[row]
for col = 0; col < width; col = col + 1 {
buf.write_string(chars[col].to_string())
}
result.push(buf.to_string())
}
{ data: result }
}
///|
/// Expand a box to the specified width by adding padding.
///
/// # Parameters
/// - `w`: Target width
/// - `align`: Horizontal alignment of original content (default: Center)
///
/// # Example
/// ```moonbit nocheck
/// let box = fill('*', 2, 3)
///
/// let _widened = box.widen(7, align=Left)
/// // Adds 4 spaces to the right
/// ```
pub fn Box::widen(self : Box, w : Int, align? : Horizontal = Center) -> Box {
guard self.width() < w else { self }
let (bh, bw) = self.dimensions()
let pad = w - bw
match align {
Left => self.beside(space(bh, pad))
Right => space(bh, pad).beside(self)
Center => [space(bh, pad / 2), self, space(bh, pad - pad / 2)] |> hconcat()
}
}
///|
/// Expand a box to the specified height by adding padding.
///
/// # Parameters
/// - `h`: Target height
/// - `align`: Vertical alignment of original content (default: Center)
///
/// # Example
/// ```moonbit nocheck
/// let box = fill('*', 2, 3)
///
/// let _heightened = box.heighten(5, align=Top)
/// // Adds 3 rows of spaces below
/// ```
pub fn Box::heighten(self : Box, h : Int, align? : Vertical = Center) -> Box {
guard self.height() < h else { self }
let (bh, bw) = self.dimensions()
let pad = h - bh
match align {
Top => self.above(space(pad, bw))
Bottom => space(pad, bw).above(self)
Center => [space(pad / 2, bw), self, space(pad - pad / 2, bw)] |> vconcat()
}
}
///|
/// Combine an array of boxes horizontally.
///
/// # Parameters
/// - `boxes`: Array of boxes to combine
/// - `align`: Vertical alignment (default: Center)
///
/// # Example
/// ```moonbit nocheck
/// let boxes = [fill('A', 2, 1), fill('B', 3, 1), fill('C', 1, 1)]
///
/// let _combined = hconcat(boxes, align=Bottom)
/// ```
pub fn hconcat(boxes : Array[Box], align? : Vertical = Center) -> Box {
boxes.fold(init=empty(), fn(a, b) { a.beside(b, align~) })
}
///|
/// Combine an array of boxes vertically.
///
/// # Parameters
/// - `boxes`: Array of boxes to combine
/// - `align`: Horizontal alignment (default: Center)
///
/// # Example
/// ```moonbit nocheck
/// let boxes = [fill('A', 1, 3), fill('B', 1, 5), fill('C', 1, 2)]
///
/// let _stacked = vconcat(boxes, align=Left)
/// ```
pub fn vconcat(boxes : Array[Box], align? : Horizontal = Center) -> Box {
boxes.fold(init=empty(), fn(a, b) { a.above(b, align~) })
}
///|
/// Arrange boxes in a 2D grid layout.
/// Each sub-array represents a row of boxes.
///
/// # Parameters
/// - `g`: 2D array where `g[i][j]` is the box at row i, column j
///
/// # Example
/// ```moonbit nocheck
/// let corner = singleton('+')
///
/// let h_bar = fill('-', 1, 3)
///
/// let v_bar = fill('|', 1, 1)
///
/// let center = fill(' ', 1, 3)
///
/// let _frame = grid([
/// [corner, h_bar, corner],
/// [v_bar, center, v_bar],
/// [corner, h_bar, corner],
/// ])
/// ```
pub fn grid(g : Array[Array[Box]]) -> Box {
g.map(a => hconcat(a)) |> vconcat()
}
///|
/// Add a simple ASCII frame around a box using '+', '-', and '|' characters.
///
/// # Example
/// ```moonbit nocheck
/// let content = fill('*', 2, 4)
///
/// let _framed = content.framed()
/// // Creates:
/// // +----+
/// // |****|
/// // |****|
/// // +----+
/// ```
pub fn Box::framed(self : Box) -> Box {
let (h, w) = self.dimensions()
let v_bar = fill('|', h, 1)
let h_bar = fill('-', 1, w)
let corner = singleton('+')
grid([[corner, h_bar, corner], [v_bar, self, v_bar], [corner, h_bar, corner]])
}