First Commit

This commit is contained in:
jmb05
2026-10-24 19:55:33 +02:00
commit d6a2c6afea
54 changed files with 12084 additions and 0 deletions

383
src/ui/action_centre.rs Normal file
View File

@@ -0,0 +1,383 @@
use crate::backend::BackendChannels;
use crate::backend::network::{
Auth, AuthType, ConnectError, NetworkCommand, NetworkInfo, NetworkState,
};
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::Overlay);
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::OnDemand);
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 instance_clone = instance.clone();
let key_controller = gtk::EventControllerKey::new();
key_controller.connect_key_pressed(move |_, key, _, _| {
if key == gdk::Key::Escape {
instance_clone.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()));
let wifi_networks_clone = wifi_networks.clone();
window.connect_focus_widget_notify(move |window| {
Self::focus_callback(&wifi_networks_clone, window)
});
let net_rx = channels.take_net_rx().unwrap();
let window_clone = window.clone();
let wifi_networks_clone = wifi_networks.clone();
glib::timeout_add_local(Duration::from_millis(100), move || {
while let Ok(message) = net_rx.try_recv() {
match message {
NetworkState::ChangeNetwork(info) => match info {
Some(info) => {
for (name, widget) in wifi_networks_clone.borrow().iter() {
if name.eq(&info.name) {
widget.add_css_class("connected");
} else {
widget.remove_css_class("connected");
}
}
}
None => {}
},
NetworkState::Networks(networks) => {
for network in &networks {
let borrowed_wifi_networks_clone = wifi_networks_clone.borrow_mut();
let widget = borrowed_wifi_networks_clone.get(&network.name);
if widget.is_none() {
let widget = Self::create_wifi_widget(
&channels,
network,
borrowed_wifi_networks_clone,
);
content.append(&widget);
} else {
let idx = ((network.signal as f32 / 100.0)
* (WIFI_ICONS.len() - 1) as f32
+ 0.5) as usize;
widget.unwrap().set_wifi_icon_from_name(WIFI_ICONS[idx]);
}
}
let mut removal: Vec<String> = Vec::new();
for (ssid, widget) in wifi_networks_clone.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_clone.borrow_mut().remove(&ssid);
}
}
NetworkState::Auth(ssid, auth) => {
if let Some(widget) = Self::find_focused_widget((&window_clone).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_clone.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_clone.borrow().get(&ssid) {
widget.stop_spinner();
}
}
_ => {}
}
}
ControlFlow::Continue
});
instance
}
fn create_wifi_widget(
channels: &BackendChannels,
network: &NetworkInfo,
mut borrowed_wifi_networks_clone: 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_clone.insert(ssid.clone(), widget.clone());
let net_tx = channels.net_tx();
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().iter() {
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(&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();
} else {
return;
}
}
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
}
});
}
}

40
src/ui/systray.rs Normal file
View File

@@ -0,0 +1,40 @@
use crate::LOGGER;
use glib::ControlFlow;
use gtk::Orientation;
use jmb_logger_rs::*;
use rustsni::{TrayEvent, TrayHost};
use std::time::Duration;
pub fn _build() -> gtk::Box {
let systray = gtk::Box::new(Orientation::Horizontal, 0);
let host = TrayHost::new();
if let Ok(mut host) = host {
glib::timeout_add_local(
Duration::from_millis(1_000),
move || {
for event in host.poll().unwrap() {
match event {
TrayEvent::ItemAdded(id) | TrayEvent::ItemChanged(id) => {
if let Some(item) = host.items().get(&id) {
println!("tray: {} — {}", item.title, item.status);
println!(" category: {}", item.category);
println!(" has menu: {}", item.has_menu());
}
}
TrayEvent::ItemRemoved(id) => {
println!("item gone: {id}");
}
TrayEvent::HostShutdown => break,
_ => {}
}
}
ControlFlow::Continue
}
);
} else {
error!(&LOGGER, "Couldn't get SystemTray");
}
systray
}

95
src/ui/title_panel.rs Normal file
View File

@@ -0,0 +1,95 @@
use crate::backend::window_manager_hypr::HyprlandComms;
use glib::prelude::CastNone;
use gtk::pango::EllipsizeMode;
use gtk::prelude::{ButtonExt, GestureSingleExt, WidgetExt};
use gtk::{Align, Button, GestureClick, Label};
use mpris::{PlaybackStatus, Player, PlayerFinder};
pub fn build() -> Button {
let title_label = Label::new(None);
title_label.set_ellipsize(EllipsizeMode::End);
title_label.set_max_width_chars(50);
let title = Button::new();
title.set_vexpand(true);
title.set_valign(Align::Fill);
title.set_child(Some(&title_label));
title.set_visible(false);
title.add_css_class("title");
title.connect_clicked(|button| {
if button.has_css_class("media") {
if let Ok(player_finder) = PlayerFinder::new() {
if let Ok(player) = player_finder.find_active() {
let _ = player.play_pause();
}
}
}
});
let right_click = GestureClick::new();
right_click.set_button(3);
right_click.connect_pressed(|_gesture, _n_press, _, _| {
if let Ok(player_finder) = PlayerFinder::new() {
if let Ok(player) = player_finder.find_active() {
let _ = player.stop();
}
}
});
title.add_controller(right_click);
title
}
pub fn manage_title(button_opt: &Option<Button>, player_opt: &Option<Player>) {
let mut handled = false;
if let Some(button) = button_opt {
if let Some(player) = player_opt {
if let Ok(status) = player.get_playback_status() {
if let Ok(metadata) = player.get_metadata() {
match status {
PlaybackStatus::Playing => {
button.set_visible(true);
button.child()
.and_downcast::<Label>()
.unwrap()
.set_label(&format!("{}", metadata.title().unwrap_or("")));
button.add_css_class("media");
button.add_css_class("playing");
handled = true;
}
PlaybackStatus::Paused => {
button.set_visible(true);
button.child()
.and_downcast::<Label>()
.unwrap()
.set_label(&format!("{}", metadata.title().unwrap_or("")));
button.add_css_class("media");
button.remove_css_class("playing");
handled = true;
}
PlaybackStatus::Stopped => {},
}
}
}
}
if !handled {
button.set_visible(true);
button.remove_css_class("media");
button.remove_css_class("playing");
set_title(&HyprlandComms::get_current_title(), button_opt);
}
}
}
pub fn set_title(title: &str, title_button: &Option<Button>) {
if let Some(button) = title_button {
if title.is_empty() {
button.set_visible(false);
} else {
button.set_visible(true);
}
button.child()
.and_downcast::<Label>()
.unwrap()
.set_label(&HyprlandComms::get_current_title());
}
}

