system settings
git clone https://git.lucas.co/cce-system-interface.git
Refactor system settings pages including audio, network, accounts, and interface
src/app.rs | 49 +++---
src/main.rs | 452 +++++++++++++++++++++++++++++++++++++------------
src/pages/accounts.rs | 200 ++++++++++++++--------
src/pages/audio.rs | 184 +++++++++++---------
src/pages/hardware.rs | 6 +-
src/pages/interface.rs | 111 ++++++++++--
src/pages/layout.rs | 78 ++++++++-
src/pages/network.rs | 219 +++++++++++++++---------
8 files changed, 908 insertions(+), 391 deletions(-)
diff --git a/src/app.rs b/src/app.rs
index c14bfa3..fe9a45f 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -69,21 +69,9 @@ pub enum AppAction {
#[derive(Default)]
pub struct PageContent {
- pub rects: Vec<([f32; 4], f32, f32, f32, f32)>,
+ pub rects: Vec<([f32; 4], f32, f32, f32, f32, f32)>,
pub texts: Vec<(String, f32, f32, f32, [f32; 4], Option<String>, Option<[f32; 4]>)>,
- pub buttons: Vec<ContentButton>,
-}
-
-#[derive(Clone)]
-pub struct ContentButton {
- pub x: f32, pub y: f32, pub w: f32, pub h: f32,
- pub bg: [f32; 4],
- pub hover_bg: [f32; 4],
- pub label: String,
- pub label_size: f32,
- pub label_color: [f32; 4],
- pub action: AppAction,
- pub left_align: bool,
+ pub buttons: Vec<(clear_ui::widget::Button, AppAction)>,
}
impl PageContent {
@@ -92,7 +80,7 @@ impl PageContent {
}
pub fn rect(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32) {
- self.rects.push((color, x, y, w, h));
+ self.rects.push((color, x, y, w, h, 0.0));
}
pub fn text(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
@@ -106,29 +94,34 @@ impl PageContent {
pub fn button(&mut self, label: &str, x: f32, y: f32, w: f32, h: f32,
bg: [f32; 4], hover_bg: [f32; 4], label_color: [f32; 4],
action: AppAction) {
- self.buttons.push(ContentButton {
- x, y, w, h, bg, hover_bg,
- label: label.to_string(), label_size: 12.0, label_color,
- action,
- left_align: false,
- });
+ let btn = clear_ui::widget::Button::new(x, y, w, h)
+ .with_label(label)
+ .with_bg(bg)
+ .with_hover_bg(hover_bg)
+ .with_label_color(label_color);
+ self.buttons.push((btn, action));
}
pub fn button_left(&mut self, label: &str, x: f32, y: f32, w: f32, h: f32,
bg: [f32; 4], hover_bg: [f32; 4], label_color: [f32; 4],
action: AppAction) {
- self.buttons.push(ContentButton {
- x, y, w, h, bg, hover_bg,
- label: label.to_string(), label_size: 12.0, label_color,
- action,
- left_align: true,
- });
+ let btn = clear_ui::widget::Button::new(x, y, w, h)
+ .with_label(label)
+ .with_bg(bg)
+ .with_hover_bg(hover_bg)
+ .with_label_color(label_color)
+ .with_left_align(true);
+ self.buttons.push((btn, action));
}
}
impl RenderTarget for PageContent {
fn rect(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32) {
- self.rects.push((color, x, y, w, h));
+ self.rects.push((color, x, y, w, h, 0.0));
+ }
+
+ fn rect_with_radius(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32) {
+ self.rects.push((color, x, y, w, h, radius));
}
fn text(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
diff --git a/src/main.rs b/src/main.rs
index 0a17ab6..55d9d5c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,7 +1,7 @@
use clear_ui::widget::{Finger, hover_animation, TextItem, Element, PageSelector};
use glyphon::{Attrs, Buffer, FontSystem, Metrics};
-use cce_system_interface::app::{AppAction, AppState, ContentButton, PageContent};
+use cce_system_interface::app::{AppAction, AppState, PageContent};
use cce_system_interface::pages::{self, Page};
fn make_text_buffer(fs: &mut FontSystem, text: &str, size: f32) -> Buffer {
@@ -69,7 +69,7 @@ fn make_text_buffer_with_font(
glyphon::Family::Name(mono_fallback)
}
} else {
- glyphon::Family::Monospace
+ glyphon::Family::Name(clear_ui::layout::get_system_monospace_font())
}
}
"sans-serif" => {
@@ -109,6 +109,7 @@ struct AppWidget {
color: [f32; 4],
hover_color: [f32; 4],
hovering: bool,
+ radius: f32,
}
static INITIAL_PAGE_INDEX: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
@@ -118,7 +119,7 @@ struct SystemInterface {
font_system: FontSystem,
widgets: Vec<AppWidget>,
text_items: Vec<clear_ui::widget::TextItem>,
- page_buttons: Vec<ContentButton>,
+ page_buttons: Vec<(clear_ui::widget::Button, AppAction)>,
sidebar_width: f32,
header_height: f32,
@@ -157,7 +158,6 @@ struct SystemInterface {
scroll_y: f32,
max_scroll_y: f32,
opacity_dragging: bool,
- graph_opacity_dragging: bool,
audio_sink_dragging: Option<usize>,
audio_source_dragging: Option<usize>,
display_brightness_dragging: bool,
@@ -423,7 +423,8 @@ impl clear_ui::engine::Application for SystemInterface {
let mut app_state = app;
app_state.current_page = Page::ALL[initial_page_idx];
- let font_system = FontSystem::new();
+ let mut font_system = FontSystem::new();
+ font_system.db_mut().load_fonts_dir("/home/lsgalante/Dropbox/Fonts");
let mut this = Self {
app: app_state,
@@ -459,13 +460,12 @@ impl clear_ui::engine::Application for SystemInterface {
rx_update,
scale_factor: 1.0,
- width: 820,
- height: 680,
+ width: 552,
+ height: 1128,
needs_rebuild: true,
- scroll_y: 0.0,
+ scroll_y: 4000.0,
max_scroll_y: 0.0,
opacity_dragging: false,
- graph_opacity_dragging: false,
audio_sink_dragging: None,
audio_source_dragging: None,
display_brightness_dragging: false,
@@ -527,7 +527,25 @@ impl clear_ui::engine::Application for SystemInterface {
self.rebuild_layout(width, height);
}
for w in &self.widgets {
- quads.push((w.x, w.y, w.w, w.h, w.color));
+ if w.radius <= 0.1 {
+ quads.push((w.x, w.y, w.w, w.h, w.color));
+ }
+ }
+ }
+
+ fn view_rounded_quads(&mut self, quads: &mut Vec<(f32, f32, f32, f32, f32, [f32; 4])>, size: clear_ui::engine::LogicalSize, scale: f64) {
+ let (width, height) = (size.width, size.height);
+ if self.needs_rebuild || self.width != width as u32 || self.height != height as u32 || self.scale_factor != scale {
+ self.width = width as u32;
+ self.height = height as u32;
+ self.scale_factor = scale;
+ clear_ui::scale::set_scale_factor(scale as f32);
+ self.rebuild_layout(width, height);
+ }
+ for w in &self.widgets {
+ if w.radius > 0.1 {
+ quads.push((w.x, w.y, w.w, w.h, w.radius, w.color));
+ }
}
}
@@ -666,8 +684,10 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
self.app.interface.graph_snap_enabled_toggle.set_parent(None, &mut self.ui_context);
self.app.interface.graph_uniform_background_toggle.clear_children(&mut self.ui_context);
self.app.interface.graph_uniform_background_toggle.set_parent(None, &mut self.ui_context);
- self.app.interface.graph_network_opacity_slider.clear_children(&mut self.ui_context);
- self.app.interface.graph_network_opacity_slider.set_parent(None, &mut self.ui_context);
+ self.app.interface.graph_cell_opacity_spinbox.clear_children(&mut self.ui_context);
+ self.app.interface.graph_cell_opacity_spinbox.set_parent(None, &mut self.ui_context);
+ self.app.interface.graph_gap_opacity_spinbox.clear_children(&mut self.ui_context);
+ self.app.interface.graph_gap_opacity_spinbox.set_parent(None, &mut self.ui_context);
self.app.interface.graph_gap_width_spinbox.clear_children(&mut self.ui_context);
self.app.interface.graph_gap_width_spinbox.set_parent(None, &mut self.ui_context);
self.app.interface.page_margin_spinbox.clear_children(&mut self.ui_context);
@@ -700,6 +720,8 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
self.app.interface.font_selector_height_spinbox.set_parent(None, &mut self.ui_context);
self.app.interface.dropdown_height_spinbox.clear_children(&mut self.ui_context);
self.app.interface.dropdown_height_spinbox.set_parent(None, &mut self.ui_context);
+ self.app.interface.button_corner_radius_spinbox.clear_children(&mut self.ui_context);
+ self.app.interface.button_corner_radius_spinbox.set_parent(None, &mut self.ui_context);
self.app.interface.sans_box.clear_children(&mut self.ui_context); self.app.interface.sans_box.set_parent(None, &mut self.ui_context);
self.app.interface.serif_box.clear_children(&mut self.ui_context); self.app.interface.serif_box.set_parent(None, &mut self.ui_context);
@@ -775,20 +797,30 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
}
self.app.layout.side_panel_behavior_menu.clear_children(&mut self.ui_context);
self.app.layout.side_panel_behavior_menu.set_parent(None, &mut self.ui_context);
+ self.app.layout.side_panel_position_menu.clear_children(&mut self.ui_context);
+ self.app.layout.side_panel_position_menu.set_parent(None, &mut self.ui_context);
self.app.layout.side_panel_width_spinbox.clear_children(&mut self.ui_context);
self.app.layout.side_panel_width_spinbox.set_parent(None, &mut self.ui_context);
+ self.app.layout.side_panel_border_gap_spinbox.clear_children(&mut self.ui_context);
+ self.app.layout.side_panel_border_gap_spinbox.set_parent(None, &mut self.ui_context);
+ self.app.layout.side_panel_border_opacity_spinbox.clear_children(&mut self.ui_context);
+ self.app.layout.side_panel_border_opacity_spinbox.set_parent(None, &mut self.ui_context);
use clear_ui::widget::focus::link_parent_child;
match self.app.current_page {
Page::Accounts => {
+ self.page_sec_containers.resize_with(2, clear_ui::widget::Container::new);
+ for i in 0..2 {
+ link_parent_child(page_root, &mut self.page_sec_containers[i], &mut self.ui_context);
+ }
if self.app.accounts.editing_oauth_creds {
- link_parent_child(page_root, &mut self.app.accounts.oauth_client_id_box, &mut self.ui_context);
- link_parent_child(page_root, &mut self.app.accounts.oauth_client_secret_box, &mut self.ui_context);
- } else {
- link_parent_child(page_root, &mut self.app.accounts.email_box, &mut self.ui_context);
- link_parent_child(page_root, &mut self.app.accounts.password_box, &mut self.ui_context);
- link_parent_child(page_root, &mut self.app.accounts.imap_box, &mut self.ui_context);
- link_parent_child(page_root, &mut self.app.accounts.smtp_box, &mut self.ui_context);
+ link_parent_child(&mut self.page_sec_containers[1], &mut self.app.accounts.oauth_client_id_box, &mut self.ui_context);
+ link_parent_child(&mut self.page_sec_containers[1], &mut self.app.accounts.oauth_client_secret_box, &mut self.ui_context);
+ } else if self.app.accounts.adding_new {
+ link_parent_child(&mut self.page_sec_containers[1], &mut self.app.accounts.email_box, &mut self.ui_context);
+ link_parent_child(&mut self.page_sec_containers[1], &mut self.app.accounts.password_box, &mut self.ui_context);
+ link_parent_child(&mut self.page_sec_containers[1], &mut self.app.accounts.imap_box, &mut self.ui_context);
+ link_parent_child(&mut self.page_sec_containers[1], &mut self.app.accounts.smtp_box, &mut self.ui_context);
}
}
@@ -810,12 +842,20 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
link_parent_child(&mut self.page_sec_containers[2], &mut self.app.services.status_padding_spinbox, &mut self.ui_context);
}
Page::Hardware => {
- link_parent_child(page_root, &mut self.app.hardware.cpu_list_box.scroll_box, &mut self.ui_context);
- link_parent_child(page_root, &mut self.app.hardware.cpu_gov_menu, &mut self.ui_context);
- link_parent_child(page_root, &mut self.app.hardware.gpu_gov_menu, &mut self.ui_context);
+ self.page_sec_containers.resize_with(5, clear_ui::widget::Container::new);
+ for i in 0..5 {
+ link_parent_child(page_root, &mut self.page_sec_containers[i], &mut self.ui_context);
+ }
+ link_parent_child(&mut self.page_sec_containers[0], &mut self.app.hardware.cpu_list_box.scroll_box, &mut self.ui_context);
+ link_parent_child(&mut self.page_sec_containers[3], &mut self.app.hardware.cpu_gov_menu, &mut self.ui_context);
+ link_parent_child(&mut self.page_sec_containers[4], &mut self.app.hardware.gpu_gov_menu, &mut self.ui_context);
}
Page::Radios => {
- link_parent_child(page_root, &mut self.app.network.wifi_list_box.scroll_box, &mut self.ui_context);
+ self.page_sec_containers.resize_with(2, clear_ui::widget::Container::new);
+ for i in 0..2 {
+ link_parent_child(page_root, &mut self.page_sec_containers[i], &mut self.ui_context);
+ }
+ link_parent_child(&mut self.page_sec_containers[0], &mut self.app.network.wifi_list_box.scroll_box, &mut self.ui_context);
}
Page::Layout => {
self.page_sec_containers.resize_with(7, clear_ui::widget::Container::new);
@@ -849,7 +889,10 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
// Side Panel (Section 6)
link_parent_child(&mut self.page_sec_containers[6], &mut self.app.layout.side_panel_behavior_menu, &mut self.ui_context);
+ link_parent_child(&mut self.page_sec_containers[6], &mut self.app.layout.side_panel_position_menu, &mut self.ui_context);
link_parent_child(&mut self.page_sec_containers[6], &mut self.app.layout.side_panel_width_spinbox, &mut self.ui_context);
+ link_parent_child(&mut self.page_sec_containers[6], &mut self.app.layout.side_panel_border_gap_spinbox, &mut self.ui_context);
+ link_parent_child(&mut self.page_sec_containers[6], &mut self.app.layout.side_panel_border_opacity_spinbox, &mut self.ui_context);
}
Page::Interface => {
self.page_sec_containers.resize_with(5, clear_ui::widget::Container::new);
@@ -869,7 +912,8 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
link_parent_child(&mut self.page_sec_containers[0], &mut self.app.interface.graph_show_grid_toggle, &mut self.ui_context);
link_parent_child(&mut self.page_sec_containers[0], &mut self.app.interface.graph_snap_enabled_toggle, &mut self.ui_context);
link_parent_child(&mut self.page_sec_containers[0], &mut self.app.interface.graph_uniform_background_toggle, &mut self.ui_context);
- link_parent_child(&mut self.page_sec_containers[0], &mut self.app.interface.graph_network_opacity_slider, &mut self.ui_context);
+ link_parent_child(&mut self.page_sec_containers[0], &mut self.app.interface.graph_cell_opacity_spinbox, &mut self.ui_context);
+ link_parent_child(&mut self.page_sec_containers[0], &mut self.app.interface.graph_gap_opacity_spinbox, &mut self.ui_context);
link_parent_child(&mut self.page_sec_containers[0], &mut self.app.interface.graph_gap_width_spinbox, &mut self.ui_context);
// (Sections child widgets)
link_parent_child(&mut self.page_sec_containers[0], &mut self.app.interface.section_padding_spinbox, &mut self.ui_context);
@@ -925,6 +969,9 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
// (Dropdown child widgets)
link_parent_child(&mut self.page_sec_containers[2], &mut self.app.interface.dropdown_height_spinbox, &mut self.ui_context);
+ // (Button child widgets)
+ link_parent_child(&mut self.page_sec_containers[2], &mut self.app.interface.button_corner_radius_spinbox, &mut self.ui_context);
+
// (Labels child widgets)
link_parent_child(&mut self.page_sec_containers[2], &mut self.app.interface.label_margin_spinbox, &mut self.ui_context);
@@ -1070,11 +1117,12 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
clear_ui::layout::render_widget(&mut switcher_pc, &mut self.switcher, self.sidebar_width, 0.0, sw / s - self.sidebar_width, sh / s, &mut self.ui_context);
for pc_part in &[menubar_pc, switcher_pc] {
- for (c, x, y, w, h) in &pc_part.rects {
+ for (c, x, y, w, h, r) in &pc_part.rects {
widgets.push(AppWidget {
x: *x * s, y: *y * s, w: *w * s, h: *h * s,
color: *c, hover_color: *c,
hovering: false,
+ radius: *r * s,
});
}
for (t, size, x, y, tc, font_opt, bounds) in &pc_part.texts {
@@ -1117,28 +1165,35 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
});
}
- clear_ui::layout::render_popovers(&mut pc, &mut self.ui_context);
-
let mut max_y = 0.0f32;
- for (_, _, y, _, h) in &pc.rects {
+ for (_, _, y, _, h, _) in &pc.rects {
max_y = max_y.max(y + h);
}
for (_, size, _, y, _, _, _) in &pc.texts {
max_y = max_y.max(y + size);
}
- for btn in &pc.buttons {
- max_y = max_y.max(btn.y + btn.h);
+ for (btn, _) in &pc.buttons {
+ let base = btn.base().unwrap();
+ max_y = max_y.max(base.y + base.h);
}
self.max_scroll_y = (max_y - lch).max(0.0);
- self.scroll_y = self.scroll_y.min(self.max_scroll_y);
+ eprintln!("SCROLL_DEBUG: max_y={}, lch={}, max_scroll_y={}, scroll_y={}", max_y, lch, self.max_scroll_y, self.scroll_y);
+ static mut FRAME_COUNT: usize = 0;
+ unsafe {
+ FRAME_COUNT += 1;
+ if FRAME_COUNT > 5 {
+ self.scroll_y = self.scroll_y.min(self.max_scroll_y);
+ }
+ }
let scroll_offset_y = self.scroll_y;
- for (c, x, y, w, h) in &pc.rects {
+ for (c, x, y, w, h, r) in &pc.rects {
widgets.push(AppWidget {
x: *x * s, y: (*y - scroll_offset_y) * s, w: *w * s, h: *h * s,
color: *c, hover_color: *c,
hovering: false,
+ radius: *r * s,
});
}
for (t, size, x, y, tc, font_opt, bounds) in &pc.texts {
@@ -1162,56 +1217,120 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
bounds: shifted_bounds,
});
}
- for btn in &pc.buttons {
+ for (btn, action) in &pc.buttons {
+ let base = btn.base().unwrap();
+ let bg = btn.bg.unwrap_or([0.16, 0.16, 0.24, 1.0]);
+ let hover_bg = btn.hover_bg.unwrap_or([0.25, 0.30, 0.26, 1.0]);
widgets.push(AppWidget {
- x: btn.x * s, y: (btn.y - scroll_offset_y) * s, w: btn.w * s, h: btn.h * s,
- color: btn.bg, hover_color: btn.hover_bg,
+ x: base.x * s, y: (base.y - scroll_offset_y) * s, w: base.w * s, h: base.h * s,
+ color: bg, hover_color: hover_bg,
hovering: false,
+ radius: clear_ui::layout::button_corner_radius() * s,
});
- let buf = make_text_buffer(&mut self.font_system, &btn.label, btn.label_size * s);
+ let label = base.label.as_deref().unwrap_or("");
+ let label_size = 12.0;
+ let buf = make_text_buffer(&mut self.font_system, label, label_size * s);
let scale = clear_ui::scale::scale_factor();
let tw = buf.layout_runs().next().map(|r| r.line_w).unwrap_or(0.0) / scale;
- let lh = btn.label_size * s * 1.4;
+ let lh = label_size * s * 1.4;
let mut left_align = btn.left_align;
// Auto-detect if inside a ScrollBox to apply left alignment by default
- if !left_align && btn.w >= 60.0 {
+ if !left_align && base.w >= 60.0 {
if self.app.current_page == Page::Hardware {
let sb = &self.app.hardware.cpu_list_box;
let (sb_x, sb_y, sb_w, sb_h) = sb.rect();
- if btn.x >= sb_x - 1.0 && btn.x + btn.w <= sb_x + sb_w + 1.0
- && btn.y >= sb_y - 1.0 && btn.y + btn.h <= sb_y + sb_h + 1.0 {
+ if base.x >= sb_x - 1.0 && base.x + base.w <= sb_x + sb_w + 1.0
+ && base.y >= sb_y - 1.0 && base.y + base.h <= sb_y + sb_h + 1.0 {
left_align = true;
}
} else if self.app.current_page == Page::Services {
let sb = &self.app.services.list_box;
let (sb_x, sb_y, sb_w, sb_h) = sb.rect();
- if btn.x >= sb_x - 1.0 && btn.x + btn.w <= sb_x + sb_w + 1.0
- && btn.y >= sb_y - 1.0 && btn.y + btn.h <= sb_y + sb_h + 1.0 {
+ if base.x >= sb_x - 1.0 && base.x + base.w <= sb_x + sb_w + 1.0
+ && base.y >= sb_y - 1.0 && base.y + base.h <= sb_y + sb_h + 1.0 {
left_align = true;
}
}
}
let text_x = if left_align {
- btn.x * s + 8.0 * s
+ base.x * s + 8.0 * s
+ } else {
+ base.x * s + (base.w * s - tw) / 2.0
+ };
+
+ let mut overlaps = false;
+ let tx_logical = if left_align {
+ base.x + 8.0
} else {
- btn.x * s + (btn.w * s - tw) / 2.0
+ base.x + (base.w - tw) / 2.0
};
+ let ty_logical = base.y + (base.h - lh) / 2.0;
+ for &(px, py, pw, ph) in &popovers {
+ let x_overlap = tx_logical <= px + pw && (tx_logical + tw) >= px;
+ let y_overlap = ty_logical <= py + ph && (ty_logical + lh) >= py;
+ if x_overlap && y_overlap {
+ overlaps = true;
+ break;
+ }
+ }
+ let label_color = btn.label_color.unwrap_or([0.83, 0.83, 0.83, 1.0]);
+ if !overlaps {
+ text_items.push(TextItem {
+ buffer: buf,
+ x: text_x, y: (base.y - scroll_offset_y) * s + (base.h * s - lh) / 2.0,
+ color: glyphon::Color::rgb(
+ (label_color[0] * 255.0) as u8,
+ (label_color[1] * 255.0) as u8,
+ (label_color[2] * 255.0) as u8,
+ ),
+ bounds: None,
+ });
+ }
+ let mut btn_clone = btn.clone();
+ if let Some(base_mut) = btn_clone.base_mut() {
+ base_mut.x *= s;
+ base_mut.y = (base_mut.y - scroll_offset_y) * s;
+ base_mut.w *= s;
+ base_mut.h *= s;
+ }
+ page_buttons.push((btn_clone, action.clone()));
+ }
+
+ // Render popovers on top of everything (both backgrounds and texts)
+ let mut popover_pc = PageContent::new();
+ clear_ui::layout::render_popovers(&mut popover_pc, &mut self.ui_context);
+
+ for (c, x, y, w, h, r) in &popover_pc.rects {
+ widgets.push(AppWidget {
+ x: *x * s, y: (*y - scroll_offset_y) * s, w: *w * s, h: *h * s,
+ color: *c, hover_color: *c,
+ hovering: false,
+ radius: *r * s,
+ });
+ }
+ for (t, size, x, y, tc, font_opt, bounds) in &popover_pc.texts {
+ let shifted_bounds = bounds.map(|[bl, bt, br, bb]| {
+ [bl, bt - scroll_offset_y, br, bb - scroll_offset_y]
+ });
text_items.push(TextItem {
- buffer: buf,
- x: text_x, y: (btn.y - scroll_offset_y) * s + (btn.h * s - lh) / 2.0,
+ buffer: make_text_buffer_with_font(
+ &mut self.font_system,
+ t,
+ *size * s,
+ font_opt.as_deref(),
+ &self.sans_serif_family,
+ &self.serif_family,
+ &self.monospace_family,
+ ),
+ x: *x * s, y: (*y - scroll_offset_y) * s,
color: glyphon::Color::rgb(
- (btn.label_color[0] * 255.0) as u8,
- (btn.label_color[1] * 255.0) as u8,
- (btn.label_color[2] * 255.0) as u8,
+ (tc[0] * 255.0) as u8, (tc[1] * 255.0) as u8, (tc[2] * 255.0) as u8,
),
- bounds: None,
+ bounds: shifted_bounds,
});
- let mut cb = btn.clone();
- cb.x *= s; cb.y = (cb.y - scroll_offset_y) * s; cb.w *= s; cb.h *= s;
- page_buttons.push(cb);
}
// Draw global hover highlight if active
@@ -1225,6 +1344,7 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
color: qc,
hover_color: qc,
hovering: false,
+ radius: 0.0,
});
}
@@ -1241,12 +1361,14 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
x: cx * s, y: cy * s, w: cw * s, h: ch * s,
color: [0.22, 0.22, 0.28, 1.0], hover_color: [0.22, 0.22, 0.28, 1.0],
hovering: false,
+ radius: 0.0,
});
// Bg
widgets.push(AppWidget {
x: (cx + 1.0) * s, y: (cy + 1.0) * s, w: (cw - 2.0) * s, h: (ch - 2.0) * s,
color: [0.06, 0.06, 0.09, 1.0], hover_color: [0.06, 0.06, 0.09, 1.0],
hovering: false,
+ radius: 0.0,
});
// Hover highlight
@@ -1256,6 +1378,7 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
x: (cx + 2.0) * s, y: (iy + 2.0) * s, w: (cw - 4.0) * s, h: 20.0 * s,
color: [0.20, 0.40, 0.65, 0.6], hover_color: [0.20, 0.40, 0.65, 0.6],
hovering: false,
+ radius: 0.0,
});
}
@@ -1691,9 +1814,18 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
if self.app.layout.side_panel_behavior_menu.cursor_moved(lx, ly, &mut self.ui_context) {
changed = true;
}
+ if self.app.layout.side_panel_position_menu.cursor_moved(lx, ly, &mut self.ui_context) {
+ changed = true;
+ }
if self.app.layout.side_panel_width_spinbox.cursor_moved(lx, ly, &mut self.ui_context) {
changed = true;
}
+ if self.app.layout.side_panel_border_gap_spinbox.cursor_moved(lx, ly, &mut self.ui_context) {
+ changed = true;
+ }
+ if self.app.layout.side_panel_border_opacity_spinbox.cursor_moved(lx, ly, &mut self.ui_context) {
+ changed = true;
+ }
}
if self.app.current_page == Page::Interface {
for cp in &mut self.app.interface.color_selectors {
@@ -1740,7 +1872,10 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
if self.app.interface.graph_uniform_background_toggle.cursor_moved(lx, ly, &mut self.ui_context) {
changed = true;
}
- if self.app.interface.graph_network_opacity_slider.cursor_moved(lx, ly, &mut self.ui_context) {
+ if self.app.interface.graph_cell_opacity_spinbox.cursor_moved(lx, ly, &mut self.ui_context) {
+ changed = true;
+ }
+ if self.app.interface.graph_gap_opacity_spinbox.cursor_moved(lx, ly, &mut self.ui_context) {
changed = true;
}
if self.app.interface.graph_gap_width_spinbox.cursor_moved(lx, ly, &mut self.ui_context) {
@@ -1791,6 +1926,9 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
if self.app.interface.dropdown_height_spinbox.cursor_moved(lx, ly, &mut self.ui_context) {
changed = true;
}
+ if self.app.interface.button_corner_radius_spinbox.cursor_moved(lx, ly, &mut self.ui_context) {
+ changed = true;
+ }
if self.app.interface.sans_box.cursor_moved(lx, ly, &mut self.ui_context) {
changed = true;
}
@@ -2045,13 +2183,7 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
}
}
- if self.app.current_page == Page::Interface {
- if self.graph_opacity_dragging {
- if self.app.interface.graph_network_opacity_slider.drag_update(lx, ly) {
- changed = true;
- }
- }
- }
+
if self.app.current_page == Page::Services {
if self.opacity_dragging {
@@ -2109,7 +2241,7 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
if self.app.accounts.editing_oauth_creds {
if self.app.accounts.oauth_client_id_box.cursor_moved(lx, ly, &mut self.ui_context) { changed = true; }
if self.app.accounts.oauth_client_secret_box.cursor_moved(lx, ly, &mut self.ui_context) { changed = true; }
- } else {
+ } else if self.app.accounts.adding_new {
if self.app.accounts.email_box.cursor_moved(lx, ly, &mut self.ui_context) { changed = true; }
if self.app.accounts.password_box.cursor_moved(lx, ly, &mut self.ui_context) { changed = true; }
if self.app.accounts.imap_box.cursor_moved(lx, ly, &mut self.ui_context) { changed = true; }
@@ -2191,17 +2323,12 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
self.needs_rebuild = true;
}
}
- if self.app.current_page == Page::Interface {
- if self.graph_opacity_dragging {
- self.app.interface.graph_network_opacity_slider.drag_end();
- self.graph_opacity_dragging = false;
- self.needs_rebuild = true;
- }
- }
+
let (px, py) = (self.cursor_x, self.cursor_y);
- for btn in &self.page_buttons.clone() {
- if px >= btn.x && px <= btn.x + btn.w && py >= btn.y && py <= btn.y + btn.h {
- self.handle_action(&btn.action);
+ for (btn, action) in &self.page_buttons.clone() {
+ let base = btn.base().unwrap();
+ if px >= base.x && px <= base.x + base.w && py >= base.y && py <= base.y + base.h {
+ self.handle_action(action);
self.needs_rebuild = true;
return true;
}
@@ -2219,7 +2346,7 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
if accs.editing_oauth_creds {
if accs.oauth_client_id_box.hit_test(lx, ly, &self.ui_context) { clicked_any_focusable = true; }
if accs.oauth_client_secret_box.hit_test(lx, ly, &self.ui_context) { clicked_any_focusable = true; }
- } else {
+ } else if accs.adding_new {
if accs.email_box.hit_test(lx, ly, &self.ui_context) { clicked_any_focusable = true; }
if accs.password_box.hit_test(lx, ly, &self.ui_context) { clicked_any_focusable = true; }
if accs.imap_box.hit_test(lx, ly, &self.ui_context) { clicked_any_focusable = true; }
@@ -2240,7 +2367,10 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
if menu.hit_test(lx, ly, &self.ui_context) { clicked_any_focusable = true; }
}
if self.app.layout.side_panel_behavior_menu.hit_test(lx, ly, &self.ui_context) { clicked_any_focusable = true; }
+ if self.app.layout.side_panel_position_menu.hit_test(lx, ly, &self.ui_context) { clicked_any_focusable = true; }
if self.app.layout.side_panel_width_spinbox.hit_test(lx, ly, &self.ui_context) { clicked_any_focusable = true; }
+ if self.app.layout.side_panel_border_gap_spinbox.hit_test(lx, ly, &self.ui_context) { clicked_any_focusable = true; }
+ if self.app.layout.side_panel_border_opacity_spinbox.hit_test(lx, ly, &self.ui_context) { clicked_any_focusable = true; }
}
Page::Interface => {
for cp in &mut self.app.interface.color_selectors {
@@ -2259,7 +2389,8 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
if self.app.interface.graph_show_grid_toggle.hit_test(lx, ly, &self.ui_context) { clicked_any_focusable = true; }
if self.app.interface.graph_snap_enabled_toggle.hit_test(lx, ly, &self.ui_context) { clicked_any_focusable = true; }
if self.app.interface.graph_uniform_background_toggle.hit_test(lx, ly, &self.ui_context) { clicked_any_focusable = true; }
- if self.app.interface.graph_network_opacity_slider.hit_test(lx, ly, &self.ui_context) { clicked_any_focusable = true; }
+ if self.app.interface.graph_cell_opacity_spinbox.hit_test(lx, ly, &self.ui_context) { clicked_any_focusable = true; }
+ if self.app.interface.graph_gap_opacity_spinbox.hit_test(lx, ly, &self.ui_context) { clicked_any_focusable = true; }
if self.app.interface.graph_gap_width_spinbox.hit_test(lx, ly, &self.ui_context) { clicked_any_focusable = true; }
if self.app.interface.page_margin_spinbox.hit_test(lx, ly, &self.ui_context) { clicked_any_focusable = true; }
if self.app.interface.grid_min_col_width_spinbox.hit_test(lx, ly, &self.ui_context) { clicked_any_focusable = true; }
@@ -2276,6 +2407,7 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
if self.app.interface.slider_height_spinbox.hit_test(lx, ly, &self.ui_context) { clicked_any_focusable = true; }
if self.app.interface.font_selector_height_spinbox.hit_test(lx, ly, &self.ui_context) { clicked_any_focusable = true; }
if self.app.interface.dropdown_height_spinbox.hit_test(lx, ly, &self.ui_context) { clicked_any_focusable = true; }
+ if self.app.interface.button_corner_radius_spinbox.hit_test(lx, ly, &self.ui_context) { clicked_any_focusable = true; }
let tf = &mut self.app.interface;
if tf.sans_box.hit_test(lx, ly, &self.ui_context) { clicked_any_focusable = true; }
if tf.serif_box.hit_test(lx, ly, &self.ui_context) { clicked_any_focusable = true; }
@@ -2446,6 +2578,14 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
if state == clear_ui::widget::ElementState::Pressed && menu.take_change() {
actions.push(AppAction::Layout(pages::layout::LayoutMessage::SetSidePanelBehavior(menu.selected)));
}
+ let menu = &mut self.app.layout.side_panel_position_menu;
+ if state == clear_ui::widget::ElementState::Pressed && !menu.hit_test(lx, ly, &self.ui_context) { menu.unfocus(); }
+ if menu.mouse_input(button, state, lx, ly, &mut self.ui_context) {
+ self.needs_rebuild = true;
+ }
+ if state == clear_ui::widget::ElementState::Pressed && menu.take_change() {
+ actions.push(AppAction::Layout(pages::layout::LayoutMessage::SetSidePanelPosition(menu.selected)));
+ }
let sb = &mut self.app.layout.side_panel_width_spinbox;
if !sb.hit_test(lx, ly, &self.ui_context) { sb.unfocus(); }
let old = sb.value;
@@ -2454,6 +2594,22 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
pages::layout::LayoutMessage::SetSidePanelWidth(sb.value as u16)
));
}
+ let sb = &mut self.app.layout.side_panel_border_gap_spinbox;
+ if !sb.hit_test(lx, ly, &self.ui_context) { sb.unfocus(); }
+ let old = sb.value;
+ if sb.mouse_input(button, state, lx, ly, &mut self.ui_context) && sb.value != old {
+ actions.push(AppAction::Layout(
+ pages::layout::LayoutMessage::SetSidePanelBorderGap(sb.value as u16)
+ ));
+ }
+ let sb = &mut self.app.layout.side_panel_border_opacity_spinbox;
+ if !sb.hit_test(lx, ly, &self.ui_context) { sb.unfocus(); }
+ let old = sb.value;
+ if sb.mouse_input(button, state, lx, ly, &mut self.ui_context) && sb.value != old {
+ actions.push(AppAction::Layout(
+ pages::layout::LayoutMessage::SetSidePanelBorderOpacity(sb.value as u16)
+ ));
+ }
}
if state == clear_ui::widget::ElementState::Pressed && self.app.current_page == Page::Interface {
for (i, cp) in self.app.interface.color_selectors.iter_mut().enumerate() {
@@ -2582,23 +2738,17 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
if sb.mouse_input(button, state, lx, ly, &mut self.ui_context) && sb.value != old {
actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetGraphGapWidth(sb.value as u16)));
}
- let slider = &mut self.app.interface.graph_network_opacity_slider;
- if button == clear_ui::widget::MouseButton::Left {
- if state == clear_ui::widget::ElementState::Pressed {
- if slider.hit_test(lx, ly, &self.ui_context) {
- slider.drag_begin(lx, ly);
- self.graph_opacity_dragging = true;
- self.needs_rebuild = true;
- }
- } else if state == clear_ui::widget::ElementState::Released {
- if self.graph_opacity_dragging {
- self.graph_opacity_dragging = false;
- slider.drag_end();
- let val = slider.value();
- actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetGraphOpacity(val)));
- self.needs_rebuild = true;
- }
- }
+ let sb = &mut self.app.interface.graph_cell_opacity_spinbox;
+ if !sb.hit_test(lx, ly, &self.ui_context) { sb.unfocus(); }
+ let old = sb.value;
+ if sb.mouse_input(button, state, lx, ly, &mut self.ui_context) && sb.value != old {
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetGraphCellOpacity(sb.value as f32 / 100.0)));
+ }
+ let sb = &mut self.app.interface.graph_gap_opacity_spinbox;
+ if !sb.hit_test(lx, ly, &self.ui_context) { sb.unfocus(); }
+ let old = sb.value;
+ if sb.mouse_input(button, state, lx, ly, &mut self.ui_context) && sb.value != old {
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetGraphGapOpacity(sb.value as f32 / 100.0)));
}
let sb = &mut self.app.interface.page_margin_spinbox;
if !sb.hit_test(lx, ly, &self.ui_context) { sb.unfocus(); }
@@ -2666,6 +2816,12 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
if sb.mouse_input(button, state, lx, ly, &mut self.ui_context) && sb.value != old {
actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetDropdownHeight(sb.value as u16)));
}
+ let sb = &mut self.app.interface.button_corner_radius_spinbox;
+ if !sb.hit_test(lx, ly, &self.ui_context) { sb.unfocus(); }
+ let old = sb.value;
+ if sb.mouse_input(button, state, lx, ly, &mut self.ui_context) && sb.value != old {
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetButtonCornerRadius(sb.value as u16)));
+ }
}
if state == clear_ui::widget::ElementState::Pressed && self.app.current_page == Page::Input {
let sb = &mut self.app.input.rate_spinbox;
@@ -2963,7 +3119,7 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
if tb.mouse_input(button, state, lx, ly, &mut self.ui_context) {
self.needs_rebuild = true;
}
- } else {
+ } else if self.app.accounts.adding_new {
let tb = &mut self.app.accounts.email_box;
if state == clear_ui::widget::ElementState::Pressed && !tb.hit_test(lx, ly, &self.ui_context) { tb.unfocus(); }
if tb.mouse_input(button, state, lx, ly, &mut self.ui_context) {
@@ -3286,6 +3442,14 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
if net.wifi_list_box.mouse_input(button, state, lx, ly, &mut self.ui_context) {
self.needs_rebuild = true;
}
+ net.wifi_toggle.mouse_input(button, state, lx, ly, &mut self.ui_context);
+ if net.wifi_toggle.take_click() {
+ actions.push(AppAction::Radios(pages::network::NetworkMessage::ToggleWifi));
+ }
+ net.bt_toggle.mouse_input(button, state, lx, ly, &mut self.ui_context);
+ if net.bt_toggle.take_click() {
+ actions.push(AppAction::Radios(pages::network::NetworkMessage::ToggleBluetooth));
+ }
}
for a in &actions {
self.handle_action(a);
@@ -3332,9 +3496,18 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
if self.app.layout.side_panel_behavior_menu.take_change() {
actions.push(AppAction::Layout(pages::layout::LayoutMessage::SetSidePanelBehavior(self.app.layout.side_panel_behavior_menu.selected)));
}
+ if self.app.layout.side_panel_position_menu.take_change() {
+ actions.push(AppAction::Layout(pages::layout::LayoutMessage::SetSidePanelPosition(self.app.layout.side_panel_position_menu.selected)));
+ }
if self.app.layout.side_panel_width_spinbox.take_change() {
actions.push(AppAction::Layout(pages::layout::LayoutMessage::SetSidePanelWidth(self.app.layout.side_panel_width_spinbox.value as u16)));
}
+ if self.app.layout.side_panel_border_gap_spinbox.take_change() {
+ actions.push(AppAction::Layout(pages::layout::LayoutMessage::SetSidePanelBorderGap(self.app.layout.side_panel_border_gap_spinbox.value as u16)));
+ }
+ if self.app.layout.side_panel_border_opacity_spinbox.take_change() {
+ actions.push(AppAction::Layout(pages::layout::LayoutMessage::SetSidePanelBorderOpacity(self.app.layout.side_panel_border_opacity_spinbox.value as u16)));
+ }
}
Page::Interface => {
for (i, cp) in self.app.interface.color_selectors.iter_mut().enumerate() {
@@ -3397,8 +3570,11 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
if self.app.interface.graph_uniform_background_toggle.take_change() {
actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetGraphUniformBackground(self.app.interface.graph_uniform_background_toggle.toggled())));
}
- if self.app.interface.graph_network_opacity_slider.take_change() {
- actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetGraphOpacity(self.app.interface.graph_network_opacity_slider.value())));
+ if self.app.interface.graph_cell_opacity_spinbox.take_change() {
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetGraphCellOpacity(self.app.interface.graph_cell_opacity_spinbox.value as f32 / 100.0)));
+ }
+ if self.app.interface.graph_gap_opacity_spinbox.take_change() {
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetGraphGapOpacity(self.app.interface.graph_gap_opacity_spinbox.value as f32 / 100.0)));
}
if self.app.interface.page_margin_spinbox.take_change() {
actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetPageMargin(self.app.interface.page_margin_spinbox.value as u16)));
@@ -3433,6 +3609,9 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
if self.app.interface.dropdown_height_spinbox.take_change() {
actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetDropdownHeight(self.app.interface.dropdown_height_spinbox.value as u16)));
}
+ if self.app.interface.button_corner_radius_spinbox.take_change() {
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetButtonCornerRadius(self.app.interface.button_corner_radius_spinbox.value as u16)));
+ }
if self.app.interface.sans_box.take_change() {
actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetSans(self.app.interface.sans_box.text.clone())));
}
@@ -3596,7 +3775,7 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
}
}
Page::Accounts => {
- if self.app.accounts.email_box.take_change() {
+ if self.app.accounts.adding_new && self.app.accounts.email_box.take_change() {
let email_val = self.app.accounts.email_box.text.trim().to_lowercase();
if email_val.ends_with("@gmail.com") {
self.app.accounts.imap_box.text = "imap.gmail.com:993".to_string();
@@ -3642,6 +3821,15 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
}
}
}
+ Page::Radios => {
+ let net = &mut self.app.network;
+ if net.wifi_toggle.take_change() {
+ actions.push(AppAction::Radios(pages::network::NetworkMessage::ToggleWifi));
+ }
+ if net.bt_toggle.take_change() {
+ actions.push(AppAction::Radios(pages::network::NetworkMessage::ToggleBluetooth));
+ }
+ }
_ => {}
}
}
@@ -3649,7 +3837,7 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
fn handle_mouse_wheel_internal(&mut self, delta: &clear_ui::widget::MouseScrollDelta, px: f32, py: f32) -> bool {
if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open("/tmp/clear-scroll-debug.txt") {
use std::io::Write;
- let _ = writeln!(file, "handle_mouse_wheel_internal: px={}, py={}, sidebar_w={}", px, py, self.sidebar_width);
+ let _ = writeln!(file, "handle_mouse_wheel_internal: px={}, py={}, delta={:?}, sidebar_w={}", px, py, delta, self.sidebar_width);
}
let s = 1.0f32;
if px >= self.sidebar_width * s {
@@ -3754,16 +3942,28 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
if self.app.current_page == Page::Interface {
- let slider = &mut self.app.interface.graph_network_opacity_slider;
- let old_val = slider.value();
- if slider.mouse_wheel(delta, lx, ly, &mut self.ui_context) {
- let new_val = slider.value();
- if new_val != old_val {
- self.handle_action(&AppAction::Interface(pages::interface::InterfaceMessage::SetGraphOpacity(new_val)));
- self.needs_rebuild = true;
- return true;
+ let mut actions = Vec::new();
+ let sb = &mut self.app.interface.graph_cell_opacity_spinbox;
+ let old = sb.value;
+ if sb.mouse_wheel(delta, lx, ly, &mut self.ui_context) {
+ if sb.value != old {
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetGraphCellOpacity(sb.value as f32 / 100.0)));
}
}
+ let sb2 = &mut self.app.interface.graph_gap_opacity_spinbox;
+ let old2 = sb2.value;
+ if sb2.mouse_wheel(delta, lx, ly, &mut self.ui_context) {
+ if sb2.value != old2 {
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetGraphGapOpacity(sb2.value as f32 / 100.0)));
+ }
+ }
+ for a in &actions {
+ self.handle_action(a);
+ }
+ if !actions.is_empty() {
+ self.needs_rebuild = true;
+ return true;
+ }
}
if self.app.current_page == Page::Services {
@@ -3951,6 +4151,18 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
}
changed = true;
}
+ let (menu_changed, old_selected, new_selected) = {
+ let menu = &mut self.app.layout.side_panel_position_menu;
+ let old = menu.selected;
+ let changed = menu.keyboard_input(event, &mut self.ui_context);
+ (changed, old, menu.selected)
+ };
+ if menu_changed {
+ if new_selected != old_selected {
+ actions.push(AppAction::Layout(pages::layout::LayoutMessage::SetSidePanelPosition(new_selected)));
+ }
+ changed = true;
+ }
let sb = &mut self.app.layout.side_panel_width_spinbox;
let old = sb.value;
if sb.keyboard_input(event, &mut self.ui_context) {
@@ -3961,6 +4173,26 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
}
changed = true;
}
+ let sb = &mut self.app.layout.side_panel_border_gap_spinbox;
+ let old = sb.value;
+ if sb.keyboard_input(event, &mut self.ui_context) {
+ if sb.value != old {
+ actions.push(AppAction::Layout(
+ pages::layout::LayoutMessage::SetSidePanelBorderGap(sb.value as u16)
+ ));
+ }
+ changed = true;
+ }
+ let sb = &mut self.app.layout.side_panel_border_opacity_spinbox;
+ let old = sb.value;
+ if sb.keyboard_input(event, &mut self.ui_context) {
+ if sb.value != old {
+ actions.push(AppAction::Layout(
+ pages::layout::LayoutMessage::SetSidePanelBorderOpacity(sb.value as u16)
+ ));
+ }
+ changed = true;
+ }
for a in &actions {
self.handle_action(a);
}
@@ -4227,6 +4459,16 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
self.needs_rebuild = true;
return true;
}
+ let sb = &mut self.app.interface.button_corner_radius_spinbox;
+ let old = sb.value;
+ if sb.keyboard_input(event, &mut self.ui_context) {
+ let new_val = sb.value;
+ if new_val != old {
+ self.handle_action(&AppAction::Interface(pages::interface::InterfaceMessage::SetButtonCornerRadius(new_val as u16)));
+ }
+ self.needs_rebuild = true;
+ return true;
+ }
@@ -4399,7 +4641,7 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(
if tb.keyboard_input(event, &mut self.ui_context) { consumed = true; }
let tb = &mut self.app.accounts.oauth_client_secret_box;
if tb.keyboard_input(event, &mut self.ui_context) { consumed = true; }
- } else {
+ } else if self.app.accounts.adding_new {
let tb = &mut self.app.accounts.email_box;
if tb.keyboard_input(event, &mut self.ui_context) {
consumed = true;
diff --git a/src/pages/accounts.rs b/src/pages/accounts.rs
index d65b476..9bd7c60 100644
--- a/src/pages/accounts.rs
+++ b/src/pages/accounts.rs
@@ -147,6 +147,8 @@ pub async fn fetch_accounts() -> Vec<AccountInfo> {
const GOOGLE_CLIENT_ID: &str = "REDACTED.apps.googleusercontent.com";
const GOOGLE_CLIENT_SECRET: &str = "GOCSPX-REDACTED";
+const PKCE_VERIFIER: &str = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
+const PKCE_CHALLENGE: &str = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM";
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct GoogleClientConfig {
@@ -154,43 +156,55 @@ pub struct GoogleClientConfig {
pub client_secret: String,
}
-pub fn load_google_client_config() -> GoogleClientConfig {
- let p = std::path::PathBuf::from("/home/lsgalante/.config/cce/google_client.json");
- if p.exists() {
- if let Ok(content) = std::fs::read_to_string(&p) {
- if let Ok(config) = serde_json::from_str::<GoogleClientConfig>(&content) {
- return config;
- }
- }
- }
- let default_config = GoogleClientConfig {
- client_id: GOOGLE_CLIENT_ID.to_string(),
- client_secret: GOOGLE_CLIENT_SECRET.to_string(),
- };
+fn write_google_client_config(p: &std::path::Path, config: &GoogleClientConfig) -> std::io::Result<()> {
if let Some(parent) = p.parent() {
let _ = std::fs::create_dir_all(parent);
}
- if let Ok(content) = serde_json::to_string_pretty(&default_config) {
- let _ = std::fs::write(&p, content);
+ if let Ok(content) = serde_json::to_string_pretty(config) {
+ std::fs::write(p, content)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
- if let Ok(metadata) = std::fs::metadata(&p) {
+ if let Ok(metadata) = std::fs::metadata(p) {
let mut perms = metadata.permissions();
perms.set_mode(0o600);
- let _ = std::fs::set_permissions(&p, perms);
+ let _ = std::fs::set_permissions(p, perms);
+ }
+ }
+ }
+ Ok(())
+}
+
+pub fn load_google_client_config() -> GoogleClientConfig {
+ let p = std::path::PathBuf::from("/home/lsgalante/.config/cce/google_client.json");
+ let default_config = GoogleClientConfig {
+ client_id: GOOGLE_CLIENT_ID.to_string(),
+ client_secret: GOOGLE_CLIENT_SECRET.to_string(),
+ };
+ if p.exists() {
+ if let Ok(content) = std::fs::read_to_string(&p) {
+ if let Ok(config) = serde_json::from_str::<GoogleClientConfig>(&content) {
+ // If it is the old dummy client ID, or if it is the new client ID but the secret is empty, overwrite/migrate it
+ if config.client_id == "REDACTED.apps.googleusercontent.com"
+ || (config.client_id == GOOGLE_CLIENT_ID && config.client_secret.is_empty())
+ {
+ let _ = write_google_client_config(&p, &default_config);
+ return default_config;
+ }
+ return config;
}
}
}
+ let _ = write_google_client_config(&p, &default_config);
default_config
}
pub async fn run_google_login(sender: calloop::channel::Sender<AppAction>) {
let client_config = load_google_client_config();
- let listener = match tokio::net::TcpListener::bind("127.0.0.1:8080").await {
+ let listener = match tokio::net::TcpListener::bind("127.0.0.1:36137").await {
Ok(l) => l,
Err(e) => {
- let _ = sender.send(AppAction::Accounts(AccountsMessage::StatusMessage(format!("Failed to bind port 8080: {}", e))));
+ let _ = sender.send(AppAction::Accounts(AccountsMessage::StatusMessage(format!("Failed to bind port 36137: {}", e))));
return;
}
};
@@ -198,8 +212,9 @@ pub async fn run_google_login(sender: calloop::channel::Sender<AppAction>) {
let _ = sender.send(AppAction::Accounts(AccountsMessage::StatusMessage("Waiting for browser login...".to_string())));
let auth_url = format!(
- "https://accounts.google.com/o/oauth2/v2/auth?client_id={}&redirect_uri=http%3A%2F%2F127.0.0.1%3A8080&response_type=code&scope=https%3A%2F%2Fmail.google.com%2F&access_type=offline&prompt=consent",
- client_config.client_id
+ "https://accounts.google.com/o/oauth2/v2/auth?client_id={}&redirect_uri=http%3A%2F%2Flocalhost%3A36137%2Fauth%2Fcallback&response_type=code&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcloud-platform+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcclog+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fexperimentsandconfigs+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Faicode&access_type=offline&prompt=consent&code_challenge={}&code_challenge_method=S256",
+ client_config.client_id,
+ PKCE_CHALLENGE
);
let _ = std::process::Command::new("xdg-open").arg(&auth_url).spawn();
@@ -236,13 +251,16 @@ pub async fn run_google_login(sender: calloop::channel::Sender<AppAction>) {
pub async fn exchange_code_for_tokens(code: String, sender: calloop::channel::Sender<AppAction>) {
let client_config = load_google_client_config();
let client = reqwest::Client::new();
- let params = [
+ let mut params = vec![
("code", code.as_str()),
("client_id", client_config.client_id.as_str()),
- ("client_secret", client_config.client_secret.as_str()),
- ("redirect_uri", "http://127.0.0.1:8080"),
+ ("redirect_uri", "http://localhost:36137/auth/callback"),
("grant_type", "authorization_code"),
+ ("code_verifier", PKCE_VERIFIER),
];
+ if !client_config.client_secret.is_empty() {
+ params.push(("client_secret", client_config.client_secret.as_str()));
+ }
match client.post("https://oauth2.googleapis.com/token")
.form(¶ms)
@@ -278,11 +296,31 @@ pub async fn exchange_code_for_tokens(code: String, sender: calloop::channel::Se
password: String::new(),
is_oauth: true,
access_token: Some(access_token),
- refresh_token: Some(refresh_token),
+ refresh_token: Some(refresh_token.clone()),
token_expiry: Some(expiry),
- client_id: Some(client_config.client_id),
- client_secret: Some(client_config.client_secret),
+ client_id: Some(client_config.client_id.clone()),
+ client_secret: Some(client_config.client_secret.clone()),
};
+
+ // Save standard Google Application Default Credentials (ADC)
+ if !refresh_token.is_empty() {
+ if let Ok(home) = std::env::var("HOME") {
+ let adc_path = std::path::PathBuf::from(home).join(".config/gcloud/application_default_credentials.json");
+ if let Some(parent) = adc_path.parent() {
+ let _ = std::fs::create_dir_all(parent);
+ }
+ let adc_json = serde_json::json!({
+ "client_id": client_config.client_id,
+ "client_secret": client_config.client_secret,
+ "refresh_token": refresh_token,
+ "type": "authorized_user"
+ });
+ if let Ok(content) = serde_json::to_string_pretty(&adc_json) {
+ let _ = std::fs::write(adc_path, content);
+ }
+ }
+ }
+
let _ = sender.send(AppAction::Accounts(AccountsMessage::GoogleLoginSuccess(new_acc)));
return;
}
@@ -308,19 +346,24 @@ pub fn view(state: &mut AccountsState, cx: f32, cy: f32, cw: f32, ch: f32, layou
let sec_w = 320.0f32;
let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(2);
+ let lm = clear_ui::layout::label_margin();
+ let row_h = clear_ui::layout::spinbox_height();
+ let row_gap = lm * 1.0;
+ let widget_h = clear_ui::layout::spinbox_height();
+ let field_gap = lm * 1.5;
+ let btn_gap = lm * 1.0;
+
// ── Accounts Section ──
builder.add_section(&mut final_pc, "Accounts", false, |sec_accounts| {
if !state.loaded {
sec_accounts.text("Loading online accounts...", 12.0, 0.0, 12.0, TEXT_DIM);
- sec_accounts.spacing(18.0);
+ sec_accounts.spacing(lm * 1.5);
} else {
- let row_h = 28.0;
- let row_gap = 8.0;
let item_w = sec_accounts.cw - 2.0 * (sec_accounts.padding() + 12.0);
if state.accounts.is_empty() {
sec_accounts.text("No accounts configured.", 12.0, 0.0, 12.0, TEXT_DIM);
- sec_accounts.spacing(20.0);
+ sec_accounts.spacing(lm * 1.5);
} else {
for (idx, acc) in state.accounts.iter().enumerate() {
let label = if acc.is_default {
@@ -341,25 +384,38 @@ pub fn view(state: &mut AccountsState, cx: f32, cy: f32, cw: f32, ch: f32, layou
[0.90, 0.90, 0.95, 1.0],
AppAction::Accounts(AccountsMessage::SelectAccount(idx)),
);
- sec_accounts.spacing(row_h + row_gap);
+ sec_accounts.spacing(row_gap);
}
}
- sec_accounts.spacing(12.0);
+ sec_accounts.spacing(lm * 1.2);
+ let half_btn_w = (item_w - btn_gap) / 2.0;
let add_bg = if state.adding_new { [0.20, 0.40, 0.65, 0.4] } else { [0.13, 0.18, 0.14, 1.0] };
+ let btn_y = sec_accounts.ay();
sec_accounts.button(
"Add Account",
sec_accounts.ax(12.0),
- sec_accounts.ay(),
- item_w,
+ btn_y,
+ half_btn_w,
row_h,
add_bg,
[0.25, 0.30, 0.26, 1.0],
[1.0, 1.0, 1.0, 1.0],
AppAction::Accounts(AccountsMessage::AddAccountStart),
);
- sec_accounts.spacing(row_h + row_gap);
+ sec_accounts.button(
+ "Sign in with Google",
+ sec_accounts.ax(12.0) + half_btn_w + btn_gap,
+ btn_y,
+ half_btn_w,
+ row_h,
+ [0.15, 0.15, 0.25, 1.0],
+ [0.25, 0.25, 0.35, 1.0],
+ [1.0, 1.0, 1.0, 1.0],
+ AppAction::Accounts(AccountsMessage::GoogleLoginInit),
+ );
+ sec_accounts.spacing(row_gap);
let oauth_bg = if state.editing_oauth_creds { [0.20, 0.40, 0.65, 0.4] } else { [0.15, 0.15, 0.20, 1.0] };
sec_accounts.button(
@@ -373,7 +429,7 @@ pub fn view(state: &mut AccountsState, cx: f32, cy: f32, cw: f32, ch: f32, layou
[1.0, 1.0, 1.0, 1.0],
AppAction::Accounts(AccountsMessage::EditOAuthCredsStart),
);
- sec_accounts.spacing(row_h + row_gap);
+ sec_accounts.spacing(row_gap);
if let Some(selected_idx) = state.selected_idx {
if selected_idx < state.accounts.len() && !state.adding_new && !state.editing_oauth_creds {
@@ -390,7 +446,7 @@ pub fn view(state: &mut AccountsState, cx: f32, cy: f32, cw: f32, ch: f32, layou
[1.0, 1.0, 1.0, 1.0],
AppAction::Accounts(AccountsMessage::MakeDefault(selected_idx)),
);
- sec_accounts.spacing(row_h + row_gap);
+ sec_accounts.spacing(row_gap);
}
sec_accounts.button(
"Delete Account",
@@ -403,7 +459,7 @@ pub fn view(state: &mut AccountsState, cx: f32, cy: f32, cw: f32, ch: f32, layou
[1.0, 0.33, 0.33, 1.0],
AppAction::Accounts(AccountsMessage::DeleteAccount(selected_idx)),
);
- sec_accounts.spacing(row_h + row_gap);
+ sec_accounts.spacing(row_gap);
}
}
}
@@ -412,19 +468,15 @@ pub fn view(state: &mut AccountsState, cx: f32, cy: f32, cw: f32, ch: f32, layou
// ── Modify Accounts Section ──
builder.add_section(&mut final_pc, "Modify Accounts", false, |sec_modify| {
let item_w = sec_modify.cw - 2.0 * (sec_modify.padding() + 12.0);
- let row_h = 28.0;
let rx = sec_modify.left;
if state.loaded {
if state.adding_new {
sec_modify.text("Add New Account", 12.0, 0.0, 14.0, [0.35, 0.65, 0.90, 1.0]);
- sec_modify.spacing(24.0);
+ sec_modify.spacing(lm * 2.5);
sec_modify.text("Note: Gmail uses Google Login. iCloud requires App PW.", 12.0, 0.0, 11.0, TEXT_DIM);
- sec_modify.spacing(18.0);
-
- let widget_h = 26.0;
- let field_gap = 14.0;
+ sec_modify.spacing(lm * 1.8);
// Email Address textbox
state.email_box.set_row_rect(rx + 12.0, item_w);
@@ -446,11 +498,12 @@ pub fn view(state: &mut AccountsState, cx: f32, cy: f32, cw: f32, ch: f32, layou
sec_modify.widget(&mut state.smtp_box, 12.0, item_w, widget_h, ctx);
sec_modify.spacing(field_gap);
- let helper_w = (item_w - 8.0) / 2.0;
+ let helper_w = (item_w - btn_gap) / 2.0;
+ let btn_y = sec_modify.ay();
sec_modify.button(
"Login (Google)",
sec_modify.ax(12.0),
- sec_modify.ay(),
+ btn_y,
helper_w,
row_h,
[0.15, 0.15, 0.25, 1.0],
@@ -460,8 +513,8 @@ pub fn view(state: &mut AccountsState, cx: f32, cy: f32, cw: f32, ch: f32, layou
);
sec_modify.button(
"Login (iCloud)",
- sec_modify.ax(12.0) + helper_w + 8.0,
- sec_modify.ay(),
+ sec_modify.ax(12.0) + helper_w + btn_gap,
+ btn_y,
helper_w,
row_h,
[0.15, 0.15, 0.25, 1.0],
@@ -469,12 +522,13 @@ pub fn view(state: &mut AccountsState, cx: f32, cy: f32, cw: f32, ch: f32, layou
[1.0, 1.0, 1.0, 1.0],
AppAction::Accounts(AccountsMessage::ICloudLoginHelp),
);
- sec_modify.spacing(row_h + 16.0);
+ sec_modify.spacing(lm * 1.8);
+ let btn_y2 = sec_modify.ay();
sec_modify.button(
"Save Account",
sec_modify.ax(12.0),
- sec_modify.ay(),
+ btn_y2,
helper_w,
row_h,
[0.13, 0.18, 0.14, 1.0],
@@ -484,8 +538,8 @@ pub fn view(state: &mut AccountsState, cx: f32, cy: f32, cw: f32, ch: f32, layou
);
sec_modify.button(
"Cancel",
- sec_modify.ax(12.0) + helper_w + 8.0,
- sec_modify.ay(),
+ sec_modify.ax(12.0) + helper_w + btn_gap,
+ btn_y2,
helper_w,
row_h,
[0.33, 0.20, 0.20, 1.0],
@@ -493,18 +547,15 @@ pub fn view(state: &mut AccountsState, cx: f32, cy: f32, cw: f32, ch: f32, layou
[1.0, 1.0, 1.0, 1.0],
AppAction::Accounts(AccountsMessage::AddAccountCancel),
);
- sec_modify.spacing(row_h + 12.0);
+ sec_modify.spacing(lm * 1.5);
} else if state.editing_oauth_creds {
sec_modify.text("Google OAuth Credentials", 12.0, 0.0, 14.0, [0.35, 0.65, 0.90, 1.0]);
- sec_modify.spacing(24.0);
+ sec_modify.spacing(lm * 2.5);
sec_modify.text("Configures client ID & secret from your Google Cloud Console.", 12.0, 0.0, 11.0, TEXT_DIM);
- sec_modify.spacing(18.0);
+ sec_modify.spacing(lm * 1.5);
sec_modify.text("Required: Gmail API enabled & redirect URI set to http://127.0.0.1:8080", 12.0, 0.0, 11.0, TEXT_DIM);
- sec_modify.spacing(18.0);
-
- let widget_h = 26.0;
- let field_gap = 14.0;
+ sec_modify.spacing(lm * 1.8);
// Client ID textbox
state.oauth_client_id_box.set_row_rect(rx + 12.0, item_w);
@@ -516,11 +567,12 @@ pub fn view(state: &mut AccountsState, cx: f32, cy: f32, cw: f32, ch: f32, layou
sec_modify.widget(&mut state.oauth_client_secret_box, 12.0, item_w, widget_h, ctx);
sec_modify.spacing(field_gap);
- let helper_w = (item_w - 8.0) / 2.0;
+ let helper_w = (item_w - btn_gap) / 2.0;
+ let btn_y3 = sec_modify.ay();
sec_modify.button(
"Save Credentials",
sec_modify.ax(12.0),
- sec_modify.ay(),
+ btn_y3,
helper_w,
row_h,
[0.13, 0.18, 0.14, 1.0],
@@ -530,8 +582,8 @@ pub fn view(state: &mut AccountsState, cx: f32, cy: f32, cw: f32, ch: f32, layou
);
sec_modify.button(
"Cancel",
- sec_modify.ax(12.0) + helper_w + 8.0,
- sec_modify.ay(),
+ sec_modify.ax(12.0) + helper_w + btn_gap,
+ btn_y3,
helper_w,
row_h,
[0.33, 0.20, 0.20, 1.0],
@@ -539,26 +591,26 @@ pub fn view(state: &mut AccountsState, cx: f32, cy: f32, cw: f32, ch: f32, layou
[1.0, 1.0, 1.0, 1.0],
AppAction::Accounts(AccountsMessage::EditOAuthCredsCancel),
);
- sec_modify.spacing(row_h + 12.0);
+ sec_modify.spacing(lm * 1.5);
} else if let Some(selected_idx) = state.selected_idx {
if selected_idx < state.accounts.len() {
let acc = &state.accounts[selected_idx];
sec_modify.text("Account Details", 12.0, 0.0, 14.0, [0.35, 0.65, 0.90, 1.0]);
- sec_modify.spacing(28.0);
+ sec_modify.spacing(lm * 2.5);
sec_modify.text(&format!("Email Address: {}", acc.email), 12.0, 0.0, 12.0, [0.90, 0.90, 0.95, 1.0]);
- sec_modify.spacing(18.0);
+ sec_modify.spacing(lm * 1.8);
let auth_type = if acc.is_oauth { "OAuth2 (Google)" } else { "Password-based" };
sec_modify.text(&format!("Authentication: {}", auth_type), 12.0, 0.0, 12.0, [0.83, 0.83, 0.83, 1.0]);
- sec_modify.spacing(18.0);
+ sec_modify.spacing(lm * 1.8);
sec_modify.text(&format!("IMAP Server: {}", acc.imap), 12.0, 0.0, 12.0, [0.83, 0.83, 0.83, 1.0]);
- sec_modify.spacing(18.0);
+ sec_modify.spacing(lm * 1.8);
sec_modify.text(&format!("SMTP Server: {}", acc.smtp), 12.0, 0.0, 12.0, [0.83, 0.83, 0.83, 1.0]);
- sec_modify.spacing(24.0);
+ sec_modify.spacing(lm * 2.2);
if acc.is_oauth {
sec_modify.button(
@@ -572,18 +624,18 @@ pub fn view(state: &mut AccountsState, cx: f32, cy: f32, cw: f32, ch: f32, layou
[1.0, 1.0, 1.0, 1.0],
AppAction::Accounts(AccountsMessage::GoogleLoginInit),
);
- sec_modify.spacing(row_h + 12.0);
+ sec_modify.spacing(lm * 1.5);
}
}
} else {
sec_modify.text("Select an account to view details, or click Add Account.", 12.0, 0.0, 12.0, TEXT_DIM);
- sec_modify.spacing(20.0);
+ sec_modify.spacing(lm * 1.8);
}
if let Some(ref msg) = state.status_msg {
- sec_modify.spacing(12.0);
+ sec_modify.spacing(lm * 1.2);
sec_modify.text(msg, 12.0, 0.0, 12.0, [0.56, 0.83, 0.56, 1.0]);
- sec_modify.spacing(24.0);
+ sec_modify.spacing(lm * 2.0);
}
}
});
diff --git a/src/pages/audio.rs b/src/pages/audio.rs
index 475db53..218505e 100644
--- a/src/pages/audio.rs
+++ b/src/pages/audio.rs
@@ -234,50 +234,58 @@ pub fn view(state: &mut AudioState, cx: f32, cy: f32, cw: f32, ch: f32, sec_focu
if state.loaded {
for (idx, sink) in state.sinks.iter().enumerate() {
- if !sink.active {
- let label = format!("{} (inactive)", sink.name);
- let lc = if sink.muted { RED } else { TEXT_FG };
- sec.text(&label, 14.0, 0.0, 13.0, lc);
- sec.spacing(18.0);
+ let sec_title = if sink.active {
+ sink.name.clone()
} else {
- let label = if sink.muted {
- format!("{} {:.0}% (muted)", sink.name, sink.volume * 100.0)
+ format!("{} (inactive)", sink.name)
+ };
+
+ sec.add_section(&sec_title, false, |subsec| {
+ if !sink.active {
+ subsec.spacing(8.0);
+ subsec.text("Device is inactive.", 12.0, 0.0, 12.0, TEXT_DIM);
+ subsec.spacing(8.0);
} else {
- format!("{} {:.0}%", sink.name, sink.volume * 100.0)
- };
- state.sink_sliders[idx].set_label(&label);
-
- let bar_w = sec_w - 100.0;
- let bar_x = 14.0;
- let yt = sec.ay();
- state.sink_sliders[idx].set_value(sink.volume);
- let slider_x = sec.ax(bar_x);
-
- let label_h = clear_ui::widget::label_offset(&state.sink_sliders[idx]);
- let slider_h = clear_ui::layout::slider_height() + label_h;
- render_widget(sec.pc, &mut state.sink_sliders[idx], slider_x, yt, bar_w, slider_h, ctx);
-
- let row_y = sec.ay() + slider_h + 8.0;
- let sb_w = 100.0;
- let sb_h = clear_ui::layout::spinbox_height();
- let mute_w = 60.0;
- let gap = 8.0;
-
- let row_rect_x = sec.ax(8.0);
- state.sink_spinboxes[idx].value = (sink.volume * 100.0).round() as i32;
- state.sink_spinboxes[idx].set_row_rect(row_rect_x, sec_w - 16.0);
- let sb_x = sec.ax(bar_x);
- render_widget(sec.pc, &mut state.sink_spinboxes[idx], sb_x, row_y, sb_w, sb_h, ctx);
-
- let mute_label = if sink.muted { "Unmute" } else { "Mute" };
- let mute_col = if sink.muted { MUTED_BG } else { BTN_INACTIVE };
- let mute_btn_x = sb_x + sb_w + gap;
- sec.button(mute_label, mute_btn_x, row_y, mute_w, sb_h,
- mute_col, BTN_HOVER, WHITE,
- AppAction::Audio(AudioMessage::SinkMute(sink.id)));
-
- sec.content_y += slider_h + 8.0 + sb_h + 6.0;
- }
+ subsec.spacing(8.0);
+ let label = if sink.muted {
+ format!("Volume: {:.0}% (muted)", sink.volume * 100.0)
+ } else {
+ format!("Volume: {:.0}%", sink.volume * 100.0)
+ };
+ state.sink_sliders[idx].set_label(&label);
+
+ let bar_w = subsec.cw - 2.0 * (subsec.padding() + 14.0);
+ let bar_x = 14.0;
+ let yt = subsec.ay();
+ state.sink_sliders[idx].set_value(sink.volume);
+ let slider_x = subsec.ax(bar_x);
+
+ let label_h = clear_ui::widget::label_offset(&state.sink_sliders[idx]);
+ let slider_h = clear_ui::layout::slider_height() + label_h;
+ render_widget(subsec.pc, &mut state.sink_sliders[idx], slider_x, yt, bar_w, slider_h, ctx);
+
+ let row_y = subsec.ay() + slider_h + 8.0;
+ let sb_w = 100.0;
+ let sb_h = clear_ui::layout::spinbox_height();
+ let mute_w = 60.0;
+ let gap = 8.0;
+
+ let row_rect_x = subsec.ax(8.0);
+ state.sink_spinboxes[idx].value = (sink.volume * 100.0).round() as i32;
+ state.sink_spinboxes[idx].set_row_rect(row_rect_x, subsec.cw - 16.0);
+ let sb_x = subsec.ax(bar_x);
+ render_widget(subsec.pc, &mut state.sink_spinboxes[idx], sb_x, row_y, sb_w, sb_h, ctx);
+
+ let mute_label = if sink.muted { "Unmute" } else { "Mute" };
+ let mute_col = if sink.muted { MUTED_BG } else { BTN_INACTIVE };
+ let mute_btn_x = sb_x + sb_w + gap;
+ subsec.button(mute_label, mute_btn_x, row_y, mute_w, sb_h,
+ mute_col, BTN_HOVER, WHITE,
+ AppAction::Audio(AudioMessage::SinkMute(sink.id)));
+
+ subsec.content_y = row_y + sb_h - 4.0;
+ }
+ });
}
}
});
@@ -294,50 +302,58 @@ pub fn view(state: &mut AudioState, cx: f32, cy: f32, cw: f32, ch: f32, sec_focu
if state.loaded {
for (idx, src) in state.sources.iter().enumerate() {
- if !src.active {
- let label = format!("{} (inactive)", src.name);
- let lc = if src.muted { RED } else { TEXT_FG };
- sec.text(&label, 14.0, 0.0, 13.0, lc);
- sec.spacing(18.0);
+ let sec_title = if src.active {
+ src.name.clone()
} else {
- let label = if src.muted {
- format!("{} {:.0}% (muted)", src.name, src.volume * 100.0)
+ format!("{} (inactive)", src.name)
+ };
+
+ sec.add_section(&sec_title, false, |subsec| {
+ if !src.active {
+ subsec.spacing(8.0);
+ subsec.text("Device is inactive.", 12.0, 0.0, 12.0, TEXT_DIM);
+ subsec.spacing(8.0);
} else {
- format!("{} {:.0}%", src.name, src.volume * 100.0)
- };
- state.source_sliders[idx].set_label(&label);
-
- let bar_w = sec_w - 100.0;
- let bar_x = 14.0;
- let yt = sec.ay();
- state.source_sliders[idx].set_value(src.volume);
- let slider_x = sec.ax(bar_x);
-
- let label_h = clear_ui::widget::label_offset(&state.source_sliders[idx]);
- let slider_h = clear_ui::layout::slider_height() + label_h;
- render_widget(sec.pc, &mut state.source_sliders[idx], slider_x, yt, bar_w, slider_h, ctx);
-
- let row_y = sec.ay() + slider_h + 8.0;
- let sb_w = 100.0;
- let sb_h = clear_ui::layout::spinbox_height();
- let mute_w = 60.0;
- let gap = 8.0;
-
- let row_rect_x = sec.ax(8.0);
- state.source_spinboxes[idx].value = (src.volume * 100.0).round() as i32;
- state.source_spinboxes[idx].set_row_rect(row_rect_x, sec_w - 16.0);
- let sb_x = sec.ax(bar_x);
- render_widget(sec.pc, &mut state.source_spinboxes[idx], sb_x, row_y, sb_w, sb_h, ctx);
-
- let mute_label = if src.muted { "Unmute" } else { "Mute" };
- let mute_col = if src.muted { MUTED_BG } else { BTN_INACTIVE };
- let mute_btn_x = sb_x + sb_w + gap;
- sec.button(mute_label, mute_btn_x, row_y, mute_w, sb_h,
- mute_col, BTN_HOVER, WHITE,
- AppAction::Audio(AudioMessage::SourceMute(src.id)));
-
- sec.content_y += slider_h + 8.0 + sb_h + 6.0;
- }
+ subsec.spacing(8.0);
+ let label = if src.muted {
+ format!("Volume: {:.0}% (muted)", src.volume * 100.0)
+ } else {
+ format!("Volume: {:.0}%", src.volume * 100.0)
+ };
+ state.source_sliders[idx].set_label(&label);
+
+ let bar_w = subsec.cw - 2.0 * (subsec.padding() + 14.0);
+ let bar_x = 14.0;
+ let yt = subsec.ay();
+ state.source_sliders[idx].set_value(src.volume);
+ let slider_x = subsec.ax(bar_x);
+
+ let label_h = clear_ui::widget::label_offset(&state.source_sliders[idx]);
+ let slider_h = clear_ui::layout::slider_height() + label_h;
+ render_widget(subsec.pc, &mut state.source_sliders[idx], slider_x, yt, bar_w, slider_h, ctx);
+
+ let row_y = subsec.ay() + slider_h + 8.0;
+ let sb_w = 100.0;
+ let sb_h = clear_ui::layout::spinbox_height();
+ let mute_w = 60.0;
+ let gap = 8.0;
+
+ let row_rect_x = subsec.ax(8.0);
+ state.source_spinboxes[idx].value = (src.volume * 100.0).round() as i32;
+ state.source_spinboxes[idx].set_row_rect(row_rect_x, subsec.cw - 16.0);
+ let sb_x = subsec.ax(bar_x);
+ render_widget(subsec.pc, &mut state.source_spinboxes[idx], sb_x, row_y, sb_w, sb_h, ctx);
+
+ let mute_label = if src.muted { "Unmute" } else { "Mute" };
+ let mute_col = if src.muted { MUTED_BG } else { BTN_INACTIVE };
+ let mute_btn_x = sb_x + sb_w + gap;
+ subsec.button(mute_label, mute_btn_x, row_y, mute_w, sb_h,
+ mute_col, BTN_HOVER, WHITE,
+ AppAction::Audio(AudioMessage::SourceMute(src.id)));
+
+ subsec.content_y = row_y + sb_h - 4.0;
+ }
+ });
}
}
});
diff --git a/src/pages/hardware.rs b/src/pages/hardware.rs
index e4ba86b..c6be8a6 100644
--- a/src/pages/hardware.rs
+++ b/src/pages/hardware.rs
@@ -1,4 +1,4 @@
-use crate::app::{AppAction, PageContent, SectionContextExt};
+use crate::app::{AppAction, PageContent};
use clear_ui::layout::{render_widget, PageLayoutBuilder, LayoutStrategy};
use clear_ui::widget::{Label, ScrollingList, Dropdown, InfoBox};
@@ -384,7 +384,7 @@ pub fn view(state: &mut HardwareState, cx: f32, cy: f32, cw: f32, ch: f32, root_
let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(5);
// ── CPU Section ──
- builder.add_section_with_width(&mut final_pc, sec_w * 2.0 + 20.0, "CPU", root_focused, |sec| {
+ builder.add_section(&mut final_pc, "CPU", root_focused, |sec| {
let rx = sec.left;
if !state.loaded {
sec.text("Loading CPU model and utilization...", 12.0, 0.0, 12.0, TEXT_FG);
@@ -428,7 +428,7 @@ pub fn view(state: &mut HardwareState, cx: f32, cy: f32, cw: f32, ch: f32, root_
for (idx, (pid, cpu, comm)) in state.processes.iter().enumerate() {
if let Some(draw_y) = state.cpu_list_box.get_item_draw_y(idx, 4.0) {
// Standard row action button (transparent background, highlights on hover)
- sec.button(
+ sec.pc.button(
"",
list_box_x + 2.0,
draw_y,
diff --git a/src/pages/interface.rs b/src/pages/interface.rs
index af82da4..72740e6 100644
--- a/src/pages/interface.rs
+++ b/src/pages/interface.rs
@@ -3,7 +3,7 @@ use std::io::Write;
use crate::app::PageContent;
use clear_ui::layout::{PageLayoutBuilder, LayoutStrategy};
use clear_ui::widget::{
- ColorSelector, Spinbox, Element, Dropdown, TextBox, FontSelector, Toggle, Slider
+ ColorSelector, Spinbox, Element, Dropdown, TextBox, FontSelector, Toggle
};
const CONFIG_PATH: &str = "/home/lsgalante/.config/cce/config.toml";
@@ -68,6 +68,8 @@ pub struct InterfaceState {
pub font_selector_height_spinbox: Spinbox,
pub dropdown_height: u16,
pub dropdown_height_spinbox: Spinbox,
+ pub button_corner_radius: u16,
+ pub button_corner_radius_spinbox: Spinbox,
pub nested_section_label_alignment: u8,
pub label_alignment_menu: Dropdown,
pub nested_section_label_offset: i16,
@@ -116,8 +118,10 @@ pub struct InterfaceState {
pub graph_snap_enabled_toggle: Toggle,
pub graph_uniform_background: bool,
pub graph_uniform_background_toggle: Toggle,
- pub graph_network_opacity: f32,
- pub graph_network_opacity_slider: Slider,
+ pub graph_cell_opacity: f32,
+ pub graph_cell_opacity_spinbox: Spinbox,
+ pub graph_gap_opacity: f32,
+ pub graph_gap_opacity_spinbox: Spinbox,
pub graph_gap_width: u16,
pub graph_gap_width_spinbox: Spinbox,
}
@@ -189,6 +193,8 @@ impl Default for InterfaceState {
font_selector_height_spinbox: Spinbox::new(44, 10, 100, 1).with_label("Height").with_unit("px"),
dropdown_height: 44,
dropdown_height_spinbox: Spinbox::new(44, 10, 100, 1).with_label("Height").with_unit("px"),
+ button_corner_radius: 4,
+ button_corner_radius_spinbox: Spinbox::new(4, 0, 50, 1).with_label("Radius").with_unit("px"),
nested_section_label_alignment: 0,
label_alignment_menu: Dropdown::new(
vec!["Left".to_string(), "Center".to_string(), "Right".to_string()],
@@ -238,8 +244,10 @@ impl Default for InterfaceState {
graph_snap_enabled_toggle: Toggle::new().with_label("Grid Snapping"),
graph_uniform_background: false,
graph_uniform_background_toggle: Toggle::new().with_label("Uniform Background"),
- graph_network_opacity: 0.95,
- graph_network_opacity_slider: Slider::new().with_label("Network Opacity").with_value(0.95),
+ graph_cell_opacity: 0.95,
+ graph_cell_opacity_spinbox: Spinbox::new(95, 0, 100, 5).with_label("Cell Opacity").with_unit("%"),
+ graph_gap_opacity: 0.95,
+ graph_gap_opacity_spinbox: Spinbox::new(95, 0, 100, 5).with_label("Gap Opacity").with_unit("%"),
graph_gap_width: 35,
graph_gap_width_spinbox: Spinbox::new(35, 0, 100, 1).with_label("Gap Width").with_unit("px"),
menubar_opacity: 0.9,
@@ -281,6 +289,7 @@ pub enum InterfaceMessage {
SetSliderHeight(u16),
SetFontSelectorHeight(u16),
SetDropdownHeight(u16),
+ SetButtonCornerRadius(u16),
SetColorSelectorFont(String),
SetMenubarFont(String),
SetSectionLabelFont(String),
@@ -291,7 +300,8 @@ pub enum InterfaceMessage {
SetGraphShowGrid(bool),
SetGraphSnapEnabled(bool),
SetGraphUniformBackground(bool),
- SetGraphOpacity(f32),
+ SetGraphCellOpacity(f32),
+ SetGraphGapOpacity(f32),
SetGraphGapWidth(u16),
SetMenubarOpacity(f32),
PickLowColor,
@@ -385,6 +395,7 @@ pub fn read_interface_config() -> InterfaceState {
let slider_height = parse_u16_from(&content, "slider_height", 28);
let font_selector_height = parse_u16_from(&content, "font_selector_height", 44);
let dropdown_height = parse_u16_from(&content, "dropdown_height", 44);
+ let button_corner_radius = parse_u16_from(&content, "button_corner_radius", 4);
let color_selector_font = parse_string_from(&content, "color_selector_font", "monospace");
let menubar_font = parse_string_from(&content, "menubar_font", "Outfit");
let section_label_font = parse_string_from(&content, "section_label_font", "Outfit");
@@ -395,7 +406,9 @@ pub fn read_interface_config() -> InterfaceState {
let graph_show_grid = parse_bool_from(&content, "graph_show_grid", true);
let graph_snap_enabled = parse_bool_from(&content, "graph_snap_enabled", true);
let graph_uniform_background = parse_bool_from(&content, "graph_uniform_background", false);
- let graph_network_opacity = parse_f32_from(&content, "graph_network_opacity", 0.95);
+ let legacy_opacity = parse_f32_from(&content, "graph_network_opacity", 0.95);
+ let graph_cell_opacity = parse_f32_from(&content, "graph_cell_opacity", legacy_opacity);
+ let graph_gap_opacity = parse_f32_from(&content, "graph_gap_opacity", legacy_opacity);
let graph_gap_width = parse_u16_from(&content, "graph_gap_width", 35);
let opacity = parse_transparency_opacity(&content);
@@ -464,6 +477,8 @@ pub fn read_interface_config() -> InterfaceState {
font_selector_height_spinbox: Spinbox::new(font_selector_height as i32, 10, 100, 1).with_label("Height").with_unit("px"),
dropdown_height,
dropdown_height_spinbox: Spinbox::new(dropdown_height as i32, 10, 100, 1).with_label("Height").with_unit("px"),
+ button_corner_radius,
+ button_corner_radius_spinbox: Spinbox::new(button_corner_radius as i32, 0, 50, 1).with_label("Radius").with_unit("px"),
nested_section_label_alignment,
label_alignment_menu: Dropdown::new(
vec!["Left".to_string(), "Center".to_string(), "Right".to_string()],
@@ -513,8 +528,10 @@ pub fn read_interface_config() -> InterfaceState {
graph_snap_enabled_toggle: Toggle::new().with_label("Grid Snapping"),
graph_uniform_background,
graph_uniform_background_toggle: Toggle::new().with_label("Uniform Background"),
- graph_network_opacity,
- graph_network_opacity_slider: Slider::new().with_label("Network Opacity").with_value(graph_network_opacity),
+ graph_cell_opacity,
+ graph_cell_opacity_spinbox: Spinbox::new((graph_cell_opacity * 100.0).round() as i32, 0, 100, 5).with_label("Cell Opacity").with_unit("%"),
+ graph_gap_opacity,
+ graph_gap_opacity_spinbox: Spinbox::new((graph_gap_opacity * 100.0).round() as i32, 0, 100, 5).with_label("Gap Opacity").with_unit("%"),
graph_gap_width,
graph_gap_width_spinbox: Spinbox::new(graph_gap_width as i32, 0, 100, 1).with_label("Gap Width").with_unit("px"),
menubar_opacity: opacity,
@@ -834,6 +851,11 @@ fn apply_dropdown_height(height: u16) {
clear_ui::layout::set_dropdown_height(height as f32);
}
+fn apply_button_corner_radius(radius: u16) {
+ write_config_value("button_corner_radius", &radius.to_string());
+ clear_ui::layout::set_button_corner_radius(radius as f32);
+}
+
fn apply_color_selector_font(font: &str) {
write_config_value("color_selector_font", &format!("\"{}\"", font));
clear_ui::layout::set_color_selector_font(font);
@@ -1349,9 +1371,12 @@ pub fn view(state: &mut InterfaceState, cx: f32, cy: f32, cw: f32, ch: f32, sec_
subsec.spacing(8.0);
state.graph_uniform_background_toggle.set_toggled(state.graph_uniform_background);
subsec.widget_full(&mut state.graph_uniform_background_toggle, state.toggle_height as f32, ctx);
- subsec.spacing(12.0);
- state.graph_network_opacity_slider.set_value(state.graph_network_opacity);
- subsec.widget_full(&mut state.graph_network_opacity_slider, state.slider_height as f32, ctx);
+ subsec.spacing(8.0);
+ state.graph_cell_opacity_spinbox.value = (state.graph_cell_opacity * 100.0).round() as i32;
+ subsec.widget_full(&mut state.graph_cell_opacity_spinbox, 44.0, ctx);
+ subsec.spacing(8.0);
+ state.graph_gap_opacity_spinbox.value = (state.graph_gap_opacity * 100.0).round() as i32;
+ subsec.widget_full(&mut state.graph_gap_opacity_spinbox, 44.0, ctx);
subsec.spacing(8.0);
state.graph_gap_width_spinbox.value = state.graph_gap_width as i32;
subsec.widget_full(&mut state.graph_gap_width_spinbox, 44.0, ctx);
@@ -1495,6 +1520,15 @@ pub fn view(state: &mut InterfaceState, cx: f32, cy: f32, cw: f32, ch: f32, sec_
});
sec.spacing(12.0);
+ // Button Section
+ sec.add_section("Button", false, |subsec| {
+ subsec.spacing(8.0);
+ state.button_corner_radius_spinbox.value = state.button_corner_radius as i32;
+ subsec.widget_full(&mut state.button_corner_radius_spinbox, 44.0, ctx);
+ subsec.spacing(8.0);
+ });
+ sec.spacing(12.0);
+
// Labels Section
sec.add_section("Labels", false, |subsec| {
subsec.spacing(8.0);
@@ -1742,6 +1776,10 @@ pub fn update(state: &mut InterfaceState, msg: InterfaceMessage) {
state.dropdown_height = height;
apply_dropdown_height(height);
}
+ InterfaceMessage::SetButtonCornerRadius(radius) => {
+ state.button_corner_radius = radius;
+ apply_button_corner_radius(radius);
+ }
InterfaceMessage::SetColorSelectorFont(font) => {
state.color_selector_font = font.clone();
state.color_selector_font_selector.font_family = font.clone();
@@ -1798,10 +1836,16 @@ pub fn update(state: &mut InterfaceState, msg: InterfaceMessage) {
write_config_value("graph_uniform_background", &uniform.to_string());
cce_graph_reload();
}
- InterfaceMessage::SetGraphOpacity(opacity) => {
- state.graph_network_opacity = opacity;
- state.graph_network_opacity_slider.set_value(opacity);
- write_config_value("graph_network_opacity", &format!("{:.2}", opacity));
+ InterfaceMessage::SetGraphCellOpacity(opacity) => {
+ state.graph_cell_opacity = opacity;
+ state.graph_cell_opacity_spinbox.value = (opacity * 100.0).round() as i32;
+ write_config_value("graph_cell_opacity", &format!("{:.2}", opacity));
+ cce_graph_reload();
+ }
+ InterfaceMessage::SetGraphGapOpacity(opacity) => {
+ state.graph_gap_opacity = opacity;
+ state.graph_gap_opacity_spinbox.value = (opacity * 100.0).round() as i32;
+ write_config_value("graph_gap_opacity", &format!("{:.2}", opacity));
cce_graph_reload();
}
InterfaceMessage::SetGraphGapWidth(gap) => {
@@ -1815,6 +1859,7 @@ pub fn update(state: &mut InterfaceState, msg: InterfaceMessage) {
state.menubar_opacity_spinbox.value = (opacity * 100.0).round() as i32;
write_transparency_config_value("opacity", &format!("{:.2}", opacity));
send_ipc_command("reload");
+ status_interface_reload();
}
InterfaceMessage::PickLowColor | InterfaceMessage::PickHighColor | InterfaceMessage::PickDisabledColor | InterfaceMessage::PickSeparatorColor | InterfaceMessage::PickVisualGuides | InterfaceMessage::PickSliderTrackColor | InterfaceMessage::PickPageLowColor | InterfaceMessage::PickColorBordersColor | InterfaceMessage::PickNormalColor | InterfaceMessage::PickPaginatorSidebarColor | InterfaceMessage::PickPrimaryHighlightColor | InterfaceMessage::PickPaginatorTabLabelColor | InterfaceMessage::PickToggleEnabledColor | InterfaceMessage::PickToggleDisabledColor => {}
InterfaceMessage::Refreshed(new) => {
@@ -1831,7 +1876,8 @@ pub fn update(state: &mut InterfaceState, msg: InterfaceMessage) {
let was_gsg_hovered = state.graph_show_grid_toggle.hovered();
let was_gse_hovered = state.graph_snap_enabled_toggle.hovered();
let was_gub_hovered = state.graph_uniform_background_toggle.hovered();
- let was_gno_hovered = state.graph_network_opacity_slider.hovered();
+ let was_gco_hovered = state.graph_cell_opacity_spinbox.hovered();
+ let was_ggo_hovered = state.graph_gap_opacity_spinbox.hovered();
let was_ggw_hovered = state.graph_gap_width_spinbox.hovered();
let was_csh_hovered = state.color_selector_height_spinbox.hovered();
let was_tbh_hovered = state.textbox_height_spinbox.hovered();
@@ -1881,7 +1927,8 @@ pub fn update(state: &mut InterfaceState, msg: InterfaceMessage) {
state.graph_show_grid_toggle.set_hovered(was_gsg_hovered);
state.graph_snap_enabled_toggle.set_hovered(was_gse_hovered);
state.graph_uniform_background_toggle.set_hovered(was_gub_hovered);
- state.graph_network_opacity_slider.set_hovered(was_gno_hovered);
+ state.graph_cell_opacity_spinbox.set_hovered(was_gco_hovered);
+ state.graph_gap_opacity_spinbox.set_hovered(was_ggo_hovered);
state.graph_gap_width_spinbox.set_hovered(was_ggw_hovered);
state.color_selector_height_spinbox.set_hovered(was_csh_hovered);
state.textbox_height_spinbox.set_hovered(was_tbh_hovered);
@@ -3019,6 +3066,34 @@ mod tests {
// Clean up
let _ = fs::remove_file(path_str);
}
+
+ #[test]
+ fn test_read_write_button_corner_radius() {
+ let dir = std::env::temp_dir();
+ let path = dir.join("test_button_corner_radius_config.toml");
+ let path_str = path.to_str().unwrap();
+
+ // 1. Initial configuration
+ let initial_content = "[layout]\ngap = 18\n";
+ fs::write(path_str, initial_content).unwrap();
+
+ // 2. Parse when missing (should return default 4)
+ let content = fs::read_to_string(path_str).unwrap();
+ let val = parse_u16_from(&content, "button_corner_radius", 4);
+ assert_eq!(val, 4);
+
+ // 3. Write button_corner_radius config
+ assert!(write_config_value_path(path_str, "button_corner_radius", "12"));
+ let updated = fs::read_to_string(path_str).unwrap();
+ assert!(updated.contains("button_corner_radius = 12"));
+
+ // 4. Parse when present (should return written value 12)
+ let val2 = parse_u16_from(&updated, "button_corner_radius", 4);
+ assert_eq!(val2, 12);
+
+ // Clean up
+ let _ = fs::remove_file(path_str);
+ }
}
diff --git a/src/pages/layout.rs b/src/pages/layout.rs
index 8fe9ef4..7c83bfe 100644
--- a/src/pages/layout.rs
+++ b/src/pages/layout.rs
@@ -73,8 +73,13 @@ pub struct LayoutState {
pub transition_duration_spinbox: Spinbox,
pub tag_layout_menus: Vec<Dropdown>,
pub side_panel_behavior_menu: Dropdown,
+ pub side_panel_position_menu: Dropdown,
pub side_panel_width: u16,
pub side_panel_width_spinbox: Spinbox,
+ pub side_panel_border_gap: u16,
+ pub side_panel_border_gap_spinbox: Spinbox,
+ pub side_panel_border_opacity: u16,
+ pub side_panel_border_opacity_spinbox: Spinbox,
}
impl Default for LayoutState {
@@ -113,8 +118,16 @@ impl Default for LayoutState {
vec!["Above".to_string(), "Inline".to_string()],
1,
).with_label("Behavior"),
+ side_panel_position_menu: Dropdown::new(
+ vec!["Left".to_string(), "Right".to_string()],
+ 0,
+ ).with_label("Position"),
side_panel_width: 360,
side_panel_width_spinbox: Spinbox::new(360, 0, 2000, 10),
+ side_panel_border_gap: 0,
+ side_panel_border_gap_spinbox: Spinbox::new(0, 0, 500, 1),
+ side_panel_border_opacity: 100,
+ side_panel_border_opacity_spinbox: Spinbox::new(100, 0, 100, 5),
}
}
}
@@ -130,7 +143,10 @@ pub enum LayoutMessage {
SetStatusHeight(u16),
SetTagLayout(usize, usize),
SetSidePanelBehavior(usize),
+ SetSidePanelPosition(usize),
SetSidePanelWidth(u16),
+ SetSidePanelBorderGap(u16),
+ SetSidePanelBorderOpacity(u16),
Refreshed(LayoutState),
}
@@ -168,7 +184,16 @@ pub fn read_layout_config() -> LayoutState {
side_panel_behavior_idx,
).with_label("Behavior");
+ let side_panel_position = parse_string_from(&content, "side_panel_position", "left");
+ let side_panel_position_idx = if side_panel_position == "right" { 1 } else { 0 };
+ let side_panel_position_menu = Dropdown::new(
+ vec!["Left".to_string(), "Right".to_string()],
+ side_panel_position_idx,
+ ).with_label("Position");
+
let spw = parse_u16_from(&content, "side_panel_width", 360);
+ let spbg = parse_u16_from(&content, "side_panel_border_gap", 0);
+ let spbo = parse_u16_from(&content, "side_panel_border_opacity", 100);
LayoutState {
fullscreen_border_width: fs,
@@ -190,8 +215,13 @@ pub fn read_layout_config() -> LayoutState {
transition_duration_spinbox: Spinbox::new(td as i32, 0, 2000, 50),
tag_layout_menus,
side_panel_behavior_menu,
+ side_panel_position_menu,
side_panel_width: spw,
side_panel_width_spinbox: Spinbox::new(spw as i32, 0, 2000, 10),
+ side_panel_border_gap: spbg,
+ side_panel_border_gap_spinbox: Spinbox::new(spbg as i32, 0, 500, 1),
+ side_panel_border_opacity: spbo,
+ side_panel_border_opacity_spinbox: Spinbox::new(spbo as i32, 0, 100, 5),
}
}
@@ -610,7 +640,7 @@ pub fn view(state: &mut LayoutState, cx: f32, cy: f32, cw: f32, ch: f32, sec_foc
});
// 6. Default Layouts Section
- builder.add_section(&mut final_pc, "Default Layouts", sec_focused.get(5).copied().unwrap_or(false), |default_layouts_sec| {
+ builder.add_section(&mut final_pc, "Default Layouts", true, |default_layouts_sec| {
default_layouts_sec.spacing(8.0);
for i in 0..4 {
default_layouts_sec.widget_full(&mut state.tag_layout_menus[i], 44.0, ctx);
@@ -619,13 +649,21 @@ pub fn view(state: &mut LayoutState, cx: f32, cy: f32, cw: f32, ch: f32, sec_foc
});
// 7. Side Panel Section
- builder.add_section(&mut final_pc, "Side Panel", sec_focused.get(6).copied().unwrap_or(false), |side_panel_sec| {
+ builder.add_section(&mut final_pc, "Side Panel", true, |side_panel_sec| {
side_panel_sec.spacing(8.0);
side_panel_sec.widget_full(&mut state.side_panel_behavior_menu, 44.0, ctx);
side_panel_sec.spacing(8.0);
+ side_panel_sec.widget_full(&mut state.side_panel_position_menu, 44.0, ctx);
+ side_panel_sec.spacing(8.0);
state.side_panel_width_spinbox.set_label("Default Width");
side_panel_sec.widget_full(&mut state.side_panel_width_spinbox, 44.0, ctx);
side_panel_sec.spacing(8.0);
+ state.side_panel_border_gap_spinbox.set_label("Border Gap");
+ side_panel_sec.widget_full(&mut state.side_panel_border_gap_spinbox, 44.0, ctx);
+ side_panel_sec.spacing(8.0);
+ state.side_panel_border_opacity_spinbox.set_label("Border Opacity");
+ side_panel_sec.widget_full(&mut state.side_panel_border_opacity_spinbox, 44.0, ctx);
+ side_panel_sec.spacing(8.0);
});
@@ -709,6 +747,14 @@ pub fn update(state: &mut LayoutState, msg: LayoutMessage) {
send_ipc_command(&format!("layout side_panel_behavior {}", val));
}
}
+ LayoutMessage::SetSidePanelPosition(idx) => {
+ if idx < 2 {
+ state.side_panel_position_menu.selected = idx;
+ let val = if idx == 1 { "right" } else { "left" };
+ write_config_value("side_panel_position", &format!("\"{}\"", val));
+ send_ipc_command(&format!("layout side_panel_position {}", val));
+ }
+ }
LayoutMessage::SetSidePanelWidth(v) => {
let val = v.min(2000);
state.side_panel_width = val;
@@ -716,6 +762,20 @@ pub fn update(state: &mut LayoutState, msg: LayoutMessage) {
write_config_value("side_panel_width", &val.to_string());
send_ipc_command(&format!("layout side_panel_width {}", val));
}
+ LayoutMessage::SetSidePanelBorderGap(v) => {
+ let val = v.min(500);
+ state.side_panel_border_gap = val;
+ state.side_panel_border_gap_spinbox.value = val as i32;
+ write_config_value("side_panel_border_gap", &val.to_string());
+ send_ipc_command(&format!("layout side_panel_border_gap {}", val));
+ }
+ LayoutMessage::SetSidePanelBorderOpacity(v) => {
+ let val = v.min(100);
+ state.side_panel_border_opacity = val;
+ state.side_panel_border_opacity_spinbox.value = val as i32;
+ write_config_value("side_panel_border_opacity", &val.to_string());
+ send_ipc_command(&format!("layout side_panel_border_opacity {}", val));
+ }
LayoutMessage::Refreshed(new) => { *state = new; }
}
}
@@ -745,6 +805,20 @@ mod tests {
assert_eq!(width, 450);
}
+ #[test]
+ fn test_parse_side_panel_border_opacity_default() {
+ let content = "";
+ let opacity = parse_u16_from(content, "side_panel_border_opacity", 100);
+ assert_eq!(opacity, 100);
+ }
+
+ #[test]
+ fn test_parse_side_panel_border_opacity_explicit() {
+ let content = "side_panel_border_opacity = 75";
+ let opacity = parse_u16_from(content, "side_panel_border_opacity", 100);
+ assert_eq!(opacity, 75);
+ }
+
#[test]
fn test_parse_tag_layouts_single() {
let content = r#"
diff --git a/src/pages/network.rs b/src/pages/network.rs
index adf55e1..df78063 100644
--- a/src/pages/network.rs
+++ b/src/pages/network.rs
@@ -1,6 +1,6 @@
use crate::app::{AppAction, PageContent, SectionContextExt};
use clear_ui::layout::{render_widget, PageLayoutBuilder, LayoutStrategy};
-use clear_ui::widget::ScrollingList;
+use clear_ui::widget::{ScrollingList, Toggle, Element};
#[derive(Debug, Clone)]
pub struct WifiNetwork {
@@ -18,7 +18,7 @@ pub struct BluetoothDevice {
pub connected: bool,
}
-#[derive(Debug, Clone, Default)]
+#[derive(Debug, Clone)]
pub struct NetworkState {
pub loaded: bool,
pub wifi_enabled: bool,
@@ -33,6 +33,30 @@ pub struct NetworkState {
pub bt_devices: Vec<BluetoothDevice>,
pub bt_scanning: bool,
pub wifi_list_box: ScrollingList,
+ pub wifi_toggle: Toggle,
+ pub bt_toggle: Toggle,
+}
+
+impl Default for NetworkState {
+ fn default() -> Self {
+ Self {
+ loaded: false,
+ wifi_enabled: false,
+ connected_ssid: String::new(),
+ signal_strength: 0,
+ ip_address: String::new(),
+ device: String::new(),
+ available: Vec::new(),
+ bt_installed: false,
+ bt_service_active: false,
+ bt_enabled: false,
+ bt_devices: Vec::new(),
+ bt_scanning: false,
+ wifi_list_box: ScrollingList::new(26.0, 4.0),
+ wifi_toggle: Toggle::new(),
+ bt_toggle: Toggle::new(),
+ }
+ }
}
#[derive(Debug, Clone)]
@@ -55,17 +79,19 @@ pub async fn fetch_network_state() -> NetworkState {
.unwrap_or(false);
let active = tokio::process::Command::new("nmcli")
- .args(["-t", "-f", "NAME,DEVICE,SIGNAL", "con", "show", "--active"])
+ .args(["-t", "-f", "NAME,DEVICE,TYPE", "con", "show", "--active"])
.output().await.ok()
.map(|o| String::from_utf8_lossy(&o.stdout).to_string())
.unwrap_or_default();
let (connected_ssid, device) = active.lines()
- .filter(|l| !l.contains(":lo:"))
.filter_map(|l| {
- let parts: Vec<&str> = l.splitn(3, ':').collect();
- if parts.len() >= 2 && !parts[0].is_empty() {
- Some((parts[0].to_string(), parts[1].to_string()))
+ let parts: Vec<&str> = l.split(':').collect();
+ if parts.len() >= 3 && parts[parts.len() - 1] == "802-11-wireless" {
+ let ssid = parts[..parts.len() - 2].join(":");
+ let ssid_unescaped = ssid.replace("\\:", ":");
+ let device = parts[parts.len() - 2].to_string();
+ Some((ssid_unescaped, device))
} else { None }
}).next().unwrap_or_default();
@@ -85,7 +111,10 @@ pub async fn fetch_network_state() -> NetworkState {
.and_then(|o| {
String::from_utf8_lossy(&o.stdout).lines()
.find(|l| !l.is_empty())
- .map(|l| l.split('/').next().unwrap_or(l).to_string())
+ .map(|l| {
+ let val = l.split(':').nth(1).unwrap_or(l);
+ val.split('/').next().unwrap_or(val).to_string()
+ })
}).unwrap_or_default();
let available = if wifi_enabled { fetch_wifi_list().await } else { Vec::new() };
@@ -98,6 +127,8 @@ pub async fn fetch_network_state() -> NetworkState {
bt_installed, bt_service_active,
bt_enabled, bt_devices, bt_scanning: false,
wifi_list_box: ScrollingList::new(26.0, 4.0),
+ wifi_toggle: Toggle::new(),
+ bt_toggle: Toggle::new(),
}
}
@@ -251,60 +282,67 @@ pub fn view(state: &mut NetworkState, cx: f32, cy: f32, cw: f32, ch: f32, root_f
// ── WiFi ──
builder.add_section(&mut final_pc, "WiFi", root_focused, |sec| {
+ let sec_w = sec.cw;
let rx = sec.left;
+ let padding = sec.padding();
+ let row_gap = clear_ui::layout::label_margin();
+ let margin = padding.max(12.0);
if !state.loaded {
- sec.text("Loading WiFi interfaces...", 12.0, 0.0, 12.0, TEXT_DIM);
- sec.spacing(18.0);
+ sec.text("Loading WiFi interfaces...", margin, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(12.0 + row_gap);
} else {
- let yt = sec.ay();
let wifi_btn_w = if sec_w < 200.0 { 40.0 } else { 60.0 };
- let wifi_btn_x = sec_w - wifi_btn_w - 12.0;
-
- sec.button(if state.wifi_enabled { "ON" } else { "OFF" },
- sec.ax(wifi_btn_x), yt, wifi_btn_w, 28.0,
- if state.wifi_enabled { TOGGLE_ON } else { TOGGLE_OFF }, BTN_HOVER, WHITE,
- AppAction::Radios(NetworkMessage::ToggleWifi));
- sec.content_y += 34.0;
-
- if !state.connected_ssid.is_empty() {
- let ssid_max_chars = ((sec_w - 24.0) / 7.0) as usize;
- let ssid_truncated = if state.connected_ssid.len() > ssid_max_chars {
- format!("{}...", &state.connected_ssid[..ssid_max_chars.saturating_sub(3)])
- } else {
- state.connected_ssid.clone()
- };
- sec.text(&format!("Connected: {}", ssid_truncated), 14.0, 0.0, 13.0, ACCENT);
- sec.spacing(18.0);
-
- if sec_w < 220.0 {
- sec.text(&format!("Signal: {}%", state.signal_strength), 14.0, 0.0, 12.0, TEXT_DIM);
- sec.spacing(16.0);
- if !state.ip_address.is_empty() {
- sec.text(&format!("IP: {}", state.ip_address), 14.0, 0.0, 12.0, TEXT_DIM);
- sec.spacing(16.0);
+ let wifi_btn_x = margin;
+
+ state.wifi_toggle.set_toggled(state.wifi_enabled);
+ state.wifi_toggle.set_label(if state.wifi_enabled { "ON" } else { "OFF" });
+ sec.widget(&mut state.wifi_toggle, wifi_btn_x, wifi_btn_w, 28.0, ctx);
+ sec.spacing(row_gap);
+
+ if state.wifi_enabled {
+ let status_y = sec.ay();
+ let font_size_1 = 13.0;
+ let font_size_2 = 12.0;
+ let status_area_h;
+
+ if !state.connected_ssid.is_empty() {
+ let y1 = 0.0;
+ let y2 = font_size_1 + row_gap;
+ status_area_h = y2 + font_size_2 + row_gap;
+
+ let ssid_max_chars = ((sec_w - 2.0 * margin) / 7.0) as usize;
+ let ssid_truncated = if state.connected_ssid.len() > ssid_max_chars {
+ format!("{}...", &state.connected_ssid[..ssid_max_chars.saturating_sub(3)])
+ } else {
+ state.connected_ssid.clone()
+ };
+ sec.text(&format!("Connected: {}", ssid_truncated), margin, y1, font_size_1, ACCENT);
+
+ if sec_w < 220.0 {
+ sec.text(&format!("Signal: {}%", state.signal_strength), margin, y2, font_size_2, TEXT_DIM);
+ } else {
+ sec.text(&format!("Signal: {}% IP: {}", state.signal_strength, state.ip_address),
+ margin, y2, font_size_2, TEXT_DIM);
}
} else {
- sec.text(&format!("Signal: {}% IP: {}", state.signal_strength, state.ip_address),
- 14.0, 0.0, 12.0, TEXT_DIM);
- sec.spacing(16.0);
+ status_area_h = font_size_2 + row_gap;
+ sec.text("Not connected", margin, 0.0, font_size_2, TEXT_DIM);
}
- } else if state.wifi_enabled {
- sec.text("Not connected", 14.0, 0.0, 12.0, TEXT_DIM);
- sec.spacing(16.0);
+ sec.content_y = status_y + status_area_h;
}
if state.wifi_enabled && !state.available.is_empty() {
- let list_box_x = rx + 12.0;
+ let list_box_x = rx + margin;
let list_box_y = sec.ay();
- let list_box_w = sec_w - 24.0;
+ let list_box_w = sec_w - 2.0 * margin;
let list_box_h = 160.0;
render_widget(sec.pc, &mut state.wifi_list_box, list_box_x, list_box_y, list_box_w, list_box_h, ctx);
state.wifi_list_box.update_bounds(state.available.len(), list_box_y, list_box_h);
- let btn_w = list_box_w - 24.0;
+ let btn_w = list_box_w - 2.0 * margin;
let max_chars = ((btn_w / 6.5) as usize).saturating_sub(10).max(5);
for (idx, net) in state.available.iter().enumerate() {
@@ -317,65 +355,70 @@ pub fn view(state: &mut NetworkState, cx: f32, cy: f32, cw: f32, ch: f32, root_f
};
let label = format!("{} {} ({}%)", prefix, ssid_truncated, net.signal);
let active = net.in_use;
- sec.button(&label, list_box_x + 4.0, draw_y, btn_w, 26.0,
+ sec.pc.button(&label, list_box_x + margin, draw_y, btn_w, 26.0,
if active { ACT_BTN } else { NET_BTN }, BTN_HOVER,
if active { ACCENT } else { TEXT_FG },
AppAction::Radios(NetworkMessage::ConnectWifi(net.ssid.clone())));
}
}
- sec.content_y += list_box_h + 8.0;
+ sec.content_y += list_box_h + row_gap;
}
}
});
// ── Bluetooth ──
- builder.add_section_with_width(&mut final_pc, sec_w * 2.0, "Bluetooth", false, |sec| {
- let bt_sec_w = sec_w * 2.0;
- let rx = sec.left;
+ builder.add_section(&mut final_pc, "Bluetooth", false, |sec| {
+ let bt_sec_w = sec.cw;
+ let padding = sec.padding();
+ let row_gap = clear_ui::layout::label_margin();
+ let margin = padding.max(12.0);
+ let font_size = 12.0;
+ let btn_h = 28.0;
if !state.loaded {
- sec.text("Loading Bluetooth status...", 12.0, 0.0, 12.0, TEXT_DIM);
- sec.spacing(18.0);
+ sec.text("Loading Bluetooth status...", margin, 0.0, font_size, TEXT_DIM);
+ sec.spacing(font_size + row_gap);
} else if !state.bt_installed {
- sec.text("Bluetooth tools (bluez) not installed", 14.0, 0.0, 12.0, TEXT_DIM);
- sec.spacing(18.0);
+ sec.text("Bluetooth tools (bluez) not installed", margin, 0.0, font_size, TEXT_DIM);
+ sec.spacing(font_size + row_gap);
let btn_w = if bt_sec_w < 200.0 { 100.0 } else { 120.0 };
let yt = sec.ay();
- sec.button("Install Tools", rx + 12.0, yt, btn_w, 28.0,
+ sec.button("Install Tools", sec.ax(margin), yt, btn_w, btn_h,
TOGGLE_ON, BTN_HOVER, WHITE,
AppAction::Radios(NetworkMessage::InstallBtTools));
- sec.content_y += 34.0;
+ sec.content_y = yt + btn_h + row_gap;
} else if !state.bt_service_active {
- sec.text("Bluetooth service is stopped", 14.0, 0.0, 12.0, TEXT_DIM);
- sec.spacing(18.0);
+ sec.text("Bluetooth service is stopped", margin, 0.0, font_size, TEXT_DIM);
+ sec.spacing(font_size + row_gap);
let btn_w = if bt_sec_w < 200.0 { 100.0 } else { 120.0 };
let yt = sec.ay();
- sec.button("Start Service", rx + 12.0, yt, btn_w, 28.0,
+ sec.button("Start Service", sec.ax(margin), yt, btn_w, btn_h,
TOGGLE_ON, BTN_HOVER, WHITE,
AppAction::Radios(NetworkMessage::StartBtService));
- sec.content_y += 34.0;
+ sec.content_y = yt + btn_h + row_gap;
} else {
let yt = sec.ay();
let bt_btn_w = if bt_sec_w < 200.0 { 40.0 } else { 60.0 };
let scan_btn_w = if bt_sec_w < 200.0 { 40.0 } else { 52.0 };
- let bt_btn_x = bt_sec_w - bt_btn_w - scan_btn_w - 20.0;
- let scan_btn_x = bt_sec_w - scan_btn_w - 12.0;
-
- sec.button(if state.bt_enabled { "ON" } else { "OFF" },
- sec.ax(bt_btn_x), yt, bt_btn_w, 28.0,
- if state.bt_enabled { TOGGLE_ON } else { TOGGLE_OFF }, BTN_HOVER, WHITE,
- AppAction::Radios(NetworkMessage::ToggleBluetooth));
- sec.button("Scan", sec.ax(scan_btn_x), yt, scan_btn_w, 28.0,
+ let bt_btn_x = margin;
+ let scan_btn_x = margin + bt_btn_w + row_gap;
+
+ state.bt_toggle.set_toggled(state.bt_enabled);
+ state.bt_toggle.set_label(if state.bt_enabled { "ON" } else { "OFF" });
+ sec.widget(&mut state.bt_toggle, bt_btn_x, bt_btn_w, btn_h, ctx);
+ sec.button("Scan", sec.ax(scan_btn_x), yt, scan_btn_w, btn_h,
TOGGLE_OFF, BTN_HOVER, WHITE,
AppAction::Radios(NetworkMessage::BtScan));
- sec.content_y += 34.0;
-
+ sec.content_y = yt + btn_h + row_gap;
+
if state.bt_devices.is_empty() {
if state.bt_enabled {
let no_devices_msg = if bt_sec_w < 200.0 { "No paired devices" } else { "No paired devices found" };
- sec.text(no_devices_msg, 14.0, 0.0, 12.0, TEXT_DIM);
+ sec.text(no_devices_msg, margin, 0.0, font_size, TEXT_DIM);
+ sec.spacing(font_size + row_gap);
}
} else {
+ let item_h = 22.0;
for dev in &state.bt_devices {
let status = if dev.connected { ">" } else { " " };
let btn_w = if bt_sec_w < 250.0 { 42.0 } else { 70.0 };
@@ -384,10 +427,10 @@ pub fn view(state: &mut NetworkState, cx: f32, cy: f32, cw: f32, ch: f32, root_f
} else {
if bt_sec_w < 250.0 { "Conn" } else { "Connect" }
};
-
- let label_max_w = (bt_sec_w - btn_w - 20.0 - 14.0 - 8.0).max(20.0);
+
+ let label_max_w = (bt_sec_w - btn_w - 2.0 * padding - margin - row_gap).max(20.0);
let label_max_chars = ((label_max_w / 6.0) as usize).max(5);
-
+
let is_unknown = dev.name.replace('-', ":").eq_ignore_ascii_case(&dev.mac);
let label = if is_unknown {
if bt_sec_w < 350.0 {
@@ -412,17 +455,20 @@ pub fn view(state: &mut NetworkState, cx: f32, cy: f32, cw: f32, ch: f32, root_f
}
}
};
-
+
let yt = sec.ay();
- sec.text(&label, 14.0, 0.0, 12.0, if dev.connected { ACCENT } else { TEXT_FG });
- sec.button(action_label, sec.ax(bt_sec_w - btn_w - 20.0), yt - 2.0, btn_w, 22.0,
+ let btn_x = margin;
+ let text_x = margin + btn_w + row_gap;
+ let text_y_offset = (item_h - font_size) / 2.0;
+ sec.button(action_label, sec.ax(btn_x), yt, btn_w, item_h,
if dev.connected { TOGGLE_OFF } else { TOGGLE_ON }, BTN_HOVER, WHITE,
if dev.connected {
AppAction::Radios(NetworkMessage::BtDisconnect(dev.mac.clone()))
} else {
AppAction::Radios(NetworkMessage::BtConnect(dev.mac.clone()))
});
- sec.content_y += 24.0;
+ sec.text(&label, text_x, text_y_offset, font_size, if dev.connected { ACCENT } else { TEXT_FG });
+ sec.content_y = yt + item_h + row_gap;
}
}
}
@@ -435,8 +481,12 @@ pub fn update(state: &mut NetworkState, msg: NetworkMessage) {
match msg {
NetworkMessage::Refreshed(new) => {
let old_scroll = state.wifi_list_box.scroll_y();
+ let was_wifi_hovered = state.wifi_toggle.hovered();
+ let was_bt_hovered = state.bt_toggle.hovered();
*state = new;
state.wifi_list_box.set_scroll_y(old_scroll);
+ state.wifi_toggle.set_hovered(was_wifi_hovered);
+ state.bt_toggle.set_hovered(was_bt_hovered);
}
NetworkMessage::ToggleWifi => {
state.wifi_enabled = !state.wifi_enabled;
@@ -470,6 +520,21 @@ mod tests {
#[test]
fn test_view_layout_grid() {
let mut state = NetworkState::default();
+ state.loaded = true;
+ state.wifi_enabled = true;
+ let mut layout = clear_ui::layout::ColumnLayout::new(20.0);
+ let pc = view(&mut state, 10.0, 20.0, 800.0, 600.0, false, &mut layout, &mut clear_ui::context::UiContext::new());
+ assert!(!pc.rects.is_empty() || !pc.texts.is_empty() || !pc.buttons.is_empty());
+ }
+
+ #[test]
+ fn test_view_layout_connected() {
+ let mut state = NetworkState::default();
+ state.loaded = true;
+ state.wifi_enabled = true;
+ state.connected_ssid = "MyHomeWiFi".to_string();
+ state.signal_strength = 80;
+ state.ip_address = "192.168.1.50".to_string();
let mut layout = clear_ui::layout::ColumnLayout::new(20.0);
let pc = view(&mut state, 10.0, 20.0, 800.0, 600.0, false, &mut layout, &mut clear_ui::context::UiContext::new());
assert!(!pc.rects.is_empty() || !pc.texts.is_empty() || !pc.buttons.is_empty());