From 93fe51b3a596daa12e500745c76e51cb4d9e3610 Mon Sep 17 00:00:00 2001 From: Akrm Al-Hakimi Date: Sun, 2 Nov 2025 01:16:46 -0400 Subject: [PATCH] ui: sharpened ui --- nmrs-ui/src/style.css | 250 +++++++++------------------------ nmrs-ui/src/ui/connect.rs | 2 + nmrs-ui/src/ui/header.rs | 35 +++-- nmrs-ui/src/ui/mod.rs | 14 +- nmrs-ui/src/ui/network_page.rs | 63 +++++---- nmrs-ui/src/ui/networks.rs | 41 ++---- 6 files changed, 152 insertions(+), 253 deletions(-) diff --git a/nmrs-ui/src/style.css b/nmrs-ui/src/style.css index b13ad1cd..b955546b 100644 --- a/nmrs-ui/src/style.css +++ b/nmrs-ui/src/style.css @@ -1,235 +1,117 @@ -/* Global dark background */ window { - background-color: #121212; /* deeper dark */ - color: #e0e0e0; -} - -scrollbar { - background: transparent; - border: none; - min-width: 0; - min-height: 0; + background-color: #121212; + color: #e0e0e0; } +scrollbar, scrollbar slider, scrollbar trough { background: transparent; border: none; + min-width: 0; + min-height: 0; } -/* Header bar */ headerbar { - background: #1e1e1e; - color: #e0e0e0; - border-bottom: 2px solid #3a3a3a; - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.6); + background: #1e1e1e; + color: #e0e0e0; + border-bottom: 1px solid #2a2a2a; } -/* Switch (toggle) */ switch { - background-color: #2a2a2a; - border-radius: 12px; - padding: 2px; + background-color: #2a2a2a; + padding: 2px; } - switch slider { - background-color: #bbbbbb; - border-radius: 10px; + background-color: #bbbbbb; } - -/* Switch when active */ switch:checked { - background-color: #3a7bd5; + background-color: #2563eb; } - switch:checked slider { - background-color: #ffffff; -} - -/* Network selection rows */ -.network-selection { - padding: 8px 12px; - border-radius: 8px; - margin: 4px; - background: #1e1e1e; - border: 1px solid #2c2c2c; -} - -.network-selection label { - font-size: 14px; - color: #e0e0e0; -} - -/* Hover effect */ -.network-selection:hover { - background: #2a2a2a; - border-color: #3a3a3a; - transition: background 150ms ease, border-color 150ms ease; + background-color: #ffffff; } -/* Selected row */ -.network-selection:selected { - background: #3a7bd5; - color: #ffffff; -} - -.wifi-secure { - color: white; -} - -.wifi-open { - color: white; -} - -label.network-good { - color: green; -} - -label.network-okay { - color: orange; -} - -label.network-poor { - color: red; -} - -/* ListBox container */ list { - background: #121212; - border: none; + background: #121212; + border: none; } - -/* Individual rows */ list > row { - background: transparent; - border: none; - padding: 0; + background: transparent; + border: none; + padding: 0; } - -/* Row when selected */ list > row:selected { - background: #3a7bd5; - color: #ffffff; -} - -.modern-dialog { - border-radius: 12px; - background: #2b2b2b; -} - -.dialog-label { - color: #e0e0e0; - font-size: 14px; -} - -.password-entry { - border-radius: 8px; - padding: 6px 10px; - background: #3a3a3a; - color: #f5f5f5; -} - -button { - border-radius: 8px; - padding: 6px 14px; -} - -.wifi-label { - font-weight: bold; - font-size: 15px; + background: #2563eb; + color: #ffffff; } -.network-arrow { - opacity: 0.5; - margin-left: 10px; +.pw-entry { + background-color: transparent; color: white; + border-color: transparent; } -.network-selection:hover .network-arrow { - opacity: 0.9; - transition: opacity 150ms ease; -} - -headerbar .wifi-label { - margin-right: 20px; -} - -headerbar label:last-child { - font-size: 12px; - opacity: 0.7; - margin-top: 4px; +.network-selection { + padding: 6px 10px; + margin: 2px 0; + background: #1e1e1e; + border: 1px solid #2a2a2a; } - -.network-arrow:hover { - opacity: 1.0; - transform: scale(1.1); - transition: transform 120ms ease, opacity 120ms ease; +.network-selection:hover { + background: #2a2a2a; + border-color: #3a3a3a; + transition: background 150ms ease, border-color 150ms ease; } - -.network-details label { +.network-selection label { font-size: 14px; color: #e0e0e0; } -.network-title { - font-size: 16px; - font-weight: bold; - margin-bottom: 8px; -} +label.network-good { color: #22c55e; } +label.network-okay { color: #f59e0b; } +label.network-poor { color: #ef4444; } .network-page { - background: #1e1e1e; - border-radius: 12px; - border: 1px solid #3a3a3a; - padding: 20px; -} - -.network-title { - font-size: 18px; - font-weight: bold; - color: #ffffff; + background: #121212; + padding: 16px 20px; + border: none; + color: #e0e0e0; } -.network-detail { - font-size: 14px; - color: #cccccc; - margin-left: 6px; +.back-button { + background: none; + border: none; + color: #9ca3af; + font-weight: 500; + font-size: 13px; + padding: 4px 0; } +.back-button:hover { color: #ffffff; } -.disconnect-btn { - background: #3a7bd5; +.network-icon { color: #ffffff; - border-radius: 8px; - padding: 6px 14px; + filter: brightness(2); } - -.forget-btn { - background: #d54d3a; +.network-title { + font-size: 18px; + font-weight: 600; color: #ffffff; - border-radius: 8px; - padding: 6px 14px; + margin-bottom: 4px; } -.network-dialog { - background: #1a1a1a; - border: none; - box-shadow: none; +.network-info { + margin-top: 14px; + padding-left: 6px; } - -.network-page { - border-radius: 12px; - background: #1a1a1a; - padding: 20px; +.info-row { padding: 2px 0; } +.info-label { + color: #a3a3a3; + font-size: 13px; + min-width: 120px; } - -button.disconnect-btn { - background: #2563eb; - color: white; - border-radius: 8px; -} - -button.forget-btn { - background: #b91c1c; - color: white; - border-radius: 8px; +.info-value { + color: #ffffff; + font-size: 14px; + font-weight: 500; } diff --git a/nmrs-ui/src/ui/connect.rs b/nmrs-ui/src/ui/connect.rs index 51a9902d..781faf1b 100644 --- a/nmrs-ui/src/ui/connect.rs +++ b/nmrs-ui/src/ui/connect.rs @@ -45,6 +45,7 @@ fn draw_connect_modal(parent: &ApplicationWindow, ssid: &str, is_eap: bool) { let user_entry = if is_eap { let user_label = Label::new(Some("Username:")); let user_entry = Entry::new(); + user_entry.add_css_class("pw-entry"); user_entry.set_placeholder_text(Some("email, username, id...")); vbox.append(&user_label); vbox.append(&user_entry); @@ -55,6 +56,7 @@ fn draw_connect_modal(parent: &ApplicationWindow, ssid: &str, is_eap: bool) { let label = Label::new(Some("Password:")); let entry = Entry::new(); + entry.add_css_class("pw-entry"); entry.set_placeholder_text(Some("Password")); entry.set_visibility(false); vbox.append(&label); diff --git a/nmrs-ui/src/ui/header.rs b/nmrs-ui/src/ui/header.rs index f5636971..b06829fc 100644 --- a/nmrs-ui/src/ui/header.rs +++ b/nmrs-ui/src/ui/header.rs @@ -17,9 +17,11 @@ pub fn build_header( status: &Label, list_container: &GtkBox, parent_window: >k::ApplicationWindow, + stack: >k::Stack, ) -> HeaderBar { let header = HeaderBar::new(); header.set_show_title_buttons(false); + let parent_window = parent_window.clone(); let status = status.clone(); let list_container = list_container.clone(); @@ -42,8 +44,9 @@ pub fn build_header( let list_container_clone = list_container.clone(); let status_clone = status.clone(); let wifi_switch_clone = wifi_switch.clone(); - let pw = parent_window.clone(); + let stack_clone = stack.clone(); + glib::MainContext::default().spawn_local(async move { clear_children(&list_container_clone); @@ -56,7 +59,8 @@ pub fn build_header( status_clone.set_text(""); match nm.list_networks().await { Ok(nets) => { - let list: ListBox = networks::networks_view(&nets, &pw); + let list: ListBox = + networks::networks_view(&nets, &pw, &stack_clone); list_container_clone.append(&list); } Err(err) => { @@ -75,11 +79,14 @@ pub fn build_header( { let pw2 = parent_window.clone(); + let stack_clone = stack.clone(); + wifi_switch.connect_active_notify(move |sw| { let pw = pw2.clone(); let list_container_clone = list_container.clone(); let status_clone = status.clone(); let sw = sw.clone(); + let stack_inner = stack_clone.clone(); glib::MainContext::default().spawn_local(async move { clear_children(&list_container_clone); @@ -100,13 +107,15 @@ pub fn build_header( list_container_clone.clone(), status_clone.clone(), &pw, + &stack_inner, ) .await; match nm.list_networks().await { Ok(nets) => { status_clone.set_text(""); - let list: ListBox = networks::networks_view(&nets, &pw); + let list: ListBox = + networks::networks_view(&nets, &pw, &stack_inner); list_container_clone.append(&list); } Err(err) => { @@ -140,6 +149,7 @@ async fn spawn_signal_listeners( list_container: GtkBox, status: Label, parent_window: >k::ApplicationWindow, + stack: >k::Stack, ) { let conn = match Connection::system().await { Ok(c) => c, @@ -187,20 +197,21 @@ async fn spawn_signal_listeners( let list_container_signal = list_container.clone(); let status_signal = status.clone(); - let pw = parent_window.clone(); + let stack_signal = stack.clone(); + glib::MainContext::default().spawn_local(async move { loop { tokio::select! { event = added.next() => match event { Some(_) => { - schedule_refresh(list_container_signal.clone(), status_signal.clone(), &pw).await; + schedule_refresh(list_container_signal.clone(), status_signal.clone(), &pw, &stack_signal).await; } None => break, }, event = removed.next() => match event { Some(_) => { - schedule_refresh(list_container_signal.clone(), status_signal.clone(), &pw).await; + schedule_refresh(list_container_signal.clone(), status_signal.clone(), &pw, &stack_signal).await; } None => break, }, @@ -214,12 +225,13 @@ async fn refresh_networks( list_container: &GtkBox, status: &Label, parent_window: >k::ApplicationWindow, + stack: >k::Stack, ) { match NetworkManager::new().await { Ok(nm) => match nm.list_networks().await { Ok(nets) => { clear_children(list_container); - let list: ListBox = networks::networks_view(&nets, parent_window); + let list: ListBox = networks::networks_view(&nets, parent_window, stack); list_container.append(&list); } Err(e) => status.set_text(&format!("Error refreshing networks: {e}")), @@ -232,6 +244,7 @@ async fn schedule_refresh( list_container: GtkBox, status: Label, parent_window: >k::ApplicationWindow, + stack: >k::Stack, ) { REFRESH_SCHEDULED.with(|flag| { if flag.get() { @@ -241,15 +254,17 @@ async fn schedule_refresh( let list_container_clone = list_container.clone(); let status_clone = status.clone(); - let pw = parent_window.clone(); + let stack_clone = stack.clone(); + glib::timeout_add_seconds_local(1, move || { let list_container_inner = list_container_clone.clone(); let status_inner = status_clone.clone(); - let pw2 = pw.clone(); + let stack_inner = stack_clone.clone(); + glib::MainContext::default().spawn_local(async move { - refresh_networks(&list_container_inner, &status_inner, &pw2).await; + refresh_networks(&list_container_inner, &status_inner, &pw2, &stack_inner).await; REFRESH_SCHEDULED.with(|f| f.set(false)); }); diff --git a/nmrs-ui/src/ui/mod.rs b/nmrs-ui/src/ui/mod.rs index f8f1855c..9ca8be27 100644 --- a/nmrs-ui/src/ui/mod.rs +++ b/nmrs-ui/src/ui/mod.rs @@ -4,7 +4,9 @@ pub mod network_page; pub mod networks; use gtk::prelude::*; -use gtk::{Application, ApplicationWindow, Box as GtkBox, Label, Orientation, ScrolledWindow}; +use gtk::{ + Application, ApplicationWindow, Box as GtkBox, Label, Orientation, ScrolledWindow, Stack, +}; pub fn build_ui(app: &Application) { let win = ApplicationWindow::new(app); @@ -12,17 +14,19 @@ pub fn build_ui(app: &Application) { win.set_default_size(400, 600); let vbox = GtkBox::new(Orientation::Vertical, 0); - let status = Label::new(None); - let list_container = GtkBox::new(Orientation::Vertical, 0); - let header = header::build_header(&status, &list_container, &win); + let stack = Stack::new(); + stack.add_named(&list_container, Some("networks")); + stack.set_visible_child_name("networks"); + + let header = header::build_header(&status, &list_container, &win, &stack); vbox.append(&header); let scroller = ScrolledWindow::new(); scroller.set_vexpand(true); - scroller.set_child(Some(&list_container)); + scroller.set_child(Some(&stack)); vbox.append(&scroller); win.set_child(Some(&vbox)); diff --git a/nmrs-ui/src/ui/network_page.rs b/nmrs-ui/src/ui/network_page.rs index d0622112..5295d204 100644 --- a/nmrs-ui/src/ui/network_page.rs +++ b/nmrs-ui/src/ui/network_page.rs @@ -1,43 +1,58 @@ +use glib::clone; use gtk::prelude::*; -use gtk::{Box, Button, Image, Label, Orientation}; +use gtk::{Align, Box, Button, Image, Label, Orientation}; use nmrs_core::models::NetworkInfo; -use relm4::RelmWidgetExt; -pub fn network_page(info: &NetworkInfo) -> Box { - let container = Box::new(Orientation::Vertical, 16); +pub fn network_page(info: &NetworkInfo, stack: >k::Stack) -> Box { + let container = Box::new(Orientation::Vertical, 0); container.add_css_class("network-page"); - container.set_margin_all(20); - let header = Box::new(Orientation::Horizontal, 8); + let back = Button::with_label("← Back"); + back.add_css_class("back-button"); + back.set_halign(Align::Start); + back.set_cursor_from_name(Some("pointer")); + back.connect_clicked(clone!( + #[weak] + stack, + move |_| { + stack.set_visible_child_name("networks"); + } + )); + container.append(&back); + + let header = Box::new(Orientation::Horizontal, 6); let icon = Image::from_icon_name("network-wireless-signal-excellent-symbolic"); - icon.set_pixel_size(32); + icon.set_pixel_size(22); let title = Label::new(Some(&info.ssid)); title.add_css_class("network-title"); header.append(&icon); header.append(&title); container.append(&header); - let details = Box::new(Orientation::Vertical, 4); - let bssid = Label::new(Some(&format!("BSSID: {}", info.bssid))); - let strength = Label::new(Some(&format!("Signal: {}%", info.strength))); - let security = Label::new(Some(&format!("Security: {}", info.security))); - details.append(&bssid); - details.append(&strength); - details.append(&security); - container.append(&details); + let info_box = Box::new(Orientation::Vertical, 6); + info_box.add_css_class("network-info"); - let actions = Box::new(Orientation::Horizontal, 12); - actions.set_halign(gtk::Align::End); + let fields = [ + ("BSSID", &info.bssid), + ("Signal Strength", &format!("{}%", info.strength)), + ("Security", &info.security), + ]; - let disconnect = Button::with_label("Disconnect"); - disconnect.add_css_class("disconnect-btn"); - actions.append(&disconnect); + for (label, value) in fields { + let row = Box::new(Orientation::Horizontal, 6); + let key = Label::new(Some(label)); + key.add_css_class("info-label"); + key.set_halign(Align::Start); - let forget = Button::with_label("Forget"); - forget.add_css_class("forget-btn"); - actions.append(&forget); + let val = Label::new(Some(value)); + val.add_css_class("info-value"); + val.set_halign(Align::Start); - container.append(&actions); + row.append(&key); + row.append(&val); + info_box.append(&row); + } + container.append(&info_box); container } diff --git a/nmrs-ui/src/ui/networks.rs b/nmrs-ui/src/ui/networks.rs index 2ba52ba3..ed02aa87 100644 --- a/nmrs-ui/src/ui/networks.rs +++ b/nmrs-ui/src/ui/networks.rs @@ -1,7 +1,6 @@ use glib::clone; use gtk::Align; use gtk::GestureClick; -use gtk::gdk; use gtk::prelude::*; use gtk::{Box, Image, Label, ListBox, ListBoxRow, Orientation}; use nmrs_core::models::WifiSecurity; @@ -13,6 +12,7 @@ use crate::ui::network_page::network_page; pub fn networks_view( networks: &[models::Network], parent_window: >k::ApplicationWindow, + stack: >k::Stack, ) -> ListBox { let conn_threshold = 75; let list = ListBox::new(); @@ -24,7 +24,6 @@ pub fn networks_view( row.add_css_class("network-selection"); let ssid = Label::new(Some(&net.ssid)); - hbox.append(&ssid); let spacer = Box::new(Orientation::Horizontal, 0); @@ -44,8 +43,8 @@ pub fn networks_view( } else { image.add_css_class("wifi-open"); } - let strength_label = Label::new(Some(&format!("{s}%"))); + let strength_label = Label::new(Some(&format!("{s}%"))); hbox.append(&image); hbox.append(&strength_label); @@ -61,59 +60,42 @@ pub fn networks_view( let arrow = Image::from_icon_name("go-next-symbolic"); arrow.set_halign(Align::End); arrow.add_css_class("network-arrow"); + arrow.set_cursor_from_name(Some("pointer")); let arrow_click = GestureClick::new(); - arrow.set_cursor_from_name(Some("pointer")); let ssid_clone = net.ssid.clone(); + arrow_click.connect_pressed(clone!( #[weak] - parent_window, + stack, move |_, _, _, _| { let ssid_clone = ssid_clone.clone(); glib::MainContext::default().spawn_local(async move { if let Ok(nm) = NetworkManager::new().await && let Ok(details) = nm.show_details(&ssid_clone).await { - let page = network_page(&details); - let dialog = gtk::Window::builder() - .title(&details.ssid) - .child(&page) - .transient_for(&parent_window) - .default_width(300) - .default_height(200) - .build(); - - let key = gtk::EventControllerKey::new(); - { - let dialog = dialog.clone(); - key.connect_key_pressed(move |_, key, _, _| { - if key == gdk::Key::Escape { - dialog.close(); - return glib::Propagation::Stop; - } - glib::Propagation::Proceed - }); - } - dialog.add_controller(key); - dialog.present(); + let page = network_page(&details, &stack); + stack.add_named(&page, Some("details")); + stack.set_visible_child_name("details"); } }); } )); + arrow.add_controller(arrow_click); + // Double-click row to connect / open modal for secured networks let ssid_str = net.ssid.clone(); let secured = net.secured; let is_eap = net.is_eap; + gesture.connect_pressed(clone!( #[weak] parent_window, move |_, n_press, _x, _y| { if n_press == 2 && secured { - println!("Double click"); connect::connect_modal(&parent_window, &ssid_str, is_eap); } else if n_press == 2 { - eprintln!("Connecting to {ssid_str}"); glib::MainContext::default().spawn_local({ let ssid = ssid_str.clone(); async move { @@ -133,7 +115,6 @@ pub fn networks_view( )); row.add_controller(gesture); - hbox.append(&arrow); row.set_child(Some(&hbox)); list.append(&row);