use super::types::{Notification, NotificationPosition};
use crate::render::{Cell, Modifier};
use crate::style::Color;
use crate::utils::char_width;
use crate::widget::theme::SEPARATOR_COLOR;
use crate::widget::traits::{RenderContext, View, WidgetProps};
use crate::{impl_props_builders, impl_styled_view};
pub struct NotificationCenter {
notifications: Vec<Notification>,
max_visible: usize,
position: NotificationPosition,
width: u16,
show_icons: bool,
show_timer: bool,
spacing: u16,
tick_counter: u64,
selected: Option<usize>,
focused: bool,
props: WidgetProps,
}
impl NotificationCenter {
pub fn new() -> Self {
Self {
notifications: Vec::new(),
max_visible: 5,
position: NotificationPosition::TopRight,
width: 40,
show_icons: true,
show_timer: true,
spacing: 1,
tick_counter: 0,
selected: None,
focused: false,
props: WidgetProps::new(),
}
}
pub fn position(mut self, position: NotificationPosition) -> Self {
self.position = position;
self
}
pub fn max_visible(mut self, max: usize) -> Self {
self.max_visible = max.max(1);
self
}
pub fn width(mut self, width: u16) -> Self {
self.width = width.max(20);
self
}
pub fn show_icons(mut self, show: bool) -> Self {
self.show_icons = show;
self
}
pub fn show_timer(mut self, show: bool) -> Self {
self.show_timer = show;
self
}
pub fn spacing(mut self, spacing: u16) -> Self {
self.spacing = spacing;
self
}
pub fn focused(mut self, focused: bool) -> Self {
self.focused = focused;
self
}
pub fn push(&mut self, mut notification: Notification) {
notification.created_at = self.tick_counter;
self.notifications.push(notification);
}
pub fn info(&mut self, message: impl Into<String>) {
self.push(Notification::info(message));
}
pub fn success(&mut self, message: impl Into<String>) {
self.push(Notification::success(message));
}
pub fn warning(&mut self, message: impl Into<String>) {
self.push(Notification::warning(message));
}
pub fn error(&mut self, message: impl Into<String>) {
self.push(Notification::error(message));
}
pub fn dismiss(&mut self, id: u64) {
self.notifications.retain(|n| n.id != id);
if self.selected.is_some_and(|s| s >= self.notifications.len()) {
self.selected = if self.notifications.is_empty() {
None
} else {
Some(self.notifications.len() - 1)
};
}
}
pub fn dismiss_selected(&mut self) {
if let Some(idx) = self.selected {
if idx < self.notifications.len() {
let id = self.notifications[idx].id;
self.dismiss(id);
}
}
}
pub fn clear(&mut self) {
self.notifications.clear();
self.selected = None;
}
pub fn count(&self) -> usize {
self.notifications.len()
}
pub fn is_empty(&self) -> bool {
self.notifications.is_empty()
}
pub fn tick(&mut self) {
self.tick_counter += 1;
for notification in &mut self.notifications {
notification.tick += 1;
}
self.notifications.retain(|n| !n.is_expired());
if self.selected.is_some_and(|s| s >= self.notifications.len()) {
self.selected = if self.notifications.is_empty() {
None
} else {
Some(self.notifications.len() - 1)
};
}
}
pub fn select_next(&mut self) {
if self.notifications.is_empty() {
self.selected = None;
return;
}
self.selected = Some(match self.selected {
Some(idx) => (idx + 1) % self.notifications.len(),
None => 0,
});
}
pub fn select_prev(&mut self) {
if self.notifications.is_empty() {
self.selected = None;
return;
}
self.selected = Some(match self.selected {
Some(0) => self.notifications.len() - 1,
Some(idx) => idx - 1,
None => self.notifications.len() - 1,
});
}
pub fn handle_key(&mut self, key: &crate::event::Key) -> bool {
use crate::event::Key;
if !self.focused || self.notifications.is_empty() {
return false;
}
match key {
Key::Up | Key::Char('k') => {
self.select_prev();
true
}
Key::Down | Key::Char('j') => {
self.select_next();
true
}
Key::Char('d') | Key::Delete => {
self.dismiss_selected();
true
}
Key::Char('c') => {
self.clear();
true
}
_ => false,
}
}
fn notification_height(&self, notification: &Notification) -> u16 {
let mut height = 1; if notification.title.is_some() {
height += 1;
}
if notification.progress.is_some() {
height += 1;
}
if notification.action.is_some() {
height += 1;
}
height + 2 }
}
impl Default for NotificationCenter {
fn default() -> Self {
Self::new()
}
}
impl View for NotificationCenter {
crate::impl_view_meta!("NotificationCenter");
fn render(&self, ctx: &mut RenderContext) {
if self.notifications.is_empty() {
return;
}
let area = ctx.area;
let visible = self
.notifications
.iter()
.rev()
.take(self.max_visible)
.collect::<Vec<_>>();
let (start_x, mut current_y, direction): (u16, u16, i16) = match self.position {
NotificationPosition::TopRight => (area.width.saturating_sub(self.width), 0, 1),
NotificationPosition::TopLeft => (0, 0, 1),
NotificationPosition::TopCenter => ((area.width.saturating_sub(self.width)) / 2, 0, 1),
NotificationPosition::BottomRight => {
(area.width.saturating_sub(self.width), area.height, -1)
}
NotificationPosition::BottomLeft => (0, area.height, -1),
NotificationPosition::BottomCenter => {
((area.width.saturating_sub(self.width)) / 2, area.height, -1)
}
};
for (idx, notification) in visible.iter().enumerate() {
let height = self.notification_height(notification);
let is_selected = self.selected == Some(self.notifications.len() - 1 - idx);
let y = if direction < 0 {
current_y.saturating_sub(height)
} else {
current_y
};
if y >= area.height || y + height > area.height {
continue;
}
self.render_notification(ctx, notification, start_x, y, is_selected);
if direction < 0 {
current_y = y.saturating_sub(self.spacing);
} else {
current_y = y + height + self.spacing;
}
}
}
}
impl NotificationCenter {
fn render_notification(
&self,
ctx: &mut RenderContext,
notification: &Notification,
x: u16,
y: u16,
is_selected: bool,
) {
let width = self.width;
let color = notification.level.color();
let bg = notification.level.bg_color();
let border_color = if is_selected { Color::WHITE } else { color };
let mut tl = Cell::new('╭');
tl.fg = Some(border_color);
ctx.set(x, y, tl);
for dx in 1..width - 1 {
let mut h = Cell::new('─');
h.fg = Some(border_color);
ctx.set(x + dx, y, h);
}
let mut tr = Cell::new('╮');
tr.fg = Some(border_color);
ctx.set(x + width - 1, y, tr);
let mut current_y = y + 1;
if let Some(ref title) = notification.title {
let mut left = Cell::new('│');
left.fg = Some(border_color);
ctx.set(x, current_y, left);
for dx in 1..width - 1 {
let mut cell = Cell::new(' ');
cell.bg = Some(bg);
ctx.set(x + dx, current_y, cell);
}
let mut content_x = x + 1;
if self.show_icons {
let mut icon = Cell::new(notification.level.icon());
icon.fg = Some(color);
icon.bg = Some(bg);
ctx.set(content_x, current_y, icon);
content_x += 2;
}
let mut dx: u16 = 0;
for ch in title.chars() {
let cw = char_width(ch) as u16;
if content_x + dx >= x + width - 2 {
break;
}
let mut cell = Cell::new(ch);
cell.fg = Some(Color::WHITE);
cell.bg = Some(bg);
cell.modifier |= Modifier::BOLD;
ctx.set(content_x + dx, current_y, cell);
dx += cw;
}
let mut right = Cell::new('│');
right.fg = Some(border_color);
ctx.set(x + width - 1, current_y, right);
current_y += 1;
}
{
let mut left = Cell::new('│');
left.fg = Some(border_color);
ctx.set(x, current_y, left);
for dx in 1..width - 1 {
let mut cell = Cell::new(' ');
cell.bg = Some(bg);
ctx.set(x + dx, current_y, cell);
}
let mut content_x = x + 1;
if self.show_icons && notification.title.is_none() {
let mut icon = Cell::new(notification.level.icon());
icon.fg = Some(color);
icon.bg = Some(bg);
ctx.set(content_x, current_y, icon);
content_x += 2;
}
let mut dx: u16 = 0;
for ch in notification.message.chars() {
let cw = char_width(ch) as u16;
if content_x + dx >= x + width - 2 {
break;
}
let mut cell = Cell::new(ch);
cell.fg = Some(Color::WHITE);
cell.bg = Some(bg);
ctx.set(content_x + dx, current_y, cell);
dx += cw;
}
let mut right = Cell::new('│');
right.fg = Some(border_color);
ctx.set(x + width - 1, current_y, right);
current_y += 1;
}
if let Some(progress) = notification.progress {
let mut left = Cell::new('│');
left.fg = Some(border_color);
ctx.set(x, current_y, left);
for dx in 1..width - 1 {
let mut cell = Cell::new(' ');
cell.bg = Some(bg);
ctx.set(x + dx, current_y, cell);
}
let bar_width = width - 4;
let filled = (progress * bar_width as f64).round() as u16;
for dx in 0..bar_width {
let ch = if dx < filled { '█' } else { '░' };
let fg = if dx < filled { color } else { SEPARATOR_COLOR };
let mut cell = Cell::new(ch);
cell.fg = Some(fg);
cell.bg = Some(bg);
ctx.set(x + 2 + dx, current_y, cell);
}
let mut right = Cell::new('│');
right.fg = Some(border_color);
ctx.set(x + width - 1, current_y, right);
current_y += 1;
}
let mut bl = Cell::new('╰');
bl.fg = Some(border_color);
ctx.set(x, current_y, bl);
if self.show_timer && notification.duration > 0 {
let remaining = notification.remaining();
let timer_width = (width - 4) as f64;
let timer_filled = (remaining * timer_width).round() as u16;
for dx in 1..width - 1 {
let ch = if dx <= timer_filled { '━' } else { '─' };
let fg = if dx <= timer_filled {
color
} else {
border_color
};
let mut cell = Cell::new(ch);
cell.fg = Some(fg);
ctx.set(x + dx, current_y, cell);
}
} else {
for dx in 1..width - 1 {
let mut h = Cell::new('─');
h.fg = Some(border_color);
ctx.set(x + dx, current_y, h);
}
}
let mut br = Cell::new('╯');
br.fg = Some(border_color);
ctx.set(x + width - 1, current_y, br);
}
}
impl_styled_view!(NotificationCenter);
impl_props_builders!(NotificationCenter);
pub fn notification_center() -> NotificationCenter {
NotificationCenter::new()
}