// Text measurement as vega-scenegraph does it when no canvas is available:
// widths are estimated as `~~(0.8 * length * fontSize)` (UTF-16 code units).
///|
pub fn font_size(item : Item) -> Double {
match item.get("fontSize") {
Null | Undef => 11.0
v => {
let x = v.to_number()
if x != 0.0 && !x.is_nan() {
x
} else {
0.0
}
}
}
}
///|
pub fn line_height(item : Item) -> Double {
match item.opt_num("lineHeight") {
Some(h) => h
None => font_size(item) + 2.0
}
}
///|
fn estimate_width(text : String, font_height : Double) -> Double {
@util.js_to_int32(0.8 * text.length().to_double() * font_height).to_double()
}
///|
fn text_limit(item : Item) -> Double {
match item.get("limit") {
Null | Undef => 0.0
v => v.to_number()
}
}
///|
/// vega `truncate`: shorten `text` with an ellipsis so its estimated width
/// stays below the item's `limit`.
fn truncate_text(item : Item, text : String) -> String {
let fs = font_size(item)
let width = fn(s : String) { estimate_width(s, fs) }
let mut limit = text_limit(item)
if width(text) < limit {
return text
}
let ellipsis = match item.get("ellipsis") {
v if v.truthy() => v.to_js_string()
_ => "\u{2026}"
}
let rtl = match item.str("dir") {
Some("rtl") => true
_ => false
}
let mut lo = 0
let mut hi = text.length()
limit = limit - width(ellipsis)
if rtl {
while lo < hi {
let mid = (lo + hi) >> 1
if width(text.unsafe_substring(start=mid, end=text.length())) > limit {
lo = mid + 1
} else {
hi = mid
}
}
ellipsis + text.unsafe_substring(start=lo, end=text.length())
} else {
while lo < hi {
let mid = 1 + ((lo + hi) >> 1)
if width(text.unsafe_substring(start=0, end=mid)) < limit {
lo = mid
} else {
hi = mid - 1
}
}
text.unsafe_substring(start=0, end=lo) + ellipsis
}
}
///|
/// vega `textValue(item, line)`: trimmed text, truncated to `limit`.
pub fn text_value(item : Item, line : @value.Value) -> String {
let text = match line {
Null | Undef => ""
v => @value.trim_js(v.to_js_string())
}
if text_limit(item) > 0.0 && text.length() > 0 {
truncate_text(item, text)
} else {
text
}
}
///|
/// The text content as one value or several lines (vega `textLines`).
pub enum TextLines {
Single(@value.Value)
Lines(Array[@value.Value])
}
///|
pub fn text_lines(item : Item) -> TextLines {
let text = item.get("text")
let lb = item.get("lineBreak")
let split : @value.Value = if lb.truthy() &&
text.truthy() &&
!(text is Arr(_)) {
let parts = text
.to_js_string()
.split(lb.to_js_string())
.map(fn(s) { @value.Value::Str(s.to_owned()) })
.collect()
Arr(parts)
} else {
text
}
match split {
Arr(items) =>
if items.length() > 1 {
Lines(items)
} else if items.length() == 1 {
Single(items[0])
} else {
Single(Undef)
}
other => Single(other)
}
}
///|
/// Width of a single line, as `textMetrics.width` (estimate).
pub fn text_width(item : Item, line : @value.Value) -> Double {
estimate_width(text_value(item, line), font_size(item))
}
///|
/// vega `offset(item)`: baseline correction in pixels.
pub fn baseline_offset(item : Item) -> Double {
let h = font_size(item)
let r = match item.str("baseline") {
Some("top") => 0.79 * h
Some("middle") => 0.3 * h
Some("bottom") => -0.21 * h
Some("line-top") => 0.29 * h + 0.5 * line_height(item)
Some("line-bottom") => 0.29 * h - 0.5 * line_height(item)
_ => 0.0
}
@util.js_round(r)
}
///|
/// vega `multiLineOffset(item)`: the extra height of a multi-line text.
pub fn multi_line_offset(item : Item) -> Double {
match text_lines(item) {
Lines(lines) => (lines.length() - 1).to_double() * line_height(item)
Single(_) => 0.0
}
}