// Port of rich/containers.py and rich/_wrap.py.
///|
/// A list of renderables that renders its contents in order.
pub struct Renderables {
renderables : Array[&Renderable]
}
///|
pub fn Renderables::new(renderables? : Array[&Renderable] = []) -> Renderables {
{ renderables: renderables.copy(), }
}
///|
/// Iterate over the renderables.
pub fn Renderables::iter(self : Renderables) -> Iter[&Renderable] {
self.renderables.iter()
}
///|
pub fn Renderables::append(
self : Renderables,
renderable : &Renderable,
) -> Unit {
self.renderables.push(renderable)
}
///|
pub impl Renderable for Renderables with fn rich_console(
self,
_console,
_options,
) {
self.renderables.map(r => Child(r))
}
///|
pub impl Renderable for Renderables with fn rich_measure(self, console, options) {
if self.renderables.is_empty() {
return Some({ minimum: 1, maximum: 1, })
}
let mut min = 0
let mut max = 0
for i, r in self.renderables {
let d = Measurement::get(console, options, r)
if i == 0 {
min = d.minimum
max = d.maximum
} else {
min = @cmp.maximum(min, d.minimum)
max = @cmp.maximum(max, d.maximum)
}
}
Some({ minimum: min, maximum: max, })
}
///|
/// A list of `Text` lines which renders each line.
pub struct Lines {
lines : Array[Text]
}
///|
pub fn Lines::new(lines : ArrayView[Text]) -> Lines {
{ lines: lines.to_owned(), }
}
///|
pub fn Lines::length(self : Lines) -> Int {
self.lines.length()
}
///|
/// The line at `index` (negative indices count from the end, as in
/// Python). Aborts if out of range.
pub fn Lines::get(self : Lines, index : Int) -> Text {
self.lines[if index < 0 { self.lines.length() + index } else { index }]
}
///|
/// Replace the line at `index` (negative indices count from the end).
pub fn Lines::set(self : Lines, index : Int, value : Text) -> Unit {
self.lines[if index < 0 { self.lines.length() + index } else { index }] = value
}
///|
/// Lines `start..end` with Python slice semantics (upstream `lines[a:b]`).
pub fn Lines::slice(self : Lines, start? : Int, end? : Int) -> Array[Text] {
let n = self.lines.length()
fn clamp(i : Int?, default : Int) -> Int {
match i {
None => default
Some(i) => if i < 0 { @cmp.maximum(0, n + i) } else { @cmp.minimum(i, n) }
}
}
let a = clamp(start, 0)
let b = clamp(end, n)
if a >= b {
[]
} else {
self.lines[a:b].to_owned()
}
}
///|
pub fn Lines::append(self : Lines, line : Text) -> Unit {
self.lines.push(line)
}
///|
pub fn Lines::extend_(self : Lines, lines : ArrayView[Text]) -> Unit {
for line in lines {
self.lines.push(line)
}
}
///|
/// Remove and return the line at `index` (default: the last line;
/// negative indices count from the end). `None` if out of range.
pub fn Lines::pop(self : Lines, index? : Int = -1) -> Text? {
let n = self.lines.length()
let i = if index < 0 { n + index } else { index }
if i < 0 || i >= n {
None
} else {
Some(self.lines.remove(i))
}
}
///|
pub fn Lines::iter(self : Lines) -> Iter[Text] {
self.lines.iter()
}
///|
/// Upstream `repr(lines)`.
pub fn Lines::repr(self : Lines) -> String {
"Lines([" + self.lines.map(l => l.repr()).join(", ") + "])"
}
///|
pub impl Renderable for Lines with fn rich_console(self, _console, _options) {
self.lines.map(l => Child(l))
}
///|
/// Justify and overflow text to a given width.
pub fn Lines::justify(
self : Lines,
console : Console,
width : Int,
justify? : JustifyMethod = Left,
overflow? : OverflowMethod = Fold,
) -> Unit raise {
match justify {
Left =>
for line in self.lines {
line.truncate(width, overflow~, pad=true)
}
Center =>
for line in self.lines {
line.rstrip()
line.truncate(width, overflow~)
line.pad_left(floor_div(width - @cells.cell_len(line.plain()), 2))
line.pad_right(width - @cells.cell_len(line.plain()))
}
Right =>
for line in self.lines {
line.rstrip()
line.truncate(width, overflow~)
line.pad_left(width - @cells.cell_len(line.plain()))
}
Full =>
for line_index, line in self.lines {
if line_index == self.lines.length() - 1 {
break
}
let words = line.split(separator=" ").lines
let mut words_size = 0
for word in words {
words_size += @cells.cell_len(word.plain())
}
let mut num_spaces = words.length() - 1
let spaces = Array::make(@cmp.maximum(0, num_spaces), 1)
let mut index = 0
if !spaces.is_empty() {
while words_size + num_spaces < width {
spaces[spaces.length() - index - 1] += 1
num_spaces += 1
index = (index + 1) % spaces.length()
}
}
let tokens : Array[Text] = []
for index, word in words {
tokens.push(word)
if index < spaces.length() {
let next_word = words[index + 1]
let style = word.get_style_at_offset(console, -1)
let next_style = next_word.get_style_at_offset(console, 0)
let space_style = if style == next_style {
StyleType::Style(style)
} else {
line.style
}
tokens.push(
Text::new(text=space_string(spaces[index]), style=space_style),
)
}
}
self.lines[line_index] = Text::new().join(tokens)
}
Default => ()
}
}
///|
/// Match `\s*\S+\s*` at `position`; returns the end offset.
fn match_word(text : String, position : Int) -> Int? {
let n = text.length()
let mut i = position
while i < n && is_py_whitespace(text[i].to_int().unsafe_to_char()) {
i += 1
}
let start_word = i
while i < n && !is_py_whitespace(text[i].to_int().unsafe_to_char()) {
i += 1
}
if i == start_word {
return None
}
while i < n && is_py_whitespace(text[i].to_int().unsafe_to_char()) {
i += 1
}
Some(i)
}
///|
/// Words of `text` as `(start, end, word)` where a word includes the
/// whitespace around it.
pub fn words(text : String) -> Array[(Int, Int, String)] {
let out = []
let mut position = 0
while match_word(text, position) is Some(end) {
out.push(
(
position,
end,
text.view(start_offset=position, end_offset=end).to_owned(),
),
)
position = end
}
out
}
///|
/// Given a string of text and a width (in cells), return the offsets at
/// which the string should be split in order to fit within the width.
/// With `fold`, words longer than `width` are folded onto new lines.
pub fn divide_line(
text : String,
width : Int,
fold? : Bool = true,
) -> Array[Int] {
let break_positions = []
let mut cell_offset = 0
let mut position = 0
// the words of `words(text)`, as views (no substring per word)
while match_word(text, position) is Some(end) {
let mut start = position
let word = text.view(start_offset=position, end_offset=end)
position = end
let mut stripped_end = word.length()
while stripped_end > 0 &&
is_py_space_unit(word.unsafe_get(stripped_end - 1).to_int()) {
stripped_end -= 1
}
let word_length = @cells.cell_len(word.view(end_offset=stripped_end))
let remaining_space = width - cell_offset
if remaining_space >= word_length {
cell_offset += @cells.cell_len(word)
} else if word_length > width {
if fold {
let folded_word = @cells.chop_cells(word, width)
for i, line in folded_word {
let last = i == folded_word.length() - 1
if start != 0 {
break_positions.push(start)
}
if last {
cell_offset = @cells.cell_len(line)
} else {
start += line.length()
}
}
} else {
if start != 0 {
break_positions.push(start)
}
cell_offset = @cells.cell_len(word)
}
} else if cell_offset != 0 && start != 0 {
break_positions.push(start)
cell_offset = @cells.cell_len(word)
}
}
break_positions
}