use super::messages::Pair;
use crate::layout::prim;
use crate::layout::primitives::{PaddingBox, padding};
use crate::layout::{Bbox, PlacedNode};
use crate::resolve::{NodeKind, ResolvedInst, ResolvedValue};
use std::collections::HashMap;
const NEST_INSET: f64 = 6.0;
const TAB_H: f64 = 16.0;
const NOTE_CLEAR: f64 = 0.5;
const MIN_EDGE: f64 = 6.0;
const CHROME_CLEAR: f64 = 32.0;
const KEYWORD_SIZE: f64 = 10.0;
const FRAME_KINDS: &[&str] = &["loop", "opt", "alt"];
pub(super) struct Frame<'a> {
inst: &'a ResolvedInst,
keyword: &'a str,
depth: usize,
elses: Vec<&'a ResolvedInst>,
pad: PaddingBox,
}
pub(super) struct FrameGeom {
top: f64,
bot: f64,
else_ys: Vec<f64>,
}
pub(super) struct Timeline {
pub msg_y: Vec<f64>,
pub note_y: Vec<f64>,
pub foot_y: f64,
pub geom: Vec<FrameGeom>,
}
impl Timeline {
pub(super) fn shift(&mut self, dy: f64) {
for y in self.msg_y.iter_mut().chain(&mut self.note_y) {
*y += dy;
}
self.foot_y += dy;
for g in &mut self.geom {
g.top += dy;
g.bot += dy;
for e in &mut g.else_ys {
*e += dy;
}
}
}
}
pub(super) fn collect(children: &[ResolvedInst]) -> Vec<Frame<'_>> {
let mut out = Vec::new();
gather(children, 0, &mut out);
out
}
fn gather<'a>(children: &'a [ResolvedInst], depth: usize, out: &mut Vec<Frame<'a>>) {
for inst in children {
if let Some(keyword) = keyword_of(inst) {
out.push(Frame {
inst,
keyword,
depth,
elses: inst.children.iter().filter(|c| is_else(c)).collect(),
pad: padding(&inst.attrs, inst.span).unwrap_or_default(),
});
gather(&inst.children, depth + 1, out);
}
}
}
fn keyword_of(inst: &ResolvedInst) -> Option<&str> {
inst.type_chain
.iter()
.map(String::as_str)
.find(|t| FRAME_KINDS.contains(t))
}
fn is_else(inst: &ResolvedInst) -> bool {
inst.type_chain.iter().any(|t| t == "else")
}
fn top_clear(p: &Pair) -> f64 {
if p.is_self() { MIN_EDGE } else { CHROME_CLEAR }
}
fn guard(inst: &ResolvedInst) -> Option<&str> {
inst.children
.iter()
.find(|c| c.kind == NodeKind::Text)
.and_then(|c| c.label.as_deref())
}
pub(super) fn timeline(
pairs: &[Pair],
frames: &[Frame],
notes: &[(usize, f64)],
gap_row: f64,
) -> Timeline {
enum Ev {
Open(usize),
Else(usize, usize),
Note(usize),
Msg(usize),
Close(usize),
}
let mut events: Vec<(usize, u8, Ev)> = Vec::new();
for (f, fr) in frames.iter().enumerate() {
events.push((fr.inst.span.start, 0, Ev::Open(f)));
events.push((fr.inst.span.end, 4, Ev::Close(f)));
for (e, el) in fr.elses.iter().enumerate() {
events.push((el.span.start, 1, Ev::Else(f, e)));
}
}
for (i, p) in pairs.iter().enumerate() {
events.push((p.span().start, 2, Ev::Msg(i)));
}
for (i, (pos, _)) in notes.iter().enumerate() {
events.push((*pos, 2, Ev::Note(i)));
}
events.sort_by_key(|(pos, rank, _)| (*pos, *rank));
let mut y = 0.0;
let mut placed = true; let mut pending: Option<usize> = None; let mut msg_y = vec![0.0; pairs.len()];
let mut note_y = vec![0.0; notes.len()];
let mut geom: Vec<FrameGeom> = frames
.iter()
.map(|f| FrameGeom {
top: 0.0,
bot: 0.0,
else_ys: vec![0.0; f.elses.len()],
})
.collect();
let top_inset = |f: usize, i: usize| frames[f].pad.top.max(top_clear(&pairs[i]));
for (_, _, ev) in &events {
match ev {
Ev::Open(f) => {
if placed {
y += gap_row;
}
geom[*f].top = y;
pending = Some(*f);
placed = false;
}
Ev::Else(f, e) => {
y += frames[*f].pad.bottom.max(MIN_EDGE);
geom[*f].else_ys[*e] = y;
pending = Some(*f);
placed = false;
}
Ev::Msg(i) => {
if let Some(f) = pending.take() {
y += top_inset(f, *i); } else if placed {
y += gap_row; }
msg_y[*i] = y;
y += pairs[*i].hook_drop(); placed = true;
}
Ev::Note(i) => {
if let Some(f) = pending.take() {
y += frames[f].pad.top.max(MIN_EDGE);
} else if placed {
y += gap_row * NOTE_CLEAR;
}
note_y[*i] = y + notes[*i].1 / 2.0;
y += notes[*i].1;
placed = true;
}
Ev::Close(f) => {
if let Some(g) = pending.take() {
y += frames[g].pad.top.max(MIN_EDGE); }
y += frames[*f].pad.bottom.max(MIN_EDGE);
geom[*f].bot = y;
placed = true;
}
}
}
Timeline {
msg_y,
note_y,
foot_y: y + gap_row,
geom,
}
}
pub(super) fn draw(
frames: &[Frame],
geom: &[FrameGeom],
pairs: &[Pair],
lifeline_x: &HashMap<String, f64>,
) -> (Vec<PlacedNode>, Vec<PlacedNode>) {
let (mut behind, mut front) = (Vec::new(), Vec::new());
for (fr, g) in frames.iter().zip(geom) {
let Some((lo, hi)) = lifeline_span(fr, pairs, lifeline_x) else {
continue; };
let nest = fr.depth as f64 * NEST_INSET;
let (left, right) = (
lo - (fr.pad.left - nest).max(MIN_EDGE),
hi + (fr.pad.right - nest).max(MIN_EDGE),
);
behind.push(border(fr.inst, left, g.top, right, g.bot));
front.extend(tab(fr, left, g.top));
let size = text_size(fr.inst);
let color = frame_color(fr.inst);
for (el, &ey) in fr.elses.iter().zip(&g.else_ys) {
behind.push(divider(fr.inst, left, right, ey));
if let Some(text) = guard(el) {
front.push(guard_text(text, left, ey + size, size, color.clone()));
}
}
}
(behind, front)
}
fn lifeline_span(
frame: &Frame,
pairs: &[Pair],
lifeline_x: &HashMap<String, f64>,
) -> Option<(f64, f64)> {
let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
for p in pairs {
let s = p.span();
if frame.inst.span.start <= s.start && s.start <= frame.inst.span.end {
let (a, b) = p.ends();
for id in [a, b] {
if let Some(&x) = lifeline_x.get(id) {
lo = lo.min(x);
hi = hi.max(x);
}
}
if p.is_self()
&& let Some(&x) = lifeline_x.get(a)
{
hi = hi.max(x + p.hook_reach());
}
}
}
(lo <= hi).then_some((lo, hi))
}
fn border(inst: &ResolvedInst, left: f64, top: f64, right: f64, bot: f64) -> PlacedNode {
let (cx, cy) = ((left + right) / 2.0, (top + bot) / 2.0);
let fill = inst
.attrs
.get("fill")
.cloned()
.unwrap_or_else(|| ResolvedValue::Ident("none".into()));
let mut n = prim::rect(cx, cy, right - left, bot - top, fill, 1.0);
prim::outline(
&mut n,
frame_stroke(inst),
inst.attrs.number("stroke-width").unwrap_or(1.0),
);
if let Some(ss) = inst.attrs.get("stroke-style") {
n.attrs.insert("stroke-style", ss.clone());
n.own_style.insert("stroke-style", ss.clone());
}
prim::round(&mut n, inst.attrs.number("radius").unwrap_or(0.0));
n
}
fn text_size(inst: &ResolvedInst) -> f64 {
inst.attrs.number("font-size").unwrap_or(KEYWORD_SIZE)
}
fn frame_stroke(inst: &ResolvedInst) -> ResolvedValue {
inst.attrs
.get("stroke")
.cloned()
.unwrap_or_else(|| super::live("group-stroke"))
}
fn frame_color(inst: &ResolvedInst) -> Option<ResolvedValue> {
inst.attrs.get("color").cloned()
}
fn tab(frame: &Frame, left: f64, top: f64) -> Vec<PlacedNode> {
let size = text_size(frame.inst);
let tab_w = prim::text_width(frame.keyword, size) + 14.0;
let r = frame
.inst
.attrs
.number("radius")
.unwrap_or(0.0)
.min(TAB_H / 2.0);
let cut = (TAB_H * 0.42).min(tab_w / 2.0);
let sw = frame.inst.attrs.number("stroke-width").unwrap_or(1.0);
let hw = sw / 2.0;
let color = frame_color(frame.inst);
let mut banner = prim::path(
tab_path(left + hw, top + hw, tab_w, TAB_H, r, cut),
super::live("fill"),
Bbox {
min_x: left,
min_y: top,
max_x: left + tab_w,
max_y: top + TAB_H,
},
);
prim::outline(&mut banner, frame_stroke(frame.inst), sw);
let mut out = vec![
banner,
prim::text_classed(
frame.keyword,
left + hw + (tab_w - cut / 2.0) / 2.0,
top + hw + TAB_H / 2.0,
size,
"sequence-tab",
),
];
if let Some(g) = guard(frame.inst) {
out.push(guard_text(
g,
left + tab_w + 4.0,
top + TAB_H / 2.0 + 1.0,
size,
color,
));
}
out
}
fn tab_path(l: f64, t: f64, w: f64, h: f64, r: f64, c: f64) -> String {
let (rgt, bot) = (l + w, t + h);
let mut d = format!(
"M {} {} L {rgt} {t} L {rgt} {} L {} {bot} L {l} {bot} ",
l + r,
t,
bot - c,
rgt - c
);
if r > 0.5 {
d.push_str(&format!("L {l} {} A {r} {r} 0 0 1 {} {t} Z", t + r, l + r));
} else {
d.push_str(&format!("L {l} {t} Z"));
}
d
}
fn divider(inst: &ResolvedInst, left: f64, right: f64, y: f64) -> PlacedNode {
let mut line = prim::line(vec![(left, y), (right, y)], frame_stroke(inst), 1.0);
let dashed = ResolvedValue::Ident("dashed".into());
line.attrs.insert("stroke-style", dashed.clone());
line.own_style.insert("stroke-style", dashed);
line
}
fn guard_text(text: &str, x: f64, cy: f64, size: f64, color: Option<ResolvedValue>) -> PlacedNode {
let label = format!("[{text}]");
let cx = x + prim::text_width(&label, size) / 2.0;
let mut n = prim::text_classed(&label, cx, cy, size, "sequence-guard");
if let Some(c) = color {
n.attrs.insert("color", c.clone());
n.own_style.insert("color", c);
}
n
}