use super::{Widget, WidgetBase, WidgetId, LayoutContext, PaintContext, EventContext};
use crate::css::{ClassList, WidgetState};
use crate::event::{Event, EventResult, MouseEventKind, MouseButton};
use crate::geometry::{BorderRadius, Color, Point, Rect, Size};
use crate::layout::{Constraints, LayoutResult};
use crate::render::Painter;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum NotificationUrgency {
Low,
#[default]
Normal,
Critical,
}
#[allow(clippy::type_complexity)]
pub struct Notification {
base: WidgetBase,
title: String,
body: String,
icon: Option<String>,
app_name: Option<String>,
urgency: NotificationUrgency,
actions: Vec<(String, String)>, hovered_action: Option<usize>,
close_hovered: bool,
on_click: Option<Box<dyn Fn() + Send + Sync>>,
on_action: Option<Box<dyn Fn(&str) + Send + Sync>>,
on_close: Option<Box<dyn Fn() + Send + Sync>>,
}
impl Notification {
pub fn new() -> Self {
Self {
base: WidgetBase::new().with_class("notification"),
title: String::new(),
body: String::new(),
icon: None,
app_name: None,
urgency: NotificationUrgency::default(),
actions: Vec::new(),
hovered_action: None,
close_hovered: false,
on_click: None,
on_action: None,
on_close: None,
}
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = title.into();
self
}
pub fn body(mut self, body: impl Into<String>) -> Self {
self.body = body.into();
self
}
pub fn icon(mut self, icon: impl Into<String>) -> Self {
self.icon = Some(icon.into());
self
}
pub fn app_name(mut self, name: impl Into<String>) -> Self {
self.app_name = Some(name.into());
self
}
pub fn urgency(mut self, urgency: NotificationUrgency) -> Self {
self.urgency = urgency;
self
}
pub fn action(mut self, id: impl Into<String>, label: impl Into<String>) -> Self {
self.actions.push((id.into(), label.into()));
self
}
pub fn on_click<F>(mut self, handler: F) -> Self
where
F: Fn() + Send + Sync + 'static,
{
self.on_click = Some(Box::new(handler));
self
}
pub fn on_action<F>(mut self, handler: F) -> Self
where
F: Fn(&str) + Send + Sync + 'static,
{
self.on_action = Some(Box::new(handler));
self
}
pub fn on_close<F>(mut self, handler: F) -> Self
where
F: Fn() + Send + Sync + 'static,
{
self.on_close = Some(Box::new(handler));
self
}
pub fn class(mut self, class: &str) -> Self {
self.base.classes.add(class);
self
}
fn get_close_button_rect(&self) -> Rect {
Rect::new(
self.base.bounds.x() + self.base.bounds.width() - 28.0,
self.base.bounds.y() + 8.0,
20.0,
20.0,
)
}
fn get_action_rects(&self) -> Vec<Rect> {
let mut rects = Vec::new();
let action_y = self.base.bounds.y() + self.base.bounds.height() - 40.0;
let mut x = self.base.bounds.x() + 16.0;
if self.icon.is_some() {
x += 48.0;
}
for (_, label) in &self.actions {
let width = label.len() as f32 * 8.0 + 24.0;
rects.push(Rect::new(x, action_y, width, 28.0));
x += width + 8.0;
}
rects
}
}
impl Default for Notification {
fn default() -> Self {
Self::new()
}
}
impl Widget for Notification {
fn id(&self) -> WidgetId {
self.base.id
}
fn type_name(&self) -> &'static str {
"notification"
}
fn element_id(&self) -> Option<&str> {
self.base.element_id.as_deref()
}
fn classes(&self) -> &ClassList {
&self.base.classes
}
fn state(&self) -> WidgetState {
self.base.state
}
fn intrinsic_size(&self, _ctx: &LayoutContext) -> Size {
let height = if self.actions.is_empty() { 80.0 } else { 120.0 };
Size::new(360.0, height)
}
fn layout(&mut self, constraints: Constraints, ctx: &LayoutContext) -> LayoutResult {
let size = constraints.constrain(self.intrinsic_size(ctx));
self.base.bounds.size = size;
LayoutResult::new(size)
}
fn paint(&self, painter: &mut Painter, rect: Rect, ctx: &PaintContext) {
let theme = ctx.style_ctx.theme;
let radius = BorderRadius::all(theme.radii.lg * theme.typography.base_size);
let accent = match self.urgency {
NotificationUrgency::Low => theme.colors.muted,
NotificationUrgency::Normal => theme.colors.card,
NotificationUrgency::Critical => theme.colors.destructive.with_alpha(0.1),
};
let shadow_rect = Rect::new(rect.x() + 2.0, rect.y() + 4.0, rect.width(), rect.height());
painter.fill_rounded_rect(shadow_rect, Color::BLACK.with_alpha(0.2), radius);
painter.fill_rounded_rect(rect, accent, radius);
painter.stroke_rect(rect, theme.colors.border, 1.0);
if self.urgency == NotificationUrgency::Critical {
let bar_rect = Rect::new(rect.x(), rect.y(), 4.0, rect.height());
painter.fill_rect(bar_rect, theme.colors.destructive);
}
let mut content_x = rect.x() + 16.0;
if let Some(ref icon) = self.icon {
painter.draw_text(
icon,
Point::new(content_x, rect.y() + 40.0),
theme.colors.foreground,
32.0,
);
content_x += 48.0;
}
if let Some(ref app_name) = self.app_name {
painter.draw_text(
app_name,
Point::new(content_x, rect.y() + 20.0),
theme.colors.muted_foreground,
11.0,
);
}
let title_y = if self.app_name.is_some() { 38.0 } else { 28.0 };
painter.draw_text(
&self.title,
Point::new(content_x, rect.y() + title_y),
theme.colors.foreground,
14.0,
);
if !self.body.is_empty() {
painter.draw_text(
&self.body,
Point::new(content_x, rect.y() + title_y + 20.0),
theme.colors.muted_foreground,
13.0,
);
}
let close_rect = self.get_close_button_rect();
let close_color = if self.close_hovered {
theme.colors.foreground
} else {
theme.colors.muted_foreground
};
painter.draw_text("✕", Point::new(close_rect.x() + 4.0, close_rect.y() + 14.0), close_color, 12.0);
if !self.actions.is_empty() {
let action_rects = self.get_action_rects();
for (i, ((_, label), rect)) in self.actions.iter().zip(action_rects.iter()).enumerate() {
let bg_color = if self.hovered_action == Some(i) {
theme.colors.accent
} else {
theme.colors.secondary
};
let fg_color = if self.hovered_action == Some(i) {
theme.colors.accent_foreground
} else {
theme.colors.secondary_foreground
};
let btn_radius = BorderRadius::all(theme.radii.sm * theme.typography.base_size);
painter.fill_rounded_rect(*rect, bg_color, btn_radius);
painter.draw_text(
label,
Point::new(rect.x() + 12.0, rect.y() + 18.0),
fg_color,
12.0,
);
}
}
}
fn handle_event(&mut self, event: &Event, ctx: &mut EventContext) -> EventResult {
if let Event::Mouse(mouse) = event {
let in_bounds = self.bounds().contains(mouse.position);
let close_rect = self.get_close_button_rect();
let in_close = close_rect.contains(mouse.position);
let action_rects = self.get_action_rects();
let in_action = action_rects.iter().position(|r| r.contains(mouse.position));
match mouse.kind {
MouseEventKind::Move => {
let old_close = self.close_hovered;
let old_action = self.hovered_action;
self.close_hovered = in_close;
self.hovered_action = in_action;
if old_close != self.close_hovered || old_action != self.hovered_action {
ctx.request_redraw();
}
}
MouseEventKind::Up if mouse.button == Some(MouseButton::Left) => {
if in_close {
if let Some(handler) = &self.on_close {
handler();
}
return EventResult::Handled;
}
if let Some(action_idx) = in_action {
if let Some((id, _)) = self.actions.get(action_idx) {
let id = id.clone();
if let Some(handler) = &self.on_action {
handler(&id);
}
return EventResult::Handled;
}
}
if in_bounds {
if let Some(handler) = &self.on_click {
handler();
}
return EventResult::Handled;
}
}
_ => {}
}
}
EventResult::Ignored
}
fn bounds(&self) -> Rect {
self.base.bounds
}
fn set_bounds(&mut self, bounds: Rect) {
self.base.bounds = bounds;
}
}