Files
jmb-bar/src/ui/action_centre.rs

395 lines
14 KiB
Rust

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<Cell<bool>>,
}
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<RefCell<HashMap<String, WifiWidget>>> =
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<String> = 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<HashMap<String, WifiWidget>>,
) -> 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<RefCell<HashMap<String, WifiWidget>>>,
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: &gtk::Window) -> Option<WifiWidget> {
let mut widget = window.focus()?;
loop {
if let Ok(widget) = widget.clone().downcast::<WifiWidget>() {
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::<gdk::Monitor>() 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
}
});
}
}