use crate::backend::BackendChannels; use crate::backend::network::{ Auth, AuthType, ConnectError, NetworkCommand, NetworkInfo, NetworkState, }; use crate::backend::window_manager_hypr::HyprlandComms; use crate::ui::widgets::{LabelEntry, WifiWidget}; use glib::prelude::Cast; use glib::{ControlFlow, clone}; use gtk::gdk::Display; use gtk::prelude::{ BoxExt, ButtonExt, DisplayExt, EditableExt, GtkWindowExt, ListModelExt, WidgetExt, }; use gtk::{ Application, ApplicationWindow, Button, Image, Label, Orientation, PolicyType, ScrolledWindow, Stack, gdk, }; use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell}; use std::cell::{Cell, RefCell, RefMut}; use std::collections::HashMap; use std::rc::Rc; use std::time::{Duration, Instant}; const WIFI_ICONS: [&str; 4] = [ "xsi-network-wireless-signal-none-symbolic", "xsi-network-wireless-signal-weak-symbolic", "xsi-network-wireless-signal-ok-symbolic", "xsi-network-wireless-signal-good-symbolic", ]; const _WIFI_ICONS_SECURED: [&str; 4] = [ "xsi-network-wireless-signal-none-secure-symbolic", "xsi-network-wireless-signal-weak-secure-symbolic", "xsi-network-wireless-signal-ok-secure-symbolic", "xsi-network-wireless-signal-good-secure-symbolic", ]; const ANIMATION_DURATION: u64 = 200; #[derive(Clone)] pub struct ActionCentre { pub window: ApplicationWindow, is_animating: Rc>, } impl ActionCentre { pub fn create_action_centre(app: Application, mut channels: BackendChannels) -> Self { let window = ApplicationWindow::builder() .application(&app) .title("JmbBar") .default_width(400) .default_height(1080) .css_classes(vec!["action-centre"]) .build(); window.init_layer_shell(); window.set_layer(Layer::Top); window.set_anchor(Edge::Top, true); window.set_anchor(Edge::Bottom, true); window.set_anchor(Edge::Right, true); window.set_margin(Edge::Top, 20); window.set_margin(Edge::Right, 20); window.set_margin(Edge::Bottom, 20); window.set_keyboard_mode(KeyboardMode::Exclusive); let instance = Self { window: window.clone(), is_animating: Rc::new(Cell::new(false)), }; let scrolled = ScrolledWindow::new(); scrolled.set_policy(PolicyType::Never, PolicyType::Automatic); scrolled.set_vexpand(true); let content = gtk::Box::new(Orientation::Vertical, 6); let add_net_stack = Stack::new(); add_net_stack.set_vhomogeneous(false); let add_network_button = Button::new(); add_network_button.add_css_class("wifi-button"); let add_net_box = gtk::Box::new(Orientation::Horizontal, 4); let add_icon = Image::from_file("icons/plus-symbolic.svg"); add_icon.add_css_class("list-add-icon"); add_icon.add_css_class("icon"); add_icon.set_pixel_size(24); add_net_box.append(&add_icon); add_net_stack.add_named(&add_network_button, Some("button")); let add_net_input_box = gtk::Box::new(Orientation::Vertical, 4); let ssid = LabelEntry::new("SSID:"); add_net_input_box.append(&ssid); let pw = LabelEntry::new("Passphrase:"); add_net_input_box.append(&pw); add_net_stack.add_named(&add_net_input_box, Some("input")); add_net_stack.set_visible_child_name("button"); add_network_button.connect_clicked(clone!( #[strong] add_net_stack, move |_button| { add_net_stack.set_visible_child_name("input"); } )); let label = Label::new(Some("Add Network...")); add_net_box.append(&label); add_network_button.set_child(Some(&add_net_box)); content.append(&add_net_stack); scrolled.set_child(Some(&content)); window.set_child(Some(&scrolled)); let key_controller = gtk::EventControllerKey::new(); key_controller.connect_key_pressed({ let instance = instance.clone(); move |_, key, _, _| { if key == gdk::Key::Escape { instance.hide_action_centre(); glib::Propagation::Stop } else { glib::Propagation::Proceed } } }); window.add_controller(key_controller); let wifi_networks: Rc>> = Rc::new(RefCell::new(HashMap::new())); window.connect_focus_widget_notify({ let wifi_networks = wifi_networks.clone(); move |window| Self::focus_callback(&wifi_networks, window) }); glib::timeout_add_local(Duration::from_millis(100), { let window = window.clone(); let wifi_networks = wifi_networks.clone(); let net_rx = channels.take_net_rx().unwrap(); move || { while let Ok(message) = net_rx.try_recv() { match message { NetworkState::ChangeNetwork(Some(info)) => { for (name, widget) in wifi_networks.borrow().iter() { if name.eq(&info.name) { widget.add_css_class("connected"); } else { widget.remove_css_class("connected"); } } } NetworkState::Networks(networks) => { for network in &networks { let borrowed_wifi_networks = wifi_networks.borrow_mut(); let widget = borrowed_wifi_networks.get(&network.name); if let Some(widget) = widget { let idx = ((network.signal as f32 / 100.0) * (WIFI_ICONS.len() - 1) as f32 + 0.5) as usize; widget.set_wifi_icon_from_name(WIFI_ICONS[idx]); } else { let widget = Self::create_wifi_widget( &channels, network, borrowed_wifi_networks, ); content.append(&widget); } } let mut removal: Vec = Vec::new(); for (ssid, widget) in wifi_networks.borrow().iter() { let network = &networks.iter().find(|n| n.name.eq(ssid)); if network.is_none() { content.remove(widget); removal.push(ssid.clone()); } } for ssid in removal { wifi_networks.borrow_mut().remove(&ssid); } } NetworkState::Auth(ssid, auth) => { if let Some(widget) = Self::find_focused_widget(window.as_ref()) { match auth { AuthType::Password => { if widget.ssid().eq(&ssid) { widget.show_password_entry(); widget.stop_spinner(); widget.grab_focus_password_entry(); } } } } } NetworkState::ConnectFail(ssid, err) => { let mut networks = wifi_networks.borrow_mut(); if let Some(widget) = networks.get(&ssid) { widget.stop_spinner(); match err { ConnectError::AuthFailed => widget.show_password_entry(), ConnectError::NotFound => { content.remove(widget); networks.remove(&ssid); } ConnectError::Unknown => {} } } } NetworkState::ConnectSuccess(ssid) => { if let Some(widget) = wifi_networks.borrow().get(&ssid) { widget.stop_spinner(); } } _ => {} } } ControlFlow::Continue } }); instance } fn create_wifi_widget( channels: &BackendChannels, network: &NetworkInfo, mut borrowed_wifi_networks: RefMut>, ) -> WifiWidget { let ssid = network.name.clone(); let widget = WifiWidget::new(); widget.set_ssid(&ssid); widget.add_css_class("wifi-button"); widget.set_secured(network.secured.unwrap()); widget.set_known(network.known); let idx = ((network.signal as f32 / 100.0) * (WIFI_ICONS.len() - 1) as f32 + 0.5) as usize; widget.set_wifi_icon_from_name(WIFI_ICONS[idx]); borrowed_wifi_networks.insert(ssid.clone(), widget.clone()); let net_tx = channels.net_tx().unwrap(); widget.connect_clicked(clone!( #[strong] net_tx, #[strong] ssid, move |widget| { let _ = net_tx.send(NetworkCommand::Connect(ssid.clone(), Auth::Open)); widget.start_spinner(); } )); widget.connect_activate_pass(clone!( #[strong] net_tx, #[weak] widget, move |entry| { let text = entry.text().to_string(); if !text.is_empty() { let _ = net_tx.send(NetworkCommand::Connect( ssid.clone(), Auth::Password(entry.text().to_string()), )); widget.start_spinner(); widget.show_ssid(); } } )); widget } fn focus_callback( wifi_networks: &Rc>>, window: &ApplicationWindow, ) { for widget in wifi_networks.clone().borrow().values() { let focused_inside = window.focus().is_some_and(|mut focus| { loop { if focus == *widget { break true; } match focus.parent() { Some(parent) => focus = parent, None => break false, } } }); if !focused_inside { widget.show_ssid(); } } } fn _has_focus_within(window: &ApplicationWindow) -> bool { let Some(mut widget) = window.focus() else { return false; }; loop { if widget == *window { return true; } match widget.parent() { Some(parent) => widget = parent, None => return false, } } } fn find_focused_widget(window: >k::Window) -> Option { let mut widget = window.focus()?; loop { if let Ok(widget) = widget.clone().downcast::() { return Some(widget); } widget = widget.parent()?; } } pub fn show_action_centre_active_monitor(&self) { let Some(display) = Display::default() else { return; }; let Some(monitor) = HyprlandComms::cursor_monitor() else { return; }; self.show_action_centre(monitor as u32, &display); } pub fn show_action_centre(&self, monitor_idx: u32, display: &Display) { let window = &self.window; let monitors = display.monitors(); let Some(monitor) = monitors.item(monitor_idx) else { return; }; let Ok(monitor) = monitor.downcast::() else { return; }; window.set_monitor(Some(&monitor)); if !window.is_visible() { self.animate_margin(-window.width() - 20, 20, ANIMATION_DURATION, || {}); window.present(); } } pub fn hide_action_centre(&self) { let clone = self.clone(); self.animate_margin( 20, -clone.window.width() - 20, ANIMATION_DURATION, move || clone.window.set_visible(false), ); } pub fn animate_margin( &self, from: i32, to: i32, duration_ms: u64, on_finished: impl Fn() + 'static, ) { let window = self.window.clone(); let flag = self.is_animating.clone(); let start = Instant::now(); let duration = Duration::from_millis(duration_ms); if flag.get() { return; } glib::timeout_add_local(Duration::from_millis(10), move || { flag.set(true); let elapsed = start.elapsed(); let t = (elapsed.as_secs_f64() / duration.as_secs_f64()).clamp(0.0, 1.0); // Ease-out cubic. let eased = 1.0 - (1.0 - t).powi(3); let margin = from as f64 + (to - from) as f64 * eased; window.set_margin(Edge::Right, margin.round() as i32); if t >= 1.0 { on_finished(); flag.set(false); ControlFlow::Break } else { ControlFlow::Continue } }); } }