use std::rc::Rc;
use svg::{self, node::element as svg_element};
use crate::{
color::Color,
draw::{Drawable, LayeredOutput, RenderLayer, StrokeDefinition, Text, TextDefinition},
geometry::{Insets, Point, Size},
};
const CORNER_FOLD_SIZE: f32 = 12.0;
#[derive(Debug, Clone)]
pub struct NoteDefinition {
background_color: Option<Color>,
stroke: Rc<StrokeDefinition>,
text: Rc<TextDefinition>,
min_width: Option<f32>,
}
impl NoteDefinition {
pub fn new() -> Self {
Self::default()
}
pub fn set_background_color(&mut self, color: Option<Color>) {
self.background_color = color;
}
pub fn set_min_width(&mut self, width: Option<f32>) {
self.min_width = width;
}
fn background_color(&self) -> Option<Color> {
self.background_color
}
pub fn stroke(&self) -> &Rc<StrokeDefinition> {
&self.stroke
}
pub fn text(&self) -> &Rc<TextDefinition> {
&self.text
}
pub fn set_text(&mut self, text: Rc<TextDefinition>) {
self.text = text;
}
pub fn set_stroke(&mut self, stroke: Rc<StrokeDefinition>) {
self.stroke = stroke;
}
}
impl Default for NoteDefinition {
fn default() -> Self {
Self {
background_color: Some(Color::new("lightyellow").expect("Invalid color")),
stroke: Rc::new(StrokeDefinition::default()),
text: Rc::new(TextDefinition::default()),
min_width: None,
}
}
}
#[derive(Debug, Clone)]
pub struct Note {
definition: Rc<NoteDefinition>,
content: String,
}
impl Note {
pub fn new(definition: Rc<NoteDefinition>, content: String) -> Self {
Self {
definition,
content,
}
}
fn text_size(&self) -> Size {
if self.content.is_empty() {
return Size::zero();
}
let text = Text::new(&self.definition.text, &self.content);
text.size()
}
fn calculate_size(&self) -> Size {
let text_size = self.text_size();
let size = text_size.add_padding(Insets::new(10.0, 10.0 + CORNER_FOLD_SIZE, 10.0, 10.0));
if let Some(min_width) = self.definition.min_width {
let width = size.width().max(min_width);
Size::new(width, size.height())
} else {
size
}
}
fn create_dog_eared_path(&self, size: Size, position: Point) -> svg_element::Path {
let bounds = position.to_bounds(size);
let min_x = bounds.min_x();
let min_y = bounds.min_y();
let max_x = bounds.max_x();
let max_y = bounds.max_y();
let fold_x = max_x - CORNER_FOLD_SIZE;
let fold_y = min_y + CORNER_FOLD_SIZE;
let path_data = format!(
"M {} {} L {} {} L {} {} L {} {} L {} {} L {} {} Z",
min_x,
min_y, fold_x,
min_y, max_x,
fold_y, max_x,
max_y, min_x,
max_y, min_x,
min_y );
let path = svg_element::Path::new().set("d", path_data);
crate::apply_stroke!(path, self.definition.stroke())
}
fn create_fold_line_path(&self, size: Size, position: Point) -> svg_element::Path {
let bounds = position.to_bounds(size);
let max_x = bounds.max_x();
let min_y = bounds.min_y();
let fold_x = max_x - CORNER_FOLD_SIZE;
let fold_y = min_y + CORNER_FOLD_SIZE;
let path_data = format!(
"M {} {} L {} {}",
fold_x,
min_y, max_x,
fold_y );
let path = svg_element::Path::new()
.set("d", path_data)
.set("fill", "none");
crate::apply_stroke!(path, self.definition.stroke())
}
fn create_fold_triangle_path(&self, size: Size, position: Point) -> svg_element::Path {
let bounds = position.to_bounds(size);
let max_x = bounds.max_x();
let min_y = bounds.min_y();
let fold_x = max_x - CORNER_FOLD_SIZE;
let fold_y = min_y + CORNER_FOLD_SIZE;
let path_data = format!(
"M {} {} L {} {} L {} {} Z",
fold_x,
min_y, max_x,
fold_y, fold_x,
fold_y );
let path = svg_element::Path::new().set("d", path_data);
crate::apply_stroke!(path, self.definition.stroke())
}
}
impl Drawable for Note {
fn render_to_layers(&self, position: Point) -> LayeredOutput {
let mut output = LayeredOutput::new();
let size = self.size();
let mut note_body = self.create_dog_eared_path(size, position);
if let Some(bg_color) = self.definition.background_color() {
note_body = note_body
.set("fill", bg_color.to_string())
.set("fill-opacity", bg_color.alpha());
}
output.add_to_layer(RenderLayer::Note, Box::new(note_body));
let mut fold_triangle = self.create_fold_triangle_path(size, position);
if let Some(bg_color) = self.definition.background_color() {
let darker = bg_color.with_alpha(bg_color.alpha() * 0.8);
fold_triangle = fold_triangle
.set("fill", darker.to_string())
.set("fill-opacity", darker.alpha());
} else {
fold_triangle = fold_triangle
.set("fill", "#e0e0e0")
.set("fill-opacity", 0.8);
}
output.add_to_layer(RenderLayer::Note, Box::new(fold_triangle));
let fold_line = self.create_fold_line_path(size, position);
output.add_to_layer(RenderLayer::Note, Box::new(fold_line));
if !self.content.is_empty() {
let text = Text::new(&self.definition.text, &self.content);
let text_output = text.render_to_layers(position);
output.merge(text_output);
}
output
}
fn size(&self) -> Size {
self.calculate_size()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_note_definition_default() {
let def = NoteDefinition::default();
assert!(def.background_color().is_some());
assert_eq!(def.stroke().width(), 1.0);
}
#[test]
fn test_note_creation() {
let def = NoteDefinition::new();
let note = Note::new(Rc::new(def.clone()), "Test note".to_string());
let size = note.size();
assert!(size.width() > 0.0);
assert!(size.height() > 0.0);
}
#[test]
fn test_note_size_calculation() {
let def = NoteDefinition::new();
let note = Note::new(Rc::new(def.clone()), "Test".to_string());
let size = note.size();
assert!(size.width() > 0.0);
assert!(size.height() > 0.0);
}
#[test]
fn test_empty_note() {
let def = NoteDefinition::new();
let note = Note::new(Rc::new(def.clone()), String::new());
let size = note.size();
assert!(size.width() > 0.0);
assert!(size.height() > 0.0);
}
#[test]
fn test_note_definition_customization() {
let mut def = NoteDefinition::new();
def.set_background_color(Some(Color::new("blue").expect("valid color")));
let _note = Note::new(Rc::new(def.clone()), String::new());
assert_eq!(
def.background_color(),
Some(Color::new("blue").expect("valid color"))
);
}
}