use super::{
LABEL_SIZE, LINE, SUBLABEL_SIZE, TAG_HEIGHT, TAG_SIZE, model::Model, track::tab_width,
};
use crate::{
diagram::{self, arrowhead, cylinder, with_shadow, wrap},
error::ChartWarning,
layout::fit_text,
metrics::TextMetrics,
scene::{Circle, Element, Hotspot, Line, Polyline, Rect, Text, TextAnchor},
spec::{FragmentSpec, MessageKind, ParticipantKind},
};
pub(super) fn message_tooltip(model: &Model, index: usize) -> String {
let message = &model.sequence.messages[index];
let (from, to) = model.ends[index];
let participants = &model.sequence.participants;
format!(
"{}. {} → {}: {}",
index + 1,
participants[from].label,
participants[to].label,
message.label
)
}
pub(super) fn polyline(
points: Vec<(f64, f64)>,
kind: MessageKind,
tooltip: Option<String>,
) -> Polyline {
let class = if kind == MessageKind::Reply {
"chartlet-seq-message chartlet-seq-reply"
} else {
"chartlet-seq-message"
};
diagram::polyline(points, class, tooltip)
}
pub(super) fn head(tip: (f64, f64), direction: (f64, f64), kind: MessageKind) -> Polyline {
if kind == MessageKind::Call {
arrowhead(tip, direction, true, "chartlet-seq-head")
} else {
arrowhead(tip, direction, false, "chartlet-seq-head-open")
}
}
pub(super) fn bar(x: f64, y: f64, width: f64, height: f64) -> Element {
Element::Rect(Rect {
x,
y,
width,
height,
class: "chartlet-seq-activation",
series_index: None,
style_index: None,
tooltip: None,
})
}
pub(super) struct Frame {
pub(super) left: f64,
pub(super) top: f64,
pub(super) right: f64,
pub(super) bottom: f64,
pub(super) label_end: f64,
}
impl Frame {
pub(super) fn draw(
&self,
index: usize,
fragment: &FragmentSpec,
metrics: &impl TextMetrics,
warnings: &mut Vec<ChartWarning>,
elements: &mut Vec<Element>,
) {
elements.push(Element::Rect(Rect {
x: self.left,
y: self.top,
width: self.right - self.left,
height: self.bottom - self.top,
class: "chartlet-seq-frame",
series_index: None,
style_index: None,
tooltip: None,
}));
let keyword = fragment.kind.keyword();
let tab = tab_width(fragment, metrics);
elements.push(Element::Rect(Rect {
x: self.left,
y: self.top,
width: tab,
height: TAG_HEIGHT,
class: "chartlet-seq-tab",
series_index: None,
style_index: None,
tooltip: None,
}));
elements.push(Element::Text(Text {
x: self.left + 6.0,
y: self.top + 12.0,
class: "chartlet-seq-tag",
anchor: TextAnchor::Start,
content: keyword.to_owned(),
}));
if let Some(label) = &fragment.label {
let room = self.label_end - self.left - tab - 12.0;
let path = format!("/sequence/fragments/{index}/label");
let content = fit_text(
&format!("[{label}]"),
room,
TAG_SIZE,
metrics,
warnings,
&path,
);
elements.push(Element::Text(Text {
x: self.left + tab + 6.0,
y: self.top + 12.0,
class: "chartlet-seq-guard",
anchor: TextAnchor::Start,
content,
}));
}
}
}
pub(super) fn guard(
(x, y): (f64, f64),
label: &str,
room: f64,
metrics: &impl TextMetrics,
warnings: &mut Vec<ChartWarning>,
path: &str,
) -> Element {
Element::Text(Text {
x,
y,
class: "chartlet-seq-guard",
anchor: TextAnchor::Start,
content: fit_text(
&format!("[{label}]"),
room,
TAG_SIZE,
metrics,
warnings,
path,
),
})
}
pub(super) fn focusable_participant(
model: &Model,
header: &Header,
metrics: &impl TextMetrics,
warnings: &mut Vec<ChartWarning>,
elements: &mut Vec<Element>,
) {
let participant = &model.sequence.participants[header.index];
elements.push(Element::NodeGroup(
model.participant_classes(header.index),
participant.id.clone(),
));
draw_participant(header, participant, metrics, warnings, elements);
elements.push(Element::GroupEnd);
elements.push(Element::Hotspot(Hotspot {
node: header.index,
label: participant.label.clone(),
x: header.x,
y: header.y,
width: header.width,
height: header.height,
}));
}
pub(super) struct Header {
pub(super) index: usize,
pub(super) x: f64,
pub(super) y: f64,
pub(super) width: f64,
pub(super) height: f64,
pub(super) figure: (f64, f64),
pub(super) wrap: bool,
pub(super) pad: f64,
}
fn draw_participant(
header: &Header,
participant: &crate::spec::ParticipantSpec,
metrics: &impl TextMetrics,
warnings: &mut Vec<ChartWarning>,
elements: &mut Vec<Element>,
) {
let Header {
x,
y,
width,
height,
figure: (figure_x, figure_y),
..
} = *header;
let tooltip = Some(match &participant.sublabel {
Some(sublabel) => format!("{} – {sublabel}", participant.label),
None => participant.label.clone(),
});
let rect = |x: f64, y: f64, class: &'static str, tooltip: Option<String>| {
Element::Rect(Rect {
x,
y,
width,
height,
class,
series_index: None,
style_index: None,
tooltip,
})
};
match participant.kind {
ParticipantKind::Service => with_shadow(
rect(x, y, "chartlet-seq-box chartlet-role-blue", tooltip),
elements,
),
ParticipantKind::External => with_shadow(
rect(
x,
y,
"chartlet-seq-box chartlet-seq-external chartlet-role-gray",
tooltip,
),
elements,
),
ParticipantKind::Queue => {
with_shadow(
rect(
x + 4.0,
y - 4.0,
"chartlet-seq-box chartlet-role-violet",
None,
),
elements,
);
with_shadow(
rect(x, y, "chartlet-seq-box chartlet-role-violet", tooltip),
elements,
);
}
ParticipantKind::Database => cylinder(
(x, y, width, height),
(
"chartlet-seq-box chartlet-role-teal",
"chartlet-diagram-rim",
),
tooltip,
elements,
),
ParticipantKind::Actor => {
figure(figure_x, figure_y, elements);
elements.push(rect(x, y, "chartlet-seq-hit", tooltip));
}
}
participant_text(header, participant, metrics, warnings, elements);
}
fn participant_text(
header: &Header,
participant: &crate::spec::ParticipantSpec,
metrics: &impl TextMetrics,
warnings: &mut Vec<ChartWarning>,
elements: &mut Vec<Element>,
) {
let Header {
index,
x,
y,
width,
height,
pad,
..
} = *header;
let center = x + width / 2.0;
let path = format!("/sequence/participants/{index}");
let room = width - 2.0 * pad;
let label_path = format!("{path}/label");
let lines = if header.wrap {
wrap(
&participant.label,
room,
LABEL_SIZE,
metrics,
warnings,
&label_path,
)
} else {
vec![fit_text(
&participant.label,
room,
LABEL_SIZE,
metrics,
warnings,
&label_path,
)]
};
let lift = LINE * crate::layout::count(lines.len() - 1) / 2.0;
let (label_y, sublabel_y) = match participant.sublabel {
Some(_) => (
y + height / 2.0 - 2.0 - lift,
y + height / 2.0 + 13.0 + lift,
),
None => (y + height / 2.0 + 4.5 - lift, 0.0),
};
for (row, line) in lines.into_iter().enumerate() {
elements.push(Element::Text(Text {
x: center,
y: label_y + LINE * crate::layout::count(row),
class: "chartlet-seq-label",
anchor: TextAnchor::Middle,
content: line,
}));
}
if let Some(sublabel) = &participant.sublabel {
elements.push(Element::Text(Text {
x: center,
y: sublabel_y,
class: "chartlet-seq-sublabel",
anchor: TextAnchor::Middle,
content: fit_text(
sublabel,
room,
SUBLABEL_SIZE,
metrics,
warnings,
&format!("{path}/sublabel"),
),
}));
}
}
fn figure(center: f64, top: f64, elements: &mut Vec<Element>) {
elements.push(Element::Circle(Circle {
cx: center,
cy: top + 5.5,
radius: 4.5,
class: "chartlet-seq-actor-head",
series_index: None,
style_index: None,
tooltip: None,
}));
for (x1, y1, x2, y2) in [
(0.0, 10.0, 0.0, 17.0),
(-7.0, 12.5, 7.0, 12.5),
(0.0, 17.0, -6.0, 23.0),
(0.0, 17.0, 6.0, 23.0),
] {
elements.push(Element::Line(Line {
x1: center + x1,
y1: top + y1,
x2: center + x2,
y2: top + y2,
class: "chartlet-seq-actor",
}));
}
}