///|
/// Record / replay of the external text-measurement side effect.
///
/// Text intrinsic sizing is the one non-deterministic, non-portable input to a
/// render: it crosses into whatever font engine is wired through the
/// TextMetricsProvider. Recording captures every `(text, font, …, available
/// size) -> IntrinsicSize` result a render observed; replaying serves those
/// results back without calling the provider, so the render is fully
/// deterministic and reproducible anywhere (e.g. a VRT snapshot replayed on a
/// machine with no font engine). See create_text_measure_with_provider, which
/// routes its result through record_replay_text_measure.
///|
/// A single recorded measurement: the encoded measurement key and its result.
pub(all) struct RecordedMetric {
key : String
min_width : Double
max_width : Double
min_height : Double
max_height : Double
}
///|
/// When Some, every measurement is appended here (recording mode).
let measurement_record_buffer : Ref[Array[RecordedMetric]?] = { val: None }
///|
/// When Some, measurements are served from here (replay mode); a miss falls
/// back to the live measure func.
let measurement_replay_table : Ref[Map[String, @layout_types.IntrinsicSize]?] = {
val: None,
}
///|
/// Field separator unlikely to appear in CSS text / font names.
let measurement_key_sep : String = "\u{0001}"
///|
fn white_space_key(ws : @style.WhiteSpace) -> String {
match ws {
@style.WhiteSpace::Normal => "n"
@style.WhiteSpace::Nowrap => "w"
@style.WhiteSpace::Pre => "p"
@style.WhiteSpace::PreWrap => "pw"
@style.WhiteSpace::PreLine => "pl"
}
}
///|
fn writing_mode_key(wm : @style.WritingMode) -> String {
match wm {
@style.WritingMode::HorizontalTb => "h"
@style.WritingMode::VerticalRl => "vr"
@style.WritingMode::VerticalLr => "vl"
}
}
///|
/// Encode the measurement inputs (everything the result depends on except the
/// available size, which is appended per call) into a stable key prefix.
fn measurement_key_prefix(
text : String,
font_size : Double,
line_height : Double,
white_space : @style.WhiteSpace,
writing_mode : @style.WritingMode,
font_weight : Double,
font_family : String,
) -> String {
let s = measurement_key_sep
text +
s +
font_size.to_string() +
s +
line_height.to_string() +
s +
white_space_key(white_space) +
s +
writing_mode_key(writing_mode) +
s +
font_weight.to_string() +
s +
font_family
}
///|
/// Wrap a measure func so its results are recorded or replayed when those modes
/// are active; otherwise return it unchanged (the common, zero-overhead path).
/// `prefix` is the per-input key from measurement_key_prefix.
fn record_replay_text_measure(
prefix : String,
inner : @layout_types.MeasureFunc,
) -> @layout_types.MeasureFunc {
match measurement_replay_table.val {
Some(table) => {
let sep = measurement_key_sep
let replayed : @layout_types.MeasureFunc = {
func: fn(available_width, available_height) {
let key = prefix +
sep +
available_width.to_string() +
sep +
available_height.to_string()
match table.get(key) {
Some(result) => result
// Unknown measurement (recording incomplete): fall back to live.
None => (inner.func)(available_width, available_height)
}
},
}
replayed
}
None =>
match measurement_record_buffer.val {
Some(buffer) => {
let sep = measurement_key_sep
let recording : @layout_types.MeasureFunc = {
func: fn(available_width, available_height) {
let result = (inner.func)(available_width, available_height)
let key = prefix +
sep +
available_width.to_string() +
sep +
available_height.to_string()
buffer.push({
key,
min_width: result.min_width,
max_width: result.max_width,
min_height: result.min_height,
max_height: result.max_height,
})
result
},
}
recording
}
None => inner
}
}
}
///|
/// Begin recording text measurements. Clears any active replay.
pub fn start_text_metrics_recording() -> Unit {
measurement_record_buffer.val = Some([])
measurement_replay_table.val = None
}
///|
/// Stop recording and return everything captured since start.
pub fn take_text_metrics_recording() -> Array[RecordedMetric] {
let recorded = match measurement_record_buffer.val {
Some(buffer) => buffer
None => []
}
measurement_record_buffer.val = None
recorded
}
///|
/// Serve text measurements from `metrics` (replay mode); a measurement not in
/// the recording falls back to the live measure func. Clears any active
/// recording.
pub fn install_text_metrics_replay(metrics : Array[RecordedMetric]) -> Unit {
let table : Map[String, @layout_types.IntrinsicSize] = {}
for m in metrics {
let size : @layout_types.IntrinsicSize = {
min_width: m.min_width,
max_width: m.max_width,
min_height: m.min_height,
max_height: m.max_height,
}
table[m.key] = size
}
measurement_replay_table.val = Some(table)
measurement_record_buffer.val = None
}
///|
/// Disable both recording and replay.
pub fn clear_text_metrics_record_replay() -> Unit {
measurement_record_buffer.val = None
measurement_replay_table.val = None
}
///|
/// Serialize a recording to a self-delimiting string (length-prefixed keys, so
/// arbitrary text/newlines in keys round-trip safely).
pub fn serialize_text_metrics_recording(
metrics : Array[RecordedMetric],
) -> String {
let buf = StringBuilder::new()
buf.write_string(metrics.length().to_string())
buf.write_char('\n')
for m in metrics {
buf.write_string(m.min_width.to_string())
buf.write_char(' ')
buf.write_string(m.max_width.to_string())
buf.write_char(' ')
buf.write_string(m.min_height.to_string())
buf.write_char(' ')
buf.write_string(m.max_height.to_string())
buf.write_char(' ')
buf.write_string(m.key.length().to_string())
buf.write_char(':')
buf.write_string(m.key)
}
buf.to_string()
}
///|
/// Parse a recording produced by serialize_text_metrics_recording.
pub fn deserialize_text_metrics_recording(
data : String,
) -> Array[RecordedMetric] {
let result : Array[RecordedMetric] = []
let len = data.length()
let mut i = 0
fn unit_at(idx : Int) -> Int {
data[idx].to_int()
}
let nl = '\n'.to_int()
let sp = ' '.to_int()
let colon = ':'.to_int()
// Read the leading count line (used only as a header; parsing is driven by
// the per-record length prefixes).
while i < len && unit_at(i) != nl {
i = i + 1
}
i = i + 1 // skip the newline
fn read_until(stop : Int) -> String {
let start = i
while i < len && unit_at(i) != stop {
i = i + 1
}
let s = data[start:i].to_string()
i = i + 1 // consume stop
s
}
while i < len {
let mw = @string.parse_double(read_until(sp)) catch { _ => 0.0 }
let xw = @string.parse_double(read_until(sp)) catch { _ => 0.0 }
let mh = @string.parse_double(read_until(sp)) catch { _ => 0.0 }
let xh = @string.parse_double(read_until(sp)) catch { _ => 0.0 }
let key_len = @string.parse_int(read_until(colon)) catch { _ => 0 }
let key = data[i:i + key_len].to_string()
i = i + key_len
result.push({
key,
min_width: mw,
max_width: xw,
min_height: mh,
max_height: xh,
})
}
result
}
///|
/// A single recorded image intrinsic size lookup: the source and its result
/// (`has_size = false` records a provider that returned "no intrinsic size").
pub(all) struct RecordedImage {
src : String
has_size : Bool
width : Double
height : Double
}
///|
let image_record_buffer : Ref[Array[RecordedImage]?] = { val: None }
///|
let image_replay_table : Ref[Map[String, (Double, Double)?]?] = { val: None }
///|
/// Resolve an image intrinsic size through the record/replay layer. Returns
/// `Some(replayed)` to short-circuit the live resolver during replay; otherwise
/// `None` and the caller resolves live (recording observes the live result via
/// note_image_intrinsic_result).
fn image_intrinsic_replay(src : String) -> (Double, Double)?? {
match image_replay_table.val {
Some(table) =>
match table.get(src) {
Some(recorded) => Some(recorded) // hit (may itself be None = "no size")
None => None // miss: fall back to live
}
None => None
}
}
///|
/// Record the (live) result of an image intrinsic size lookup when recording.
fn note_image_intrinsic_result(
src : String,
result : (Double, Double)?,
) -> Unit {
match image_record_buffer.val {
Some(buffer) =>
match result {
Some((w, h)) =>
buffer.push({ src, has_size: true, width: w, height: h })
None => buffer.push({ src, has_size: false, width: 0.0, height: 0.0 })
}
None => ()
}
}
///|
/// Begin recording image intrinsic sizes. Clears any active image replay.
pub fn start_image_intrinsic_recording() -> Unit {
image_record_buffer.val = Some([])
image_replay_table.val = None
}
///|
pub fn take_image_intrinsic_recording() -> Array[RecordedImage] {
let recorded = match image_record_buffer.val {
Some(buffer) => buffer
None => []
}
image_record_buffer.val = None
recorded
}
///|
pub fn install_image_intrinsic_replay(images : Array[RecordedImage]) -> Unit {
let table : Map[String, (Double, Double)?] = {}
for im in images {
table[im.src] = if im.has_size { Some((im.width, im.height)) } else { None }
}
image_replay_table.val = Some(table)
image_record_buffer.val = None
}
///|
pub fn clear_image_intrinsic_record_replay() -> Unit {
image_record_buffer.val = None
image_replay_table.val = None
}
///|
/// Serialize image recordings (length-prefixed src, so any URL round-trips).
pub fn serialize_image_intrinsic_recording(
images : Array[RecordedImage],
) -> String {
let buf = StringBuilder::new()
buf.write_string(images.length().to_string())
buf.write_char('\n')
for im in images {
buf.write_string(if im.has_size { "1" } else { "0" })
buf.write_char(' ')
buf.write_string(im.width.to_string())
buf.write_char(' ')
buf.write_string(im.height.to_string())
buf.write_char(' ')
buf.write_string(im.src.length().to_string())
buf.write_char(':')
buf.write_string(im.src)
}
buf.to_string()
}
///|
pub fn deserialize_image_intrinsic_recording(
data : String,
) -> Array[RecordedImage] {
let result : Array[RecordedImage] = []
let len = data.length()
let mut i = 0
fn unit_at(idx : Int) -> Int {
data[idx].to_int()
}
let nl = '\n'.to_int()
let sp = ' '.to_int()
let colon = ':'.to_int()
while i < len && unit_at(i) != nl {
i = i + 1
}
i = i + 1
fn read_until(stop : Int) -> String {
let start = i
while i < len && unit_at(i) != stop {
i = i + 1
}
let s = data[start:i].to_string()
i = i + 1
s
}
while i < len {
let has = read_until(sp) == "1"
let w = @string.parse_double(read_until(sp)) catch { _ => 0.0 }
let h = @string.parse_double(read_until(sp)) catch { _ => 0.0 }
let src_len = @string.parse_int(read_until(colon)) catch { _ => 0 }
let src = data[i:i + src_len].to_string()
i = i + src_len
result.push({ src, has_size: has, width: w, height: h })
}
result
}