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]
}
fn command(&mut self, name: &str) -> Result<(), CapabilityAccessError> {
match name {
"dismiss" => {
self.dismiss();
Ok(())
}
"set_message" | "set_level" => Err(CapabilityAccessError::OutOfRange),
_ => Err(CapabilityAccessError::UnknownCommand),
}
}
}
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 style = self.base.style().clone();
let theme = crate::style::resolved_theme_style("toast");
let resolved = style
.background_color
.or_else(|| theme.as_ref().and_then(|t| t.background_color))
.unwrap_or(Color::rgb(240, 245, 253));
let border = style
.border_color
.or_else(|| theme.as_ref().and_then(|t| t.border_color))
.unwrap_or_else(|| resolved.blend(&Color::BLACK, 0.15));
let text_color = style
.text_color
.or_else(|| theme.as_ref().and_then(|t| t.text_color))
.unwrap_or(Color::rgb(0, 0, 0));
let background = resolved.blend(&text_color, 0.08);
let accent = crate::style::semantic_color(match self.level {
ToastLevel::Info => crate::style::SemanticColor::Info,
ToastLevel::Success => crate::style::SemanticColor::Success,
ToastLevel::Warning => crate::style::SemanticColor::Warning,
ToastLevel::Error => crate::style::SemanticColor::Error,
})
.map(|token| token.blend(&background, 0.15))
.unwrap_or_else(|| background.blend(&text_color, 0.6));
let close_color = text_color.blend(&background, 0.4);
context.fill_rect(rect, background);
context.draw_rect(rect, border);
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(),
text_color,
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),
close_color,
);
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),
close_color,
);
}
}
}
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,
}
}