use std::{collections::HashMap, rc::Rc};
use orrery_core::{
draw::{
ActivationBox as DrawActivationBox, ActivationBoxDefinition, Fragment as DrawFragment,
FragmentSection as DrawFragmentSection, Lifeline, Note, PositionedArrowWithText,
PositionedDrawable,
},
geometry::{Bounds, Point, Size},
identifier::Id,
semantic::{Fragment, FragmentSection},
};
use crate::layout::{component::Component, positioning::LayoutBounds};
#[derive(Debug, Clone)]
pub struct Participant<'a> {
component: Component<'a>,
lifeline: PositionedDrawable<Lifeline>,
}
impl<'a> Participant<'a> {
pub fn new(component: Component<'a>, lifeline: PositionedDrawable<Lifeline>) -> Self {
Self {
component,
lifeline,
}
}
pub fn component(&self) -> &Component<'_> {
&self.component
}
pub fn lifeline(&self) -> &PositionedDrawable<Lifeline> {
&self.lifeline
}
}
#[derive(Debug, Clone)]
pub struct ActivationBox {
center_y: f32,
participant_x: f32,
drawable: DrawActivationBox,
}
#[derive(Debug, Clone)]
pub struct ActivationTiming {
participant_x: f32,
start_y: f32,
nesting_level: u32,
definition: Rc<ActivationBoxDefinition>,
}
impl ActivationTiming {
pub fn new(
participant_x: f32,
start_y: f32,
nesting_level: u32,
definition: Rc<ActivationBoxDefinition>,
) -> Self {
Self {
participant_x,
start_y,
nesting_level,
definition,
}
}
pub fn to_activation_box(&self, end_y: f32) -> ActivationBox {
const EDGE_CASE_BUFFER: f32 = 15.0;
let end_y = if end_y <= self.start_y {
self.start_y + EDGE_CASE_BUFFER
} else {
end_y
};
let center_y = (self.start_y() + end_y) / 2.0;
let height = end_y - self.start_y();
let drawable =
DrawActivationBox::new(Rc::clone(&self.definition), height, self.nesting_level());
ActivationBox {
participant_x: self.participant_x,
center_y,
drawable,
}
}
fn start_y(&self) -> f32 {
self.start_y
}
fn nesting_level(&self) -> u32 {
self.nesting_level
}
}
impl ActivationBox {
pub fn participant_x(&self) -> f32 {
self.participant_x
}
pub fn center_y(&self) -> f32 {
self.center_y
}
pub fn drawable(&self) -> &DrawActivationBox {
&self.drawable
}
pub fn intersection_x(&self, participant_position: Point, target_x: f32) -> f32 {
let bounds = self.calculate_bounds(participant_position);
if target_x > participant_position.x() {
bounds.max_x()
} else {
bounds.min_x()
}
}
fn calculate_bounds(&self, participant_position: Point) -> Bounds {
let position_with_center_y = participant_position.with_y(self.center_y);
self.drawable.calculate_bounds(position_with_center_y)
}
}
pub struct FragmentTiming<'a> {
start_y: f32,
min_x: f32,
max_x: f32,
fragment: &'a Fragment,
active_section: Option<(&'a FragmentSection, f32)>,
sections: Vec<DrawFragmentSection>,
}
impl<'a> FragmentTiming<'a> {
pub fn new(fragment: &'a Fragment, start_y: f32) -> Self {
Self {
start_y,
min_x: f32::MAX,
max_x: f32::MIN,
fragment,
active_section: None,
sections: Vec::new(),
}
}
pub fn start_section(&mut self, section: &'a FragmentSection, start_y: f32) {
#[cfg(debug_assertions)]
assert!(self.active_section.is_none());
self.active_section = Some((section, start_y));
}
pub fn end_section(&mut self, end_y: f32) -> Result<(), &'static str> {
let (ast_section, start_y) = self
.active_section
.take()
.ok_or("There is no active fragment section")?;
let section = DrawFragmentSection::new(
ast_section.title().map(|title| title.to_string()),
end_y - start_y,
);
self.sections.push(section);
Ok(())
}
pub fn update_x(&mut self, source_x: f32, target_x: f32) {
self.min_x = self.min_x.min(source_x.min(target_x));
self.max_x = self.max_x.max(source_x.max(target_x));
}
pub fn section_header_height(&self, section: &FragmentSection) -> f32 {
let definition = self.fragment.definition();
let section_header_height = definition.section_header_size(section.title()).height();
if self.sections.is_empty() {
let fragment_header_height = definition.header_size(self.fragment.operation()).height();
section_header_height.max(fragment_header_height)
} else {
section_header_height
}
}
pub fn bottom_padding(&self) -> f32 {
self.fragment.definition().bottom_padding()
}
pub fn into_fragment(self, end_y: f32) -> PositionedDrawable<DrawFragment> {
#[cfg(debug_assertions)]
assert!(self.active_section.is_none());
let drawable = DrawFragment::new(
Rc::clone(self.fragment.definition()),
self.fragment.operation().to_string(),
self.sections,
Size::new(self.max_x - self.min_x, end_y - self.start_y),
);
let center_x = (self.min_x + self.max_x) / 2.0;
let center_y = (self.start_y + end_y) / 2.0;
let position = Point::new(center_x, center_y);
PositionedDrawable::new(drawable).with_position(position)
}
}
#[derive(Debug, Clone)]
pub struct Layout<'a> {
participants: HashMap<Id, Participant<'a>>,
messages: Vec<PositionedArrowWithText<'a>>,
activations: Vec<ActivationBox>,
fragments: Vec<PositionedDrawable<DrawFragment>>,
notes: Vec<PositionedDrawable<Note>>,
max_lifeline_end: f32, bounds: Bounds,
}
impl<'a> Layout<'a> {
pub fn new(
participants: HashMap<Id, Participant<'a>>,
messages: Vec<PositionedArrowWithText<'a>>,
activations: Vec<ActivationBox>,
fragments: Vec<PositionedDrawable<DrawFragment>>,
notes: Vec<PositionedDrawable<Note>>,
max_lifeline_end: f32,
) -> Self {
let bounds = participants
.values()
.map(|participant| participant.component().bounds())
.reduce(|acc, bounds| acc.merge(&bounds))
.unwrap_or_default()
.with_max_y(max_lifeline_end);
Self {
participants,
messages,
activations,
fragments,
notes,
max_lifeline_end,
bounds,
}
}
pub fn participants(&self) -> &HashMap<Id, Participant<'a>> {
&self.participants
}
pub fn messages(&self) -> &[PositionedArrowWithText<'a>] {
&self.messages
}
pub fn activations(&self) -> &[ActivationBox] {
&self.activations
}
pub fn fragments(&self) -> &[PositionedDrawable<DrawFragment>] {
&self.fragments
}
pub fn notes(&self) -> &[PositionedDrawable<Note>] {
&self.notes
}
pub fn max_lifeline_end(&self) -> f32 {
self.max_lifeline_end
}
}
impl<'a> LayoutBounds for Layout<'a> {
fn layout_bounds(&self) -> Bounds {
self.bounds
}
}
#[cfg(test)]
mod tests {
use orrery_core::draw::{Drawable, FragmentDefinition};
use super::*;
#[test]
fn test_activation_box_get_intersection_x() {
let definition = ActivationBoxDefinition::default();
let drawable = DrawActivationBox::new(Rc::new(definition), 20.0, 0);
let activation_box = ActivationBox {
center_y: 100.0,
participant_x: 50.0,
drawable,
};
let participant_position = Point::new(50.0, 80.0);
let rightward_x = activation_box.intersection_x(participant_position, 60.0);
assert_eq!(rightward_x, 54.0);
let leftward_x = activation_box.intersection_x(participant_position, 40.0);
assert_eq!(leftward_x, 46.0); }
#[test]
fn test_activation_box_nesting_offset() {
let definition = ActivationBoxDefinition::default();
let drawable = DrawActivationBox::new(Rc::new(definition), 20.0, 2);
let activation_box = ActivationBox {
center_y: 100.0,
participant_x: 50.0,
drawable,
};
let participant_position = Point::new(50.0, 80.0);
let bounds = activation_box.calculate_bounds(participant_position);
assert_eq!(bounds.min_x(), 54.0);
assert_eq!(bounds.max_x(), 62.0);
}
#[test]
fn test_fragment_timing_lifecycle() {
let fragment_def = Rc::new(FragmentDefinition::default());
let section1 = FragmentSection::new(Some("section 1".to_string()), vec![]);
let section2 = FragmentSection::new(Some("section 2".to_string()), vec![]);
let fragment = Fragment::new("alt".to_string(), vec![section1, section2], fragment_def);
let start_y = 100.0;
let mut fragment_timing = FragmentTiming::new(&fragment, start_y);
fragment_timing.start_section(&fragment.sections()[0], 120.0);
let result = fragment_timing.end_section(180.0);
assert!(result.is_ok());
fragment_timing.start_section(&fragment.sections()[1], 180.0);
let result = fragment_timing.end_section(240.0);
assert!(result.is_ok());
fragment_timing.update_x(50.0, 200.0);
let end_y = 250.0;
let final_fragment = fragment_timing.into_fragment(end_y);
assert!(final_fragment.inner().size().height() > 0.0);
assert!(final_fragment.inner().size().width() > 0.0);
}
#[test]
fn test_fragment_timing_bounds_tracking() {
let fragment_def = Rc::new(FragmentDefinition::default());
let fragment = Fragment::new("opt".to_string(), vec![], fragment_def);
let mut fragment_timing = FragmentTiming::new(&fragment, 100.0);
assert_eq!(fragment_timing.min_x, f32::MAX);
assert_eq!(fragment_timing.max_x, f32::MIN);
fragment_timing.update_x(50.0, 150.0);
assert_eq!(fragment_timing.min_x, 50.0);
assert_eq!(fragment_timing.max_x, 150.0);
fragment_timing.update_x(30.0, 100.0);
assert_eq!(fragment_timing.min_x, 30.0);
assert_eq!(fragment_timing.max_x, 150.0);
fragment_timing.update_x(60.0, 200.0);
assert_eq!(fragment_timing.min_x, 30.0); assert_eq!(fragment_timing.max_x, 200.0);
fragment_timing.update_x(40.0, 180.0);
assert_eq!(fragment_timing.min_x, 30.0);
assert_eq!(fragment_timing.max_x, 200.0);
}
#[test]
fn test_section_header_height_first_section_header_dominates() {
let fragment_def = Rc::new(FragmentDefinition::default());
let section = FragmentSection::new(Some("x".to_string()), vec![]);
let fragment = Fragment::new("alt".to_string(), vec![section], fragment_def.clone());
let fragment_timing = FragmentTiming::new(&fragment, 0.0);
let height = fragment_timing.section_header_height(&fragment.sections()[0]);
let header_height = fragment_def.header_size("alt").height();
assert_eq!(height, header_height);
}
#[test]
fn test_section_header_height_first_section_title_dominates() {
let fragment_def = Rc::new(FragmentDefinition::default());
let tall_title = "line1\nline2\nline3\nline4\nline5";
let section = FragmentSection::new(Some(tall_title.to_string()), vec![]);
let fragment = Fragment::new("alt".to_string(), vec![section], fragment_def.clone());
let fragment_timing = FragmentTiming::new(&fragment, 0.0);
let height = fragment_timing.section_header_height(&fragment.sections()[0]);
let title_height = fragment_def.section_header_size(Some(tall_title)).height();
assert_eq!(height, title_height);
}
#[test]
fn test_section_header_height_subsequent_section() {
let fragment_def = Rc::new(FragmentDefinition::default());
let section1 = FragmentSection::new(Some("guard1".to_string()), vec![]);
let section2 = FragmentSection::new(Some("guard2".to_string()), vec![]);
let fragment = Fragment::new(
"alt".to_string(),
vec![section1, section2],
fragment_def.clone(),
);
let mut fragment_timing = FragmentTiming::new(&fragment, 0.0);
fragment_timing.start_section(&fragment.sections()[0], 0.0);
fragment_timing.end_section(50.0).unwrap();
let height = fragment_timing.section_header_height(&fragment.sections()[1]);
let title_height = fragment_def.section_header_size(Some("guard2")).height();
assert_eq!(height, title_height);
}
#[test]
fn test_section_header_height_no_title() {
let fragment_def = Rc::new(FragmentDefinition::default());
let section = FragmentSection::new(None, vec![]);
let fragment = Fragment::new("opt".to_string(), vec![section], fragment_def.clone());
let mut fragment_timing = FragmentTiming::new(&fragment, 0.0);
fragment_timing.start_section(&fragment.sections()[0], 0.0);
fragment_timing.end_section(50.0).unwrap();
let no_title_section = FragmentSection::new(None, vec![]);
let height = fragment_timing.section_header_height(&no_title_section);
assert_eq!(height, 0.0);
}
#[test]
fn test_bottom_padding_delegates_to_definition() {
let mut fragment_def = FragmentDefinition::default();
fragment_def.set_bounds_padding(orrery_core::geometry::Insets::new(5.0, 10.0, 15.0, 20.0));
let fragment_def = Rc::new(fragment_def);
let fragment = Fragment::new("loop".to_string(), vec![], fragment_def);
let fragment_timing = FragmentTiming::new(&fragment, 0.0);
assert_eq!(fragment_timing.bottom_padding(), 15.0);
}
}