use super::block_bounds::SequenceBlockBounds;
use super::constants::sequence_actor_popup_panel_height;
use super::metrics::{SequenceMathHeightMode, measure_sequence_label_for_layout};
use crate::math::MathRenderer;
use crate::model::{Bounds, LayoutEdge, LayoutNode};
use crate::text::{TextMeasurer, TextStyle};
use merman_core::MermaidConfig;
use merman_core::diagrams::sequence::SequenceDiagramRenderModel;
use std::collections::HashMap;
pub(super) struct SequenceRootBoundsContext<'a> {
pub(super) model: &'a SequenceDiagramRenderModel,
pub(super) diagram_title: Option<&'a str>,
pub(super) nodes: &'a [LayoutNode],
pub(super) edges: &'a [LayoutEdge],
pub(super) block_bounds: Option<SequenceBlockBounds>,
pub(super) actor_index: &'a HashMap<&'a str, usize>,
pub(super) actor_centers_x: &'a [f64],
pub(super) actor_left_x: &'a [f64],
pub(super) actor_widths: &'a [f64],
pub(super) actor_box: &'a [Option<usize>],
pub(super) box_margins: &'a [f64],
pub(super) actor_width_min: f64,
pub(super) actor_height: f64,
pub(super) bottom_box_top_y: f64,
pub(super) diagram_margin_x: f64,
pub(super) diagram_margin_y: f64,
pub(super) bottom_margin_adj: f64,
pub(super) box_margin: f64,
pub(super) has_boxes: bool,
pub(super) mirror_actors: bool,
pub(super) measurer: &'a dyn TextMeasurer,
pub(super) msg_text_style: &'a TextStyle,
pub(super) math_config: &'a MermaidConfig,
pub(super) math_renderer: Option<&'a (dyn MathRenderer + Send + Sync)>,
}
pub(super) fn sequence_root_bounds(ctx: SequenceRootBoundsContext<'_>) -> Bounds {
let mut content = sequence_content_bounds(&ctx);
if let Some(block_bounds) = ctx.block_bounds {
content.include_bounds(block_bounds.min_x, block_bounds.max_x, block_bounds.max_y);
}
include_actor_popup_bottoms(&mut content, &ctx);
let extra_vert_for_title =
if super::sequence_render_title(ctx.model.title.as_deref(), ctx.diagram_title).is_some() {
40.0
} else {
0.0
};
let vb_min_y = -(ctx.diagram_margin_y + extra_vert_for_title);
let mut bounds_box_stopy = if ctx.mirror_actors {
content.max_y + ctx.bottom_margin_adj
} else {
content.max_y
}
.max(0.0);
if ctx.has_boxes {
bounds_box_stopy += ctx.box_margin;
}
let mut bounds_box = ActorHorizontalBounds::from_content(content.min_x, content.max_x);
bounds_box.include_actor_boxes(&ctx);
include_self_message_bounds(&mut bounds_box, &ctx);
Bounds {
min_x: bounds_box.start_x - ctx.diagram_margin_x,
min_y: vb_min_y,
max_x: bounds_box.stop_x + ctx.diagram_margin_x,
max_y: bounds_box_stopy + ctx.diagram_margin_y,
}
}
fn sequence_content_bounds(ctx: &SequenceRootBoundsContext<'_>) -> ContentBounds {
let mut content = ContentBounds::new();
for n in ctx.nodes {
let left = n.x - n.width / 2.0;
let right = n.x + n.width / 2.0;
let bottom = n.y + n.height / 2.0;
content.include_x(left, right);
if ctx.mirror_actors || !n.id.starts_with("actor-bottom-") {
content.include_y(bottom);
}
}
if !ctx.mirror_actors {
for e in ctx.edges {
if e.id.starts_with("lifeline-") {
continue;
}
for p in &e.points {
content.include_y(p.y);
}
if let Some(label) = e.label.as_ref() {
content.include_y(label.y + label.height / 2.0);
}
}
}
content.or_fallback(
ctx.actor_width_min.max(1.0),
(ctx.bottom_box_top_y + ctx.actor_height).max(1.0),
)
}
fn include_actor_popup_bottoms(content: &mut ContentBounds, ctx: &SequenceRootBoundsContext<'_>) {
for actor_id in &ctx.model.actor_order {
let Some(actor) = ctx.model.actors.get(actor_id) else {
continue;
};
if actor.links.is_empty() {
continue;
}
let popup_bottom = ctx.actor_height + sequence_actor_popup_panel_height(actor.links.len());
let popup_content_bottom = if ctx.mirror_actors {
popup_bottom - ctx.diagram_margin_y - if ctx.has_boxes { ctx.box_margin } else { 0.0 }
} else {
popup_bottom
};
content.include_y(popup_content_bottom.max(0.0));
}
}
fn include_self_message_bounds(
bounds_box: &mut ActorHorizontalBounds,
ctx: &SequenceRootBoundsContext<'_>,
) {
for msg in &ctx.model.messages {
let (Some(from), Some(to)) = (msg.from.as_deref(), msg.to.as_deref()) else {
continue;
};
if from != to {
continue;
}
if msg.message_type == 2 {
continue;
}
let Some(&i) = ctx.actor_index.get(from) else {
continue;
};
let center_x = ctx.actor_centers_x[i] + 1.0;
let text = msg.message_text();
let (text_w, _text_h) = if text.is_empty() {
(1.0, 1.0)
} else {
measure_sequence_label_for_layout(
ctx.measurer,
text,
ctx.msg_text_style,
ctx.math_config,
ctx.math_renderer,
SequenceMathHeightMode::Bound,
)
};
let dx = (text_w.max(1.0) / 2.0).max(ctx.actor_width_min / 2.0);
bounds_box.include(center_x - dx, center_x + dx);
}
}
#[derive(Clone, Copy)]
struct ContentBounds {
min_x: f64,
max_x: f64,
max_y: f64,
}
impl ContentBounds {
fn new() -> Self {
Self {
min_x: f64::INFINITY,
max_x: f64::NEG_INFINITY,
max_y: f64::NEG_INFINITY,
}
}
fn include_x(&mut self, left: f64, right: f64) {
self.min_x = self.min_x.min(left);
self.max_x = self.max_x.max(right);
}
fn include_y(&mut self, y: f64) {
self.max_y = self.max_y.max(y);
}
fn include_bounds(&mut self, min_x: f64, max_x: f64, max_y: f64) {
self.include_x(min_x, max_x);
self.include_y(max_y);
}
fn or_fallback(mut self, fallback_width: f64, fallback_max_y: f64) -> Self {
if !self.min_x.is_finite() {
self.min_x = 0.0;
self.max_x = fallback_width;
self.max_y = fallback_max_y;
}
self
}
}
struct ActorHorizontalBounds {
start_x: f64,
stop_x: f64,
}
impl ActorHorizontalBounds {
fn from_content(min_x: f64, max_x: f64) -> Self {
Self {
start_x: min_x,
stop_x: max_x,
}
}
fn include_actor_boxes(&mut self, ctx: &SequenceRootBoundsContext<'_>) {
for i in 0..ctx.model.actor_order.len() {
let left = ctx.actor_left_x[i];
let right = left + ctx.actor_widths[i];
if let Some(bi) = ctx.actor_box[i] {
let m = ctx.box_margins[bi];
self.include(left - m, right + m);
} else {
self.include(left, right);
}
}
}
fn include(&mut self, left: f64, right: f64) {
self.start_x = self.start_x.min(left);
self.stop_x = self.stop_x.max(right);
}
}