use crate::{
draw::{Drawable, LayeredOutput, Shape, Text, text_positioning::TextPositioningStrategy},
geometry::{Point, Size},
};
#[derive(Debug, Clone)]
pub struct ShapeWithText<'a> {
shape: Shape,
text: Option<Text<'a>>,
text_positioning_strategy: TextPositioningStrategy,
inner_content_size: Option<Size>,
}
impl<'a> ShapeWithText<'a> {
pub fn new(shape: Shape, text: Option<Text<'a>>) -> Self {
let text_positioning_strategy = shape.text_positioning_strategy();
let mut instance = Self {
shape,
text,
text_positioning_strategy,
inner_content_size: None,
};
if instance.text.is_some()
&& instance
.text_positioning_strategy
.text_affects_shape_content()
{
if let Err(e) = instance.update_shape_content_size() {
panic!("Failed to assign text to a content-supporting shape: {e}");
}
}
instance
}
pub fn set_inner_content_size(&mut self, size: Size) -> Result<(), &'static str> {
if !self.shape.supports_content() {
return Err("Cannot set inner content size on content-free shapes");
}
self.inner_content_size = Some(size);
let text_size = self.text_size();
let total = Size::new(
size.width().max(text_size.width()),
text_size.height() + size.height(),
);
self.shape
.expand_content_size_to(total)
.expect("Shape should support content at this point");
if !size.is_zero() {
let current_padding = self.shape.padding();
let adjusted_top = (current_padding.top() - text_size.height()).max(0.0);
let new_padding = current_padding.with_top(adjusted_top);
self.shape.set_padding(new_padding);
}
Ok(())
}
pub fn text_size(&self) -> Size {
self.text.as_ref().map(|t| t.size()).unwrap_or_default()
}
pub fn shape_to_inner_content_min_point(&self) -> Point {
let base = self.shape.shape_to_container_min_point();
let text_size = self.text_size();
self.text_positioning_strategy
.calculate_inner_content_min_point(base, text_size)
}
pub fn content_size(&self) -> Option<Size> {
self.inner_content_size
}
pub fn find_intersection(&self, a: Point, b: Point) -> Point {
self.shape.find_intersection(a, b, self.size())
}
fn update_shape_content_size(&mut self) -> Result<(), &'static str> {
let text_size = self.text_size();
self.shape.expand_content_size_to(text_size)
}
fn shape_to_container_min_point_no_top_padding(&self) -> Point {
let additional_space = self.shape.calculate_additional_space();
let padding = self.shape.padding();
Point::new(
padding.left() + additional_space.width() / 2.0,
additional_space.height() / 2.0,
)
}
fn calculate_text_position(&self, total_position: Point) -> Point {
if self.text.is_none() {
return Point::default();
}
let shape_size = self.shape.inner_size();
let text_size = self.text_size();
let has_inner_content = text_size != self.shape.content_size();
self.text_positioning_strategy.calculate_text_position(
total_position,
shape_size,
text_size,
self.shape.shape_to_container_min_point(),
self.shape_to_container_min_point_no_top_padding(),
has_inner_content,
)
}
}
impl<'a> Drawable for ShapeWithText<'a> {
fn render_to_layers(&self, position: Point) -> LayeredOutput {
let mut output = LayeredOutput::new();
let shape_size = self.shape.inner_size();
let text_size = self.text_size();
let shape_position = self
.text_positioning_strategy
.calculate_shape_position(position, shape_size, text_size);
let shape_output = self.shape.render_to_layers(shape_position);
output.merge(shape_output);
if let Some(text) = &self.text {
let text_pos = self.calculate_text_position(position);
let text_output = text.render_to_layers(text_pos);
output.merge(text_output);
}
output
}
fn size(&self) -> Size {
let shape_size = self.shape.outer_size();
let text_size = self.text_size();
self.text_positioning_strategy
.calculate_total_size(shape_size, text_size)
}
}
#[cfg(test)]
mod tests {
use std::rc::Rc;
use super::*;
use crate::draw::{
TextDefinition,
shape::{ActorDefinition, RectangleDefinition, ShapeDefinition},
};
fn create_rectangle_shape() -> Shape {
let rect_def: Rc<Box<dyn ShapeDefinition>> = Rc::new(Box::new(RectangleDefinition::new()));
Shape::new(rect_def)
}
fn create_actor_shape() -> Shape {
let actor_def: Rc<Box<dyn ShapeDefinition>> = Rc::new(Box::new(ActorDefinition::new()));
Shape::new(actor_def)
}
#[test]
fn test_shape_with_text_new_content_supporting() {
let shape = create_rectangle_shape();
let shape_only_outer_size = shape.outer_size();
let text_def = TextDefinition::default();
let text = Text::new(&text_def, "Test Label");
let shape_with_text = ShapeWithText::new(shape, Some(text));
let total_size = shape_with_text.size();
assert!(
total_size.width() >= shape_only_outer_size.width(),
"Width should be at least shape width"
);
assert!(
total_size.height() >= shape_only_outer_size.height(),
"Height should be at least shape height"
);
}
#[test]
fn test_shape_with_text_new_content_free() {
let shape = create_actor_shape();
let shape_only_outer_size = shape.outer_size();
let text_def = TextDefinition::default();
let text = Text::new(&text_def, "User");
let shape_with_text = ShapeWithText::new(shape, Some(text));
let total_size = shape_with_text.size();
assert!(
total_size.width() >= shape_only_outer_size.width(),
"Width should be at least shape width"
);
assert!(
total_size.height() > shape_only_outer_size.height(),
"Height should be greater than shape-only height (text is below)"
);
}
#[test]
fn test_shape_with_text_new_no_text() {
let rect = create_rectangle_shape();
let rect_outer_size = rect.outer_size();
let rect_with_text = ShapeWithText::new(rect, None);
assert_eq!(
rect_with_text.text_size(),
Size::default(),
"text_size should be zero when no text (Rectangle)"
);
assert_eq!(
rect_with_text.size(),
rect_outer_size,
"Total size should equal shape outer size when no text (Rectangle)"
);
assert!(
rect_with_text.content_size().is_none(),
"content_size should be None initially (Rectangle)"
);
let actor = create_actor_shape();
let actor_outer_size = actor.outer_size();
let actor_with_text = ShapeWithText::new(actor, None);
assert_eq!(
actor_with_text.text_size(),
Size::default(),
"text_size should be zero when no text (Actor)"
);
assert_eq!(
actor_with_text.size(),
actor_outer_size,
"Total size should equal shape outer size when no text (Actor)"
);
assert!(
actor_with_text.content_size().is_none(),
"content_size should be None initially (Actor)"
);
}
#[test]
fn test_shape_with_text_text_size() {
let text_def = TextDefinition::default();
let text = Text::new(&text_def, "Sample Text");
let expected_text_size = text.size();
let shape = create_rectangle_shape();
let shape_with_text = ShapeWithText::new(shape, Some(text));
assert_eq!(
shape_with_text.text_size(),
expected_text_size,
"text_size should return the text dimensions"
);
let shape = create_rectangle_shape();
let shape_with_text = ShapeWithText::new(shape, None);
assert_eq!(
shape_with_text.text_size(),
Size::default(),
"text_size should return zero when no text"
);
}
#[test]
fn test_shape_with_text_size_with_longer_text() {
let shape = create_rectangle_shape();
let shape_only_size = shape.outer_size();
let text_def = TextDefinition::default();
let long_text = Text::new(&text_def, "This is a much longer text label for testing");
let shape_with_text = ShapeWithText::new(shape, Some(long_text));
let total_size = shape_with_text.size();
assert!(
total_size.width() > shape_only_size.width(),
"Longer text should expand shape width"
);
}
#[test]
fn test_shape_with_text_set_inner_content_size() {
let shape = create_rectangle_shape();
let text_def = TextDefinition::default();
let text = Text::new(&text_def, "Header");
let mut shape_with_text = ShapeWithText::new(shape, Some(text));
let size_before = shape_with_text.size();
let inner_content = Size::new(200.0, 100.0);
let result = shape_with_text.set_inner_content_size(inner_content);
assert!(
result.is_ok(),
"set_inner_content_size should succeed for content-supporting shapes"
);
assert_eq!(
shape_with_text.content_size(),
Some(inner_content),
"content_size should return the set inner content size"
);
let size_after = shape_with_text.size();
assert!(
size_after.width() > size_before.width(),
"Width should accommodate inner content"
);
assert!(
size_after.height() > size_before.height(),
"Height should accommodate inner content"
);
}
#[test]
fn test_shape_with_text_set_inner_content_size_error() {
let shape = create_actor_shape();
let text_def = TextDefinition::default();
let text = Text::new(&text_def, "User");
let mut shape_with_text = ShapeWithText::new(shape, Some(text));
let inner_content = Size::new(50.0, 50.0);
let result = shape_with_text.set_inner_content_size(inner_content);
assert!(
result.is_err(),
"set_inner_content_size should fail for content-free shapes"
);
assert!(
shape_with_text.content_size().is_none(),
"content_size should remain None after failed set"
);
}
#[test]
fn test_shape_with_text_shape_to_inner_content_min_point() {
let shape = create_rectangle_shape();
let text_def = TextDefinition::default();
let text = Text::new(&text_def, "Header");
let text_size = text.size();
let shape_with_text = ShapeWithText::new(shape, Some(text));
let min_point_with_text = shape_with_text.shape_to_inner_content_min_point();
let shape = create_rectangle_shape();
let shape_with_text_no_text = ShapeWithText::new(shape, None);
let min_point_no_text = shape_with_text_no_text.shape_to_inner_content_min_point();
assert!(
min_point_with_text.y() >= min_point_no_text.y() + text_size.height(),
"Inner content min point should account for text height"
);
}
#[test]
fn test_shape_with_text_render_to_layers() {
let shape = create_rectangle_shape();
let text_def = TextDefinition::default();
let text = Text::new(&text_def, "Label");
let shape_with_text = ShapeWithText::new(shape, Some(text));
let output = shape_with_text.render_to_layers(Point::new(100.0, 100.0));
assert!(!output.is_empty(), "Render output should not be empty");
let actor = create_actor_shape();
let text_def = TextDefinition::default();
let text = Text::new(&text_def, "User");
let actor_with_text = ShapeWithText::new(actor, Some(text));
let output = actor_with_text.render_to_layers(Point::new(100.0, 100.0));
assert!(
!output.is_empty(),
"Render output for content-free shape should not be empty"
);
let shape = create_rectangle_shape();
let shape_with_text = ShapeWithText::new(shape, None);
let output = shape_with_text.render_to_layers(Point::new(100.0, 100.0));
assert!(
!output.is_empty(),
"Render output without text should not be empty"
);
}
#[test]
fn test_shape_with_text_find_intersection() {
let shape = create_rectangle_shape();
let text_def = TextDefinition::default();
let text = Text::new(&text_def, "Label");
let shape_with_text = ShapeWithText::new(shape, Some(text));
let total_size = shape_with_text.size();
let center = Point::new(100.0, 100.0);
let target = Point::new(200.0, 100.0);
let shape_with_text_intersection = shape_with_text.find_intersection(center, target);
let shape_intersection = shape_with_text
.shape
.find_intersection(center, target, total_size);
assert_eq!(
shape_with_text_intersection, shape_intersection,
"find_intersection should delegate to underlying shape"
);
}
}