use crate::graphics::{
Alignment, Color, ColorProperty, DynamicColor, FontDesc, GraphicsContext, Rect, Rectangle,
TextLayout, TextRenderer, VerticalAlignment,
};
use crate::ui::Component;
use anyhow::Result;
use std::time::Duration;
#[derive(Clone)]
pub struct TextComponent {
layout: TextLayout,
bounds: Rectangle,
text_color: DynamicColor,
background_color: Option<DynamicColor>,
}
#[allow(dead_code)]
impl TextComponent {
pub fn new(text: &str, x: i16, y: i16, width: u16, height: u16) -> Self {
let bounds = Rectangle {
x,
y,
width,
height,
};
let text_color = DynamicColor::new(Color::WHITE);
let layout = TextLayout::new()
.text(text)
.font(FontDesc::new("DejaVu Sans", 16.0))
.color(text_color.current())
.bounds(Rect::new(x as i32, y as i32, width as u32, height as u32))
.alignment(Alignment::Left)
.vertical_alignment(VerticalAlignment::Top)
.wrap(true);
Self {
layout,
bounds,
text_color,
background_color: None,
}
}
pub fn with_font(mut self, font: FontDesc) -> Self {
self.layout = self.layout.font(font);
self.recalculate_bounds();
self
}
fn recalculate_bounds(&mut self) {
let (width, height) = self.calculate_text_size();
self.bounds.width = width as u16;
self.bounds.height = height as u16;
self.layout = self.layout.clone().bounds(Rect::new(
self.bounds.x as i32,
self.bounds.y as i32,
width,
height,
));
}
pub fn with_color(mut self, color: Color) -> Self {
self.text_color.set_immediate(color);
self.layout = self.layout.color(color);
self
}
pub fn with_dynamic_color(mut self, dynamic_color: DynamicColor) -> Self {
self.text_color = dynamic_color;
self.layout = self.layout.color(self.text_color.current());
self
}
#[allow(dead_code)]
pub fn with_background(mut self, color: Color) -> Self {
self.background_color = Some(DynamicColor::new(color));
self.layout = self.layout.background(color);
self
}
pub fn with_dynamic_background(mut self, dynamic_color: DynamicColor) -> Self {
let color = dynamic_color.current();
self.background_color = Some(dynamic_color);
self.layout = self.layout.background(color);
self
}
pub fn with_alignment(mut self, alignment: Alignment) -> Self {
self.layout = self.layout.alignment(alignment);
self
}
#[allow(dead_code)]
pub fn with_vertical_alignment(mut self, alignment: VerticalAlignment) -> Self {
self.layout = self.layout.vertical_alignment(alignment);
self
}
pub fn centered(mut self) -> Self {
self.layout = self
.layout
.alignment(Alignment::Center)
.vertical_alignment(VerticalAlignment::Middle);
self
}
pub fn set_text(&mut self, text: &str) {
self.layout = self.layout.clone().text(text);
}
pub fn animate_text_color(&mut self, color: Color, duration: Duration) {
self.text_color.animate_to(color, duration);
}
pub fn set_text_color(&mut self, color: Color) {
self.text_color.set_immediate(color);
}
pub fn animate_background_color(&mut self, color: Color, duration: Duration) {
if let Some(ref mut bg_color) = self.background_color {
bg_color.animate_to(color, duration);
} else {
let mut new_bg = DynamicColor::new(Color::TRANSPARENT);
new_bg.animate_to(color, duration);
self.background_color = Some(new_bg);
}
}
pub fn set_background_color(&mut self, color: Color) {
if let Some(ref mut bg_color) = self.background_color {
bg_color.set_immediate(color);
} else {
self.background_color = Some(DynamicColor::new(color));
}
}
pub fn current_text_color(&self) -> Color {
self.text_color.current()
}
pub fn current_background_color(&self) -> Option<Color> {
self.background_color.as_ref().map(|bg| bg.current())
}
pub fn calculate_text_size(&self) -> (u32, u32) {
let temp_surface = cairo::ImageSurface::create(cairo::Format::ARgb32, 1, 1).unwrap();
let temp_context = cairo::Context::new(&temp_surface).unwrap();
let font = self.layout.get_font();
temp_context.select_font_face(
&font.family,
cairo::FontSlant::Normal,
cairo::FontWeight::Normal,
);
temp_context.set_font_size(font.size);
let text = self.layout.get_text();
let text_extents = temp_context.text_extents(text).unwrap();
let font_extents = temp_context.font_extents().unwrap();
let width = (text_extents.width() + 10.0).ceil() as u32;
let height = (font_extents.height() + 6.0).ceil() as u32;
(width, height)
}
pub fn auto_sized(text: &str, x: i16, y: i16) -> Self {
let mut component = Self::new(text, x, y, 100, 30);
let (width, height) = component.calculate_text_size();
component.bounds.width = width as u16;
component.bounds.height = height as u16;
component.layout = component
.layout
.bounds(Rect::new(x as i32, y as i32, width, height));
component
}
pub fn render_with_text_renderer(&self, text_renderer: &TextRenderer) -> Result<()> {
self.layout
.render(text_renderer, self.bounds.x as i32, self.bounds.y as i32)
}
pub fn render_with_cairo(&self, cairo_context: &cairo::Context) -> Result<()> {
use cairo::{FontSlant, FontWeight};
cairo_context.save().unwrap();
let font = self.layout.get_font();
let color = self.layout.get_color();
let text = self.layout.get_text();
cairo_context.select_font_face(&font.family, FontSlant::Normal, FontWeight::Normal);
cairo_context.set_font_size(font.size);
cairo_context.set_source_rgba(color.r, color.g, color.b, color.a);
let font_extents = cairo_context.font_extents().unwrap();
let baseline_y = self.bounds.y as f64 + font_extents.ascent() + 2.0;
cairo_context.move_to(self.bounds.x as f64 + 5.0, baseline_y); cairo_context.show_text(text).unwrap();
cairo_context.restore().unwrap();
Ok(())
}
}
impl Component for TextComponent {
fn render(&self, graphics: &mut GraphicsContext) -> Result<()> {
if let Ok(Some(cairo_context)) = graphics.get_cairo_context() {
self.render_with_cairo(&cairo_context)
} else {
self.render_with_text_renderer(graphics.text_renderer())
}
}
fn bounds(&self) -> Rectangle {
self.bounds
}
fn update(&mut self, delta_time: f64) -> bool {
let mut needs_redraw = false;
if self.text_color.is_animating() {
let new_color = self.text_color.update(delta_time);
self.layout = self.layout.clone().color(new_color);
needs_redraw = true;
}
if let Some(ref mut bg_color) = self.background_color {
if bg_color.is_animating() {
let new_bg_color = bg_color.update(delta_time);
self.layout = self.layout.clone().background(new_bg_color);
needs_redraw = true;
}
}
needs_redraw
}
}
impl ColorProperty for TextComponent {
fn color_mut(&mut self) -> &mut DynamicColor {
&mut self.text_color
}
fn color(&self) -> &DynamicColor {
&self.text_color
}
}
#[allow(dead_code)]
pub struct MultilineTextComponent {
lines: Vec<TextComponent>,
bounds: Rectangle,
line_spacing: f64,
}
#[allow(dead_code)]
impl MultilineTextComponent {
pub fn new(lines: Vec<&str>, x: i16, y: i16, width: u16, line_height: u16) -> Self {
let total_height = lines.len() as u16 * line_height;
let text_components = lines
.iter()
.enumerate()
.map(|(i, line)| {
let line_y = y + (i as u16 * line_height) as i16;
TextComponent::new(line, x, line_y, width, line_height)
})
.collect();
Self {
lines: text_components,
bounds: Rectangle {
x,
y,
width,
height: total_height,
},
line_spacing: 1.2,
}
}
pub fn with_font(mut self, font: FontDesc) -> Self {
self.lines = self
.lines
.into_iter()
.map(|line| line.with_font(font.clone()))
.collect();
self
}
pub fn with_color(mut self, color: Color) -> Self {
self.lines = self
.lines
.into_iter()
.map(|line| line.with_color(color))
.collect();
self
}
pub fn render_with_text_renderer(&self, text_renderer: &TextRenderer) -> Result<()> {
self.lines
.iter()
.try_for_each(|line| line.render_with_text_renderer(text_renderer))
}
pub fn render_with_cairo(&self, cairo_context: &cairo::Context) -> Result<()> {
self.lines
.iter()
.try_for_each(|line| line.render_with_cairo(cairo_context))
}
}
impl Component for MultilineTextComponent {
fn render(&self, graphics: &mut GraphicsContext) -> Result<()> {
if let Ok(Some(cairo_context)) = graphics.get_cairo_context() {
self.render_with_cairo(&cairo_context)
} else {
self.render_with_text_renderer(graphics.text_renderer())
}
}
fn bounds(&self) -> Rectangle {
self.bounds
}
fn update(&mut self, _delta_time: f64) -> bool {
false }
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
#[test]
fn test_text_component_creation() {
let component = TextComponent::new("Test", 10, 20, 100, 50);
assert_eq!(component.bounds().x, 10);
assert_eq!(component.bounds().y, 20);
assert_eq!(component.bounds().width, 100);
assert_eq!(component.bounds().height, 50);
assert_eq!(component.current_text_color(), Color::WHITE);
assert!(component.current_background_color().is_none());
}
#[test]
fn test_text_component_with_color() {
let color = Color::rgb(1.0, 0.0, 0.0);
let component = TextComponent::new("Test", 0, 0, 100, 50).with_color(color);
assert_eq!(component.current_text_color(), color);
}
#[test]
fn test_text_component_with_background() {
let bg_color = Color::rgb(0.5, 0.5, 0.5);
let component = TextComponent::new("Test", 0, 0, 100, 50).with_background(bg_color);
assert_eq!(component.current_background_color(), Some(bg_color));
}
#[test]
fn test_text_component_with_dynamic_color() {
let dynamic_color =
DynamicColor::new(Color::rgb(0.0, 1.0, 0.0)).with_duration(Duration::from_millis(500));
let component = TextComponent::new("Test", 0, 0, 100, 50).with_dynamic_color(dynamic_color);
assert_eq!(component.current_text_color(), Color::rgb(0.0, 1.0, 0.0));
}
#[test]
fn test_text_component_with_dynamic_background() {
let dynamic_bg = DynamicColor::new(Color::rgb(0.2, 0.2, 0.2));
let component =
TextComponent::new("Test", 0, 0, 100, 50).with_dynamic_background(dynamic_bg);
assert_eq!(
component.current_background_color(),
Some(Color::rgb(0.2, 0.2, 0.2))
);
}
#[test]
fn test_set_text_color() {
let mut component = TextComponent::new("Test", 0, 0, 100, 50);
let new_color = Color::rgb(1.0, 0.5, 0.0);
component.set_text_color(new_color);
assert_eq!(component.current_text_color(), new_color);
assert!(!component.text_color.is_animating());
}
#[test]
fn test_animate_text_color() {
let mut component = TextComponent::new("Test", 0, 0, 100, 50);
let target_color = Color::rgb(0.0, 0.0, 1.0);
component.animate_text_color(target_color, Duration::from_millis(100));
assert!(component.text_color.is_animating());
assert_eq!(component.text_color.target(), target_color);
}
#[test]
fn test_set_background_color() {
let mut component = TextComponent::new("Test", 0, 0, 100, 50);
let bg_color = Color::rgb(0.3, 0.3, 0.3);
component.set_background_color(bg_color);
assert_eq!(component.current_background_color(), Some(bg_color));
if let Some(ref bg) = component.background_color {
assert!(!bg.is_animating());
}
}
#[test]
fn test_animate_background_color() {
let mut component = TextComponent::new("Test", 0, 0, 100, 50);
let target_bg = Color::rgb(0.8, 0.8, 0.8);
component.animate_background_color(target_bg, Duration::from_millis(200));
assert!(component.background_color.is_some());
if let Some(ref bg) = component.background_color {
assert!(bg.is_animating());
assert_eq!(bg.target(), target_bg);
}
}
#[test]
fn test_animate_background_color_when_already_set() {
let mut component =
TextComponent::new("Test", 0, 0, 100, 50).with_background(Color::rgb(0.1, 0.1, 0.1));
let new_target = Color::rgb(0.9, 0.9, 0.9);
component.animate_background_color(new_target, Duration::from_millis(150));
if let Some(ref bg) = component.background_color {
assert!(bg.is_animating());
assert_eq!(bg.target(), new_target);
}
}
#[test]
fn test_component_update_no_animation() {
let mut component = TextComponent::new("Test", 0, 0, 100, 50);
let needs_redraw = component.update(0.016);
assert!(!needs_redraw);
}
#[test]
fn test_component_update_with_text_animation() {
let mut component = TextComponent::new("Test", 0, 0, 100, 50);
component.animate_text_color(Color::rgb(1.0, 0.0, 0.0), Duration::from_millis(50));
let needs_redraw = component.update(0.016);
assert!(needs_redraw);
thread::sleep(Duration::from_millis(60));
let mut needs_redraw = true;
while needs_redraw {
needs_redraw = component.update(0.016);
}
assert!(!component.update(0.016));
}
#[test]
fn test_component_update_with_background_animation() {
let mut component = TextComponent::new("Test", 0, 0, 100, 50);
component.animate_background_color(Color::rgb(0.5, 0.5, 0.5), Duration::from_millis(50));
let needs_redraw = component.update(0.016);
assert!(needs_redraw);
thread::sleep(Duration::from_millis(60));
let mut needs_redraw = true;
while needs_redraw {
needs_redraw = component.update(0.016);
}
assert!(!component.update(0.016));
}
#[test]
fn test_component_update_with_both_animations() {
let mut component = TextComponent::new("Test", 0, 0, 100, 50);
component.animate_text_color(Color::rgb(1.0, 0.0, 0.0), Duration::from_millis(100));
component.animate_background_color(Color::rgb(0.0, 1.0, 0.0), Duration::from_millis(100));
let needs_redraw = component.update(0.016);
assert!(needs_redraw);
assert!(component.text_color.is_animating());
if let Some(ref bg) = component.background_color {
assert!(bg.is_animating());
}
}
#[test]
fn test_color_property_trait_implementation() {
let mut component = TextComponent::new("Test", 0, 0, 100, 50);
let initial_color = component.color().current();
assert_eq!(initial_color, Color::WHITE);
component.set_color(Color::rgb(0.5, 0.5, 0.5));
assert_eq!(component.color().current(), Color::rgb(0.5, 0.5, 0.5));
component.animate_color_to(Color::rgb(1.0, 1.0, 0.0), Duration::from_millis(100));
assert!(component.color().is_animating());
assert_eq!(component.color().target(), Color::rgb(1.0, 1.0, 0.0));
}
#[test]
fn test_color_property_update_color() {
let mut component = TextComponent::new("Test", 0, 0, 100, 50);
let needs_redraw = component.update_color(0.016);
assert!(!needs_redraw);
component.animate_color_to(Color::rgb(1.0, 0.0, 1.0), Duration::from_millis(50));
let needs_redraw = component.update_color(0.016);
assert!(needs_redraw);
thread::sleep(Duration::from_millis(60));
let mut needs_redraw = true;
while needs_redraw {
needs_redraw = component.update_color(0.016);
}
assert!(!component.update_color(0.016));
}
#[test]
fn test_builder_pattern_chaining() {
let component = TextComponent::new("Test", 10, 20, 200, 100)
.with_color(Color::rgb(1.0, 0.0, 0.0))
.with_background(Color::rgb(0.0, 0.0, 0.0))
.with_alignment(Alignment::Center)
.centered();
assert_eq!(component.current_text_color(), Color::rgb(1.0, 0.0, 0.0));
assert_eq!(
component.current_background_color(),
Some(Color::rgb(0.0, 0.0, 0.0))
);
}
#[test]
fn test_text_component_is_visible() {
let component = TextComponent::new("Test", 0, 0, 100, 50);
assert!(component.is_visible());
}
#[test]
fn test_text_component_should_not_remove() {
let component = TextComponent::new("Test", 0, 0, 100, 50);
assert!(!component.should_remove());
}
#[test]
fn test_text_setting() {
let mut component = TextComponent::new("Initial", 0, 0, 100, 50);
component.set_text("Updated Text");
assert_eq!(component.bounds().width, 100);
}
#[test]
fn test_color_animations_are_independent() {
let mut component = TextComponent::new("Test", 0, 0, 100, 50);
component.animate_text_color(Color::rgb(1.0, 0.0, 0.0), Duration::from_millis(100));
component.animate_background_color(Color::rgb(0.0, 1.0, 0.0), Duration::from_millis(200));
assert!(component.text_color.is_animating());
if let Some(ref bg) = component.background_color {
assert!(bg.is_animating());
}
thread::sleep(Duration::from_millis(120));
while component.text_color.is_animating() {
component.update(0.016);
}
assert!(!component.text_color.is_animating());
if let Some(ref bg) = component.background_color {
assert!(bg.is_animating()); }
}
}