use super::{ShapeId, ShapeStyle, ShapeTrait};
use kurbo::{Affine, BezPath, Point, Rect};
use serde::{Deserialize, Serialize};
use std::sync::RwLock;
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum FontFamily {
#[default]
GelPen,
Roboto,
ArchitectsDaughter,
}
impl FontFamily {
pub fn name(&self) -> &'static str {
match self {
FontFamily::GelPen => "GelPen",
FontFamily::Roboto => "Roboto",
FontFamily::ArchitectsDaughter => "Architects Daughter",
}
}
pub fn display_name(&self) -> &'static str {
match self {
FontFamily::GelPen => "GelPen",
FontFamily::Roboto => "Roboto",
FontFamily::ArchitectsDaughter => "Architects",
}
}
pub fn all() -> &'static [FontFamily] {
&[FontFamily::GelPen, FontFamily::Roboto, FontFamily::ArchitectsDaughter]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum FontWeight {
Light,
#[default]
Regular,
Heavy,
}
impl FontWeight {
pub fn display_name(&self) -> &'static str {
match self {
FontWeight::Light => "Light",
FontWeight::Regular => "Regular",
FontWeight::Heavy => "Heavy",
}
}
pub fn all() -> &'static [FontWeight] {
&[FontWeight::Light, FontWeight::Regular, FontWeight::Heavy]
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Text {
pub(crate) id: ShapeId,
pub position: Point,
pub content: String,
pub font_size: f64,
pub font_family: FontFamily,
pub font_weight: FontWeight,
pub style: ShapeStyle,
#[serde(skip)]
cached_size: RwLock<Option<(f64, f64)>>,
}
impl Clone for Text {
fn clone(&self) -> Self {
Self {
id: self.id,
position: self.position,
content: self.content.clone(),
font_size: self.font_size,
font_family: self.font_family,
font_weight: self.font_weight,
style: self.style.clone(),
cached_size: RwLock::new(
self.cached_size.read().ok().and_then(|guard| *guard)
),
}
}
}
impl Text {
pub const DEFAULT_FONT_SIZE: f64 = 20.0;
pub fn new(position: Point, content: String) -> Self {
Self {
id: Uuid::new_v4(),
position,
content,
font_size: Self::DEFAULT_FONT_SIZE,
font_family: FontFamily::default(),
font_weight: FontWeight::default(),
style: ShapeStyle::default(),
cached_size: RwLock::new(None),
}
}
pub fn set_cached_size(&self, width: f64, height: f64) {
if let Ok(mut cache) = self.cached_size.write() {
*cache = Some((width, height));
}
}
pub fn invalidate_cache(&self) {
if let Ok(mut cache) = self.cached_size.write() {
*cache = None;
}
}
pub fn with_font_size(mut self, size: f64) -> Self {
self.font_size = size;
self
}
pub fn with_font_family(mut self, family: FontFamily) -> Self {
self.font_family = family;
self
}
pub fn with_font_weight(mut self, weight: FontWeight) -> Self {
self.font_weight = weight;
self
}
pub fn set_content(&mut self, content: String) {
self.content = content;
self.invalidate_cache();
}
pub fn content(&self) -> &str {
&self.content
}
fn approximate_width(&self) -> f64 {
let max_line_len = self.content
.lines()
.map(|line| line.len())
.max()
.unwrap_or(0);
let char_width_factor = match (&self.font_family, &self.font_weight) {
(FontFamily::GelPen, FontWeight::Light) => 0.50,
(FontFamily::GelPen, FontWeight::Regular) => 0.55,
(FontFamily::GelPen, FontWeight::Heavy) => 0.60,
(FontFamily::Roboto, FontWeight::Light) => 0.45,
(FontFamily::Roboto, FontWeight::Regular) => 0.48,
(FontFamily::Roboto, FontWeight::Heavy) => 0.52,
(FontFamily::ArchitectsDaughter, _) => 0.58,
};
max_line_len as f64 * self.font_size * char_width_factor
}
fn approximate_height(&self) -> f64 {
let line_count = self.content.lines().count().max(1);
let line_count = if self.content.ends_with('\n') {
line_count + 1
} else {
line_count
};
line_count as f64 * self.font_size * 1.2
}
}
impl ShapeTrait for Text {
fn id(&self) -> ShapeId {
self.id
}
fn bounds(&self) -> Rect {
let (width, height) = self.cached_size
.read()
.ok()
.and_then(|guard| *guard)
.map(|(w, h)| (w.max(20.0), h))
.unwrap_or_else(|| {
(self.approximate_width().max(20.0), self.approximate_height())
});
Rect::new(
self.position.x,
self.position.y,
self.position.x + width,
self.position.y + height,
)
}
fn hit_test(&self, point: Point, tolerance: f64) -> bool {
let bounds = self.bounds().inflate(tolerance, tolerance);
bounds.contains(point)
}
fn to_path(&self) -> BezPath {
let bounds = self.bounds();
let mut path = BezPath::new();
path.move_to(Point::new(bounds.x0, bounds.y0));
path.line_to(Point::new(bounds.x1, bounds.y0));
path.line_to(Point::new(bounds.x1, bounds.y1));
path.line_to(Point::new(bounds.x0, bounds.y1));
path.close_path();
path
}
fn style(&self) -> &ShapeStyle {
&self.style
}
fn style_mut(&mut self) -> &mut ShapeStyle {
&mut self.style
}
fn transform(&mut self, affine: Affine) {
self.position = affine * self.position;
let coeffs = affine.as_coeffs();
let scale = (coeffs[0].abs() + coeffs[3].abs()) / 2.0;
if (scale - 1.0).abs() > 0.01 {
self.font_size *= scale;
}
}
fn clone_box(&self) -> Box<dyn ShapeTrait + Send + Sync> {
Box::new(self.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_text_creation() {
let text = Text::new(Point::new(100.0, 100.0), "Hello".to_string());
assert_eq!(text.content(), "Hello");
assert!((text.font_size - Text::DEFAULT_FONT_SIZE).abs() < f64::EPSILON); }
#[test]
fn test_text_with_font_size() {
let text = Text::new(Point::new(0.0, 0.0), "Test".to_string())
.with_font_size(32.0);
assert!((text.font_size - 32.0).abs() < f64::EPSILON);
}
#[test]
fn test_hit_test() {
let text = Text::new(Point::new(100.0, 100.0), "Hello World".to_string());
let bounds = text.bounds();
let center = Point::new(
(bounds.x0 + bounds.x1) / 2.0,
(bounds.y0 + bounds.y1) / 2.0,
);
assert!(text.hit_test(center, 0.0));
assert!(!text.hit_test(Point::new(0.0, 0.0), 0.0));
}
#[test]
fn test_bounds() {
let text = Text::new(Point::new(100.0, 100.0), "Hi".to_string());
let bounds = text.bounds();
assert!(bounds.width() > 0.0);
assert!(bounds.height() > 0.0);
}
}