use super::item::ToastLevel;
use crate::core::{Color, Font, HorizontalAlignment, Point, Rect};
use crate::event::{Event, EventHandler};
use crate::render::RenderContext;
use crate::signal::Signal1;
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};
use crate::{impl_widget_property_hooks, property_names_of};
pub struct Toast {
base: BaseWidget,
message: String,
level: ToastLevel,
ttl_ms: u32,
dismissible: bool,
pub dismissed: Signal1<String>,
}
impl Toast {
pub fn new(geometry: Rect, message: impl Into<String>) -> Self {
Self {
base: BaseWidget::new(WidgetKind::Toast, geometry, "Toast"),
message: message.into(),
level: ToastLevel::Info,
ttl_ms: 3000,
dismissible: true,
dismissed: Signal1::new(),
}
}
pub fn message(&self) -> &str {
&self.message
}
pub fn set_message(&mut self, message: impl Into<String>) {
self.message = message.into();
self.base.request_redraw();
}
pub fn level(&self) -> ToastLevel {
self.level
}
pub fn set_level(&mut self, level: ToastLevel) {
self.level = level;
self.base.request_redraw();
}
pub fn ttl_ms(&self) -> u32 {
self.ttl_ms
}
pub fn set_ttl_ms(&mut self, ttl_ms: u32) {
self.ttl_ms = ttl_ms.max(100);
}
pub fn is_dismissible(&self) -> bool {
self.dismissible
}
pub fn set_dismissible(&mut self, dismissible: bool) {
self.dismissible = dismissible;
self.base.request_redraw();
}
pub fn dismiss(&mut self) {
self.dismissed.emit(self.message.clone());
}
fn close_rect(&self) -> Option<Rect> {
if !self.dismissible {
return None;
}
let rect = self.geometry();
let size = 14.min(rect.height);
Some(Rect::new(
rect.x + rect.width as i32 - size as i32 - 6,
rect.y + (rect.height as i32 - size as i32) / 2,
size,
size,
))
}
fn is_over_close(&self, pos: Point) -> bool {
self.close_rect().is_some_and(|close| {
pos.x >= close.x
&& pos.x < close.x + close.width as i32
&& pos.y >= close.y
&& pos.y < close.y + close.height as i32
})
}
}
impl Widget for Toast {
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(320, 48)
}
impl_draw_bridge!();
impl_widget_property_hooks!();
}
impl WidgetProperties for Toast {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
match name {
"message" | "text" => Ok(CapabilityValue::String(self.message.clone())),
"level" => Ok(CapabilityValue::String(toast_level_token(self.level).to_string())),
"ttl_ms" => Ok(CapabilityValue::UInt(self.ttl_ms as u64)),
"dismissible" => Ok(CapabilityValue::Bool(self.dismissible)),
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
match name {
"message" | "text" => match value {
CapabilityValue::String(text) => {
self.set_message(text);
Ok(())
}
_ => Err(CapabilityAccessError::TypeMismatch),
},
"level" => match value {
CapabilityValue::String(token) => {
let level =
parse_toast_level(&token).ok_or(CapabilityAccessError::TypeMismatch)?;
self.set_level(level);
Ok(())
}
_ => Err(CapabilityAccessError::TypeMismatch),
},
"ttl_ms" => match value {
CapabilityValue::UInt(ms) => {
self.set_ttl_ms(u32::try_from(ms).unwrap_or(u32::MAX));
Ok(())
}
_ => Err(CapabilityAccessError::TypeMismatch),
},
"dismissible" => match value {
CapabilityValue::Bool(flag) => {
self.set_dismissible(flag);
Ok(())
}
_ => Err(CapabilityAccessError::TypeMismatch),
},
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of!["message", "level", "ttl_ms", "dismissible", BASE_PROPERTY_NAMES]
}
}
impl EventHandler for Toast {
fn handle_event(&mut self, event: &Event) {
self.base.handle_event(event);
if !self.base.is_enabled() {
return;
}
match event {
Event::MousePress { pos, button: 1 } if self.is_over_close(*pos) => {
self.dismiss();
}
Event::KeyPress { key: 27, modifiers: _ } => {
self.dismiss();
}
_ => {}
}
}
}
impl Draw for Toast {
fn draw(&mut self, context: &mut RenderContext) {
let rect = self.geometry();
let (accent, background) = match self.level {
ToastLevel::Info => (Color::rgb(76, 124, 201), Color::rgb(240, 245, 253)),
ToastLevel::Success => (Color::rgb(58, 161, 103), Color::rgb(240, 250, 244)),
ToastLevel::Warning => (Color::rgb(220, 158, 54), Color::rgb(253, 249, 240)),
ToastLevel::Error => (Color::rgb(209, 85, 74), Color::rgb(253, 242, 241)),
};
context.fill_rect(rect, background);
context.draw_rect(rect, Color::rgb(208, 214, 223));
context.fill_rect(Rect::new(rect.x, rect.y, 4, rect.height), accent);
let text_x = rect.x + 12;
context.draw_text(
Point::new(text_x, rect.y + (rect.height as i32 + 12) / 2),
&self.message,
&Font::default(),
Color::rgb(44, 55, 72),
HorizontalAlignment::Left,
);
if let Some(close) = self.close_rect() {
context.draw_line(
Point::new(close.x + 3, close.y + 3),
Point::new(close.x + close.width as i32 - 3, close.y + close.height as i32 - 3),
Color::rgb(120, 131, 148),
);
context.draw_line(
Point::new(close.x + close.width as i32 - 3, close.y + 3),
Point::new(close.x + 3, close.y + close.height as i32 - 3),
Color::rgb(120, 131, 148),
);
}
}
}
fn toast_level_token(level: ToastLevel) -> &'static str {
match level {
ToastLevel::Info => "info",
ToastLevel::Success => "success",
ToastLevel::Warning => "warning",
ToastLevel::Error => "error",
}
}
fn parse_toast_level(token: &str) -> Option<ToastLevel> {
match token {
"info" => Some(ToastLevel::Info),
"success" => Some(ToastLevel::Success),
"warning" => Some(ToastLevel::Warning),
"error" => Some(ToastLevel::Error),
_ => None,
}
}