use crate::context::Context;
use crate::render::Buffer;
use crate::render::style::Style;
use crate::util::Rect;
use crate::ui::{
Widget, BaseWidget, WidgetId, WidgetState, UIEvent, UIResult,
next_widget_id
};
use unicode_width::{UnicodeWidthStr, UnicodeWidthChar};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TextAlign {
Left,
Center,
Right,
}
#[derive(Debug, Clone)]
pub enum LabelAnimation {
Spotlight {
highlight_style: Style,
frames_per_char: usize,
scale_amplitude: f32,
},
Wave {
amplitude: f32,
wavelength: f32,
speed: f32,
},
FadeIn {
frames_per_char: usize,
loop_anim: bool,
},
Typewriter {
frames_per_char: usize,
show_cursor: bool,
loop_anim: bool,
},
}
pub struct Label {
base: BaseWidget,
text: String,
align: TextAlign,
wrap: bool,
animation: Option<LabelAnimation>,
frame: usize,
}
impl Label {
pub fn new(text: &str) -> Self {
Self {
base: BaseWidget::new(next_widget_id()),
text: text.to_string(),
align: TextAlign::Left,
wrap: false,
animation: None,
frame: 0,
}
}
pub fn with_style(mut self, style: Style) -> Self {
self.base.style = style;
self
}
pub fn with_align(mut self, align: TextAlign) -> Self {
self.align = align;
self
}
pub fn with_wrap(mut self, wrap: bool) -> Self {
self.wrap = wrap;
self
}
pub fn with_animation(mut self, animation: LabelAnimation) -> Self {
self.animation = Some(animation);
self
}
pub fn with_spotlight(mut self, highlight_style: Style, frames_per_char: usize, scale_amplitude: f32) -> Self {
self.animation = Some(LabelAnimation::Spotlight {
highlight_style,
frames_per_char: frames_per_char.max(1),
scale_amplitude,
});
self
}
pub fn with_wave(mut self, amplitude: f32, wavelength: f32, speed: f32) -> Self {
self.animation = Some(LabelAnimation::Wave {
amplitude,
wavelength: wavelength.max(1.0),
speed,
});
self
}
pub fn with_fade_in(mut self, frames_per_char: usize, loop_anim: bool) -> Self {
self.animation = Some(LabelAnimation::FadeIn {
frames_per_char: frames_per_char.max(1),
loop_anim,
});
self
}
pub fn with_typewriter(mut self, frames_per_char: usize, show_cursor: bool, loop_anim: bool) -> Self {
self.animation = Some(LabelAnimation::Typewriter {
frames_per_char: frames_per_char.max(1),
show_cursor,
loop_anim,
});
self
}
pub fn set_text(&mut self, text: &str) {
if self.text != text {
self.text = text.to_string();
self.mark_dirty();
}
}
pub fn text(&self) -> &str {
&self.text
}
pub fn set_align(&mut self, align: TextAlign) {
if self.align != align {
self.align = align;
self.mark_dirty();
}
}
pub fn set_wrap(&mut self, wrap: bool) {
if self.wrap != wrap {
self.wrap = wrap;
self.mark_dirty();
}
}
pub fn set_animation(&mut self, animation: Option<LabelAnimation>) {
self.animation = animation;
self.frame = 0;
self.mark_dirty();
}
pub fn animation(&self) -> Option<&LabelAnimation> {
self.animation.as_ref()
}
}
impl Widget for Label {
fn id(&self) -> WidgetId { self.base.id }
fn bounds(&self) -> Rect { self.base.bounds }
fn set_bounds(&mut self, bounds: Rect) {
self.base.bounds = bounds;
self.base.state.dirty = true;
}
fn state(&self) -> &WidgetState { &self.base.state }
fn state_mut(&mut self) -> &mut WidgetState { &mut self.base.state }
fn as_any(&self) -> &dyn std::any::Any { self }
fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
fn update(&mut self, _dt: f32, _ctx: &mut Context) -> UIResult<()> {
if self.animation.is_some() {
self.frame = self.frame.wrapping_add(1);
self.mark_dirty();
}
Ok(())
}
fn render(&self, buffer: &mut Buffer, _ctx: &Context) -> UIResult<()> {
if !self.state().visible {
return Ok(());
}
let bounds = self.bounds();
if bounds.width == 0 || bounds.height == 0 {
return Ok(());
}
let style = self.base.style;
if let Some(ref anim) = self.animation {
return self.render_animated(buffer, style, anim);
}
if self.wrap {
self.render_wrapped(buffer, style)?;
} else {
self.render_single_line(buffer, style)?;
}
Ok(())
}
fn handle_event(&mut self, _event: &UIEvent, _ctx: &mut Context) -> UIResult<bool> {
Ok(false)
}
fn preferred_size(&self, available: Rect) -> Rect {
if self.text.is_empty() {
return Rect::new(available.x, available.y, 0, 1);
}
if self.wrap {
let lines = crate::ui::text_util::wrap_text(&self.text, available.width);
let height = (lines.len() as u16).min(available.height);
let width = if lines.is_empty() {
0
} else {
lines.iter()
.map(|line| line.width() as u16)
.max()
.unwrap_or(0)
.min(available.width)
};
Rect::new(available.x, available.y, width, height)
} else {
let width = (self.text.width() as u16).min(available.width);
Rect::new(available.x, available.y, width, 1)
}
}
}
impl Label {
fn visual_text_width(&self) -> u16 {
let scale_x = self.base.style.scale_x.unwrap_or(1.0);
if scale_x > 1.0 {
(self.text.width() as f32 * scale_x).ceil() as u16
} else {
self.text.width() as u16
}
}
fn render_single_line(&self, buffer: &mut Buffer, style: Style) -> UIResult<()> {
let bounds = self.bounds();
let text_width = self.visual_text_width();
if text_width == 0 {
return Ok(());
}
let buffer_area = *buffer.area();
if bounds.y >= buffer_area.y + buffer_area.height || bounds.x >= buffer_area.x + buffer_area.width {
return Ok(());
}
let start_x = match self.align {
TextAlign::Left => bounds.x,
TextAlign::Center => bounds.x + (bounds.width.saturating_sub(text_width)) / 2,
TextAlign::Right => bounds.x + bounds.width.saturating_sub(text_width),
};
if start_x < bounds.x + bounds.width && start_x < buffer_area.x + buffer_area.width {
buffer.set_string(start_x, bounds.y, &self.text, style);
}
Ok(())
}
fn render_wrapped(&self, buffer: &mut Buffer, style: Style) -> UIResult<()> {
let bounds = self.bounds();
let lines = crate::ui::text_util::wrap_text(&self.text, bounds.width);
let buffer_area = *buffer.area();
if bounds.y >= buffer_area.y + buffer_area.height || bounds.x >= buffer_area.x + buffer_area.width {
return Ok(());
}
let scale_x = self.base.style.scale_x.unwrap_or(1.0);
for (i, line) in lines.iter().enumerate() {
let y = bounds.y + i as u16;
if y >= bounds.y + bounds.height || y >= buffer_area.y + buffer_area.height {
break;
}
let line_width = if scale_x > 1.0 {
(line.width() as f32 * scale_x).ceil() as u16
} else {
line.width() as u16
};
let start_x = match self.align {
TextAlign::Left => bounds.x,
TextAlign::Center => bounds.x + (bounds.width.saturating_sub(line_width)) / 2,
TextAlign::Right => bounds.x + bounds.width.saturating_sub(line_width),
};
if start_x < bounds.x + bounds.width && start_x < buffer_area.x + buffer_area.width {
buffer.set_string(start_x, y, line, style);
}
}
Ok(())
}
fn aligned_start_x(&self, text_width: u16) -> u16 {
let bounds = self.bounds();
match self.align {
TextAlign::Left => bounds.x,
TextAlign::Center => bounds.x + bounds.width.saturating_sub(text_width) / 2,
TextAlign::Right => bounds.x + bounds.width.saturating_sub(text_width),
}
}
fn render_animated(&self, buffer: &mut Buffer, base_style: Style, anim: &LabelAnimation) -> UIResult<()> {
let bounds = self.bounds();
let buffer_area = *buffer.area();
if bounds.y >= buffer_area.y + buffer_area.height
|| bounds.x >= buffer_area.x + buffer_area.width
{
return Ok(());
}
let text_width = self.visual_text_width();
if text_width == 0 {
return Ok(());
}
let start_x = self.aligned_start_x(text_width);
match anim {
LabelAnimation::Spotlight { highlight_style, frames_per_char, scale_amplitude } => {
self.render_spotlight(buffer, base_style, start_x, *highlight_style, *frames_per_char, *scale_amplitude);
}
LabelAnimation::Wave { amplitude, wavelength, speed } => {
self.render_wave(buffer, base_style, start_x, *amplitude, *wavelength, *speed);
}
LabelAnimation::FadeIn { frames_per_char, loop_anim } => {
self.render_fade_in(buffer, base_style, start_x, *frames_per_char, *loop_anim);
}
LabelAnimation::Typewriter { frames_per_char, show_cursor, loop_anim } => {
self.render_typewriter(buffer, base_style, start_x, *frames_per_char, *show_cursor, *loop_anim);
}
}
Ok(())
}
fn render_spotlight(&self, buffer: &mut Buffer, base_style: Style, start_x: u16,
highlight_style: Style, frames_per_char: usize, scale_amplitude: f32) {
let bounds = self.bounds();
let buffer_area = *buffer.area();
let char_count = self.text.chars().count();
if char_count == 0 { return; }
let cycle_len = char_count * frames_per_char;
let frame_in_cycle = self.frame % cycle_len;
let active_idx = frame_in_cycle / frames_per_char;
let progress = (frame_in_cycle % frames_per_char) as f32 / frames_per_char as f32;
let active_scale = 1.0 + scale_amplitude * (progress * std::f32::consts::PI).sin();
let normal_style = base_style.scale(1.0, 1.0);
let mut x = start_x;
for (i, ch) in self.text.chars().enumerate() {
if x >= bounds.x + bounds.width || x >= buffer_area.x + buffer_area.width { break; }
let style = if i == active_idx {
highlight_style.scale_fixed_slot(active_scale, active_scale)
} else {
normal_style
};
buffer.set_string(x, bounds.y, ch.to_string(), style);
x += UnicodeWidthChar::width(ch).unwrap_or(1) as u16;
}
}
fn render_wave(&self, buffer: &mut Buffer, base_style: Style, start_x: u16,
amplitude: f32, wavelength: f32, speed: f32) {
let bounds = self.bounds();
let buffer_area = *buffer.area();
let pi2 = 2.0 * std::f32::consts::PI;
let mut x = start_x;
for (i, ch) in self.text.chars().enumerate() {
if x >= bounds.x + bounds.width || x >= buffer_area.x + buffer_area.width { break; }
let phase = speed * self.frame as f32 + i as f32 * pi2 / wavelength;
let scale_y = (1.0 + amplitude * phase.sin()).max(0.1);
let style = base_style.scale(1.0, scale_y);
buffer.set_string(x, bounds.y, ch.to_string(), style);
x += UnicodeWidthChar::width(ch).unwrap_or(1) as u16;
}
}
fn render_fade_in(&self, buffer: &mut Buffer, base_style: Style, start_x: u16,
frames_per_char: usize, loop_anim: bool) {
let bounds = self.bounds();
let buffer_area = *buffer.area();
let char_count = self.text.chars().count();
if char_count == 0 { return; }
let total_frames = char_count * frames_per_char;
let effective_frame = if loop_anim {
self.frame % total_frames
} else {
self.frame.min(total_frames)
};
let fully_revealed = effective_frame / frames_per_char;
let sub_progress = (effective_frame % frames_per_char) as f32 / frames_per_char as f32;
let normal_style = base_style.scale_uniform(1.0);
let mut x = start_x;
for (i, ch) in self.text.chars().enumerate() {
if x >= bounds.x + bounds.width || x >= buffer_area.x + buffer_area.width { break; }
if i < fully_revealed {
buffer.set_string(x, bounds.y, ch.to_string(), normal_style);
} else if i == fully_revealed && fully_revealed < char_count {
let scale = 0.1 + 0.9 * sub_progress;
let style = base_style.scale_uniform(scale);
buffer.set_string(x, bounds.y, ch.to_string(), style);
}
x += UnicodeWidthChar::width(ch).unwrap_or(1) as u16;
}
}
fn render_typewriter(&self, buffer: &mut Buffer, base_style: Style, start_x: u16,
frames_per_char: usize, show_cursor: bool, loop_anim: bool) {
let bounds = self.bounds();
let buffer_area = *buffer.area();
let char_count = self.text.chars().count();
if char_count == 0 { return; }
let total_frames = char_count * frames_per_char;
let loop_len = total_frames + frames_per_char;
let effective_frame = if loop_anim {
self.frame % loop_len
} else {
self.frame.min(total_frames)
};
let revealed = (effective_frame / frames_per_char).min(char_count);
let normal_style = base_style.scale_uniform(1.0);
let mut x = start_x;
for (i, ch) in self.text.chars().enumerate() {
if x >= bounds.x + bounds.width || x >= buffer_area.x + buffer_area.width { break; }
if i < revealed {
buffer.set_string(x, bounds.y, ch.to_string(), normal_style);
}
x += UnicodeWidthChar::width(ch).unwrap_or(1) as u16;
}
if show_cursor && revealed < char_count {
let cursor_x = start_x + self.text.chars().take(revealed).map(|c| UnicodeWidthChar::width(c).unwrap_or(1) as u16).sum::<u16>();
if cursor_x < bounds.x + bounds.width && cursor_x < buffer_area.x + buffer_area.width {
if (self.frame / 8).is_multiple_of(2) {
buffer.set_string(cursor_x, bounds.y, "▌", base_style.scale_uniform(1.0));
}
}
}
}
}