use "collections"
actor TextBox is Widget
"""
A multi-line text display widget with optional word wrap.
When word wrap is enabled, text breaks at word boundaries (spaces) to
fit within the widget's width. When disabled, lines are hard-truncated
at the width. Lines beyond the widget's height are not displayed.
"""
let _state: WidgetState
var _text: String val
var _fg: Color
var _bg: Color
var _wrap: Bool
new create(
p: WidgetParent tag,
text: String val = "",
fg: Color = White,
bg: Color = Default,
wrap: Bool = true)
=>
_state = WidgetState(p)
_text = text
_fg = fg
_bg = bg
_wrap = wrap
be set_text(text: String val) =>
"""
Update the text content and re-render.
"""
_text = text
render_and_send()
be set_color(fg: Color, bg: Color = Default) =>
"""
Update the text colors and re-render.
"""
_fg = fg
_bg = bg
render_and_send()
be set_wrap(wrap: Bool) =>
"""
Enable or disable word wrapping and re-render.
"""
_wrap = wrap
render_and_send()
// -- Widget required helpers --
fun ref state(): WidgetState => _state
fun ref render(): Grid =>
let w = _state.width
let h = _state.height
let fg = _fg
let bg = _bg
let lines_ref = _layout_lines()
let lines: Array[String val] iso = recover iso
Array[String val](lines_ref.size())
end
for line in lines_ref.values() do
lines.push(line)
end
let lines_val: Array[String val] val = consume lines
let cells = recover val
let size = w * h
let arr = Array[Cell](size)
for row in Range(0, h) do
if row < lines_val.size() then
try
let line = lines_val(row)?
for col in Range(0, w) do
if col < line.size() then
try
arr.push(Cell(line(col)?.u32(), 1, fg, bg, 0))
else
arr.push(Cell(' ', 1, fg, bg, 0))
end
else
arr.push(Cell(' ', 1, fg, bg, 0))
end
end
else
for col in Range(0, w) do
arr.push(Cell(' ', 1, fg, bg, 0))
end
end
else
for col in Range(0, w) do
arr.push(Cell(' ', 1, fg, bg, 0))
end
end
end
arr
end
GridFactory(w, h, cells)
fun ref _layout_lines(): Array[String val] ref =>
"""
Split text into display lines. Respects explicit newlines.
When wrapping is enabled, breaks long lines at word boundaries.
"""
let result = Array[String val]
let raw_lines = _split_newlines()
for raw_line in raw_lines.values() do
if _wrap and (raw_line.size() > _state.width) then
_wrap_line(raw_line, result)
else
result.push(raw_line)
end
end
result
fun ref _split_newlines(): Array[String val] ref =>
"""
Split text on newline characters.
"""
let lines = Array[String val]
var start: USize = 0
var i: USize = 0
while i < _text.size() do
try
if _text(i)? == '\n' then
lines.push(_text.substring(start.isize(), i.isize()))
start = i + 1
end
end
i = i + 1
end
// Final segment
if start <= _text.size() then
lines.push(_text.substring(start.isize()))
end
lines
fun ref _wrap_line(line: String val, out: Array[String val] ref) =>
"""
Word-wrap a single line into multiple lines that fit within _state.width.
Breaks at the last space before the width limit. If a word is longer
than _state.width, hard-breaks it.
"""
var remaining = line
while remaining.size() > _state.width do
// Find last space within width
var break_at = _state.width
var found_space = false
var j = _state.width
while j > 0 do
j = j - 1
try
if remaining(j)? == ' ' then
break_at = j
found_space = true
break
end
end
end
if found_space then
out.push(remaining.substring(0, break_at.isize()))
// Skip the space
remaining = remaining.substring((break_at + 1).isize())
else
// No space found — hard break at width
out.push(remaining.substring(0, _state.width.isize()))
remaining = remaining.substring(_state.width.isize())
end
end
// Remainder
if remaining.size() > 0 then
out.push(remaining)
end