///|
let font_size : Int32 = 13
///|
let title_size : Int32 = 16
///|
let line_height : Double = 17.0
///|
let title_line_height : Double = 21.0
///|
let pad_x : Double = 8.0
///|
let pad_y : Double = 5.0
///|
let natural_column_gap : Double = 56.0
///|
let box_gap : Double = 16.0
///|
let margin : Double = 20.0
///|
let min_value_width : Double = 16.0
///|
priv struct Budget {
limit : Int32
diagnostics : Array[@diagrams.Diagnostic]
mut used : Int32
mut failed : Bool
}
///|
fn Budget::charge(self : Budget, units : Int32) -> Bool {
if self.failed {
return false
}
if units < 0 || units > self.limit - self.used {
self.failed = true
self.diagnostics.push(
@diagrams.Diagnostic::error(
"data-tree/layout-limit", "Data-tree layout exceeds its work limit",
),
)
return false
}
self.used += units
true
}
///|
fn metrics(size : Int32, weight : Int32) -> @metrics.FontMetrics {
@metrics.FontMetrics::from_defaults(
profile=Monospace,
font_size=size,
font_weight=weight,
)
}
///|
/// The measured width of one line of text.
pub fn text_width(
line : String,
size~ : Int32 = 13,
weight~ : Int32 = 400,
) -> Double {
metrics(size, weight).measure(line).width.to_double()
}
///|
/// The line breaks of the text: LF, CR LF and CR.
fn split_breaks(text : String) -> Array[String] {
let out = []
let mut start = 0
let mut i = 0
let n = text.length()
while i < n {
let c = text.code_unit_at(i).to_int()
if c == 10 || c == 13 {
out.push(text.view(start_offset=start, end_offset=i).to_owned())
i += if c == 13 && i + 1 < n && text.code_unit_at(i + 1).to_int() == 10 {
2
} else {
1
}
start = i
} else {
i += 1
}
}
out.push(text.view(start_offset=start, end_offset=n).to_owned())
out
}
///|
/// Wrap text into lines no wider than `max_width`. A line breaks after its
/// last space when it has one, else between code points. Every code unit of
/// the text stays in the result; only line breaks become separate lines.
fn wrap(
text : String,
font : @metrics.FontMetrics,
max_width : Double,
budget : Budget,
) -> Array[String] {
let lines = []
for segment in split_breaks(text) {
let n = segment.length()
if n == 0 {
lines.push("")
continue
}
let mut start = 0
while start < n {
let mut end = start
let mut after_space = -1
while end < n {
let c = segment.code_unit_at(end).to_int()
let next = end +
(if c >= 0xd800 && c <= 0xdbff && end + 1 < n { 2 } else { 1 })
if !budget.charge(next - start) {
return lines
}
let candidate = segment
.view(start_offset=start, end_offset=next)
.to_owned()
if end > start && font.measure(candidate).width.to_double() > max_width {
break
}
if c == 32 {
after_space = next
}
end = next
}
if end < n && after_space > start {
end = after_space
}
lines.push(segment.view(start_offset=start, end_offset=end).to_owned())
start = end
}
}
lines
}
///|
priv struct Text {
lines : Array[String]
width : Double
height : Double
size : Int32
weight : Int32
ink : Ink
}
///|
fn measure_text(
text : String,
font : @metrics.FontMetrics,
size : Int32,
weight : Int32,
ink : Ink,
max_width : Double,
budget : Budget,
) -> Text {
let lines = wrap(text, font, max_width, budget)
let mut width = 0.0
for line in lines {
if !budget.charge(line.length() + 1) {
break
}
let w = font.measure(line).width.to_double()
if w > width {
width = w
}
}
let height = lines.length().to_double() *
(if size == title_size { title_line_height } else { line_height })
{ lines, width, height, size, weight, ink, }
}
///|
fn Text::place(self : Text, x : Double, y : Double) -> Label {
{
lines: ReadOnlyArray::from_array(self.lines),
box: { x, y, width: self.width, height: self.height, },
size: self.size.to_double(),
weight: self.weight,
ink: self.ink,
}
}
///|
priv struct MeasuredRow {
entry : @model.Member
kind : Kind
key : Text
value : Text?
offset : Double
height : Double
}
///|
priv struct Measured {
kind : Kind
depth : Int32
width : Double
height : Double
key_width : Double
rows : Array[MeasuredRow]
label : Text?
}
///|
fn failure(diagnostics : ArrayView[@diagrams.Diagnostic]) -> Result {
{ geometry: None, diagnostics: ReadOnlyArray::from_array(diagnostics), }
}
///|
/// Measured data geometry. Each container is a box with ordered rows;
/// arrays show the position of each row. A nested container is a separate
/// box in the next column, joined by a connector from its row. Inline
/// scalars stay in their row, with no connector. Empty containers and a
/// scalar root are visible boxes. Layout is iterative and every bound is
/// explicit; exhaustion gives a diagnostic and no geometry.
pub fn layout(
tree : @model.DataTree,
limits~ : Limits = Limits::new(),
) -> Result {
let column_gap = natural_column_gap.max(limits.minimum_approach * 2.0 + 16.0)
let diagnostics : Array[@diagrams.Diagnostic] = []
if limits.nodes <= 0 ||
limits.depth <= 0 ||
limits.work <= 0 ||
limits.width <= 0 ||
limits.output_units <= 0 {
diagnostics.push(
@diagrams.Diagnostic::error(
"data-tree/invalid-limits", "Data-tree layout limits must be positive",
),
)
return failure(diagnostics)
}
let indexed = @model.Index::build(
tree,
limits=@model.Limits::new(
nodes=limits.nodes,
depth=limits.depth,
work=limits.work,
),
)
guard indexed.index is Some(index) else {
return failure(indexed.diagnostics.iter().to_array())
}
let budget : Budget = {
limit: limits.work,
diagnostics,
used: 0,
failed: false,
}
let max_width = limits.width.to_double()
let regular = metrics(font_size, 400)
let bold = metrics(font_size, 700)
let heading = metrics(title_size, 700)
let row_offset : Map[String, Double] = Map([])
let marked_members : Map[String, Bool] = Map([])
let marked_nodes : Map[String, Bool] = Map([])
for highlight in tree.highlights {
for target in highlight.targets {
if index.find_member(target) is Some(_) {
marked_members[target] = true
} else {
marked_nodes[target] = true
}
}
}
// A highlighted inline scalar marks its row.
for id, _ in marked_nodes {
if index.node(id) is Some(node) &&
!node.is_container() &&
index.owner(id) is Some(owner) {
marked_members[owner.id] = true
}
}
let measured : Map[String, Measured] = Map([])
let order : Array[String] = []
for id in index.order {
if !budget.charge(1) {
return failure(diagnostics)
}
let node = index.node(id).unwrap()
if !node.is_container() && id != tree.root {
continue
}
order.push(id)
let kind = kind_of(node.value)
let depth = index.depth(id).unwrap_or(0)
let members = node.members()
if members.is_empty() {
let text = match node.value {
Object(_) => "{}"
Array(_) => "[]"
value => display_text(value)
}
let label = measure_text(
text,
regular,
font_size,
400,
ink_of(kind),
max_width,
budget,
)
measured[id] = {
kind,
depth,
width: (label.width + 2.0 * pad_x).max(32.0),
height: label.height + 2.0 * pad_y,
key_width: 0.0,
rows: [],
label: Some(label),
}
continue
}
let rows = []
let mut key_width = 0.0
let mut value_width = min_value_width
let mut offset = 0.0
for entry in members {
let value = index.node(entry.value).unwrap()
let value_kind = kind_of(value.value)
let key = match entry.key {
Name(name) =>
measure_text(name, bold, font_size, 700, Key, max_width, budget)
Index(i) =>
measure_text(
"[\{i}]",
regular,
font_size,
400,
Index,
max_width,
budget,
)
}
let text = display_text(value.value)
let value_text = if text.is_empty() {
None
} else {
Some(
measure_text(
text,
regular,
font_size,
400,
ink_of(value_kind),
max_width,
budget,
),
)
}
if budget.failed {
return failure(diagnostics)
}
key_width = key_width.max(key.width)
let value_height = match value_text {
Some(t) => {
value_width = value_width.max(t.width)
t.height
}
None => line_height
}
let height = key.height.max(value_height) + 2.0 * pad_y
rows.push({
entry,
kind: value_kind,
key,
value: value_text,
offset,
height,
})
row_offset[entry.id] = offset
offset += height
}
measured[id] = {
kind,
depth,
width: key_width + value_width + 4.0 * pad_x,
height: offset,
key_width,
rows,
label: None,
}
}
// Column positions from the widest box of each depth.
let columns : Array[Double] = []
for id in order {
let m = measured[id]
while columns.length() <= m.depth {
columns.push(0.0)
}
columns[m.depth] = columns[m.depth].max(m.width)
}
let column_x : Array[Double] = []
let mut x = margin
for width in columns {
column_x.push(x)
x += width + column_gap
}
let title = tree.title.map(text => {
measure_text(text, heading, title_size, 700, Title, max_width * 2.0, budget).place(
margin, margin,
)
})
if budget.failed {
return failure(diagnostics)
}
let top = margin + title.map(t => t.box.height + 12.0).unwrap_or(0.0)
let free : Array[Double] = Array::make(columns.length(), top)
let placed_y : Map[String, Double] = Map([])
for id in order {
if !budget.charge(1) {
return failure(diagnostics)
}
let m = measured[id]
let wanted = match index.owner(id) {
None => top
Some(owner) => {
let parent = index.container(owner.id).unwrap()
placed_y[parent] + row_offset[owner.id]
}
}
let y = wanted.max(free[m.depth])
placed_y[id] = y
free[m.depth] = y + m.height + box_gap
}
let boxes : Array[Box] = []
let connectors : Array[Connector] = []
let mut height = top
let mut width = margin
for id in order {
if !budget.charge(1) {
return failure(diagnostics)
}
let m = measured[id]
let bx = column_x[m.depth]
let by = placed_y[id]
let frame : Rect = { x: bx, y: by, width: m.width, height: m.height, }
height = height.max(frame.bottom())
width = width.max(frame.right())
let divider = bx + m.key_width + 2.0 * pad_x
let rows = []
for row in m.rows {
let ry = by + row.offset
let row_box : Rect = { x: bx, y: ry, width: m.width, height: row.height, }
let child = measured.get(row.entry.value)
let port : Point? = child.map(_ => {
x: frame.right(),
y: ry + row.height / 2.0,
})
rows.push({
entry: row.entry.id,
node: row.entry.value,
kind: row.kind,
box: row_box,
key: Some(row.key.place(bx + pad_x, ry + pad_y)),
value: row.value.map(t => t.place(divider + pad_x, ry + pad_y)),
highlighted: marked_members.contains(row.entry.id),
port,
})
if child is Some(c) && port is Some(start) {
let cy = placed_y[row.entry.value]
let anchor = cy +
c.rows.get(0).map(r => r.height).unwrap_or(c.height) / 2.0
let column_right = column_x[m.depth] + columns[m.depth]
let target_x = column_x[c.depth]
let bend = (target_x - column_right) / 2.0
connectors.push({
entry: row.entry.id,
source: id,
target: row.entry.value,
points: [
start,
{ x: column_right, y: start.y, },
{ x: column_right + bend, y: start.y, },
{ x: target_x - bend, y: anchor, },
{ x: target_x, y: anchor, },
],
})
}
}
boxes.push({
node: id,
kind: m.kind,
box: frame,
divider: if m.rows.is_empty() {
None
} else {
Some(divider)
},
rows: ReadOnlyArray::from_array(rows),
label: m.label.map(t => t.place(bx + pad_x, by + pad_y)),
highlighted: marked_nodes.contains(id),
depth: m.depth,
})
}
if title is Some(t) {
width = width.max(t.box.right())
}
let total_width = (width + margin).ceil()
let total_height = (height + margin).ceil()
if total_width > 16777216.0 || total_height > 16777216.0 {
diagnostics.push(
@diagrams.Diagnostic::error(
"data-tree/layout-limit", "Data-tree geometry exceeds its safe numeric bound",
),
)
return failure(diagnostics)
}
{
geometry: Some({
tree,
width: total_width.to_int(),
height: total_height.to_int(),
title,
boxes: ReadOnlyArray::from_array(boxes),
connectors: ReadOnlyArray::from_array(connectors),
}),
diagnostics: ReadOnlyArray::from_array(diagnostics),
}
}