use crate::core::{Color, Font, Point, Rect};
use crate::event::{Event, EventHandler};
use crate::render::RenderContext;
use crate::signal::Signal1;
use crate::widget::{BaseWidget, Draw, Widget, WidgetKind};
pub struct FloatingLabel {
base: BaseWidget,
text: String,
label: String,
placeholder: String,
is_focused: bool,
show_label_above: bool,
animation_progress: f32,
pub text_changed: Signal1<String>,
}
impl FloatingLabel {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::FloatingLabel, geometry, "FloatingLabel"),
text: String::new(),
label: String::new(),
placeholder: String::new(),
is_focused: false,
show_label_above: false,
animation_progress: 0.0,
text_changed: Signal1::new(),
}
}
pub fn text(&self) -> &str {
&self.text
}
pub fn set_text(&mut self, text: String) {
self.text = text;
self.update_label_state();
self.text_changed.emit(self.text.clone());
self.base.request_redraw();
}
pub fn label(&self) -> &str {
&self.label
}
pub fn set_label(&mut self, label: String) {
self.label = label;
self.update_label_state();
self.base.request_redraw();
}
pub fn placeholder(&self) -> &str {
&self.placeholder
}
pub fn set_placeholder(&mut self, placeholder: String) {
self.placeholder = placeholder;
self.base.request_redraw();
}
pub fn is_focused(&self) -> bool {
self.is_focused
}
pub fn is_empty(&self) -> bool {
self.text.is_empty()
}
pub fn set_focused(&mut self, focused: bool) {
if self.is_focused != focused {
self.is_focused = focused;
self.update_label_state();
self.base.request_redraw();
}
}
fn update_label_state(&mut self) {
let should_float = self.is_focused || !self.text.is_empty();
if should_float != self.show_label_above {
self.show_label_above = should_float;
self.animation_progress = if should_float { 1.0 } else { 0.0 };
}
}
}
impl Widget for FloatingLabel {
fn base(&self) -> &BaseWidget {
&self.base
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
}
impl Draw for FloatingLabel {
fn draw(&mut self, context: &mut RenderContext) {
let rect = self.geometry();
let is_enabled = self.base.is_enabled();
let bg_color = if is_enabled {
Color::rgba(255, 255, 255, 255)
} else {
Color::rgba(240, 240, 240, 255)
};
context.fill_rounded_rect(rect, 4, bg_color);
let border_color = if self.is_focused {
Color::rgba(52, 120, 246, 255)
} else {
Color::rgba(180, 180, 180, 255)
};
let underline_y = rect.y + rect.height as i32 - 2;
let underline_rect = Rect::new(rect.x + 2, underline_y, rect.width - 4, 2);
context.fill_rounded_rect(underline_rect, 1, border_color);
let input_font = Font::simple("sans-serif", 14.0);
let label_font = Font::simple("sans-serif", 11.0);
let padding = 8i32;
let label_top_margin = 4i32;
let has_label = !self.label.is_empty();
let text_field_top_offset = if has_label && self.show_label_above {
16i32 } else {
6i32
};
if has_label {
let label_color = if self.is_focused {
Color::rgba(52, 120, 246, 255)
} else if is_enabled {
Color::rgba(100, 100, 100, 255)
} else {
Color::rgba(180, 180, 180, 255)
};
if self.show_label_above {
let label_x = rect.x + padding;
let label_y = rect.y + label_top_margin + 10;
context.draw_text(
Point::new(label_x, label_y),
&self.label,
&label_font,
label_color,
);
} else if self.text.is_empty() && !self.is_focused {
let label_x = rect.x + padding;
let label_y = rect.y + text_field_top_offset + 14;
context.draw_text(
Point::new(label_x, label_y),
&self.label,
&input_font,
Color::rgba(160, 160, 160, 255),
);
}
}
let show_placeholder =
self.text.is_empty() && !self.is_focused && !(has_label && !self.show_label_above);
if show_placeholder && !self.placeholder.is_empty() {
let placeholder_x = rect.x + padding;
let placeholder_y = rect.y + text_field_top_offset + 14;
context.draw_text(
Point::new(placeholder_x, placeholder_y),
&self.placeholder,
&input_font,
Color::rgba(180, 180, 180, 255),
);
}
if !self.text.is_empty() {
let text_x = rect.x + padding;
let text_y = rect.y + text_field_top_offset + 14;
let text_color = if is_enabled {
Color::rgba(0, 0, 0, 255)
} else {
Color::rgba(160, 160, 160, 255)
};
context.draw_text(Point::new(text_x, text_y), &self.text, &input_font, text_color);
}
}
}
impl EventHandler for FloatingLabel {
fn handle_event(&mut self, event: &Event) {
if !self.base.is_enabled() {
return;
}
match event {
Event::MousePress { pos, button } if *button == 1 => {
let rect = self.geometry();
if rect.contains_point(*pos) {
self.set_focused(true);
}
}
Event::KeyPress { key, modifiers: _ } => {
if *key == 9 {
self.set_focused(false);
} else if *key == 13 {
self.set_focused(false);
} else if *key >= 32 && *key <= 126 {
let c = char::from_u32(*key).unwrap_or(' ');
if self.is_focused {
let mut new_text = self.text.clone();
new_text.push(c);
self.set_text(new_text);
}
} else if *key == 8 {
if self.is_focused && !self.text.is_empty() {
let mut new_text = self.text.clone();
new_text.pop();
self.set_text(new_text);
}
}
}
_ => {
self.base.handle_event(event);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::widget::svg::render_to_svg;
use std::sync::{Arc, Mutex};
#[test]
fn floating_label_default_creation() {
let fl = FloatingLabel::new(Rect::new(0, 0, 200, 50));
assert_eq!(fl.kind(), WidgetKind::FloatingLabel);
assert!(fl.text().is_empty());
assert!(fl.label().is_empty());
assert!(fl.placeholder().is_empty());
assert!(!fl.is_focused());
assert!(fl.is_empty());
}
#[test]
fn floating_label_set_text_and_label() {
let mut fl = FloatingLabel::new(Rect::new(0, 0, 200, 50));
fl.set_label("Username".to_string());
assert_eq!(fl.label(), "Username");
fl.set_text("hello".to_string());
assert_eq!(fl.text(), "hello");
assert!(!fl.is_empty());
assert!(fl.show_label_above);
assert!((fl.animation_progress - 1.0).abs() < f32::EPSILON);
}
#[test]
fn floating_label_focus_toggle() {
let mut fl = FloatingLabel::new(Rect::new(0, 0, 200, 50));
fl.set_label("Email".to_string());
assert!(!fl.is_focused());
assert!(!fl.show_label_above);
fl.set_focused(true);
assert!(fl.is_focused());
assert!(fl.show_label_above);
fl.set_focused(false);
assert!(!fl.is_focused());
assert!(!fl.show_label_above);
}
#[test]
fn floating_label_text_changed_signal() {
let mut fl = FloatingLabel::new(Rect::new(0, 0, 200, 50));
let captured = Arc::new(Mutex::new(None::<String>));
fl.text_changed.connect({
let captured = Arc::clone(&captured);
move |val: Arc<String>| {
*captured.lock().unwrap() = Some(val.to_string());
}
});
fl.set_text("World".to_string());
assert_eq!(captured.lock().unwrap().as_deref(), Some("World"));
}
#[test]
fn floating_label_placeholder() {
let mut fl = FloatingLabel::new(Rect::new(0, 0, 200, 50));
fl.set_placeholder("Enter text here...".to_string());
assert_eq!(fl.placeholder(), "Enter text here...");
}
#[test]
fn floating_label_focus_on_click() {
let mut fl = FloatingLabel::new(Rect::new(0, 0, 200, 50));
assert!(!fl.is_focused());
fl.handle_event(&Event::MousePress { pos: Point::new(10, 10), button: 1 });
assert!(fl.is_focused());
}
#[test]
fn floating_label_keyboard_input() {
let mut fl = FloatingLabel::new(Rect::new(0, 0, 200, 50));
fl.set_focused(true);
fl.handle_event(&Event::KeyPress { key: 65, modifiers: 0 });
assert_eq!(fl.text(), "A");
fl.handle_event(&Event::KeyPress { key: 66, modifiers: 0 });
assert_eq!(fl.text(), "AB");
fl.handle_event(&Event::KeyPress { key: 8, modifiers: 0 });
assert_eq!(fl.text(), "A");
}
#[test]
fn floating_label_svg_output() {
let mut fl = FloatingLabel::new(Rect::new(0, 0, 200, 50));
fl.set_label("Name".to_string());
fl.set_placeholder("Enter name".to_string());
fl.set_text("John".to_string());
let svg = render_to_svg(&mut fl);
assert!(svg.starts_with("<svg"));
assert!(svg.ends_with("</svg>"));
}
}