521
src/ui/utilities_panel.rs Normal file
View File

@@ -0,0 +1,521 @@
use crate::LOGGER;
use crate::backend::BackendChannels;
use crate::backend::audio::MediaCommand;
use crate::backend::brightness_control::BrightnessMessage;
use crate::backend::network::{NetworkCommand, NetworkState, NetworkType};
use crate::backend::tailscale::TailscaleState;
use crate::ui::action_centre::ActionCentre;
use crate::ui::widgets::{ExpandableBarItem, IconSlider};
use battery::State;
use glib::prelude::Cast;
use glib::subclass::prelude::ObjectSubclassIsExt;
use glib::{ControlFlow, clone};
use gtk::prelude::{
ApplicationExt, BoxExt, ButtonExt, DisplayExt, ListModelExt, MonitorExt, ToggleButtonExt,
WidgetExt,
};
use gtk::{
Adjustment, Align, Application, Button, Image, Label, Orientation, Revealer,
RevealerTransitionType, gdk,
};
use jmb_logger_rs::*;
use std::cell::Cell;
use std::process::Command;
use std::rc::Rc;
use std::time::Duration;
const BRIGHTNESS_ICONS: [&str; 3] = ["", "", ""];
const BATTERY_ICONS: [&str; 5] = ["", "", "", "", ""];
const WIFI_ICON_PATHS: [&str; 3] = [
"icons/wifi_low.svg",
"icons/wifi_medium.svg",
"icons/wifi_full.svg",
];
const VOLUME_ICON_PATHS: [&str; 4] = [
"icons/volume-off.svg",
"icons/volume-low.svg",
"icons/volume.svg",
"icons/volume-high.svg",
];
pub fn build(
app: &Application,
action_centre: ActionCentre,
mut channels: BackendChannels,
monitor_idx: u32,
display: &gdk::Display,
) -> gtk::Box {
let utilities = gtk::Box::new(Orientation::Horizontal, 0);
utilities.set_halign(Align::End);
utilities.set_vexpand(true);
utilities.set_hexpand(true);
utilities.set_valign(Align::Fill);
utilities.add_css_class("utilities");
let monitors = display.monitors();
let monitor = if let Some(monitor) = monitors.item(monitor_idx)
&& let Ok(monitor) = monitor.downcast::<gdk::Monitor>()
&& let Some(connector) = monitor.connector()
{
String::from(connector)
} else {
String::new()
};
let monitor_valid = !monitor.is_empty();
if monitor_valid {
let _ = channels
.br_tx()
.send(BrightnessMessage::Request(monitor.clone()))
.is_ok();
}
let volume_adjustment = Adjustment::new(60.0, 1.0, 100.0, 1.0, 5.0, 0.0);
let volume = IconSlider::new(Some(volume_adjustment));
volume.imp().main_icon.set_icon_path(VOLUME_ICON_PATHS[2]);
volume.imp().second_icon.set_icon_name("bluetooth-symbolic");
volume.imp().second_icon.add_child_css_class("bt-icon");
let media_tx = channels.media_tx();
let changed_progr = Rc::new(Cell::new(false));
let changed_progr_clone = changed_progr.clone();
volume.connect_value_changed(move |slider, button, _| {
if !changed_progr_clone.get() {
let _ = media_tx.send(MediaCommand::Cmd(
slider.value() as u8,
button.is_active(),
false,
));
}
});
let media_tx = channels.media_tx();
volume.connect_clicked(move |slider, button, _| {
if button.is_active() {
let _ = media_tx.send(MediaCommand::Cmd(slider.value() as u8, true, false));
} else {
let _ = media_tx.send(MediaCommand::Cmd(slider.value() as u8, false, false));
}
});
let brightness_adjustment = Adjustment::new(60.0, 1.0, 100.0, 1.0, 5.0, 0.0);
let brightness = IconSlider::new(Some(brightness_adjustment));
brightness.imp().main_icon.set_font_icon("");
let br_tx = channels.br_tx();
brightness.connect_value_changed(clone!(
#[strong]
monitor,
move |slider, _button, _| {
if monitor_valid {
let slider_val = slider.value().max(1.0);
let _ = br_tx.send(BrightnessMessage::Value(monitor.clone(), slider_val as u8));
let idx =
((slider_val / 100.0) * (BRIGHTNESS_ICONS.len() - 1) as f64 + 0.5) as usize;
slider.imp().main_icon.set_font_icon(BRIGHTNESS_ICONS[idx]);
}
}
));
let br_tx = channels.br_tx();
brightness.connect_clicked(clone!(
#[strong]
monitor,
move |slider, _button, _| {
if monitor_valid {
let _ = br_tx.send(BrightnessMessage::Value(monitor.clone(), 60));
slider.set_value(60.0);
let idx = ((60.0 / 100.0) * (BRIGHTNESS_ICONS.len() - 1) as f64 + 0.5) as usize;
slider.imp().main_icon.set_font_icon(BRIGHTNESS_ICONS[idx]);
}
}
));
let caffeine_button = Button::with_label("");
caffeine_button.add_css_class("caffeine");
let hypridle = channels.hypridle();
caffeine_button.connect_clicked(move |button| {
let mut child = hypridle.lock().expect("Couldn't get lock on hypridle");
if child.is_none() {
if let Ok(new_child) = Command::new("hypridle").spawn() {
let _ = child.insert(new_child);
button.remove_css_class("on")
}
} else if child.is_some()
&& let Ok(exit) = child.as_mut().unwrap().try_wait()
&& let Some(_) = exit
{
if let Ok(new_child) = Command::new("hypridle").spawn() {
let _ = child.insert(new_child);
button.remove_css_class("on")
}
} else {
if let Ok(_) = child.as_mut().unwrap().kill() {
let _ = child.take();
button.add_css_class("on");
}
}
});
let stats = ExpandableBarItem::new("");
stats.add_css_class("stats");
stats.set_spacing(0);
stats.set_vexpand(true);
stats.set_valign(Align::Fill);
let cpu_box = gtk::Box::new(Orientation::Horizontal, 8);
let cpu_icon = Label::new(Some(""));
cpu_icon.add_css_class("stat-icon");
cpu_box.append(&cpu_icon);
let cpu_label = Label::new(None);
cpu_box.append(&cpu_label);
stats.set_button_child(Some(&cpu_box));
let stats_children = gtk::Box::new(Orientation::Horizontal, 6);
stats_children.add_css_class("expandable-content");
let ram_box = gtk::Box::new(Orientation::Horizontal, 6);
let ram_icon = Label::new(Some(""));
ram_icon.add_css_class("stat-icon");
ram_box.append(&ram_icon);
let ram_label = Label::new(None);
ram_box.append(&ram_label);
stats_children.append(&ram_box);
let disk_box = gtk::Box::new(Orientation::Horizontal, 9);
let disk_icon = Label::new(Some(""));
disk_icon.add_css_class("stat-icon");
disk_box.append(&disk_icon);
let disk_label = Label::new(None);
disk_box.append(&disk_label);
stats_children.append(&disk_box);
let gpu_box = gtk::Box::new(Orientation::Horizontal, 9);
let gpu_icon = Image::from_file("icons/gpu-card.svg");
gpu_icon.add_css_class("stat-icon");
gpu_box.append(&gpu_icon);
let gpu_label = Label::new(None);
gpu_box.append(&gpu_label);
stats_children.append(&gpu_box);
stats.set_second_child(stats_children);
let network_button = Button::new();
network_button.add_css_class("network");
let network_button_content = gtk::Box::new(Orientation::Horizontal, 0);
let network_icon = Image::from_file("../../icons/disconnected.svg");
network_icon.add_css_class("network-icon");
network_button_content.append(&network_icon);
let network_label = Label::new(None);
network_label.add_css_class("network-label");
network_label.set_visible(false);
network_button_content.append(&network_label);
network_button.set_child(Some(&network_button_content));
network_button.connect_clicked(clone!(
#[weak]
display,
move |_| {
if action_centre.window.is_visible() {
action_centre.hide_action_centre();
} else {
action_centre.show_action_centre(monitor_idx, &display);
}
}
));
let wg_button = Button::with_label("wg");
wg_button.add_css_class("vpn");
let net_tx = channels.net_tx();
wg_button.connect_clicked(move |_| {
let _ = net_tx.send(NetworkCommand::ToggleVpn(String::from("wg")));
});
let ts_button = Button::with_label("ts");
ts_button.add_css_class("vpn");
ts_button.connect_clicked(move |button| {
let _ = Command::new("tailscale")
.arg(if button.has_css_class("on") {
"down"
} else {
"up"
})
.output();
});
let tud_button = Button::with_label("tud");
tud_button.add_css_class("vpn");
let mvd_button = Button::with_label("mvd");
mvd_button.add_css_class("vpn");
let vpn_box = gtk::Box::new(Orientation::Horizontal, 0);
vpn_box.set_valign(Align::Fill);
vpn_box.set_vexpand(true);
vpn_box.append(&wg_button);
vpn_box.append(&ts_button);
//vpn_box.append(&tud_button);
//vpn_box.append(&mvd_button);
let vpn_revealer = Revealer::new();
vpn_revealer.set_transition_type(RevealerTransitionType::SlideRight);
vpn_revealer.set_transition_duration(500);
vpn_revealer.set_reveal_child(false);
vpn_revealer.set_child(Some(&vpn_box));
vpn_revealer.add_css_class("revealer");
let vpn_button = Button::with_label("");
vpn_button.add_css_class("vpn-icon");
let battery_widget = ExpandableBarItem::new("");
battery_widget.add_css_class("battery");
battery_widget.set_spacing(0);
battery_widget.set_vexpand(true);
battery_widget.set_valign(Align::Fill);
let battery_children = gtk::Box::new(Orientation::Horizontal, 0);
let wattage = Label::new(None);
battery_children.append(&wattage);
let remaining_time = Label::new(None);
battery_children.append(&remaining_time);
battery_widget.set_second_child(battery_children);
let time = ExpandableBarItem::new("");
time.add_css_class("time");
time.set_spacing(0);
time.set_vexpand(true);
time.set_valign(Align::Fill);
let time_hidden = Label::new(None);
time.set_second_child(time_hidden.clone());
let power_button = Button::with_label("");
power_button.add_css_class("power-button");
power_button.connect_clicked(clone!(
#[strong]
app,
move |_button| {
app.quit();
//let _ = Command::new("wlogout").spawn();
}
));
utilities.append(&volume);
utilities.append(&brightness);
utilities.append(&caffeine_button);
utilities.append(&stats);
utilities.append(&network_button);
utilities.append(&vpn_button);
utilities.append(&vpn_revealer);
//utilities.append(&systray_box);
utilities.append(&battery_widget);
utilities.append(&time);
utilities.append(&power_button);
vpn_button.connect_clicked(move |_button| {
vpn_revealer.set_reveal_child(!vpn_revealer.is_child_revealed());
});
glib::timeout_add_local(Duration::from_millis(1000), move || {
let timestamp = time_format::now().unwrap();
let time_str = time_format::strftime_local("%H:%M", timestamp).unwrap_or(String::new());
time.set_title(&time_str);
let time_sec =
time_format::strftime_local(":%S %Y-%m-%d", timestamp).unwrap_or(String::new());
time_hidden.set_label(&time_sec);
ControlFlow::Continue
});
let media_rx = channels.take_media_rx().unwrap();
let br_rx = channels.take_br_rx().unwrap();
let net_rx = channels.take_net_rx().unwrap();
let stats_rx = channels.take_stats_rx().unwrap();
let ts_rx = channels.take_ts_rx().unwrap();
let hypridle = channels.hypridle();
glib::timeout_add_local(Duration::from_millis(100), move || {
while let Ok(command) = media_rx.try_recv() {
match command {
MediaCommand::Cmd(vol, mute, bt) => {
changed_progr.set(true);
volume.set_value(vol as f64);
changed_progr.set(false);
volume.show_scale(!mute);
volume.imp().button.set_active(mute);
if mute {
volume.imp().button.add_css_class("muted");
volume.imp().second_icon.add_child_css_class("muted");
volume
.imp()
.main_icon
.set_icon_path("icons/volume-mute.svg");
} else {
volume.imp().button.remove_css_class("muted");
volume.imp().second_icon.remove_child_css_class("muted");
let idx = ((volume.value() / 100.0) * (VOLUME_ICON_PATHS.len() - 1) as f64
+ 0.5) as usize;
volume.imp().main_icon.set_icon_path(VOLUME_ICON_PATHS[idx]);
}
volume.imp().second_icon.set_visible(bt);
}
_ => {}
}
}
while let Ok(message) = br_rx.try_recv() {
match message {
BrightnessMessage::Value(mon, value) => {
if monitor.eq(&mon) {
brightness.set_value(value as f64);
let idx = ((value as f32 / 100.0) * (BRIGHTNESS_ICONS.len() - 1) as f32
+ 0.5) as usize;
brightness
.imp()
.main_icon
.set_font_icon(BRIGHTNESS_ICONS[idx]);
}
}
_ => {}
}
}
if let Ok(mut hypridle) = hypridle.try_lock() {
if hypridle.is_none() {
caffeine_button.add_css_class("on");
} else {
if let Ok(exit) = hypridle.as_mut().unwrap().try_wait()
&& exit.is_some()
{
caffeine_button.add_css_class("on");
} else {
caffeine_button.remove_css_class("on");
}
}
}
while let Ok(result) = net_rx.try_recv() {
trace!(&LOGGER, "NetworkResult: {:?}", result);
match result {
NetworkState::ChangeNetwork(cfg) => {
match cfg {
Some(network) => {
match network.network_type {
NetworkType::Wifi => {
//let idx = ((20.0 / 100.0) * (WIFI_ICON_PATHS.len() - 1) as f32 + 0.5) as usize;
let idx = ((network.signal as f32 / 100.0)
* (WIFI_ICON_PATHS.len() - 1) as f32
+ 0.5)
as usize;
network_icon.set_from_file(Some(WIFI_ICON_PATHS[idx]));
network_label.set_label(&network.name);
network_label.set_visible(true);
}
NetworkType::Wired => {
network_icon.set_from_file(Some("icons/ethernet.svg"));
network_label.set_visible(false);
}
}
}
None => {
network_icon.set_from_file(Some("icons/disconnected.svg"));
network_label.set_visible(false);
}
}
}
NetworkState::VpnState { connected, profile } => match profile.as_str() {
"wg" => {
if connected {
wg_button.add_css_class("on");
} else {
wg_button.remove_css_class("on");
}
}
_ => {}
},
_ => {}
}
}
while let Ok(tailscale_state) = ts_rx.try_recv() {
match tailscale_state {
TailscaleState::Stopped => {
ts_button.remove_css_class("on");
ts_button.remove_css_class("starting");
ts_button.remove_css_class("unknown");
}
TailscaleState::Starting => {
ts_button.remove_css_class("on");
ts_button.add_css_class("starting");
ts_button.remove_css_class("unknown");
}
TailscaleState::Running => {
ts_button.add_css_class("on");
ts_button.remove_css_class("starting");
ts_button.remove_css_class("unknown");
}
_ => {
ts_button.remove_css_class("on");
ts_button.remove_css_class("starting");
ts_button.add_css_class("unknown");
}
}
}
if wg_button.has_css_class("on") || ts_button.has_css_class("on") {
vpn_button.add_css_class("on");
} else {
vpn_button.remove_css_class("on");
}
while let Ok(stats) = stats_rx.try_recv() {
cpu_label.set_label(&format!("{}%", stats.cpu));
ram_label.set_label(&format!("{}%", stats.ram));
for disk in stats.disks {
if disk.0.eq("/") {
disk_label.set_label(&format!("{}%", disk.1));
break;
}
}
for gpu in stats.gpus {
gpu_label.set_label(&format!("{}%", gpu.1));
break;
}
set_battery_css_classes(&battery_widget, stats.battery_state, stats.battery_charge);
let idx = ((stats.battery_charge as f32 / 100.0) * (BATTERY_ICONS.len() - 1) as f32
+ 0.5) as usize;
let idx = idx.min(BATTERY_ICONS.len() - 1);
battery_widget.set_title(&format!("{} {}%", BATTERY_ICONS[idx], stats.battery_charge));
wattage.set_label(&format!("{:.1}W", stats.battery_draw));
remaining_time.set_label(&format!("{}h", format_remaining(stats.battery_time)));
}
ControlFlow::Continue
});
utilities
}
fn format_remaining(ms: u64) -> String {
let total_minutes = ms / 60_000;
let hours = total_minutes / 60;
let minutes = total_minutes % 60;
format!("{hours}:{minutes:02}")
}
fn set_battery_css_classes(battery_widget: &ExpandableBarItem, state: State, percentage: u8) {
if state == State::Charging {
battery_widget.add_css_class("charging");
} else {
battery_widget.remove_css_class("charging");
battery_widget.remove_css_class("critical");
battery_widget.remove_css_class("low");
battery_widget.remove_css_class("medium");
match percentage {
0..=10 => {
battery_widget.add_css_class("critical");
}
11..=20 => {
battery_widget.add_css_class("low");
}
21..=50 => {
battery_widget.add_css_class("medium");
}
51..=100 => {}
101.. => {
battery_widget.set_title("<invalid>");
}
}
}
}

