use super::constants::PANEL_REVEAL_DURATION_MS;
use super::panel_content;
use crate::config::NotificationConfig;
use crate::state::NotificationState;
use gtk4::prelude::*;
use gtk4_layer_shell::LayerShell;
use std::cell::RefCell;
use std::rc::Rc;
pub struct NotificationPanel {
pub win: gtk4::ApplicationWindow,
backdrops: Vec<gtk4::ApplicationWindow>,
revealer: gtk4::Revealer,
list_box: gtk4::Box,
panel_box: gtk4::Box,
state: Rc<RefCell<NotificationState>>,
config: Rc<RefCell<NotificationConfig>>,
on_notification_click: Rc<dyn Fn(u32)>,
on_state_change: Rc<dyn Fn()>,
on_panel_open: Rc<dyn Fn()>,
}
impl NotificationPanel {
pub fn new(
app: >k4::Application,
state: &Rc<RefCell<NotificationState>>,
config: &Rc<RefCell<NotificationConfig>>,
on_notification_click: Rc<dyn Fn(u32)>,
on_state_change: Rc<dyn Fn()>,
on_panel_open: Rc<dyn Fn()>,
) -> Self {
let initial_width = config.borrow().panel_width;
let backdrops = nwg_common::layer_shell::create_fullscreen_backdrops(
app,
"mac-notification-backdrop",
"notification-backdrop",
None,
);
let win = gtk4::ApplicationWindow::new(app);
win.add_css_class("notification-panel-window");
win.set_width_request(initial_width);
setup_panel_window(&win);
let revealer = gtk4::Revealer::new();
revealer.set_transition_type(gtk4::RevealerTransitionType::SlideLeft);
revealer.set_transition_duration(PANEL_REVEAL_DURATION_MS);
revealer.set_reveal_child(false);
win.set_child(Some(&revealer));
let panel_box = gtk4::Box::new(gtk4::Orientation::Vertical, 0);
panel_box.add_css_class("notification-panel");
panel_box.set_width_request(initial_width);
revealer.set_child(Some(&panel_box));
let scrolled = gtk4::ScrolledWindow::new();
scrolled.set_vexpand(true);
scrolled.set_hexpand(true);
let list_box = gtk4::Box::new(gtk4::Orientation::Vertical, 0);
list_box.add_css_class("panel-list");
scrolled.set_child(Some(&list_box));
let header = build_header(state, &on_state_change, &list_box);
panel_box.append(&header);
panel_box.append(&scrolled);
for backdrop in &backdrops {
let backdrop_click = gtk4::GestureClick::new();
let revealer_bd = revealer.clone();
let win_bd = win.clone();
let backdrops_bd = backdrops.clone();
backdrop_click.connect_released(move |gesture, _, _, _| {
gesture.set_state(gtk4::EventSequenceState::Claimed);
hide_panel(&revealer_bd, &win_bd, &backdrops_bd);
});
backdrop.add_controller(backdrop_click);
}
let key_ctrl = gtk4::EventControllerKey::new();
let revealer_esc = revealer.clone();
let win_esc = win.clone();
let backdrops_esc = backdrops.clone();
key_ctrl.connect_key_pressed(move |_, key, _, _| {
if key == gtk4::gdk::Key::Escape {
hide_panel(&revealer_esc, &win_esc, &backdrops_esc);
gtk4::glib::Propagation::Stop
} else {
gtk4::glib::Propagation::Proceed
}
});
win.add_controller(key_ctrl);
let panel = Self {
win,
backdrops,
revealer,
list_box,
panel_box,
state: Rc::clone(state),
config: Rc::clone(config),
on_notification_click,
on_state_change,
on_panel_open,
};
panel.rebuild();
panel.win.present();
panel.win.set_visible(false);
for backdrop in &panel.backdrops {
backdrop.present();
backdrop.set_visible(false);
}
panel
}
pub fn toggle(&self) {
if self.revealer.reveals_child() {
hide_panel(&self.revealer, &self.win, &self.backdrops);
} else {
(self.on_panel_open)();
let list = self.list_box.clone();
let state = Rc::clone(&self.state);
let config = Rc::clone(&self.config);
let panel_box = self.panel_box.clone();
let on_click = Rc::clone(&self.on_notification_click);
let on_change = Rc::clone(&self.on_state_change);
let win = self.win.clone();
let backdrops = self.backdrops.clone();
let revealer = self.revealer.clone();
gtk4::glib::idle_add_local_once(move || {
rebuild_list(&list, &state, on_click, on_change);
let width = config.borrow().panel_width;
win.set_width_request(width);
panel_box.set_width_request(width);
for backdrop in &backdrops {
backdrop.set_visible(true);
}
win.set_visible(true);
revealer.set_reveal_child(true);
});
}
}
pub fn is_visible(&self) -> bool {
self.revealer.reveals_child()
}
pub fn rebuild(&self) {
rebuild_list(
&self.list_box,
&self.state,
Rc::clone(&self.on_notification_click),
Rc::clone(&self.on_state_change),
);
}
}
fn hide_panel(
revealer: >k4::Revealer,
win: >k4::ApplicationWindow,
backdrops: &[gtk4::ApplicationWindow],
) {
revealer.set_reveal_child(false);
for backdrop in backdrops {
backdrop.set_visible(false);
}
let win = win.clone();
gtk4::glib::timeout_add_local_once(
std::time::Duration::from_millis(PANEL_REVEAL_DURATION_MS as u64),
move || {
win.set_visible(false);
},
);
}
fn setup_panel_window(win: >k4::ApplicationWindow) {
win.init_layer_shell();
win.set_namespace(Some("nwg-notification-panel"));
win.set_layer(gtk4_layer_shell::Layer::Overlay);
win.set_exclusive_zone(-1);
win.set_keyboard_mode(gtk4_layer_shell::KeyboardMode::OnDemand);
win.set_anchor(gtk4_layer_shell::Edge::Top, true);
win.set_anchor(gtk4_layer_shell::Edge::Right, true);
win.set_anchor(gtk4_layer_shell::Edge::Bottom, true);
}
fn build_header(
state: &Rc<RefCell<NotificationState>>,
on_state_change: &Rc<dyn Fn()>,
list_box: >k4::Box,
) -> gtk4::Box {
let header = gtk4::Box::new(gtk4::Orientation::Horizontal, 8);
header.add_css_class("panel-header");
header.set_margin_start(12);
header.set_margin_end(12);
header.set_margin_top(12);
header.set_margin_bottom(8);
let title = gtk4::Label::new(Some("Notifications"));
title.add_css_class("panel-title");
title.set_hexpand(true);
title.set_halign(gtk4::Align::Start);
header.append(&title);
let dnd_btn = gtk4::Button::from_icon_name("notifications-disabled-symbolic");
dnd_btn.add_css_class("panel-dnd");
dnd_btn.set_tooltip_text(Some("Do Not Disturb"));
let state_dnd = Rc::clone(state);
let on_change_dnd = Rc::clone(on_state_change);
dnd_btn.connect_clicked(move |btn| {
let new_dnd = !state_dnd.borrow().dnd;
state_dnd.borrow_mut().dnd = new_dnd;
let icon = if new_dnd {
"notifications-disabled-symbolic"
} else {
"preferences-system-notifications-symbolic"
};
btn.set_icon_name(icon);
log::info!("DND {}", if new_dnd { "enabled" } else { "disabled" });
on_change_dnd();
});
header.append(&dnd_btn);
let clear_btn = gtk4::Button::with_label("Clear All");
clear_btn.add_css_class("panel-clear");
let state_clear = Rc::clone(state);
let on_change_clear = Rc::clone(on_state_change);
let list_clear = list_box.clone();
clear_btn.connect_clicked(move |_| {
state_clear.borrow_mut().dismiss_all();
while let Some(child) = list_clear.first_child() {
list_clear.remove(&child);
}
let empty = gtk4::Label::new(Some("No notifications"));
empty.add_css_class("panel-empty");
empty.set_margin_top(40);
list_clear.append(&empty);
log::info!("Cleared all notifications");
on_change_clear();
});
header.append(&clear_btn);
header
}
fn rebuild_list(
list_box: >k4::Box,
state: &Rc<RefCell<NotificationState>>,
on_click: Rc<dyn Fn(u32)>,
on_state_change: Rc<dyn Fn()>,
) {
let list_rebuild = list_box.clone();
let state_rebuild = Rc::clone(state);
let on_click_rebuild = Rc::clone(&on_click);
let on_change_rebuild = Rc::clone(&on_state_change);
let on_rebuild: Rc<dyn Fn()> = Rc::new(move || {
let list = list_rebuild.clone();
let state = Rc::clone(&state_rebuild);
let on_click = Rc::clone(&on_click_rebuild);
let on_change = Rc::clone(&on_change_rebuild);
gtk4::glib::idle_add_local_once(move || {
rebuild_list(&list, &state, on_click, Rc::clone(&on_change));
on_change();
});
});
panel_content::build_grouped_list(list_box, state, on_click, on_rebuild);
}