///|
/// 绘制位置:一条顶线、一条内容行,或一条底线。
pub enum Line {
Top
Bottom
At(Int32)
} derive(Debug)
///|
pub extend Line with Debug::{to_repr}
///|
/// 一次渲染里不变的绘制参数。`grid` 供交叉字符判断使用:某个位置该用 `┼` 还是
/// `┴`,取决于它上方是不是跨列占位、左侧是不是跨行占位。
priv struct DrawCtx {
chars : Chars
pad_l : Int32
pad_r : Int32
col_widths : Array[Int32]
row_heights : Array[Int32]
cols : Array[Column]
head_style : @clicolor.Style
border_style : @clicolor.Style
truncate : String
wrap : Bool
on_word_boundary : Bool
has_head : Bool
grid : Array[Array[Placed]]
}
///|
fn placed_at(ctx : DrawCtx, y : Int32, x : Int32) -> Placed? {
if y < 0 || y >= ctx.grid.length() {
return None
}
let row = ctx.grid[y]
if x < 0 || x >= row.length() {
return None
}
Some(row[x])
}
///|
fn is_colspan_at(ctx : DrawCtx, y : Int32, x : Int32) -> Bool {
match placed_at(ctx, y, x) {
Some(p) =>
match p.kind {
ColSpan => true
_ => false
}
None => false
}
}
///|
fn is_rowspan_at(ctx : DrawCtx, y : Int32, x : Int32) -> Bool {
match placed_at(ctx, y, x) {
Some(p) =>
match p.kind {
RowSpan(_, _) => true
_ => false
}
None => false
}
}
///|
/// 跨行占位格对应的原格子。内容、行高、列宽都由原格子说了算。
fn origin_placed(ctx : DrawCtx, p : Placed, cell_offset : Int32) -> Placed {
match placed_at(ctx, p.y - cell_offset, p.x) {
Some(q) => q
None => p
}
}
///|
/// 格子宽度:它覆盖的列宽之和,加中间的竖线。
fn placed_width(ctx : DrawCtx, p : Placed) -> Int32 {
let span = span_of(p.cell.col_span)
let mut total = -1
for i in 0.. Int32 {
let span = span_of(p.cell.row_span)
let mut total = -1
for i in 0.. Bool {
p.x + span_of(p.cell.col_span) == ctx.col_widths.length()
}
///|
fn placed_h_align(ctx : DrawCtx, p : Placed) -> Align {
match p.cell.h_align {
Some(a) => a
None => if p.x < ctx.cols.length() { ctx.cols[p.x].align } else { Left }
}
}
///|
fn truncate_symbol(ctx : DrawCtx, p : Placed) -> String {
match p.cell.truncate {
Some(s) => s
None =>
if p.x < ctx.cols.length() {
match ctx.cols[p.x].truncate {
Some(s) => s
None => ctx.truncate
}
} else {
ctx.truncate
}
}
}
///|
fn placed_lines(ctx : DrawCtx, p : Placed) -> Array[String] {
let inner = inner_width(
ctx.col_widths,
p.x,
span_of(p.cell.col_span),
ctx.pad_l,
ctx.pad_r,
)
let raw = cell_lines(
p,
inner,
wraps(p, ctx.cols, ctx.wrap),
ctx.on_word_boundary,
)
// 超链接逐行包:终端按行渲染,跨行的 OSC 8 在多数终端里不会继承。
match p.cell.link {
Some(url) => raw.map(fn(line) { @clicolor.hyperlink(url, line) })
None => raw
}
}
///|
/// 该格子左边界用什么字符。跨行占位格要接住左侧格子,所以走 `rightMid`。
fn left_edge(ctx : DrawCtx, p : Placed, spanning : Int32?) -> String {
let mut left = if p.x == 0 { ctx.chars.left } else { ctx.chars.middle }
match spanning {
Some(off) =>
if p.x > 0 {
let mut cy = p.y + off
let mut cx = p.x - 1
let mut done = false
while !done {
match placed_at(ctx, cy, cx) {
Some(q) =>
match q.kind {
ColSpan => {
cy = q.y
cx = q.x - 1
}
RowSpan(_, _) => done = true
Content => {
left = ctx.chars.right_mid
done = true
}
}
None => done = true
}
}
}
None => ()
}
left
}
///|
/// 顶线上的交叉字符。四种情况:表格左上角、列之间的 `┬`、跨列格子上方的 `┬`、
/// 以及左侧跨行格子在顶线上该退成 `├`。
fn top_left_char(ctx : DrawCtx, p : Placed, offset : Int32) -> String {
let x = p.x + offset
if p.y == 0 {
if x == 0 {
ctx.chars.top_left
} else if offset == 0 {
ctx.chars.top_mid
} else {
ctx.chars.top
}
} else if x == 0 {
ctx.chars.left_mid
} else {
let mut ch = if offset == 0 {
ctx.chars.mid_mid
} else {
ctx.chars.bottom_mid
}
if is_colspan_at(ctx, p.y - 1, x) {
ch = if offset == 0 { ctx.chars.top_mid } else { ctx.chars.mid }
}
if offset == 0 {
let mut i = 1
while is_colspan_at(ctx, p.y, x - i) {
i += 1
}
if is_rowspan_at(ctx, p.y, x - i) {
ch = ctx.chars.left_mid
}
}
ch
}
}
///|
fn stylize(
ctx : DrawCtx,
p : Placed,
left : String,
content : String,
right : String,
) -> String {
let styled_content = if p.y == 0 && ctx.has_head {
ctx.head_style.text(content)
} else {
match p.cell.color {
Some(c) => c.text(content)
None => content
}
}
ctx.border_style.text(left) + styled_content + ctx.border_style.text(right)
}
///|
fn draw_top(ctx : DrawCtx, p : Placed) -> String {
let buf = StringBuilder()
let span = span_of(p.cell.col_span)
let first_row = p.y == 0
for i in 0.. String {
let left = if p.x == 0 { ctx.chars.bottom_left } else { ctx.chars.bottom_mid }
let content = repeat_str(ctx.chars.bottom, placed_width(ctx, p))
let right = if placed_draw_right(ctx, p) {
ctx.chars.bottom_right
} else {
""
}
ctx.border_style.text(left + content + right)
}
///|
fn draw_empty(ctx : DrawCtx, p : Placed, spanning : Int32?) -> String {
let left = left_edge(ctx, p, spanning)
let right = if placed_draw_right(ctx, p) { ctx.chars.right } else { "" }
stylize(ctx, p, left, repeat_str(" ", placed_width(ctx, p)), right)
}
///|
fn draw_line_at(
ctx : DrawCtx,
p : Placed,
line_num : Int32,
lines : Array[String],
force_truncate : Bool,
spanning : Int32?,
) -> String {
let left = left_edge(ctx, p, spanning)
let right = if placed_draw_right(ctx, p) { ctx.chars.right } else { "" }
let symbol = truncate_symbol(ctx, p)
let len = placed_width(ctx, p) - ctx.pad_l - ctx.pad_r
let raw = if line_num < lines.length() { lines[line_num] } else { "" }
let with_symbol = if force_truncate { raw + symbol } else { raw }
let fitted = truncate_to(with_symbol, len, symbol)
let padded = pad_to(fitted, len, placed_h_align(ctx, p))
let content = spaces(ctx.pad_l) + padded + spaces(ctx.pad_r)
stylize(ctx, p, left, content, right)
}
///|
/// 画一个有内容的格子:顶线、内容行、底线。内容不足格高时按纵向对齐补空行。
fn draw_content(
ctx : DrawCtx,
p : Placed,
line : Line,
spanning : Int32?,
) -> String {
match line {
Top => draw_top(ctx, p)
Bottom => draw_bottom(ctx, p)
At(n) => {
let lines = placed_lines(ctx, p)
let height = placed_height(ctx, p)
let pad_len = if height > lines.length() {
height - lines.length()
} else {
0
}
let pad_top = match p.cell.v_align {
Some(Middle) => (pad_len + 1) / 2
Some(Bottom) => pad_len
_ => 0
}
if n < pad_top || n >= pad_top + lines.length() {
draw_empty(ctx, p, spanning)
} else {
let force = lines.length() > height && n + 1 >= height
draw_line_at(ctx, p, n - pad_top, lines, force, spanning)
}
}
}
}
///|
/// 画一个跨行占位格:内容全部委托给原格子,只把行号按偏移挪过去。
fn draw_row_span(
ctx : DrawCtx,
p : Placed,
cell_offset : Int32,
offset : Int32,
line : Line,
) -> String {
let origin = origin_placed(ctx, p, cell_offset)
match line {
Top => draw_content(ctx, origin, At(offset), Some(cell_offset))
Bottom => draw_bottom(ctx, origin)
// 内容行不带跨行偏移:左右边界按普通列位处理,与 cli-table3 的
// `RowSpanCell.draw` 一致——只有 `top` 分支才把偏移继续往下传。
At(n) => draw_content(ctx, origin, At(offset + 1 + n), None)
}
}
///|
/// 画一个位置。跨列占位格不画任何东西。
fn draw_placed(ctx : DrawCtx, p : Placed, line : Line) -> String {
match p.kind {
ColSpan => ""
RowSpan(cell_offset, offset) =>
draw_row_span(ctx, p, cell_offset, offset, line)
Content => draw_content(ctx, p, line, None)
}
}
///|
/// 画一整行的某个位置,把所有格子拼起来。全空的行不占输出(无边框形态靠它)。
fn draw_row_line(
out : Array[String],
ctx : DrawCtx,
row : Array[Placed],
line : Line,
) -> Unit {
let buf = StringBuilder()
for p in row {
buf.write_string(draw_placed(ctx, p, line))
}
let s = buf.to_string()
if s != "" {
out.push(s)
}
}