646
src/ui/widgets.rs Normal file
View File

@@ -0,0 +1,646 @@
use glib::ParamSpec;
use glib::object::IsA;
use glib::prelude::ObjectExt;
use glib::subclass::prelude::*;
use gtk::prelude::*;
use gtk::{Adjustment, Button, Label, Scale, ToggleButton, Widget};
mod imp_label_entry {
use glib::subclass::object::ObjectImpl;
use glib::subclass::prelude::*;
use gtk::prelude::*;
use gtk::subclass::box_::BoxImpl;
use gtk::subclass::prelude::*;
use gtk::{Entry, Label, Orientation};
#[derive(Default)]
pub struct LabelEntry {
pub label: std::cell::OnceCell<Label>,
pub entry: std::cell::OnceCell<Entry>,
}
#[glib::object_subclass]
impl ObjectSubclass for LabelEntry {
const NAME: &'static str = "EntryLabel";
type Type = super::LabelEntry;
type ParentType = gtk::Box;
}
impl ObjectImpl for LabelEntry {
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
obj.set_orientation(Orientation::Horizontal);
obj.set_spacing(4);
let label = Label::new(None);
let entry = Entry::new();
obj.append(&label);
obj.append(&entry);
self.label.set(label.clone()).unwrap();
self.entry.set(entry.clone()).unwrap();
}
}
impl WidgetImpl for LabelEntry {}
impl BoxImpl for LabelEntry {}
}
mod imp_icon_label {
use crate::ui::widgets::Icon;
use glib::Properties;
use glib::property::PropertySet;
use glib::subclass::object::ObjectImpl;
use glib::subclass::prelude::*;
use gtk::prelude::*;
use gtk::subclass::box_::BoxImpl;
use gtk::subclass::prelude::*;
use gtk::{Label, Orientation};
use std::cell::RefCell;
#[derive(Default, Properties)]
#[properties(wrapper_type = super::IconLabel)]
pub struct IconLabel {
pub icon: std::cell::OnceCell<Icon>,
pub label: std::cell::OnceCell<Label>,
#[property(get, set = Self::set_icon_path, name = "icon-path")]
icon_path: RefCell<String>,
#[property(get, set = Self::set_icon_name, name = "icon-name")]
icon_name: RefCell<String>,
#[property(get, set = Self::set_font_icon, name = "font-icon")]
font_icon: RefCell<String>,
#[property(get, set = Self::set_label, name = "label")]
label_text: RefCell<String>,
}
#[glib::object_subclass]
impl ObjectSubclass for IconLabel {
const NAME: &'static str = "JBIconLabel";
type Type = super::IconLabel;
type ParentType = gtk::Box;
}
#[glib::derived_properties]
impl ObjectImpl for IconLabel {
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
obj.set_orientation(Orientation::Horizontal);
obj.set_spacing(4);
let icon = Icon::new();
obj.append(&icon);
let label = Label::new(None);
obj.append(&label);
self.icon.set(icon.clone()).unwrap();
self.label.set(label.clone()).unwrap();
}
}
impl WidgetImpl for IconLabel {}
impl BoxImpl for IconLabel {}
impl IconLabel {
fn set_icon_path(&self, path: String) {
self.icon_path.set(path.clone());
self.icon.get().unwrap().set_icon_path(path);
}
pub fn set_icon_name(&self, name: String) {
self.icon_name.set(name.clone());
self.icon.get().unwrap().set_icon_name(name);
}
pub fn set_font_icon(&self, icon: &str) {
self.font_icon.set(icon.to_string());
self.icon.get().unwrap().set_font_icon(icon);
}
pub fn set_label(&self, text: &str) {
self.label_text.set(text.to_string());
self.label.get().unwrap().set_text(text);
}
}
}
mod imp_icon {
use glib::Properties;
use glib::object::ObjectExt;
use glib::property::PropertySet;
use glib::subclass::InitializingObject;
use glib::subclass::object::ObjectImpl;
use glib::subclass::prelude::*;
use gtk::prelude::WidgetExt;
use gtk::subclass::box_::BoxImpl;
use gtk::subclass::prelude::*;
use gtk::{CompositeTemplate, Image, Label, TemplateChild};
use std::cell::{Cell, RefCell};
#[derive(CompositeTemplate, Default, Properties)]
#[properties(wrapper_type = super::Icon)]
#[template(resource = "/net/jmb19905/jmb-bar/icon.ui")]
pub struct Icon {
#[template_child]
pub image: TemplateChild<Image>,
#[template_child]
pub label: TemplateChild<Label>,
#[property(get, set = Self::add_child_css_class, name = "child-css-class")]
child_css_class: RefCell<String>,
#[property(get, set = Self::set_icon_size, minimum = 1, maximum = 255, default = 16, name = "pixel-size")]
pixel_size: Cell<i32>,
#[property(get, set = Self::set_icon_path, name = "icon-path")]
icon_path: RefCell<String>,
#[property(get, set = Self::set_icon_name, name = "icon-name")]
icon_name: RefCell<String>,
#[property(get, set = Self::set_font_icon, name = "font-icon")]
font_icon: RefCell<String>,
}
#[glib::object_subclass]
impl ObjectSubclass for Icon {
const NAME: &'static str = "JBIcon";
type Type = super::Icon;
type ParentType = gtk::Box;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
#[glib::derived_properties]
impl ObjectImpl for Icon {}
impl WidgetImpl for Icon {}
impl BoxImpl for Icon {}
impl Icon {
fn add_child_css_class(&self, class: &str) {
self.child_css_class.set(class.to_string());
if self.label.is_bound() {
self.label.add_css_class(class);
}
if self.image.is_bound() {
self.image.add_css_class(class);
}
}
fn set_icon_size(&self, size: i32) {
self.pixel_size.set(size);
if self.image.is_bound() {
self.image.set_pixel_size(size);
}
}
fn set_icon_path(&self, path: String) {
self.image.set_from_file(Some(path));
self.label.set_visible(false);
self.image.set_visible(true);
}
pub fn set_icon_name(&self, name: String) {
self.image.set_icon_name(Some(&name));
self.label.set_visible(false);
self.image.set_visible(true);
}
pub fn set_font_icon(&self, icon: &str) {
self.label.set_label(icon);
self.label.set_visible(true);
self.image.set_visible(false);
}
}
}
mod imp_icon_slider {
use glib::subclass::InitializingObject;
use glib::subclass::object::ObjectImpl;
use glib::subclass::prelude::ObjectSubclass;
use gtk::subclass::box_::BoxImpl;
use gtk::subclass::prelude::*;
use gtk::{CompositeTemplate, TemplateChild, ToggleButton};
#[derive(CompositeTemplate, Default)]
#[template(resource = "/net/jmb19905/jmb-bar/icon_slider.ui")]
pub struct IconSlider {
#[template_child]
pub button: TemplateChild<ToggleButton>,
#[template_child]
pub main_icon: TemplateChild<super::Icon>,
#[template_child]
pub second_icon: TemplateChild<super::Icon>,
#[template_child]
pub slider: TemplateChild<gtk::Scale>,
}
#[glib::object_subclass]
impl ObjectSubclass for IconSlider {
const NAME: &'static str = "JBIconSlider";
type Type = super::IconSlider;
type ParentType = gtk::Box;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for IconSlider {}
impl WidgetImpl for IconSlider {}
impl BoxImpl for IconSlider {}
}
mod imp_bar_item {
use glib::Properties;
use glib::object::ObjectExt;
use glib::property::PropertySet;
use glib::subclass::InitializingObject;
use glib::subclass::object::ObjectImpl;
use glib::subclass::prelude::*;
use gtk::prelude::{ButtonExt, WidgetExt};
use gtk::subclass::box_::BoxImpl;
use gtk::subclass::prelude::*;
use gtk::{Button, CompositeTemplate, EventControllerMotion, Revealer, TemplateChild, Widget};
use std::cell::RefCell;
#[derive(CompositeTemplate, Default, Properties)]
#[properties(wrapper_type = super::ExpandableBarItem)]
#[template(resource = "/net/jmb19905/jmb-bar/expandable_bar_item.ui")]
pub struct ExpandableBarItem {
#[template_child]
pub main_button: TemplateChild<Button>,
#[template_child]
pub revealer: TemplateChild<Revealer>,
#[property(get, set = Self::set_main_child, name = "main-child")]
pub main_child: RefCell<Option<Widget>>,
#[property(get, set = Self::set_second_child, name = "second-child")]
pub second_child: RefCell<Option<Widget>>,
}
#[glib::object_subclass]
impl ObjectSubclass for ExpandableBarItem {
const NAME: &'static str = "JBExpandableBarItem";
type Type = super::ExpandableBarItem;
type ParentType = gtk::Box;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
#[glib::derived_properties]
impl ObjectImpl for ExpandableBarItem {
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
let motion = EventControllerMotion::new();
let revealer_enter = self.revealer.clone();
let button_enter = self.main_button.clone();
motion.connect_enter(move |_, _, _| {
revealer_enter.set_reveal_child(true);
button_enter.add_css_class("hovered");
});
let revealer_leave = self.revealer.clone();
let button_leave = self.main_button.clone();
motion.connect_leave(move |_| {
revealer_leave.set_reveal_child(false);
button_leave.remove_css_class("hovered");
});
obj.add_controller(motion);
}
}
impl WidgetImpl for ExpandableBarItem {}
impl BoxImpl for ExpandableBarItem {}
impl ExpandableBarItem {
fn set_main_child(&self, widget: Option<&Widget>) {
self.main_child.set(widget.cloned());
self.main_button.set_child(widget);
}
fn set_second_child(&self, widget: Option<&Widget>) {
self.second_child.set(widget.cloned());
self.revealer.set_child(widget);
}
}
}
mod imp_wifi_widget {
use glib::subclass::InitializingObject;
use glib::subclass::prelude::ObjectSubclass;
use gtk::subclass::prelude::*;
use gtk::{Button, CompositeTemplate, Label, PasswordEntry, Spinner, Stack, TemplateChild};
#[derive(CompositeTemplate, Default)]
#[template(resource = "/net/jmb19905/jmb-bar/wifi_widget.ui")]
pub struct WifiWidget {
#[template_child]
pub wifi_icon: TemplateChild<super::Icon>,
#[template_child]
pub spinner: TemplateChild<Spinner>,
#[template_child]
pub stack: TemplateChild<Stack>,
#[template_child]
pub ssid: TemplateChild<Label>,
#[template_child]
pub password: TemplateChild<PasswordEntry>,
#[template_child]
pub known: TemplateChild<super::Icon>,
#[template_child]
pub secure: TemplateChild<super::Icon>,
}
#[glib::object_subclass]
impl ObjectSubclass for WifiWidget {
const NAME: &'static str = "JBWifiWidget";
type Type = super::WifiWidget;
type ParentType = Button;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for WifiWidget {}
impl WidgetImpl for WifiWidget {}
impl ButtonImpl for WifiWidget {}
}
glib::wrapper! {
pub struct LabelEntry(ObjectSubclass<imp_label_entry::LabelEntry>)
@extends gtk::Box, Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable;
}
impl LabelEntry {
pub fn new(title: &str) -> Self {
let obj: Self = glib::Object::new();
if let Some(label) = obj.clone().downcast::<LabelEntry>().unwrap().label() {
label.set_label(title);
}
obj
}
pub fn label(&self) -> Option<Label> {
let imp = self.imp();
if let Some(label) = imp.label.get() {
return Some(label.clone());
}
None
}
}
glib::wrapper! {
pub struct IconLabel(ObjectSubclass<imp_icon_label::IconLabel>)
@extends gtk::Box, Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable;
}
impl IconLabel {
pub fn new() -> Self {
let obj: Self = glib::Object::new();
obj
}
}
glib::wrapper! {
pub struct Icon(ObjectSubclass<imp_icon::Icon>)
@extends gtk::Box, Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable;
}
impl Icon {
pub fn new() -> Self {
let obj: Self = glib::Object::new();
obj
}
pub fn from_font_icon(icon: &str) -> Self {
let obj = Self::new();
obj.set_font_icon(icon);
obj
}
pub fn from_icon_path(
path: Option<impl AsRef<std::path::Path> + std::borrow::Borrow<str>>,
) -> Self {
let obj = Self::new();
if let Some(path) = path {
obj.set_icon_path(path);
}
obj
}
pub fn from_icon_name(name: &str) -> Self {
let obj = Self::new();
obj.set_icon_name(name);
obj
}
pub fn add_child_css_class(&self, class: &str) {
self.imp().image.add_css_class(class);
self.imp().label.add_css_class(class);
}
pub fn remove_child_css_class(&self, class: &str) {
self.imp().image.remove_css_class(class);
self.imp().label.remove_css_class(class);
}
}
glib::wrapper! {
pub struct IconSlider(ObjectSubclass<imp_icon_slider::IconSlider>)
@extends gtk::Box, Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable;
}
impl IconSlider {
pub fn new(adjustment: Option<Adjustment>) -> Self {
let obj: Self = glib::Object::new();
if let Some(adj) = adjustment {
obj.imp().slider.set_adjustment(&adj);
}
obj
}
pub fn show_second_icon(&self, val: bool) {
self.imp().second_icon.set_visible(val);
}
pub fn connect_value_changed<A>(&self, action: A)
where
A: Fn(&IconSlider, &ToggleButton, &Scale) + 'static,
{
let clone = self.clone();
self.imp().slider.connect_value_changed(move |scale| {
action(&clone, &clone.imp().button, scale);
});
}
pub fn connect_clicked<A>(&self, action: A)
where
A: Fn(&IconSlider, &ToggleButton, &Scale) + 'static,
{
let clone = self.clone();
self.imp().button.connect_clicked(move |button| {
action(&clone, button, &clone.imp().slider);
});
}
pub fn value(&self) -> f64 {
self.imp().slider.value()
}
pub fn set_value(&self, val: f64) {
self.imp().slider.set_value(val);
}
pub fn show_scale(&self, val: bool) {
self.imp().slider.set_visible(val);
}
}
glib::wrapper! {
pub struct ExpandableBarItem(ObjectSubclass<imp_bar_item::ExpandableBarItem>)
@extends gtk::Box, Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable;
}
impl ExpandableBarItem {
pub fn new(title: &str) -> Self {
let obj: Self = glib::Object::new();
obj.set_title(title);
obj
}
pub fn set_title(&self, title: &str) {
self.imp().main_button.set_label(title);
}
pub fn set_icon(&self, icon_name: &str) {
self.imp().main_button.set_label(icon_name);
}
pub fn set_button_child(&self, child: Option<&impl IsA<Widget>>) {
self.imp().main_button.set_child(child);
}
pub fn connect_clicked<F>(&self, action: F)
where
F: Fn(&Button) + 'static,
{
self.imp().main_button.connect_clicked(action);
}
}
glib::wrapper! {
pub struct WifiWidget(ObjectSubclass<imp_wifi_widget::WifiWidget>)
@extends Button, Widget,
@implements gtk::Actionable, gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
}
impl WifiWidget {
pub fn new() -> Self {
let obj: Self = glib::Object::new();
obj
}
pub fn set_ssid(&self, ssid: &str) {
self.imp().ssid.set_label(ssid);
}
pub fn ssid(&self) -> glib::GString {
self.imp().ssid.get().text()
}
pub fn set_secured(&self, secured: bool) {
if secured {
let _ = &self.imp().secure.set_icon_name("channel-secure-symbolic");
}
}
pub fn set_known(&self, known: bool) {
if known {
let _ = &self.imp().known.set_icon_name("user-bookmarks-symbolic");
}
}
pub fn grab_focus_password_entry(&self) -> bool {
self.imp().password.grab_focus()
}
pub fn stop_spinner(&self) {
self.imp().spinner.stop();
}
pub fn start_spinner(&self) {
self.imp().spinner.start();
}
pub fn set_stack_child(&self, name: &str) {
self.imp().stack.set_visible_child_name(name);
}
pub fn show_password_entry(&self) {
self.set_stack_child("pass");
}
pub fn show_ssid(&self) {
self.set_stack_child("ssid");
}
pub fn set_wifi_icon_from_file(&self, icon_path: &str) {
self.imp().wifi_icon.set_icon_path(icon_path);
}
pub fn set_wifi_icon_from_name(&self, icon: &str) {
self.imp().wifi_icon.set_icon_name(icon);
}
pub fn connect_activate_pass<F>(&self, action: F)
where
F: Fn(&gtk::PasswordEntry) + 'static,
{
self.imp().password.connect_activate(action);
}
pub fn connect_has_focus_pass<F>(&self, action: F)
where
F: Fn(&gtk::PasswordEntry, &ParamSpec) + 'static,
{
self.imp()
.password
.connect_notify_local(Some("has-focus"), action);
}
}

View File

@@ -0,0 +1,62 @@
use crate::LOGGER;
use crate::backend::window_manager_hypr::HyprlandComms;
use gtk::prelude::{BoxExt, ButtonExt, WidgetExt};
use gtk::{Align, Button, Orientation};
use hyprland::data::Workspace;
use hyprland::prelude::HyprDataActive;
use jmb_logger_rs::*;
use std::collections::HashMap;
pub(crate) fn build(buttons: &mut HashMap<i32, Button>, monitor: Option<u32>) -> (gtk::Box, Button) {
let workspaces = gtk::Box::new(Orientation::Horizontal, 0);
workspaces.set_halign(Align::Start);
workspaces.set_valign(Align::Center);
workspaces.set_vexpand(true);
workspaces.set_valign(Align::Fill);
workspaces.set_hexpand(true);
workspaces.add_css_class("workspaces");
for workspace in 1..10 {
let button = Button::with_label(&format!("{workspace}"));
button.set_vexpand(true);
button.add_css_class("workspace-button");
button.set_visible(false);
let initial_workspace = Workspace::get_active()
.ok()
.map(|workspace| workspace.id().unwrap_or(1) as i32)
.unwrap_or(1);
let is_active_ws_on_current_monitor = Workspace::get_active()
.ok()
.map(|ws| ws.monitor_id)
.flatten()
.is_some_and(|id| monitor.is_some_and(|other_id| id == other_id as i128));
if workspace == initial_workspace && is_active_ws_on_current_monitor {
button.add_css_class("active");
button.set_visible(true);
}
if HyprlandComms::workspace_added(workspace, monitor.map(|monitor| monitor as i128)) {
button.set_visible(true);
}
button.connect_clicked(move |_button| {
if let Err(err) = HyprlandComms::dispatch(&format!("hl.dsp.focus({{ workspace = {workspace} }})")) {
warn!(&LOGGER, "Couldn't change workspace focus {}", err.to_string());
}
});
workspaces.append(&button);
buttons.insert(workspace, button);
}
let submap_button = Button::new();
submap_button.add_css_class("submap");
submap_button.connect_clicked(|_| {
if let Err(err) = HyprlandComms::dispatch("hl.dsp.submap('reset')") {
warn!(&LOGGER, "Couldn't reset submap {}", err.to_string());
}
});
workspaces.append(&submap_button);
(workspaces, submap_button)
}