use std::rc::Rc;
use svg::{self, node::element as svg_element};
use crate::{
color::Color,
draw::{Drawable, LayeredOutput, RenderLayer, StrokeDefinition, Text, TextDefinition},
geometry::{Bounds, Insets, Point, Size},
};
#[cfg(test)]
use crate::draw::StrokeStyle;
#[derive(Debug, Clone)]
pub struct FragmentDefinition {
border_stroke: Rc<StrokeDefinition>,
background_color: Option<Color>,
operation_label_text_definition: Rc<TextDefinition>,
section_title_text_definition: Rc<TextDefinition>,
separator_stroke: Rc<StrokeDefinition>,
pentagon_fill_color: Color,
bounds_padding: Insets,
}
impl FragmentDefinition {
pub fn new() -> Self {
Self::default()
}
pub fn set_background_color(&mut self, color: Option<Color>) {
self.background_color = color;
}
pub fn set_operation_label_text(&mut self, text: Rc<TextDefinition>) {
self.operation_label_text_definition = text;
}
pub fn set_section_title_text(&mut self, text: Rc<TextDefinition>) {
self.section_title_text_definition = text;
}
pub fn set_border_stroke(&mut self, stroke: Rc<StrokeDefinition>) {
self.border_stroke = stroke;
}
pub fn set_separator_stroke(&mut self, stroke: Rc<StrokeDefinition>) {
self.separator_stroke = stroke;
}
pub fn set_bounds_padding(&mut self, padding: Insets) {
self.bounds_padding = padding;
}
pub fn set_pentagon_fill_color(&mut self, color: Color) {
self.pentagon_fill_color = color;
}
pub fn border_stroke(&self) -> &Rc<StrokeDefinition> {
&self.border_stroke
}
fn background_color(&self) -> Option<&Color> {
self.background_color.as_ref()
}
pub fn separator_stroke(&self) -> &Rc<StrokeDefinition> {
&self.separator_stroke
}
pub fn operation_label_text(&self) -> &Rc<TextDefinition> {
&self.operation_label_text_definition
}
pub fn section_title_text(&self) -> &Rc<TextDefinition> {
&self.section_title_text_definition
}
fn bounds_padding(&self) -> Insets {
self.bounds_padding
}
pub fn pentagon_fill_color(&self) -> &Color {
&self.pentagon_fill_color
}
pub fn header_size(&self, operation: &str) -> Size {
let text = Text::new(&self.operation_label_text_definition, operation);
let text_size = text.calculate_size();
let triangle_width = Self::triangle_width(text_size);
Size::new(text_size.width() + triangle_width, text_size.height())
}
pub fn section_header_size(&self, title: Option<&str>) -> Size {
match title {
Some(title) => {
let formatted = format!("[{}]", title);
let text = Text::new(&self.section_title_text_definition, &formatted);
text.calculate_size()
}
None => Size::zero(),
}
}
pub fn bottom_padding(&self) -> f32 {
self.bounds_padding.bottom()
}
fn triangle_width(content_size: Size) -> f32 {
let height = content_size.height();
10.0f32.min(height / 2.0).max(height / 4.0)
}
fn create_pentagon_path(&self, content_bounds: Bounds) -> (svg_element::Path, Bounds) {
let top_left = content_bounds.min_point();
let triangle_width = Self::triangle_width(content_bounds.to_size());
let path_data = format!(
"M {} {} L {} {} L {} {} L {} {}",
content_bounds.max_x() + triangle_width,
content_bounds.min_y(),
content_bounds.max_x() + triangle_width,
content_bounds.max_y() - triangle_width,
content_bounds.max_x(),
content_bounds.max_y(),
content_bounds.min_x(),
content_bounds.max_y()
);
let path = svg_element::Path::new()
.set("d", path_data)
.set("fill", self.pentagon_fill_color.to_string())
.set("fill-opacity", self.pentagon_fill_color.alpha());
let path = crate::apply_stroke!(path, &self.border_stroke);
let pentagon_size = Size::new(
content_bounds.width() + triangle_width,
content_bounds.height(),
);
let pentagon_bounds = Bounds::new_from_top_left(top_left, pentagon_size);
(path, pentagon_bounds)
}
}
impl Default for FragmentDefinition {
fn default() -> Self {
let mut operation_label_text_definition = TextDefinition::new();
operation_label_text_definition.set_font_size(12);
operation_label_text_definition.set_color(Some(Color::default()));
operation_label_text_definition.set_padding(Insets::new(4.0, 8.0, 4.0, 8.0));
let mut section_title_text_definition = TextDefinition::new();
section_title_text_definition.set_font_size(11);
section_title_text_definition
.set_color(Some(Color::new("#666666").expect("Invalid color")));
section_title_text_definition.set_padding(Insets::new(2.0, 4.0, 2.0, 20.0));
Self {
border_stroke: Rc::new(StrokeDefinition::default()),
background_color: None,
operation_label_text_definition: Rc::new(operation_label_text_definition),
section_title_text_definition: Rc::new(section_title_text_definition),
separator_stroke: Rc::new(StrokeDefinition::default_dashed()),
pentagon_fill_color: Color::new("white").expect("Invalid color"),
bounds_padding: Insets::new(0.0, 20.0, 0.0, 20.0),
}
}
}
#[derive(Debug, Clone)]
pub struct FragmentSection {
title: Option<String>, height: f32,
}
impl FragmentSection {
pub fn new(title: Option<String>, height: f32) -> Self {
Self { title, height }
}
pub fn title(&self) -> Option<&str> {
self.title.as_deref()
}
pub fn height(&self) -> f32 {
self.height
}
}
#[derive(Debug, Clone)]
pub struct Fragment {
definition: Rc<FragmentDefinition>,
operation: String,
sections: Vec<FragmentSection>,
size: Size,
}
impl Fragment {
pub fn new(
definition: Rc<FragmentDefinition>,
operation: String,
sections: Vec<FragmentSection>,
size: Size,
) -> Self {
Self {
definition,
operation,
sections,
size,
}
}
fn operation(&self) -> &str {
&self.operation
}
fn sections(&self) -> &[FragmentSection] {
&self.sections
}
fn size(&self) -> Size {
self.size
}
}
impl Drawable for Fragment {
fn render_to_layers(&self, position: Point) -> LayeredOutput {
let mut output = LayeredOutput::new();
let bounds_padding = self.definition.bounds_padding();
let expanded_size = self.size().add_padding(bounds_padding);
let bounds = position.to_bounds(expanded_size);
let top_left = bounds.min_point();
if let Some(bg_color) = self.definition.background_color() {
let background = svg_element::Rectangle::new()
.set("x", top_left.x())
.set("y", top_left.y())
.set("width", bounds.width())
.set("height", bounds.height())
.set("fill", bg_color.to_string())
.set("fill-opacity", bg_color.alpha());
output.add_to_layer(RenderLayer::Fragment, Box::new(background));
}
let border = svg_element::Rectangle::new()
.set("x", top_left.x())
.set("y", top_left.y())
.set("width", bounds.width())
.set("height", bounds.height())
.set("fill", "none");
let border = crate::apply_stroke!(border, self.definition.border_stroke());
output.add_to_layer(RenderLayer::Fragment, Box::new(border));
let operation_text = Text::new(
&self.definition.operation_label_text_definition,
self.operation(),
);
let pentagon_content_size = operation_text.size();
let pentagon_content_bounds = Bounds::new_from_top_left(top_left, pentagon_content_size);
let (pentagon, pentagon_bounds) = self
.definition
.create_pentagon_path(pentagon_content_bounds);
output.add_to_layer(RenderLayer::Fragment, Box::new(pentagon));
let op_text_output = operation_text.render_to_layers(pentagon_content_bounds.center());
output.merge(op_text_output);
let mut current_y = top_left.y();
for (i, section) in self.sections().iter().enumerate() {
if i > 0 {
let separator = svg_element::Line::new()
.set("x1", top_left.x())
.set("y1", current_y)
.set("x2", top_left.x() + expanded_size.width())
.set("y2", current_y);
let separator = crate::apply_stroke!(separator, self.definition.separator_stroke());
output.add_to_layer(RenderLayer::Fragment, Box::new(separator));
}
if let Some(title) = section.title() {
let formatted_title = format!("[{}]", title);
let title_text = Text::new(
&self.definition.section_title_text_definition,
&formatted_title,
);
let title_size = title_text.size();
let title_position = if i == 0 {
Point::new(
pentagon_bounds.max_x() + title_size.width() / 2.0,
pentagon_bounds.center().y(),
)
} else {
Point::new(
top_left.x() + title_size.width() / 2.0 + bounds_padding.left(),
current_y + title_size.height() / 2.0,
)
};
let title_output = title_text.render_to_layers(title_position);
output.merge(title_output);
}
current_y += section.height();
}
output
}
fn size(&self) -> Size {
self.size
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fragment_definition_custom_values() {
let mut definition = FragmentDefinition::new();
definition.set_background_color(Some(Color::new("#f0f0f0").unwrap()));
assert!(definition.background_color().is_some());
let bg_color = definition.background_color().unwrap().to_string();
assert!(
bg_color.contains("240"),
"Background color should contain value 240"
);
assert_eq!(definition.border_stroke().color().to_string(), "black");
assert_eq!(definition.border_stroke().width(), 1.0);
assert_eq!(*definition.border_stroke().style(), StrokeStyle::Solid);
assert_eq!(definition.separator_stroke().color().to_string(), "black");
assert_eq!(definition.separator_stroke().width(), 1.0);
assert_eq!(*definition.separator_stroke().style(), StrokeStyle::Dashed);
}
#[test]
fn test_header_size_includes_triangle() {
let definition = FragmentDefinition::default();
let header = definition.header_size("alt");
assert!(header.width() > 0.0);
assert!(header.height() > 0.0);
let text = Text::new(definition.operation_label_text(), "alt");
let text_size = text.calculate_size();
assert!(header.width() > text_size.width());
assert_eq!(header.height(), text_size.height());
}
#[test]
fn test_section_header_size() {
let definition = FragmentDefinition::default();
let with_title = definition.section_header_size(Some("condition"));
assert!(with_title.width() > 0.0);
assert!(with_title.height() > 0.0);
let without_title = definition.section_header_size(None);
assert_eq!(without_title.width(), 0.0);
assert_eq!(without_title.height(), 0.0);
}
#[test]
fn test_bottom_padding() {
let mut definition = FragmentDefinition::default();
assert_eq!(definition.bottom_padding(), 0.0);
definition.set_bounds_padding(Insets::new(5.0, 10.0, 15.0, 20.0));
assert_eq!(definition.bottom_padding(), 15.0);
}
#[test]
fn test_triangle_width_clamping() {
let small = FragmentDefinition::triangle_width(Size::new(50.0, 8.0));
assert_eq!(small, 4.0);
let large = FragmentDefinition::triangle_width(Size::new(50.0, 30.0));
assert_eq!(large, 10.0);
let large = FragmentDefinition::triangle_width(Size::new(50.0, 60.0));
assert_eq!(large, 15.0); }
#[test]
fn test_fragment_section_creation() {
let section1 = FragmentSection::new(Some("test section".to_string()), 100.0);
assert_eq!(section1.title(), Some("test section"));
assert_eq!(section1.height(), 100.0);
let section2 = FragmentSection::new(None, 50.0);
assert_eq!(section2.title(), None);
assert_eq!(section2.height(), 50.0);
}
#[test]
fn test_fragment_creation() {
let definition = FragmentDefinition::default();
let sections = vec![
FragmentSection::new(Some("section 1".to_string()), 80.0),
FragmentSection::new(Some("section 2".to_string()), 60.0),
FragmentSection::new(None, 40.0),
];
let fragment = Fragment::new(
Rc::new(definition),
"alt".to_string(),
sections.clone(),
Size::new(200.0, 180.0),
);
assert_eq!(fragment.operation(), "alt");
assert_eq!(fragment.sections().len(), 3);
assert_eq!(fragment.size(), Size::new(200.0, 180.0));
}
#[test]
fn test_fragment_render_to_svg() {
let definition = FragmentDefinition::default();
let sections = vec![
FragmentSection::new(Some("successful".to_string()), 100.0),
FragmentSection::new(Some("failed".to_string()), 80.0),
];
let fragment = Fragment::new(
Rc::new(definition),
"alt".to_string(),
sections,
Size::new(300.0, 200.0),
);
assert_eq!(fragment.size(), Size::new(300.0, 200.0));
let position = Point::new(50.0, 100.0);
let output = fragment.render_to_layers(position);
let svg_nodes = output.render();
assert!(!svg_nodes.is_empty());
}
}