use crate::core::{Color, HorizontalAlignment, Rect, Size};
use crate::event::{Event, EventHandler};
use crate::render::RenderContext;
use crate::signal::{GenericSignal, Signal1};
use crate::widget::capability::coercion::{expect_bool, expect_string, expect_usize};
use crate::widget::capability::properties_trait::{base_property_get, base_property_set};
use crate::widget::capability::types::{CapabilityAccessError, CapabilityValue};
use crate::widget::capability::WidgetProperties;
use crate::widget::metrics::role_font;
use crate::widget::{BaseWidget, Draw, IconName, Widget, WidgetKind};
use crate::{impl_widget_property_hooks, property_names_of};
pub struct WebFindBar {
base: BaseWidget,
query: String,
match_index: usize,
match_count: usize,
case_sensitive: bool,
visible: bool,
pub search_requested: Signal1<(String, bool)>,
pub next_requested: GenericSignal,
pub previous_requested: GenericSignal,
pub closed: GenericSignal,
}
impl WebFindBar {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::WebFindBar, geometry, "WebFindBar"),
query: String::new(),
match_index: 0,
match_count: 0,
case_sensitive: false,
visible: false,
search_requested: Signal1::new(),
next_requested: GenericSignal::new(),
previous_requested: GenericSignal::new(),
closed: GenericSignal::new(),
}
}
pub fn open(&mut self) {
if self.visible {
return;
}
self.visible = true;
self.base.request_redraw();
}
pub fn close(&mut self) {
if !self.visible {
return;
}
self.visible = false;
self.base.request_redraw();
self.closed.emit();
}
pub fn is_open(&self) -> bool {
self.visible
}
pub fn query(&self) -> &str {
&self.query
}
pub fn set_query(&mut self, query: impl Into<String>) {
let query = query.into();
if self.query == query {
return;
}
self.query = query;
self.base.request_redraw();
self.search_requested.emit((self.query.clone(), self.case_sensitive));
}
pub fn match_progress(&self) -> (usize, usize) {
(self.match_index, self.match_count)
}
pub fn set_match_progress(&mut self, index: usize, count: usize) {
let index = index.min(count);
if self.match_index == index && self.match_count == count {
return;
}
self.match_index = index;
self.match_count = count;
self.base.request_redraw();
}
pub fn case_sensitive(&self) -> bool {
self.case_sensitive
}
pub fn set_case_sensitive(&mut self, case_sensitive: bool) {
if self.case_sensitive == case_sensitive {
return;
}
self.case_sensitive = case_sensitive;
self.base.request_redraw();
if !self.query.is_empty() {
self.search_requested.emit((self.query.clone(), self.case_sensitive));
}
}
pub fn next_match(&mut self) {
self.next_requested.emit();
}
pub fn previous_match(&mut self) {
self.previous_requested.emit();
}
}
impl Widget for WebFindBar {
fn base(&self) -> &BaseWidget {
&self.base
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn set_state_theme_hook(&mut self) {
crate::style::reapply_active_theme_state(self);
}
fn size_hint(&self) -> Size {
Size::new(280, 32)
}
impl_draw_bridge!();
impl_widget_property_hooks!();
fn event_signal_dyn(&self, name: &str) -> Option<crate::signal::EventSignalRef> {
use crate::signal::EventSignalRef;
match name {
"search_requested" => Some(EventSignalRef::mapped(
"search_requested",
&self.search_requested,
|(query, case_sensitive)| {
CapabilityValue::Tuple(crate::compat::Vec::from([
CapabilityValue::String(query.clone()),
CapabilityValue::Bool(*case_sensitive),
]))
},
)),
"next_requested" => Some(EventSignalRef::unit("next_requested", &self.next_requested)),
"previous_requested" => {
Some(EventSignalRef::unit("previous_requested", &self.previous_requested))
}
"closed" => Some(EventSignalRef::unit("closed", &self.closed)),
_ => None,
}
}
}
impl WidgetProperties for WebFindBar {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
match name {
"query" => Ok(CapabilityValue::String(self.query.clone())),
"case_sensitive" => Ok(CapabilityValue::Bool(self.case_sensitive)),
"visible" => Ok(CapabilityValue::Bool(self.visible)),
"match_index" => Ok(CapabilityValue::UInt(self.match_index as u64)),
"match_count" => Ok(CapabilityValue::UInt(self.match_count as u64)),
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
match name {
"query" => {
self.set_query(expect_string(value)?);
Ok(())
}
"case_sensitive" => {
self.set_case_sensitive(expect_bool(value)?);
Ok(())
}
"visible" => {
if expect_bool(value)? {
self.open();
} else {
self.close();
}
Ok(())
}
"match_index" => {
let index = expect_usize(value)?;
self.set_match_progress(index, self.match_count);
Ok(())
}
"match_count" => {
let count = expect_usize(value)?;
self.set_match_progress(self.match_index, count);
Ok(())
}
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of![
"query",
"case_sensitive",
"visible",
"match_index",
"match_count",
BASE_PROPERTY_NAMES
]
}
fn command(&mut self, name: &str) -> Result<(), CapabilityAccessError> {
match name {
"next" => {
self.next_match();
Ok(())
}
"previous" => {
self.previous_match();
Ok(())
}
"close" => {
self.close();
Ok(())
}
_ => Err(CapabilityAccessError::UnknownCommand),
}
}
}
impl Draw for WebFindBar {
fn draw(&mut self, context: &mut RenderContext) {
let rect = self.geometry();
if rect.width == 0 || rect.height == 0 {
return;
}
let style = self.base.style();
let ink = style.text_color.unwrap_or(Color::rgba(40, 40, 40, 255));
let surface = style.background_color.unwrap_or(Color::rgba(250, 250, 250, 255));
let font = &role_font(style, 12.0);
let fill = if self.visible { surface } else { surface.with_alpha(60) };
context.fill_rounded_rect(rect, 6, fill);
context.draw_rounded_rect_stroke(rect, 6, ink.with_alpha(60), 1);
if !self.visible {
return;
}
let actions = 84u32;
let text_width = rect.width.saturating_sub(actions + 12);
let shown = if self.query.is_empty() { "Find in page" } else { &self.query };
context.draw_text_fitted_cow(
Rect::new(rect.x + 8, rect.y + 8, text_width, rect.height.saturating_sub(16)),
shown,
font,
if self.query.is_empty() { ink.with_alpha(120) } else { ink },
HorizontalAlignment::Left,
);
let right = rect.right();
let counter = format!("{}/{}", self.match_index, self.match_count);
context.draw_text_fitted_cow(
Rect::new(right - 112, rect.y + 8, 36, rect.height.saturating_sub(16)),
&counter,
font,
ink.with_alpha(160),
HorizontalAlignment::Center,
);
for (x, icon) in [(right - 70, IconName::ChevronLeft), (right - 46, IconName::ChevronRight)]
{
crate::widget::draw_icon_at(
context,
Rect::new(x, rect.y + 8, 16, rect.height.saturating_sub(16)),
ink,
icon,
);
}
context.draw_text_fitted_cow(
Rect::new(right - 24, rect.y + 8, 16, rect.height.saturating_sub(16)),
"Aa",
font,
if self.case_sensitive { ink } else { ink.with_alpha(90) },
HorizontalAlignment::Center,
);
}
}
impl EventHandler for WebFindBar {
fn handle_event(&mut self, event: &Event) {
self.base.handle_event(event);
if !self.base.is_enabled() || !self.visible {
return;
}
match event {
Event::KeyPress { key: 27, .. } => self.close(),
Event::KeyPress { key: 13, modifiers } => {
if *modifiers & 1 != 0 {
self.previous_match();
} else {
self.next_match();
}
}
Event::KeyPress { key: 8, .. } => {
if self.query.pop().is_some() {
let query = self.query.clone();
self.base.request_redraw();
self.search_requested.emit((query, self.case_sensitive));
}
}
Event::TextInput { text } => {
self.query.push_str(text);
let query = self.query.clone();
self.base.request_redraw();
self.search_requested.emit((query, self.case_sensitive));
}
Event::MousePress { pos, button: 1, .. } => {
if !self.geometry().contains_point(*pos) {
return;
}
let right = self.geometry().right();
if pos.x >= right - 24 {
self.set_case_sensitive(!self.case_sensitive);
} else if pos.x >= right - 48 {
self.next_match();
} else if pos.x >= right - 72 {
self.previous_match();
}
}
_ => {}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_query_and_match_counter_are_bounded_by_the_reported_count() {
let mut bar = WebFindBar::new(Rect::new(0, 0, 320, 36));
bar.open();
assert!(bar.is_open());
bar.set_query("needle");
assert_eq!(bar.query(), "needle");
bar.set_match_progress(9, 3);
assert_eq!(bar.match_progress(), (3, 3));
bar.close();
assert!(!bar.is_open());
}
#[test]
fn the_property_route_round_trips_and_reports_searches() {
let mut bar = WebFindBar::new(Rect::new(0, 0, 320, 36));
let searches = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
let sink = searches.clone();
bar.search_requested.connect(move |value| {
let (query, case_sensitive) = &*value;
sink.lock().unwrap().push((query.clone(), *case_sensitive));
});
bar.set("query", CapabilityValue::String("rust".into())).unwrap();
bar.set("case_sensitive", CapabilityValue::Bool(true)).unwrap();
bar.set("match_count", CapabilityValue::UInt(7)).unwrap();
bar.set("match_index", CapabilityValue::UInt(2)).unwrap();
assert_eq!(bar.get("query").unwrap(), CapabilityValue::String("rust".into()));
assert_eq!(bar.get("case_sensitive").unwrap(), CapabilityValue::Bool(true));
assert_eq!(bar.get("match_count").unwrap(), CapabilityValue::UInt(7));
assert_eq!(bar.get("match_index").unwrap(), CapabilityValue::UInt(2));
assert_eq!(
searches.lock().unwrap().as_slice(),
[("rust".to_string(), false), ("rust".to_string(), true)]
);
let closed = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let flag = closed.clone();
bar.closed.connect(move || {
flag.store(true, std::sync::atomic::Ordering::SeqCst);
});
bar.set("visible", CapabilityValue::Bool(true)).unwrap();
assert!(bar.get("visible").unwrap() == CapabilityValue::Bool(true));
bar.set("visible", CapabilityValue::Bool(false)).unwrap();
assert_eq!(bar.get("visible").unwrap(), CapabilityValue::Bool(false));
assert!(closed.load(std::sync::atomic::Ordering::SeqCst));
assert!(bar.set("match_progress", CapabilityValue::UInt(1)).is_err());
assert!(bar.get("no_such_property").is_err());
}
#[test]
fn the_command_channel_emits_the_requests_the_host_answers() {
let mut bar = WebFindBar::new(Rect::new(0, 0, 320, 36));
bar.set("visible", CapabilityValue::Bool(true)).unwrap();
let nexts = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
let prevs = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
let closes = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
let n = nexts.clone();
bar.next_requested.connect(move || {
n.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
});
let p = prevs.clone();
bar.previous_requested.connect(move || {
p.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
});
let c = closes.clone();
bar.closed.connect(move || {
c.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
});
bar.command("next").unwrap();
bar.command("previous").unwrap();
bar.command("close").unwrap();
let order = std::sync::atomic::Ordering::SeqCst;
assert_eq!(nexts.load(order), 1);
assert_eq!(prevs.load(order), 1);
assert_eq!(closes.load(order), 1);
assert_eq!(bar.get("visible").unwrap(), CapabilityValue::Bool(false));
assert_eq!(bar.command("no_such_command"), Err(CapabilityAccessError::UnknownCommand));
}
}