use crate::core::{Color, Font, HorizontalAlignment, Point, Rect};
use crate::event::{Event, EventHandler};
use crate::render::RenderContext;
use crate::signal::{GenericSignal, Signal1};
use crate::undo::{TextSnapshotCommand, UndoStack};
use crate::widget::{BaseWidget, Draw, Widget, WidgetKind};
use std::cell::RefCell;
use std::rc::Rc;
pub struct SearchBar {
base: BaseWidget,
text: String,
placeholder: String,
is_active: bool,
cancel_button_visible: bool,
pub text_changed: Signal1<String>,
pub search_submitted: Signal1<String>,
pub canceled: GenericSignal,
undo_stack: UndoStack,
history_target: Rc<RefCell<String>>,
restoring_history: bool,
}
impl SearchBar {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::SearchBar, geometry, "SearchBar"),
text: String::new(),
placeholder: "Search".to_string(),
is_active: false,
cancel_button_visible: true,
text_changed: Signal1::new(),
search_submitted: Signal1::new(),
canceled: GenericSignal::new(),
undo_stack: UndoStack::new(),
history_target: Rc::new(RefCell::new(String::new())),
restoring_history: false,
}
}
pub fn text(&self) -> &str {
&self.text
}
pub fn set_text(&mut self, text: impl Into<String>) {
let new_text = text.into();
if self.text != new_text {
let before = self.text.clone();
self.text = new_text.clone();
if !self.restoring_history {
*self.history_target.borrow_mut() = self.text.clone();
self.undo_stack.push(Box::new(TextSnapshotCommand::new(
self.history_target.clone(),
before,
self.text.clone(),
"search_bar_text",
)));
}
self.text_changed.emit(new_text);
self.base.request_redraw();
}
}
pub fn placeholder(&self) -> &str {
&self.placeholder
}
pub fn set_placeholder(&mut self, placeholder: impl Into<String>) {
self.placeholder = placeholder.into();
self.base.request_redraw();
}
pub fn set_active(&mut self, active: bool) {
if self.is_active != active {
self.is_active = active;
self.base.request_redraw();
}
}
pub fn is_active(&self) -> bool {
self.is_active
}
pub fn set_cancel_button_visible(&mut self, visible: bool) {
self.cancel_button_visible = visible;
self.base.request_redraw();
}
pub fn cancel_button_visible(&self) -> bool {
self.cancel_button_visible
}
pub fn clear(&mut self) {
if !self.text.is_empty() {
self.set_text(String::new());
}
}
pub fn undo(&mut self) -> bool {
if self.undo_stack.undo().is_err() {
return false;
}
self.restore_history_text();
true
}
pub fn redo(&mut self) -> bool {
if self.undo_stack.redo().is_err() {
return false;
}
self.restore_history_text();
true
}
pub fn can_undo(&self) -> bool {
self.undo_stack.can_undo()
}
pub fn can_redo(&self) -> bool {
self.undo_stack.can_redo()
}
fn restore_history_text(&mut self) {
self.restoring_history = true;
self.text = self.history_target.borrow().clone();
self.restoring_history = false;
self.text_changed.emit(self.text.clone());
self.base.request_redraw();
}
fn submit(&mut self) {
let text = self.text.clone();
if !text.is_empty() {
self.search_submitted.emit(text);
}
}
}
impl Widget for SearchBar {
fn base(&self) -> &BaseWidget {
&self.base
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn size_hint(&self) -> crate::core::Size {
crate::core::Size::new(200, 36)
}
}
impl Draw for SearchBar {
fn draw(&mut self, context: &mut RenderContext) {
let rect = self.geometry();
let is_enabled = self.base.is_enabled();
let cancel_width: u32 = if self.is_active && self.cancel_button_visible { 60 } else { 0 };
let field_width = rect.width.saturating_sub(cancel_width + 4);
let cancel_x = rect.x + rect.width as i32 - cancel_width as i32;
if self.is_active && self.cancel_button_visible {
let _cancel_rect = Rect::new(cancel_x, rect.y, cancel_width, rect.height);
let cancel_font = Font::simple("sans-serif", 14.0);
let cancel_text = "Cancel";
let metrics = context.measure_text(cancel_text, &cancel_font);
let text_x = cancel_x + (cancel_width as i32 - metrics.width as i32) / 2;
let text_y =
rect.y + (rect.height as i32 - metrics.height as i32) / 2 + metrics.ascent as i32;
context.draw_text(
Point::new(text_x, text_y),
cancel_text,
&cancel_font,
Color::rgba(52, 120, 246, 255),
HorizontalAlignment::Left,
);
}
let field_rect = Rect::new(rect.x, rect.y, field_width, rect.height);
let field_color = if !is_enabled {
Color::rgba(235, 235, 235, 200)
} else {
Color::rgba(200, 200, 205, 200)
};
let corner_radius = rect.height / 2;
context.fill_rounded_rect(field_rect, corner_radius, field_color);
let icon_size: u32 = 14;
let icon_left = rect.x + 10;
let icon_top = rect.y + (rect.height as i32 - icon_size as i32) / 2;
let icon_color = Color::rgba(140, 140, 140, 255);
context.draw_circle_stroke(
Point::new(icon_left + (icon_size / 2) as i32, icon_top + (icon_size / 2) as i32),
icon_size / 2,
icon_color,
2,
);
let handle_offset = icon_size as i32;
context.draw_line(
Point::new(icon_left + handle_offset - 2, icon_top + handle_offset - 2),
Point::new(icon_left + handle_offset + 3, icon_top + handle_offset + 3),
icon_color,
);
let text_left = icon_left + icon_size as i32 + 6;
let text_width = if self.text.is_empty() {
field_width.saturating_sub((text_left - rect.x) as u32)
} else {
field_width.saturating_sub((text_left - rect.x) as u32 + 24)
};
let font = Font::simple("sans-serif", 14.0);
if self.text.is_empty() {
let metrics = context.measure_text(&self.placeholder, &font);
if text_width >= metrics.width {
let text_y = rect.y
+ (rect.height as i32 - metrics.height as i32) / 2
+ metrics.ascent as i32;
context.draw_text(
Point::new(text_left, text_y),
&self.placeholder,
&font,
Color::rgba(160, 160, 160, 255),
HorizontalAlignment::Left,
);
}
} else {
let metrics = context.measure_text(&self.text, &font);
if text_width >= metrics.width {
let text_y = rect.y
+ (rect.height as i32 - metrics.height as i32) / 2
+ metrics.ascent as i32;
context.draw_text(
Point::new(text_left, text_y),
&self.text,
&font,
Color::rgba(40, 40, 40, 255),
HorizontalAlignment::Left,
);
}
let clear_x = rect.x + field_width as i32 - 22;
let clear_y = rect.y + (rect.height as i32 - 16) / 2;
let clear_center = Point::new(clear_x + 8, clear_y + 8);
context.fill_circle(clear_center, 8, Color::rgba(180, 180, 180, 200));
context.draw_line(
Point::new(clear_center.x - 3, clear_center.y - 3),
Point::new(clear_center.x + 3, clear_center.y + 3),
Color::WHITE,
);
context.draw_line(
Point::new(clear_center.x + 3, clear_center.y - 3),
Point::new(clear_center.x - 3, clear_center.y + 3),
Color::WHITE,
);
}
}
}
impl EventHandler for SearchBar {
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();
let cancel_width: u32 =
if self.is_active && self.cancel_button_visible { 60 } else { 0 };
let cancel_x = rect.x + rect.width as i32 - cancel_width as i32;
if self.is_active && self.cancel_button_visible && pos.x >= cancel_x {
self.is_active = false;
self.canceled.emit();
self.base.request_redraw();
return;
}
let field_width = rect.width.saturating_sub(cancel_width + 4);
let clear_x = rect.x + field_width as i32 - 22;
if !self.text.is_empty()
&& pos.x >= clear_x
&& pos.x < clear_x + 16
&& pos.y >= rect.y
&& pos.y < rect.y + rect.height as i32
{
self.clear();
return;
}
if !self.is_active {
self.set_active(true);
}
}
}
Event::KeyPress { key, modifiers } => {
if *key == 90 && *modifiers == 2 {
let _ = self.undo();
return;
}
if *key == 89 && *modifiers == 2 {
let _ = self.redo();
return;
}
if *key == 13 || *key == 10 {
self.submit();
} else if *key == 8 {
if !self.text.is_empty() {
let mut chars: Vec<char> = self.text.chars().collect();
chars.pop();
self.set_text(chars.into_iter().collect::<String>());
}
} else if *key >= 32 && *key <= 126 {
if let Some(c) = char::from_u32(*key) {
let mut new_text = self.text.clone();
new_text.push(c);
self.set_text(new_text);
}
}
}
Event::TextInput { text } | Event::ImeCommit { text } => {
if !text.is_empty() {
let mut new_text = self.text.clone();
new_text.push_str(text);
self.set_text(new_text);
}
}
Event::ImePreedit { .. } => {}
_ => {
self.base.handle_event(event);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::Point;
use crate::widget::svg::render_to_svg;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
fn make_search_bar() -> SearchBar {
SearchBar::new(Rect::new(0, 0, 300, 40))
}
#[test]
fn search_bar_default_state() {
let sb = make_search_bar();
assert_eq!(sb.text(), "");
assert_eq!(sb.placeholder(), "Search");
assert!(!sb.is_active());
assert!(sb.cancel_button_visible());
assert_eq!(sb.kind(), WidgetKind::SearchBar);
}
#[test]
fn search_bar_set_text() {
let mut sb = make_search_bar();
sb.set_text("hello");
assert_eq!(sb.text(), "hello");
}
#[test]
fn search_bar_text_changed_signal() {
let mut sb = make_search_bar();
let captured = Arc::new(Mutex::new(None));
sb.text_changed.connect({
let captured = Arc::clone(&captured);
move |val: Arc<String>| {
*captured.lock().unwrap() = Some((*val).clone());
}
});
sb.set_text("test");
assert_eq!(captured.lock().unwrap().as_deref(), Some("test"));
}
#[test]
fn search_bar_submit_signal() {
let mut sb = make_search_bar();
sb.set_text("query");
let captured = Arc::new(Mutex::new(None));
sb.search_submitted.connect({
let captured = Arc::clone(&captured);
move |val: Arc<String>| {
*captured.lock().unwrap() = Some((*val).clone());
}
});
sb.handle_event(&Event::KeyPress { key: 13, modifiers: 0 });
assert_eq!(captured.lock().unwrap().as_deref(), Some("query"));
}
#[test]
fn search_bar_clear_button_and_signal() {
let mut sb = make_search_bar();
sb.set_text("hello");
let captured = Arc::new(Mutex::new(None));
sb.text_changed.connect({
let captured = Arc::clone(&captured);
move |val: Arc<String>| {
*captured.lock().unwrap() = Some((*val).clone());
}
});
sb.clear();
assert_eq!(sb.text(), "");
assert_eq!(captured.lock().unwrap().as_deref(), Some(""));
}
#[test]
fn search_bar_cancel_signal() {
let mut sb = make_search_bar();
sb.set_active(true);
let captured = Arc::new(AtomicBool::new(false));
sb.canceled.connect({
let captured = Arc::clone(&captured);
move || {
captured.store(true, Ordering::SeqCst);
}
});
sb.handle_event(&Event::MousePress { pos: Point::new(260, 20), button: 1 });
assert!(!sb.is_active());
assert!(captured.load(Ordering::SeqCst));
}
#[test]
fn search_bar_set_placeholder() {
let mut sb = make_search_bar();
sb.set_placeholder("Find...");
assert_eq!(sb.placeholder(), "Find...");
}
#[test]
fn search_bar_svg_output() {
let mut sb = make_search_bar();
sb.set_text("foo");
let svg = render_to_svg(&mut sb);
assert!(svg.starts_with("<svg"));
assert!(svg.ends_with("</svg>"));
}
#[test]
fn search_bar_undo_redo_restores_text() {
let mut sb = make_search_bar();
sb.set_text("alpha");
sb.set_text("beta");
assert!(sb.can_undo());
assert!(sb.undo());
assert_eq!(sb.text(), "alpha");
assert!(sb.can_redo());
assert!(sb.redo());
assert_eq!(sb.text(), "beta");
}
#[test]
fn search_bar_key_shortcuts_drive_history() {
let mut sb = make_search_bar();
sb.handle_event(&Event::KeyPress { key: 97, modifiers: 0 });
sb.handle_event(&Event::KeyPress { key: 98, modifiers: 0 });
assert_eq!(sb.text(), "ab");
sb.handle_event(&Event::KeyPress { key: 90, modifiers: 2 });
assert_eq!(sb.text(), "a");
sb.handle_event(&Event::KeyPress { key: 89, modifiers: 2 });
assert_eq!(sb.text(), "ab");
}
#[test]
fn search_bar_accepts_unicode_text_input_events() {
let mut sb = make_search_bar();
sb.handle_event(&Event::text_input("你好"));
sb.handle_event(&Event::ime_commit("世界"));
assert_eq!(sb.text(), "你好世界");
assert!(sb.undo());
assert_eq!(sb.text(), "你好");
}
}