use crate::core::{Font, HorizontalAlignment, ObjectId, Point, Rect, Size};
use crate::impl_widget_property_hooks;
use crate::property_names_of;
use crate::render::RenderContext;
use crate::signal::GenericSignal;
use crate::widget::capability::coercion::expect_string;
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::{BaseWidget, Draw, Widget, WidgetKind};
pub struct PopupWindow {
base: BaseWidget,
content_widget: Option<ObjectId>,
title: String,
pub opened: GenericSignal,
pub closed: GenericSignal,
}
impl PopupWindow {
pub fn new(geometry: Rect) -> Self {
Self::with_title(String::new(), geometry)
}
pub fn with_title(title: String, geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::PopupWindow, geometry, "PopupWindow"),
content_widget: None,
title,
opened: GenericSignal::new(),
closed: GenericSignal::new(),
}
}
pub fn title(&self) -> &str {
&self.title
}
pub fn set_title(&mut self, title: String) {
self.title = title;
self.base.request_redraw();
}
pub fn content_widget(&self) -> Option<ObjectId> {
self.content_widget
}
pub fn set_content_widget(&mut self, widget: Option<ObjectId>) {
if let Some(old) = self.content_widget {
self.base.remove_child(old);
}
self.content_widget = widget;
if let Some(id) = widget {
self.base.add_child(id);
}
self.base.request_redraw();
}
pub fn open(&mut self) {
self.show();
self.opened.emit();
}
pub fn close(&mut self) {
self.hide();
self.closed.emit();
}
}
impl Widget for PopupWindow {
fn base(&self) -> &BaseWidget {
&self.base
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn size_hint(&self) -> Size {
crate::core::Size::new(300, 200)
}
fn as_draw_mut(&mut self) -> Option<&mut dyn crate::widget::Draw> {
Some(self)
}
impl_widget_property_hooks!();
}
impl WidgetProperties for PopupWindow {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
match name {
"title" => Ok(CapabilityValue::String(self.title().to_string())),
"has_content" => Ok(CapabilityValue::Bool(self.content_widget().is_some())),
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
match name {
"title" => {
self.set_title(expect_string(value)?);
Ok(())
}
"has_content" => Err(CapabilityAccessError::ReadOnlyProperty),
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of!["title", "has_content", BASE_PROPERTY_NAMES]
}
}
impl Draw for PopupWindow {
fn draw(&mut self, context: &mut RenderContext) {
let rect = self.base.geometry();
if rect.width == 0 || rect.height == 0 {
return;
}
use crate::core::Color;
context.fill_rect(rect, Color::rgb(255, 255, 255));
context.draw_rect(rect, Color::rgb(120, 120, 120));
if self.title.is_empty() {
return;
}
let bar_height = TITLE_BAR_HEIGHT.min(rect.height);
context.fill_rect(
Rect::new(rect.x, rect.y, rect.width, bar_height),
Color::rgb(240, 240, 240),
);
context.draw_line(
Point::new(rect.x, rect.y + bar_height as i32),
Point::new(rect.x + rect.width as i32, rect.y + bar_height as i32),
Color::rgb(120, 120, 120),
);
context.draw_text(
Point::new(rect.x + 8, rect.y + (bar_height / 2) as i32),
&self.title,
&Font::default(),
Color::rgb(40, 40, 40),
HorizontalAlignment::Left,
);
}
}
pub const TITLE_BAR_HEIGHT: u32 = 24;
impl PopupWindow {
pub fn content_rect(&self) -> Rect {
let rect = self.base.geometry();
if self.title.is_empty() {
return rect;
}
let inset = TITLE_BAR_HEIGHT.min(rect.height);
Rect::new(rect.x, rect.y + inset as i32, rect.width, rect.height - inset)
}
}
impl crate::event::EventHandler for PopupWindow {
fn handle_event(&mut self, event: &crate::event::Event) {
if !self.base.is_enabled() {
return;
}
match event {
crate::event::Event::MousePress { pos: _, button } if *button == 1 => {
self.base.set_mouse_pressed(true);
}
crate::event::Event::MouseRelease { pos: _, button } if *button == 1 => {
self.base.set_mouse_pressed(false);
}
_ => { }
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::object::Object;
use std::sync::{Arc, Mutex};
#[test]
fn popup_open_close_emits_lifecycle_signals() {
let mut popup = PopupWindow::new(Rect::new(0, 0, 120, 80));
let opened = Arc::new(Mutex::new(0usize));
let closed = Arc::new(Mutex::new(0usize));
let opened_sink = opened.clone();
popup.opened.connect(move || {
if let Ok(mut count) = opened_sink.lock() {
*count += 1;
}
});
let closed_sink = closed.clone();
popup.closed.connect(move || {
if let Ok(mut count) = closed_sink.lock() {
*count += 1;
}
});
popup.open();
popup.close();
assert!(!popup.is_visible());
assert_eq!(*opened.lock().expect("opened lock poisoned"), 1);
assert_eq!(*closed.lock().expect("closed lock poisoned"), 1);
}
#[test]
fn popup_replaces_content_widget_child_binding() {
let mut popup = PopupWindow::new(Rect::new(0, 0, 120, 80));
let old_id = Object::new("OldContent").id();
let new_id = Object::new("NewContent").id();
popup.set_content_widget(Some(old_id));
assert_eq!(popup.content_widget(), Some(old_id));
assert_eq!(popup.children(), &[old_id]);
popup.set_content_widget(Some(new_id));
assert_eq!(popup.content_widget(), Some(new_id));
assert_eq!(popup.children(), &[new_id]);
}
#[test]
fn popup_with_a_title_paints_chrome_the_titleless_one_does_not() {
let mut titleless = PopupWindow::new(Rect::new(0, 0, 160, 100));
let mut titled = PopupWindow::with_title("Details".to_string(), Rect::new(0, 0, 160, 100));
let plain = crate::widget::svg::render_to_svg(&mut titleless);
let decorated = crate::widget::svg::render_to_svg(&mut titled);
assert_ne!(plain, decorated, "a titled popup must paint more than a titleless one");
assert!(decorated.contains("Details"), "the title text must appear in the rendered output");
}
#[test]
fn popup_content_rect_insets_only_when_a_title_is_present() {
let titleless = PopupWindow::new(Rect::new(0, 0, 160, 100));
assert_eq!(titleless.content_rect(), Rect::new(0, 0, 160, 100));
let titled = PopupWindow::with_title("T".to_string(), Rect::new(0, 0, 160, 100));
assert_eq!(
titled.content_rect(),
Rect::new(0, TITLE_BAR_HEIGHT as i32, 160, 100 - TITLE_BAR_HEIGHT)
);
}
#[test]
fn popup_title_bar_clamps_to_a_short_popup() {
let mut tiny = PopupWindow::with_title("T".to_string(), Rect::new(0, 0, 60, 8));
let _ = crate::widget::svg::render_to_svg(&mut tiny);
assert_eq!(tiny.content_rect().height, 0);
}
}