system settings
git clone https://git.lucas.co/cce-system-interface.git
Adapt clear-system-interface to clear-ui Element trait and remove unused panels (modified: src/app.rs, src/main.rs, src/pages/accounts.rs and 17 others)
src/app.rs | 54 +-
src/main.rs | 1513 +++++++++++++++++++++++---------------------
src/pages/accounts.rs | 98 ++-
src/pages/audio.rs | 65 +-
src/pages/backup.rs | 192 ------
src/pages/display.rs | 371 +++++++++--
src/pages/hardware.rs | 234 +++----
src/pages/input.rs | 90 +--
src/pages/interface.rs | 1314 ++++++++++++++++++++++++++++++++++++--
src/pages/layout.rs | 328 ++++------
src/pages/mod.rs | 22 +-
src/pages/network.rs | 52 +-
src/pages/notifications.rs | 470 --------------
src/pages/screensaver.rs | 330 ----------
src/pages/services.rs | 874 +++++++++++++++++++++++--
src/pages/status.rs | 363 -----------
src/pages/storage.rs | 235 ++++++-
src/pages/system_info.rs | 35 +-
src/pages/typeface.rs | 1299 -------------------------------------
src/widgets.rs | 2 +-
20 files changed, 3887 insertions(+), 4054 deletions(-)
diff --git a/src/app.rs b/src/app.rs
index 9a45c1e..08ba843 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -1,20 +1,15 @@
-use clear_ui::layout::{RenderTarget, Radial};
+use clear_ui::layout::RenderTarget;
use crate::pages::audio;
use crate::pages::display;
use crate::pages::input;
use crate::pages::layout;
use crate::pages::network;
-use crate::pages::notifications;
use crate::pages::hardware;
-use crate::pages::status;
-use crate::pages::storage;
use crate::pages::system_info;
-use crate::pages::backup;
-use crate::pages::typeface;
+use crate::pages::storage;
use crate::pages::services;
use crate::pages::interface;
-use crate::pages::screensaver;
use crate::pages::accounts;
use crate::pages::Page;
@@ -27,14 +22,9 @@ pub struct AppState {
pub input: input::InputState,
pub hardware: hardware::HardwareState,
pub system_info: system_info::SystemState,
- pub status: status::StatusState,
pub storage: storage::StorageState,
- pub notifications: notifications::NotificationsState,
- pub backup: backup::BackupState,
- pub typeface: typeface::TypefaceState,
pub services: services::ServicesState,
pub interface: interface::InterfaceState,
- pub screensaver: screensaver::ScreensaverState,
pub accounts: accounts::AccountsState,
}
@@ -49,14 +39,9 @@ impl Default for AppState {
input: input::InputState::default(),
hardware: hardware::HardwareState::default(),
system_info: system_info::SystemState::default(),
- status: status::StatusState::default(),
storage: storage::StorageState::default(),
- notifications: notifications::read_notifications_config(),
- backup: backup::BackupState::default(),
- typeface: typeface::TypefaceState::default(),
services: services::ServicesState::default(),
interface: interface::InterfaceState::default(),
- screensaver: screensaver::read_screensaver_config(),
accounts: accounts::AccountsState::default_mock(),
}
}
@@ -71,14 +56,9 @@ pub enum AppAction {
Input(input::InputMessage),
Hardware(hardware::HardwareMessage),
SystemInfo(system_info::SystemMessage),
- Status(status::StatusMessage),
Storage(storage::StorageMessage),
- Notifications(notifications::NotificationsMessage),
- Backup(backup::BackupMessage),
- Typeface(typeface::TypefaceMessage),
Services(services::ServicesMessage),
Interface(interface::InterfaceMessage),
- Screensaver(screensaver::ScreensaverMessage),
Accounts(accounts::AccountsMessage),
}
@@ -164,5 +144,35 @@ impl RenderTarget for PageContent {
}
}
+pub trait SectionContextExt {
+ 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);
+ 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);
+}
+
+impl<'a> SectionContextExt for clear_ui::layout::SectionContext<'a, PageContent> {
+ 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.pc.button(label, x, y, w, h, bg, hover_bg, label_color, action);
+ self.content_y = self.content_y.max(y + h);
+ }
+
+ 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.pc.button_left(label, x, y, w, h, bg, hover_bg, label_color, action);
+ self.content_y = self.content_y.max(y + h);
+ }
+}
+
+impl<'a> SectionContextExt for clear_ui::layout::SubsectionContext<'a, PageContent> {
+ 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.pc.button(label, x, y, w, h, bg, hover_bg, label_color, action);
+ self.content_y = self.content_y.max(y + h);
+ }
+
+ 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.pc.button_left(label, x, y, w, h, bg, hover_bg, label_color, action);
+ self.content_y = self.content_y.max(y + h);
+ }
+}
+
+
diff --git a/src/main.rs b/src/main.rs
index c60d28c..e80e393 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,80 +1,9 @@
-use std::sync::Arc;
-
-use clear_ui::color;
-use clear_ui::widget::{Spinbox, Widget, Finger, Slider, hover_animation, TextItem};
-use glyphon::{
- Attrs, Buffer, Cache, FontSystem, Metrics, Resolution, SwashCache, TextArea, TextAtlas,
- TextBounds, TextRenderer, Viewport,
-};
+use clear_ui::widget::{Finger, hover_animation, TextItem, Element};
+use glyphon::{Attrs, Buffer, FontSystem, Metrics};
use clear_system_interface::app::{AppAction, AppState, ContentButton, PageContent};
use clear_system_interface::pages::{self, Page};
-use smithay_client_toolkit::{
- compositor::{CompositorHandler, CompositorState},
- delegate_compositor, delegate_keyboard, delegate_pointer, delegate_registry,
- delegate_seat, delegate_shm, delegate_xdg_shell, delegate_xdg_window, delegate_output,
- registry::{ProvidesRegistryState, RegistryState},
- output::{OutputHandler, OutputState},
- seat::{
- keyboard::KeyboardHandler,
- pointer::{PointerHandler, ThemedPointer, ThemeSpec, CursorIcon},
- Capability, SeatHandler, SeatState,
- },
- shell::{
- xdg::{
- window::{Window as XdgWindow, WindowConfigure, WindowHandler, WindowDecorations},
- XdgShell,
- },
- WaylandSurface,
- },
- shm::{Shm, ShmHandler},
-};
-use wayland_client::{
- globals::registry_queue_init,
- protocol::{wl_keyboard, wl_output, wl_pointer, wl_seat, wl_shm, wl_surface},
- Connection, QueueHandle, Proxy,
-};
-use calloop::EventLoop;
-use calloop_wayland_source::WaylandSource;
-
-#[repr(C)]
-#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
-struct Vertex {
- position: [f32; 2],
- color: [f32; 4],
-}
-
-impl Vertex {
- const ATTRIBS: [wgpu::VertexAttribute; 2] = wgpu::vertex_attr_array![
- 0 => Float32x2,
- 1 => Float32x4,
- ];
-
- fn desc() -> wgpu::VertexBufferLayout<'static> {
- wgpu::VertexBufferLayout {
- array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
- step_mode: wgpu::VertexStepMode::Vertex,
- attributes: &Self::ATTRIBS,
- }
- }
-}
-
-fn quad_vertices(x: f32, y: f32, w: f32, h: f32, sw: f32, sh: f32, c: [f32; 4]) -> [Vertex; 6] {
- let x0 = (x / sw) * 2.0 - 1.0;
- let y0 = 1.0 - (y / sh) * 2.0;
- let x1 = ((x + w) / sw) * 2.0 - 1.0;
- let y1 = 1.0 - ((y + h) / sh) * 2.0;
- [
- Vertex { position: [x0, y0], color: c },
- Vertex { position: [x1, y0], color: c },
- Vertex { position: [x0, y1], color: c },
- Vertex { position: [x1, y0], color: c },
- Vertex { position: [x1, y1], color: c },
- Vertex { position: [x0, y1], color: c },
- ]
-}
-
fn make_text_buffer(fs: &mut FontSystem, text: &str, size: f32) -> Buffer {
let metrics = Metrics::new(size, size * 1.4);
let mut buf = Buffer::new(fs, metrics);
@@ -107,12 +36,16 @@ fn make_text_buffer_with_font(
let metrics = Metrics::new(size, size * 1.4);
let mut buf = Buffer::new(fs, metrics);
let mut attrs = Attrs::new();
- let mut resolved_storage = None;
+ let resolved_storage = font.and_then(|font_name| match font_name {
+ "monospace" if !mono_fallback.is_empty() => find_cased_family(fs, mono_fallback),
+ "sans-serif" if !sans_fallback.is_empty() => find_cased_family(fs, sans_fallback),
+ "serif" if !serif_fallback.is_empty() => find_cased_family(fs, serif_fallback),
+ _ => None,
+ });
if let Some(font_name) = font {
let family = match font_name {
"monospace" => {
if !mono_fallback.is_empty() {
- resolved_storage = find_cased_family(fs, mono_fallback);
if let Some(ref cased) = resolved_storage {
glyphon::Family::Name(cased)
} else {
@@ -124,7 +57,6 @@ fn make_text_buffer_with_font(
}
"sans-serif" => {
if !sans_fallback.is_empty() {
- resolved_storage = find_cased_family(fs, sans_fallback);
if let Some(ref cased) = resolved_storage {
glyphon::Family::Name(cased)
} else {
@@ -136,7 +68,6 @@ fn make_text_buffer_with_font(
}
"serif" => {
if !serif_fallback.is_empty() {
- resolved_storage = find_cased_family(fs, serif_fallback);
if let Some(ref cased) = resolved_storage {
glyphon::Family::Name(cased)
} else {
@@ -160,18 +91,6 @@ struct AppWidget {
color: [f32; 4],
hover_color: [f32; 4],
hovering: bool,
- kind: WidgetKind,
-}
-
-#[derive(Clone)]
-enum WidgetKind {
- ActionButton(AppAction),
- Static,
-}
-
-enum ColorSelectorAction {
- Background([u8; 3]),
- Border([u8; 3]),
}
static INITIAL_PAGE_INDEX: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
@@ -199,19 +118,16 @@ struct SystemInterface {
rx_fingers: std::sync::mpsc::Receiver<Vec<Finger>>,
rx_hardware: std::sync::mpsc::Receiver<pages::hardware::HardwareState>,
rx_system: std::sync::mpsc::Receiver<pages::system_info::SystemState>,
- rx_status: std::sync::mpsc::Receiver<pages::status::StatusState>,
+ rx_status: std::sync::mpsc::Receiver<pages::services::StatusData>,
rx_storage: std::sync::mpsc::Receiver<pages::storage::StorageState>,
- rx_notifications: std::sync::mpsc::Receiver<pages::notifications::NotificationsState>,
- rx_screensaver: std::sync::mpsc::Receiver<pages::screensaver::ScreensaverState>,
- rx_backup_state: std::sync::mpsc::Receiver<pages::backup::BackupState>,
- rx_typeface: std::sync::mpsc::Receiver<pages::typeface::TypefaceState>,
+ rx_notifications: std::sync::mpsc::Receiver<pages::services::NotificationsConfig>,
+ rx_typeface: std::sync::mpsc::Receiver<pages::interface::InterfaceState>,
rx_services: std::sync::mpsc::Receiver<Vec<pages::services::ServiceInfo>>,
rx_interface: std::sync::mpsc::Receiver<pages::interface::InterfaceState>,
rx_accounts: std::sync::mpsc::Receiver<Vec<pages::accounts::AccountInfo>>,
- tx_backup: std::sync::mpsc::Sender<pages::backup::BackupMessage>,
- rx_backup: std::sync::mpsc::Receiver<pages::backup::BackupMessage>,
- tx_color_selector: std::sync::mpsc::Sender<ColorSelectorAction>,
- rx_color_selector: std::sync::mpsc::Receiver<ColorSelectorAction>,
+ tx_backup: std::sync::mpsc::Sender<pages::storage::StorageMessage>,
+ rx_backup: std::sync::mpsc::Receiver<pages::storage::StorageMessage>,
+
scale_factor: f64,
width: u32,
@@ -317,9 +233,12 @@ impl clear_ui::engine::Application for SystemInterface {
let rx_fingers = {
let (tx, rx) = std::sync::mpsc::channel::<Vec<Finger>>();
tokio::spawn(async move {
- let socket_path = "/tmp/clear-input-coords.sock";
+ let socket_path = match std::env::var("WAYLAND_DISPLAY") {
+ Ok(display) => format!("/tmp/clear-input-coords-{}.sock", display),
+ Err(_) => "/tmp/clear-input-coords.sock".to_string(),
+ };
loop {
- if let Ok(stream) = tokio::net::UnixStream::connect(socket_path).await {
+ if let Ok(stream) = tokio::net::UnixStream::connect(&socket_path).await {
use tokio::io::AsyncBufReadExt;
let reader = tokio::io::BufReader::new(stream);
let mut lines = reader.lines();
@@ -338,32 +257,20 @@ impl clear_ui::engine::Application for SystemInterface {
};
let rx_system = spawn_bg(5, || pages::system_info::fetch_system_state());
let rx_hardware = spawn_bg(3, || pages::hardware::fetch_hardware_state());
- let rx_status = spawn_bg(10, || pages::status::fetch_status_state());
+ let rx_status = spawn_bg(10, || pages::services::fetch_status_state());
let rx_storage = spawn_bg(10, || pages::storage::fetch_storage_state());
let rx_notifications = {
- let (tx, rx) = std::sync::mpsc::channel::<pages::notifications::NotificationsState>();
- tokio::spawn(async move {
- loop {
- let val = tokio::task::spawn_blocking(|| pages::notifications::read_notifications_config()).await;
- if let Ok(val) = val { if tx.send(val).is_err() { break; } }
- tokio::time::sleep(std::time::Duration::from_secs(30)).await;
- }
- });
- rx
- };
- let rx_screensaver = {
- let (tx, rx) = std::sync::mpsc::channel::<pages::screensaver::ScreensaverState>();
+ let (tx, rx) = std::sync::mpsc::channel::<pages::services::NotificationsConfig>();
tokio::spawn(async move {
loop {
- let val = tokio::task::spawn_blocking(|| pages::screensaver::read_screensaver_config()).await;
+ let val = tokio::task::spawn_blocking(|| pages::services::read_notifications_config()).await;
if let Ok(val) = val { if tx.send(val).is_err() { break; } }
tokio::time::sleep(std::time::Duration::from_secs(30)).await;
}
});
rx
};
- let rx_backup_state = spawn_bg(30, || pages::backup::fetch_backup_state());
- let rx_typeface = spawn_bg(30, || pages::typeface::fetch_typeface_state());
+ let rx_typeface = spawn_bg(30, || pages::interface::fetch_typeface_state());
let rx_services = spawn_bg(3, || pages::services::fetch_services());
let rx_accounts = spawn_bg(3, || pages::accounts::fetch_accounts());
let rx_interface = {
@@ -378,9 +285,8 @@ impl clear_ui::engine::Application for SystemInterface {
rx
};
let (tx_backup, rx_backup) = std::sync::mpsc::channel();
- let (tx_color_selector, rx_color_selector) = std::sync::mpsc::channel();
- let (sans_family, serif_family, monospace_family, _, _, _, _, _) = pages::typeface::read_preferred_fonts();
+ let (sans_family, serif_family, monospace_family, _, _, _, _, _) = pages::interface::read_preferred_fonts();
let pages_names = Page::ALL.iter().map(|p| p.label().to_string()).collect::<Vec<_>>();
let paginator = clear_ui::widget::Paginator::new(56.0, pages_names)
@@ -416,16 +322,13 @@ impl clear_ui::engine::Application for SystemInterface {
rx_status,
rx_storage,
rx_notifications,
- rx_screensaver,
- rx_backup_state,
rx_typeface,
rx_services,
rx_interface,
rx_accounts,
tx_backup,
rx_backup,
- tx_color_selector,
- rx_color_selector,
+
scale_factor: 1.0,
width: 820,
height: 680,
@@ -510,7 +413,9 @@ impl clear_ui::engine::Application for SystemInterface {
}
}
- fn handle_mouse_input(&mut self, button: clear_ui::widget::MouseButton, state: clear_ui::widget::ElementState, _pos: clear_ui::engine::LogicalPosition, needs_rebuild: &mut bool) -> Option<Self::Message> {
+ fn handle_mouse_input(&mut self, button: clear_ui::widget::MouseButton, state: clear_ui::widget::ElementState, pos: clear_ui::engine::LogicalPosition, needs_rebuild: &mut bool) -> Option<Self::Message> {
+ self.cursor_x = pos.x;
+ self.cursor_y = pos.y;
if self.handle_mouse_input_internal(button, state) {
*needs_rebuild = true;
}
@@ -533,7 +438,7 @@ impl clear_ui::engine::Application for SystemInterface {
impl SystemInterface {
-fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f32, f32, f32, f32)>) {
+fn collect_popover_rects(w: &dyn clear_ui::widget::Element, popovers: &mut Vec<(f32, f32, f32, f32)>) {
if let Some(rect) = w.popover_rect() {
popovers.push(rect);
}
@@ -573,7 +478,7 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
widgets.push(AppWidget {
x: *x * s, y: *y * s, w: *w * s, h: *h * s,
color: *c, hover_color: *c,
- hovering: false, kind: WidgetKind::Static,
+ hovering: false,
});
}
for (t, size, x, y, tc, font_opt, bounds) in &paginator_pc.texts {
@@ -599,28 +504,12 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
let mut pc = self.render_page_content(lcx, lcy, lcw, lch);
clear_ui::layout::render_popovers(&mut pc);
- // ── Rebuild Widget Focus Hierarchy ──
+ // ── Rebuild Element Focus Hierarchy ──
self.page_root_container.clear_children();
self.page_root_container.set_parent(None);
self.page_sec_containers.clear();
// Clear all widgets' hierarchy links
- self.app.typeface.sans_box.clear_children(); self.app.typeface.sans_box.set_parent(None);
- self.app.typeface.serif_box.clear_children(); self.app.typeface.serif_box.set_parent(None);
- self.app.typeface.mono_box.clear_children(); self.app.typeface.mono_box.set_parent(None);
- self.app.typeface.borders_menu.clear_children(); self.app.typeface.borders_menu.set_parent(None);
- self.app.typeface.borders_box.clear_children(); self.app.typeface.borders_box.set_parent(None);
- self.app.typeface.status_menu.clear_children(); self.app.typeface.status_menu.set_parent(None);
- self.app.typeface.status_box.clear_children(); self.app.typeface.status_box.set_parent(None);
- self.app.typeface.fuzzel_menu.clear_children(); self.app.typeface.fuzzel_menu.set_parent(None);
- self.app.typeface.fuzzel_box.clear_children(); self.app.typeface.fuzzel_box.set_parent(None);
- self.app.typeface.terminal_menu.clear_children(); self.app.typeface.terminal_menu.set_parent(None);
- self.app.typeface.terminal_box.clear_children(); self.app.typeface.terminal_box.set_parent(None);
- self.app.typeface.paginator_menu.clear_children(); self.app.typeface.paginator_menu.set_parent(None);
- self.app.typeface.paginator_box.clear_children(); self.app.typeface.paginator_box.set_parent(None);
- self.app.typeface.search_box.clear_children(); self.app.typeface.search_box.set_parent(None);
- self.app.typeface.list_box.scroll_box.clear_children(); self.app.typeface.list_box.scroll_box.set_parent(None);
-
self.app.services.search_box.clear_children(); self.app.services.search_box.set_parent(None);
self.app.accounts.email_box.clear_children(); self.app.accounts.email_box.set_parent(None);
self.app.accounts.password_box.clear_children(); self.app.accounts.password_box.set_parent(None);
@@ -659,8 +548,26 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
self.app.interface.tab_padding_spinbox_y.clear_children();
self.app.interface.tab_padding_spinbox_y.set_parent(None);
- self.app.notifications.duration_spinbox.clear_children(); self.app.notifications.duration_spinbox.set_parent(None);
- self.app.notifications.opacity_slider.clear_children(); self.app.notifications.opacity_slider.set_parent(None);
+ self.app.interface.sans_box.clear_children(); self.app.interface.sans_box.set_parent(None);
+ self.app.interface.serif_box.clear_children(); self.app.interface.serif_box.set_parent(None);
+ self.app.interface.mono_box.clear_children(); self.app.interface.mono_box.set_parent(None);
+ self.app.interface.borders_menu.clear_children(); self.app.interface.borders_menu.set_parent(None);
+ self.app.interface.borders_box.clear_children(); self.app.interface.borders_box.set_parent(None);
+ self.app.interface.status_menu.clear_children(); self.app.interface.status_menu.set_parent(None);
+ self.app.interface.status_box.clear_children(); self.app.interface.status_box.set_parent(None);
+ self.app.interface.fuzzel_menu.clear_children(); self.app.interface.fuzzel_menu.set_parent(None);
+ self.app.interface.fuzzel_box.clear_children(); self.app.interface.fuzzel_box.set_parent(None);
+ self.app.interface.terminal_menu.clear_children(); self.app.interface.terminal_menu.set_parent(None);
+ self.app.interface.terminal_box.clear_children(); self.app.interface.terminal_box.set_parent(None);
+ self.app.interface.paginator_menu.clear_children(); self.app.interface.paginator_menu.set_parent(None);
+ self.app.interface.paginator_box.clear_children(); self.app.interface.paginator_box.set_parent(None);
+ self.app.interface.search_box.clear_children(); self.app.interface.search_box.set_parent(None);
+ self.app.interface.list_box.scroll_box.clear_children(); self.app.interface.list_box.scroll_box.set_parent(None);
+
+ self.app.services.notifications_enable_toggle.clear_children(); self.app.services.notifications_enable_toggle.set_parent(None);
+ self.app.services.notifications_bell_toggle.clear_children(); self.app.services.notifications_bell_toggle.set_parent(None);
+ self.app.services.notifications_duration_spinbox.clear_children(); self.app.services.notifications_duration_spinbox.set_parent(None);
+ self.app.services.notifications_opacity_slider.clear_children(); self.app.services.notifications_opacity_slider.set_parent(None);
self.app.input.rate_spinbox.clear_children(); self.app.input.rate_spinbox.set_parent(None);
self.app.input.delay_spinbox.clear_children(); self.app.input.delay_spinbox.set_parent(None);
@@ -688,12 +595,18 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
}
self.app.display.brightness_spinbox.clear_children(); self.app.display.brightness_spinbox.set_parent(None);
- self.app.status.padding_spinbox.clear_children(); self.app.status.padding_spinbox.set_parent(None);
+ self.app.services.status_separators_toggle.clear_children(); self.app.services.status_separators_toggle.set_parent(None);
+ self.app.services.status_underline_toggle.clear_children(); self.app.services.status_underline_toggle.set_parent(None);
+ self.app.services.status_padding_spinbox.clear_children(); self.app.services.status_padding_spinbox.set_parent(None);
for menu in &mut self.app.layout.tag_layout_menus {
menu.clear_children();
menu.set_parent(None);
}
+ self.app.layout.side_panel_behavior_menu.clear_children();
+ self.app.layout.side_panel_behavior_menu.set_parent(None);
+ self.app.layout.side_panel_width_spinbox.clear_children();
+ self.app.layout.side_panel_width_spinbox.set_parent(None);
use clear_ui::widget::focus::link_parent_child;
match self.app.current_page {
@@ -708,84 +621,128 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
link_parent_child(&mut self.page_root_container, &mut self.app.accounts.smtp_box);
}
}
- Page::Typefaces => {
- self.page_sec_containers.resize_with(3, clear_ui::widget::Container::new);
-
- link_parent_child(&mut self.page_root_container, &mut self.page_sec_containers[0]);
- link_parent_child(&mut self.page_root_container, &mut self.page_sec_containers[1]);
- link_parent_child(&mut self.page_root_container, &mut self.page_sec_containers[2]);
-
- link_parent_child(&mut self.page_sec_containers[0], &mut self.app.typeface.sans_box);
- link_parent_child(&mut self.page_sec_containers[0], &mut self.app.typeface.serif_box);
- link_parent_child(&mut self.page_sec_containers[0], &mut self.app.typeface.mono_box);
-
- link_parent_child(&mut self.page_sec_containers[1], &mut self.app.typeface.borders_menu);
- link_parent_child(&mut self.page_sec_containers[1], &mut self.app.typeface.borders_box);
- link_parent_child(&mut self.page_sec_containers[1], &mut self.app.typeface.status_menu);
- link_parent_child(&mut self.page_sec_containers[1], &mut self.app.typeface.status_box);
- link_parent_child(&mut self.page_sec_containers[1], &mut self.app.typeface.fuzzel_menu);
- link_parent_child(&mut self.page_sec_containers[1], &mut self.app.typeface.fuzzel_box);
- link_parent_child(&mut self.page_sec_containers[1], &mut self.app.typeface.terminal_menu);
- link_parent_child(&mut self.page_sec_containers[1], &mut self.app.typeface.terminal_box);
- link_parent_child(&mut self.page_sec_containers[1], &mut self.app.typeface.paginator_menu);
- link_parent_child(&mut self.page_sec_containers[1], &mut self.app.typeface.paginator_box);
-
- link_parent_child(&mut self.page_sec_containers[2], &mut self.app.typeface.search_box);
- link_parent_child(&mut self.page_sec_containers[2], &mut self.app.typeface.list_box.scroll_box);
- }
+
Page::Services => {
- link_parent_child(&mut self.page_root_container, &mut self.app.services.search_box);
- link_parent_child(&mut self.page_root_container, &mut self.app.services.list_box.scroll_box);
+ self.page_sec_containers.resize_with(3, clear_ui::widget::Container::new);
+ for i in 0..3 {
+ link_parent_child(&mut self.page_root_container, &mut self.page_sec_containers[i]);
+ }
+ link_parent_child(&mut self.page_sec_containers[0], &mut self.app.services.search_box);
+ link_parent_child(&mut self.page_sec_containers[0], &mut self.app.services.list_box.scroll_box);
+
+ link_parent_child(&mut self.page_sec_containers[1], &mut self.app.services.notifications_enable_toggle);
+ link_parent_child(&mut self.page_sec_containers[1], &mut self.app.services.notifications_bell_toggle);
+ link_parent_child(&mut self.page_sec_containers[1], &mut self.app.services.notifications_duration_spinbox);
+ link_parent_child(&mut self.page_sec_containers[1], &mut self.app.services.notifications_opacity_slider);
+
+ link_parent_child(&mut self.page_sec_containers[2], &mut self.app.services.status_separators_toggle);
+ link_parent_child(&mut self.page_sec_containers[2], &mut self.app.services.status_underline_toggle);
+ link_parent_child(&mut self.page_sec_containers[2], &mut self.app.services.status_padding_spinbox);
}
Page::Hardware => {
link_parent_child(&mut self.page_root_container, &mut self.app.hardware.cpu_list_box.scroll_box);
+ link_parent_child(&mut self.page_root_container, &mut self.app.hardware.cpu_gov_menu);
+ link_parent_child(&mut self.page_root_container, &mut self.app.hardware.gpu_gov_menu);
}
Page::Radios => {
link_parent_child(&mut self.page_root_container, &mut self.app.network.wifi_list_box.scroll_box);
}
Page::Layout => {
- self.page_sec_containers.resize_with(5, clear_ui::widget::Container::new);
+ self.page_sec_containers.resize_with(7, clear_ui::widget::Container::new);
- for i in 0..5 {
+ for i in 0..7 {
link_parent_child(&mut self.page_root_container, &mut self.page_sec_containers[i]);
}
- for sb in &mut self.app.layout.spinboxes {
- link_parent_child(&mut self.page_sec_containers[0], sb);
- }
+ // Fullscreen (Section 0)
+ link_parent_child(&mut self.page_sec_containers[0], &mut self.app.layout.spinboxes[0]);
+ // Cascade (Section 1)
+ link_parent_child(&mut self.page_sec_containers[1], &mut self.app.layout.spinboxes[1]);
link_parent_child(&mut self.page_sec_containers[1], &mut self.app.layout.cascade_offset_spinbox);
link_parent_child(&mut self.page_sec_containers[1], &mut self.app.layout.edge_gap_spinbox);
link_parent_child(&mut self.page_sec_containers[1], &mut self.app.layout.top_gap_spinbox);
- link_parent_child(&mut self.page_sec_containers[1], &mut self.app.layout.grid_gap_spinbox);
- link_parent_child(&mut self.page_sec_containers[2], &mut self.app.layout.transition_duration_spinbox);
+ // Grid (Section 2)
+ link_parent_child(&mut self.page_sec_containers[2], &mut self.app.layout.spinboxes[2]);
+ link_parent_child(&mut self.page_sec_containers[2], &mut self.app.layout.grid_gap_spinbox);
- link_parent_child(&mut self.page_sec_containers[3], &mut self.app.layout.status_height_spinbox);
-
+ // Floating (Section 3)
+ link_parent_child(&mut self.page_sec_containers[3], &mut self.app.layout.spinboxes[3]);
+
+ // Movement (Section 4)
+ link_parent_child(&mut self.page_sec_containers[4], &mut self.app.layout.transition_duration_spinbox);
+
+ // Default Layouts (Section 5)
for menu in &mut self.app.layout.tag_layout_menus {
- link_parent_child(&mut self.page_sec_containers[4], menu);
+ link_parent_child(&mut self.page_sec_containers[5], menu);
}
+
+ // Side Panel (Section 6)
+ link_parent_child(&mut self.page_sec_containers[6], &mut self.app.layout.side_panel_behavior_menu);
+ link_parent_child(&mut self.page_sec_containers[6], &mut self.app.layout.side_panel_width_spinbox);
}
Page::Interface => {
- for cs in &mut self.app.interface.color_selectors {
- link_parent_child(&mut self.page_root_container, cs);
+ self.page_sec_containers.resize_with(10, clear_ui::widget::Container::new);
+
+ for i in 0..10 {
+ link_parent_child(&mut self.page_root_container, &mut self.page_sec_containers[i]);
}
- link_parent_child(&mut self.page_root_container, &mut self.app.interface.tab_margin_spinbox_x);
- link_parent_child(&mut self.page_root_container, &mut self.app.interface.tab_margin_spinbox_y);
- link_parent_child(&mut self.page_root_container, &mut self.app.interface.tab_padding_spinbox_x);
- link_parent_child(&mut self.page_root_container, &mut self.app.interface.tab_padding_spinbox_y);
- }
- Page::Notifications => {
- link_parent_child(&mut self.page_root_container, &mut self.app.notifications.duration_spinbox);
- link_parent_child(&mut self.page_root_container, &mut self.app.notifications.opacity_slider);
- }
- Page::Screensaver => {
- link_parent_child(&mut self.page_root_container, &mut self.app.screensaver.enable_toggle);
- link_parent_child(&mut self.page_root_container, &mut self.app.screensaver.lock_screen_toggle);
- link_parent_child(&mut self.page_root_container, &mut self.app.screensaver.timeout_spinbox);
- link_parent_child(&mut self.page_root_container, &mut self.app.screensaver.style_menu);
+
+ // Section 0: Pages
+ link_parent_child(&mut self.page_sec_containers[0], &mut self.app.interface.color_selectors[0]);
+
+ // Section 1: Layout
+ link_parent_child(&mut self.page_sec_containers[1], &mut self.app.interface.color_selectors[7]);
+ link_parent_child(&mut self.page_sec_containers[1], &mut self.app.interface.color_selectors[1]);
+ link_parent_child(&mut self.page_sec_containers[1], &mut self.app.interface.color_selectors[2]);
+
+ // Section 2: Status
+ link_parent_child(&mut self.page_sec_containers[2], &mut self.app.interface.color_selectors[8]);
+ link_parent_child(&mut self.page_sec_containers[2], &mut self.app.interface.color_selectors[3]);
+ link_parent_child(&mut self.page_sec_containers[2], &mut self.app.interface.color_selectors[4]);
+
+ // Section 3: Controls
+ link_parent_child(&mut self.page_sec_containers[3], &mut self.app.interface.color_selectors[5]);
+ link_parent_child(&mut self.page_sec_containers[3], &mut self.app.interface.color_selectors[6]);
+
+ // Section 4: Primary Highlight
+ link_parent_child(&mut self.page_sec_containers[4], &mut self.app.interface.color_selectors[10]);
+
+ // Section 5: Paginator
+ link_parent_child(&mut self.page_sec_containers[5], &mut self.app.interface.color_selectors[9]);
+ link_parent_child(&mut self.page_sec_containers[5], &mut self.app.interface.color_selectors[11]);
+ link_parent_child(&mut self.page_sec_containers[5], &mut self.app.interface.tab_margin_spinbox_x);
+ link_parent_child(&mut self.page_sec_containers[5], &mut self.app.interface.tab_margin_spinbox_y);
+ link_parent_child(&mut self.page_sec_containers[5], &mut self.app.interface.tab_padding_spinbox_x);
+ link_parent_child(&mut self.page_sec_containers[5], &mut self.app.interface.tab_padding_spinbox_y);
+
+ // Section 6: Toggles
+ link_parent_child(&mut self.page_sec_containers[6], &mut self.app.interface.color_selectors[12]);
+ link_parent_child(&mut self.page_sec_containers[6], &mut self.app.interface.color_selectors[13]);
+
+ // Section 7: System Typefaces
+ link_parent_child(&mut self.page_sec_containers[7], &mut self.app.interface.sans_box);
+ link_parent_child(&mut self.page_sec_containers[7], &mut self.app.interface.serif_box);
+ link_parent_child(&mut self.page_sec_containers[7], &mut self.app.interface.mono_box);
+
+ // Section 8: Program Typefaces
+ link_parent_child(&mut self.page_sec_containers[8], &mut self.app.interface.borders_menu);
+ link_parent_child(&mut self.page_sec_containers[8], &mut self.app.interface.borders_box);
+ link_parent_child(&mut self.page_sec_containers[8], &mut self.app.interface.status_menu);
+ link_parent_child(&mut self.page_sec_containers[8], &mut self.app.interface.status_box);
+ link_parent_child(&mut self.page_sec_containers[8], &mut self.app.interface.fuzzel_menu);
+ link_parent_child(&mut self.page_sec_containers[8], &mut self.app.interface.fuzzel_box);
+ link_parent_child(&mut self.page_sec_containers[8], &mut self.app.interface.terminal_menu);
+ link_parent_child(&mut self.page_sec_containers[8], &mut self.app.interface.terminal_box);
+ link_parent_child(&mut self.page_sec_containers[8], &mut self.app.interface.paginator_menu);
+ link_parent_child(&mut self.page_sec_containers[8], &mut self.app.interface.paginator_box);
+
+ // Section 9: Typefaces (List & Preview)
+ link_parent_child(&mut self.page_sec_containers[9], &mut self.app.interface.search_box);
+ link_parent_child(&mut self.page_sec_containers[9], &mut self.app.interface.list_box.scroll_box);
}
+
Page::Input => {
self.page_sec_containers.resize_with(5, clear_ui::widget::Container::new);
link_parent_child(&mut self.page_root_container, &mut self.page_sec_containers[0]);
@@ -828,9 +785,10 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
}
Page::Display => {
link_parent_child(&mut self.page_root_container, &mut self.app.display.brightness_spinbox);
- }
- Page::Status => {
- link_parent_child(&mut self.page_root_container, &mut self.app.status.padding_spinbox);
+ link_parent_child(&mut self.page_root_container, &mut self.app.display.screensaver_enable_toggle);
+ link_parent_child(&mut self.page_root_container, &mut self.app.display.screensaver_lock_screen_toggle);
+ link_parent_child(&mut self.page_root_container, &mut self.app.display.screensaver_timeout_spinbox);
+ link_parent_child(&mut self.page_root_container, &mut self.app.display.screensaver_style_menu);
}
_ => {}
}
@@ -871,7 +829,7 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
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, kind: WidgetKind::Static,
+ hovering: false,
});
}
for (t, size, x, y, tc, font_opt, bounds) in &pc.texts {
@@ -897,7 +855,6 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
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,
hovering: false,
- kind: WidgetKind::ActionButton(btn.action.clone()),
});
let buf = make_text_buffer(&mut self.font_system, &btn.label, btn.label_size * s);
let tw = buf.layout_runs().next().map(|r| r.line_w).unwrap_or(0.0);
@@ -906,8 +863,8 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
// Auto-detect if inside a ScrollBox to apply left alignment by default
if !left_align && btn.w >= 60.0 {
- if self.app.current_page == Page::Typefaces {
- let sb = &self.app.typeface.list_box;
+ if self.app.current_page == Page::Interface {
+ let sb = &self.app.interface.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 {
@@ -962,7 +919,6 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
color: qc,
hover_color: qc,
hovering: false,
- kind: WidgetKind::Static,
});
}
@@ -977,13 +933,13 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
widgets.push(AppWidget {
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, kind: WidgetKind::Static,
+ hovering: false,
});
// 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, kind: WidgetKind::Static,
+ hovering: false,
});
// Hover highlight
@@ -992,7 +948,7 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
widgets.push(AppWidget {
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, kind: WidgetKind::Static,
+ hovering: false,
});
}
@@ -1023,7 +979,7 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
fn render_page_content(&mut self, cx: f32, cy: f32, cw: f32, ch: f32) -> PageContent {
use pages::*;
- use clear_ui::layout::{LayoutStrategy, GridLayout};
+ use clear_ui::layout::GridLayout;
let mut layout = GridLayout::new(320.0, 20.0);
let root_focused = clear_ui::widget::focus::is_focused(&self.page_root_container);
let sec_focused: Vec<bool> = self.page_sec_containers.iter()
@@ -1038,14 +994,9 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
Page::Hardware => hardware::view(&mut self.app.hardware, cx, cy, cw, ch, root_focused, &mut layout),
Page::Input => input::view(&mut self.app.input, cx, cy, cw, ch, &sec_focused, &mut layout),
Page::System => system_info::view(&self.app.system_info, cx, cy, cw, ch, &mut layout),
- Page::Status => status::view(&mut self.app.status, cx, cy, cw, ch, &mut layout),
Page::Storage => storage::view(&self.app.storage, cx, cy, cw, ch, &mut layout),
- Page::Notifications => notifications::view(&mut self.app.notifications, cx, cy, cw, ch, &mut layout),
- Page::Backup => backup::view(&self.app.backup, cx, cy, cw, ch, &mut layout),
- Page::Screensaver => screensaver::view(&mut self.app.screensaver, cx, cy, cw, ch, &mut layout),
- Page::Typefaces => typeface::view(&mut self.app.typeface, cx, cy, cw, ch, &sec_focused, &mut layout),
- Page::Services => services::view(&mut self.app.services, cx, cy, cw, ch, root_focused, &mut layout),
- Page::Interface => interface::view(&mut self.app.interface, cx, cy, cw, ch, &mut layout),
+ Page::Services => services::view(&mut self.app.services, cx, cy, cw, ch, &sec_focused, &mut layout),
+ Page::Interface => interface::view(&mut self.app.interface, cx, cy, cw, ch, &sec_focused, &mut layout),
}
}
@@ -1168,7 +1119,7 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
self.needs_rebuild = true;
}
while let Ok(s) = self.rx_status.try_recv() {
- status::update(&mut self.app.status, status::StatusMessage::Refreshed(s));
+ services::update(&mut self.app.services, services::ServicesMessage::StatusRefreshed(s));
self.needs_rebuild = true;
}
while let Ok(s) = self.rx_storage.try_recv() {
@@ -1176,22 +1127,14 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
self.needs_rebuild = true;
}
while let Ok(s) = self.rx_notifications.try_recv() {
- notifications::update(&mut self.app.notifications, notifications::NotificationsMessage::Refreshed(s));
- self.needs_rebuild = true;
- }
- while let Ok(s) = self.rx_screensaver.try_recv() {
- screensaver::update(&mut self.app.screensaver, pages::screensaver::ScreensaverMessage::Refreshed(s));
- self.needs_rebuild = true;
- }
- while let Ok(s) = self.rx_backup_state.try_recv() {
- pages::backup::update(&mut self.app.backup, pages::backup::BackupMessage::Refreshed(s));
+ services::update(&mut self.app.services, services::ServicesMessage::NotificationsRefreshed(s));
self.needs_rebuild = true;
}
while let Ok(s) = self.rx_typeface.try_recv() {
self.sans_serif_family = s.sans_serif.clone();
self.serif_family = s.serif.clone();
self.monospace_family = s.monospace.clone();
- typeface::update(&mut self.app.typeface, typeface::TypefaceMessage::Refreshed(s));
+ interface::update(&mut self.app.interface, interface::InterfaceMessage::TypefaceRefreshed(s));
self.needs_rebuild = true;
}
while let Ok(s) = self.rx_services.try_recv() {
@@ -1207,18 +1150,7 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
self.needs_rebuild = true;
}
while let Ok(m) = self.rx_backup.try_recv() {
- self.handle_action(&AppAction::Backup(m));
- self.needs_rebuild = true;
- }
- while let Ok(action) = self.rx_color_selector.try_recv() {
- match action {
- ColorSelectorAction::Background(rgb) => {
- interface::update(&mut self.app.interface, pages::interface::InterfaceMessage::SetLowColor(rgb));
- }
- ColorSelectorAction::Border(rgb) => {
- interface::update(&mut self.app.interface, pages::interface::InterfaceMessage::SetHighColor(rgb));
- }
- }
+ self.handle_action(&AppAction::Storage(m));
self.needs_rebuild = true;
}
}
@@ -1233,29 +1165,25 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
AppAction::Input(m) => input::update(&mut self.app.input, m.clone()),
AppAction::SystemInfo(m) => system_info::update(&mut self.app.system_info, m.clone()),
AppAction::Hardware(m) => hardware::update(&mut self.app.hardware, m.clone()),
- AppAction::Status(m) => status::update(&mut self.app.status, m.clone()),
- AppAction::Storage(m) => storage::update(&mut self.app.storage, m.clone()),
- AppAction::Notifications(m) => notifications::update(&mut self.app.notifications, m.clone()),
- AppAction::Typeface(m) => {
- typeface::update(&mut self.app.typeface, m.clone());
- self.sans_serif_family = self.app.typeface.sans_serif.clone();
- self.serif_family = self.app.typeface.serif.clone();
- self.monospace_family = self.app.typeface.monospace.clone();
- }
- AppAction::Services(m) => services::update(&mut self.app.services, m.clone()),
- AppAction::Interface(m) => interface::update(&mut self.app.interface, m.clone()),
- AppAction::Screensaver(m) => screensaver::update(&mut self.app.screensaver, m.clone()),
- AppAction::Backup(m) => match m {
- pages::backup::BackupMessage::StartBackup => {
- pages::backup::update(&mut self.app.backup, pages::backup::BackupMessage::StartBackup);
+ AppAction::Storage(m) => match m {
+ pages::storage::StorageMessage::StartBackup => {
+ pages::storage::update(&mut self.app.storage, pages::storage::StorageMessage::StartBackup);
let tx = self.tx_backup.clone();
tokio::spawn(async move {
- let res = pages::backup::run_backup().await;
- let _ = tx.send(pages::backup::BackupMessage::BackupFinished(res));
+ let res = pages::storage::run_backup().await;
+ let _ = tx.send(pages::storage::StorageMessage::BackupFinished(res));
});
}
- _ => pages::backup::update(&mut self.app.backup, m.clone()),
+ _ => pages::storage::update(&mut self.app.storage, m.clone()),
},
+
+ AppAction::Services(m) => services::update(&mut self.app.services, m.clone()),
+ AppAction::Interface(m) => {
+ interface::update(&mut self.app.interface, m.clone());
+ self.sans_serif_family = self.app.interface.sans_serif.clone();
+ self.serif_family = self.app.interface.serif.clone();
+ self.monospace_family = self.app.interface.monospace.clone();
+ }
AppAction::Accounts(m) => match m {
pages::accounts::AccountsMessage::GoogleLoginInit => {
pages::accounts::update(&mut self.app.accounts, m.clone());
@@ -1288,8 +1216,10 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
let ly = self.cursor_y / s + self.scroll_y;
clear_ui::widget::hover_animation::set_cursor_pos(lx, ly_no_scroll);
let mut changed = false;
- if self.paginator.cursor_moved(lx_no_scroll, ly_no_scroll) {
- changed = true;
+ if lx_no_scroll < self.sidebar_width {
+ if self.paginator.cursor_moved(lx_no_scroll, ly_no_scroll) {
+ changed = true;
+ }
}
for w in &mut self.widgets {
let was = w.hovering;
@@ -1329,6 +1259,12 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
changed = true;
}
}
+ if self.app.layout.side_panel_behavior_menu.cursor_moved(lx, ly) {
+ changed = true;
+ }
+ if self.app.layout.side_panel_width_spinbox.cursor_moved(lx, ly) {
+ changed = true;
+ }
}
if self.app.current_page == Page::Interface {
for cp in &mut self.app.interface.color_selectors {
@@ -1348,6 +1284,78 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
if self.app.interface.tab_padding_spinbox_y.cursor_moved(lx, ly) {
changed = true;
}
+ if self.app.interface.sans_box.cursor_moved(lx, ly) {
+ changed = true;
+ }
+ if self.app.interface.serif_box.cursor_moved(lx, ly) {
+ changed = true;
+ }
+ if self.app.interface.mono_box.cursor_moved(lx, ly) {
+ changed = true;
+ }
+ if self.app.interface.borders_menu.cursor_moved(lx, ly) {
+ changed = true;
+ }
+ if self.app.interface.borders_box.cursor_moved(lx, ly) {
+ changed = true;
+ }
+ if self.app.interface.status_menu.cursor_moved(lx, ly) {
+ changed = true;
+ }
+ if self.app.interface.status_box.cursor_moved(lx, ly) {
+ changed = true;
+ }
+ if self.app.interface.fuzzel_menu.cursor_moved(lx, ly) {
+ changed = true;
+ }
+ if self.app.interface.fuzzel_box.cursor_moved(lx, ly) {
+ changed = true;
+ }
+ if self.app.interface.terminal_menu.cursor_moved(lx, ly) {
+ changed = true;
+ }
+ if self.app.interface.terminal_box.cursor_moved(lx, ly) {
+ changed = true;
+ }
+ if self.app.interface.paginator_menu.cursor_moved(lx, ly) {
+ changed = true;
+ }
+ if self.app.interface.paginator_box.cursor_moved(lx, ly) {
+ changed = true;
+ }
+ if self.app.interface.search_box.cursor_moved(lx, ly) {
+ changed = true;
+ }
+ if self.app.interface.list_box.cursor_moved(lx, ly) {
+ changed = true;
+ }
+ if self.app.interface.borders_size_box.cursor_moved(lx, ly) {
+ changed = true;
+ }
+ if self.app.interface.status_size_box.cursor_moved(lx, ly) {
+ changed = true;
+ }
+ if self.app.interface.fuzzel_size_box.cursor_moved(lx, ly) {
+ changed = true;
+ }
+ if self.app.interface.terminal_size_box.cursor_moved(lx, ly) {
+ changed = true;
+ }
+ if self.app.interface.paginator_size_box.cursor_moved(lx, ly) {
+ changed = true;
+ }
+ let query = self.app.interface.search_box.text.to_lowercase();
+ let matching_count = self.app.interface.all_fonts.iter()
+ .filter(|f| f.to_lowercase().contains(&query))
+ .count();
+ for i in 0..matching_count.min(self.app.interface.font_buttons.len()) {
+ if self.app.interface.font_buttons[i].cursor_moved(lx, ly) {
+ changed = true;
+ }
+ if self.app.interface.copy_buttons[i].cursor_moved(lx, ly) {
+ changed = true;
+ }
+ }
}
if self.app.current_page == Page::Input {
if self.app.input.rate_spinbox.cursor_moved(lx, ly) {
@@ -1419,41 +1427,28 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
}
}
}
- }
- if self.opacity_dragging && self.app.current_page == Page::Notifications {
- if self.app.notifications.opacity_slider.drag_update(lx, ly) {
- changed = true;
- }
- } else if self.app.current_page == Page::Notifications {
- if self.app.notifications.enable_toggle.cursor_moved(lx, ly) {
+ if self.app.display.screensaver_enable_toggle.cursor_moved(lx, ly) {
changed = true;
}
- if self.app.notifications.bell_toggle.cursor_moved(lx, ly) {
+ if self.app.display.screensaver_lock_screen_toggle.cursor_moved(lx, ly) {
changed = true;
}
- if self.app.notifications.duration_spinbox.cursor_moved(lx, ly) {
+ if self.app.display.screensaver_timeout_spinbox.cursor_moved(lx, ly) {
changed = true;
}
- if self.app.notifications.opacity_slider.cursor_moved(lx, ly) {
+ if self.app.display.screensaver_style_menu.cursor_moved(lx, ly) {
changed = true;
}
}
- if self.app.current_page == Page::Screensaver {
- if self.app.screensaver.enable_toggle.cursor_moved(lx, ly) {
- changed = true;
- }
- if self.app.screensaver.lock_screen_toggle.cursor_moved(lx, ly) {
- changed = true;
- }
- if self.app.screensaver.timeout_spinbox.cursor_moved(lx, ly) {
+
+ if self.app.current_page == Page::Hardware {
+ if self.app.hardware.cpu_label.cursor_moved(lx, ly) {
changed = true;
}
- if self.app.screensaver.style_menu.cursor_moved(lx, ly) {
+ if self.app.hardware.cpu_usage_label.cursor_moved(lx, ly) {
changed = true;
}
- }
- if self.app.current_page == Page::Hardware {
- if self.app.hardware.cpu_label.cursor_moved(lx, ly) {
+ if self.app.hardware.cpu_temp_label.cursor_moved(lx, ly) {
changed = true;
}
for gpu_lbl in &mut self.app.hardware.gpu_labels {
@@ -1464,104 +1459,67 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
if self.app.hardware.cpu_list_box.cursor_moved(lx, ly) {
changed = true;
}
- }
- if self.app.current_page == Page::Status {
- if self.app.status.status_label.cursor_moved(lx, ly) {
- changed = true;
- }
- if self.app.status.size_label.cursor_moved(lx, ly) {
+ if self.app.hardware.cpu_gov_menu.cursor_moved(lx, ly) {
changed = true;
}
- if self.app.status.separators_toggle.cursor_moved(lx, ly) {
- changed = true;
- }
- if self.app.status.underline_toggle.cursor_moved(lx, ly) {
- changed = true;
- }
- if self.app.status.padding_spinbox.cursor_moved(lx, ly) {
+ if self.app.hardware.gpu_gov_menu.cursor_moved(lx, ly) {
changed = true;
}
}
- if self.app.current_page == Page::Typefaces {
- if self.app.typeface.sans_box.cursor_moved(lx, ly) {
- changed = true;
- }
- if self.app.typeface.serif_box.cursor_moved(lx, ly) {
- changed = true;
- }
- if self.app.typeface.mono_box.cursor_moved(lx, ly) {
- changed = true;
- }
- if self.app.typeface.borders_menu.cursor_moved(lx, ly) {
- changed = true;
- }
- if self.app.typeface.borders_box.cursor_moved(lx, ly) {
- changed = true;
- }
- if self.app.typeface.status_menu.cursor_moved(lx, ly) {
- changed = true;
- }
- if self.app.typeface.status_box.cursor_moved(lx, ly) {
- changed = true;
- }
- if self.app.typeface.fuzzel_menu.cursor_moved(lx, ly) {
- changed = true;
- }
- if self.app.typeface.fuzzel_box.cursor_moved(lx, ly) {
- changed = true;
- }
- if self.app.typeface.terminal_menu.cursor_moved(lx, ly) {
- changed = true;
- }
- if self.app.typeface.terminal_box.cursor_moved(lx, ly) {
- changed = true;
- }
- if self.app.typeface.paginator_menu.cursor_moved(lx, ly) {
- changed = true;
- }
- if self.app.typeface.paginator_box.cursor_moved(lx, ly) {
- changed = true;
- }
- if self.app.typeface.search_box.cursor_moved(lx, ly) {
- changed = true;
- }
- if self.app.typeface.list_box.cursor_moved(lx, ly) {
- changed = true;
- }
- if self.app.typeface.borders_size_box.cursor_moved(lx, ly) {
- changed = true;
- }
- if self.app.typeface.status_size_box.cursor_moved(lx, ly) {
- changed = true;
- }
- if self.app.typeface.fuzzel_size_box.cursor_moved(lx, ly) {
- changed = true;
- }
- if self.app.typeface.terminal_size_box.cursor_moved(lx, ly) {
- changed = true;
- }
- if self.app.typeface.paginator_size_box.cursor_moved(lx, ly) {
- changed = true;
- }
- let query = self.app.typeface.search_box.text.to_lowercase();
- let matching_count = self.app.typeface.all_fonts.iter()
- .filter(|f| f.to_lowercase().contains(&query))
- .count();
- for i in 0..matching_count.min(self.app.typeface.font_buttons.len()) {
- if self.app.typeface.font_buttons[i].cursor_moved(lx, ly) {
+
+ if self.app.current_page == Page::Services {
+ if self.opacity_dragging {
+ if self.app.services.notifications_opacity_slider.drag_update(lx, ly) {
changed = true;
}
- if self.app.typeface.copy_buttons[i].cursor_moved(lx, ly) {
+ } else {
+ if self.app.services.search_box.cursor_moved(lx, ly) {
+ changed = true;
+ }
+ if self.app.services.list_box.cursor_moved(lx, ly) {
+ changed = true;
+ }
+ let query = if self.app.services.search_box.editing {
+ self.app.services.search_box.edit_buffer.to_lowercase()
+ } else {
+ self.app.services.search_box.text.to_lowercase()
+ };
+ let matching_count = self.app.services.services.iter()
+ .filter(|s| s.is_system == (self.app.services.active_tab == pages::services::ServiceTab::System))
+ .filter(|s| s.name.to_lowercase().contains(&query) || s.description.to_lowercase().contains(&query))
+ .count();
+ for i in 0..matching_count.min(self.app.services.service_items.len()) {
+ if self.app.services.service_items[i].cursor_moved(lx, ly) {
+ changed = true;
+ }
+ }
+ if self.app.services.notifications_enable_toggle.cursor_moved(lx, ly) {
+ changed = true;
+ }
+ if self.app.services.notifications_bell_toggle.cursor_moved(lx, ly) {
+ changed = true;
+ }
+ if self.app.services.notifications_duration_spinbox.cursor_moved(lx, ly) {
+ changed = true;
+ }
+ if self.app.services.notifications_opacity_slider.cursor_moved(lx, ly) {
+ changed = true;
+ }
+ if self.app.services.status_label.cursor_moved(lx, ly) {
+ changed = true;
+ }
+ if self.app.services.status_size_label.cursor_moved(lx, ly) {
+ changed = true;
+ }
+ if self.app.services.status_separators_toggle.cursor_moved(lx, ly) {
+ changed = true;
+ }
+ if self.app.services.status_underline_toggle.cursor_moved(lx, ly) {
+ changed = true;
+ }
+ if self.app.services.status_padding_spinbox.cursor_moved(lx, ly) {
changed = true;
}
- }
- }
- if self.app.current_page == Page::Services {
- if self.app.services.search_box.cursor_moved(lx, ly) {
- changed = true;
- }
- if self.app.services.list_box.cursor_moved(lx, ly) {
- changed = true;
}
}
if self.app.current_page == Page::Accounts {
@@ -1591,17 +1549,21 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
}
}
- if self.paginator.mouse_input(button, state, lx_no_scroll, ly_no_scroll) {
- if self.paginator.take_click() {
- let idx = self.paginator.selected_page();
- if idx < Page::ALL.len() {
- clear_ui::widget::focus::clear_focus();
- self.app.current_page = Page::ALL[idx];
- self.scroll_y = 0.0;
+ if lx_no_scroll < self.sidebar_width {
+ if self.paginator.mouse_input(button, state, lx_no_scroll, ly_no_scroll) {
+ if self.paginator.take_click() {
+ let idx = self.paginator.selected_page();
+ if idx < Page::ALL.len() {
+ clear_ui::widget::focus::clear_focus();
+ let new_page = Page::ALL[idx];
+ self.app.current_page = new_page;
+ self.scroll_y = 0.0;
+ pages::interface::write_config_value("last_page", &format!("\"{}\"", new_page.label().to_lowercase()));
+ }
}
+ self.needs_rebuild = true;
+ return true;
}
- self.needs_rebuild = true;
- return true;
}
if button != clear_ui::widget::MouseButton::Left && button != clear_ui::widget::MouseButton::Right { return false; }
@@ -1647,6 +1609,8 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
for menu in &mut self.app.layout.tag_layout_menus {
if menu.hit_test(lx, ly) { clicked_any_focusable = true; }
}
+ if self.app.layout.side_panel_behavior_menu.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if self.app.layout.side_panel_width_spinbox.hit_test(lx, ly) { clicked_any_focusable = true; }
}
Page::Interface => {
for cp in &mut self.app.interface.color_selectors {
@@ -1656,6 +1620,22 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
if self.app.interface.tab_margin_spinbox_y.hit_test(lx, ly) { clicked_any_focusable = true; }
if self.app.interface.tab_padding_spinbox_x.hit_test(lx, ly) { clicked_any_focusable = true; }
if self.app.interface.tab_padding_spinbox_y.hit_test(lx, ly) { clicked_any_focusable = true; }
+ let tf = &mut self.app.interface;
+ if tf.sans_box.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if tf.serif_box.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if tf.mono_box.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if tf.borders_menu.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if tf.borders_box.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if tf.status_menu.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if tf.status_box.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if tf.fuzzel_menu.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if tf.fuzzel_box.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if tf.terminal_menu.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if tf.terminal_box.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if tf.paginator_menu.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if tf.paginator_box.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if tf.search_box.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if tf.list_box.hit_test(lx, ly) { clicked_any_focusable = true; }
}
Page::Input => {
if self.app.input.rate_spinbox.hit_test(lx, ly) { clicked_any_focusable = true; }
@@ -1666,14 +1646,7 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
if self.app.input.trackpoint_accel_speed_spinbox.hit_test(lx, ly) { clicked_any_focusable = true; }
if self.app.input.trackpoint_accel_profile_menu.hit_test(lx, ly) { clicked_any_focusable = true; }
}
- Page::Notifications => {
- if self.app.notifications.duration_spinbox.hit_test(lx, ly) { clicked_any_focusable = true; }
- if self.app.notifications.opacity_slider.hit_test(lx, ly) { clicked_any_focusable = true; }
- }
- Page::Screensaver => {
- if self.app.screensaver.timeout_spinbox.hit_test(lx, ly) { clicked_any_focusable = true; }
- if self.app.screensaver.style_menu.hit_test(lx, ly) { clicked_any_focusable = true; }
- }
+
Page::Audio => {
for sb in &mut self.app.audio.sink_spinboxes {
if sb.hit_test(lx, ly) { clicked_any_focusable = true; }
@@ -1692,38 +1665,24 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
if scale_lbl.hit_test(lx, ly) { clicked_any_focusable = true; }
}
}
- }
- Page::Status => {
- if self.app.status.status_label.hit_test(lx, ly) { clicked_any_focusable = true; }
- if self.app.status.size_label.hit_test(lx, ly) { clicked_any_focusable = true; }
- if self.app.status.padding_spinbox.hit_test(lx, ly) { clicked_any_focusable = true; }
- }
- Page::Typefaces => {
- let tf = &mut self.app.typeface;
- if tf.sans_box.hit_test(lx, ly) { clicked_any_focusable = true; }
- if tf.serif_box.hit_test(lx, ly) { clicked_any_focusable = true; }
- if tf.mono_box.hit_test(lx, ly) { clicked_any_focusable = true; }
- if tf.borders_menu.hit_test(lx, ly) { clicked_any_focusable = true; }
- if tf.borders_box.hit_test(lx, ly) { clicked_any_focusable = true; }
- if tf.status_menu.hit_test(lx, ly) { clicked_any_focusable = true; }
- if tf.status_box.hit_test(lx, ly) { clicked_any_focusable = true; }
- if tf.fuzzel_menu.hit_test(lx, ly) { clicked_any_focusable = true; }
- if tf.fuzzel_box.hit_test(lx, ly) { clicked_any_focusable = true; }
- if tf.terminal_menu.hit_test(lx, ly) { clicked_any_focusable = true; }
- if tf.terminal_box.hit_test(lx, ly) { clicked_any_focusable = true; }
- if tf.paginator_menu.hit_test(lx, ly) { clicked_any_focusable = true; }
- if tf.paginator_box.hit_test(lx, ly) { clicked_any_focusable = true; }
- if tf.search_box.hit_test(lx, ly) { clicked_any_focusable = true; }
- if tf.list_box.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if self.app.display.screensaver_timeout_spinbox.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if self.app.display.screensaver_style_menu.hit_test(lx, ly) { clicked_any_focusable = true; }
}
Page::Services => {
let srv = &mut self.app.services;
if srv.search_box.hit_test(lx, ly) { clicked_any_focusable = true; }
if srv.list_box.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if srv.notifications_duration_spinbox.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if srv.notifications_opacity_slider.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if srv.status_label.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if srv.status_size_label.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if srv.status_padding_spinbox.hit_test(lx, ly) { clicked_any_focusable = true; }
}
Page::Hardware => {
let hw = &mut self.app.hardware;
if hw.cpu_list_box.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if hw.cpu_gov_menu.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if hw.gpu_gov_menu.hit_test(lx, ly) { clicked_any_focusable = true; }
}
Page::Radios => {
let net = &mut self.app.network;
@@ -1809,6 +1768,22 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
actions.push(AppAction::Layout(pages::layout::LayoutMessage::SetTagLayout(idx + 1, menu.selected)));
}
}
+ let menu = &mut self.app.layout.side_panel_behavior_menu;
+ if state == clear_ui::widget::ElementState::Pressed && !menu.hit_test(lx, ly) { menu.unfocus(); }
+ if menu.mouse_input(button, state, lx, ly) {
+ self.needs_rebuild = true;
+ }
+ if state == clear_ui::widget::ElementState::Pressed && menu.take_change() {
+ actions.push(AppAction::Layout(pages::layout::LayoutMessage::SetSidePanelBehavior(menu.selected)));
+ }
+ let sb = &mut self.app.layout.side_panel_width_spinbox;
+ if !sb.hit_test(lx, ly) { sb.unfocus(); }
+ let old = sb.value;
+ if sb.mouse_input(button, state, lx, ly) && sb.value != old {
+ actions.push(AppAction::Layout(
+ pages::layout::LayoutMessage::SetSidePanelWidth(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() {
@@ -1929,12 +1904,18 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
actions.push(AppAction::Input(pages::input::InputMessage::ApplyCursorSize));
}
}
- if state == clear_ui::widget::ElementState::Pressed && self.app.current_page == Page::Notifications {
- let sb = &mut self.app.notifications.duration_spinbox;
+ if state == clear_ui::widget::ElementState::Pressed && self.app.current_page == Page::Services {
+ let sb = &mut self.app.services.notifications_duration_spinbox;
if !sb.hit_test(lx, ly) { sb.unfocus(); }
let old = sb.value;
if sb.mouse_input(button, state, lx, ly) && sb.value != old {
- actions.push(AppAction::Notifications(pages::notifications::NotificationsMessage::SetDuration(sb.value)));
+ actions.push(AppAction::Services(pages::services::ServicesMessage::SetNotificationsDuration(sb.value)));
+ }
+ let sb2 = &mut self.app.services.status_padding_spinbox;
+ if !sb2.hit_test(lx, ly) { sb2.unfocus(); }
+ let old2 = sb2.value;
+ if sb2.mouse_input(button, state, lx, ly) && sb2.value != old2 {
+ actions.push(AppAction::Services(pages::services::ServicesMessage::StatusSetPadding(sb2.value as u16)));
}
}
if self.app.current_page == Page::Input {
@@ -1985,18 +1966,18 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
actions.push(AppAction::Input(pages::input::InputMessage::ApplyCursorTheme(menu.selected)));
}
}
- if self.app.current_page == Page::Notifications {
- let toggle = &mut self.app.notifications.enable_toggle;
+ if self.app.current_page == Page::Services {
+ let toggle = &mut self.app.services.notifications_enable_toggle;
toggle.mouse_input(button, state, lx, ly);
if toggle.take_click() {
- actions.push(AppAction::Notifications(pages::notifications::NotificationsMessage::ToggleEnable));
+ actions.push(AppAction::Services(pages::services::ServicesMessage::ToggleNotificationsEnable));
}
- let toggle = &mut self.app.notifications.bell_toggle;
+ let toggle = &mut self.app.services.notifications_bell_toggle;
toggle.mouse_input(button, state, lx, ly);
if toggle.take_click() {
- actions.push(AppAction::Notifications(pages::notifications::NotificationsMessage::ToggleBell));
+ actions.push(AppAction::Services(pages::services::ServicesMessage::ToggleNotificationsBell));
}
- let slider = &mut self.app.notifications.opacity_slider;
+ let slider = &mut self.app.services.notifications_opacity_slider;
if button == clear_ui::widget::MouseButton::Left {
if state == clear_ui::widget::ElementState::Pressed {
if slider.hit_test(lx, ly) {
@@ -2009,41 +1990,78 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
self.opacity_dragging = false;
slider.drag_end();
let val = slider.value() as f32 / 100.0;
- actions.push(AppAction::Notifications(pages::notifications::NotificationsMessage::SetOpacity(val)));
+ actions.push(AppAction::Services(pages::services::ServicesMessage::SetNotificationsOpacity(val)));
self.needs_rebuild = true;
}
}
}
+ if state == clear_ui::widget::ElementState::Pressed {
+ let lbl1 = &mut self.app.services.status_label;
+ if !lbl1.hit_test(lx, ly) { lbl1.unfocus(); }
+ lbl1.mouse_input(button, state, lx, ly);
+
+ let lbl2 = &mut self.app.services.status_size_label;
+ if !lbl2.hit_test(lx, ly) { lbl2.unfocus(); }
+ lbl2.mouse_input(button, state, lx, ly);
+ }
+ let toggle = &mut self.app.services.status_separators_toggle;
+ toggle.mouse_input(button, state, lx, ly);
+ if toggle.take_click() {
+ actions.push(AppAction::Services(pages::services::ServicesMessage::StatusToggleSeparators));
+ }
+ let toggle2 = &mut self.app.services.status_underline_toggle;
+ toggle2.mouse_input(button, state, lx, ly);
+ if toggle2.take_click() {
+ actions.push(AppAction::Services(pages::services::ServicesMessage::StatusToggleUnderline));
+ }
}
- if self.app.current_page == Page::Screensaver {
- let toggle = &mut self.app.screensaver.enable_toggle;
+ if self.app.current_page == Page::Display {
+ let toggle = &mut self.app.display.screensaver_enable_toggle;
toggle.mouse_input(button, state, lx, ly);
if toggle.take_click() {
- actions.push(AppAction::Screensaver(pages::screensaver::ScreensaverMessage::ToggleEnable));
+ actions.push(AppAction::Display(pages::display::DisplayMessage::ToggleScreensaverEnable));
}
- let toggle = &mut self.app.screensaver.lock_screen_toggle;
+ let toggle = &mut self.app.display.screensaver_lock_screen_toggle;
toggle.mouse_input(button, state, lx, ly);
if toggle.take_click() {
- actions.push(AppAction::Screensaver(pages::screensaver::ScreensaverMessage::ToggleLockScreen));
+ actions.push(AppAction::Display(pages::display::DisplayMessage::ToggleScreensaverLockScreen));
}
- if state == clear_ui::widget::ElementState::Pressed {
- let sb = &mut self.app.screensaver.timeout_spinbox;
- if !sb.hit_test(lx, ly) { sb.unfocus(); }
- let old = sb.value;
- if sb.mouse_input(button, state, lx, ly) && sb.value != old {
- actions.push(AppAction::Screensaver(pages::screensaver::ScreensaverMessage::SetTimeout(sb.value)));
+ let menu = &mut self.app.display.screensaver_style_menu;
+ if state == clear_ui::widget::ElementState::Pressed && !menu.hit_test(lx, ly) { menu.unfocus(); }
+ if menu.mouse_input(button, state, lx, ly) {
+ self.needs_rebuild = true;
+ }
+ if state == clear_ui::widget::ElementState::Pressed && menu.take_change() {
+ actions.push(AppAction::Display(pages::display::DisplayMessage::SetScreensaverStyle(menu.selected)));
+ }
+ }
+ if self.app.current_page == Page::Hardware {
+ let menu = &mut self.app.hardware.cpu_gov_menu;
+ if state == clear_ui::widget::ElementState::Pressed && !menu.hit_test(lx, ly) { menu.unfocus(); }
+ if menu.mouse_input(button, state, lx, ly) {
+ self.needs_rebuild = true;
+ }
+ if state == clear_ui::widget::ElementState::Pressed && menu.take_change() {
+ if menu.selected == 0 {
+ actions.push(AppAction::Hardware(pages::hardware::HardwareMessage::SetCpuPerformance));
+ } else {
+ actions.push(AppAction::Hardware(pages::hardware::HardwareMessage::SetCpuPowersave));
}
}
- let menu = &mut self.app.screensaver.style_menu;
+ let menu = &mut self.app.hardware.gpu_gov_menu;
if state == clear_ui::widget::ElementState::Pressed && !menu.hit_test(lx, ly) { menu.unfocus(); }
if menu.mouse_input(button, state, lx, ly) {
self.needs_rebuild = true;
}
if state == clear_ui::widget::ElementState::Pressed && menu.take_change() {
- actions.push(AppAction::Screensaver(pages::screensaver::ScreensaverMessage::SetStyle(menu.selected)));
+ if menu.selected == 0 {
+ actions.push(AppAction::Hardware(pages::hardware::HardwareMessage::SetGpuDefault));
+ } else {
+ actions.push(AppAction::Hardware(pages::hardware::HardwareMessage::SetGpuPowersave));
+ }
}
}
@@ -2090,36 +2108,15 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
scale_lbl.mouse_input(button, state, lx, ly);
}
}
- }
- if state == clear_ui::widget::ElementState::Pressed && self.app.current_page == Page::Status {
- let lbl1 = &mut self.app.status.status_label;
- if !lbl1.hit_test(lx, ly) { lbl1.unfocus(); }
- lbl1.mouse_input(button, state, lx, ly);
-
- let lbl2 = &mut self.app.status.size_label;
- if !lbl2.hit_test(lx, ly) { lbl2.unfocus(); }
- lbl2.mouse_input(button, state, lx, ly);
- let sb = &mut self.app.status.padding_spinbox;
+ let sb = &mut self.app.display.screensaver_timeout_spinbox;
if !sb.hit_test(lx, ly) { sb.unfocus(); }
let old = sb.value;
if sb.mouse_input(button, state, lx, ly) && sb.value != old {
- actions.push(AppAction::Status(pages::status::StatusMessage::SetPadding(sb.value as u16)));
+ actions.push(AppAction::Display(pages::display::DisplayMessage::SetScreensaverTimeout(sb.value)));
}
}
- if self.app.current_page == Page::Status {
- let toggle = &mut self.app.status.separators_toggle;
- toggle.mouse_input(button, state, lx, ly);
- if toggle.take_click() {
- actions.push(AppAction::Status(pages::status::StatusMessage::ToggleSeparators));
- }
- let toggle2 = &mut self.app.status.underline_toggle;
- toggle2.mouse_input(button, state, lx, ly);
- if toggle2.take_click() {
- actions.push(AppAction::Status(pages::status::StatusMessage::ToggleUnderline));
- }
- }
if self.app.current_page == Page::Accounts {
if self.app.accounts.editing_oauth_creds {
let tb = &mut self.app.accounts.oauth_client_id_box;
@@ -2178,184 +2175,184 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
}
}
}
- if self.app.current_page == Page::Typefaces {
- let tb = &mut self.app.typeface.sans_box;
+ if self.app.current_page == Page::Interface {
+ let tb = &mut self.app.interface.sans_box;
if state == clear_ui::widget::ElementState::Pressed && !tb.hit_test(lx, ly) { tb.unfocus(); }
if tb.mouse_input(button, state, lx, ly) {
self.needs_rebuild = true;
}
if state == clear_ui::widget::ElementState::Pressed && tb.take_change() {
- actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetSans(tb.text.clone())));
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetSans(tb.text.clone())));
}
- let tb = &mut self.app.typeface.serif_box;
+ let tb = &mut self.app.interface.serif_box;
if state == clear_ui::widget::ElementState::Pressed && !tb.hit_test(lx, ly) { tb.unfocus(); }
if tb.mouse_input(button, state, lx, ly) {
self.needs_rebuild = true;
}
if state == clear_ui::widget::ElementState::Pressed && tb.take_change() {
- actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetSerif(tb.text.clone())));
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetSerif(tb.text.clone())));
}
- let tb = &mut self.app.typeface.mono_box;
+ let tb = &mut self.app.interface.mono_box;
if state == clear_ui::widget::ElementState::Pressed && !tb.hit_test(lx, ly) { tb.unfocus(); }
if tb.mouse_input(button, state, lx, ly) {
self.needs_rebuild = true;
}
if state == clear_ui::widget::ElementState::Pressed && tb.take_change() {
- actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetMono(tb.text.clone())));
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetMono(tb.text.clone())));
}
- let menu = &mut self.app.typeface.borders_menu;
+ let menu = &mut self.app.interface.borders_menu;
if state == clear_ui::widget::ElementState::Pressed && !menu.hit_test(lx, ly) { menu.unfocus(); }
if menu.mouse_input(button, state, lx, ly) {
self.needs_rebuild = true;
}
if state == clear_ui::widget::ElementState::Pressed && menu.take_change() {
- actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetBordersMenu(menu.selected)));
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetBordersMenu(menu.selected)));
}
- let tb = &mut self.app.typeface.borders_box;
+ let tb = &mut self.app.interface.borders_box;
if state == clear_ui::widget::ElementState::Pressed && !tb.hit_test(lx, ly) { tb.unfocus(); }
if tb.mouse_input(button, state, lx, ly) {
self.needs_rebuild = true;
}
if state == clear_ui::widget::ElementState::Pressed && tb.take_change() {
- actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetBorders(tb.text.clone())));
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetBorders(tb.text.clone())));
}
- let menu = &mut self.app.typeface.status_menu;
+ let menu = &mut self.app.interface.status_menu;
if state == clear_ui::widget::ElementState::Pressed && !menu.hit_test(lx, ly) { menu.unfocus(); }
if menu.mouse_input(button, state, lx, ly) {
self.needs_rebuild = true;
}
if state == clear_ui::widget::ElementState::Pressed && menu.take_change() {
- actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetStatusMenu(menu.selected)));
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetStatusMenu(menu.selected)));
}
- let tb = &mut self.app.typeface.status_box;
+ let tb = &mut self.app.interface.status_box;
if state == clear_ui::widget::ElementState::Pressed && !tb.hit_test(lx, ly) { tb.unfocus(); }
if tb.mouse_input(button, state, lx, ly) {
self.needs_rebuild = true;
}
if state == clear_ui::widget::ElementState::Pressed && tb.take_change() {
- actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetStatus(tb.text.clone())));
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetStatus(tb.text.clone())));
}
- let menu = &mut self.app.typeface.fuzzel_menu;
+ let menu = &mut self.app.interface.fuzzel_menu;
if state == clear_ui::widget::ElementState::Pressed && !menu.hit_test(lx, ly) { menu.unfocus(); }
if menu.mouse_input(button, state, lx, ly) {
self.needs_rebuild = true;
}
if state == clear_ui::widget::ElementState::Pressed && menu.take_change() {
- actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetFuzzelMenu(menu.selected)));
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetFuzzelMenu(menu.selected)));
}
- let tb = &mut self.app.typeface.fuzzel_box;
+ let tb = &mut self.app.interface.fuzzel_box;
if state == clear_ui::widget::ElementState::Pressed && !tb.hit_test(lx, ly) { tb.unfocus(); }
if tb.mouse_input(button, state, lx, ly) {
self.needs_rebuild = true;
}
if state == clear_ui::widget::ElementState::Pressed && tb.take_change() {
- actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetFuzzel(tb.text.clone())));
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetFuzzel(tb.text.clone())));
}
- let menu = &mut self.app.typeface.terminal_menu;
+ let menu = &mut self.app.interface.terminal_menu;
if state == clear_ui::widget::ElementState::Pressed && !menu.hit_test(lx, ly) { menu.unfocus(); }
if menu.mouse_input(button, state, lx, ly) {
self.needs_rebuild = true;
}
if state == clear_ui::widget::ElementState::Pressed && menu.take_change() {
- actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetTerminalMenu(menu.selected)));
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetTerminalMenu(menu.selected)));
}
- let tb = &mut self.app.typeface.terminal_box;
+ let tb = &mut self.app.interface.terminal_box;
if state == clear_ui::widget::ElementState::Pressed && !tb.hit_test(lx, ly) { tb.unfocus(); }
if tb.mouse_input(button, state, lx, ly) {
self.needs_rebuild = true;
}
if state == clear_ui::widget::ElementState::Pressed && tb.take_change() {
- actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetTerminal(tb.text.clone())));
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetTerminal(tb.text.clone())));
}
- let menu = &mut self.app.typeface.paginator_menu;
+ let menu = &mut self.app.interface.paginator_menu;
if state == clear_ui::widget::ElementState::Pressed && !menu.hit_test(lx, ly) { menu.unfocus(); }
if menu.mouse_input(button, state, lx, ly) {
self.needs_rebuild = true;
}
if state == clear_ui::widget::ElementState::Pressed && menu.take_change() {
- actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetPaginatorMenu(menu.selected)));
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetPaginatorMenu(menu.selected)));
}
- let tb = &mut self.app.typeface.paginator_box;
+ let tb = &mut self.app.interface.paginator_box;
if state == clear_ui::widget::ElementState::Pressed && !tb.hit_test(lx, ly) { tb.unfocus(); }
if tb.mouse_input(button, state, lx, ly) {
self.needs_rebuild = true;
}
if state == clear_ui::widget::ElementState::Pressed && tb.take_change() {
- actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetPaginator(tb.text.clone())));
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetPaginator(tb.text.clone())));
}
- let tb = &mut self.app.typeface.search_box;
+ let tb = &mut self.app.interface.search_box;
if state == clear_ui::widget::ElementState::Pressed && !tb.hit_test(lx, ly) { tb.unfocus(); }
if tb.mouse_input(button, state, lx, ly) {
self.needs_rebuild = true;
}
if state == clear_ui::widget::ElementState::Pressed && tb.take_change() {
- actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetSearch(tb.text.clone())));
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetSearch(tb.text.clone())));
}
- let sb = &mut self.app.typeface.borders_size_box;
+ let sb = &mut self.app.interface.borders_size_box;
if state == clear_ui::widget::ElementState::Pressed && !sb.hit_test(lx, ly) { sb.unfocus(); }
let old_val = sb.value;
if sb.mouse_input(button, state, lx, ly) {
self.needs_rebuild = true;
}
if sb.value != old_val {
- actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetBordersSize(sb.value)));
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetBordersSize(sb.value)));
}
- let sb = &mut self.app.typeface.status_size_box;
+ let sb = &mut self.app.interface.status_size_box;
if state == clear_ui::widget::ElementState::Pressed && !sb.hit_test(lx, ly) { sb.unfocus(); }
let old_val = sb.value;
if sb.mouse_input(button, state, lx, ly) {
self.needs_rebuild = true;
}
if sb.value != old_val {
- actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetStatusSize(sb.value)));
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetStatusSize(sb.value)));
}
- let sb = &mut self.app.typeface.fuzzel_size_box;
+ let sb = &mut self.app.interface.fuzzel_size_box;
if state == clear_ui::widget::ElementState::Pressed && !sb.hit_test(lx, ly) { sb.unfocus(); }
let old_val = sb.value;
if sb.mouse_input(button, state, lx, ly) {
self.needs_rebuild = true;
}
if sb.value != old_val {
- actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetFuzzelSize(sb.value)));
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetFuzzelSize(sb.value)));
}
- let sb = &mut self.app.typeface.terminal_size_box;
+ let sb = &mut self.app.interface.terminal_size_box;
if state == clear_ui::widget::ElementState::Pressed && !sb.hit_test(lx, ly) { sb.unfocus(); }
let old_val = sb.value;
if sb.mouse_input(button, state, lx, ly) {
self.needs_rebuild = true;
}
if sb.value != old_val {
- actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetTerminalSize(sb.value)));
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetTerminalSize(sb.value)));
}
- let sb = &mut self.app.typeface.paginator_size_box;
+ let sb = &mut self.app.interface.paginator_size_box;
if state == clear_ui::widget::ElementState::Pressed && !sb.hit_test(lx, ly) { sb.unfocus(); }
let old_val = sb.value;
if sb.mouse_input(button, state, lx, ly) {
self.needs_rebuild = true;
}
if sb.value != old_val {
- actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetPaginatorSize(sb.value)));
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetPaginatorSize(sb.value)));
}
- let tf = &mut self.app.typeface;
+ let tf = &mut self.app.interface;
let query = tf.search_box.text.to_lowercase();
let matching_fonts: Vec<String> = tf.all_fonts.iter()
.filter(|font| font.to_lowercase().contains(&query))
@@ -2387,9 +2384,9 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
if let Some(idx) = clicked_idx {
if let Some(font_name) = matching_fonts.get(idx) {
let action = if is_copy {
- AppAction::Typeface(pages::typeface::TypefaceMessage::CopyFontName(font_name.clone()))
+ AppAction::Interface(pages::interface::InterfaceMessage::CopyFontName(font_name.clone()))
} else {
- AppAction::Typeface(pages::typeface::TypefaceMessage::SelectFont(font_name.clone()))
+ AppAction::Interface(pages::interface::InterfaceMessage::SelectFont(font_name.clone()))
};
actions.push(action);
}
@@ -2409,6 +2406,20 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
if srv.list_box.mouse_input(button, state, lx, ly) {
self.needs_rebuild = true;
}
+ let query = if srv.search_box.editing {
+ srv.search_box.edit_buffer.to_lowercase()
+ } else {
+ srv.search_box.text.to_lowercase()
+ };
+ let matching_count = srv.services.iter()
+ .filter(|s| s.is_system == (srv.active_tab == pages::services::ServiceTab::System))
+ .filter(|s| s.name.to_lowercase().contains(&query) || s.description.to_lowercase().contains(&query))
+ .count();
+ for i in 0..matching_count.min(srv.service_items.len()) {
+ if srv.service_items[i].mouse_input(button, state, lx, ly) {
+ self.needs_rebuild = true;
+ }
+ }
}
if state == clear_ui::widget::ElementState::Pressed && self.app.current_page == Page::Hardware {
let hw = &mut self.app.hardware;
@@ -2450,8 +2461,8 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
}
}
- if self.app.current_page == Page::Typefaces {
- let tf = &mut self.app.typeface;
+ if self.app.current_page == Page::Interface {
+ let tf = &mut self.app.interface;
if tf.list_box.mouse_wheel(delta, lx, ly) {
self.needs_rebuild = true;
return true;
@@ -2501,14 +2512,14 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
false
}
- fn get_page_root_widget(&mut self) -> Option<*mut (dyn clear_ui::widget::Widget + 'static)> {
+ fn get_page_root_widget(&mut self) -> Option<*mut (dyn clear_ui::widget::Element + 'static)> {
match self.app.current_page {
- Page::Typefaces | Page::Services | Page::Hardware | Page::Radios |
- Page::Layout | Page::Interface | Page::Notifications | Page::Input |
+ Page::Services | Page::Hardware | Page::Radios |
+ Page::Layout | Page::Interface | Page::Input |
Page::Audio | Page::Display => {
- let ptr = &mut self.page_root_container as &mut dyn clear_ui::widget::Widget as *mut dyn clear_ui::widget::Widget;
+ let ptr = &mut self.page_root_container as &mut dyn clear_ui::widget::Element as *mut dyn clear_ui::widget::Element;
let static_ptr = unsafe {
- std::mem::transmute::<*mut dyn clear_ui::widget::Widget, *mut (dyn clear_ui::widget::Widget + 'static)>(ptr)
+ std::mem::transmute::<*mut dyn clear_ui::widget::Element, *mut (dyn clear_ui::widget::Element + 'static)>(ptr)
};
Some(static_ptr)
}
@@ -2619,6 +2630,28 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
}
changed = true;
}
+ let (menu_changed, old_selected, new_selected) = {
+ let menu = &mut self.app.layout.side_panel_behavior_menu;
+ let old = menu.selected;
+ let changed = menu.keyboard_input(event);
+ (changed, old, menu.selected)
+ };
+ if menu_changed {
+ if new_selected != old_selected {
+ actions.push(AppAction::Layout(pages::layout::LayoutMessage::SetSidePanelBehavior(new_selected)));
+ }
+ changed = true;
+ }
+ let sb = &mut self.app.layout.side_panel_width_spinbox;
+ let old = sb.value;
+ if sb.keyboard_input(event) {
+ if sb.value != old {
+ actions.push(AppAction::Layout(
+ pages::layout::LayoutMessage::SetSidePanelWidth(sb.value as u16)
+ ));
+ }
+ changed = true;
+ }
for a in &actions {
self.handle_action(a);
}
@@ -2666,7 +2699,6 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
let old = sb.value;
if sb.keyboard_input(event) {
let new_val = sb.value;
- drop(sb);
if new_val != old {
self.handle_action(&AppAction::Interface(pages::interface::InterfaceMessage::SetTabMarginX(new_val as u16)));
}
@@ -2677,7 +2709,6 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
let old = sb.value;
if sb.keyboard_input(event) {
let new_val = sb.value;
- drop(sb);
if new_val != old {
self.handle_action(&AppAction::Interface(pages::interface::InterfaceMessage::SetTabMarginY(new_val as u16)));
}
@@ -2688,7 +2719,6 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
let old = sb.value;
if sb.keyboard_input(event) {
let new_val = sb.value;
- drop(sb);
if new_val != old {
self.handle_action(&AppAction::Interface(pages::interface::InterfaceMessage::SetTabPaddingX(new_val as u16)));
}
@@ -2699,179 +2729,13 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
let old = sb.value;
if sb.keyboard_input(event) {
let new_val = sb.value;
- drop(sb);
if new_val != old {
self.handle_action(&AppAction::Interface(pages::interface::InterfaceMessage::SetTabPaddingY(new_val as u16)));
}
self.needs_rebuild = true;
return true;
}
- }
- if self.app.current_page == Page::Screensaver {
- let sb = &mut self.app.screensaver.timeout_spinbox;
- let old = sb.value;
- if sb.keyboard_input(event) {
- let new_val = sb.value;
- drop(sb);
- if new_val != old {
- self.handle_action(&AppAction::Screensaver(pages::screensaver::ScreensaverMessage::SetTimeout(new_val)));
- }
- self.needs_rebuild = true;
- return true;
- }
- }
- if self.app.current_page == Page::Notifications {
- let sb = &mut self.app.notifications.duration_spinbox;
- let old = sb.value;
- if sb.keyboard_input(event) {
- let new_val = sb.value;
- drop(sb);
- if new_val != old {
- self.handle_action(&AppAction::Notifications(pages::notifications::NotificationsMessage::SetDuration(new_val)));
- }
- self.needs_rebuild = true;
- return true;
- }
- }
- if self.app.current_page == Page::Input {
- if self.app.input.rate_spinbox.keyboard_input(event) {
- self.handle_action(&AppAction::Input(pages::input::InputMessage::ApplyRepeat));
- self.needs_rebuild = true;
- return true;
- }
- if self.app.input.delay_spinbox.keyboard_input(event) {
- self.handle_action(&AppAction::Input(pages::input::InputMessage::ApplyRepeat));
- self.needs_rebuild = true;
- return true;
- }
- if self.app.input.scroll_friction_spinbox.keyboard_input(event) {
- self.handle_action(&AppAction::Input(pages::input::InputMessage::ApplyScrollFriction));
- self.needs_rebuild = true;
- return true;
- }
- if self.app.input.scroll_speed_spinbox.keyboard_input(event) {
- self.handle_action(&AppAction::Input(pages::input::InputMessage::ApplyScrollSpeed));
- self.needs_rebuild = true;
- return true;
- }
- if self.app.input.pointer_friction_spinbox.keyboard_input(event) {
- self.handle_action(&AppAction::Input(pages::input::InputMessage::ApplyPointerFriction));
- self.needs_rebuild = true;
- return true;
- }
- if self.app.input.trackpad_friction_spinbox.keyboard_input(event) {
- self.handle_action(&AppAction::Input(pages::input::InputMessage::ApplyTrackpadFriction));
- self.needs_rebuild = true;
- return true;
- }
- if self.app.input.trackpoint_accel_speed_spinbox.keyboard_input(event) {
- self.handle_action(&AppAction::Input(pages::input::InputMessage::ApplyTrackpointAccelSpeed));
- self.needs_rebuild = true;
- return true;
- }
- if self.app.input.cursor_size_spinbox.keyboard_input(event) {
- self.handle_action(&AppAction::Input(pages::input::InputMessage::ApplyCursorSize));
- self.needs_rebuild = true;
- return true;
- }
- }
- if self.app.current_page == Page::Accounts {
- let mut consumed = false;
- if self.app.accounts.editing_oauth_creds {
- let tb = &mut self.app.accounts.oauth_client_id_box;
- if tb.keyboard_input(event) { consumed = true; }
- let tb = &mut self.app.accounts.oauth_client_secret_box;
- if tb.keyboard_input(event) { consumed = true; }
- } else {
- let tb = &mut self.app.accounts.email_box;
- if tb.keyboard_input(event) {
- consumed = true;
- let email_val = tb.edit_buffer.trim().to_lowercase();
- if email_val.ends_with("@gmail.com") {
- self.app.accounts.imap_box.text = "imap.gmail.com:993".to_string();
- self.app.accounts.imap_box.edit_buffer = "imap.gmail.com:993".to_string();
- self.app.accounts.smtp_box.text = "smtp.gmail.com:465".to_string();
- self.app.accounts.smtp_box.edit_buffer = "smtp.gmail.com:465".to_string();
- } else if email_val.ends_with("@icloud.com") {
- self.app.accounts.imap_box.text = "imap.mail.me.com:993".to_string();
- self.app.accounts.imap_box.edit_buffer = "imap.mail.me.com:993".to_string();
- self.app.accounts.smtp_box.text = "smtp.mail.me.com:587".to_string();
- self.app.accounts.smtp_box.edit_buffer = "smtp.mail.me.com:587".to_string();
- } else if email_val.ends_with("@outlook.com") || email_val.ends_with("@hotmail.com") {
- self.app.accounts.imap_box.text = "outlook.office365.com:993".to_string();
- self.app.accounts.imap_box.edit_buffer = "outlook.office365.com:993".to_string();
- self.app.accounts.smtp_box.text = "smtp.office365.com:587".to_string();
- self.app.accounts.smtp_box.edit_buffer = "smtp.office365.com:587".to_string();
- }
- }
- let tb = &mut self.app.accounts.password_box;
- if tb.keyboard_input(event) { consumed = true; }
- let tb = &mut self.app.accounts.imap_box;
- if tb.keyboard_input(event) { consumed = true; }
- let tb = &mut self.app.accounts.smtp_box;
- if tb.keyboard_input(event) { consumed = true; }
- }
-
- if consumed {
- self.needs_rebuild = true;
- return true;
- }
- }
- if self.app.current_page == Page::Audio {
- let mut actions = Vec::new();
- for (i, sb) in self.app.audio.sink_spinboxes.iter_mut().enumerate() {
- let old = sb.value;
- if sb.keyboard_input(event) {
- if sb.value != old {
- let id = self.app.audio.sinks[i].id;
- actions.push(AppAction::Audio(pages::audio::AudioMessage::SinkVolume(id, sb.value as f32 / 100.0)));
- }
- }
- }
- for (i, sb) in self.app.audio.source_spinboxes.iter_mut().enumerate() {
- let old = sb.value;
- if sb.keyboard_input(event) {
- if sb.value != old {
- let id = self.app.audio.sources[i].id;
- actions.push(AppAction::Audio(pages::audio::AudioMessage::SourceVolume(id, sb.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::Display {
- let sb = &mut self.app.display.brightness_spinbox;
- let old = sb.value;
- if sb.keyboard_input(event) {
- let new_val = sb.value;
- drop(sb);
- if new_val != old {
- self.handle_action(&AppAction::Display(pages::display::DisplayMessage::BrightnessSet(new_val as u32)));
- }
- self.needs_rebuild = true;
- return true;
- }
- }
- if self.app.current_page == Page::Status {
- let sb = &mut self.app.status.padding_spinbox;
- let old = sb.value;
- if sb.keyboard_input(event) {
- let new_val = sb.value;
- drop(sb);
- if new_val != old {
- self.handle_action(&AppAction::Status(pages::status::StatusMessage::SetPadding(new_val as u16)));
- }
- self.needs_rebuild = true;
- return true;
- }
- }
- if self.app.current_page == Page::Typefaces {
+
if event.state == clear_ui::widget::ElementState::Pressed {
let is_down = match (&event.logical_key, event.ctrl) {
(clear_ui::widget::Key::Character(c), true) if c == "n" || c == "N" => true,
@@ -2883,9 +2747,9 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
(clear_ui::widget::Key::Named(clear_ui::widget::NamedKey::ArrowUp), false) => true,
_ => false,
};
- if is_down && clear_ui::widget::focus::is_focused(&self.app.typeface.list_box.scroll_box) {
+ if is_down && clear_ui::widget::focus::is_focused(&self.app.interface.list_box.scroll_box) {
let next_idx_font_scroll = {
- let tf = &self.app.typeface;
+ let tf = &self.app.interface;
let query = tf.search_box.text.to_lowercase();
let matching_fonts: Vec<&String> = tf.all_fonts.iter()
.filter(|font| font.to_lowercase().contains(&query))
@@ -2919,14 +2783,14 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
};
if let Some((font, scroll_y)) = next_idx_font_scroll {
- self.app.typeface.list_box.set_scroll_y(scroll_y);
- self.handle_action(&AppAction::Typeface(pages::typeface::TypefaceMessage::SelectFont(font)));
+ self.app.interface.list_box.set_scroll_y(scroll_y);
+ self.handle_action(&AppAction::Interface(pages::interface::InterfaceMessage::SelectFont(font)));
self.needs_rebuild = true;
return true;
}
- } else if is_up && clear_ui::widget::focus::is_focused(&self.app.typeface.list_box.scroll_box) {
+ } else if is_up && clear_ui::widget::focus::is_focused(&self.app.interface.list_box.scroll_box) {
let next_idx_font_scroll = {
- let tf = &self.app.typeface;
+ let tf = &self.app.interface;
let query = tf.search_box.text.to_lowercase();
let matching_fonts: Vec<&String> = tf.all_fonts.iter()
.filter(|font| font.to_lowercase().contains(&query))
@@ -2960,8 +2824,8 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
};
if let Some((font, scroll_y)) = next_idx_font_scroll {
- self.app.typeface.list_box.set_scroll_y(scroll_y);
- self.handle_action(&AppAction::Typeface(pages::typeface::TypefaceMessage::SelectFont(font)));
+ self.app.interface.list_box.set_scroll_y(scroll_y);
+ self.handle_action(&AppAction::Interface(pages::interface::InterfaceMessage::SelectFont(font)));
self.needs_rebuild = true;
return true;
}
@@ -2971,106 +2835,106 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
let mut actions = Vec::new();
let mut consumed = false;
- let tf = &mut self.app.typeface;
+ let tf = &mut self.app.interface;
let tb = &mut tf.sans_box;
if tb.keyboard_input(event) {
if tb.take_change() {
- actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetSans(tb.text.clone())));
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetSans(tb.text.clone())));
}
consumed = true;
}
- let tb = &mut self.app.typeface.serif_box;
+ let tb = &mut self.app.interface.serif_box;
if tb.keyboard_input(event) {
if tb.take_change() {
- actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetSerif(tb.text.clone())));
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetSerif(tb.text.clone())));
}
consumed = true;
}
- let tb = &mut self.app.typeface.mono_box;
+ let tb = &mut self.app.interface.mono_box;
if tb.keyboard_input(event) {
if tb.take_change() {
- actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetMono(tb.text.clone())));
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetMono(tb.text.clone())));
}
consumed = true;
}
- let tb = &mut self.app.typeface.borders_box;
+ let tb = &mut self.app.interface.borders_box;
if tb.keyboard_input(event) {
if tb.take_change() {
- actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetBorders(tb.text.clone())));
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetBorders(tb.text.clone())));
}
consumed = true;
}
- let tb = &mut self.app.typeface.status_box;
+ let tb = &mut self.app.interface.status_box;
if tb.keyboard_input(event) {
if tb.take_change() {
- actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetStatus(tb.text.clone())));
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetStatus(tb.text.clone())));
}
consumed = true;
}
- let tb = &mut self.app.typeface.fuzzel_box;
+ let tb = &mut self.app.interface.fuzzel_box;
if tb.keyboard_input(event) {
if tb.take_change() {
- actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetFuzzel(tb.text.clone())));
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetFuzzel(tb.text.clone())));
}
consumed = true;
}
- let tb = &mut self.app.typeface.terminal_box;
+ let tb = &mut self.app.interface.terminal_box;
if tb.keyboard_input(event) {
if tb.take_change() {
- actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetTerminal(tb.text.clone())));
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetTerminal(tb.text.clone())));
}
consumed = true;
}
- let tb = &mut self.app.typeface.paginator_box;
+ let tb = &mut self.app.interface.paginator_box;
if tb.keyboard_input(event) {
if tb.take_change() {
- actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetPaginator(tb.text.clone())));
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetPaginator(tb.text.clone())));
}
consumed = true;
}
- let tb = &mut self.app.typeface.search_box;
+ let tb = &mut self.app.interface.search_box;
if tb.keyboard_input(event) {
if tb.take_change() {
- actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetSearch(tb.text.clone())));
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetSearch(tb.text.clone())));
}
consumed = true;
}
- let sb = &mut self.app.typeface.borders_size_box;
+ let sb = &mut self.app.interface.borders_size_box;
if sb.keyboard_input(event) {
- actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetBordersSize(sb.value)));
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetBordersSize(sb.value)));
consumed = true;
}
- let sb = &mut self.app.typeface.status_size_box;
+ let sb = &mut self.app.interface.status_size_box;
if sb.keyboard_input(event) {
- actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetStatusSize(sb.value)));
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetStatusSize(sb.value)));
consumed = true;
}
- let sb = &mut self.app.typeface.fuzzel_size_box;
+ let sb = &mut self.app.interface.fuzzel_size_box;
if sb.keyboard_input(event) {
- actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetFuzzelSize(sb.value)));
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetFuzzelSize(sb.value)));
consumed = true;
}
- let sb = &mut self.app.typeface.terminal_size_box;
+ let sb = &mut self.app.interface.terminal_size_box;
if sb.keyboard_input(event) {
- actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetTerminalSize(sb.value)));
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetTerminalSize(sb.value)));
consumed = true;
}
- let sb = &mut self.app.typeface.paginator_size_box;
+ let sb = &mut self.app.interface.paginator_size_box;
if sb.keyboard_input(event) {
- actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetPaginatorSize(sb.value)));
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetPaginatorSize(sb.value)));
consumed = true;
}
@@ -3082,6 +2946,177 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
return true;
}
}
+ if self.app.current_page == Page::Services {
+ let sb = &mut self.app.services.notifications_duration_spinbox;
+ let old = sb.value;
+ if sb.keyboard_input(event) {
+ let new_val = sb.value;
+ if new_val != old {
+ self.handle_action(&AppAction::Services(pages::services::ServicesMessage::SetNotificationsDuration(new_val)));
+ }
+ self.needs_rebuild = true;
+ return true;
+ }
+ let sb = &mut self.app.services.status_padding_spinbox;
+ let old = sb.value;
+ if sb.keyboard_input(event) {
+ let new_val = sb.value;
+ if new_val != old {
+ self.handle_action(&AppAction::Services(pages::services::ServicesMessage::StatusSetPadding(new_val as u16)));
+ }
+ self.needs_rebuild = true;
+ return true;
+ }
+ }
+ if self.app.current_page == Page::Input {
+ if self.app.input.rate_spinbox.keyboard_input(event) {
+ self.handle_action(&AppAction::Input(pages::input::InputMessage::ApplyRepeat));
+ self.needs_rebuild = true;
+ return true;
+ }
+ if self.app.input.delay_spinbox.keyboard_input(event) {
+ self.handle_action(&AppAction::Input(pages::input::InputMessage::ApplyRepeat));
+ self.needs_rebuild = true;
+ return true;
+ }
+ if self.app.input.scroll_friction_spinbox.keyboard_input(event) {
+ self.handle_action(&AppAction::Input(pages::input::InputMessage::ApplyScrollFriction));
+ self.needs_rebuild = true;
+ return true;
+ }
+ if self.app.input.scroll_speed_spinbox.keyboard_input(event) {
+ self.handle_action(&AppAction::Input(pages::input::InputMessage::ApplyScrollSpeed));
+ self.needs_rebuild = true;
+ return true;
+ }
+ if self.app.input.pointer_friction_spinbox.keyboard_input(event) {
+ self.handle_action(&AppAction::Input(pages::input::InputMessage::ApplyPointerFriction));
+ self.needs_rebuild = true;
+ return true;
+ }
+ if self.app.input.trackpad_friction_spinbox.keyboard_input(event) {
+ self.handle_action(&AppAction::Input(pages::input::InputMessage::ApplyTrackpadFriction));
+ self.needs_rebuild = true;
+ return true;
+ }
+ if self.app.input.trackpoint_accel_speed_spinbox.keyboard_input(event) {
+ self.handle_action(&AppAction::Input(pages::input::InputMessage::ApplyTrackpointAccelSpeed));
+ self.needs_rebuild = true;
+ return true;
+ }
+ if self.app.input.cursor_size_spinbox.keyboard_input(event) {
+ self.handle_action(&AppAction::Input(pages::input::InputMessage::ApplyCursorSize));
+ self.needs_rebuild = true;
+ return true;
+ }
+ }
+ if self.app.current_page == Page::Accounts {
+ let mut consumed = false;
+ if self.app.accounts.editing_oauth_creds {
+ let tb = &mut self.app.accounts.oauth_client_id_box;
+ if tb.keyboard_input(event) { consumed = true; }
+ let tb = &mut self.app.accounts.oauth_client_secret_box;
+ if tb.keyboard_input(event) { consumed = true; }
+ } else {
+ let tb = &mut self.app.accounts.email_box;
+ if tb.keyboard_input(event) {
+ consumed = true;
+ let email_val = tb.edit_buffer.trim().to_lowercase();
+ if email_val.ends_with("@gmail.com") {
+ self.app.accounts.imap_box.text = "imap.gmail.com:993".to_string();
+ self.app.accounts.imap_box.edit_buffer = "imap.gmail.com:993".to_string();
+ self.app.accounts.smtp_box.text = "smtp.gmail.com:465".to_string();
+ self.app.accounts.smtp_box.edit_buffer = "smtp.gmail.com:465".to_string();
+ } else if email_val.ends_with("@icloud.com") {
+ self.app.accounts.imap_box.text = "imap.mail.me.com:993".to_string();
+ self.app.accounts.imap_box.edit_buffer = "imap.mail.me.com:993".to_string();
+ self.app.accounts.smtp_box.text = "smtp.mail.me.com:587".to_string();
+ self.app.accounts.smtp_box.edit_buffer = "smtp.mail.me.com:587".to_string();
+ } else if email_val.ends_with("@outlook.com") || email_val.ends_with("@hotmail.com") {
+ self.app.accounts.imap_box.text = "outlook.office365.com:993".to_string();
+ self.app.accounts.imap_box.edit_buffer = "outlook.office365.com:993".to_string();
+ self.app.accounts.smtp_box.text = "smtp.office365.com:587".to_string();
+ self.app.accounts.smtp_box.edit_buffer = "smtp.office365.com:587".to_string();
+ }
+ }
+ let tb = &mut self.app.accounts.password_box;
+ if tb.keyboard_input(event) { consumed = true; }
+ let tb = &mut self.app.accounts.imap_box;
+ if tb.keyboard_input(event) { consumed = true; }
+ let tb = &mut self.app.accounts.smtp_box;
+ if tb.keyboard_input(event) { consumed = true; }
+ }
+
+ if consumed {
+ self.needs_rebuild = true;
+ return true;
+ }
+ }
+ if self.app.current_page == Page::Audio {
+ let mut actions = Vec::new();
+ for (i, sb) in self.app.audio.sink_spinboxes.iter_mut().enumerate() {
+ let old = sb.value;
+ if sb.keyboard_input(event) {
+ if sb.value != old {
+ let id = self.app.audio.sinks[i].id;
+ actions.push(AppAction::Audio(pages::audio::AudioMessage::SinkVolume(id, sb.value as f32 / 100.0)));
+ }
+ }
+ }
+ for (i, sb) in self.app.audio.source_spinboxes.iter_mut().enumerate() {
+ let old = sb.value;
+ if sb.keyboard_input(event) {
+ if sb.value != old {
+ let id = self.app.audio.sources[i].id;
+ actions.push(AppAction::Audio(pages::audio::AudioMessage::SourceVolume(id, sb.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::Display {
+ let sb = &mut self.app.display.brightness_spinbox;
+ let old = sb.value;
+ if sb.keyboard_input(event) {
+ let new_val = sb.value;
+ if new_val != old {
+ self.handle_action(&AppAction::Display(pages::display::DisplayMessage::BrightnessSet(new_val as u32)));
+ }
+ self.needs_rebuild = true;
+ return true;
+ }
+ let sb = &mut self.app.display.screensaver_timeout_spinbox;
+ let old = sb.value;
+ if sb.keyboard_input(event) {
+ let new_val = sb.value;
+ if new_val != old {
+ self.handle_action(&AppAction::Display(pages::display::DisplayMessage::SetScreensaverTimeout(new_val)));
+ }
+ self.needs_rebuild = true;
+ return true;
+ }
+ let (menu_changed, old_selected, new_selected) = {
+ let menu = &mut self.app.display.screensaver_style_menu;
+ let old = menu.selected;
+ let changed = menu.keyboard_input(event);
+ (changed, old, menu.selected)
+ };
+ if menu_changed {
+ if new_selected != old_selected {
+ self.handle_action(&AppAction::Display(pages::display::DisplayMessage::SetScreensaverStyle(new_selected)));
+ }
+ self.needs_rebuild = true;
+ return true;
+ }
+ }
+
+
if self.app.current_page == Page::Services {
let srv = &mut self.app.services;
if srv.list_box.keyboard_input(event) {
@@ -3101,6 +3136,40 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
self.needs_rebuild = true;
return true;
}
+ let (cpu_changed, old_cpu, new_cpu) = {
+ let menu = &mut hw.cpu_gov_menu;
+ let old = menu.selected;
+ let changed = menu.keyboard_input(event);
+ (changed, old, menu.selected)
+ };
+ if cpu_changed {
+ if new_cpu != old_cpu {
+ if new_cpu == 0 {
+ self.handle_action(&AppAction::Hardware(pages::hardware::HardwareMessage::SetCpuPerformance));
+ } else {
+ self.handle_action(&AppAction::Hardware(pages::hardware::HardwareMessage::SetCpuPowersave));
+ }
+ }
+ self.needs_rebuild = true;
+ return true;
+ }
+ let (gpu_changed, old_gpu, new_gpu) = {
+ let menu = &mut hw.gpu_gov_menu;
+ let old = menu.selected;
+ let changed = menu.keyboard_input(event);
+ (changed, old, menu.selected)
+ };
+ if gpu_changed {
+ if new_gpu != old_gpu {
+ if new_gpu == 0 {
+ self.handle_action(&AppAction::Hardware(pages::hardware::HardwareMessage::SetGpuDefault));
+ } else {
+ self.handle_action(&AppAction::Hardware(pages::hardware::HardwareMessage::SetGpuPowersave));
+ }
+ }
+ self.needs_rebuild = true;
+ return true;
+ }
}
if self.app.current_page == Page::Radios {
let net = &mut self.app.network;
@@ -3119,6 +3188,26 @@ fn main() {
let _guard = rt.enter();
let mut initial_page = Page::ALL[0];
+
+ // Try to load last_page from config
+ let config_path = "/home/lsgalante/.config/ccec/config.toml";
+ if let Ok(content) = std::fs::read_to_string(config_path) {
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if trimmed.starts_with("last_page") {
+ if let Some(val_str) = trimmed.split('=').nth(1) {
+ let last_page_val = val_str.trim().trim_matches('"').trim_matches('\'').trim().to_lowercase();
+ for page in Page::ALL {
+ if page.label().to_lowercase() == last_page_val {
+ initial_page = page;
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
let args: Vec<String> = std::env::args().collect();
if args.len() > 1 {
let arg = args.last().unwrap().to_lowercase();
diff --git a/src/pages/accounts.rs b/src/pages/accounts.rs
index 1507aee..1e75bce 100644
--- a/src/pages/accounts.rs
+++ b/src/pages/accounts.rs
@@ -1,6 +1,6 @@
-use crate::app::{AppAction, PageContent};
-use clear_ui::layout::{Section, PageLayoutBuilder, LayoutStrategy};
-use clear_ui::widget::{TextBox, Widget};
+use crate::app::{AppAction, PageContent, SectionContextExt};
+use clear_ui::layout::{PageLayoutBuilder, LayoutStrategy};
+use clear_ui::widget::{TextBox, Element};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
pub struct AccountInfo {
@@ -309,10 +309,9 @@ pub fn view(state: &mut AccountsState, cx: f32, cy: f32, cw: f32, ch: f32, layou
let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(2);
// ── Accounts Section ──
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec_accounts = Section::new(pc, rx, ry, sec_w, "Accounts");
+ builder.add_section(&mut final_pc, "Accounts", false, |sec_accounts| {
if !state.loaded {
- sec_accounts.text(pc, "Loading online accounts...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec_accounts.text("Loading online accounts...", 12.0, 0.0, 12.0, TEXT_DIM);
sec_accounts.spacing(18.0);
} else {
let row_h = 28.0;
@@ -320,7 +319,7 @@ pub fn view(state: &mut AccountsState, cx: f32, cy: f32, cw: f32, ch: f32, layou
let item_w = sec_w - 40.0;
if state.accounts.is_empty() {
- sec_accounts.text(pc, "No accounts configured.", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec_accounts.text("No accounts configured.", 12.0, 0.0, 12.0, TEXT_DIM);
sec_accounts.spacing(20.0);
} else {
for (idx, acc) in state.accounts.iter().enumerate() {
@@ -331,7 +330,7 @@ pub fn view(state: &mut AccountsState, cx: f32, cy: f32, cw: f32, ch: f32, layou
};
let is_selected = state.selected_idx == Some(idx) && !state.adding_new && !state.editing_oauth_creds;
let bg_col = if is_selected { [0.20, 0.40, 0.65, 0.4] } else { [0.10, 0.10, 0.16, 0.3] };
- pc.button(
+ sec_accounts.button(
&label,
sec_accounts.ax(12.0),
sec_accounts.ay(),
@@ -349,7 +348,7 @@ pub fn view(state: &mut AccountsState, cx: f32, cy: f32, cw: f32, ch: f32, layou
sec_accounts.spacing(12.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] };
- pc.button(
+ sec_accounts.button(
"Add Account",
sec_accounts.ax(12.0),
sec_accounts.ay(),
@@ -363,7 +362,7 @@ pub fn view(state: &mut AccountsState, cx: f32, cy: f32, cw: f32, ch: f32, layou
sec_accounts.spacing(row_h + 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] };
- pc.button(
+ sec_accounts.button(
"Google API Settings",
sec_accounts.ax(12.0),
sec_accounts.ay(),
@@ -380,7 +379,7 @@ pub fn view(state: &mut AccountsState, cx: f32, cy: f32, cw: f32, ch: f32, layou
if selected_idx < state.accounts.len() && !state.adding_new && !state.editing_oauth_creds {
let acc = &state.accounts[selected_idx];
if !acc.is_default {
- pc.button(
+ sec_accounts.button(
"Make Default",
sec_accounts.ax(12.0),
sec_accounts.ay(),
@@ -393,7 +392,7 @@ pub fn view(state: &mut AccountsState, cx: f32, cy: f32, cw: f32, ch: f32, layou
);
sec_accounts.spacing(row_h + row_gap);
}
- pc.button(
+ sec_accounts.button(
"Delete Account",
sec_accounts.ax(12.0),
sec_accounts.ay(),
@@ -408,52 +407,47 @@ pub fn view(state: &mut AccountsState, cx: f32, cy: f32, cw: f32, ch: f32, layou
}
}
}
- sec_accounts.finish(pc)
});
// ── Modify Accounts Section ──
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec_modify = Section::new(pc, rx, ry, sec_w, "Modify Accounts");
+ builder.add_section(&mut final_pc, "Modify Accounts", false, |sec_modify| {
let item_w = sec_w - 40.0;
let row_h = 28.0;
+ let rx = sec_modify.left;
if state.loaded {
if state.adding_new {
- sec_modify.text(pc, "Add New Account", 12.0, 0.0, 14.0, [0.35, 0.65, 0.90, 1.0]);
+ 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.text(pc, "Note: Gmail uses Google Login. iCloud requires App PW.", 12.0, 0.0, 11.0, TEXT_DIM);
+ 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;
// Email Address textbox
- let email_top = state.email_box.top_room();
state.email_box.set_row_rect(rx + 12.0, item_w);
- clear_ui::layout::render_widget(pc, &mut state.email_box, rx + 12.0, sec_modify.ay() + email_top, item_w, widget_h);
- sec_modify.spacing(widget_h + email_top + field_gap);
+ sec_modify.widget(&mut state.email_box, 12.0, item_w, widget_h);
+ sec_modify.spacing(field_gap);
// Password textbox
- let password_top = state.password_box.top_room();
state.password_box.set_row_rect(rx + 12.0, item_w);
- clear_ui::layout::render_widget(pc, &mut state.password_box, rx + 12.0, sec_modify.ay() + password_top, item_w, widget_h);
- sec_modify.spacing(widget_h + password_top + field_gap);
+ sec_modify.widget(&mut state.password_box, 12.0, item_w, widget_h);
+ sec_modify.spacing(field_gap);
// IMAP Server textbox
- let imap_top = state.imap_box.top_room();
state.imap_box.set_row_rect(rx + 12.0, item_w);
- clear_ui::layout::render_widget(pc, &mut state.imap_box, rx + 12.0, sec_modify.ay() + imap_top, item_w, widget_h);
- sec_modify.spacing(widget_h + imap_top + field_gap);
+ sec_modify.widget(&mut state.imap_box, 12.0, item_w, widget_h);
+ sec_modify.spacing(field_gap);
// SMTP Server textbox
- let smtp_top = state.smtp_box.top_room();
state.smtp_box.set_row_rect(rx + 12.0, item_w);
- clear_ui::layout::render_widget(pc, &mut state.smtp_box, rx + 12.0, sec_modify.ay() + smtp_top, item_w, widget_h);
- sec_modify.spacing(widget_h + smtp_top + field_gap);
+ sec_modify.widget(&mut state.smtp_box, 12.0, item_w, widget_h);
+ sec_modify.spacing(field_gap);
let helper_w = (item_w - 8.0) / 2.0;
- pc.button(
+ sec_modify.button(
"Login (Google)",
sec_modify.ax(12.0),
sec_modify.ay(),
@@ -464,7 +458,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::GoogleLoginInit),
);
- pc.button(
+ sec_modify.button(
"Login (iCloud)",
sec_modify.ax(12.0) + helper_w + 8.0,
sec_modify.ay(),
@@ -477,7 +471,7 @@ pub fn view(state: &mut AccountsState, cx: f32, cy: f32, cw: f32, ch: f32, layou
);
sec_modify.spacing(row_h + 16.0);
- pc.button(
+ sec_modify.button(
"Save Account",
sec_modify.ax(12.0),
sec_modify.ay(),
@@ -488,7 +482,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::AddAccountSave),
);
- pc.button(
+ sec_modify.button(
"Cancel",
sec_modify.ax(12.0) + helper_w + 8.0,
sec_modify.ay(),
@@ -501,31 +495,29 @@ pub fn view(state: &mut AccountsState, cx: f32, cy: f32, cw: f32, ch: f32, layou
);
sec_modify.spacing(row_h + 12.0);
} else if state.editing_oauth_creds {
- sec_modify.text(pc, "Google OAuth Credentials", 12.0, 0.0, 14.0, [0.35, 0.65, 0.90, 1.0]);
+ 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.text(pc, "Configures client ID & secret from your Google Cloud Console.", 12.0, 0.0, 11.0, TEXT_DIM);
+ 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.text(pc, "Required: Gmail API enabled & redirect URI set to http://127.0.0.1:8080", 12.0, 0.0, 11.0, TEXT_DIM);
+ 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;
// Client ID textbox
- let client_id_top = state.oauth_client_id_box.top_room();
state.oauth_client_id_box.set_row_rect(rx + 12.0, item_w);
- clear_ui::layout::render_widget(pc, &mut state.oauth_client_id_box, rx + 12.0, sec_modify.ay() + client_id_top, item_w, widget_h);
- sec_modify.spacing(widget_h + client_id_top + field_gap);
+ sec_modify.widget(&mut state.oauth_client_id_box, 12.0, item_w, widget_h);
+ sec_modify.spacing(field_gap);
// Client Secret textbox
- let client_secret_top = state.oauth_client_secret_box.top_room();
state.oauth_client_secret_box.set_row_rect(rx + 12.0, item_w);
- clear_ui::layout::render_widget(pc, &mut state.oauth_client_secret_box, rx + 12.0, sec_modify.ay() + client_secret_top, item_w, widget_h);
- sec_modify.spacing(widget_h + client_secret_top + field_gap);
+ sec_modify.widget(&mut state.oauth_client_secret_box, 12.0, item_w, widget_h);
+ sec_modify.spacing(field_gap);
let helper_w = (item_w - 8.0) / 2.0;
- pc.button(
+ sec_modify.button(
"Save Credentials",
sec_modify.ax(12.0),
sec_modify.ay(),
@@ -536,7 +528,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::EditOAuthCredsSave),
);
- pc.button(
+ sec_modify.button(
"Cancel",
sec_modify.ax(12.0) + helper_w + 8.0,
sec_modify.ay(),
@@ -552,24 +544,24 @@ pub fn view(state: &mut AccountsState, cx: f32, cy: f32, cw: f32, ch: f32, layou
if selected_idx < state.accounts.len() {
let acc = &state.accounts[selected_idx];
- sec_modify.text(pc, "Account Details", 12.0, 0.0, 14.0, [0.35, 0.65, 0.90, 1.0]);
+ 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.text(pc, &format!("Email Address: {}", acc.email), 12.0, 0.0, 12.0, [0.90, 0.90, 0.95, 1.0]);
+ 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);
let auth_type = if acc.is_oauth { "OAuth2 (Google)" } else { "Password-based" };
- sec_modify.text(pc, &format!("Authentication: {}", auth_type), 12.0, 0.0, 12.0, [0.83, 0.83, 0.83, 1.0]);
+ 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.text(pc, &format!("IMAP Server: {}", acc.imap), 12.0, 0.0, 12.0, [0.83, 0.83, 0.83, 1.0]);
+ 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.text(pc, &format!("SMTP Server: {}", acc.smtp), 12.0, 0.0, 12.0, [0.83, 0.83, 0.83, 1.0]);
+ 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);
if acc.is_oauth {
- pc.button(
+ sec_modify.button(
"Click to Login (Browser)",
sec_modify.ax(12.0),
sec_modify.ay(),
@@ -584,19 +576,17 @@ pub fn view(state: &mut AccountsState, cx: f32, cy: f32, cw: f32, ch: f32, layou
}
}
} else {
- sec_modify.text(pc, "Select an account to view details, or click Add Account.", 12.0, 0.0, 12.0, TEXT_DIM);
+ 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);
}
if let Some(ref msg) = state.status_msg {
sec_modify.spacing(12.0);
- sec_modify.text(pc, msg, 12.0, 0.0, 12.0, [0.56, 0.83, 0.56, 1.0]);
+ 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.finish(pc)
});
-
final_pc
}
diff --git a/src/pages/audio.rs b/src/pages/audio.rs
index 9a2ff9f..7f49f26 100644
--- a/src/pages/audio.rs
+++ b/src/pages/audio.rs
@@ -1,6 +1,6 @@
-use crate::app::{AppAction, PageContent};
-use clear_ui::layout::{render_widget, Section, PageLayoutBuilder, LayoutStrategy, GridLayout};
-use clear_ui::widget::{Spinbox, Widget};
+use crate::app::{AppAction, PageContent, SectionContextExt};
+use clear_ui::layout::{render_widget, PageLayoutBuilder, LayoutStrategy};
+use clear_ui::widget::{Spinbox, Element};
#[derive(Debug, Clone)]
pub struct AudioSink {
@@ -213,14 +213,12 @@ pub fn view(state: &mut AudioState, cx: f32, cy: f32, cw: f32, ch: f32, sec_focu
let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(2);
// ── Output section ──
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec = Section::new(pc, rx, ry, sec_w, "Output");
-
+ builder.add_section(&mut final_pc, "Output", sec_focused.first().copied().unwrap_or(false), |sec| {
if !state.loaded {
- sec.text(pc, "Loading output devices...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec.text("Loading output devices...", 12.0, 0.0, 12.0, TEXT_DIM);
sec.spacing(18.0);
} else if state.sinks.is_empty() {
- sec.text(pc, "No output devices found", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec.text("No output devices found", 12.0, 0.0, 12.0, TEXT_DIM);
sec.spacing(18.0);
}
@@ -234,16 +232,18 @@ pub fn view(state: &mut AudioState, cx: f32, cy: f32, cw: f32, ch: f32, sec_focu
format!("{} {:.0}%", sink.name, sink.volume * 100.0)
};
let lc = if sink.muted { RED } else { TEXT_FG };
- sec.text(pc, &label, 14.0, 0.0, 13.0, lc);
+ sec.text(&label, 14.0, 0.0, 13.0, lc);
sec.spacing(18.0);
if sink.active {
let bar_w = sec_w - 100.0;
let bar_x = 14.0;
let yt = sec.ay();
- pc.rect(BLANK_BAR, sec.ax(bar_x), yt, bar_w, 8.0);
- pc.rect(FILL_BAR, sec.ax(bar_x), yt, bar_w * sink.volume, 8.0);
- sec.text(pc, &format!("{:.0}%", sink.volume * 100.0), bar_x + bar_w + 8.0, -2.0, 11.0, TEXT_DIM);
+ let usage_bar_x = sec.ax(bar_x);
+ let mut usage_bar = clear_ui::widget::UsageBar::new(sink.volume)
+ .with_colors(FILL_BAR, BLANK_BAR);
+ render_widget(sec.pc, &mut usage_bar, usage_bar_x, yt, bar_w, 8.0);
+ sec.text(&format!("{:.0}%", sink.volume * 100.0), bar_x + bar_w + 8.0, -2.0, 11.0, TEXT_DIM);
let row_y = sec.ay() + 12.0;
let sb_w = 100.0;
@@ -251,13 +251,16 @@ pub fn view(state: &mut AudioState, cx: f32, cy: f32, cw: f32, ch: f32, sec_focu
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(sec.ax(8.0), sec_w - 16.0);
- render_widget(pc, &mut state.sink_spinboxes[idx], sec.ax(bar_x), row_y, sb_w, sb_h);
+ 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);
let mute_label = if sink.muted { "Unmute" } else { "Mute" };
let mute_col = if sink.muted { MUTED_BG } else { BTN_INACTIVE };
- pc.button(mute_label, sec.ax(bar_x) + sb_w + gap, row_y, mute_w, sb_h,
+ 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)));
@@ -267,19 +270,15 @@ pub fn view(state: &mut AudioState, cx: f32, cy: f32, cw: f32, ch: f32, sec_focu
}
}
}
-
- sec.finish_focused(pc, sec_focused.first().copied().unwrap_or(false))
});
// ── Input section ──
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec = Section::new(pc, rx, ry, sec_w, "Input");
-
+ builder.add_section(&mut final_pc, "Input", sec_focused.get(1).copied().unwrap_or(false), |sec| {
if !state.loaded {
- sec.text(pc, "Loading input devices...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec.text("Loading input devices...", 12.0, 0.0, 12.0, TEXT_DIM);
sec.spacing(18.0);
} else if state.sources.is_empty() {
- sec.text(pc, "No input devices found", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec.text("No input devices found", 12.0, 0.0, 12.0, TEXT_DIM);
sec.spacing(18.0);
}
@@ -293,16 +292,18 @@ pub fn view(state: &mut AudioState, cx: f32, cy: f32, cw: f32, ch: f32, sec_focu
format!("{} {:.0}%", src.name, src.volume * 100.0)
};
let lc = if src.muted { RED } else { TEXT_FG };
- sec.text(pc, &label, 14.0, 0.0, 13.0, lc);
+ sec.text(&label, 14.0, 0.0, 13.0, lc);
sec.spacing(18.0);
if src.active {
let bar_w = sec_w - 100.0;
let bar_x = 14.0;
let yt = sec.ay();
- pc.rect(BLANK_BAR, sec.ax(bar_x), yt, bar_w, 8.0);
- pc.rect(FILL_BAR, sec.ax(bar_x), yt, bar_w * src.volume, 8.0);
- sec.text(pc, &format!("{:.0}%", src.volume * 100.0), bar_x + bar_w + 8.0, -2.0, 11.0, TEXT_DIM);
+ let usage_bar_x = sec.ax(bar_x);
+ let mut usage_bar = clear_ui::widget::UsageBar::new(src.volume)
+ .with_colors(FILL_BAR, BLANK_BAR);
+ render_widget(sec.pc, &mut usage_bar, usage_bar_x, yt, bar_w, 8.0);
+ sec.text(&format!("{:.0}%", src.volume * 100.0), bar_x + bar_w + 8.0, -2.0, 11.0, TEXT_DIM);
let row_y = sec.ay() + 12.0;
let sb_w = 100.0;
@@ -310,13 +311,16 @@ pub fn view(state: &mut AudioState, cx: f32, cy: f32, cw: f32, ch: f32, sec_focu
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(sec.ax(8.0), sec_w - 16.0);
- render_widget(pc, &mut state.source_spinboxes[idx], sec.ax(bar_x), row_y, sb_w, sb_h);
+ 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);
let mute_label = if src.muted { "Unmute" } else { "Mute" };
let mute_col = if src.muted { MUTED_BG } else { BTN_INACTIVE };
- pc.button(mute_label, sec.ax(bar_x) + sb_w + gap, row_y, mute_w, sb_h,
+ 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)));
@@ -326,8 +330,6 @@ pub fn view(state: &mut AudioState, cx: f32, cy: f32, cw: f32, ch: f32, sec_focu
}
}
}
-
- sec.finish_focused(pc, sec_focused.get(1).copied().unwrap_or(false))
});
final_pc
@@ -371,6 +373,7 @@ pub fn update(state: &mut AudioState, msg: AudioMessage) {
#[cfg(test)]
mod tests {
use super::*;
+ use clear_ui::layout::GridLayout;
#[test]
fn test_view_layout_grid() {
diff --git a/src/pages/backup.rs b/src/pages/backup.rs
deleted file mode 100644
index 04a21d4..0000000
--- a/src/pages/backup.rs
+++ /dev/null
@@ -1,192 +0,0 @@
-use crate::app::{AppAction, PageContent};
-use clear_ui::layout::{Section, PageLayoutBuilder, LayoutStrategy};
-use std::fs;
-
-#[derive(Debug, Clone, Default)]
-pub struct BackupState {
- pub loaded: bool,
- pub in_progress: bool,
- pub last_backup_time: String,
- pub backup_size: String,
- pub error_message: Option<String>,
-}
-
-#[derive(Debug, Clone)]
-pub enum BackupMessage {
- Refreshed(BackupState),
- StartBackup,
- BackupFinished(Result<(String, String), String>),
-}
-
-fn status_path() -> String {
- format!("{}/.config/clear-system-interface/backup_status.txt", std::env::var("HOME").unwrap_or_default())
-}
-
-pub fn read_backup_status() -> (String, String, Option<String>) {
- let path_str = status_path();
- let content = fs::read_to_string(path_str).unwrap_or_default();
-
- let mut last_backup = "Never".to_string();
- let mut size = "0 B".to_string();
- let mut err_msg = None;
-
- for line in content.lines() {
- let trimmed = line.trim();
- if trimmed.starts_with("last_backup_time") {
- if let Some(val) = trimmed.split('=').nth(1) {
- last_backup = val.trim().to_string();
- }
- } else if trimmed.starts_with("backup_size") {
- if let Some(val) = trimmed.split('=').nth(1) {
- size = val.trim().to_string();
- }
- } else if trimmed.starts_with("error_message") {
- if let Some(val) = trimmed.split('=').nth(1) {
- let v = val.trim().to_string();
- if !v.is_empty() {
- err_msg = Some(v);
- }
- }
- }
- }
-
- (last_backup, size, err_msg)
-}
-
-pub async fn fetch_backup_state() -> BackupState {
- let (last_backup, size, err) = read_backup_status();
- BackupState {
- loaded: true,
- in_progress: false,
- last_backup_time: last_backup,
- backup_size: size,
- error_message: err,
- }
-}
-
-pub async fn run_backup() -> Result<(String, String), String> {
- // Run the backup system helper script via pkexec (graphical auth prompt)
- let output = tokio::process::Command::new("pkexec")
- .arg("/home/lsgalante/.local/share/clear-system-interface/helpers/backup-system.sh")
- .output()
- .await
- .map_err(|e| format!("Failed to run backup script: {}", e))?;
-
- if !output.status.success() {
- // Retrieve any specific error message written to the status file by the script
- let (_, _, err_msg) = read_backup_status();
- if let Some(msg) = err_msg {
- return Err(msg);
- }
- let err = String::from_utf8_lossy(&output.stderr).to_string();
- return Err(format!("Backup process failed: {}", err));
- }
-
- let (last_backup, size, _) = read_backup_status();
- Ok((last_backup, size))
-}
-
-const LABEL_FG: [f32; 4] = [0.56, 0.83, 0.56, 1.0];
-const TEXT_FG: [f32; 4] = [0.83, 0.83, 0.83, 1.0];
-const TEXT_DIM: [f32; 4] = [0.53, 0.53, 0.60, 1.0];
-const RED: [f32; 4] = [1.0, 0.33, 0.33, 1.0];
-const GREEN: [f32; 4] = [0.36, 0.56, 0.38, 1.0];
-const BTN_BG: [f32; 4] = [0.20, 0.40, 0.65, 1.0];
-const BTN_HOVER: [f32; 4] = [0.28, 0.50, 0.78, 1.0];
-const BTN_DISABLED: [f32; 4] = [0.15, 0.18, 0.22, 1.0];
-const WHITE: [f32; 4] = [1.0, 1.0, 1.0, 1.0];
-
-pub fn view(state: &BackupState, cx: f32, cy: f32, cw: f32, ch: f32, layout: &mut dyn LayoutStrategy) -> PageContent {
- let mut final_pc = PageContent::new();
- let sec_w = 320.0f32;
- let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(1);
-
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec = Section::new(pc, rx, ry, sec_w, "Full System Backup");
-
- if !state.loaded {
- sec.text(pc, "Loading backup state...", 12.0, 0.0, 12.0, TEXT_DIM);
- sec.spacing(18.0);
- } else {
- // Status Row
- sec.text(pc, "Backup Status", 12.0, 0.0, 12.0, LABEL_FG);
- let status_text = if state.in_progress { "Backing up..." } else { "Idle" };
- let status_color = if state.in_progress { GREEN } else { TEXT_FG };
- sec.text(pc, status_text, 120.0, 0.0, 12.0, status_color);
- sec.spacing(18.0);
-
- // Last Backup Row
- sec.text(pc, "Last Backup", 12.0, 0.0, 12.0, LABEL_FG);
- sec.text(pc, &state.last_backup_time, 120.0, 0.0, 12.0, TEXT_FG);
- sec.spacing(18.0);
-
- // Backup Size Row
- sec.text(pc, "Archive Size", 12.0, 0.0, 12.0, LABEL_FG);
- sec.text(pc, &state.backup_size, 120.0, 0.0, 12.0, TEXT_FG);
- sec.spacing(18.0);
-
- // Target Directories Row
- sec.text(pc, "Backup Targets", 12.0, 0.0, 12.0, LABEL_FG);
- sec.text(pc, "Entire Filesystem (/) [Preserving attributes]", 120.0, 0.0, 12.0, TEXT_DIM);
- sec.spacing(18.0);
-
- // Destination Archive Row
- sec.text(pc, "Destination", 12.0, 0.0, 12.0, LABEL_FG);
- sec.text(pc, "USB Drive (/mnt/usb or /run/media/...)", 120.0, 0.0, 12.0, TEXT_DIM);
- sec.spacing(24.0);
-
- // Error message if present
- if let Some(ref err) = state.error_message {
- sec.text(pc, "Error:", 12.0, 0.0, 12.0, RED);
- sec.text(pc, err, 60.0, 0.0, 11.0, RED);
- sec.spacing(18.0);
- }
-
- // Action Button
- let btn_w = 120.0;
- let btn_h = 32.0;
- let yt = sec.ay();
-
- let (btn_label, bg, hover, action) = if state.in_progress {
- ("Backing up...", BTN_DISABLED, BTN_DISABLED, AppAction::Backup(BackupMessage::StartBackup))
- } else {
- ("Run Backup", BTN_BG, BTN_HOVER, AppAction::Backup(BackupMessage::StartBackup))
- };
-
- sec.row(1, 0.0, btn_h, |_, x, _| {
- pc.button(btn_label, x, yt, btn_w, btn_h, bg, hover, WHITE, action.clone());
- });
- sec.spacing(12.0);
- }
- sec.finish(pc)
- });
-
- final_pc
-}
-
-pub fn update(state: &mut BackupState, msg: BackupMessage) {
- match msg {
- BackupMessage::Refreshed(new) => {
- let in_prog = state.in_progress;
- *state = new;
- state.in_progress = in_prog;
- }
- BackupMessage::StartBackup => {
- state.in_progress = true;
- state.error_message = None;
- }
- BackupMessage::BackupFinished(res) => {
- state.in_progress = false;
- match res {
- Ok((date, size)) => {
- state.last_backup_time = date;
- state.backup_size = size;
- state.error_message = None;
- }
- Err(err) => {
- state.error_message = Some(err);
- }
- }
- }
- }
-}
diff --git a/src/pages/display.rs b/src/pages/display.rs
index eca7ba9..d572001 100644
--- a/src/pages/display.rs
+++ b/src/pages/display.rs
@@ -1,6 +1,8 @@
-use crate::app::PageContent;
-use clear_ui::layout::{render_widget, Section, PageLayoutBuilder, LayoutStrategy, Subsection};
-use clear_ui::widget::{Spinbox, Label, Widget};
+use crate::app::{PageContent, SectionContextExt};
+use clear_ui::layout::{render_widget, PageLayoutBuilder, LayoutStrategy};
+use clear_ui::widget::{Spinbox, Label, Element, Toggle, Dropdown};
+
+const CONFIG_PATH: &str = "/home/lsgalante/.config/ccec/config.toml";
#[derive(Debug, Clone)]
pub struct DisplayOutput {
@@ -51,6 +53,15 @@ pub struct DisplayState {
pub night_light: bool,
pub brightness_spinbox: Spinbox,
pub night_light_label: Label,
+ // screensaver fields:
+ pub screensaver_enable: bool,
+ pub screensaver_enable_toggle: Toggle,
+ pub screensaver_timeout: i32,
+ pub screensaver_timeout_spinbox: Spinbox,
+ pub screensaver_lock_screen: bool,
+ pub screensaver_lock_screen_toggle: Toggle,
+ pub screensaver_style: String,
+ pub screensaver_style_menu: Dropdown,
}
impl Default for DisplayState {
@@ -63,6 +74,19 @@ impl Default for DisplayState {
night_light: false,
brightness_spinbox: Spinbox::new(50, 0, 100, 5).with_unit("%"),
night_light_label: Label::new("Night Light: OFF").with_font_size(13.0).with_color([0xd4, 0xd4, 0xd4]),
+ screensaver_enable: true,
+ screensaver_enable_toggle: Toggle::new().with_label("Enable Screensaver"),
+ screensaver_timeout: 10,
+ screensaver_timeout_spinbox: Spinbox::new(10, 1, 120, 1)
+ .with_label("Screensaver Timeout")
+ .with_unit("m"),
+ screensaver_lock_screen: true,
+ screensaver_lock_screen_toggle: Toggle::new().with_label("Lock Screen on Activation"),
+ screensaver_style: "starfield".to_string(),
+ screensaver_style_menu: Dropdown::new(
+ vec!["Blank".to_string(), "Starfield".to_string(), "Matrix Rain".to_string()],
+ 1,
+ ).with_label("Screensaver Style"),
}
}
}
@@ -71,6 +95,159 @@ impl Default for DisplayState {
pub enum DisplayMessage {
Refreshed(DisplayState),
BrightnessSet(u32),
+ ToggleScreensaverEnable,
+ ToggleScreensaverLockScreen,
+ SetScreensaverTimeout(i32),
+ SetScreensaverStyle(usize),
+ StartScreensaverPreview,
+}
+
+fn parse_screensaver_enable(content: &str) -> bool {
+ let mut in_section = false;
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if trimmed == "[screensaver]" {
+ in_section = true;
+ continue;
+ }
+ if trimmed.starts_with('[') && in_section {
+ break;
+ }
+ if in_section && trimmed.starts_with("enable") {
+ if let Some(val) = trimmed.split('=').nth(1) {
+ return val.trim() == "true";
+ }
+ }
+ }
+ true // default to true
+}
+
+fn parse_screensaver_lock_screen(content: &str) -> bool {
+ let mut in_section = false;
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if trimmed == "[screensaver]" {
+ in_section = true;
+ continue;
+ }
+ if trimmed.starts_with('[') && in_section {
+ break;
+ }
+ if in_section && trimmed.starts_with("lock_screen") {
+ if let Some(val) = trimmed.split('=').nth(1) {
+ return val.trim() == "true";
+ }
+ }
+ }
+ true // default to true
+}
+
+fn parse_screensaver_timeout(content: &str) -> i32 {
+ let mut in_section = false;
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if trimmed == "[screensaver]" {
+ in_section = true;
+ continue;
+ }
+ if trimmed.starts_with('[') && in_section {
+ break;
+ }
+ if in_section && trimmed.starts_with("timeout") {
+ if let Some(val) = trimmed.split('=').nth(1) {
+ if let Ok(t) = val.trim().parse::<i32>() {
+ return t;
+ }
+ }
+ }
+ }
+ 10 // default to 10 minutes
+}
+
+fn parse_screensaver_style(content: &str) -> String {
+ let mut in_section = false;
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if trimmed == "[screensaver]" {
+ in_section = true;
+ continue;
+ }
+ if trimmed.starts_with('[') && in_section {
+ break;
+ }
+ if in_section && trimmed.starts_with("style") {
+ if let Some(val) = trimmed.split('=').nth(1) {
+ return val.trim().trim_matches('"').to_string();
+ }
+ }
+ }
+ "starfield".to_string() // default to starfield
+}
+
+pub fn write_config_value(key: &str, value: &str) {
+ let content = std::fs::read_to_string(CONFIG_PATH).unwrap_or_default();
+ let new_line = format!("{} = {}", key, value);
+
+ let mut found = false;
+ let mut updated_lines = Vec::new();
+ let mut in_section = false;
+
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if trimmed == "[screensaver]" {
+ in_section = true;
+ updated_lines.push(line.to_string());
+ continue;
+ }
+ if trimmed.starts_with('[') && in_section {
+ in_section = false;
+ }
+ if in_section && trimmed.starts_with(key) {
+ found = true;
+ updated_lines.push(new_line.clone());
+ } else {
+ updated_lines.push(line.to_string());
+ }
+ }
+
+ let mut updated = updated_lines.join("\n");
+
+ if !found {
+ let mut result = String::new();
+ let has_section = content.lines().any(|l| l.trim() == "[screensaver]");
+ if has_section {
+ let mut in_section = false;
+ let mut inserted = false;
+ for line in updated.lines() {
+ if line.trim() == "[screensaver]" {
+ in_section = true;
+ result.push_str(line);
+ result.push('\n');
+ continue;
+ }
+ if line.trim().starts_with('[') && in_section {
+ if !inserted {
+ result.push_str(&new_line);
+ result.push('\n');
+ inserted = true;
+ }
+ in_section = false;
+ }
+ result.push_str(line);
+ result.push('\n');
+ }
+ if !inserted {
+ result.push_str(&new_line);
+ result.push('\n');
+ }
+ updated = result;
+ } else {
+ updated.push_str("\n[screensaver]\n");
+ updated.push_str(&new_line);
+ updated.push_str("\n");
+ }
+ }
+ let _ = std::fs::write(CONFIG_PATH, updated);
}
pub async fn fetch_display_state() -> DisplayState {
@@ -80,6 +257,20 @@ pub async fn fetch_display_state() -> DisplayState {
let pct = if max_brightness > 0.0 {
(brightness / max_brightness * 100.0).round() as i32
} else { 50 };
+
+ let content = std::fs::read_to_string(CONFIG_PATH).unwrap_or_default();
+ let screensaver_enable = parse_screensaver_enable(&content);
+ let screensaver_timeout = parse_screensaver_timeout(&content);
+ let screensaver_lock_screen = parse_screensaver_lock_screen(&content);
+ let screensaver_style = parse_screensaver_style(&content);
+
+ let style_idx = match screensaver_style.to_lowercase().as_str() {
+ "blank" => 0,
+ "starfield" => 1,
+ "matrix" => 2,
+ _ => 1, // default to Starfield
+ };
+
DisplayState {
loaded: true,
brightness, max_brightness, outputs, night_light,
@@ -87,6 +278,19 @@ pub async fn fetch_display_state() -> DisplayState {
night_light_label: Label::new(if night_light { "Night Light: ON" } else { "Night Light: OFF" })
.with_font_size(13.0)
.with_color([0xd4, 0xd4, 0xd4]),
+ screensaver_enable,
+ screensaver_enable_toggle: Toggle::new().with_label("Enable Screensaver"),
+ screensaver_timeout,
+ screensaver_timeout_spinbox: Spinbox::new(screensaver_timeout, 1, 120, 1)
+ .with_label("Screensaver Timeout")
+ .with_unit("m"),
+ screensaver_lock_screen,
+ screensaver_lock_screen_toggle: Toggle::new().with_label("Lock Screen on Activation"),
+ screensaver_style: screensaver_style.clone(),
+ screensaver_style_menu: Dropdown::new(
+ vec!["Blank".to_string(), "Starfield".to_string(), "Matrix Rain".to_string()],
+ style_idx,
+ ).with_label("Screensaver Style"),
}
}
@@ -166,22 +370,23 @@ fn spawn_brightness(pct: u32) {
.args(["set", &format!("{}%", pct), "-n"]).spawn();
}
-const TEXT_FG: [f32; 4] = [0.83, 0.83, 0.83, 1.0];
const TEXT_DIM: [f32; 4] = [0.53, 0.53, 0.60, 1.0];
const BLANK_BAR: [f32; 4] = [0.15, 0.15, 0.24, 1.0];
const FILL_BAR: [f32; 4] = [0.30, 0.50, 0.32, 1.0];
+const BTN_BG: [f32; 4] = [0.20, 0.40, 0.65, 1.0];
+const BTN_HOVER: [f32; 4] = [0.28, 0.50, 0.78, 1.0];
+const WHITE: [f32; 4] = [1.0, 1.0, 1.0, 1.0];
+
pub fn view(state: &mut DisplayState, cx: f32, cy: f32, cw: f32, ch: f32, layout: &mut dyn LayoutStrategy) -> PageContent {
let mut final_pc = PageContent::new();
let sec_w = 320.0f32;
- let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(3);
+ let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(4);
// ── Brightness ──
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec = Section::new(pc, rx, ry, sec_w, "Brightness");
-
+ builder.add_section(&mut final_pc, "Brightness", false, |sec| {
if !state.loaded {
- sec.text(pc, "Loading display settings...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec.text("Loading display settings...", 12.0, 0.0, 12.0, TEXT_DIM);
sec.spacing(18.0);
} else {
let bright_pct = if state.max_brightness > 0.0 {
@@ -190,65 +395,93 @@ pub fn view(state: &mut DisplayState, cx: f32, cy: f32, cw: f32, ch: f32, layout
let bar_w = sec_w - 100.0;
let yt = sec.ay();
- pc.rect(BLANK_BAR, sec.ax(12.0), yt, bar_w, 8.0);
- pc.rect(FILL_BAR, sec.ax(12.0), yt, bar_w * bright_pct as f32 / 100.0, 8.0);
- pc.text(&format!("{}%", bright_pct), sec.ax(16.0 + bar_w), yt - 2.0, 11.0, TEXT_DIM);
- sec.content_y += 14.0;
+ let usage_bar_x = sec.ax(12.0);
+ let mut usage_bar = clear_ui::widget::UsageBar::new(bright_pct as f32 / 100.0)
+ .with_colors(FILL_BAR, BLANK_BAR);
+ render_widget(sec.pc, &mut usage_bar, usage_bar_x, yt, bar_w, 8.0);
+ sec.text(&format!("{}%", bright_pct), 16.0 + bar_w, -2.0, 11.0, TEXT_DIM);
+ sec.spacing(14.0);
let yt = sec.ay();
let sb_w = 100.0;
let sb_h = 26.0;
state.brightness_spinbox.value = bright_pct;
- state.brightness_spinbox.set_row_rect(sec.ax(8.0), sec_w - 16.0);
- render_widget(pc, &mut state.brightness_spinbox, sec.ax(12.0), yt, sb_w, sb_h);
- sec.content_y += sb_h + 12.0;
+ let row_rect_x = sec.ax(8.0);
+ state.brightness_spinbox.set_row_rect(row_rect_x, sec_w - 16.0);
+ let sb_x = sec.ax(12.0);
+ render_widget(sec.pc, &mut state.brightness_spinbox, sb_x, yt, sb_w, sb_h);
+ sec.spacing(sb_h + 12.0);
}
- sec.finish(pc)
});
// ── Night Light ──
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec = Section::new(pc, rx, ry, sec_w, "Night Light");
+ builder.add_section(&mut final_pc, "Night Light", false, |sec| {
if !state.loaded {
- sec.text(pc, "Loading...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec.text("Loading...", 12.0, 0.0, 12.0, TEXT_DIM);
sec.spacing(18.0);
} else {
let nl_label = if state.night_light { "Night Light: ON" } else { "Night Light: OFF" };
state.night_light_label.set_text(nl_label);
- sec.widget(pc, &mut state.night_light_label, 12.0, sec_w - 24.0, 20.0);
+ sec.widget(&mut state.night_light_label, 12.0, sec_w - 24.0, 20.0);
sec.spacing(8.0);
}
- sec.finish(pc)
});
// ── Outputs ──
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec = Section::new(pc, rx, ry, sec_w, "Outputs");
-
+ builder.add_section(&mut final_pc, "Outputs", false, |sec| {
if !state.loaded {
- sec.text(pc, "Loading outputs...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec.text("Loading outputs...", 12.0, 0.0, 12.0, TEXT_DIM);
sec.spacing(18.0);
} else {
for out in &mut state.outputs {
- let mut subsec = Subsection::new(
- pc,
- sec.ax(0.0) + Section::ROW_PADDING_X,
- sec.content_y,
- sec.cw - 2.0 * Section::ROW_PADDING_X,
- &out.name,
- );
-
- subsec.widget(pc, &mut out.resolution_label, 12.0, 240.0, 20.0);
-
- if let Some(ref mut scale_lbl) = out.scale_label {
- subsec.widget(pc, scale_lbl, 12.0, 240.0, 20.0);
- }
-
- let sub_h = subsec.finish(pc);
- sec.content_y = sub_h;
+ sec.add_subsection(&out.name, false, |subsec| {
+ subsec.widget(&mut out.resolution_label, 12.0, 240.0, 20.0);
+ if let Some(ref mut scale_lbl) = out.scale_label {
+ subsec.widget(scale_lbl, 12.0, 240.0, 20.0);
+ }
+ });
}
}
- sec.finish(pc)
+ });
+
+ // ── Screensaver Settings ──
+ builder.add_section(&mut final_pc, "Screensaver Settings", false, |sec| {
+ let toggle_w = 48.0;
+ let toggle_h = 24.0;
+
+ state.screensaver_enable_toggle.set_toggled(state.screensaver_enable);
+ sec.widget(&mut state.screensaver_enable_toggle, 14.0, toggle_w, toggle_h);
+ sec.spacing(8.0);
+
+ state.screensaver_lock_screen_toggle.set_toggled(state.screensaver_lock_screen);
+ sec.widget(&mut state.screensaver_lock_screen_toggle, 14.0, toggle_w, toggle_h);
+ sec.spacing(16.0);
+
+ state.screensaver_timeout_spinbox.value = state.screensaver_timeout;
+ sec.widget(&mut state.screensaver_timeout_spinbox, 14.0, 200.0, 26.0);
+ sec.spacing(16.0);
+
+ sec.widget(&mut state.screensaver_style_menu, 14.0, 200.0, 26.0);
+ sec.spacing(24.0);
+
+ let btn_w = 160.0;
+ let btn_h = 32.0;
+ let btn_y = sec.ay();
+ let cols = sec.row_layout(1, 0.0);
+ if let Some(&(x, _)) = cols.first() {
+ sec.button(
+ "Preview Screensaver",
+ x,
+ btn_y,
+ btn_w,
+ btn_h,
+ BTN_BG,
+ BTN_HOVER,
+ WHITE,
+ crate::app::AppAction::Display(DisplayMessage::StartScreensaverPreview),
+ );
+ }
+ sec.spacing(12.0);
});
final_pc
@@ -268,10 +501,20 @@ pub fn update(state: &mut DisplayState, msg: DisplayMessage) {
out.scale_label.as_ref().map(|l| l.hovered()).unwrap_or(false)
));
}
+
+ let enable_hover = state.screensaver_enable_toggle.hovered();
+ let lock_hover = state.screensaver_lock_screen_toggle.hovered();
+ let timeout_hover = state.screensaver_timeout_spinbox.hovered();
+ let style_hover = state.screensaver_style_menu.hovered();
*state = new;
state.night_light_label.set_hovered(was_nl_hovered);
+ state.screensaver_enable_toggle.set_hovered(enable_hover);
+ state.screensaver_lock_screen_toggle.set_hovered(lock_hover);
+ state.screensaver_timeout_spinbox.set_hovered(timeout_hover);
+ state.screensaver_style_menu.set_hovered(style_hover);
+
for out in &mut state.outputs {
if let Some(&(name_h, res_h, scale_h)) = hovers.get(&out.name) {
out.name_label.set_hovered(name_h);
@@ -288,6 +531,48 @@ pub fn update(state: &mut DisplayState, msg: DisplayMessage) {
spawn_brightness(pct);
state.brightness_spinbox.value = pct as i32;
}
+ DisplayMessage::ToggleScreensaverEnable => {
+ state.screensaver_enable = !state.screensaver_enable;
+ write_config_value("enable", &state.screensaver_enable.to_string());
+ }
+ DisplayMessage::ToggleScreensaverLockScreen => {
+ state.screensaver_lock_screen = !state.screensaver_lock_screen;
+ write_config_value("lock_screen", &state.screensaver_lock_screen.to_string());
+ }
+ DisplayMessage::SetScreensaverTimeout(t) => {
+ state.screensaver_timeout = t;
+ write_config_value("timeout", &state.screensaver_timeout.to_string());
+ }
+ DisplayMessage::SetScreensaverStyle(idx) => {
+ state.screensaver_style_menu.selected = idx;
+ let val = match idx {
+ 0 => "blank",
+ 1 => "starfield",
+ 2 => "matrix",
+ _ => "starfield",
+ };
+ state.screensaver_style = val.to_string();
+ write_config_value("style", &format!("\"{}\"", val));
+ }
+ DisplayMessage::StartScreensaverPreview => {
+ let style_flag = match state.screensaver_style_menu.selected {
+ 0 => "blank",
+ 1 => "starfield",
+ 2 => "matrix",
+ _ => "starfield",
+ };
+
+ // Spawn screensaver tool from PATH or local directory
+ std::process::Command::new("/home/lsgalante/Dropbox/Clear/cce-screenaver/target/debug/cce-screenaver")
+ .arg(style_flag)
+ .spawn()
+ .or_else(|_| {
+ std::process::Command::new("cce-screenaver")
+ .arg(style_flag)
+ .spawn()
+ })
+ .ok();
+ }
}
}
diff --git a/src/pages/hardware.rs b/src/pages/hardware.rs
index 26013c5..7145765 100644
--- a/src/pages/hardware.rs
+++ b/src/pages/hardware.rs
@@ -1,6 +1,6 @@
-use crate::app::{AppAction, PageContent};
-use clear_ui::layout::{Section, PageLayoutBuilder, LayoutStrategy};
-use clear_ui::widget::{Label, ScrollingList};
+use crate::app::{AppAction, PageContent, SectionContextExt};
+use clear_ui::layout::{render_widget, PageLayoutBuilder, LayoutStrategy};
+use clear_ui::widget::{Label, ScrollingList, Dropdown, InfoBox};
#[derive(Debug, Clone, Default)]
pub struct BatteryInfo {
@@ -23,6 +23,8 @@ pub struct HardwareState {
pub gpus: Vec<String>,
pub loaded: bool,
pub cpu_label: Label,
+ pub cpu_usage_label: Label,
+ pub cpu_temp_label: Label,
pub gpu_labels: Vec<Label>,
pub processes: Vec<(String, String, String)>, // (pid, cpu, comm)
pub cpu_list_box: ScrollingList,
@@ -32,6 +34,8 @@ pub struct HardwareState {
pub on_ac: bool,
pub cpu_powersave: bool,
pub gpu_powersave: bool,
+ pub cpu_gov_menu: Dropdown,
+ pub gpu_gov_menu: Dropdown,
}
impl Default for HardwareState {
@@ -43,6 +47,8 @@ impl Default for HardwareState {
gpus: Vec::new(),
loaded: false,
cpu_label: Label::new("CPU Info"),
+ cpu_usage_label: Label::new("CPU Usage"),
+ cpu_temp_label: Label::new("CPU Temp"),
gpu_labels: Vec::new(),
processes: Vec::new(),
cpu_list_box: ScrollingList::new(24.0, 2.0),
@@ -51,6 +57,14 @@ impl Default for HardwareState {
on_ac: true,
cpu_powersave: false,
gpu_powersave: false,
+ cpu_gov_menu: Dropdown::new(
+ vec!["Performance".to_string(), "Powersave".to_string()],
+ 0,
+ ).with_label("CPU Governor"),
+ gpu_gov_menu: Dropdown::new(
+ vec!["Default (80W)".to_string(), "Eco Cap (5W)".to_string()],
+ 0,
+ ).with_label("GPU Power Limit"),
}
}
}
@@ -307,8 +321,9 @@ pub async fn fetch_hardware_state() -> HardwareState {
let tp_gpu_temp = read_thinkpad_gpu_temp();
let nv_gpu_temp = read_nvidia_gpu_temp().await;
- let cpu_temp_str = cpu_temp.map(|t| format!(" — {:.0}°C", t)).unwrap_or_default();
- let cpu_label_text = format!("CPU {} ({} cores) — {:.0}%{}", cpu_model, cpu_cores, cpu_usage, cpu_temp_str);
+ let cpu_label_text = format!("CPU {} ({} cores)", cpu_model, cpu_cores);
+ let cpu_usage_text = format!("Usage {:.0}%", cpu_usage);
+ let cpu_temp_text = cpu_temp.map(|t| format!("Temp {:.0}°C", t)).unwrap_or_else(|| "Temp N/A".to_string());
let gpu_labels = gpus.iter().map(|gpu_name| {
let temp = if gpu_name.to_lowercase().contains("nvidia") {
@@ -332,6 +347,8 @@ pub async fn fetch_hardware_state() -> HardwareState {
gpus,
loaded: true,
cpu_label: Label::new(&cpu_label_text).with_font_size(12.0).with_color([212, 212, 212]),
+ cpu_usage_label: Label::new(&cpu_usage_text).with_font_size(12.0).with_color([212, 212, 212]),
+ cpu_temp_label: Label::new(&cpu_temp_text).with_font_size(12.0).with_color([212, 212, 212]),
gpu_labels,
processes,
cpu_list_box: ScrollingList::new(24.0, 2.0),
@@ -339,16 +356,20 @@ pub async fn fetch_hardware_state() -> HardwareState {
on_ac,
cpu_powersave,
gpu_powersave,
+ cpu_gov_menu: Dropdown::new(
+ vec!["Performance".to_string(), "Powersave".to_string()],
+ if cpu_powersave { 1 } else { 0 },
+ ).with_label("CPU Governor"),
+ gpu_gov_menu: Dropdown::new(
+ vec!["Default (80W)".to_string(), "Eco Cap (5W)".to_string()],
+ if gpu_powersave { 1 } else { 0 },
+ ).with_label("GPU Power Limit"),
}
}
const TEXT_FG: [f32; 4] = [0.83, 0.83, 0.83, 1.0];
const TEXT_DIM: [f32; 4] = [0.53, 0.53, 0.60, 1.0];
const ACCENT: [f32; 4] = [0.36, 0.56, 0.38, 1.0];
-const BTN_ACTIVE: [f32; 4] = [0.20, 0.40, 0.22, 1.0];
-const BTN_INACTIVE: [f32; 4] = [0.13, 0.18, 0.14, 1.0];
-const BTN_HOVER: [f32; 4] = [0.25, 0.30, 0.26, 1.0];
-const WHITE: [f32; 4] = [1.0, 1.0, 1.0, 1.0];
const RED: [f32; 4] = [1.0, 0.33, 0.33, 1.0];
const ORANGE: [f32; 4] = [1.0, 0.73, 0.20, 1.0];
@@ -358,33 +379,41 @@ 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(&mut final_pc, |pc, rx, ry| {
- let mut sec = Section::new(pc, rx, ry, sec_w, "CPU");
+ builder.add_section_with_width(&mut final_pc, sec_w * 2.0 + 20.0, "CPU", root_focused, |sec| {
+ let rx = sec.left;
if !state.loaded {
- sec.text(pc, "Loading CPU model and utilization...", 12.0, 0.0, 12.0, TEXT_FG);
+ sec.text("Loading CPU model and utilization...", 12.0, 0.0, 12.0, TEXT_FG);
sec.spacing(10.0);
} else {
// CPU Info Label
- sec.widget(pc, &mut state.cpu_label, 12.0, sec_w - 24.0, 26.0);
+ sec.widget(&mut state.cpu_label, 12.0, sec.cw - 24.0, 26.0);
+ sec.spacing(12.0);
+
+ // CPU Usage Label
+ sec.widget(&mut state.cpu_usage_label, 12.0, sec.cw - 24.0, 26.0);
+ sec.spacing(12.0);
+
+ // CPU Temp Label
+ sec.widget(&mut state.cpu_temp_label, 12.0, sec.cw - 24.0, 26.0);
sec.spacing(12.0);
// Scrolling box configuration for process list
let list_box_x = rx + 12.0;
let list_box_y = sec.ay();
- let list_box_w = sec_w - 24.0;
+ let list_box_w = sec.cw - 24.0;
let list_box_h = 220.0;
// Render the standardized ScrollBox widget
- clear_ui::layout::render_widget(pc, &mut state.cpu_list_box, list_box_x, list_box_y, list_box_w, list_box_h);
+ render_widget(sec.pc, &mut state.cpu_list_box, list_box_x, list_box_y, list_box_w, list_box_h);
// Header for process list columns (drawn static on top of the ScrollBox background)
let header_h = 22.0;
- pc.rect([0.12, 0.12, 0.16, 0.5], list_box_x + 1.0, list_box_y + 1.0, list_box_w - 2.0, header_h);
- pc.rect([0.18, 0.18, 0.24, 1.0], list_box_x + 1.0, list_box_y + header_h, list_box_w - 2.0, 1.0); // Divider
+ sec.pc.rect([0.12, 0.12, 0.16, 0.5], list_box_x + 1.0, list_box_y + 1.0, list_box_w - 2.0, header_h);
+ sec.pc.rect([0.18, 0.18, 0.24, 1.0], list_box_x + 1.0, list_box_y + header_h, list_box_w - 2.0, 1.0); // Divider
- pc.text("PID", list_box_x + 12.0, list_box_y + 5.0, 11.0, [0.53, 0.53, 0.60, 1.0]);
- pc.text("COMMAND", list_box_x + 80.0, list_box_y + 5.0, 11.0, [0.53, 0.53, 0.60, 1.0]);
- pc.text("CPU %", list_box_x + list_box_w - 60.0, list_box_y + 5.0, 11.0, [0.53, 0.53, 0.60, 1.0]);
+ sec.pc.text("PID", list_box_x + 12.0, list_box_y + 5.0, 11.0, [0.53, 0.53, 0.60, 1.0]);
+ sec.pc.text("COMMAND", list_box_x + 80.0, list_box_y + 5.0, 11.0, [0.53, 0.53, 0.60, 1.0]);
+ sec.pc.text("CPU %", list_box_x + list_box_w - 60.0, list_box_y + 5.0, 11.0, [0.53, 0.53, 0.60, 1.0]);
let row_h = 24.0;
// Update ScrollingList bounds for the scrollable viewport (which starts below the header)
@@ -394,7 +423,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)
- pc.button(
+ sec.button(
"",
list_box_x + 2.0,
draw_y,
@@ -406,41 +435,37 @@ pub fn view(state: &mut HardwareState, cx: f32, cy: f32, cw: f32, ch: f32, root_
AppAction::Hardware(HardwareMessage::None),
);
- pc.text(pid, list_box_x + 12.0, draw_y + 6.0, 12.0, [0.80, 0.80, 0.85, 1.0]);
- pc.text(comm, list_box_x + 80.0, draw_y + 6.0, 12.0, [0.80, 0.80, 0.85, 1.0]);
- pc.text(&format!("{}%", cpu), list_box_x + list_box_w - 60.0, draw_y + 6.0, 12.0, [0.56, 0.83, 0.56, 1.0]);
+ sec.pc.text(pid, list_box_x + 12.0, draw_y + 6.0, 12.0, [0.80, 0.80, 0.85, 1.0]);
+ sec.pc.text(comm, list_box_x + 80.0, draw_y + 6.0, 12.0, [0.80, 0.80, 0.85, 1.0]);
+ sec.pc.text(&format!("{}%", cpu), list_box_x + list_box_w - 60.0, draw_y + 6.0, 12.0, [0.56, 0.83, 0.56, 1.0]);
}
}
if state.processes.is_empty() {
- pc.text("No active processes", list_box_x + 12.0, list_box_y + header_h + 16.0, 12.0, TEXT_DIM);
+ sec.pc.text("No active processes", list_box_x + 12.0, list_box_y + header_h + 16.0, 12.0, TEXT_DIM);
}
sec.content_y += list_box_h;
}
- sec.finish_focused(pc, root_focused)
});
// ── GPU Section ──
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec_gpu = Section::new(pc, rx, ry, sec_w, "GPU");
+ builder.add_section(&mut final_pc, "GPU", false, |sec_gpu| {
if !state.loaded {
- sec_gpu.text(pc, "Loading GPU models...", 12.0, 0.0, 12.0, TEXT_FG);
+ sec_gpu.text("Loading GPU models...", 12.0, 0.0, 12.0, TEXT_FG);
sec_gpu.spacing(10.0);
} else {
for (i, gpu_lbl) in state.gpu_labels.iter_mut().enumerate() {
if i > 0 { sec_gpu.spacing(12.0); }
- sec_gpu.widget(pc, gpu_lbl, 12.0, sec_w - 24.0, 26.0);
+ sec_gpu.widget(gpu_lbl, 12.0, sec_gpu.cw - 24.0, 26.0);
}
}
- sec_gpu.finish(pc)
});
// ── Battery Section ──
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec_bat = Section::new(pc, rx, ry, sec_w, "Battery");
+ builder.add_section(&mut final_pc, "Battery", false, |sec_bat| {
if !state.loaded {
- sec_bat.text(pc, "Loading battery status...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec_bat.text("Loading battery status...", 12.0, 0.0, 12.0, TEXT_DIM);
sec_bat.spacing(18.0);
} else {
let bat = &state.battery;
@@ -455,12 +480,12 @@ pub fn view(state: &mut HardwareState, cx: f32, cy: f32, cw: f32, ch: f32, root_
else { ACCENT };
let pct_str = format!("{} {:.0}%", bat_icon, bat.percentage);
- sec_bat.text(pc, &pct_str, 12.0, 0.0, 24.0, pct_color);
+ sec_bat.text(&pct_str, 12.0, 0.0, 24.0, pct_color);
sec_bat.spacing(30.0);
let state_str = format!("{} • {:.1}W • {:.1}/{:.1} Wh",
bat.state, bat.energy_rate, bat.energy, bat.energy_full);
- sec_bat.text(pc, &state_str, 12.0, 0.0, 12.0, TEXT_DIM);
+ sec_bat.text(&state_str, 12.0, 0.0, 12.0, TEXT_DIM);
sec_bat.spacing(18.0);
let time_str = if bat.time_to_empty > 0 {
@@ -469,100 +494,89 @@ pub fn view(state: &mut HardwareState, cx: f32, cy: f32, cw: f32, ch: f32, root_
format!("Time to full: {}", format_duration(bat.time_to_full))
} else { String::new() };
if !time_str.is_empty() {
- sec_bat.text(pc, &time_str, 12.0, 0.0, 12.0, TEXT_DIM);
+ sec_bat.text(&time_str, 12.0, 0.0, 12.0, TEXT_DIM);
sec_bat.spacing(18.0);
}
let detail_str = format!("{} {}", bat.vendor, bat.model);
- sec_bat.text(pc, &detail_str, 12.0, 0.0, 11.0, TEXT_DIM);
+ sec_bat.text(&detail_str, 12.0, 0.0, 11.0, TEXT_DIM);
sec_bat.spacing(20.0);
let ac_str = if state.on_ac { "On AC Power" } else { "On Battery" };
- sec_bat.text(pc, ac_str, 12.0, 0.0, 14.0, TEXT_FG);
+ sec_bat.text(ac_str, 12.0, 0.0, 14.0, TEXT_FG);
}
- sec_bat.finish(pc)
});
// ── CPU Governor section ──
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec_gov = Section::new(pc, rx, ry, sec_w, "CPU Governor");
+ builder.add_section(&mut final_pc, "CPU Governor", false, |sec_gov| {
+ let rx = sec_gov.left;
if !state.loaded {
- sec_gov.text(pc, "Loading CPU governor...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec_gov.text("Loading CPU governor...", 12.0, 0.0, 12.0, TEXT_DIM);
sec_gov.spacing(18.0);
} else {
- let btn_h = 44.0;
- let yt = sec_gov.ay();
-
- sec_gov.row(2, 8.0, btn_h, |i, x, w| {
- if i == 0 {
- let perf_active = !state.cpu_powersave;
- let (perf_bg, perf_desc, perf_desc_color) = if perf_active {
- (BTN_ACTIVE, "Governor set to performance", ACCENT)
- } else {
- (BTN_INACTIVE, "Switch to performance governor", TEXT_DIM)
- };
-
- pc.button("Performance", x, yt, w, btn_h,
- perf_bg, BTN_HOVER, WHITE,
- AppAction::Hardware(HardwareMessage::SetCpuPerformance));
- pc.text(perf_desc, x + 4.0, yt + 26.0, 10.0, perf_desc_color);
- } else {
- let (save_bg, save_desc, save_desc_color) = if state.cpu_powersave {
- (BTN_ACTIVE, "Governor set to powersave — lower power, slower burst", ACCENT)
- } else {
- (BTN_INACTIVE, "Switch to powersave governor (requires auth)", TEXT_DIM)
- };
-
- pc.button("Powersave", x, yt, w, btn_h,
- save_bg, BTN_HOVER, WHITE,
- AppAction::Hardware(HardwareMessage::SetCpuPowersave));
- pc.text(save_desc, x + 4.0, yt + 26.0, 10.0, save_desc_color);
- }
- });
+ sec_gov.widget(&mut state.cpu_gov_menu, 12.0, sec_gov.cw - 24.0, 26.0);
sec_gov.spacing(12.0);
+
+ let (info_title, info_lines) = if state.cpu_powersave {
+ (
+ "CPU Governor: Powersave",
+ vec![
+ "• Active: powersave".to_string(),
+ "• Governor set to powersave — lower power, slower burst".to_string(),
+ ],
+ )
+ } else {
+ (
+ "CPU Governor: Performance",
+ vec![
+ "• Active: performance".to_string(),
+ "• Governor set to performance".to_string(),
+ ],
+ )
+ };
+
+ let mut info_box = InfoBox::new(info_title, info_lines);
+ let info_h = 80.0;
+ let info_y = sec_gov.ay();
+ render_widget(sec_gov.pc, &mut info_box, rx + 12.0, info_y, sec_gov.cw - 24.0, info_h);
+ sec_gov.spacing(info_h + 12.0);
}
- sec_gov.finish(pc)
});
// ── GPU Power section ──
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec_gpow = Section::new(pc, rx, ry, sec_w, "GPU Power");
+ builder.add_section(&mut final_pc, "GPU Power", false, |sec_gpow| {
+ let rx = sec_gpow.left;
if !state.loaded {
- sec_gpow.text(pc, "Loading GPU power status...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec_gpow.text("Loading GPU power status...", 12.0, 0.0, 12.0, TEXT_DIM);
sec_gpow.spacing(18.0);
} else {
- let btn_h = 44.0;
- let yt = sec_gpow.ay();
-
- sec_gpow.row(2, 8.0, btn_h, |i, x, w| {
- if i == 0 {
- let gpu_def_active = !state.gpu_powersave;
- let (gpu_def_bg, gpu_def_desc, gpu_def_desc_c) = if gpu_def_active {
- (BTN_ACTIVE, "NVIDIA running at default power limit", ACCENT)
- } else {
- (BTN_INACTIVE, "Restore default power limit (requires auth)", TEXT_DIM)
- };
-
- pc.button("80W Default", x, yt, w, btn_h,
- gpu_def_bg, BTN_HOVER, WHITE,
- AppAction::Hardware(HardwareMessage::SetGpuDefault));
- pc.text(gpu_def_desc, x + 4.0, yt + 26.0, 10.0, gpu_def_desc_c);
- } else {
- let (gpu_cap_bg, gpu_cap_desc, gpu_cap_desc_c) = if state.gpu_powersave {
- (BTN_ACTIVE, "NVIDIA power limit capped at 5W — minimal draw", ACCENT)
- } else {
- (BTN_INACTIVE, "Cap NVIDIA to 5W power limit (requires auth)", TEXT_DIM)
- };
-
- pc.button("5W Cap", x, yt, w, btn_h,
- gpu_cap_bg, BTN_HOVER, WHITE,
- AppAction::Hardware(HardwareMessage::SetGpuPowersave));
- pc.text(gpu_cap_desc, x + 4.0, yt + 26.0, 10.0, gpu_cap_desc_c);
- }
- });
+ sec_gpow.widget(&mut state.gpu_gov_menu, 12.0, sec_gpow.cw - 24.0, 26.0);
sec_gpow.spacing(12.0);
+
+ let (info_title, info_lines) = if state.gpu_powersave {
+ (
+ "GPU Power Limit: Eco Cap",
+ vec![
+ "• Mode: 5W Cap".to_string(),
+ "• NVIDIA power limit capped at 5W — minimal draw".to_string(),
+ ],
+ )
+ } else {
+ (
+ "GPU Power Limit: Default",
+ vec![
+ "• Mode: 80W Default".to_string(),
+ "• NVIDIA running at default power limit".to_string(),
+ ],
+ )
+ };
+
+ let mut info_box = InfoBox::new(info_title, info_lines);
+ let info_h = 80.0;
+ let info_y = sec_gpow.ay();
+ render_widget(sec_gpow.pc, &mut info_box, rx + 12.0, info_y, sec_gpow.cw - 24.0, info_h);
+ sec_gpow.spacing(info_h + 12.0);
}
- sec_gpow.finish(pc)
});
final_pc
@@ -577,6 +591,8 @@ pub fn update(state: &mut HardwareState, msg: HardwareMessage) {
state.cpu_cores = new.cpu_cores;
state.gpus = new.gpus;
state.cpu_label = new.cpu_label;
+ state.cpu_usage_label = new.cpu_usage_label;
+ state.cpu_temp_label = new.cpu_temp_label;
state.gpu_labels = new.gpu_labels;
state.processes = new.processes;
let old_scroll = state.cpu_list_box.scroll_y();
@@ -587,21 +603,27 @@ pub fn update(state: &mut HardwareState, msg: HardwareMessage) {
state.on_ac = new.on_ac;
state.cpu_powersave = new.cpu_powersave;
state.gpu_powersave = new.gpu_powersave;
+ state.cpu_gov_menu.selected = new.cpu_gov_menu.selected;
+ state.gpu_gov_menu.selected = new.gpu_gov_menu.selected;
}
HardwareMessage::SetCpuPerformance => {
state.cpu_powersave = false;
+ state.cpu_gov_menu.selected = 0;
spawn_cpu_power(false);
}
HardwareMessage::SetCpuPowersave => {
state.cpu_powersave = true;
+ state.cpu_gov_menu.selected = 1;
spawn_cpu_power(true);
}
HardwareMessage::SetGpuDefault => {
state.gpu_powersave = false;
+ state.gpu_gov_menu.selected = 0;
spawn_gpu_power(false);
}
HardwareMessage::SetGpuPowersave => {
state.gpu_powersave = true;
+ state.gpu_gov_menu.selected = 1;
spawn_gpu_power(true);
}
HardwareMessage::None => {}
diff --git a/src/pages/input.rs b/src/pages/input.rs
index 11e30a4..aa21d65 100644
--- a/src/pages/input.rs
+++ b/src/pages/input.rs
@@ -2,8 +2,8 @@ use std::fs;
use std::io::Write;
use crate::app::PageContent;
-use clear_ui::layout::{Section, PageLayoutBuilder, LayoutStrategy};
-use clear_ui::widget::{Spinbox, Toggle, Trackpad, Dropdown, Finger, Widget};
+use clear_ui::layout::{PageLayoutBuilder, LayoutStrategy};
+use clear_ui::widget::{Spinbox, Toggle, Trackpad, Dropdown, Finger, Element};
const CONFIG_PATH: &str = "/home/lsgalante/.config/ccec/config.toml";
@@ -395,117 +395,94 @@ pub fn view(state: &mut InputState, cx: f32, cy: f32, cw: f32, ch: f32, sec_focu
let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(7);
// ── Touchpad ──
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec = Section::new(pc, rx, ry, sec_w, "Touchpad");
-
+ builder.add_section(&mut final_pc, "Touchpad", false, |sec| {
let toggle_w = 48.0;
- let toggle_h = 24.0;
+ let toggle_h = 42.0;
state.tap_toggle.set_toggled(state.tap_to_click);
- sec.widget(pc, &mut state.tap_toggle, 14.0, toggle_w, toggle_h);
+ sec.widget(&mut state.tap_toggle, 14.0, toggle_w, toggle_h);
sec.spacing(8.0);
// Built-in trackpad visualizer widget
let pad_w = 280.0;
- let pad_h = 140.0;
+ let pad_h = 158.0;
state.trackpad.set_fingers(state.fingers.clone());
- sec.widget(pc, &mut state.trackpad, 14.0, pad_w, pad_h);
+ sec.widget(&mut state.trackpad, 14.0, pad_w, pad_h);
sec.spacing(12.0);
- sec.finish(pc)
});
// ── Trackpoint ──
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec = Section::new(pc, rx, ry, sec_w, "Trackpoint");
-
+ builder.add_section(&mut final_pc, "Trackpoint", sec_focused.first().copied().unwrap_or(false), |sec| {
let toggle_w = 48.0;
- let toggle_h = 24.0;
+ let toggle_h = 42.0;
state.dwtp_toggle.set_toggled(state.dwtp);
- sec.widget(pc, &mut state.dwtp_toggle, 14.0, toggle_w, toggle_h);
+ sec.widget(&mut state.dwtp_toggle, 14.0, toggle_w, toggle_h);
sec.spacing(12.0);
- sec.widget(pc, &mut state.trackpoint_accel_speed_spinbox, 14.0, 200.0, 26.0);
+ sec.widget(&mut state.trackpoint_accel_speed_spinbox, 14.0, 200.0, 44.0);
sec.spacing(12.0);
- sec.widget(pc, &mut state.trackpoint_accel_profile_menu, 14.0, 200.0, 26.0);
+ sec.widget(&mut state.trackpoint_accel_profile_menu, 14.0, 200.0, 44.0);
sec.spacing(8.0);
- sec.finish_focused(pc, sec_focused.first().copied().unwrap_or(false))
});
// ── Keyboard ──
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec = Section::new(pc, rx, ry, sec_w, "Keyboard");
-
- sec.widget(pc, &mut state.rate_spinbox, 14.0, 200.0, 26.0);
+ builder.add_section(&mut final_pc, "Keyboard", sec_focused.get(1).copied().unwrap_or(false), |sec| {
+ sec.widget(&mut state.rate_spinbox, 14.0, 200.0, 44.0);
sec.spacing(8.0);
- sec.widget(pc, &mut state.delay_spinbox, 14.0, 200.0, 26.0);
+ sec.widget(&mut state.delay_spinbox, 14.0, 200.0, 44.0);
sec.spacing(8.0);
- sec.finish_focused(pc, sec_focused.get(1).copied().unwrap_or(false))
});
// ── Cursor ──
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec = Section::new(pc, rx, ry, sec_w, "Cursor");
-
- sec.widget(pc, &mut state.cursor_theme_menu, 14.0, 200.0, 26.0);
+ builder.add_section(&mut final_pc, "Cursor", sec_focused.get(2).copied().unwrap_or(false), |sec| {
+ sec.widget(&mut state.cursor_theme_menu, 14.0, 200.0, 44.0);
sec.spacing(12.0);
- sec.widget(pc, &mut state.cursor_size_spinbox, 14.0, 200.0, 26.0);
+ sec.widget(&mut state.cursor_size_spinbox, 14.0, 200.0, 44.0);
sec.spacing(8.0);
-
- sec.finish_focused(pc, sec_focused.get(2).copied().unwrap_or(false))
});
// ── Scrolling ──
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec = Section::new(pc, rx, ry, sec_w, "Scrolling");
-
+ builder.add_section(&mut final_pc, "Scrolling", sec_focused.get(3).copied().unwrap_or(false), |sec| {
let toggle_w = 48.0;
- let toggle_h = 24.0;
+ let toggle_h = 42.0;
state.scroll_toggle.set_toggled(state.inertial_scroll);
- sec.widget(pc, &mut state.scroll_toggle, 14.0, toggle_w, toggle_h);
+ sec.widget(&mut state.scroll_toggle, 14.0, toggle_w, toggle_h);
sec.spacing(12.0);
- sec.widget(pc, &mut state.scroll_friction_spinbox, 14.0, 200.0, 26.0);
+ sec.widget(&mut state.scroll_friction_spinbox, 14.0, 200.0, 44.0);
sec.spacing(12.0);
state.natural_toggle.set_toggled(state.natural_scroll);
- sec.widget(pc, &mut state.natural_toggle, 14.0, toggle_w, toggle_h);
+ sec.widget(&mut state.natural_toggle, 14.0, toggle_w, toggle_h);
sec.spacing(12.0);
- sec.widget(pc, &mut state.scroll_speed_spinbox, 14.0, 200.0, 26.0);
+ sec.widget(&mut state.scroll_speed_spinbox, 14.0, 200.0, 44.0);
sec.spacing(8.0);
-
- sec.finish_focused(pc, sec_focused.get(3).copied().unwrap_or(false))
});
// ── Inertial Input ──
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec = Section::new(pc, rx, ry, sec_w, "Inertial Input");
-
+ builder.add_section(&mut final_pc, "Inertial Input", sec_focused.get(4).copied().unwrap_or(false), |sec| {
let toggle_w = 48.0;
- let toggle_h = 24.0;
+ let toggle_h = 42.0;
state.pointer_toggle.set_toggled(state.inertial_pointer);
- sec.widget(pc, &mut state.pointer_toggle, 14.0, toggle_w, toggle_h);
+ sec.widget(&mut state.pointer_toggle, 14.0, toggle_w, toggle_h);
sec.spacing(12.0);
- sec.widget(pc, &mut state.pointer_friction_spinbox, 14.0, 200.0, 26.0);
+ sec.widget(&mut state.pointer_friction_spinbox, 14.0, 200.0, 44.0);
sec.spacing(16.0);
state.trackpad_toggle.set_toggled(state.inertial_trackpad);
- sec.widget(pc, &mut state.trackpad_toggle, 14.0, toggle_w, toggle_h);
+ sec.widget(&mut state.trackpad_toggle, 14.0, toggle_w, toggle_h);
sec.spacing(12.0);
- sec.widget(pc, &mut state.trackpad_friction_spinbox, 14.0, 200.0, 26.0);
+ sec.widget(&mut state.trackpad_friction_spinbox, 14.0, 200.0, 44.0);
sec.spacing(8.0);
-
- sec.finish_focused(pc, sec_focused.get(4).copied().unwrap_or(false))
});
// ── Keybindings ──
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec = Section::new(pc, rx, ry, sec_w, "Keyboard Bindings");
-
+ builder.add_section(&mut final_pc, "Keyboard Bindings", false, |sec| {
for kb in &state.keybinds {
let binding = if kb.mods.is_empty() {
kb.key.clone()
@@ -517,12 +494,11 @@ pub fn view(state: &mut InputState, cx: f32, cy: f32, cw: f32, ch: f32, sec_focu
} else {
format!("{}: {}", kb.action, kb.command)
};
- sec.text(pc, &binding, 14.0, 0.0, 12.0, TEXT_FG);
+ sec.text(&binding, 14.0, 0.0, 12.0, TEXT_FG);
let label_w = sec_w - 200.0;
- sec.text(pc, &action_label, 14.0 + label_w.min(180.0), 0.0, 12.0, TEXT_DIM);
+ sec.text(&action_label, 14.0 + label_w.min(180.0), 0.0, 12.0, TEXT_DIM);
sec.spacing(18.0);
}
- sec.finish(pc)
});
final_pc
diff --git a/src/pages/interface.rs b/src/pages/interface.rs
index 9ea2489..f418194 100644
--- a/src/pages/interface.rs
+++ b/src/pages/interface.rs
@@ -1,9 +1,10 @@
use std::fs;
use std::io::Write;
use crate::app::PageContent;
-use crate::pages::typeface::parse_u16_from;
-use clear_ui::layout::{Section, PageLayoutBuilder, LayoutStrategy};
-use clear_ui::widget::{ColorSelector, Spinbox, Widget};
+use clear_ui::layout::{PageLayoutBuilder, LayoutStrategy};
+use clear_ui::widget::{
+ ColorSelector, Spinbox, Element, ScrollingList, Dropdown, TextBox, Button, InfoBox, FontPreview, InteractiveListItem
+};
const CONFIG_PATH: &str = "/home/lsgalante/.config/ccec/config.toml";
@@ -39,6 +40,41 @@ pub struct InterfaceState {
pub paginator_tab_padding_y: u16,
pub tab_padding_spinbox_x: Spinbox,
pub tab_padding_spinbox_y: Spinbox,
+ // Typeface state fields
+ pub typeface_loaded: bool,
+ pub sans_serif: String,
+ pub serif: String,
+ pub monospace: String,
+ pub window_borders: String,
+ pub status_interface: String,
+ pub fuzzel: String,
+ pub terminal: String,
+ pub paginator: String,
+ pub all_fonts: Vec<String>,
+ pub mono_fonts: Vec<String>,
+ pub sans_box: TextBox,
+ pub serif_box: TextBox,
+ pub mono_box: TextBox,
+ pub borders_box: TextBox,
+ pub status_box: TextBox,
+ pub fuzzel_box: TextBox,
+ pub terminal_box: TextBox,
+ pub paginator_box: TextBox,
+ pub search_box: TextBox,
+ pub selected_font: Option<String>,
+ pub list_box: ScrollingList,
+ pub borders_menu: Dropdown,
+ pub status_menu: Dropdown,
+ pub fuzzel_menu: Dropdown,
+ pub terminal_menu: Dropdown,
+ pub paginator_menu: Dropdown,
+ pub borders_size_box: Spinbox,
+ pub status_size_box: Spinbox,
+ pub fuzzel_size_box: Spinbox,
+ pub terminal_size_box: Spinbox,
+ pub paginator_size_box: Spinbox,
+ pub font_buttons: Vec<InteractiveListItem>,
+ pub copy_buttons: Vec<Button>,
}
impl Default for InterfaceState {
@@ -82,6 +118,40 @@ impl Default for InterfaceState {
paginator_tab_padding_y: 14,
tab_padding_spinbox_x: Spinbox::new(10, 0, 100, 1).with_label("Tab Padding X").with_unit("px"),
tab_padding_spinbox_y: Spinbox::new(14, 0, 100, 1).with_label("Tab Padding Y").with_unit("px"),
+ typeface_loaded: false,
+ sans_serif: String::new(),
+ serif: String::new(),
+ monospace: String::new(),
+ window_borders: String::new(),
+ status_interface: String::new(),
+ fuzzel: String::new(),
+ terminal: String::new(),
+ paginator: String::new(),
+ all_fonts: Vec::new(),
+ mono_fonts: Vec::new(),
+ sans_box: TextBox::default(),
+ serif_box: TextBox::default(),
+ mono_box: TextBox::default(),
+ borders_box: TextBox::default(),
+ status_box: TextBox::default(),
+ fuzzel_box: TextBox::default(),
+ terminal_box: TextBox::default(),
+ paginator_box: TextBox::default(),
+ search_box: TextBox::default(),
+ selected_font: None,
+ list_box: ScrollingList::new(24.0, 4.0),
+ borders_menu: Dropdown::default(),
+ status_menu: Dropdown::default(),
+ fuzzel_menu: Dropdown::default(),
+ terminal_menu: Dropdown::default(),
+ paginator_menu: Dropdown::default(),
+ borders_size_box: Spinbox::new(11, 6, 72, 1),
+ status_size_box: Spinbox::new(11, 6, 72, 1),
+ fuzzel_size_box: Spinbox::new(14, 6, 72, 1),
+ terminal_size_box: Spinbox::new(12, 6, 72, 1),
+ paginator_size_box: Spinbox::new(12, 6, 72, 1),
+ font_buttons: Vec::new(),
+ copy_buttons: Vec::new(),
}
}
}
@@ -121,6 +191,28 @@ pub enum InterfaceMessage {
PickToggleEnabledColor,
PickToggleDisabledColor,
Refreshed(InterfaceState),
+ TypefaceRefreshed(InterfaceState),
+ SetSans(String),
+ SetSerif(String),
+ SetMono(String),
+ SetBorders(String),
+ SetStatus(String),
+ SetFuzzel(String),
+ SetTerminal(String),
+ SetPaginator(String),
+ SetSearch(String),
+ SelectFont(String),
+ CopyFontName(String),
+ SetBordersMenu(usize),
+ SetStatusMenu(usize),
+ SetFuzzelMenu(usize),
+ SetTerminalMenu(usize),
+ SetPaginatorMenu(usize),
+ SetBordersSize(i32),
+ SetStatusSize(i32),
+ SetFuzzelSize(i32),
+ SetTerminalSize(i32),
+ SetPaginatorSize(i32),
}
pub fn read_interface_config() -> InterfaceState {
@@ -208,6 +300,40 @@ pub fn read_interface_config() -> InterfaceState {
paginator_tab_padding_y,
tab_padding_spinbox_x: Spinbox::new(paginator_tab_padding_x as i32, 0, 100, 1).with_label("Tab Padding X").with_unit("px"),
tab_padding_spinbox_y: Spinbox::new(paginator_tab_padding_y as i32, 0, 100, 1).with_label("Tab Padding Y").with_unit("px"),
+ typeface_loaded: false,
+ sans_serif: String::new(),
+ serif: String::new(),
+ monospace: String::new(),
+ window_borders: String::new(),
+ status_interface: String::new(),
+ fuzzel: String::new(),
+ terminal: String::new(),
+ paginator: String::new(),
+ all_fonts: Vec::new(),
+ mono_fonts: Vec::new(),
+ sans_box: TextBox::default(),
+ serif_box: TextBox::default(),
+ mono_box: TextBox::default(),
+ borders_box: TextBox::default(),
+ status_box: TextBox::default(),
+ fuzzel_box: TextBox::default(),
+ terminal_box: TextBox::default(),
+ paginator_box: TextBox::default(),
+ search_box: TextBox::default(),
+ selected_font: None,
+ list_box: ScrollingList::new(24.0, 4.0),
+ borders_menu: Dropdown::default(),
+ status_menu: Dropdown::default(),
+ fuzzel_menu: Dropdown::default(),
+ terminal_menu: Dropdown::default(),
+ paginator_menu: Dropdown::default(),
+ borders_size_box: Spinbox::new(11, 6, 72, 1),
+ status_size_box: Spinbox::new(11, 6, 72, 1),
+ fuzzel_size_box: Spinbox::new(14, 6, 72, 1),
+ terminal_size_box: Spinbox::new(12, 6, 72, 1),
+ paginator_size_box: Spinbox::new(12, 6, 72, 1),
+ font_buttons: Vec::new(),
+ copy_buttons: Vec::new(),
}
}
@@ -233,11 +359,11 @@ fn parse_hex(s: &str) -> [u8; 3] {
} else { [0x0a, 0x1a, 0x0e] }
}
-fn write_config_value(key: &str, value: &str) -> bool {
+pub fn write_config_value(key: &str, value: &str) -> bool {
write_config_value_path(CONFIG_PATH, key, value)
}
-fn write_config_value_path(path: &str, key: &str, value: &str) -> bool {
+pub fn write_config_value_path(path: &str, key: &str, value: &str) -> bool {
let content = fs::read_to_string(path).unwrap_or_default();
let old_key = match key {
"low_color" => "background_color",
@@ -421,105 +547,681 @@ fn apply_paginator_tab_padding_y(padding: u16) {
send_ipc_command(&format!("layout paginator_tab_padding_y {}", padding));
}
-pub fn view(state: &mut InterfaceState, cx: f32, cy: f32, cw: f32, ch: f32, layout: &mut dyn LayoutStrategy) -> PageContent {
+const FONTS_CONF_PATH: &str = "/home/lsgalante/.config/fontconfig/fonts.conf";
+
+fn parse_font_for_alias(content: &str, alias: &str) -> Option<String> {
+ let lines: Vec<&str> = content.lines().collect();
+ for i in 0..lines.len() {
+ let line = lines[i].trim();
+ if line.contains("<test") && line.contains("name=\"family\"") && line.contains(&format!("<string>{}</string>", alias)) {
+ for j in (i + 1)..(i + 6).min(lines.len()) {
+ let next_line = lines[j].trim();
+ if next_line.contains("<edit") {
+ for k in (j + 1)..(j + 6).min(lines.len()) {
+ let str_line = lines[k].trim();
+ if str_line.contains("<string>") && str_line.contains("</string>") {
+ if let Some(start) = str_line.find("<string>") {
+ if let Some(end) = str_line.find("</string>") {
+ let font = &str_line[start + 8..end];
+ return Some(font.to_string());
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ None
+}
+
+pub fn read_preferred_fonts() -> (String, String, String, String, String, String, String, String) {
+ let content = fs::read_to_string(FONTS_CONF_PATH).unwrap_or_default();
+
+ let sans = parse_font_for_alias(&content, "sans-serif").unwrap_or_else(|| "Noto Sans".to_string());
+ let serif = parse_font_for_alias(&content, "serif").unwrap_or_else(|| "Noto Serif".to_string());
+ let mono = parse_font_for_alias(&content, "monospace").unwrap_or_else(|| "Noto Sans Mono".to_string());
+ let borders = parse_font_for_alias(&content, "window-borders").unwrap_or_else(|| "Noto Sans".to_string());
+ let status = parse_font_for_alias(&content, "status-interface").unwrap_or_else(|| "Noto Sans".to_string());
+ let fuzzel_font = parse_font_for_alias(&content, "fuzzel").unwrap_or_else(|| "Noto Sans".to_string());
+ let term = parse_font_for_alias(&content, "terminal").unwrap_or_else(|| "Noto Sans Mono".to_string());
+ let paginator = parse_font_for_alias(&content, "paginator-tab-labels").unwrap_or_else(|| "Noto Sans Mono".to_string());
+
+ (sans, serif, mono, borders, status, fuzzel_font, term, paginator)
+}
+
+pub fn save_preferred_fonts(
+ sans: &str,
+ serif: &str,
+ mono: &str,
+ borders: &str,
+ status: &str,
+ fuzzel: &str,
+ terminal: &str,
+ paginator: &str,
+) {
+ let content = fs::read_to_string(FONTS_CONF_PATH).unwrap_or_default();
+
+ let mut dirs = Vec::new();
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if trimmed.starts_with("<dir>") && trimmed.ends_with("</dir>") {
+ dirs.push(trimmed.to_string());
+ }
+ }
+ if dirs.is_empty() {
+ dirs.push("<dir>~/Dropbox/Fonts</dir>".to_string());
+ }
+
+ let mut new_content = String::new();
+ new_content.push_str("<?xml version=\"1.0\"?>\n");
+ new_content.push_str("<!DOCTYPE fontconfig SYSTEM \"fonts.dtd\">\n");
+ new_content.push_str("<fontconfig>\n");
+
+ for dir in dirs {
+ new_content.push_str(&format!(" {}\n", dir));
+ }
+
+ // Sans-Serif
+ new_content.push_str(" <match target=\"pattern\">\n");
+ new_content.push_str(" <test qual=\"any\" name=\"family\"><string>sans-serif</string></test>\n");
+ new_content.push_str(" <edit name=\"family\" mode=\"assign\" binding=\"same\">\n");
+ new_content.push_str(&format!(" <string>{}</string>\n", sans));
+ new_content.push_str(" </edit>\n");
+ new_content.push_str(" </match>\n");
+
+ // Serif
+ new_content.push_str(" <match target=\"pattern\">\n");
+ new_content.push_str(" <test qual=\"any\" name=\"family\"><string>serif</string></test>\n");
+ new_content.push_str(" <edit name=\"family\" mode=\"assign\" binding=\"same\">\n");
+ new_content.push_str(&format!(" <string>{}</string>\n", serif));
+ new_content.push_str(" </edit>\n");
+ new_content.push_str(" </match>\n");
+
+ // Monospace
+ new_content.push_str(" <match target=\"pattern\">\n");
+ new_content.push_str(" <test qual=\"any\" name=\"family\"><string>monospace</string></test>\n");
+ new_content.push_str(" <edit name=\"family\" mode=\"assign\" binding=\"same\">\n");
+ new_content.push_str(&format!(" <string>{}</string>\n", mono));
+ new_content.push_str(" </edit>\n");
+ new_content.push_str(" </match>\n");
+
+ // Window Borders
+ new_content.push_str(" <match target=\"pattern\">\n");
+ new_content.push_str(" <test qual=\"any\" name=\"family\"><string>window-borders</string></test>\n");
+ new_content.push_str(" <edit name=\"family\" mode=\"assign\" binding=\"same\">\n");
+ new_content.push_str(&format!(" <string>{}</string>\n", borders));
+ new_content.push_str(" </edit>\n");
+ new_content.push_str(" </match>\n");
+
+ // Status Interface
+ new_content.push_str(" <match target=\"pattern\">\n");
+ new_content.push_str(" <test qual=\"any\" name=\"family\"><string>status-interface</string></test>\n");
+ new_content.push_str(" <edit name=\"family\" mode=\"assign\" binding=\"same\">\n");
+ new_content.push_str(&format!(" <string>{}</string>\n", status));
+ new_content.push_str(" </edit>\n");
+ new_content.push_str(" </match>\n");
+
+ // Fuzzel
+ new_content.push_str(" <match target=\"pattern\">\n");
+ new_content.push_str(" <test qual=\"any\" name=\"family\"><string>fuzzel</string></test>\n");
+ new_content.push_str(" <edit name=\"family\" mode=\"assign\" binding=\"same\">\n");
+ new_content.push_str(&format!(" <string>{}</string>\n", fuzzel));
+ new_content.push_str(" </edit>\n");
+ new_content.push_str(" </match>\n");
+
+ // Terminal
+ new_content.push_str(" <match target=\"pattern\">\n");
+ new_content.push_str(" <test qual=\"any\" name=\"family\"><string>terminal</string></test>\n");
+ new_content.push_str(" <edit name=\"family\" mode=\"assign\" binding=\"same\">\n");
+ new_content.push_str(&format!(" <string>{}</string>\n", terminal));
+ new_content.push_str(" </edit>\n");
+ new_content.push_str(" </match>\n");
+
+ // Paginator Tab Labels
+ new_content.push_str(" <match target=\"pattern\">\n");
+ new_content.push_str(" <test qual=\"any\" name=\"family\"><string>paginator-tab-labels</string></test>\n");
+ new_content.push_str(" <edit name=\"family\" mode=\"assign\" binding=\"same\">\n");
+ new_content.push_str(&format!(" <string>{}</string>\n", paginator));
+ new_content.push_str(" </edit>\n");
+ new_content.push_str(" </match>\n");
+
+ new_content.push_str("</fontconfig>\n");
+
+ let _ = fs::write(FONTS_CONF_PATH, new_content);
+}
+
+pub fn parse_u16_from(content: &str, key: &str, default: u16) -> u16 {
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if let Some(rest) = trimmed.strip_prefix(key) {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+ let val_str = rest.trim_end_matches('"').trim();
+ if let Ok(val) = val_str.parse::<u16>() {
+ return val;
+ }
+ }
+ }
+ default
+}
+
+fn read_border_font_size() -> Option<u16> {
+ let content = fs::read_to_string("/home/lsgalante/.config/ccec/config.toml").ok()?;
+ Some(parse_u16_from(&content, "border_font_size", 11))
+}
+
+fn read_status_size() -> Option<u16> {
+ let content = fs::read_to_string("/home/lsgalante/.config/ccec/config.toml").ok()?;
+ Some(parse_u16_from(&content, "status_font_size", 11))
+}
+
+fn write_status_size(size: u16) {
+ write_config_value("status_font_size", &size.to_string());
+ let _ = std::process::Command::new("pkill")
+ .args(["-f", "clear-status-interface"])
+ .spawn();
+}
+
+fn read_fuzzel_size() -> Option<u16> {
+ let ini = fs::read_to_string("/home/lsgalante/.config/fuzzel/fuzzel.ini").ok()?;
+ for line in ini.lines() {
+ let trimmed = line.trim();
+ if trimmed.starts_with("font") {
+ if let Some(pos) = trimmed.find("size=") {
+ let size_str = &trimmed[pos + 5..];
+ let end_pos = size_str.find(|c: char| !c.is_ascii_digit()).unwrap_or(size_str.len());
+ return size_str[..end_pos].parse::<u16>().ok();
+ }
+ }
+ }
+ None
+}
+
+fn write_fuzzel_size(size: u16) {
+ let path = "/home/lsgalante/.config/fuzzel/fuzzel.ini";
+ let ini = fs::read_to_string(path).unwrap_or_default();
+ let mut new_lines = Vec::new();
+ for line in ini.lines() {
+ let trimmed = line.trim();
+ if trimmed.starts_with("font") {
+ if let Some(pos) = line.find("size=") {
+ let mut new_line = line[..pos + 5].to_string();
+ new_line.push_str(&size.to_string());
+ let size_str = &line[pos + 5..];
+ let skip = size_str.find(|c: char| !c.is_ascii_digit()).unwrap_or(size_str.len());
+ new_line.push_str(&size_str[skip..]);
+ new_lines.push(new_line);
+ } else {
+ new_lines.push(line.to_string());
+ }
+ } else {
+ new_lines.push(line.to_string());
+ }
+ }
+ let _ = fs::write(path, new_lines.join("\n"));
+}
+
+fn read_terminal_size() -> Option<u16> {
+ let ini = fs::read_to_string("/home/lsgalante/.config/foot/foot.ini").ok()?;
+ for line in ini.lines() {
+ let trimmed = line.trim();
+ if trimmed.starts_with("font") {
+ if let Some(pos) = trimmed.find("terminal:size=") {
+ let size_str = &trimmed[pos + 14..];
+ let end_pos = size_str.find(|c: char| !c.is_ascii_digit()).unwrap_or(size_str.len());
+ return size_str[..end_pos].parse::<u16>().ok();
+ }
+ }
+ }
+ None
+}
+
+fn write_terminal_size(size: u16) {
+ let path = "/home/lsgalante/.config/foot/foot.ini";
+ let ini = fs::read_to_string(path).unwrap_or_default();
+ let mut new_lines = Vec::new();
+ for line in ini.lines() {
+ let trimmed = line.trim();
+ if trimmed.starts_with("font") {
+ if let Some(pos) = line.find("terminal:size=") {
+ let mut new_line = line[..pos + 14].to_string();
+ new_line.push_str(&size.to_string());
+ let size_str = &line[pos + 14..];
+ let skip = size_str.find(|c: char| !c.is_ascii_digit()).unwrap_or(size_str.len());
+ new_line.push_str(&size_str[skip..]);
+ new_lines.push(new_line);
+ } else {
+ new_lines.push(line.to_string());
+ }
+ } else {
+ new_lines.push(line.to_string());
+ }
+ }
+ let _ = fs::write(path, new_lines.join("\n"));
+}
+
+fn read_paginator_size() -> Option<u16> {
+ let content = fs::read_to_string("/home/lsgalante/.config/ccec/config.toml").ok()?;
+ Some(parse_u16_from(&content, "paginator_font_size", 12))
+}
+
+fn write_paginator_size(size: u16) {
+ write_config_value("paginator_font_size", &size.to_string());
+}
+
+fn parse_families(output: Option<std::process::Output>) -> Vec<String> {
+ let mut families = Vec::new();
+ if let Some(o) = output {
+ let text = String::from_utf8_lossy(&o.stdout);
+ for line in text.lines() {
+ let trimmed = line.trim();
+ if !trimmed.is_empty() {
+ let family = trimmed.split(',').next().unwrap_or(trimmed).to_string();
+ if !family.is_empty() && !families.contains(&family) {
+ families.push(family);
+ }
+ }
+ }
+ }
+ families.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase()));
+ families
+}
+
+pub async fn fetch_typeface_state() -> InterfaceState {
+ let (sans, serif, mono, borders, status, fuzzel_font, term, paginator_font) = read_preferred_fonts();
+
+ let all_output = tokio::process::Command::new("fc-list")
+ .args([":", "family"])
+ .output().await.ok();
+ let all_fonts = parse_families(all_output);
+
+ let mono_output = tokio::process::Command::new("fc-list")
+ .args([":spacing=100", "family"])
+ .output().await.ok();
+ let mono_fonts = parse_families(mono_output);
+
+ let selected_font = all_fonts.first().cloned();
+
+ let determine_dropdown_index = |font: &str, sans: &str, serif: &str, mono: &str| -> usize {
+ if font == sans {
+ 0
+ } else if font == serif {
+ 1
+ } else if font == mono {
+ 2
+ } else {
+ 3
+ }
+ };
+
+ let borders_idx = determine_dropdown_index(&borders, &sans, &serif, &mono);
+ let status_idx = determine_dropdown_index(&status, &sans, &serif, &mono);
+ let fuzzel_idx = determine_dropdown_index(&fuzzel_font, &sans, &serif, &mono);
+ let terminal_idx = determine_dropdown_index(&term, &sans, &serif, &mono);
+ let paginator_idx = determine_dropdown_index(&paginator_font, &sans, &serif, &mono);
+
+ let menu_options = vec![
+ "Sans-Serif".to_string(),
+ "Serif".to_string(),
+ "Monospace".to_string(),
+ "Other".to_string(),
+ ];
+
+ let mut borders_box = TextBox::new(borders.clone()).with_label("Window Borders").with_width(300.0);
+ borders_box.disabled = borders_idx != 3;
+
+ let mut status_box = TextBox::new(status.clone()).with_label("Status Interface").with_width(300.0);
+ status_box.disabled = status_idx != 3;
+
+ let mut fuzzel_box = TextBox::new(fuzzel_font.clone()).with_label("Fuzzel").with_width(300.0);
+ fuzzel_box.disabled = fuzzel_idx != 3;
+
+ let mut terminal_box = TextBox::new(term.clone()).with_label("Terminal").with_width(300.0);
+ terminal_box.disabled = terminal_idx != 3;
+
+ let mut paginator_box = TextBox::new(paginator_font.clone()).with_label("Paginator Tab Labels").with_width(300.0);
+ paginator_box.disabled = paginator_idx != 3;
+
+ let borders_size = read_border_font_size().unwrap_or(11);
+ let status_size = read_status_size().unwrap_or(11);
+ let fuzzel_size = read_fuzzel_size().unwrap_or(14);
+ let terminal_size = read_terminal_size().unwrap_or(12);
+ let paginator_size = read_paginator_size().unwrap_or(12);
+
+ let mut state = InterfaceState::default();
+ state.typeface_loaded = true;
+ state.sans_serif = sans.clone();
+ state.serif = serif.clone();
+ state.monospace = mono.clone();
+ state.window_borders = borders;
+ state.status_interface = status;
+ state.fuzzel = fuzzel_font;
+ state.terminal = term;
+ state.paginator = paginator_font;
+ state.all_fonts = all_fonts;
+ state.mono_fonts = mono_fonts;
+ state.sans_box = TextBox::new(sans).with_label("Sans-Serif");
+ state.serif_box = TextBox::new(serif).with_label("Serif");
+ state.mono_box = TextBox::new(mono).with_label("Monospace");
+ state.borders_box = borders_box;
+ state.status_box = status_box;
+ state.fuzzel_box = fuzzel_box;
+ state.terminal_box = terminal_box;
+ state.paginator_box = paginator_box;
+ state.search_box = TextBox::new(String::new()).with_label("Filter Fonts");
+ state.selected_font = selected_font;
+ state.list_box = ScrollingList::new(24.0, 4.0);
+ state.borders_menu = Dropdown::new(menu_options.clone(), borders_idx);
+ state.status_menu = Dropdown::new(menu_options.clone(), status_idx);
+ state.fuzzel_menu = Dropdown::new(menu_options.clone(), fuzzel_idx);
+ state.terminal_menu = Dropdown::new(menu_options.clone(), terminal_idx);
+ state.paginator_menu = Dropdown::new(menu_options, paginator_idx);
+ state.borders_size_box = Spinbox::new(borders_size as i32, 6, 72, 1);
+ state.status_size_box = Spinbox::new(status_size as i32, 6, 72, 1);
+ state.fuzzel_size_box = Spinbox::new(fuzzel_size as i32, 6, 72, 1);
+ state.terminal_size_box = Spinbox::new(terminal_size as i32, 6, 72, 1);
+ state.paginator_size_box = Spinbox::new(paginator_size as i32, 6, 72, 1);
+ state
+}
+
+pub fn view(state: &mut InterfaceState, cx: f32, cy: f32, cw: f32, ch: f32, sec_focused: &[bool], layout: &mut dyn LayoutStrategy) -> PageContent {
let mut final_pc = PageContent::new();
let sec_w = 320.0f32;
- let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(6);
+ let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(10);
// 1. Pages Section
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec = Section::new(pc, rx, ry, sec_w, "Pages");
+ builder.add_section(&mut final_pc, "Pages", false, |sec| {
sec.spacing(8.0);
state.color_selectors[0].color = state.page_low_color;
- sec.widget(pc, &mut state.color_selectors[0], 12.0, 220.0, 22.0);
+ sec.widget(&mut state.color_selectors[0], 12.0, 220.0, 40.0);
sec.spacing(8.0);
- sec.finish(pc)
});
// 2. Layout Section
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec = Section::new(pc, rx, ry, sec_w, "Layout");
+ builder.add_section(&mut final_pc, "Layout", false, |sec| {
sec.spacing(8.0);
state.color_selectors[7].color = state.low_color;
- sec.widget(pc, &mut state.color_selectors[7], 12.0, 220.0, 22.0);
+ sec.widget(&mut state.color_selectors[7], 12.0, 220.0, 40.0);
sec.spacing(8.0);
state.color_selectors[1].color = state.high_color;
- sec.widget(pc, &mut state.color_selectors[1], 12.0, 220.0, 22.0);
+ sec.widget(&mut state.color_selectors[1], 12.0, 220.0, 40.0);
sec.spacing(8.0);
state.color_selectors[2].color = state.visual_guides_color;
- sec.widget(pc, &mut state.color_selectors[2], 12.0, 220.0, 22.0);
+ sec.widget(&mut state.color_selectors[2], 12.0, 220.0, 40.0);
sec.spacing(8.0);
- sec.finish(pc)
});
// 3. Status Section
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec = Section::new(pc, rx, ry, sec_w, "Status");
+ builder.add_section(&mut final_pc, "Status", false, |sec| {
sec.spacing(8.0);
state.color_selectors[8].color = state.normal_color;
- sec.widget(pc, &mut state.color_selectors[8], 12.0, 220.0, 22.0);
+ sec.widget(&mut state.color_selectors[8], 12.0, 220.0, 40.0);
sec.spacing(8.0);
state.color_selectors[3].color = state.disabled_color;
- sec.widget(pc, &mut state.color_selectors[3], 12.0, 220.0, 22.0);
+ sec.widget(&mut state.color_selectors[3], 12.0, 220.0, 40.0);
sec.spacing(8.0);
state.color_selectors[4].color = state.separator_color;
- sec.widget(pc, &mut state.color_selectors[4], 12.0, 220.0, 22.0);
+ sec.widget(&mut state.color_selectors[4], 12.0, 220.0, 40.0);
sec.spacing(8.0);
- sec.finish(pc)
});
// 4. Controls Section
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec = Section::new(pc, rx, ry, sec_w, "Controls");
+ builder.add_section(&mut final_pc, "Controls", false, |sec| {
sec.spacing(8.0);
state.color_selectors[5].color = state.slider_track_color;
- sec.widget(pc, &mut state.color_selectors[5], 12.0, 220.0, 22.0);
+ sec.widget(&mut state.color_selectors[5], 12.0, 220.0, 40.0);
sec.spacing(8.0);
state.color_selectors[6].color = state.color_borders_color;
- sec.widget(pc, &mut state.color_selectors[6], 12.0, 220.0, 22.0);
+ sec.widget(&mut state.color_selectors[6], 12.0, 220.0, 40.0);
+ sec.spacing(8.0);
+ });
+
+ // 5. Primary Highlight Section
+ builder.add_section(&mut final_pc, "Primary Highlight", false, |sec| {
sec.spacing(8.0);
state.color_selectors[10].color = state.primary_highlight_color;
- sec.widget(pc, &mut state.color_selectors[10], 12.0, 220.0, 22.0);
+ sec.widget(&mut state.color_selectors[10], 12.0, 220.0, 40.0);
sec.spacing(8.0);
- sec.finish(pc)
});
// 5. Paginator Section
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec = Section::new(pc, rx, ry, sec_w, "Paginator");
+ builder.add_section(&mut final_pc, "Paginator", false, |sec| {
sec.spacing(8.0);
state.color_selectors[9].color = state.paginator_sidebar_color;
- sec.widget(pc, &mut state.color_selectors[9], 12.0, 220.0, 22.0);
+ sec.widget(&mut state.color_selectors[9], 12.0, 220.0, 40.0);
sec.spacing(8.0);
state.color_selectors[11].color = state.paginator_tab_label_color;
- sec.widget(pc, &mut state.color_selectors[11], 12.0, 220.0, 22.0);
- sec.spacing(8.0);
+ sec.widget(&mut state.color_selectors[11], 12.0, 220.0, 40.0);
state.tab_margin_spinbox_x.value = state.paginator_tab_margin_x as i32;
- sec.widget(pc, &mut state.tab_margin_spinbox_x, 12.0, 200.0, 26.0);
+ sec.widget(&mut state.tab_margin_spinbox_x, 12.0, 200.0, 44.0);
sec.spacing(8.0);
state.tab_margin_spinbox_y.value = state.paginator_tab_margin_y as i32;
- sec.widget(pc, &mut state.tab_margin_spinbox_y, 12.0, 200.0, 26.0);
+ sec.widget(&mut state.tab_margin_spinbox_y, 12.0, 200.0, 44.0);
sec.spacing(8.0);
state.tab_padding_spinbox_x.value = state.paginator_tab_padding_x as i32;
- sec.widget(pc, &mut state.tab_padding_spinbox_x, 12.0, 200.0, 26.0);
+ sec.widget(&mut state.tab_padding_spinbox_x, 12.0, 200.0, 44.0);
sec.spacing(8.0);
state.tab_padding_spinbox_y.value = state.paginator_tab_padding_y as i32;
- sec.widget(pc, &mut state.tab_padding_spinbox_y, 12.0, 200.0, 26.0);
+ sec.widget(&mut state.tab_padding_spinbox_y, 12.0, 200.0, 44.0);
sec.spacing(8.0);
- sec.finish(pc)
});
// 6. Toggles Section
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec = Section::new(pc, rx, ry, sec_w, "Toggles");
+ builder.add_section(&mut final_pc, "Toggles", false, |sec| {
sec.spacing(8.0);
state.color_selectors[12].color = state.toggle_enabled_color;
- sec.widget(pc, &mut state.color_selectors[12], 12.0, 220.0, 22.0);
+ sec.widget(&mut state.color_selectors[12], 12.0, 220.0, 40.0);
sec.spacing(8.0);
state.color_selectors[13].color = state.toggle_disabled_color;
- sec.widget(pc, &mut state.color_selectors[13], 12.0, 220.0, 22.0);
+ sec.widget(&mut state.color_selectors[13], 12.0, 220.0, 40.0);
sec.spacing(8.0);
- sec.finish(pc)
+ });
+
+ let widget_h = 26.0;
+ const TEXT_DIM: [f32; 4] = [0.53, 0.53, 0.60, 1.0];
+
+ // 7. System Typefaces Section
+ builder.add_section(&mut final_pc, "System Typefaces", sec_focused.get(7).copied().unwrap_or(false), |sec| {
+ sec.spacing(8.0);
+
+ if !state.typeface_loaded {
+ sec.text("Loading typefaces...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(18.0);
+ } else {
+ let inner_w = sec_w - 24.0;
+ // Sans-Serif
+ sec.widget(&mut state.sans_box, 12.0, inner_w, 44.0);
+ sec.spacing(12.0);
+
+ // Serif
+ sec.widget(&mut state.serif_box, 12.0, inner_w, 44.0);
+ sec.spacing(12.0);
+
+ // Monospace
+ sec.widget(&mut state.mono_box, 12.0, inner_w, 44.0);
+ sec.spacing(8.0);
+ }
+ });
+
+ // 8. Program Typefaces Section
+ builder.add_section(&mut final_pc, "Program Typefaces", sec_focused.get(8).copied().unwrap_or(false), |sec| {
+ sec.spacing(8.0);
+
+ if !state.typeface_loaded {
+ sec.text("Loading typefaces...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(18.0);
+ } else {
+ let inner_w = sec_w - 24.0;
+
+ // Window Borders
+ let start_y = sec.ay();
+ let cols = sec.row_layout(2, 10.0);
+ if cols.len() == 2 {
+ state.borders_menu.set_row_rect(cols[0].0, cols[0].1);
+ clear_ui::layout::render_widget(sec.pc, &mut state.borders_menu, cols[0].0, start_y, cols[0].1, widget_h);
+ state.borders_size_box.set_row_rect(cols[1].0, cols[1].1);
+ clear_ui::layout::render_widget(sec.pc, &mut state.borders_size_box, cols[1].0, start_y, cols[1].1, widget_h);
+ }
+ sec.spacing(widget_h);
+ sec.widget(&mut state.borders_box, 12.0, inner_w, 44.0);
+ sec.spacing(16.0);
+
+ // Status Interface
+ let start_y = sec.ay();
+ let cols = sec.row_layout(2, 10.0);
+ if cols.len() == 2 {
+ state.status_menu.set_row_rect(cols[0].0, cols[0].1);
+ clear_ui::layout::render_widget(sec.pc, &mut state.status_menu, cols[0].0, start_y, cols[0].1, widget_h);
+ state.status_size_box.set_row_rect(cols[1].0, cols[1].1);
+ clear_ui::layout::render_widget(sec.pc, &mut state.status_size_box, cols[1].0, start_y, cols[1].1, widget_h);
+ }
+ sec.spacing(widget_h);
+ sec.widget(&mut state.status_box, 12.0, inner_w, 44.0);
+ sec.spacing(16.0);
+
+ // Fuzzel
+ let start_y = sec.ay();
+ let cols = sec.row_layout(2, 10.0);
+ if cols.len() == 2 {
+ state.fuzzel_menu.set_row_rect(cols[0].0, cols[0].1);
+ clear_ui::layout::render_widget(sec.pc, &mut state.fuzzel_menu, cols[0].0, start_y, cols[0].1, widget_h);
+ state.fuzzel_size_box.set_row_rect(cols[1].0, cols[1].1);
+ clear_ui::layout::render_widget(sec.pc, &mut state.fuzzel_size_box, cols[1].0, start_y, cols[1].1, widget_h);
+ }
+ sec.spacing(widget_h);
+ sec.widget(&mut state.fuzzel_box, 12.0, inner_w, 44.0);
+ sec.spacing(16.0);
+
+ // Terminal
+ let start_y = sec.ay();
+ let cols = sec.row_layout(2, 10.0);
+ if cols.len() == 2 {
+ state.terminal_menu.set_row_rect(cols[0].0, cols[0].1);
+ clear_ui::layout::render_widget(sec.pc, &mut state.terminal_menu, cols[0].0, start_y, cols[0].1, widget_h);
+ state.terminal_size_box.set_row_rect(cols[1].0, cols[1].1);
+ clear_ui::layout::render_widget(sec.pc, &mut state.terminal_size_box, cols[1].0, start_y, cols[1].1, widget_h);
+ }
+ sec.spacing(widget_h);
+ sec.widget(&mut state.terminal_box, 12.0, inner_w, 44.0);
+ sec.spacing(16.0);
+
+ // Paginator Tab Labels
+ let start_y = sec.ay();
+ let cols = sec.row_layout(2, 10.0);
+ if cols.len() == 2 {
+ state.paginator_menu.set_row_rect(cols[0].0, cols[0].1);
+ clear_ui::layout::render_widget(sec.pc, &mut state.paginator_menu, cols[0].0, start_y, cols[0].1, widget_h);
+ state.paginator_size_box.set_row_rect(cols[1].0, cols[1].1);
+ clear_ui::layout::render_widget(sec.pc, &mut state.paginator_size_box, cols[1].0, start_y, cols[1].1, widget_h);
+ }
+ sec.spacing(widget_h);
+ sec.widget(&mut state.paginator_box, 12.0, inner_w, 44.0);
+ sec.spacing(8.0);
+ }
+ });
+
+ // 9. Typefaces Section (List & Preview)
+ builder.add_section(&mut final_pc, "Typefaces", sec_focused.get(9).copied().unwrap_or(false), |sec| {
+ sec.spacing(12.0);
+
+ if !state.typeface_loaded {
+ sec.text("Loading installed fonts...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(18.0);
+ } else {
+ let inner_w = sec_w - 24.0;
+
+ // 1. Search Box
+ let search_x = sec.left + 12.0;
+ state.search_box.set_row_rect(search_x, inner_w);
+ let search_y = sec.ay();
+ clear_ui::layout::render_widget(
+ sec.pc,
+ &mut state.search_box,
+ search_x,
+ search_y,
+ inner_w,
+ 44.0,
+ );
+ sec.spacing(44.0 + 12.0);
+
+ // 2. Scrolling List Box
+ let list_box_x = sec.left + 12.0;
+ let list_box_y = sec.ay();
+ let list_box_h = 200.0;
+
+ clear_ui::layout::render_widget(sec.pc, &mut state.list_box, list_box_x, list_box_y, inner_w, list_box_h);
+
+ let query = state.search_box.text.to_lowercase();
+ let matching_fonts: Vec<&String> = state.all_fonts.iter()
+ .filter(|font| font.to_lowercase().contains(&query))
+ .collect();
+
+ if state.font_buttons.len() != matching_fonts.len() {
+ state.font_buttons.clear();
+ state.copy_buttons.clear();
+ for _ in 0..matching_fonts.len() {
+ state.font_buttons.push(InteractiveListItem::new(""));
+ state.copy_buttons.push(Button::new_copy_icon(0.0, 0.0, 0.0, 0.0));
+ }
+ }
+
+ let btn_h = 24.0;
+ let list_inner_x = sec.left + 16.0;
+ let list_inner_w = inner_w - 16.0;
+
+ state.list_box.update_bounds(matching_fonts.len(), list_box_y, list_box_h);
+
+ for (idx, font_name) in matching_fonts.iter().enumerate() {
+ if let Some(draw_y) = state.list_box.get_item_draw_y(idx, 0.0) {
+ let is_selected = state.selected_font.as_ref() == Some(*font_name);
+
+ let font_btn = &mut state.font_buttons[idx];
+ font_btn.title = font_name.to_string();
+ font_btn.selected = is_selected;
+ clear_ui::layout::render_widget(sec.pc, font_btn, list_inner_x, draw_y, list_inner_w - 44.0, btn_h);
+
+ let copy_btn = &mut state.copy_buttons[idx];
+ copy_btn.set_text("📋");
+ copy_btn.selected = is_selected;
+ clear_ui::layout::render_widget(sec.pc, copy_btn, list_inner_x + list_inner_w - 40.0, draw_y, 40.0, btn_h);
+ }
+ }
+
+ if matching_fonts.is_empty() {
+ sec.pc.text("No fonts match query", list_inner_x + 8.0, list_box_y + 16.0, 12.0, TEXT_DIM);
+ }
+
+ sec.spacing(list_box_h + 12.0);
+
+ // 3. Info Box
+ let info_h = 96.0;
+ let info_y = sec.ay();
+ let info_x = sec.left + 12.0;
+ let mut info_box = InfoBox::new(
+ "Font Directories & Installation",
+ vec![
+ "• Active Directory: ~/Dropbox/Fonts".to_string(),
+ "• Place TTF/OTF files there to install new fonts.".to_string(),
+ "• Changes will be cached automatically by fontconfig.".to_string(),
+ ],
+ );
+ clear_ui::layout::render_widget(sec.pc, &mut info_box, info_x, info_y, inner_w, info_h);
+ sec.spacing(info_h + 12.0);
+
+ // 4. Preview Card
+ let card_x = sec.left + 12.0;
+ if let Some(ref font_name) = state.selected_font {
+ let card_h = 240.0;
+ let card_y = sec.ay();
+ let mut font_preview = FontPreview::new(font_name.clone());
+ clear_ui::layout::render_widget(sec.pc, &mut font_preview, card_x, card_y, inner_w, card_h);
+ sec.spacing(card_h + 8.0);
+ } else {
+ let text_y = sec.ay() + 20.0;
+ sec.pc.text("Select a font to preview", card_x + 12.0, text_y, 13.0, TEXT_DIM);
+ sec.spacing(40.0);
+ }
+ }
});
final_pc
@@ -606,11 +1308,509 @@ pub fn update(state: &mut InterfaceState, msg: InterfaceMessage) {
let was_my_hovered = state.tab_margin_spinbox_y.hovered();
let was_px_hovered = state.tab_padding_spinbox_x.hovered();
let was_py_hovered = state.tab_padding_spinbox_y.hovered();
+ // Preserve typeface fields
+ let typeface_loaded = state.typeface_loaded;
+ let sans_serif = state.sans_serif.clone();
+ let serif = state.serif.clone();
+ let monospace = state.monospace.clone();
+ let window_borders = state.window_borders.clone();
+ let status_interface = state.status_interface.clone();
+ let fuzzel = state.fuzzel.clone();
+ let terminal = state.terminal.clone();
+ let paginator = state.paginator.clone();
+ let all_fonts = state.all_fonts.clone();
+ let mono_fonts = state.mono_fonts.clone();
+ let sans_box = state.sans_box.clone();
+ let serif_box = state.serif_box.clone();
+ let mono_box = state.mono_box.clone();
+ let borders_box = state.borders_box.clone();
+ let status_box = state.status_box.clone();
+ let fuzzel_box = state.fuzzel_box.clone();
+ let terminal_box = state.terminal_box.clone();
+ let paginator_box = state.paginator_box.clone();
+ let search_box = state.search_box.clone();
+ let selected_font = state.selected_font.clone();
+ let list_box = state.list_box.clone();
+ let borders_menu = state.borders_menu.clone();
+ let status_menu = state.status_menu.clone();
+ let fuzzel_menu = state.fuzzel_menu.clone();
+ let terminal_menu = state.terminal_menu.clone();
+ let paginator_menu = state.paginator_menu.clone();
+ let borders_size_box = state.borders_size_box.clone();
+ let status_size_box = state.status_size_box.clone();
+ let fuzzel_size_box = state.fuzzel_size_box.clone();
+ let terminal_size_box = state.terminal_size_box.clone();
+ let paginator_size_box = state.paginator_size_box.clone();
+ let font_buttons = state.font_buttons.clone();
+ let copy_buttons = state.copy_buttons.clone();
+
*state = new;
+
state.tab_margin_spinbox_x.set_hovered(was_mx_hovered);
state.tab_margin_spinbox_y.set_hovered(was_my_hovered);
state.tab_padding_spinbox_x.set_hovered(was_px_hovered);
state.tab_padding_spinbox_y.set_hovered(was_py_hovered);
+
+ state.typeface_loaded = typeface_loaded;
+ state.sans_serif = sans_serif;
+ state.serif = serif;
+ state.monospace = monospace;
+ state.window_borders = window_borders;
+ state.status_interface = status_interface;
+ state.fuzzel = fuzzel;
+ state.terminal = terminal;
+ state.paginator = paginator;
+ state.all_fonts = all_fonts;
+ state.mono_fonts = mono_fonts;
+ state.sans_box = sans_box;
+ state.serif_box = serif_box;
+ state.mono_box = mono_box;
+ state.borders_box = borders_box;
+ state.status_box = status_box;
+ state.fuzzel_box = fuzzel_box;
+ state.terminal_box = terminal_box;
+ state.paginator_box = paginator_box;
+ state.search_box = search_box;
+ state.selected_font = selected_font;
+ state.list_box = list_box;
+ state.borders_menu = borders_menu;
+ state.status_menu = status_menu;
+ state.fuzzel_menu = fuzzel_menu;
+ state.terminal_menu = terminal_menu;
+ state.paginator_menu = paginator_menu;
+ state.borders_size_box = borders_size_box;
+ state.status_size_box = status_size_box;
+ state.fuzzel_size_box = fuzzel_size_box;
+ state.terminal_size_box = terminal_size_box;
+ state.paginator_size_box = paginator_size_box;
+ state.font_buttons = font_buttons;
+ state.copy_buttons = copy_buttons;
+ }
+ InterfaceMessage::TypefaceRefreshed(new) => {
+ state.typeface_loaded = new.typeface_loaded;
+ state.all_fonts = new.all_fonts;
+ state.mono_fonts = new.mono_fonts;
+ if state.selected_font.is_none() {
+ state.selected_font = new.selected_font.clone();
+ }
+ if !state.sans_box.editing {
+ state.sans_serif = new.sans_serif.clone();
+ state.sans_box = new.sans_box;
+ }
+ if !state.serif_box.editing {
+ state.serif = new.serif.clone();
+ state.serif_box = new.serif_box;
+ }
+ if !state.mono_box.editing {
+ state.monospace = new.monospace.clone();
+ state.mono_box = new.mono_box;
+ }
+ if !state.borders_box.editing {
+ state.window_borders = new.window_borders.clone();
+ state.borders_box = new.borders_box;
+ state.borders_menu = new.borders_menu;
+ }
+ if !state.status_box.editing {
+ state.status_interface = new.status_interface.clone();
+ state.status_box = new.status_box;
+ state.status_menu = new.status_menu;
+ }
+ if !state.fuzzel_box.editing {
+ state.fuzzel = new.fuzzel.clone();
+ state.fuzzel_box = new.fuzzel_box;
+ state.fuzzel_menu = new.fuzzel_menu;
+ }
+ if !state.terminal_box.editing {
+ state.terminal = new.terminal.clone();
+ state.terminal_box = new.terminal_box;
+ state.terminal_menu = new.terminal_menu;
+ }
+ if !state.paginator_box.editing {
+ state.paginator = new.paginator.clone();
+ state.paginator_box = new.paginator_box;
+ state.paginator_menu = new.paginator_menu;
+ }
+ if !state.search_box.editing {
+ state.search_box = new.search_box;
+ }
+ state.borders_size_box = new.borders_size_box;
+ state.status_size_box = new.status_size_box;
+ state.fuzzel_size_box = new.fuzzel_size_box;
+ state.terminal_size_box = new.terminal_size_box;
+ state.paginator_size_box = new.paginator_size_box;
+ state.font_buttons = new.font_buttons;
+ state.copy_buttons = new.copy_buttons;
+ let old_scroll = state.list_box.scroll_y();
+ state.list_box = new.list_box;
+ state.list_box.set_scroll_y(old_scroll);
+ }
+ InterfaceMessage::SetSans(sans) => {
+ state.sans_serif = sans.clone();
+ state.sans_box.text = sans;
+ if state.borders_menu.selected == 0 {
+ state.window_borders = state.sans_serif.clone();
+ state.borders_box.text = state.sans_serif.clone();
+ }
+ if state.status_menu.selected == 0 {
+ state.status_interface = state.sans_serif.clone();
+ state.status_box.text = state.sans_serif.clone();
+ }
+ if state.fuzzel_menu.selected == 0 {
+ state.fuzzel = state.sans_serif.clone();
+ state.fuzzel_box.text = state.sans_serif.clone();
+ }
+ if state.terminal_menu.selected == 0 {
+ state.terminal = state.sans_serif.clone();
+ state.terminal_box.text = state.sans_serif.clone();
+ }
+ if state.paginator_menu.selected == 0 {
+ state.paginator = state.sans_serif.clone();
+ state.paginator_box.text = state.sans_serif.clone();
+ }
+ save_preferred_fonts(
+ &state.sans_serif,
+ &state.serif,
+ &state.monospace,
+ &state.window_borders,
+ &state.status_interface,
+ &state.fuzzel,
+ &state.terminal,
+ &state.paginator,
+ );
+ }
+ InterfaceMessage::SetSerif(serif) => {
+ state.serif = serif.clone();
+ state.serif_box.text = serif;
+ if state.borders_menu.selected == 1 {
+ state.window_borders = state.serif.clone();
+ state.borders_box.text = state.serif.clone();
+ }
+ if state.status_menu.selected == 1 {
+ state.status_interface = state.serif.clone();
+ state.status_box.text = state.serif.clone();
+ }
+ if state.fuzzel_menu.selected == 1 {
+ state.fuzzel = state.serif.clone();
+ state.fuzzel_box.text = state.serif.clone();
+ }
+ if state.terminal_menu.selected == 1 {
+ state.terminal = state.serif.clone();
+ state.terminal_box.text = state.serif.clone();
+ }
+ if state.paginator_menu.selected == 1 {
+ state.paginator = state.serif.clone();
+ state.paginator_box.text = state.serif.clone();
+ }
+ save_preferred_fonts(
+ &state.sans_serif,
+ &state.serif,
+ &state.monospace,
+ &state.window_borders,
+ &state.status_interface,
+ &state.fuzzel,
+ &state.terminal,
+ &state.paginator,
+ );
+ }
+ InterfaceMessage::SetMono(mono) => {
+ state.monospace = mono.clone();
+ state.mono_box.text = mono;
+ if state.borders_menu.selected == 2 {
+ state.window_borders = state.monospace.clone();
+ state.borders_box.text = state.monospace.clone();
+ }
+ if state.status_menu.selected == 2 {
+ state.status_interface = state.monospace.clone();
+ state.status_box.text = state.monospace.clone();
+ }
+ if state.fuzzel_menu.selected == 2 {
+ state.fuzzel = state.monospace.clone();
+ state.fuzzel_box.text = state.monospace.clone();
+ }
+ if state.terminal_menu.selected == 2 {
+ state.terminal = state.monospace.clone();
+ state.terminal_box.text = state.monospace.clone();
+ }
+ if state.paginator_menu.selected == 2 {
+ state.paginator = state.monospace.clone();
+ state.paginator_box.text = state.monospace.clone();
+ }
+ save_preferred_fonts(
+ &state.sans_serif,
+ &state.serif,
+ &state.monospace,
+ &state.window_borders,
+ &state.status_interface,
+ &state.fuzzel,
+ &state.terminal,
+ &state.paginator,
+ );
+ }
+ InterfaceMessage::SetBorders(borders) => {
+ state.window_borders = borders.clone();
+ state.borders_box.text = borders;
+ save_preferred_fonts(
+ &state.sans_serif,
+ &state.serif,
+ &state.monospace,
+ &state.window_borders,
+ &state.status_interface,
+ &state.fuzzel,
+ &state.terminal,
+ &state.paginator,
+ );
+ }
+ InterfaceMessage::SetStatus(status) => {
+ state.status_interface = status.clone();
+ state.status_box.text = status;
+ save_preferred_fonts(
+ &state.sans_serif,
+ &state.serif,
+ &state.monospace,
+ &state.window_borders,
+ &state.status_interface,
+ &state.fuzzel,
+ &state.terminal,
+ &state.paginator,
+ );
+ }
+ InterfaceMessage::SetFuzzel(fuzzel) => {
+ state.fuzzel = fuzzel.clone();
+ state.fuzzel_box.text = fuzzel;
+ save_preferred_fonts(
+ &state.sans_serif,
+ &state.serif,
+ &state.monospace,
+ &state.window_borders,
+ &state.status_interface,
+ &state.fuzzel,
+ &state.terminal,
+ &state.paginator,
+ );
+ }
+ InterfaceMessage::SetTerminal(term) => {
+ state.terminal = term.clone();
+ state.terminal_box.text = term;
+ save_preferred_fonts(
+ &state.sans_serif,
+ &state.serif,
+ &state.monospace,
+ &state.window_borders,
+ &state.status_interface,
+ &state.fuzzel,
+ &state.terminal,
+ &state.paginator,
+ );
+ }
+ InterfaceMessage::SetPaginator(paginator) => {
+ state.paginator = paginator.clone();
+ state.paginator_box.text = paginator;
+ save_preferred_fonts(
+ &state.sans_serif,
+ &state.serif,
+ &state.monospace,
+ &state.window_borders,
+ &state.status_interface,
+ &state.fuzzel,
+ &state.terminal,
+ &state.paginator,
+ );
+ }
+ InterfaceMessage::SetSearch(search) => {
+ state.search_box.text = search;
+ }
+ InterfaceMessage::SelectFont(font) => {
+ state.selected_font = Some(font);
+ }
+ InterfaceMessage::CopyFontName(font) => {
+ use std::io::Write;
+ std::thread::spawn({
+ let text = font.clone();
+ move || {
+ let mut copied = false;
+ let child = std::process::Command::new("wl-copy")
+ .stdin(std::process::Stdio::piped())
+ .stderr(std::process::Stdio::piped())
+ .spawn();
+ match child {
+ Ok(mut child) => {
+ if let Some(mut stdin) = child.stdin.take() {
+ let _ = stdin.write_all(text.as_bytes());
+ }
+ match child.wait_with_output() {
+ Ok(output) => {
+ if output.status.success() {
+ copied = true;
+ } else {
+ let err_msg = String::from_utf8_lossy(&output.stderr);
+ eprintln!("wl-copy exited with error status: {:?}, stderr: {}", output.status, err_msg);
+ }
+ }
+ Err(e) => {
+ eprintln!("wl-copy wait failed: {:?}", e);
+ }
+ }
+ }
+ Err(e) => {
+ eprintln!("wl-copy spawn failed: {:?}", e);
+ }
+ }
+ if !copied {
+ if let Ok(mut child) = std::process::Command::new("xclip")
+ .arg("-selection")
+ .arg("clipboard")
+ .stdin(std::process::Stdio::piped())
+ .spawn()
+ {
+ if let Some(mut stdin) = child.stdin.take() {
+ let _ = stdin.write_all(text.as_bytes());
+ }
+ let _ = child.wait();
+ }
+ }
+ }
+ });
+ }
+ InterfaceMessage::SetBordersMenu(idx) => {
+ state.borders_menu.selected = idx;
+ state.borders_box.disabled = idx != 3;
+ if idx == 0 {
+ state.window_borders = state.sans_serif.clone();
+ state.borders_box.text = state.sans_serif.clone();
+ } else if idx == 1 {
+ state.window_borders = state.serif.clone();
+ state.borders_box.text = state.serif.clone();
+ } else if idx == 2 {
+ state.window_borders = state.monospace.clone();
+ state.borders_box.text = state.monospace.clone();
+ }
+ save_preferred_fonts(
+ &state.sans_serif,
+ &state.serif,
+ &state.monospace,
+ &state.window_borders,
+ &state.status_interface,
+ &state.fuzzel,
+ &state.terminal,
+ &state.paginator,
+ );
+ }
+ InterfaceMessage::SetStatusMenu(idx) => {
+ state.status_menu.selected = idx;
+ state.status_box.disabled = idx != 3;
+ if idx == 0 {
+ state.status_interface = state.sans_serif.clone();
+ state.status_box.text = state.sans_serif.clone();
+ } else if idx == 1 {
+ state.status_interface = state.serif.clone();
+ state.status_box.text = state.serif.clone();
+ } else if idx == 2 {
+ state.status_interface = state.monospace.clone();
+ state.status_box.text = state.monospace.clone();
+ }
+ save_preferred_fonts(
+ &state.sans_serif,
+ &state.serif,
+ &state.monospace,
+ &state.window_borders,
+ &state.status_interface,
+ &state.fuzzel,
+ &state.terminal,
+ &state.paginator,
+ );
+ }
+ InterfaceMessage::SetFuzzelMenu(idx) => {
+ state.fuzzel_menu.selected = idx;
+ state.fuzzel_box.disabled = idx != 3;
+ if idx == 0 {
+ state.fuzzel = state.sans_serif.clone();
+ state.fuzzel_box.text = state.sans_serif.clone();
+ } else if idx == 1 {
+ state.fuzzel = state.serif.clone();
+ state.fuzzel_box.text = state.serif.clone();
+ } else if idx == 2 {
+ state.fuzzel = state.monospace.clone();
+ state.fuzzel_box.text = state.monospace.clone();
+ }
+ save_preferred_fonts(
+ &state.sans_serif,
+ &state.serif,
+ &state.monospace,
+ &state.window_borders,
+ &state.status_interface,
+ &state.fuzzel,
+ &state.terminal,
+ &state.paginator,
+ );
+ }
+ InterfaceMessage::SetTerminalMenu(idx) => {
+ state.terminal_menu.selected = idx;
+ state.terminal_box.disabled = idx != 3;
+ if idx == 0 {
+ state.terminal = state.sans_serif.clone();
+ state.terminal_box.text = state.sans_serif.clone();
+ } else if idx == 1 {
+ state.terminal = state.serif.clone();
+ state.terminal_box.text = state.serif.clone();
+ } else if idx == 2 {
+ state.terminal = state.monospace.clone();
+ state.terminal_box.text = state.monospace.clone();
+ }
+ save_preferred_fonts(
+ &state.sans_serif,
+ &state.serif,
+ &state.monospace,
+ &state.window_borders,
+ &state.status_interface,
+ &state.fuzzel,
+ &state.terminal,
+ &state.paginator,
+ );
+ }
+ InterfaceMessage::SetPaginatorMenu(idx) => {
+ state.paginator_menu.selected = idx;
+ state.paginator_box.disabled = idx != 3;
+ if idx == 0 {
+ state.paginator = state.sans_serif.clone();
+ state.paginator_box.text = state.sans_serif.clone();
+ } else if idx == 1 {
+ state.paginator = state.serif.clone();
+ state.paginator_box.text = state.serif.clone();
+ } else if idx == 2 {
+ state.paginator = state.monospace.clone();
+ state.paginator_box.text = state.monospace.clone();
+ }
+ save_preferred_fonts(
+ &state.sans_serif,
+ &state.serif,
+ &state.monospace,
+ &state.window_borders,
+ &state.status_interface,
+ &state.fuzzel,
+ &state.terminal,
+ &state.paginator,
+ );
+ }
+ InterfaceMessage::SetBordersSize(val) => {
+ state.borders_size_box.value = val;
+ write_config_value("border_font_size", &val.to_string());
+ send_ipc_command(&format!("layout border_font_size {}", val));
+ }
+ InterfaceMessage::SetStatusSize(val) => {
+ state.status_size_box.value = val;
+ write_status_size(val as u16);
+ }
+ InterfaceMessage::SetFuzzelSize(val) => {
+ state.fuzzel_size_box.value = val;
+ write_fuzzel_size(val as u16);
+ }
+ InterfaceMessage::SetTerminalSize(val) => {
+ state.terminal_size_box.value = val;
+ write_terminal_size(val as u16);
+ }
+ InterfaceMessage::SetPaginatorSize(val) => {
+ state.paginator_size_box.value = val;
+ write_paginator_size(val as u16);
}
}
}
@@ -683,4 +1883,30 @@ mod tests {
// Clean up
let _ = fs::remove_file(path_str);
}
+
+ #[test]
+ fn test_parse_font_for_alias() {
+ let content = r#"<?xml version="1.0"?>
+<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
+<fontconfig>
+ <dir>~/Dropbox/Fonts</dir>
+ <match target="pattern">
+ <test qual="any" name="family"><string>sans-serif</string></test>
+ <edit name="family" mode="assign" binding="same">
+ <string>Adwaita Sans</string>
+ </edit>
+ </match>
+ <match target="pattern">
+ <test qual="any" name="family"><string>monospace</string></test>
+ <edit name="family" mode="assign" binding="same">
+ <string>Berkeley Mono</string>
+ </edit>
+ </match>
+</fontconfig>
+"#;
+
+ assert_eq!(parse_font_for_alias(content, "sans-serif"), Some("Adwaita Sans".to_string()));
+ assert_eq!(parse_font_for_alias(content, "monospace"), Some("Berkeley Mono".to_string()));
+ assert_eq!(parse_font_for_alias(content, "serif"), None);
+ }
}
diff --git a/src/pages/layout.rs b/src/pages/layout.rs
index c38c5b6..4018cb1 100644
--- a/src/pages/layout.rs
+++ b/src/pages/layout.rs
@@ -2,8 +2,9 @@ use std::fs;
use std::io::Write;
use crate::app::PageContent;
-use clear_ui::layout::{render_widget, Section, PageLayoutBuilder, LayoutStrategy};
-use clear_ui::widget::{Spinbox, Dropdown};
+use clear_ui::layout::{render_widget, PageLayoutBuilder, LayoutStrategy};
+use clear_ui::widget::{Spinbox, Dropdown, LayoutPreview, PreviewLayoutMode};
+
const CONFIG_PATH: &str = "/home/lsgalante/.config/ccec/config.toml";
@@ -71,6 +72,9 @@ pub struct LayoutState {
pub status_height_spinbox: Spinbox,
pub transition_duration_spinbox: Spinbox,
pub tag_layout_menus: Vec<Dropdown>,
+ pub side_panel_behavior_menu: Dropdown,
+ pub side_panel_width: u16,
+ pub side_panel_width_spinbox: Spinbox,
}
impl Default for LayoutState {
@@ -105,6 +109,12 @@ impl Default for LayoutState {
0
).with_label(&format!("Tag {}", i))
}).collect(),
+ side_panel_behavior_menu: Dropdown::new(
+ vec!["Above".to_string(), "Inline".to_string()],
+ 1,
+ ).with_label("Behavior"),
+ side_panel_width: 360,
+ side_panel_width_spinbox: Spinbox::new(360, 0, 2000, 10),
}
}
}
@@ -119,6 +129,8 @@ pub enum LayoutMessage {
SetTransitionDuration(u16),
SetStatusHeight(u16),
SetTagLayout(usize, usize),
+ SetSidePanelBehavior(usize),
+ SetSidePanelWidth(u16),
Refreshed(LayoutState),
}
@@ -149,6 +161,15 @@ pub fn read_layout_config() -> LayoutState {
Dropdown::new(dropdown_options.clone(), idx).with_label(&format!("Tag {}", i))
}).collect();
+ let side_panel_behavior = parse_string_from(&content, "side_panel_behavior", "inline");
+ let side_panel_behavior_idx = if side_panel_behavior == "above" { 0 } else { 1 };
+ let side_panel_behavior_menu = Dropdown::new(
+ vec!["Above".to_string(), "Inline".to_string()],
+ side_panel_behavior_idx,
+ ).with_label("Behavior");
+
+ let spw = parse_u16_from(&content, "side_panel_width", 360);
+
LayoutState {
fullscreen_border_width: fs,
cascade_border_width: ca,
@@ -168,6 +189,9 @@ pub fn read_layout_config() -> LayoutState {
status_height_spinbox: Spinbox::new(sh as i32, 0, 100, 1),
transition_duration_spinbox: Spinbox::new(td as i32, 0, 2000, 50),
tag_layout_menus,
+ side_panel_behavior_menu,
+ side_panel_width: spw,
+ side_panel_width_spinbox: Spinbox::new(spw as i32, 0, 2000, 10),
}
}
@@ -275,6 +299,17 @@ fn parse_u16_from(content: &str, key: &str, default: u16) -> u16 {
default
}
+fn parse_string_from(content: &str, key: &str, default: &str) -> String {
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if let Some(rest) = trimmed.strip_prefix(key) {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+ return rest.trim_end_matches('"').trim().to_string();
+ }
+ }
+ default.to_string()
+}
+
fn write_config_value(key: &str, value: &str) -> bool {
let content = fs::read_to_string(CONFIG_PATH).unwrap_or_default();
let new_line = format!("{} = {}", key, value);
@@ -337,6 +372,7 @@ fn apply_edge_gap(val: u16) {
}
#[derive(Debug, Clone)]
+#[allow(dead_code)]
struct PreviewWindow {
app_id: String,
title: String,
@@ -350,6 +386,7 @@ struct PreviewWindow {
layout_mode: String,
}
+#[allow(dead_code)]
struct LayoutStatusInfo {
active_tags: u32,
focused_tags: u32,
@@ -476,56 +513,20 @@ fn read_current_layout_status() -> LayoutStatusInfo {
}
}
-fn get_short_app_name(app_id: &str) -> String {
- let lower = app_id.to_lowercase();
- if lower.contains("foot") || lower.contains("terminal") || lower.contains("kitty") || lower.contains("alacritty") {
- "Term".to_string()
- } else if lower.contains("firefox") || lower.contains("chrome") || lower.contains("qutebrowser") || lower.contains("browser") {
- "Web".to_string()
- } else if lower.contains("code") || lower.contains("vscodium") || lower.contains("neovim") || lower.contains("nvim") {
- "Code".to_string()
- } else if lower.contains("spotify") || lower.contains("music") {
- "Musc".to_string()
- } else if lower.contains("discord") {
- "Disc".to_string()
- } else if lower.contains("interface") {
- "Intf".to_string()
- } else if lower.is_empty() {
- "Win".to_string()
- } else {
- let mut s = lower;
- s.truncate(4);
- if let Some(first) = s.chars().next() {
- let first_upper = first.to_uppercase().to_string();
- format!("{}{}", first_upper, &s[first.len_utf8()..])
- } else {
- "Win".to_string()
- }
- }
-}
-
-struct SimNode {
- x: f32,
- y: f32,
- w: f32,
- h: f32,
- label: String,
-}
-
pub fn view(state: &mut LayoutState, cx: f32, cy: f32, cw: f32, ch: f32, sec_focused: &[bool], layout: &mut dyn LayoutStrategy) -> PageContent {
let mut final_pc = PageContent::new();
let sec_w = 320.0f32;
- let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(5);
+ let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(8);
// Current Layout Section (Read-only visual preview)
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec_cl = Section::new(pc, rx, ry, sec_w, "Current Layout");
+ builder.add_section(&mut final_pc, "Current Layout", false, |sec_cl| {
sec_cl.spacing(8.0);
let info = read_current_layout_status();
let card_w = (sec_w - 24.0) / 2.0;
let card_h = 135.0;
+ let rx = sec_cl.left;
for tag_idx in 0..4 {
let col = tag_idx % 2;
@@ -533,188 +534,98 @@ pub fn view(state: &mut LayoutState, cx: f32, cy: f32, cw: f32, ch: f32, sec_foc
let tx = rx + 8.0 + col as f32 * (card_w + 8.0);
let ty = sec_cl.ay() + row as f32 * (card_h + 8.0);
- // Draw card background
let is_active = (info.active_tags & (1 << tag_idx)) != 0;
- let bg_col = if is_active { [0.12, 0.24, 0.14, 0.55] } else { [0.08, 0.08, 0.12, 0.35] };
- let border_col = if is_active { [0.36, 0.56, 0.38, 0.95] } else { [0.24, 0.24, 0.28, 0.45] };
- pc.rect(bg_col, tx, ty, card_w, card_h);
- // Card border
- pc.rect(border_col, tx, ty, card_w, 1.0);
- pc.rect(border_col, tx, ty + card_h - 1.0, card_w, 1.0);
- pc.rect(border_col, tx, ty, 1.0, card_h);
- pc.rect(border_col, tx + card_w - 1.0, ty, 1.0, card_h);
-
- // Tag index text
- pc.text(&format!("TAG {}", tag_idx + 1), tx + 8.0, ty + 8.0, 10.0, [0.55, 0.55, 0.60, 1.0]);
-
- // Layout Name
let layout_idx = state.tag_layout_menus.get(tag_idx).map(|m| m.selected).unwrap_or(0);
- let layout_name = match layout_idx {
- 1 => "Cascade",
- 2 => "Stack",
- 3 => "Grid",
- 4 => "L-Tiled",
- 5 => "R-Tiled",
- 6 => "Equal",
- 7 => "Spiral",
- 8 => "Floating",
- _ => "Fullscreen",
+ let mode = match layout_idx {
+ 1 => PreviewLayoutMode::Cascade,
+ 2 => PreviewLayoutMode::Stack,
+ 3 => PreviewLayoutMode::Grid,
+ 4 => PreviewLayoutMode::LeftTiled,
+ 5 => PreviewLayoutMode::RightTiled,
+ 6 => PreviewLayoutMode::Equal,
+ 7 => PreviewLayoutMode::Spiral,
+ 8 => PreviewLayoutMode::Floating,
+ _ => PreviewLayoutMode::Fullscreen,
};
- pc.text(layout_name, tx + 8.0, ty + 20.0, 13.0, [0.90, 0.90, 0.95, 1.0]);
-
- // Visual nodes layout preview inside card
- let preview_x = tx + 8.0;
- let preview_y = ty + 38.0;
- let preview_w = card_w - 16.0;
- let preview_h = card_h - 46.0;
-
- // Gray border for preview box
- pc.rect([0.16, 0.16, 0.20, 0.6], preview_x, preview_y, preview_w, preview_h);
- pc.rect([0.22, 0.22, 0.26, 0.8], preview_x, preview_y, preview_w, 1.0);
- pc.rect([0.22, 0.22, 0.26, 0.8], preview_x, preview_y + preview_h - 1.0, preview_w, 1.0);
- pc.rect([0.22, 0.22, 0.26, 0.8], preview_x, preview_y, 1.0, preview_h);
- pc.rect([0.22, 0.22, 0.26, 0.8], preview_x + preview_w - 1.0, preview_y, 1.0, preview_h);
-
- // Simulate layout windows preview
- let mut nodes = Vec::new();
- match layout_idx {
- 0 => { // Fullscreen
- nodes.push(SimNode { x: 2.0, y: 2.0, w: preview_w - 4.0, h: preview_h - 4.0, label: "F".to_string() });
- }
- 1 => { // Cascade
- nodes.push(SimNode { x: 2.0, y: 2.0, w: preview_w - 12.0, h: preview_h - 12.0, label: "1".to_string() });
- nodes.push(SimNode { x: 6.0, y: 6.0, w: preview_w - 12.0, h: preview_h - 12.0, label: "2".to_string() });
- nodes.push(SimNode { x: 10.0, y: 10.0, w: preview_w - 12.0, h: preview_h - 12.0, label: "3".to_string() });
- }
- 2 => { // Stack
- nodes.push(SimNode { x: 2.0, y: 2.0, w: preview_w - 4.0, h: preview_h - 4.0, label: "Stack".to_string() });
- }
- 3 => { // Grid
- let hw = (preview_w - 6.0) / 2.0;
- let hh = (preview_h - 6.0) / 2.0;
- nodes.push(SimNode { x: 2.0, y: 2.0, w: hw, h: hh, label: "1".to_string() });
- nodes.push(SimNode { x: 4.0 + hw, y: 2.0, w: hw, h: hh, label: "2".to_string() });
- nodes.push(SimNode { x: 2.0, y: 4.0 + hh, w: hw, h: hh, label: "3".to_string() });
- nodes.push(SimNode { x: 4.0 + hw, y: 4.0 + hh, w: hw, h: hh, label: "4".to_string() });
- }
- 4 => { // Left Tiled (Main window on left, stack on right)
- let mw = (preview_w - 6.0) * 0.55;
- let sw = (preview_w - 6.0) - mw;
- let sh = (preview_h - 6.0) / 2.0;
- nodes.push(SimNode { x: 2.0, y: 2.0, w: mw, h: preview_h - 4.0, label: "M".to_string() });
- nodes.push(SimNode { x: 4.0 + mw, y: 2.0, w: sw, h: sh, label: "1".to_string() });
- nodes.push(SimNode { x: 4.0 + mw, y: 4.0 + sh, w: sw, h: sh, label: "2".to_string() });
- }
- 5 => { // Right Tiled (Main window on right, stack on left)
- let mw = (preview_w - 6.0) * 0.55;
- let sw = (preview_w - 6.0) - mw;
- let sh = (preview_h - 6.0) / 2.0;
- nodes.push(SimNode { x: 2.0, y: 2.0, w: sw, h: sh, label: "1".to_string() });
- nodes.push(SimNode { x: 2.0, y: 4.0 + sh, w: sw, h: sh, label: "2".to_string() });
- nodes.push(SimNode { x: 4.0 + sw, y: 2.0, w: mw, h: preview_h - 4.0, label: "M".to_string() });
- }
- 6 => { // Equal (Split evenly horizontally)
- let ew = (preview_w - 8.0) / 3.0;
- nodes.push(SimNode { x: 2.0, y: 2.0, w: ew, h: preview_h - 4.0, label: "1".to_string() });
- nodes.push(SimNode { x: 4.0 + ew, y: 2.0, w: ew, h: preview_h - 4.0, label: "2".to_string() });
- nodes.push(SimNode { x: 6.0 + 2.0 * ew, y: 2.0, w: ew, h: preview_h - 4.0, label: "3".to_string() });
- }
- 7 => { // Spiral (Fibonacci layout)
- let w1 = (preview_w - 6.0) * 0.5;
- let w2 = (preview_w - 6.0) - w1;
- let h2 = (preview_h - 6.0) * 0.5;
- nodes.push(SimNode { x: 2.0, y: 2.0, w: w1, h: preview_h - 4.0, label: "1".to_string() });
- nodes.push(SimNode { x: 4.0 + w1, y: 2.0, w: w2, h: h2, label: "2".to_string() });
- nodes.push(SimNode { x: 4.0 + w1, y: 4.0 + h2, w: w2 * 0.5, h: h2, label: "3".to_string() });
- nodes.push(SimNode { x: 4.0 + w1 + w2 * 0.5, y: 4.0 + h2, w: w2 * 0.5, h: h2, label: "4".to_string() });
- }
- 8 => { // Floating (Scatter windows randomly)
- nodes.push(SimNode { x: 4.0, y: 6.0, w: preview_w * 0.45, h: preview_h * 0.5, label: "1".to_string() });
- nodes.push(SimNode { x: preview_w * 0.4, y: 12.0, w: preview_w * 0.5, h: preview_h * 0.45, label: "2".to_string() });
- nodes.push(SimNode { x: 8.0, y: preview_h * 0.4, w: preview_w * 0.55, h: preview_h * 0.5, label: "3".to_string() });
- }
- _ => {}
- }
-
- // Draw simulated layout preview rectangles
- for node in nodes {
- let rect_x = preview_x + node.x;
- let rect_y = preview_y + node.y;
-
- // Semi-transparent blue for node backgrounds, slightly highlighted if active tag
- let node_bg = if is_active { [0.30, 0.45, 0.65, 0.45] } else { [0.20, 0.24, 0.30, 0.25] };
- let node_border = if is_active { [0.45, 0.65, 0.90, 0.85] } else { [0.35, 0.40, 0.45, 0.55] };
-
- pc.rect(node_bg, rect_x, rect_y, node.w, node.h);
-
- // Draw node border lines
- pc.rect(node_border, rect_x, rect_y, node.w, 1.0);
- pc.rect(node_border, rect_x, rect_y + node.h - 1.0, node.w, 1.0);
- pc.rect(node_border, rect_x, rect_y, 1.0, node.h);
- pc.rect(node_border, rect_x + node.w - 1.0, rect_y, 1.0, node.h);
-
- // Center the label text inside the simulated node
- let text_sz = 9.0;
- let text_w = node.label.len() as f32 * 6.0;
- let text_color = if is_active { [0.95, 0.95, 1.0, 0.95] } else { [0.70, 0.70, 0.75, 0.75] };
- let tx_offset = ((node.w - text_w) / 2.0).max(1.0);
- let ty_offset = ((node.h - text_sz) / 2.0).max(1.0);
-
- pc.text(&node.label, rect_x + tx_offset, rect_y + ty_offset, text_sz, text_color);
- }
+
+ let mut preview = LayoutPreview::new(mode)
+ .with_active(is_active)
+ .with_label(&format!("TAG {}", tag_idx + 1));
+ render_widget(sec_cl.pc, &mut preview, tx, ty, card_w, card_h);
}
sec_cl.content_y += 2.0 * (card_h + 8.0) + 4.0;
- sec_cl.finish(pc)
});
- // 1. Border Width Section
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec_bw = Section::new(pc, rx, ry, sec_w, "Border Width");
- sec_bw.spacing(8.0);
- for (i, param) in WidthParam::ALL.iter().enumerate() {
- state.spinboxes[i].set_label(param.label());
- sec_bw.widget(pc, &mut state.spinboxes[i], 14.0, 200.0, 26.0);
- sec_bw.spacing(8.0);
- }
- sec_bw.finish_focused(pc, sec_focused.first().copied().unwrap_or(false))
+ // 1. Fullscreen Section
+ builder.add_section(&mut final_pc, "Fullscreen", sec_focused.get(0).copied().unwrap_or(false), |sec_fs| {
+ sec_fs.spacing(8.0);
+ state.spinboxes[0].set_label("Border Width");
+ sec_fs.widget(&mut state.spinboxes[0], 14.0, 200.0, 44.0);
+ sec_fs.spacing(8.0);
});
// 2. Cascade Section
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec_cascade = Section::new(pc, rx, ry, sec_w, "Cascade");
+ builder.add_section(&mut final_pc, "Cascade", sec_focused.get(1).copied().unwrap_or(false), |sec_cascade| {
+ sec_cascade.spacing(8.0);
+ state.spinboxes[1].set_label("Border Width");
+ sec_cascade.widget(&mut state.spinboxes[1], 14.0, 200.0, 44.0);
sec_cascade.spacing(8.0);
state.cascade_offset_spinbox.set_label("Offset");
- sec_cascade.widget(pc, &mut state.cascade_offset_spinbox, 14.0, 200.0, 26.0);
+ sec_cascade.widget(&mut state.cascade_offset_spinbox, 14.0, 200.0, 44.0);
sec_cascade.spacing(8.0);
state.edge_gap_spinbox.set_label("Edge Gap");
- sec_cascade.widget(pc, &mut state.edge_gap_spinbox, 14.0, 200.0, 26.0);
+ sec_cascade.widget(&mut state.edge_gap_spinbox, 14.0, 200.0, 44.0);
sec_cascade.spacing(8.0);
state.top_gap_spinbox.set_label("Top Gap");
- sec_cascade.widget(pc, &mut state.top_gap_spinbox, 14.0, 200.0, 26.0);
+ sec_cascade.widget(&mut state.top_gap_spinbox, 14.0, 200.0, 44.0);
sec_cascade.spacing(8.0);
- sec_cascade.finish_focused(pc, sec_focused.get(1).copied().unwrap_or(false))
});
- // 3. Movement Section
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut movement_sec = Section::new(pc, rx, ry, sec_w, "Movement");
+ // 3. Grid Section
+ builder.add_section(&mut final_pc, "Grid", sec_focused.get(2).copied().unwrap_or(false), |sec_grid| {
+ sec_grid.spacing(8.0);
+ state.spinboxes[2].set_label("Border Width");
+ sec_grid.widget(&mut state.spinboxes[2], 14.0, 200.0, 44.0);
+ sec_grid.spacing(8.0);
+ state.grid_gap_spinbox.set_label("Gap");
+ sec_grid.widget(&mut state.grid_gap_spinbox, 14.0, 200.0, 44.0);
+ sec_grid.spacing(8.0);
+ });
+
+ // 4. Floating Section
+ builder.add_section(&mut final_pc, "Floating", sec_focused.get(3).copied().unwrap_or(false), |sec_float| {
+ sec_float.spacing(8.0);
+ state.spinboxes[3].set_label("Border Width");
+ sec_float.widget(&mut state.spinboxes[3], 14.0, 200.0, 44.0);
+ sec_float.spacing(8.0);
+ });
+
+ // 5. Movement Section
+ builder.add_section(&mut final_pc, "Movement", sec_focused.get(4).copied().unwrap_or(false), |movement_sec| {
movement_sec.spacing(8.0);
state.transition_duration_spinbox.set_label("Duration (ms)");
- movement_sec.widget(pc, &mut state.transition_duration_spinbox, 14.0, 200.0, 26.0);
+ movement_sec.widget(&mut state.transition_duration_spinbox, 14.0, 200.0, 44.0);
movement_sec.spacing(8.0);
- movement_sec.finish_focused(pc, sec_focused.get(2).copied().unwrap_or(false))
});
- // 4. Default Layouts Section
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut default_layouts_sec = Section::new(pc, rx, ry, sec_w, "Default Layouts");
+ // 6. Default Layouts Section
+ builder.add_section(&mut final_pc, "Default Layouts", sec_focused.get(5).copied().unwrap_or(false), |default_layouts_sec| {
default_layouts_sec.spacing(8.0);
for i in 0..4 {
- default_layouts_sec.widget(pc, &mut state.tag_layout_menus[i], 14.0, 200.0, 26.0);
+ default_layouts_sec.widget(&mut state.tag_layout_menus[i], 14.0, 200.0, 44.0);
default_layouts_sec.spacing(8.0);
}
- default_layouts_sec.finish_focused(pc, sec_focused.get(3).copied().unwrap_or(false))
+ });
+
+ // 7. Side Panel Section
+ builder.add_section(&mut final_pc, "Side Panel", sec_focused.get(6).copied().unwrap_or(false), |side_panel_sec| {
+ side_panel_sec.spacing(8.0);
+ side_panel_sec.widget(&mut state.side_panel_behavior_menu, 14.0, 200.0, 44.0);
+ side_panel_sec.spacing(8.0);
+ state.side_panel_width_spinbox.set_label("Default Width");
+ side_panel_sec.widget(&mut state.side_panel_width_spinbox, 14.0, 200.0, 44.0);
+ side_panel_sec.spacing(8.0);
});
final_pc
@@ -789,6 +700,21 @@ pub fn update(state: &mut LayoutState, msg: LayoutMessage) {
send_ipc_command(&format!("tag-layout {} {}", tag, mode_str));
}
}
+ LayoutMessage::SetSidePanelBehavior(idx) => {
+ if idx < 2 {
+ state.side_panel_behavior_menu.selected = idx;
+ let val = if idx == 0 { "above" } else { "inline" };
+ write_config_value("side_panel_behavior", &format!("\"{}\"", val));
+ send_ipc_command(&format!("layout side_panel_behavior {}", val));
+ }
+ }
+ LayoutMessage::SetSidePanelWidth(v) => {
+ let val = v.min(2000);
+ state.side_panel_width = val;
+ state.side_panel_width_spinbox.value = val as i32;
+ write_config_value("side_panel_width", &val.to_string());
+ send_ipc_command(&format!("layout side_panel_width {}", val));
+ }
LayoutMessage::Refreshed(new) => { *state = new; }
}
}
@@ -804,6 +730,20 @@ mod tests {
assert_eq!(modes, vec!["cascade", "cascade", "cascade", "cascade"]);
}
+ #[test]
+ fn test_parse_side_panel_width_default() {
+ let content = "";
+ let width = parse_u16_from(content, "side_panel_width", 360);
+ assert_eq!(width, 360);
+ }
+
+ #[test]
+ fn test_parse_side_panel_width_explicit() {
+ let content = "side_panel_width = 450";
+ let width = parse_u16_from(content, "side_panel_width", 360);
+ assert_eq!(width, 450);
+ }
+
#[test]
fn test_parse_tag_layouts_single() {
let content = r#"
diff --git a/src/pages/mod.rs b/src/pages/mod.rs
index 6a4f46d..f9e9a9e 100644
--- a/src/pages/mod.rs
+++ b/src/pages/mod.rs
@@ -6,14 +6,9 @@ pub mod storage;
pub mod system_info;
pub mod keybindings;
pub mod input;
-pub mod status;
pub mod hardware;
-pub mod notifications;
-pub mod backup;
-pub mod typeface;
pub mod services;
pub mod interface;
-pub mod screensaver;
pub mod accounts;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -28,32 +23,22 @@ pub enum Page {
System,
Hardware,
Input,
- Status,
- Notifications,
- Backup,
- Typefaces,
Interface,
- Screensaver,
}
impl Page {
- pub const ALL: [Page; 16] = [
+ pub const ALL: [Page; 11] = [
Page::Accounts,
Page::Audio,
- Page::Backup,
Page::Interface,
Page::Display,
Page::Input,
Page::Layout,
- Page::Notifications,
Page::Hardware,
Page::Radios,
Page::Services,
- Page::Status,
Page::Storage,
Page::System,
- Page::Typefaces,
- Page::Screensaver,
];
pub fn label(self) -> &'static str {
@@ -68,12 +53,7 @@ impl Page {
Page::System => "System",
Page::Hardware => "Hardware",
Page::Input => "Input",
- Page::Status => "Status",
- Page::Notifications => "Notifications",
- Page::Backup => "Backup",
- Page::Typefaces => "Typefaces",
Page::Interface => "Interface",
- Page::Screensaver => "Screensaver",
}
}
diff --git a/src/pages/network.rs b/src/pages/network.rs
index 798bce4..1808140 100644
--- a/src/pages/network.rs
+++ b/src/pages/network.rs
@@ -1,5 +1,5 @@
-use crate::app::{AppAction, PageContent};
-use clear_ui::layout::{Section, PageLayoutBuilder, LayoutStrategy};
+use crate::app::{AppAction, PageContent, SectionContextExt};
+use clear_ui::layout::{render_widget, PageLayoutBuilder, LayoutStrategy};
use clear_ui::widget::ScrollingList;
#[derive(Debug, Clone)]
@@ -250,18 +250,18 @@ pub fn view(state: &mut NetworkState, cx: f32, cy: f32, cw: f32, ch: f32, root_f
let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(2);
// ── WiFi ──
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec = Section::new(pc, rx, ry, sec_w, "WiFi");
+ builder.add_section(&mut final_pc, "WiFi", root_focused, |sec| {
+ let rx = sec.left;
if !state.loaded {
- sec.text(pc, "Loading WiFi interfaces...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec.text("Loading WiFi interfaces...", 12.0, 0.0, 12.0, TEXT_DIM);
sec.spacing(18.0);
} 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;
- pc.button(if state.wifi_enabled { "ON" } else { "OFF" },
+ 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));
@@ -274,23 +274,23 @@ pub fn view(state: &mut NetworkState, cx: f32, cy: f32, cw: f32, ch: f32, root_f
} else {
state.connected_ssid.clone()
};
- sec.text(pc, &format!("Connected: {}", ssid_truncated), 14.0, 0.0, 13.0, ACCENT);
+ sec.text(&format!("Connected: {}", ssid_truncated), 14.0, 0.0, 13.0, ACCENT);
sec.spacing(18.0);
if sec_w < 220.0 {
- sec.text(pc, &format!("Signal: {}%", state.signal_strength), 14.0, 0.0, 12.0, TEXT_DIM);
+ 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(pc, &format!("IP: {}", state.ip_address), 14.0, 0.0, 12.0, TEXT_DIM);
+ sec.text(&format!("IP: {}", state.ip_address), 14.0, 0.0, 12.0, TEXT_DIM);
sec.spacing(16.0);
}
} else {
- sec.text(pc, &format!("Signal: {}% IP: {}", state.signal_strength, state.ip_address),
+ sec.text(&format!("Signal: {}% IP: {}", state.signal_strength, state.ip_address),
14.0, 0.0, 12.0, TEXT_DIM);
sec.spacing(16.0);
}
} else if state.wifi_enabled {
- sec.text(pc, "Not connected", 14.0, 0.0, 12.0, TEXT_DIM);
+ sec.text("Not connected", 14.0, 0.0, 12.0, TEXT_DIM);
sec.spacing(16.0);
}
@@ -300,7 +300,7 @@ pub fn view(state: &mut NetworkState, cx: f32, cy: f32, cw: f32, ch: f32, root_f
let list_box_w = sec_w - 24.0;
let list_box_h = 160.0;
- clear_ui::layout::render_widget(pc, &mut state.wifi_list_box, list_box_x, list_box_y, list_box_w, list_box_h);
+ render_widget(sec.pc, &mut state.wifi_list_box, list_box_x, list_box_y, list_box_w, list_box_h);
state.wifi_list_box.update_bounds(state.available.len(), list_box_y, list_box_h);
@@ -317,7 +317,7 @@ 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;
- pc.button(&label, list_box_x + 4.0, draw_y, btn_w, 26.0,
+ sec.button(&label, list_box_x + 4.0, 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())));
@@ -326,32 +326,31 @@ pub fn view(state: &mut NetworkState, cx: f32, cy: f32, cw: f32, ch: f32, root_f
sec.content_y += list_box_h + 8.0;
}
}
- sec.finish_focused(pc, root_focused)
});
// ── Bluetooth ──
- builder.add_section(&mut final_pc, |pc, rx, ry| {
+ builder.add_section_with_width(&mut final_pc, sec_w * 2.0, "Bluetooth", false, |sec| {
let bt_sec_w = sec_w * 2.0;
- let mut sec = Section::new(pc, rx, ry, bt_sec_w, "Bluetooth");
+ let rx = sec.left;
if !state.loaded {
- sec.text(pc, "Loading Bluetooth status...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec.text("Loading Bluetooth status...", 12.0, 0.0, 12.0, TEXT_DIM);
sec.spacing(18.0);
} else if !state.bt_installed {
- sec.text(pc, "Bluetooth tools (bluez) not installed", 14.0, 0.0, 12.0, TEXT_DIM);
+ sec.text("Bluetooth tools (bluez) not installed", 14.0, 0.0, 12.0, TEXT_DIM);
sec.spacing(18.0);
let btn_w = if bt_sec_w < 200.0 { 100.0 } else { 120.0 };
let yt = sec.ay();
- pc.button("Install Tools", rx + 12.0, yt, btn_w, 28.0,
+ sec.button("Install Tools", rx + 12.0, yt, btn_w, 28.0,
TOGGLE_ON, BTN_HOVER, WHITE,
AppAction::Radios(NetworkMessage::InstallBtTools));
sec.content_y += 34.0;
} else if !state.bt_service_active {
- sec.text(pc, "Bluetooth service is stopped", 14.0, 0.0, 12.0, TEXT_DIM);
+ sec.text("Bluetooth service is stopped", 14.0, 0.0, 12.0, TEXT_DIM);
sec.spacing(18.0);
let btn_w = if bt_sec_w < 200.0 { 100.0 } else { 120.0 };
let yt = sec.ay();
- pc.button("Start Service", rx + 12.0, yt, btn_w, 28.0,
+ sec.button("Start Service", rx + 12.0, yt, btn_w, 28.0,
TOGGLE_ON, BTN_HOVER, WHITE,
AppAction::Radios(NetworkMessage::StartBtService));
sec.content_y += 34.0;
@@ -362,11 +361,11 @@ pub fn view(state: &mut NetworkState, cx: f32, cy: f32, cw: f32, ch: f32, root_f
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;
- pc.button(if state.bt_enabled { "ON" } else { "OFF" },
+ 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));
- pc.button("Scan", sec.ax(scan_btn_x), yt, scan_btn_w, 28.0,
+ sec.button("Scan", sec.ax(scan_btn_x), yt, scan_btn_w, 28.0,
TOGGLE_OFF, BTN_HOVER, WHITE,
AppAction::Radios(NetworkMessage::BtScan));
sec.content_y += 34.0;
@@ -374,7 +373,7 @@ pub fn view(state: &mut NetworkState, cx: f32, cy: f32, cw: f32, ch: f32, root_f
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(pc, no_devices_msg, 14.0, 0.0, 12.0, TEXT_DIM);
+ sec.text(no_devices_msg, 14.0, 0.0, 12.0, TEXT_DIM);
}
} else {
for dev in &state.bt_devices {
@@ -415,8 +414,8 @@ pub fn view(state: &mut NetworkState, cx: f32, cy: f32, cw: f32, ch: f32, root_f
};
let yt = sec.ay();
- sec.text(pc, &label, 14.0, 0.0, 12.0, if dev.connected { ACCENT } else { TEXT_FG });
- pc.button(action_label, sec.ax(bt_sec_w - btn_w - 20.0), yt - 2.0, btn_w, 22.0,
+ 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,
if dev.connected { TOGGLE_OFF } else { TOGGLE_ON }, BTN_HOVER, WHITE,
if dev.connected {
AppAction::Radios(NetworkMessage::BtDisconnect(dev.mac.clone()))
@@ -427,7 +426,6 @@ pub fn view(state: &mut NetworkState, cx: f32, cy: f32, cw: f32, ch: f32, root_f
}
}
}
- sec.finish(pc)
});
final_pc
diff --git a/src/pages/notifications.rs b/src/pages/notifications.rs
deleted file mode 100644
index cc70ceb..0000000
--- a/src/pages/notifications.rs
+++ /dev/null
@@ -1,470 +0,0 @@
-use std::fs;
-use std::io::Write;
-
-use crate::app::{AppAction, PageContent};
-use clear_ui::layout::{Section, PageLayoutBuilder, LayoutStrategy};
-use clear_ui::widget::{Toggle, Spinbox, Slider};
-
-const CONFIG_PATH: &str = "/home/lsgalante/.config/ccec/config.toml";
-
-fn get_socket_path() -> String {
- match std::env::var("WAYLAND_DISPLAY") {
- Ok(display) => format!("/tmp/ccec-{}.sock", display),
- Err(_) => "/tmp/ccec.sock".to_string(),
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct NotificationsState {
- pub enable: bool,
- pub enable_toggle: Toggle,
- pub bell: bool,
- pub bell_toggle: Toggle,
- pub duration: i32,
- pub duration_spinbox: Spinbox,
- pub opacity: f32,
- pub opacity_slider: Slider,
-}
-
-impl Default for NotificationsState {
- fn default() -> Self {
- Self {
- enable: true,
- enable_toggle: Toggle::new().with_label("Enable Notifications"),
- bell: false,
- bell_toggle: Toggle::new().with_label("Play Bell Sound"),
- duration: 5,
- duration_spinbox: Spinbox::new(5, 1, 60, 1)
- .with_label("Notification Duration")
- .with_unit("s"),
- opacity: 0.9,
- opacity_slider: Slider::new()
- .with_label("Transparency")
- .with_value(0.9),
- }
- }
-}
-
-#[derive(Debug, Clone)]
-pub enum NotificationsMessage {
- ToggleEnable,
- ToggleBell,
- SetDuration(i32),
- SetOpacity(f32),
- SendTestNotification,
- Refreshed(NotificationsState),
-}
-
-pub fn read_notifications_config() -> NotificationsState {
- let content = fs::read_to_string(CONFIG_PATH).unwrap_or_default();
- let enable = parse_notifications_enable(&content);
- let bell = parse_notifications_bell(&content);
- let duration = parse_notifications_duration(&content);
- let opacity = parse_transparency_opacity(&content);
- NotificationsState {
- enable,
- enable_toggle: Toggle::new().with_label("Enable Notifications"),
- bell,
- bell_toggle: Toggle::new().with_label("Play Bell Sound"),
- duration,
- duration_spinbox: Spinbox::new(duration, 1, 60, 1)
- .with_label("Notification Duration")
- .with_unit("s"),
- opacity,
- opacity_slider: Slider::new()
- .with_label("Transparency")
- .with_value(opacity),
- }
-}
-
-fn parse_notifications_enable(content: &str) -> bool {
- let mut in_section = false;
- for line in content.lines() {
- let trimmed = line.trim();
- if trimmed == "[notifications]" {
- in_section = true;
- continue;
- }
- if trimmed.starts_with('[') && in_section {
- break;
- }
- if in_section && trimmed.starts_with("enable") {
- if let Some(val) = trimmed.split('=').nth(1) {
- return val.trim() == "true";
- }
- }
- }
- true // default to true
-}
-
-fn parse_notifications_bell(content: &str) -> bool {
- let mut in_section = false;
- for line in content.lines() {
- let trimmed = line.trim();
- if trimmed == "[notifications]" {
- in_section = true;
- continue;
- }
- if trimmed.starts_with('[') && in_section {
- break;
- }
- if in_section && trimmed.starts_with("bell") {
- if let Some(val) = trimmed.split('=').nth(1) {
- return val.trim() == "true";
- }
- }
- }
- false // default to false
-}
-
-fn parse_notifications_duration(content: &str) -> i32 {
- let mut in_section = false;
- for line in content.lines() {
- let trimmed = line.trim();
- if trimmed == "[notifications]" {
- in_section = true;
- continue;
- }
- if trimmed.starts_with('[') && in_section {
- break;
- }
- if in_section && trimmed.starts_with("duration") {
- if let Some(val) = trimmed.split('=').nth(1) {
- if let Ok(d) = val.trim().parse::<i32>() {
- return d;
- }
- }
- }
- }
- 5 // default to 5 seconds
-}
-
-fn send_ipc_command(cmd: &str) {
- if let Ok(mut stream) = std::os::unix::net::UnixStream::connect(get_socket_path()) {
- let _ = stream.write_all(format!("{}\n", cmd).as_bytes());
- }
-}
-
-fn write_config_value(key: &str, value: &str) {
- let content = fs::read_to_string(CONFIG_PATH).unwrap_or_default();
- let new_line = format!("{} = {}", key, value);
-
- let mut found = false;
- let mut updated_lines = Vec::new();
- let mut in_section = false;
-
- for line in content.lines() {
- let trimmed = line.trim();
- if trimmed == "[notifications]" {
- in_section = true;
- updated_lines.push(line.to_string());
- continue;
- }
- if trimmed.starts_with('[') && in_section {
- in_section = false;
- }
- if in_section && trimmed.starts_with(key) {
- found = true;
- updated_lines.push(new_line.clone());
- } else {
- updated_lines.push(line.to_string());
- }
- }
-
- let mut updated = updated_lines.join("\n");
-
- if !found {
- let mut result = String::new();
- let has_section = content.lines().any(|l| l.trim() == "[notifications]");
- if has_section {
- let mut in_section = false;
- let mut inserted = false;
- for line in updated.lines() {
- if line.trim() == "[notifications]" {
- in_section = true;
- result.push_str(line);
- result.push('\n');
- continue;
- }
- if line.trim().starts_with('[') && in_section {
- if !inserted {
- result.push_str(&new_line);
- result.push('\n');
- inserted = true;
- }
- in_section = false;
- }
- result.push_str(line);
- result.push('\n');
- }
- if !inserted {
- result.push_str(&new_line);
- result.push('\n');
- }
- updated = result;
- } else {
- updated.push_str("\n[notifications]\n");
- updated.push_str(&new_line);
- updated.push_str("\n");
- }
- }
- let _ = fs::write(CONFIG_PATH, updated);
-}
-
-fn write_enable_notifications(enabled: bool) {
- write_config_value("enable", &enabled.to_string());
- send_ipc_command("reload");
-}
-
-const BTN_BG: [f32; 4] = [0.20, 0.40, 0.65, 1.0];
-const BTN_HOVER: [f32; 4] = [0.28, 0.50, 0.78, 1.0];
-const WHITE: [f32; 4] = [1.0, 1.0, 1.0, 1.0];
-
-fn parse_transparency_opacity(content: &str) -> f32 {
- let mut in_section = false;
- for line in content.lines() {
- let trimmed = line.trim();
- if trimmed == "[transparency]" {
- in_section = true;
- continue;
- }
- if trimmed.starts_with('[') && in_section {
- break;
- }
- if in_section && trimmed.starts_with("opacity") {
- if let Some(val) = trimmed.split('=').nth(1) {
- if let Ok(o) = val.trim().parse::<f32>() {
- return o.clamp(0.0, 1.0);
- }
- }
- }
- }
- 0.9 // default to 0.9
-}
-
-fn write_transparency_config_value(key: &str, value: &str) {
- let content = fs::read_to_string(CONFIG_PATH).unwrap_or_default();
- let new_line = format!("{} = {}", key, value);
-
- let mut found = false;
- let mut updated_lines = Vec::new();
- let mut in_section = false;
-
- for line in content.lines() {
- let trimmed = line.trim();
- if trimmed == "[transparency]" {
- in_section = true;
- updated_lines.push(line.to_string());
- continue;
- }
- if trimmed.starts_with('[') && in_section {
- in_section = false;
- }
- if in_section && trimmed.starts_with(key) {
- found = true;
- updated_lines.push(new_line.clone());
- } else {
- updated_lines.push(line.to_string());
- }
- }
-
- let mut updated = updated_lines.join("\n");
-
- if !found {
- let mut result = String::new();
- let has_section = content.lines().any(|l| l.trim() == "[transparency]");
- if has_section {
- let mut in_section = false;
- let mut inserted = false;
- for line in updated.lines() {
- if line.trim() == "[transparency]" {
- in_section = true;
- result.push_str(line);
- result.push('\n');
- continue;
- }
- if line.trim().starts_with('[') && in_section {
- if !inserted {
- result.push_str(&new_line);
- result.push('\n');
- inserted = true;
- }
- in_section = false;
- }
- result.push_str(line);
- result.push('\n');
- }
- if !inserted {
- result.push_str(&new_line);
- result.push('\n');
- }
- updated = result;
- } else {
- updated.push_str("\n[transparency]\n");
- updated.push_str(&new_line);
- updated.push_str("\n");
- }
- }
- let _ = fs::write(CONFIG_PATH, updated);
-}
-
-pub fn view(state: &mut NotificationsState, cx: f32, cy: f32, cw: f32, ch: f32, layout: &mut dyn LayoutStrategy) -> PageContent {
- let mut final_pc = PageContent::new();
- let sec_w = 320.0f32;
- let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(2);
-
- // ── System Notifications ──
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec = Section::new(pc, rx, ry, sec_w, "System Notifications");
-
- let toggle_w = 48.0;
- let toggle_h = 24.0;
- state.enable_toggle.set_toggled(state.enable);
- sec.widget(pc, &mut state.enable_toggle, 14.0, toggle_w, toggle_h);
- sec.spacing(8.0);
-
- state.bell_toggle.set_toggled(state.bell);
- sec.widget(pc, &mut state.bell_toggle, 14.0, toggle_w, toggle_h);
- sec.spacing(16.0);
-
- state.duration_spinbox.value = state.duration;
- state.duration_spinbox.set_label("Notification Duration");
- sec.widget(pc, &mut state.duration_spinbox, 14.0, 200.0, 26.0);
- sec.spacing(16.0);
-
- let btn_w = 160.0;
- let btn_h = 32.0;
- let btn_y = sec.ay();
- sec.row(1, 0.0, btn_h, |_, x, _| {
- pc.button(
- "Send Test Notification",
- x,
- btn_y,
- btn_w,
- btn_h,
- BTN_BG,
- BTN_HOVER,
- WHITE,
- AppAction::Notifications(NotificationsMessage::SendTestNotification),
- );
- });
- sec.spacing(12.0);
- sec.finish(pc)
- });
-
- // ── Transparency ──
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec2 = Section::new(pc, rx, ry, sec_w, "Transparency");
- state.opacity_slider.set_value(state.opacity);
- sec2.widget(pc, &mut state.opacity_slider, 14.0, 300.0, 20.0);
- sec2.spacing(12.0);
- sec2.finish(pc)
- });
-
- final_pc
-}
-
-pub fn update(state: &mut NotificationsState, msg: NotificationsMessage) {
- match msg {
- NotificationsMessage::ToggleEnable => {
- state.enable = !state.enable;
- write_enable_notifications(state.enable);
- }
- NotificationsMessage::ToggleBell => {
- state.bell = !state.bell;
- write_config_value("bell", &state.bell.to_string());
- }
- NotificationsMessage::SetDuration(d) => {
- state.duration = d;
- write_config_value("duration", &state.duration.to_string());
- }
- NotificationsMessage::SetOpacity(o) => {
- state.opacity = o;
- write_transparency_config_value("opacity", &format!("{:.2}", o));
- send_ipc_command("reload");
- }
- NotificationsMessage::SendTestNotification => {
- send_ipc_command("notify \"ccec\" \"System notifications are working correctly!\"");
- }
- NotificationsMessage::Refreshed(new) => {
- *state = new;
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_parse_notifications_enable_default() {
- assert!(parse_notifications_enable(""));
- assert!(parse_notifications_enable("[layout]\ngap = 18\n"));
- }
-
- #[test]
- fn test_parse_notifications_enable_explicit() {
- let content = "\
-[notifications]
-enable = false
-";
- assert!(!parse_notifications_enable(content));
-
- let content = "\
-[notifications]
-enable = true
-";
- assert!(parse_notifications_enable(content));
- }
-
- #[test]
- fn test_parse_notifications_enable_other_sections() {
- let content = "\
-[layout]
-enable = false
-
-[notifications]
-enable = true
-
-[input]
-enable = false
-";
- assert!(parse_notifications_enable(content));
-
- let content = "\
-[layout]
-enable = true
-
-[notifications]
-enable = false
-
-[input]
-enable = true
-";
- assert!(!parse_notifications_enable(content));
- }
-
- #[test]
- fn test_parse_notifications_duration_default() {
- assert_eq!(parse_notifications_duration(""), 5);
- assert_eq!(parse_notifications_duration("[notifications]\n"), 5);
- }
-
- #[test]
- fn test_parse_notifications_duration_explicit() {
- let content = "\
-[notifications]
-duration = 10
-";
- assert_eq!(parse_notifications_duration(content), 10);
- }
-
- #[test]
- fn test_view_layout_grid() {
- let mut state = NotificationsState::default();
- let mut layout = clear_ui::layout::ColumnLayout::new(20.0);
- let pc = view(&mut state, 10.0, 20.0, 800.0, 600.0, &mut layout);
- assert!(!pc.rects.is_empty() || !pc.texts.is_empty());
- }
-}
-
diff --git a/src/pages/screensaver.rs b/src/pages/screensaver.rs
deleted file mode 100644
index 9f17ef5..0000000
--- a/src/pages/screensaver.rs
+++ /dev/null
@@ -1,330 +0,0 @@
-use std::fs;
-use crate::app::{AppAction, PageContent};
-use clear_ui::layout::{Section, PageLayoutBuilder, LayoutStrategy};
-use clear_ui::widget::{Toggle, Spinbox, Dropdown};
-
-const CONFIG_PATH: &str = "/home/lsgalante/.config/ccec/config.toml";
-
-#[derive(Debug, Clone)]
-pub struct ScreensaverState {
- pub enable: bool,
- pub enable_toggle: Toggle,
- pub timeout: i32,
- pub timeout_spinbox: Spinbox,
- pub lock_screen: bool,
- pub lock_screen_toggle: Toggle,
- pub style: String,
- pub style_menu: Dropdown,
-}
-
-impl Default for ScreensaverState {
- fn default() -> Self {
- Self {
- enable: true,
- enable_toggle: Toggle::new().with_label("Enable Screensaver"),
- timeout: 10,
- timeout_spinbox: Spinbox::new(10, 1, 120, 1)
- .with_label("Screensaver Timeout")
- .with_unit("m"),
- lock_screen: true,
- lock_screen_toggle: Toggle::new().with_label("Lock Screen on Activation"),
- style: "starfield".to_string(),
- style_menu: Dropdown::new(
- vec!["Blank".to_string(), "Starfield".to_string(), "Matrix Rain".to_string()],
- 1,
- ).with_label("Screensaver Style"),
- }
- }
-}
-
-#[derive(Debug, Clone)]
-pub enum ScreensaverMessage {
- ToggleEnable,
- ToggleLockScreen,
- SetTimeout(i32),
- SetStyle(usize),
- StartPreview,
- Refreshed(ScreensaverState),
-}
-
-pub fn read_screensaver_config() -> ScreensaverState {
- let content = fs::read_to_string(CONFIG_PATH).unwrap_or_default();
- let enable = parse_screensaver_enable(&content);
- let timeout = parse_screensaver_timeout(&content);
- let lock_screen = parse_screensaver_lock_screen(&content);
- let style = parse_screensaver_style(&content);
-
- let style_idx = match style.to_lowercase().as_str() {
- "blank" => 0,
- "starfield" => 1,
- "matrix" => 2,
- _ => 1, // default to Starfield
- };
-
- ScreensaverState {
- enable,
- enable_toggle: Toggle::new().with_label("Enable Screensaver"),
- timeout,
- timeout_spinbox: Spinbox::new(timeout, 1, 120, 1)
- .with_label("Screensaver Timeout")
- .with_unit("m"),
- lock_screen,
- lock_screen_toggle: Toggle::new().with_label("Lock Screen on Activation"),
- style: style.clone(),
- style_menu: Dropdown::new(
- vec!["Blank".to_string(), "Starfield".to_string(), "Matrix Rain".to_string()],
- style_idx,
- ).with_label("Screensaver Style"),
- }
-}
-
-fn parse_screensaver_enable(content: &str) -> bool {
- let mut in_section = false;
- for line in content.lines() {
- let trimmed = line.trim();
- if trimmed == "[screensaver]" {
- in_section = true;
- continue;
- }
- if trimmed.starts_with('[') && in_section {
- break;
- }
- if in_section && trimmed.starts_with("enable") {
- if let Some(val) = trimmed.split('=').nth(1) {
- return val.trim() == "true";
- }
- }
- }
- true // default to true
-}
-
-fn parse_screensaver_lock_screen(content: &str) -> bool {
- let mut in_section = false;
- for line in content.lines() {
- let trimmed = line.trim();
- if trimmed == "[screensaver]" {
- in_section = true;
- continue;
- }
- if trimmed.starts_with('[') && in_section {
- break;
- }
- if in_section && trimmed.starts_with("lock_screen") {
- if let Some(val) = trimmed.split('=').nth(1) {
- return val.trim() == "true";
- }
- }
- }
- true // default to true
-}
-
-fn parse_screensaver_timeout(content: &str) -> i32 {
- let mut in_section = false;
- for line in content.lines() {
- let trimmed = line.trim();
- if trimmed == "[screensaver]" {
- in_section = true;
- continue;
- }
- if trimmed.starts_with('[') && in_section {
- break;
- }
- if in_section && trimmed.starts_with("timeout") {
- if let Some(val) = trimmed.split('=').nth(1) {
- if let Ok(t) = val.trim().parse::<i32>() {
- return t;
- }
- }
- }
- }
- 10 // default to 10 minutes
-}
-
-fn parse_screensaver_style(content: &str) -> String {
- let mut in_section = false;
- for line in content.lines() {
- let trimmed = line.trim();
- if trimmed == "[screensaver]" {
- in_section = true;
- continue;
- }
- if trimmed.starts_with('[') && in_section {
- break;
- }
- if in_section && trimmed.starts_with("style") {
- if let Some(val) = trimmed.split('=').nth(1) {
- return val.trim().trim_matches('"').to_string();
- }
- }
- }
- "starfield".to_string() // default to starfield
-}
-
-fn write_config_value(key: &str, value: &str) {
- let content = fs::read_to_string(CONFIG_PATH).unwrap_or_default();
- let new_line = format!("{} = {}", key, value);
-
- let mut found = false;
- let mut updated_lines = Vec::new();
- let mut in_section = false;
-
- for line in content.lines() {
- let trimmed = line.trim();
- if trimmed == "[screensaver]" {
- in_section = true;
- updated_lines.push(line.to_string());
- continue;
- }
- if trimmed.starts_with('[') && in_section {
- in_section = false;
- }
- if in_section && trimmed.starts_with(key) {
- found = true;
- updated_lines.push(new_line.clone());
- } else {
- updated_lines.push(line.to_string());
- }
- }
-
- let mut updated = updated_lines.join("\n");
-
- if !found {
- let mut result = String::new();
- let has_section = content.lines().any(|l| l.trim() == "[screensaver]");
- if has_section {
- let mut in_section = false;
- let mut inserted = false;
- for line in updated.lines() {
- if line.trim() == "[screensaver]" {
- in_section = true;
- result.push_str(line);
- result.push('\n');
- continue;
- }
- if line.trim().starts_with('[') && in_section {
- if !inserted {
- result.push_str(&new_line);
- result.push('\n');
- inserted = true;
- }
- in_section = false;
- }
- result.push_str(line);
- result.push('\n');
- }
- if !inserted {
- result.push_str(&new_line);
- result.push('\n');
- }
- updated = result;
- } else {
- updated.push_str("\n[screensaver]\n");
- updated.push_str(&new_line);
- updated.push_str("\n");
- }
- }
- let _ = fs::write(CONFIG_PATH, updated);
-}
-
-const BTN_BG: [f32; 4] = [0.20, 0.40, 0.65, 1.0];
-const BTN_HOVER: [f32; 4] = [0.28, 0.50, 0.78, 1.0];
-const WHITE: [f32; 4] = [1.0, 1.0, 1.0, 1.0];
-
-pub fn view(state: &mut ScreensaverState, cx: f32, cy: f32, cw: f32, ch: f32, layout: &mut dyn LayoutStrategy) -> PageContent {
- let mut final_pc = PageContent::new();
- let sec_w = 320.0f32;
- let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(1);
-
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec = Section::new(pc, rx, ry, sec_w, "Screensaver Settings");
-
- let toggle_w = 48.0;
- let toggle_h = 24.0;
-
- state.enable_toggle.set_toggled(state.enable);
- sec.widget(pc, &mut state.enable_toggle, 14.0, toggle_w, toggle_h);
- sec.spacing(8.0);
-
- state.lock_screen_toggle.set_toggled(state.lock_screen);
- sec.widget(pc, &mut state.lock_screen_toggle, 14.0, toggle_w, toggle_h);
- sec.spacing(16.0);
-
- state.timeout_spinbox.value = state.timeout;
- sec.widget(pc, &mut state.timeout_spinbox, 14.0, 200.0, 26.0);
- sec.spacing(16.0);
-
- sec.widget(pc, &mut state.style_menu, 14.0, 200.0, 26.0);
- sec.spacing(24.0);
-
- let btn_w = 160.0;
- let btn_h = 32.0;
- let btn_y = sec.ay();
- sec.row(1, 0.0, btn_h, |_, x, _| {
- pc.button(
- "Preview Screensaver",
- x,
- btn_y,
- btn_w,
- btn_h,
- BTN_BG,
- BTN_HOVER,
- WHITE,
- AppAction::Screensaver(ScreensaverMessage::StartPreview),
- );
- });
- sec.spacing(12.0);
- sec.finish(pc)
- });
-
- final_pc
-}
-
-pub fn update(state: &mut ScreensaverState, msg: ScreensaverMessage) {
- match msg {
- ScreensaverMessage::ToggleEnable => {
- state.enable = !state.enable;
- write_config_value("enable", &state.enable.to_string());
- }
- ScreensaverMessage::ToggleLockScreen => {
- state.lock_screen = !state.lock_screen;
- write_config_value("lock_screen", &state.lock_screen.to_string());
- }
- ScreensaverMessage::SetTimeout(t) => {
- state.timeout = t;
- write_config_value("timeout", &state.timeout.to_string());
- }
- ScreensaverMessage::SetStyle(idx) => {
- state.style_menu.selected = idx;
- let val = match idx {
- 0 => "blank",
- 1 => "starfield",
- 2 => "matrix",
- _ => "starfield",
- };
- state.style = val.to_string();
- write_config_value("style", &format!("\"{}\"", val));
- }
- ScreensaverMessage::StartPreview => {
- let style_flag = match state.style_menu.selected {
- 0 => "blank",
- 1 => "starfield",
- 2 => "matrix",
- _ => "starfield",
- };
-
- // Spawn screensaver tool from PATH or local directory
- std::process::Command::new("/home/lsgalante/Dropbox/Clear/cce-screenaver/target/debug/cce-screenaver")
- .arg(style_flag)
- .spawn()
- .or_else(|_| {
- std::process::Command::new("cce-screenaver")
- .arg(style_flag)
- .spawn()
- })
- .ok();
- }
- ScreensaverMessage::Refreshed(new) => {
- *state = new;
- }
- }
-}
diff --git a/src/pages/services.rs b/src/pages/services.rs
index 8e1092f..f6c33b3 100644
--- a/src/pages/services.rs
+++ b/src/pages/services.rs
@@ -1,9 +1,30 @@
-use crate::app::PageContent;
-use clear_ui::layout::{Section, PageLayoutBuilder, LayoutStrategy};
-use clear_ui::widget::{Widget, TextLabel, ScrollBox, ScrollingList, TextBox};
-use clear_ui::widget::{ElementState, KeyEvent, MouseButton, Key, NamedKey};
+use std::fs;
+use std::io::Write;
+use crate::app::{AppAction, PageContent, SectionContextExt};
+use clear_ui::layout::{PageLayoutBuilder, LayoutStrategy};
+use clear_ui::widget::{Element, ScrollingList, TextBox, StatusDot, DotStatus, InteractiveListItem, Toggle, Spinbox, Slider, Label};
+use crate::pages::interface::parse_u16_from;
+// ── Notifications Data and Settings Configuration ──
+#[derive(Debug, Clone)]
+pub struct NotificationsConfig {
+ pub enable: bool,
+ pub bell: bool,
+ pub duration: i32,
+ pub opacity: f32,
+}
+
+// ── Status Interface Data ──
+
+#[derive(Debug, Clone)]
+pub struct StatusData {
+ pub font_size: u16,
+ pub padding: u16,
+ pub separators: bool,
+ pub underline: bool,
+ pub running: bool,
+}
// ── Service Types and Page State ──
@@ -35,6 +56,29 @@ pub struct ServicesState {
pub active_tab: ServiceTab,
pub search_box: TextBox,
pub list_box: ScrollingList,
+ pub service_items: Vec<InteractiveListItem>,
+ pub notifications_loaded: bool,
+ pub notifications_enable: bool,
+ pub notifications_enable_toggle: Toggle,
+ pub notifications_bell: bool,
+ pub notifications_bell_toggle: Toggle,
+ pub notifications_duration: i32,
+ pub notifications_duration_spinbox: Spinbox,
+ pub notifications_opacity: f32,
+ pub notifications_opacity_slider: Slider,
+
+ // Status Interface fields
+ pub status_loaded: bool,
+ pub status_font_size: u16,
+ pub status_padding: u16,
+ pub status_separators: bool,
+ pub status_underline: bool,
+ pub status_running: bool,
+ pub status_label: Label,
+ pub status_size_label: Label,
+ pub status_separators_toggle: Toggle,
+ pub status_underline_toggle: Toggle,
+ pub status_padding_spinbox: Spinbox,
}
impl Default for ServicesState {
@@ -45,6 +89,33 @@ impl Default for ServicesState {
active_tab: ServiceTab::System,
search_box: TextBox::new(String::new()).with_label("Filter Services"),
list_box: ScrollingList::new(36.0, 6.0),
+ service_items: Vec::new(),
+ notifications_loaded: false,
+ notifications_enable: true,
+ notifications_enable_toggle: Toggle::new().with_label("Enable Notifications"),
+ notifications_bell: false,
+ notifications_bell_toggle: Toggle::new().with_label("Play Bell Sound"),
+ notifications_duration: 5,
+ notifications_duration_spinbox: Spinbox::new(5, 1, 60, 1)
+ .with_label("Notification Duration")
+ .with_unit("s"),
+ notifications_opacity: 0.9,
+ notifications_opacity_slider: Slider::new()
+ .with_label("Transparency")
+ .with_value(0.9),
+
+ // Status Interface default initialization
+ status_loaded: false,
+ status_font_size: 11,
+ status_padding: 8,
+ status_separators: true,
+ status_underline: true,
+ status_running: false,
+ status_label: Label::new("Status Interface: Stopped").with_font_size(14.0).with_color([170, 51, 51]),
+ status_size_label: Label::new("Font size: 11px").with_font_size(13.0).with_color([212, 212, 212]),
+ status_separators_toggle: Toggle::new().with_label("Show Separators"),
+ status_underline_toggle: Toggle::new().with_label("Show Underline"),
+ status_padding_spinbox: Spinbox::new(8, 0, 32, 1).with_label("Side Padding").with_unit("px"),
}
}
}
@@ -56,6 +127,21 @@ pub enum ServicesMessage {
Start(String, bool),
Stop(String, bool),
Restart(String, bool),
+ ToggleNotificationsEnable,
+ ToggleNotificationsBell,
+ SetNotificationsDuration(i32),
+ SetNotificationsOpacity(f32),
+ SendTestNotification,
+ NotificationsRefreshed(NotificationsConfig),
+
+ // Status Interface variants
+ StatusRefreshed(StatusData),
+ StatusFontSizeUp,
+ StatusFontSizeDown,
+ StatusToggleSeparators,
+ StatusToggleUnderline,
+ StatusReload,
+ StatusSetPadding(u16),
}
// ── Background Fetching ──
@@ -138,16 +224,14 @@ fn service_action(name: &str, action: &str, is_system: bool) {
const TEXT_DIM: [f32; 4] = [0.53, 0.53, 0.60, 1.0];
-pub fn view(state: &mut ServicesState, cx: f32, cy: f32, cw: f32, ch: f32, root_focused: bool, layout: &mut dyn LayoutStrategy) -> PageContent {
+pub fn view(state: &mut ServicesState, cx: f32, cy: f32, cw: f32, ch: f32, sec_focused: &[bool], layout: &mut dyn LayoutStrategy) -> PageContent {
let mut final_pc = PageContent::new();
let sec_w = 320.0f32;
- let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(1);
-
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec = Section::new(pc, rx, ry, sec_w, "Services");
+ let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(3);
+ builder.add_section(&mut final_pc, "Services", sec_focused.first().copied().unwrap_or(false), |sec| {
if !state.loaded {
- sec.text(pc, "Loading systemd services...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec.text("Loading systemd services...", 12.0, 0.0, 12.0, TEXT_DIM);
sec.spacing(18.0);
} else {
// Tab header buttons: System Services, User Services
@@ -161,9 +245,12 @@ pub fn view(state: &mut ServicesState, cx: f32, cy: f32, cw: f32, ch: f32, root_
let label1 = if tab_w < 110.0 { "System" } else { "System Services" };
let label2 = if tab_w < 110.0 { "User" } else { "User Services" };
- pc.button(
+ let tab_x1 = sec.left + 12.0;
+ let tab_x2 = sec.left + 12.0 + tab_w + 8.0;
+
+ sec.pc.button(
label1,
- rx + 12.0,
+ tab_x1,
tab_y,
tab_w,
tab_h,
@@ -173,9 +260,9 @@ pub fn view(state: &mut ServicesState, cx: f32, cy: f32, cw: f32, ch: f32, root_
crate::app::AppAction::Services(ServicesMessage::SetTab(ServiceTab::System)),
);
- pc.button(
+ sec.pc.button(
label2,
- rx + 12.0 + tab_w + 8.0,
+ tab_x2,
tab_y,
tab_w,
tab_h,
@@ -187,28 +274,28 @@ pub fn view(state: &mut ServicesState, cx: f32, cy: f32, cw: f32, ch: f32, root_
sec.content_y += tab_h + 12.0;
// Search textbox
- let search_y = sec.ay() + state.search_box.top_room();
+ let search_y = sec.ay();
let search_w = sec_w - 24.0;
- let search_h = 28.0;
+ let search_h = 46.0;
- state.search_box.set_row_rect(rx + 12.0, search_w);
+ state.search_box.set_row_rect(sec.left + 12.0, search_w);
clear_ui::layout::render_widget(
- pc,
+ sec.pc,
&mut state.search_box,
- rx + 12.0,
+ sec.left + 12.0,
search_y,
search_w,
search_h,
);
- sec.content_y += search_h + state.search_box.top_room() + 16.0;
+ sec.content_y += search_h + 16.0;
// Scroll box list
- let list_box_x = rx + 12.0;
+ let list_box_x = sec.left + 12.0;
let list_box_y = sec.ay();
let list_box_w = sec_w - 24.0;
let list_box_h = 360.0;
- clear_ui::layout::render_widget(pc, &mut state.list_box, list_box_x, list_box_y, list_box_w, list_box_h);
+ clear_ui::layout::render_widget(sec.pc, &mut state.list_box, list_box_x, list_box_y, list_box_w, list_box_h);
// Filter services
let query = if state.search_box.editing {
@@ -226,24 +313,16 @@ pub fn view(state: &mut ServicesState, cx: f32, cy: f32, cw: f32, ch: f32, root_
let item_h = state.list_box.item_height;
+ if state.service_items.len() != filtered_services.len() {
+ state.service_items.clear();
+ for _ in 0..filtered_services.len() {
+ state.service_items.push(InteractiveListItem::new(""));
+ }
+ }
+
for (idx, service) in filtered_services.iter().enumerate() {
if let Some(draw_y) = state.list_box.get_item_draw_y(idx, 4.0) {
- // Item background
- let bg_color = [0.08, 0.08, 0.12, 0.2];
- pc.rect(bg_color, list_box_x + 4.0, draw_y, list_box_w - 24.0, item_h);
-
- // Status indicator color
let is_active = service.active_state == "active" || service.sub_state == "running";
- let status_color = if service.active_state == "failed" {
- [0.85, 0.25, 0.25, 1.0] // failed = red
- } else if is_active {
- [0.25, 0.75, 0.35, 1.0] // active = green
- } else {
- [0.55, 0.55, 0.60, 1.0] // inactive/dead = gray
- };
-
- // Render status dot (small square)
- pc.rect(status_color, list_box_x + 14.0, draw_y + (item_h - 10.0) / 2.0, 10.0, 10.0);
// Control buttons: Start, Stop, Restart on the right
let is_small = sec_w < 350.0;
@@ -259,9 +338,6 @@ pub fn view(state: &mut ServicesState, cx: f32, cy: f32, cw: f32, ch: f32, root_
let btn_y = draw_y + (item_h - 22.0) / 2.0;
let btn_h = 22.0;
- // Service Name
- pc.text(&service.name, list_box_x + 32.0, draw_y + 4.0, 13.0, [0.90, 0.90, 0.95, 1.0]);
-
// Service Description (Truncate dynamically based on remaining space before Start button)
let text_max_w = (start_x - 8.0) - (list_box_x + 32.0);
let max_chars = ((text_max_w / 6.0) as usize).max(10);
@@ -271,7 +347,23 @@ pub fn view(state: &mut ServicesState, cx: f32, cy: f32, cw: f32, ch: f32, root_
} else {
desc.to_string()
};
- pc.text(&desc_truncated, list_box_x + 32.0, draw_y + 19.0, 11.0, [0.55, 0.55, 0.60, 1.0]);
+
+ // Render InteractiveListItem background and text labels
+ let item_btn = &mut state.service_items[idx];
+ item_btn.title = service.name.clone();
+ item_btn.subtitle = Some(desc_truncated);
+ clear_ui::layout::render_widget(sec.pc, item_btn, list_box_x + 24.0, draw_y, list_box_w - 44.0, item_h);
+
+ // Render StatusDot
+ let status_dot_state = if service.active_state == "failed" {
+ DotStatus::Error
+ } else if is_active {
+ DotStatus::Active
+ } else {
+ DotStatus::Inactive
+ };
+ let mut dot = StatusDot::new(status_dot_state);
+ clear_ui::layout::render_widget(sec.pc, &mut dot, list_box_x + 10.0, draw_y + (item_h - 10.0) / 2.0, 10.0, 10.0);
let active_txt = [0.90, 0.90, 0.95, 1.0];
let disabled_txt = [0.40, 0.40, 0.45, 1.0];
@@ -281,7 +373,7 @@ pub fn view(state: &mut ServicesState, cx: f32, cy: f32, cw: f32, ch: f32, root_
let restart_lbl = if is_small { "⟳" } else { "Restart" };
// Start button
- pc.button(
+ sec.pc.button(
start_lbl,
start_x,
btn_y,
@@ -294,7 +386,7 @@ pub fn view(state: &mut ServicesState, cx: f32, cy: f32, cw: f32, ch: f32, root_
);
// Stop button
- pc.button(
+ sec.pc.button(
stop_lbl,
stop_x,
btn_y,
@@ -307,7 +399,7 @@ pub fn view(state: &mut ServicesState, cx: f32, cy: f32, cw: f32, ch: f32, root_
);
// Restart button
- pc.button(
+ sec.pc.button(
restart_lbl,
restart_x,
btn_y,
@@ -322,12 +414,145 @@ pub fn view(state: &mut ServicesState, cx: f32, cy: f32, cw: f32, ch: f32, root_
}
if filtered_services.is_empty() {
- pc.text("No services match the query", list_box_x + 16.0, list_box_y + 16.0, 12.0, TEXT_DIM);
+ sec.pc.text("No services match the query", list_box_x + 16.0, list_box_y + 16.0, 12.0, TEXT_DIM);
}
sec.content_y += list_box_h;
}
- sec.finish_focused(pc, root_focused)
+ });
+
+ // ── System Notifications ──
+ builder.add_section(&mut final_pc, "System Notifications", sec_focused.get(1).copied().unwrap_or(false), |sec2| {
+ let toggle_w = 48.0;
+ let toggle_h = 42.0;
+ state.notifications_enable_toggle.set_toggled(state.notifications_enable);
+ sec2.widget(&mut state.notifications_enable_toggle, 14.0, toggle_w, toggle_h);
+ sec2.spacing(8.0);
+
+ state.notifications_bell_toggle.set_toggled(state.notifications_bell);
+ sec2.widget(&mut state.notifications_bell_toggle, 14.0, toggle_w, toggle_h);
+ sec2.spacing(16.0);
+
+ state.notifications_duration_spinbox.value = state.notifications_duration;
+ state.notifications_duration_spinbox.set_label("Notification Duration");
+ sec2.widget(&mut state.notifications_duration_spinbox, 14.0, 200.0, 44.0);
+ sec2.spacing(16.0);
+
+ // Opacity Slider (Transparency, moved here)
+ state.notifications_opacity_slider.set_value(state.notifications_opacity);
+ sec2.widget(&mut state.notifications_opacity_slider, 14.0, 300.0, 38.0);
+ sec2.spacing(16.0);
+
+ let btn_w = 160.0;
+ let btn_h = 32.0;
+ let btn_y = sec2.ay();
+ let white_color = [1.0, 1.0, 1.0, 1.0];
+ let btn_bg = [0.20, 0.40, 0.65, 1.0];
+ let btn_hover = [0.28, 0.50, 0.78, 1.0];
+
+ let cols = sec2.row_layout(1, 0.0);
+ if let Some(&(x, _)) = cols.first() {
+ sec2.button(
+ "Send Test Notification",
+ x,
+ btn_y,
+ btn_w,
+ btn_h,
+ btn_bg,
+ btn_hover,
+ white_color,
+ AppAction::Services(ServicesMessage::SendTestNotification),
+ );
+ }
+ sec2.spacing(12.0);
+ });
+
+ // ── Status Interface ──
+ builder.add_section(&mut final_pc, "Status Interface", sec_focused.get(2).copied().unwrap_or(false), |sec3| {
+ if !state.status_loaded {
+ sec3.text("Loading Status Interface status...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec3.spacing(18.0);
+ } else {
+ // Status
+ let status_text = if state.status_running { "Status Interface: Running" } else { "Status Interface: Stopped" };
+ let status_color = if state.status_running { [92, 143, 97] } else { [170, 51, 51] };
+ state.status_label.set_text(status_text);
+ state.status_label.set_color(status_color);
+ sec3.widget(&mut state.status_label, 12.0, sec_w - 24.0, 20.0);
+ sec3.spacing(12.0);
+
+ // Font size
+ state.status_size_label.set_text(&format!("Font size: {}px", state.status_font_size));
+ sec3.widget(&mut state.status_size_label, 12.0, sec_w - 24.0, 20.0);
+ sec3.spacing(12.0);
+
+ let btn_h = 28.0;
+ let yt = sec3.ay();
+ let ax1 = sec3.ax(12.0);
+ let ax2 = sec3.ax(56.0);
+ let ax3 = sec3.ax(12.0 + 36.0 + 8.0);
+ let text_color = [0.83, 0.83, 0.83, 1.0];
+
+ sec3.button(
+ "-1",
+ ax1,
+ yt,
+ 36.0,
+ btn_h,
+ [0.13, 0.18, 0.14, 1.0],
+ [0.25, 0.30, 0.26, 1.0],
+ [1.0, 1.0, 1.0, 1.0],
+ AppAction::Services(ServicesMessage::StatusFontSizeDown),
+ );
+
+ sec3.pc.text(&format!(" {}px ", state.status_font_size), ax2, yt + 7.0, 13.0, text_color);
+
+ sec3.button(
+ "+1",
+ ax3,
+ yt,
+ 36.0,
+ btn_h,
+ [0.20, 0.40, 0.22, 1.0],
+ [0.25, 0.30, 0.26, 1.0],
+ [1.0, 1.0, 1.0, 1.0],
+ AppAction::Services(ServicesMessage::StatusFontSizeUp),
+ );
+ sec3.spacing(16.0);
+
+ // Separators toggle
+ state.status_separators_toggle.set_toggled(state.status_separators);
+ sec3.widget(&mut state.status_separators_toggle, 12.0, 48.0, 42.0);
+ sec3.spacing(16.0);
+
+ // Underline toggle
+ state.status_underline_toggle.set_toggled(state.status_underline);
+ sec3.widget(&mut state.status_underline_toggle, 12.0, 48.0, 42.0);
+ sec3.spacing(16.0);
+
+ // Padding spinbox
+ state.status_padding_spinbox.value = state.status_padding as i32;
+ sec3.widget(&mut state.status_padding_spinbox, 12.0, 200.0, 44.0);
+ sec3.spacing(16.0);
+
+ // Reload button
+ let yt_reload = sec3.ay();
+ let btn_w = (sec_w - 24.0).min(200.0);
+ let rx = sec3.left;
+ let button_x = rx + sec_w / 2.0 - btn_w / 2.0;
+ sec3.button(
+ "Reload Status Interface",
+ button_x,
+ yt_reload,
+ btn_w,
+ 32.0,
+ [0.13, 0.18, 0.14, 1.0],
+ [0.25, 0.30, 0.26, 1.0],
+ [1.0, 1.0, 1.0, 1.0],
+ AppAction::Services(ServicesMessage::StatusReload),
+ );
+ sec3.spacing(12.0);
+ }
});
final_pc
@@ -338,10 +563,12 @@ pub fn update(state: &mut ServicesState, msg: ServicesMessage) {
ServicesMessage::Refreshed(new_services) => {
state.loaded = true;
state.services = new_services;
+ state.service_items.clear();
}
ServicesMessage::SetTab(tab) => {
state.active_tab = tab;
state.list_box.set_scroll_y(0.0);
+ state.service_items.clear();
}
ServicesMessage::Start(name, is_system) => {
if let Some(srv) = state.services.iter_mut().find(|s| s.name == name && s.is_system == is_system) {
@@ -364,6 +591,441 @@ pub fn update(state: &mut ServicesState, msg: ServicesMessage) {
}
service_action(&name, "restart", is_system);
}
+ ServicesMessage::ToggleNotificationsEnable => {
+ state.notifications_enable = !state.notifications_enable;
+ write_enable_notifications(state.notifications_enable);
+ }
+ ServicesMessage::ToggleNotificationsBell => {
+ state.notifications_bell = !state.notifications_bell;
+ write_config_value("bell", &state.notifications_bell.to_string());
+ }
+ ServicesMessage::SetNotificationsDuration(d) => {
+ state.notifications_duration = d;
+ write_config_value("duration", &state.notifications_duration.to_string());
+ }
+ ServicesMessage::SetNotificationsOpacity(o) => {
+ state.notifications_opacity = o;
+ write_transparency_config_value("opacity", &format!("{:.2}", o));
+ send_ipc_command("reload");
+ }
+ ServicesMessage::SendTestNotification => {
+ send_ipc_command("notify \"ccec\" \"System notifications are working correctly!\"");
+ }
+ ServicesMessage::NotificationsRefreshed(new) => {
+ state.notifications_loaded = true;
+ state.notifications_enable = new.enable;
+ state.notifications_bell = new.bell;
+ state.notifications_duration = new.duration;
+ state.notifications_opacity = new.opacity;
+ }
+ ServicesMessage::StatusRefreshed(new) => {
+ let was_status_hovered = state.status_label.hovered();
+ let was_size_hovered = state.status_size_label.hovered();
+ let was_separators_hovered = state.status_separators_toggle.hovered();
+ let was_underline_hovered = state.status_underline_toggle.hovered();
+
+ state.status_loaded = true;
+ state.status_font_size = new.font_size;
+ state.status_padding = new.padding;
+ state.status_separators = new.separators;
+ state.status_underline = new.underline;
+ state.status_running = new.running;
+
+ state.status_label.set_hovered(was_status_hovered);
+ state.status_size_label.set_hovered(was_size_hovered);
+ state.status_separators_toggle.set_hovered(was_separators_hovered);
+ state.status_underline_toggle.set_hovered(was_underline_hovered);
+ }
+ ServicesMessage::StatusFontSizeUp => {
+ if state.status_font_size < 28 {
+ state.status_font_size += 1;
+ write_status_font_size(state.status_font_size);
+ status_interface_reload();
+ }
+ }
+ ServicesMessage::StatusFontSizeDown => {
+ if state.status_font_size > 8 {
+ state.status_font_size -= 1;
+ write_status_font_size(state.status_font_size);
+ status_interface_reload();
+ }
+ }
+ ServicesMessage::StatusToggleSeparators => {
+ state.status_separators = !state.status_separators;
+ write_status_separators(state.status_separators);
+ status_interface_reload();
+ }
+ ServicesMessage::StatusToggleUnderline => {
+ state.status_underline = !state.status_underline;
+ write_status_underline(state.status_underline);
+ status_interface_reload();
+ }
+ ServicesMessage::StatusSetPadding(val) => {
+ state.status_padding = val;
+ write_status_padding(val);
+ status_interface_reload();
+ }
+ ServicesMessage::StatusReload => {
+ status_interface_reload();
+ }
+ }
+}
+
+// ── Notifications Configuration Reader & Writer ──
+
+const CONFIG_PATH: &str = "/home/lsgalante/.config/ccec/config.toml";
+
+fn get_socket_path() -> String {
+ match std::env::var("WAYLAND_DISPLAY") {
+ Ok(display) => format!("/tmp/ccec-{}.sock", display),
+ Err(_) => "/tmp/ccec.sock".to_string(),
+ }
+}
+
+pub fn read_notifications_config() -> NotificationsConfig {
+ let content = fs::read_to_string(CONFIG_PATH).unwrap_or_default();
+ let enable = parse_notifications_enable(&content);
+ let bell = parse_notifications_bell(&content);
+ let duration = parse_notifications_duration(&content);
+ let opacity = parse_transparency_opacity(&content);
+ NotificationsConfig {
+ enable,
+ bell,
+ duration,
+ opacity,
+ }
+}
+
+fn parse_notifications_enable(content: &str) -> bool {
+ let mut in_section = false;
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if trimmed == "[notifications]" {
+ in_section = true;
+ continue;
+ }
+ if trimmed.starts_with('[') && in_section {
+ break;
+ }
+ if in_section && trimmed.starts_with("enable") {
+ if let Some(val) = trimmed.split('=').nth(1) {
+ return val.trim() == "true";
+ }
+ }
+ }
+ true // default to true
+}
+
+fn parse_notifications_bell(content: &str) -> bool {
+ let mut in_section = false;
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if trimmed == "[notifications]" {
+ in_section = true;
+ continue;
+ }
+ if trimmed.starts_with('[') && in_section {
+ break;
+ }
+ if in_section && trimmed.starts_with("bell") {
+ if let Some(val) = trimmed.split('=').nth(1) {
+ return val.trim() == "true";
+ }
+ }
+ }
+ false // default to false
+}
+
+fn parse_notifications_duration(content: &str) -> i32 {
+ let mut in_section = false;
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if trimmed == "[notifications]" {
+ in_section = true;
+ continue;
+ }
+ if trimmed.starts_with('[') && in_section {
+ break;
+ }
+ if in_section && trimmed.starts_with("duration") {
+ if let Some(val) = trimmed.split('=').nth(1) {
+ if let Ok(d) = val.trim().parse::<i32>() {
+ return d;
+ }
+ }
+ }
+ }
+ 5 // default to 5 seconds
+}
+
+fn parse_transparency_opacity(content: &str) -> f32 {
+ let mut in_section = false;
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if trimmed == "[transparency]" {
+ in_section = true;
+ continue;
+ }
+ if trimmed.starts_with('[') && in_section {
+ break;
+ }
+ if in_section && trimmed.starts_with("opacity") {
+ if let Some(val) = trimmed.split('=').nth(1) {
+ if let Ok(o) = val.trim().parse::<f32>() {
+ return o.clamp(0.0, 1.0);
+ }
+ }
+ }
+ }
+ 0.9 // default to 0.9
+}
+
+fn send_ipc_command(cmd: &str) {
+ if let Ok(mut stream) = std::os::unix::net::UnixStream::connect(get_socket_path()) {
+ let _ = stream.write_all(format!("{}\n", cmd).as_bytes());
+ }
+}
+
+fn write_config_value(key: &str, value: &str) {
+ let content = fs::read_to_string(CONFIG_PATH).unwrap_or_default();
+ let new_line = format!("{} = {}", key, value);
+
+ let mut found = false;
+ let mut updated_lines = Vec::new();
+ let mut in_section = false;
+
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if trimmed == "[notifications]" {
+ in_section = true;
+ updated_lines.push(line.to_string());
+ continue;
+ }
+ if trimmed.starts_with('[') && in_section {
+ in_section = false;
+ }
+ if in_section && trimmed.starts_with(key) {
+ found = true;
+ updated_lines.push(new_line.clone());
+ } else {
+ updated_lines.push(line.to_string());
+ }
+ }
+
+ let mut updated = updated_lines.join("\n");
+
+ if !found {
+ let mut result = String::new();
+ let has_section = content.lines().any(|l| l.trim() == "[notifications]");
+ if has_section {
+ let mut in_section = false;
+ let mut inserted = false;
+ for line in updated.lines() {
+ if line.trim() == "[notifications]" {
+ in_section = true;
+ result.push_str(line);
+ result.push('\n');
+ continue;
+ }
+ if line.trim().starts_with('[') && in_section {
+ if !inserted {
+ result.push_str(&new_line);
+ result.push('\n');
+ inserted = true;
+ }
+ in_section = false;
+ }
+ result.push_str(line);
+ result.push('\n');
+ }
+ if !inserted {
+ result.push_str(&new_line);
+ result.push('\n');
+ }
+ updated = result;
+ } else {
+ updated.push_str("\n[notifications]\n");
+ updated.push_str(&new_line);
+ updated.push_str("\n");
+ }
+ }
+ let _ = fs::write(CONFIG_PATH, updated);
+}
+
+fn write_enable_notifications(enabled: bool) {
+ write_config_value("enable", &enabled.to_string());
+ send_ipc_command("reload");
+}
+
+fn write_transparency_config_value(key: &str, value: &str) {
+ let content = fs::read_to_string(CONFIG_PATH).unwrap_or_default();
+ let new_line = format!("{} = {}", key, value);
+
+ let mut found = false;
+ let mut updated_lines = Vec::new();
+ let mut in_section = false;
+
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if trimmed == "[transparency]" {
+ in_section = true;
+ updated_lines.push(line.to_string());
+ continue;
+ }
+ if trimmed.starts_with('[') && in_section {
+ in_section = false;
+ }
+ if in_section && trimmed.starts_with(key) {
+ found = true;
+ updated_lines.push(new_line.clone());
+ } else {
+ updated_lines.push(line.to_string());
+ }
+ }
+
+ let mut updated = updated_lines.join("\n");
+
+ if !found {
+ let mut result = String::new();
+ let has_section = content.lines().any(|l| l.trim() == "[transparency]");
+ if has_section {
+ let mut in_section = false;
+ let mut inserted = false;
+ for line in updated.lines() {
+ if line.trim() == "[transparency]" {
+ in_section = true;
+ result.push_str(line);
+ result.push('\n');
+ continue;
+ }
+ if line.trim().starts_with('[') && in_section {
+ if !inserted {
+ result.push_str(&new_line);
+ result.push('\n');
+ inserted = true;
+ }
+ in_section = false;
+ }
+ result.push_str(line);
+ result.push('\n');
+ }
+ if !inserted {
+ result.push_str(&new_line);
+ result.push('\n');
+ }
+ updated = result;
+ } else {
+ updated.push_str("\n[transparency]\n");
+ updated.push_str(&new_line);
+ updated.push_str("\n");
+ }
+ }
+ let _ = fs::write(CONFIG_PATH, updated);
+}
+
+thread_local! {
+ static TEST_CONFIG_PATH: std::cell::RefCell<Option<String>> = std::cell::RefCell::new(None);
+}
+
+fn get_config_path() -> String {
+ #[cfg(test)]
+ {
+ TEST_CONFIG_PATH.with(|p| {
+ if let Some(path) = p.borrow().as_ref() {
+ return path.clone();
+ }
+ "/home/lsgalante/.config/ccec/config.toml".to_string()
+ })
+ }
+ #[cfg(not(test))]
+ {
+ "/home/lsgalante/.config/ccec/config.toml".to_string()
+ }
+}
+
+fn write_status_value(key: &str, value: &str) {
+ crate::pages::interface::write_config_value_path(&get_config_path(), key, value);
+}
+
+fn read_status_font_size() -> Option<u16> {
+ let content = std::fs::read_to_string(&get_config_path()).ok()?;
+ Some(parse_u16_from(&content, "status_font_size", 11))
+}
+
+fn write_status_font_size(size: u16) {
+ write_status_value("status_font_size", &size.to_string());
+}
+
+fn read_status_padding() -> Option<u16> {
+ let content = std::fs::read_to_string(&get_config_path()).ok()?;
+ Some(parse_u16_from(&content, "status_padding", 8))
+}
+
+fn write_status_padding(padding: u16) {
+ write_status_value("status_padding", &padding.to_string());
+}
+
+fn read_status_separators() -> Option<bool> {
+ let content = std::fs::read_to_string(&get_config_path()).ok()?;
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if let Some(rest) = trimmed.strip_prefix("status_separators") {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+ if let Ok(val) = rest.trim_end_matches('"').trim().parse::<bool>() {
+ return Some(val);
+ }
+ }
+ }
+ Some(true)
+}
+
+fn write_status_separators(val: bool) {
+ write_status_value("status_separators", &val.to_string());
+}
+
+fn read_status_underline() -> Option<bool> {
+ let content = std::fs::read_to_string(&get_config_path()).ok()?;
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if let Some(rest) = trimmed.strip_prefix("status_underline") {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+ if let Ok(val) = rest.trim_end_matches('"').trim().parse::<bool>() {
+ return Some(val);
+ }
+ }
+ }
+ Some(true)
+}
+
+fn write_status_underline(val: bool) {
+ write_status_value("status_underline", &val.to_string());
+}
+
+fn status_interface_reload() {
+ let _ = std::process::Command::new("pkill")
+ .args(["-f", "clear-status-interface"])
+ .status();
+ std::thread::sleep(std::time::Duration::from_millis(150));
+ send_ipc_command("spawn clear-status-interface");
+}
+
+pub async fn fetch_status_state() -> StatusData {
+ let running = tokio::process::Command::new("pgrep")
+ .args(["-f", "clear-status-interface"]).output().await.ok()
+ .map(|o| !o.stdout.is_empty())
+ .unwrap_or(false);
+
+ let font_size = read_status_font_size().unwrap_or(11);
+ let padding = read_status_padding().unwrap_or(8);
+ let separators = read_status_separators().unwrap_or(true);
+ let underline = read_status_underline().unwrap_or(true);
+
+ StatusData {
+ font_size,
+ padding,
+ separators,
+ underline,
+ running,
}
}
@@ -375,8 +1037,126 @@ mod tests {
fn test_view_layout_grid() {
let mut state = ServicesState::default();
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);
+ let sec_focused = vec![false, false];
+ let pc = view(&mut state, 10.0, 20.0, 800.0, 600.0, &sec_focused, &mut layout);
assert!(!pc.rects.is_empty() || !pc.texts.is_empty());
}
+
+ #[test]
+ fn test_parse_notifications_enable_default() {
+ assert!(parse_notifications_enable(""));
+ assert!(parse_notifications_enable("[layout]\ngap = 18\n"));
+ }
+
+ #[test]
+ fn test_parse_notifications_enable_explicit() {
+ let content = "\
+[notifications]
+enable = false
+";
+ assert!(!parse_notifications_enable(content));
+
+ let content = "\
+[notifications]
+enable = true
+";
+ assert!(parse_notifications_enable(content));
+ }
+
+ #[test]
+ fn test_parse_notifications_enable_other_sections() {
+ let content = "\
+[layout]
+enable = false
+
+[notifications]
+enable = true
+
+[input]
+enable = false
+";
+ assert!(parse_notifications_enable(content));
+
+ let content = "\
+[layout]
+enable = true
+
+[notifications]
+enable = false
+
+[input]
+enable = true
+";
+ assert!(!parse_notifications_enable(content));
+ }
+
+ #[test]
+ fn test_parse_notifications_duration_default() {
+ assert_eq!(parse_notifications_duration(""), 5);
+ assert_eq!(parse_notifications_duration("[notifications]\n"), 5);
+ }
+
+ #[test]
+ fn test_parse_notifications_duration_explicit() {
+ let content = "\
+[notifications]
+duration = 10
+";
+ assert_eq!(parse_notifications_duration(content), 10);
+ }
+
+ #[test]
+ fn test_read_write_separators() {
+ let dir = std::env::temp_dir();
+ let path = dir.join("test_status_separators.toml");
+ let path_str = path.to_str().unwrap().to_string();
+
+ let _ = fs::write(&path_str, "[layout]\nstatus_separators = true\nstatus_padding = 8\n");
+ TEST_CONFIG_PATH.with(|p| *p.borrow_mut() = Some(path_str));
+
+ let original = read_status_separators().unwrap_or(true);
+ write_status_separators(!original);
+ assert_eq!(read_status_separators(), Some(!original));
+ write_status_separators(original);
+ assert_eq!(read_status_separators(), Some(original));
+
+ let _ = fs::remove_file(path);
+ }
+
+ #[test]
+ fn test_read_write_padding() {
+ let dir = std::env::temp_dir();
+ let path = dir.join("test_status_padding.toml");
+ let path_str = path.to_str().unwrap().to_string();
+
+ let _ = fs::write(&path_str, "[layout]\nstatus_separators = true\nstatus_padding = 8\n");
+ TEST_CONFIG_PATH.with(|p| *p.borrow_mut() = Some(path_str));
+
+ let original = read_status_padding().unwrap_or(8);
+ write_status_padding(12);
+ assert_eq!(read_status_padding(), Some(12));
+ write_status_padding(original);
+ assert_eq!(read_status_padding(), Some(original));
+
+ let _ = fs::remove_file(path);
+ }
+
+ #[test]
+ fn test_read_write_underline() {
+ let dir = std::env::temp_dir();
+ let path = dir.join("test_status_underline.toml");
+ let path_str = path.to_str().unwrap().to_string();
+
+ let _ = fs::write(&path_str, "[layout]\nstatus_underline = true\nstatus_padding = 8\n");
+ TEST_CONFIG_PATH.with(|p| *p.borrow_mut() = Some(path_str));
+
+ let original = read_status_underline().unwrap_or(true);
+ write_status_underline(!original);
+ assert_eq!(read_status_underline(), Some(!original));
+ write_status_underline(original);
+ assert_eq!(read_status_underline(), Some(original));
+
+ let _ = fs::remove_file(path);
+ }
}
diff --git a/src/pages/status.rs b/src/pages/status.rs
deleted file mode 100644
index cc22d45..0000000
--- a/src/pages/status.rs
+++ /dev/null
@@ -1,363 +0,0 @@
-use crate::app::{AppAction, PageContent};
-use clear_ui::layout::{Section, PageLayoutBuilder, LayoutStrategy};
-use clear_ui::widget::{Label, Toggle, Widget, Spinbox};
-
-use crate::pages::typeface::parse_u16_from;
-
-#[derive(Debug, Clone)]
-pub struct StatusState {
- pub font_size: u16,
- pub padding: u16,
- pub separators: bool,
- pub underline: bool,
- pub running: bool,
- pub loaded: bool,
- pub status_label: Label,
- pub size_label: Label,
- pub separators_toggle: Toggle,
- pub underline_toggle: Toggle,
- pub padding_spinbox: Spinbox,
-}
-
-impl Default for StatusState {
- fn default() -> Self {
- Self {
- font_size: 11,
- padding: 8,
- separators: true,
- underline: true,
- running: false,
- loaded: false,
- status_label: Label::new("Status Interface: Stopped").with_font_size(14.0).with_color([170, 51, 51]),
- size_label: Label::new("Font size: 11px").with_font_size(13.0).with_color([212, 212, 212]),
- separators_toggle: Toggle::new().with_label("Show Separators"),
- underline_toggle: Toggle::new().with_label("Show Underline"),
- padding_spinbox: Spinbox::new(8, 0, 32, 1).with_label("Side Padding").with_unit("px"),
- }
- }
-}
-
-#[derive(Debug, Clone)]
-pub enum StatusMessage {
- Refreshed(StatusState),
- FontSizeUp,
- FontSizeDown,
- ToggleSeparators,
- ToggleUnderline,
- ReloadStatus,
- SetPadding(u16),
-}
-
-pub async fn fetch_status_state() -> StatusState {
- let running = tokio::process::Command::new("pgrep")
- .args(["-f", "clear-status-interface"]).output().await.ok()
- .map(|o| !o.stdout.is_empty())
- .unwrap_or(false);
-
- let font_size = read_status_font_size().unwrap_or(11);
- let padding = read_status_padding().unwrap_or(8);
- let separators = read_status_separators().unwrap_or(true);
- let underline = read_status_underline().unwrap_or(true);
- let status_color = if running { [92, 143, 97] } else { [170, 51, 51] };
- StatusState {
- font_size,
- padding,
- separators,
- underline,
- running,
- loaded: true,
- status_label: Label::new(&format!("Status Interface: {}", if running { "Running" } else { "Stopped" }))
- .with_font_size(14.0)
- .with_color(status_color),
- size_label: Label::new(&format!("Font size: {}px", font_size))
- .with_font_size(13.0)
- .with_color([212, 212, 212]),
- separators_toggle: Toggle::new().with_label("Show Separators"),
- underline_toggle: Toggle::new().with_label("Show Underline"),
- padding_spinbox: Spinbox::new(padding as i32, 0, 32, 1).with_label("Side Padding").with_unit("px"),
- }
-}
-
-#[cfg(test)]
-thread_local! {
- static TEST_CONFIG_PATH: std::cell::RefCell<Option<String>> = std::cell::RefCell::new(None);
-}
-
-fn get_config_path() -> String {
- #[cfg(test)]
- {
- TEST_CONFIG_PATH.with(|p| {
- if let Some(path) = p.borrow().as_ref() {
- return path.clone();
- }
- "/home/lsgalante/.config/ccec/config.toml".to_string()
- })
- }
- #[cfg(not(test))]
- {
- "/home/lsgalante/.config/ccec/config.toml".to_string()
- }
-}
-
-fn write_status_value(key: &str, value: &str) {
- crate::pages::typeface::write_config_value_path(&get_config_path(), key, value);
-}
-
-fn read_status_font_size() -> Option<u16> {
- let content = std::fs::read_to_string(&get_config_path()).ok()?;
- Some(parse_u16_from(&content, "status_font_size", 11))
-}
-
-fn write_status_font_size(size: u16) {
- write_status_value("status_font_size", &size.to_string());
-}
-
-fn read_status_padding() -> Option<u16> {
- let content = std::fs::read_to_string(&get_config_path()).ok()?;
- Some(parse_u16_from(&content, "status_padding", 8))
-}
-
-fn write_status_padding(padding: u16) {
- write_status_value("status_padding", &padding.to_string());
-}
-
-fn read_status_separators() -> Option<bool> {
- let content = std::fs::read_to_string(&get_config_path()).ok()?;
- for line in content.lines() {
- let trimmed = line.trim();
- if let Some(rest) = trimmed.strip_prefix("status_separators") {
- let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
- if let Ok(val) = rest.trim_end_matches('"').trim().parse::<bool>() {
- return Some(val);
- }
- }
- }
- Some(true)
-}
-
-fn write_status_separators(val: bool) {
- write_status_value("status_separators", &val.to_string());
-}
-
-fn read_status_underline() -> Option<bool> {
- let content = std::fs::read_to_string(&get_config_path()).ok()?;
- for line in content.lines() {
- let trimmed = line.trim();
- if let Some(rest) = trimmed.strip_prefix("status_underline") {
- let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
- if let Ok(val) = rest.trim_end_matches('"').trim().parse::<bool>() {
- return Some(val);
- }
- }
- }
- Some(true)
-}
-
-fn write_status_underline(val: bool) {
- write_status_value("status_underline", &val.to_string());
-}
-
-fn get_socket_path() -> String {
- match std::env::var("WAYLAND_DISPLAY") {
- Ok(display) => format!("/tmp/ccec-{}.sock", display),
- Err(_) => "/tmp/ccec.sock".to_string(),
- }
-}
-
-fn send_ipc_command(cmd: &str) {
- if let Ok(mut stream) = std::os::unix::net::UnixStream::connect(get_socket_path()) {
- use std::io::Write;
- let _ = stream.write_all(format!("{}\n", cmd).as_bytes());
- }
-}
-
-fn status_interface_reload() {
- let _ = std::process::Command::new("pkill")
- .args(["-f", "clear-status-interface"])
- .status();
- std::thread::sleep(std::time::Duration::from_millis(150));
- send_ipc_command("spawn clear-status-interface");
-}
-
-const TEXT_FG: [f32; 4] = [0.83, 0.83, 0.83, 1.0];
-const BTN_ACTIVE: [f32; 4] = [0.20, 0.40, 0.22, 1.0];
-const BTN_INACTIVE: [f32; 4] = [0.13, 0.18, 0.14, 1.0];
-const BTN_HOVER: [f32; 4] = [0.25, 0.30, 0.26, 1.0];
-const WHITE: [f32; 4] = [1.0, 1.0, 1.0, 1.0];
-
-pub fn view(state: &mut StatusState, cx: f32, cy: f32, cw: f32, ch: f32, layout: &mut dyn LayoutStrategy) -> PageContent {
- let mut final_pc = PageContent::new();
- let sec_w = 320.0f32;
- let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(1);
-
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec = Section::new(pc, rx, ry, sec_w, "Status Interface");
- if !state.loaded {
- sec.text(pc, "Loading Status Interface status...", 12.0, 0.0, 12.0, TEXT_FG);
- sec.spacing(18.0);
- } else {
- // Status
- let status_text = if state.running { "Status Interface: Running" } else { "Status Interface: Stopped" };
- state.status_label.set_text(status_text);
- sec.widget(pc, &mut state.status_label, 12.0, sec_w - 24.0, 20.0);
- sec.spacing(12.0);
-
- // Font size
- state.size_label.set_text(&format!("Font size: {}px", state.font_size));
- sec.widget(pc, &mut state.size_label, 12.0, sec_w - 24.0, 20.0);
- sec.spacing(12.0);
-
- let btn_h = 28.0;
- let yt = sec.ay();
- pc.button("-1", sec.ax(12.0), yt, 36.0, btn_h,
- BTN_INACTIVE, BTN_HOVER, WHITE,
- AppAction::Status(StatusMessage::FontSizeDown));
- pc.text(&format!(" {}px ", state.font_size), sec.ax(56.0), yt + 7.0, 13.0, TEXT_FG);
- pc.button("+1", sec.ax(12.0 + 36.0 + 8.0), yt, 36.0, btn_h,
- BTN_ACTIVE, BTN_HOVER, WHITE,
- AppAction::Status(StatusMessage::FontSizeUp));
- sec.content_y += btn_h + 16.0;
-
- // Separators toggle
- state.separators_toggle.set_toggled(state.separators);
- sec.widget(pc, &mut state.separators_toggle, 12.0, 48.0, 24.0);
- sec.spacing(16.0);
-
- // Underline toggle
- state.underline_toggle.set_toggled(state.underline);
- sec.widget(pc, &mut state.underline_toggle, 12.0, 48.0, 24.0);
- sec.spacing(16.0);
-
- // Padding spinbox
- state.padding_spinbox.value = state.padding as i32;
- sec.widget(pc, &mut state.padding_spinbox, 12.0, 200.0, 26.0);
- sec.spacing(16.0);
-
- // Reload button
- let yt = sec.ay();
- let btn_w = (sec_w - 24.0).min(200.0);
- pc.button("Reload Status Interface", rx + sec_w / 2.0 - btn_w / 2.0, yt, btn_w, 32.0,
- BTN_INACTIVE, BTN_HOVER, WHITE,
- AppAction::Status(StatusMessage::ReloadStatus));
- }
- sec.finish(pc)
- });
-
- final_pc
-}
-
-pub fn update(state: &mut StatusState, msg: StatusMessage) {
- match msg {
- StatusMessage::Refreshed(new) => {
- let was_status_hovered = state.status_label.hovered();
- let was_size_hovered = state.size_label.hovered();
- let was_separators_hovered = state.separators_toggle.hovered();
- let was_underline_hovered = state.underline_toggle.hovered();
- *state = new;
- state.status_label.set_hovered(was_status_hovered);
- state.size_label.set_hovered(was_size_hovered);
- state.separators_toggle.set_hovered(was_separators_hovered);
- state.underline_toggle.set_hovered(was_underline_hovered);
- }
- StatusMessage::FontSizeUp => {
- if state.font_size < 28 {
- state.font_size += 1;
- write_status_font_size(state.font_size);
- status_interface_reload();
- }
- }
- StatusMessage::FontSizeDown => {
- if state.font_size > 8 {
- state.font_size -= 1;
- write_status_font_size(state.font_size);
- status_interface_reload();
- }
- }
- StatusMessage::ToggleSeparators => {
- state.separators = !state.separators;
- write_status_separators(state.separators);
- status_interface_reload();
- }
- StatusMessage::ToggleUnderline => {
- state.underline = !state.underline;
- write_status_underline(state.underline);
- status_interface_reload();
- }
- StatusMessage::SetPadding(val) => {
- state.padding = val;
- write_status_padding(val);
- status_interface_reload();
- }
- StatusMessage::ReloadStatus => {
- status_interface_reload();
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use std::fs;
-
- #[test]
- fn test_read_write_separators() {
- let dir = std::env::temp_dir();
- let path = dir.join("test_status_separators.toml");
- let path_str = path.to_str().unwrap().to_string();
-
- let _ = fs::write(&path_str, "[layout]\nstatus_separators = true\nstatus_padding = 8\n");
- TEST_CONFIG_PATH.with(|p| *p.borrow_mut() = Some(path_str));
-
- let original = read_status_separators().unwrap_or(true);
- write_status_separators(!original);
- assert_eq!(read_status_separators(), Some(!original));
- write_status_separators(original);
- assert_eq!(read_status_separators(), Some(original));
-
- let _ = fs::remove_file(path);
- }
-
- #[test]
- fn test_read_write_padding() {
- let dir = std::env::temp_dir();
- let path = dir.join("test_status_padding.toml");
- let path_str = path.to_str().unwrap().to_string();
-
- let _ = fs::write(&path_str, "[layout]\nstatus_separators = true\nstatus_padding = 8\n");
- TEST_CONFIG_PATH.with(|p| *p.borrow_mut() = Some(path_str));
-
- let original = read_status_padding().unwrap_or(8);
- write_status_padding(12);
- assert_eq!(read_status_padding(), Some(12));
- write_status_padding(original);
- assert_eq!(read_status_padding(), Some(original));
-
- let _ = fs::remove_file(path);
- }
-
- #[test]
- fn test_read_write_underline() {
- let dir = std::env::temp_dir();
- let path = dir.join("test_status_underline.toml");
- let path_str = path.to_str().unwrap().to_string();
-
- let _ = fs::write(&path_str, "[layout]\nstatus_underline = true\nstatus_padding = 8\n");
- TEST_CONFIG_PATH.with(|p| *p.borrow_mut() = Some(path_str));
-
- let original = read_status_underline().unwrap_or(true);
- write_status_underline(!original);
- assert_eq!(read_status_underline(), Some(!original));
- write_status_underline(original);
- assert_eq!(read_status_underline(), Some(original));
-
- let _ = fs::remove_file(path);
- }
-
- #[test]
- fn test_view_layout_grid() {
- let mut state = StatusState::default();
- let mut layout = clear_ui::layout::ColumnLayout::new(20.0);
- let pc = view(&mut state, 10.0, 20.0, 800.0, 600.0, &mut layout);
- assert!(!pc.rects.is_empty() || !pc.texts.is_empty());
- }
-}
diff --git a/src/pages/storage.rs b/src/pages/storage.rs
index 740413d..f31797c 100644
--- a/src/pages/storage.rs
+++ b/src/pages/storage.rs
@@ -1,18 +1,80 @@
-use crate::app::PageContent;
-use clear_ui::layout::{Section, PageLayoutBuilder, LayoutStrategy};
+use crate::app::{AppAction, PageContent, SectionContextExt};
+use clear_ui::layout::{render_widget, PageLayoutBuilder, LayoutStrategy};
+use std::fs;
-#[derive(Debug, Clone, Default)]
+#[derive(Debug, Clone)]
pub struct StorageState {
pub disk_total: f64,
pub disk_used: f64,
pub ram_total: f64,
pub ram_used: f64,
pub loaded: bool,
+
+ // Backup states
+ pub backup_loaded: bool,
+ pub backup_in_progress: bool,
+ pub last_backup_time: String,
+ pub backup_size: String,
+ pub error_message: Option<String>,
+}
+
+impl Default for StorageState {
+ fn default() -> Self {
+ Self {
+ disk_total: 0.0,
+ disk_used: 0.0,
+ ram_total: 0.0,
+ ram_used: 0.0,
+ loaded: false,
+ backup_loaded: false,
+ backup_in_progress: false,
+ last_backup_time: "Never".to_string(),
+ backup_size: "0 B".to_string(),
+ error_message: None,
+ }
+ }
}
#[derive(Debug, Clone)]
pub enum StorageMessage {
Refreshed(StorageState),
+ StartBackup,
+ BackupFinished(Result<(String, String), String>),
+}
+
+fn status_path() -> String {
+ format!("{}/.config/clear-system-interface/backup_status.txt", std::env::var("HOME").unwrap_or_default())
+}
+
+pub fn read_backup_status() -> (String, String, Option<String>) {
+ let path_str = status_path();
+ let content = fs::read_to_string(path_str).unwrap_or_default();
+
+ let mut last_backup = "Never".to_string();
+ let mut size = "0 B".to_string();
+ let mut err_msg = None;
+
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if trimmed.starts_with("last_backup_time") {
+ if let Some(val) = trimmed.split('=').nth(1) {
+ last_backup = val.trim().to_string();
+ }
+ } else if trimmed.starts_with("backup_size") {
+ if let Some(val) = trimmed.split('=').nth(1) {
+ size = val.trim().to_string();
+ }
+ } else if trimmed.starts_with("error_message") {
+ if let Some(val) = trimmed.split('=').nth(1) {
+ let v = val.trim().to_string();
+ if !v.is_empty() {
+ err_msg = Some(v);
+ }
+ }
+ }
+ }
+
+ (last_backup, size, err_msg)
}
pub async fn fetch_storage_state() -> StorageState {
@@ -30,7 +92,42 @@ pub async fn fetch_storage_state() -> StorageState {
.unwrap_or_default();
let (ram_total, ram_used) = parse_mem(&mem_output);
- StorageState { disk_total, disk_used, ram_total, ram_used, loaded: true }
+ let (last_backup, size, err) = read_backup_status();
+
+ StorageState {
+ disk_total,
+ disk_used,
+ ram_total,
+ ram_used,
+ loaded: true,
+ backup_loaded: true,
+ backup_in_progress: false,
+ last_backup_time: last_backup,
+ backup_size: size,
+ error_message: err,
+ }
+}
+
+pub async fn run_backup() -> Result<(String, String), String> {
+ // Run the backup system helper script via pkexec (graphical auth prompt)
+ let output = tokio::process::Command::new("pkexec")
+ .arg("/home/lsgalante/.local/share/clear-system-interface/helpers/backup-system.sh")
+ .output()
+ .await
+ .map_err(|e| format!("Failed to run backup script: {}", e))?;
+
+ if !output.status.success() {
+ // Retrieve any specific error message written to the status file by the script
+ let (_, _, err_msg) = read_backup_status();
+ if let Some(msg) = err_msg {
+ return Err(msg);
+ }
+ let err = String::from_utf8_lossy(&output.stderr).to_string();
+ return Err(format!("Backup process failed: {}", err));
+ }
+
+ let (last_backup, size, _) = read_backup_status();
+ Ok((last_backup, size))
}
fn parse_disk(info: &str) -> (f64, f64) {
@@ -61,16 +158,23 @@ fn parse_mem(info: &str) -> (f64, f64) {
const LABEL_FG: [f32; 4] = [0.56, 0.83, 0.56, 1.0];
const TEXT_FG: [f32; 4] = [0.83, 0.83, 0.83, 1.0];
+const TEXT_DIM: [f32; 4] = [0.53, 0.53, 0.60, 1.0];
+const RED: [f32; 4] = [1.0, 0.33, 0.33, 1.0];
+const GREEN: [f32; 4] = [0.36, 0.56, 0.38, 1.0];
+const BTN_BG: [f32; 4] = [0.20, 0.40, 0.65, 1.0];
+const BTN_HOVER: [f32; 4] = [0.28, 0.50, 0.78, 1.0];
+const BTN_DISABLED: [f32; 4] = [0.15, 0.18, 0.22, 1.0];
+const WHITE: [f32; 4] = [1.0, 1.0, 1.0, 1.0];
pub fn view(state: &StorageState, cx: f32, cy: f32, cw: f32, ch: f32, layout: &mut dyn LayoutStrategy) -> PageContent {
let mut final_pc = PageContent::new();
let sec_w = 320.0f32;
- let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(1);
+ let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(3);
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec = Section::new(pc, rx, ry, sec_w, "Local Storage");
+ // Section 1: Local Storage
+ builder.add_section(&mut final_pc, "Local Storage", false, |sec| {
if !state.loaded {
- sec.text(pc, "Loading storage and memory usage...", 12.0, 0.0, 12.0, TEXT_FG);
+ sec.text("Loading storage usage...", 12.0, 0.0, 12.0, TEXT_FG);
sec.spacing(18.0);
} else {
let disk_pct = if state.disk_total > 0.0 {
@@ -79,8 +183,8 @@ pub fn view(state: &StorageState, cx: f32, cy: f32, cw: f32, ch: f32, layout: &m
0.0
};
- sec.text(pc, "Disk", 12.0, 0.0, 12.0, LABEL_FG);
- sec.text(pc,
+ sec.text("Disk", 12.0, 0.0, 12.0, LABEL_FG);
+ sec.text(
&format!("{:.0} / {:.0} GiB ({:.0}%)", state.disk_used, state.disk_total, disk_pct),
100.0, 0.0, 12.0, TEXT_FG,
);
@@ -88,32 +192,98 @@ pub fn view(state: &StorageState, cx: f32, cy: f32, cw: f32, ch: f32, layout: &m
let bar_w = sec_w - 24.0;
let yt = sec.ay();
- pc.rect([0.15, 0.15, 0.25, 1.0], sec.ax(12.0), yt, bar_w, 8.0);
- if disk_pct > 0.0 {
- pc.rect([0.36, 0.60, 0.36, 1.0], sec.ax(12.0), yt, bar_w * (disk_pct as f32 / 100.0).min(1.0), 8.0);
- }
- sec.content_y += 20.0;
+ let disk_bar_x = sec.ax(12.0);
+ let mut disk_bar = clear_ui::widget::UsageBar::new((disk_pct as f32 / 100.0).min(1.0))
+ .with_colors([0.36, 0.60, 0.36, 1.0], [0.15, 0.15, 0.25, 1.0]);
+ render_widget(sec.pc, &mut disk_bar, disk_bar_x, yt, bar_w, 8.0);
+ }
+ });
+ // Section 2: Memory
+ builder.add_section(&mut final_pc, "Memory", false, |sec| {
+ if !state.loaded {
+ sec.text("Loading memory usage...", 12.0, 0.0, 12.0, TEXT_FG);
+ sec.spacing(18.0);
+ } else {
let ram_pct = if state.ram_total > 0.0 {
state.ram_used / state.ram_total * 100.0
} else {
0.0
};
- sec.text(pc, "RAM", 12.0, 0.0, 12.0, LABEL_FG);
- sec.text(pc,
+ sec.text("RAM", 12.0, 0.0, 12.0, LABEL_FG);
+ sec.text(
&format!("{:.1} / {:.1} GiB ({:.0}%)", state.ram_used, state.ram_total, ram_pct),
100.0, 0.0, 12.0, TEXT_FG,
);
sec.spacing(18.0);
+ let bar_w = sec_w - 24.0;
let yt = sec.ay();
- pc.rect([0.15, 0.15, 0.25, 1.0], sec.ax(12.0), yt, bar_w, 8.0);
- if ram_pct > 0.0 {
- pc.rect([0.50, 0.50, 0.65, 1.0], sec.ax(12.0), yt, bar_w * (ram_pct as f32 / 100.0).min(1.0), 8.0);
+ let ram_bar_x = sec.ax(12.0);
+ let mut ram_bar = clear_ui::widget::UsageBar::new((ram_pct as f32 / 100.0).min(1.0))
+ .with_colors([0.50, 0.50, 0.65, 1.0], [0.15, 0.15, 0.25, 1.0]);
+ render_widget(sec.pc, &mut ram_bar, ram_bar_x, yt, bar_w, 8.0);
+ }
+ });
+
+ // Section 2: Full System Backup
+ builder.add_section(&mut final_pc, "Full System Backup", false, |sec| {
+ if !state.backup_loaded {
+ sec.text("Loading backup state...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(18.0);
+ } else {
+ // Status Row
+ sec.text("Backup Status", 12.0, 0.0, 12.0, LABEL_FG);
+ let status_text = if state.backup_in_progress { "Backing up..." } else { "Idle" };
+ let status_color = if state.backup_in_progress { GREEN } else { TEXT_FG };
+ sec.text(status_text, 120.0, 0.0, 12.0, status_color);
+ sec.spacing(18.0);
+
+ // Last Backup Row
+ sec.text("Last Backup", 12.0, 0.0, 12.0, LABEL_FG);
+ sec.text(&state.last_backup_time, 120.0, 0.0, 12.0, TEXT_FG);
+ sec.spacing(18.0);
+
+ // Backup Size Row
+ sec.text("Archive Size", 12.0, 0.0, 12.0, LABEL_FG);
+ sec.text(&state.backup_size, 120.0, 0.0, 12.0, TEXT_FG);
+ sec.spacing(18.0);
+
+ // Target Directories Row
+ sec.text("Backup Targets", 12.0, 0.0, 12.0, LABEL_FG);
+ sec.text("Entire Filesystem (/) [Preserving attributes]", 120.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(18.0);
+
+ // Destination Archive Row
+ sec.text("Destination", 12.0, 0.0, 12.0, LABEL_FG);
+ sec.text("USB Drive (/mnt/usb or /run/media/...)", 120.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(24.0);
+
+ // Error message if present
+ if let Some(ref err) = state.error_message {
+ sec.text("Error:", 12.0, 0.0, 12.0, RED);
+ sec.text(err, 60.0, 0.0, 11.0, RED);
+ sec.spacing(18.0);
+ }
+
+ // Action Button
+ let btn_w = 120.0;
+ let btn_h = 32.0;
+ let yt = sec.ay();
+
+ let (btn_label, bg, hover, action) = if state.backup_in_progress {
+ ("Backing up...", BTN_DISABLED, BTN_DISABLED, AppAction::Storage(StorageMessage::StartBackup))
+ } else {
+ ("Run Backup", BTN_BG, BTN_HOVER, AppAction::Storage(StorageMessage::StartBackup))
+ };
+
+ let cols = sec.row_layout(1, 0.0);
+ if let Some(&(x, _)) = cols.first() {
+ sec.button(btn_label, x, yt, btn_w, btn_h, bg, hover, WHITE, action.clone());
}
+ sec.spacing(12.0);
}
- sec.finish(pc)
});
final_pc
@@ -121,6 +291,27 @@ pub fn view(state: &StorageState, cx: f32, cy: f32, cw: f32, ch: f32, layout: &m
pub fn update(state: &mut StorageState, msg: StorageMessage) {
match msg {
- StorageMessage::Refreshed(new) => { *state = new; }
+ StorageMessage::Refreshed(new) => {
+ let in_prog = state.backup_in_progress;
+ *state = new;
+ state.backup_in_progress = in_prog;
+ }
+ StorageMessage::StartBackup => {
+ state.backup_in_progress = true;
+ state.error_message = None;
+ }
+ StorageMessage::BackupFinished(res) => {
+ state.backup_in_progress = false;
+ match res {
+ Ok((date, size)) => {
+ state.last_backup_time = date;
+ state.backup_size = size;
+ state.error_message = None;
+ }
+ Err(err) => {
+ state.error_message = Some(err);
+ }
+ }
+ }
}
}
diff --git a/src/pages/system_info.rs b/src/pages/system_info.rs
index 5335ad8..713173c 100644
--- a/src/pages/system_info.rs
+++ b/src/pages/system_info.rs
@@ -1,5 +1,5 @@
-use crate::app::{AppAction, PageContent};
-use clear_ui::layout::{Section, PageLayoutBuilder, LayoutStrategy};
+use crate::app::{AppAction, PageContent, SectionContextExt};
+use clear_ui::layout::{PageLayoutBuilder, LayoutStrategy};
#[derive(Debug, Clone, Default)]
pub struct SystemState {
@@ -54,48 +54,45 @@ pub fn view(state: &SystemState, cx: f32, cy: f32, cw: f32, ch: f32, layout: &mu
let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(2);
// 1. System Section
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec = Section::new(pc, rx, ry, sec_w, "System");
+ builder.add_section(&mut final_pc, "System", false, |sec| {
if !state.loaded {
- sec.text(pc, "Loading system information...", 12.0, 0.0, 14.0, TEXT_FG);
+ sec.text("Loading system information...", 12.0, 0.0, 14.0, TEXT_FG);
sec.spacing(10.0);
} else {
- sec.text(pc, &format!("{} — Linux {}", state.hostname, state.kernel), 12.0, 0.0, 14.0, TEXT_FG);
+ sec.text(&format!("{} — Linux {}", state.hostname, state.kernel), 12.0, 0.0, 14.0, TEXT_FG);
sec.spacing(10.0);
- sec.text(pc, &format!("Uptime: {}", state.uptime), 12.0, 0.0, 12.0, TEXT_DIM);
+ sec.text(&format!("Uptime: {}", state.uptime), 12.0, 0.0, 12.0, TEXT_DIM);
}
- sec.finish(pc)
});
// 2. System Actions Section
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec_act = Section::new(pc, rx, ry, sec_w, "System Actions");
- let yt = sec_act.ay();
+ builder.add_section(&mut final_pc, "System Actions", false, |sec| {
+ let yt = sec.ay();
let act_btn_h = 32.0;
- sec_act.row(4, 8.0, act_btn_h, |i, x, w| {
+ let cols = sec.row_layout(4, 8.0);
+ for (i, &(x, w)) in cols.iter().enumerate() {
match i {
0 => {
- pc.button("Suspend", x, yt, w, act_btn_h,
+ sec.button("Suspend", x, yt, w, act_btn_h,
SAFE_BG, BTN_HOVER, WHITE, AppAction::SystemInfo(SystemMessage::Suspend));
}
1 => {
- pc.button("Hibernate", x, yt, w, act_btn_h,
+ sec.button("Hibernate", x, yt, w, act_btn_h,
SAFE_BG, BTN_HOVER, WHITE, AppAction::SystemInfo(SystemMessage::Hibernate));
}
2 => {
- pc.button("Reboot", x, yt, w, act_btn_h,
+ sec.button("Reboot", x, yt, w, act_btn_h,
DANGER_BG, BTN_HOVER, WHITE, AppAction::SystemInfo(SystemMessage::Reboot));
}
3 => {
- pc.button("Power Off", x, yt, w, act_btn_h,
+ sec.button("Power Off", x, yt, w, act_btn_h,
DANGER_BG, BTN_HOVER, WHITE, AppAction::SystemInfo(SystemMessage::PowerOff));
}
_ => {}
}
- });
- sec_act.spacing(12.0);
- sec_act.finish(pc)
+ }
+ sec.spacing(12.0);
});
final_pc
diff --git a/src/pages/typeface.rs b/src/pages/typeface.rs
deleted file mode 100644
index 959bc41..0000000
--- a/src/pages/typeface.rs
+++ /dev/null
@@ -1,1299 +0,0 @@
-use std::fs;
-use crate::app::PageContent;
-use clear_ui::layout::{Section, PageLayoutBuilder, LayoutStrategy};
-use clear_ui::widget::{Widget, TextLabel, ScrollBox, ScrollingList, Dropdown, TextBox, Spinbox, Button};
-use clear_ui::widget::{ElementState, KeyEvent, MouseButton, Key, NamedKey};
-
-const FONTS_CONF_PATH: &str = "/home/lsgalante/.config/fontconfig/fonts.conf";
-
-// ── TypefaceState and TypefaceMessage ──
-
-#[derive(Debug, Clone)]
-pub struct TypefaceState {
- pub loaded: bool,
- pub sans_serif: String,
- pub serif: String,
- pub monospace: String,
- pub window_borders: String,
- pub status_interface: String,
- pub fuzzel: String,
- pub terminal: String,
- pub paginator: String,
- pub all_fonts: Vec<String>,
- pub mono_fonts: Vec<String>,
- pub sans_box: TextBox,
- pub serif_box: TextBox,
- pub mono_box: TextBox,
- pub borders_box: TextBox,
- pub status_box: TextBox,
- pub fuzzel_box: TextBox,
- pub terminal_box: TextBox,
- pub paginator_box: TextBox,
- pub search_box: TextBox,
- pub selected_font: Option<String>,
- pub list_box: ScrollingList,
- pub borders_menu: Dropdown,
- pub status_menu: Dropdown,
- pub fuzzel_menu: Dropdown,
- pub terminal_menu: Dropdown,
- pub paginator_menu: Dropdown,
- pub borders_size_box: Spinbox,
- pub status_size_box: Spinbox,
- pub fuzzel_size_box: Spinbox,
- pub terminal_size_box: Spinbox,
- pub paginator_size_box: Spinbox,
- pub font_buttons: Vec<Button>,
- pub copy_buttons: Vec<Button>,
-}
-
-impl Default for TypefaceState {
- fn default() -> Self {
- Self {
- loaded: false,
- sans_serif: String::new(),
- serif: String::new(),
- monospace: String::new(),
- window_borders: String::new(),
- status_interface: String::new(),
- fuzzel: String::new(),
- terminal: String::new(),
- paginator: String::new(),
- all_fonts: Vec::new(),
- mono_fonts: Vec::new(),
- sans_box: TextBox::default(),
- serif_box: TextBox::default(),
- mono_box: TextBox::default(),
- borders_box: TextBox::default(),
- status_box: TextBox::default(),
- fuzzel_box: TextBox::default(),
- terminal_box: TextBox::default(),
- paginator_box: TextBox::default(),
- search_box: TextBox::default(),
- selected_font: None,
- list_box: ScrollingList::new(24.0, 4.0),
- borders_menu: Dropdown::default(),
- status_menu: Dropdown::default(),
- fuzzel_menu: Dropdown::default(),
- terminal_menu: Dropdown::default(),
- paginator_menu: Dropdown::default(),
- borders_size_box: Spinbox::new(11, 6, 72, 1),
- status_size_box: Spinbox::new(11, 6, 72, 1),
- fuzzel_size_box: Spinbox::new(14, 6, 72, 1),
- terminal_size_box: Spinbox::new(12, 6, 72, 1),
- paginator_size_box: Spinbox::new(12, 6, 72, 1),
- font_buttons: Vec::new(),
- copy_buttons: Vec::new(),
- }
- }
-}
-
-#[derive(Debug, Clone)]
-pub enum TypefaceMessage {
- Refreshed(TypefaceState),
- SetSans(String),
- SetSerif(String),
- SetMono(String),
- SetBorders(String),
- SetStatus(String),
- SetFuzzel(String),
- SetTerminal(String),
- SetPaginator(String),
- SetSearch(String),
- SelectFont(String),
- CopyFontName(String),
- SetBordersMenu(usize),
- SetStatusMenu(usize),
- SetFuzzelMenu(usize),
- SetTerminalMenu(usize),
- SetPaginatorMenu(usize),
- SetBordersSize(i32),
- SetStatusSize(i32),
- SetFuzzelSize(i32),
- SetTerminalSize(i32),
- SetPaginatorSize(i32),
-}
-
-fn parse_font_for_alias(content: &str, alias: &str) -> Option<String> {
- let lines: Vec<&str> = content.lines().collect();
- for i in 0..lines.len() {
- let line = lines[i].trim();
- if line.contains("<test") && line.contains("name=\"family\"") && line.contains(&format!("<string>{}</string>", alias)) {
- for j in (i + 1)..(i + 6).min(lines.len()) {
- let next_line = lines[j].trim();
- if next_line.contains("<edit") {
- for k in (j + 1)..(j + 6).min(lines.len()) {
- let str_line = lines[k].trim();
- if str_line.contains("<string>") && str_line.contains("</string>") {
- if let Some(start) = str_line.find("<string>") {
- if let Some(end) = str_line.find("</string>") {
- let font = &str_line[start + 8..end];
- return Some(font.to_string());
- }
- }
- }
- }
- }
- }
- }
- }
- None
-}
-
-pub fn read_preferred_fonts() -> (String, String, String, String, String, String, String, String) {
- let content = fs::read_to_string(FONTS_CONF_PATH).unwrap_or_default();
-
- let sans = parse_font_for_alias(&content, "sans-serif").unwrap_or_else(|| "Noto Sans".to_string());
- let serif = parse_font_for_alias(&content, "serif").unwrap_or_else(|| "Noto Serif".to_string());
- let mono = parse_font_for_alias(&content, "monospace").unwrap_or_else(|| "Noto Sans Mono".to_string());
- let borders = parse_font_for_alias(&content, "window-borders").unwrap_or_else(|| "Noto Sans".to_string());
- let status = parse_font_for_alias(&content, "status-interface").unwrap_or_else(|| "Noto Sans".to_string());
- let fuzzel_font = parse_font_for_alias(&content, "fuzzel").unwrap_or_else(|| "Noto Sans".to_string());
- let term = parse_font_for_alias(&content, "terminal").unwrap_or_else(|| "Noto Sans Mono".to_string());
- let paginator = parse_font_for_alias(&content, "paginator-tab-labels").unwrap_or_else(|| "Noto Sans Mono".to_string());
-
- (sans, serif, mono, borders, status, fuzzel_font, term, paginator)
-}
-
-pub fn save_preferred_fonts(
- sans: &str,
- serif: &str,
- mono: &str,
- borders: &str,
- status: &str,
- fuzzel: &str,
- terminal: &str,
- paginator: &str,
-) {
- let content = fs::read_to_string(FONTS_CONF_PATH).unwrap_or_default();
-
- let mut dirs = Vec::new();
- for line in content.lines() {
- let trimmed = line.trim();
- if trimmed.starts_with("<dir>") && trimmed.ends_with("</dir>") {
- dirs.push(trimmed.to_string());
- }
- }
- if dirs.is_empty() {
- dirs.push("<dir>~/Dropbox/Fonts</dir>".to_string());
- }
-
- let mut new_content = String::new();
- new_content.push_str("<?xml version=\"1.0\"?>\n");
- new_content.push_str("<!DOCTYPE fontconfig SYSTEM \"fonts.dtd\">\n");
- new_content.push_str("<fontconfig>\n");
-
- for dir in dirs {
- new_content.push_str(&format!(" {}\n", dir));
- }
-
- // Sans-Serif
- new_content.push_str(" <match target=\"pattern\">\n");
- new_content.push_str(" <test qual=\"any\" name=\"family\"><string>sans-serif</string></test>\n");
- new_content.push_str(" <edit name=\"family\" mode=\"assign\" binding=\"same\">\n");
- new_content.push_str(&format!(" <string>{}</string>\n", sans));
- new_content.push_str(" </edit>\n");
- new_content.push_str(" </match>\n");
-
- // Serif
- new_content.push_str(" <match target=\"pattern\">\n");
- new_content.push_str(" <test qual=\"any\" name=\"family\"><string>serif</string></test>\n");
- new_content.push_str(" <edit name=\"family\" mode=\"assign\" binding=\"same\">\n");
- new_content.push_str(&format!(" <string>{}</string>\n", serif));
- new_content.push_str(" </edit>\n");
- new_content.push_str(" </match>\n");
-
- // Monospace
- new_content.push_str(" <match target=\"pattern\">\n");
- new_content.push_str(" <test qual=\"any\" name=\"family\"><string>monospace</string></test>\n");
- new_content.push_str(" <edit name=\"family\" mode=\"assign\" binding=\"same\">\n");
- new_content.push_str(&format!(" <string>{}</string>\n", mono));
- new_content.push_str(" </edit>\n");
- new_content.push_str(" </match>\n");
-
- // Window Borders
- new_content.push_str(" <match target=\"pattern\">\n");
- new_content.push_str(" <test qual=\"any\" name=\"family\"><string>window-borders</string></test>\n");
- new_content.push_str(" <edit name=\"family\" mode=\"assign\" binding=\"same\">\n");
- new_content.push_str(&format!(" <string>{}</string>\n", borders));
- new_content.push_str(" </edit>\n");
- new_content.push_str(" </match>\n");
-
- // Status Interface
- new_content.push_str(" <match target=\"pattern\">\n");
- new_content.push_str(" <test qual=\"any\" name=\"family\"><string>status-interface</string></test>\n");
- new_content.push_str(" <edit name=\"family\" mode=\"assign\" binding=\"same\">\n");
- new_content.push_str(&format!(" <string>{}</string>\n", status));
- new_content.push_str(" </edit>\n");
- new_content.push_str(" </match>\n");
-
- // Fuzzel
- new_content.push_str(" <match target=\"pattern\">\n");
- new_content.push_str(" <test qual=\"any\" name=\"family\"><string>fuzzel</string></test>\n");
- new_content.push_str(" <edit name=\"family\" mode=\"assign\" binding=\"same\">\n");
- new_content.push_str(&format!(" <string>{}</string>\n", fuzzel));
- new_content.push_str(" </edit>\n");
- new_content.push_str(" </match>\n");
-
- // Terminal
- new_content.push_str(" <match target=\"pattern\">\n");
- new_content.push_str(" <test qual=\"any\" name=\"family\"><string>terminal</string></test>\n");
- new_content.push_str(" <edit name=\"family\" mode=\"assign\" binding=\"same\">\n");
- new_content.push_str(&format!(" <string>{}</string>\n", terminal));
- new_content.push_str(" </edit>\n");
- new_content.push_str(" </match>\n");
-
- // Paginator
- new_content.push_str(" <match target=\"pattern\">\n");
- new_content.push_str(" <test qual=\"any\" name=\"family\"><string>paginator-tab-labels</string></test>\n");
- new_content.push_str(" <edit name=\"family\" mode=\"assign\" binding=\"same\">\n");
- new_content.push_str(&format!(" <string>{}</string>\n", paginator));
- new_content.push_str(" </edit>\n");
- new_content.push_str(" </match>\n");
-
- new_content.push_str("</fontconfig>\n");
-
- if let Some(parent) = std::path::Path::new(FONTS_CONF_PATH).parent() {
- let _ = fs::create_dir_all(parent);
- }
- let _ = fs::write(FONTS_CONF_PATH, new_content);
-
- let _ = std::process::Command::new("fc-cache")
- .arg("-f")
- .spawn();
-}
-
-pub fn parse_u16_from(content: &str, key: &str, default: u16) -> u16 {
- for line in content.lines() {
- let trimmed = line.trim();
- if let Some(rest) = trimmed.strip_prefix(key) {
- let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
- return rest.trim_end_matches('"').trim().parse::<u16>().unwrap_or(default);
- }
- }
- default
-}
-
-pub fn write_config_value(key: &str, value: &str) -> bool {
- write_config_value_path("/home/lsgalante/.config/ccec/config.toml", key, value)
-}
-
-pub fn write_config_value_path(path: &str, key: &str, value: &str) -> bool {
- let content = fs::read_to_string(path).unwrap_or_default();
- let new_line = format!("{} = {}", key, value);
- let mut found = false;
- let updated: String = content.lines()
- .map(|line| {
- if line.trim().starts_with(key) { found = true; new_line.clone() }
- else { line.to_string() }
- }).collect::<Vec<_>>().join("\n");
- if !found {
- let mut result = String::new();
- let mut in_layout = false;
- let mut inserted = false;
- for line in updated.lines() {
- if line.trim() == "[layout]" { in_layout = true; }
- else if line.trim().starts_with('[') && in_layout {
- if !inserted { result.push_str(&new_line); result.push('\n'); inserted = true; }
- in_layout = false;
- }
- result.push_str(line); result.push('\n');
- }
- if in_layout && !inserted { result.push_str(&new_line); result.push('\n'); }
- fs::write(path, result).is_ok()
- } else { fs::write(path, updated).is_ok() }
-}
-
-fn get_socket_path() -> String {
- match std::env::var("WAYLAND_DISPLAY") {
- Ok(display) => format!("/tmp/ccec-{}.sock", display),
- Err(_) => "/tmp/ccec.sock".to_string(),
- }
-}
-
-fn send_ipc_command(cmd: &str) {
- if let Ok(mut stream) = std::os::unix::net::UnixStream::connect(get_socket_path()) {
- use std::io::Write;
- let _ = stream.write_all(format!("{}\n", cmd).as_bytes());
- }
-}
-
-fn read_border_font_size() -> Option<u16> {
- let content = fs::read_to_string("/home/lsgalante/.config/ccec/config.toml").ok()?;
- Some(parse_u16_from(&content, "border_font_size", 11))
-}
-
-fn read_status_size() -> Option<u16> {
- let content = fs::read_to_string("/home/lsgalante/.config/ccec/config.toml").ok()?;
- Some(parse_u16_from(&content, "status_font_size", 11))
-}
-
-fn write_status_size(size: u16) {
- write_config_value("status_font_size", &size.to_string());
- let _ = std::process::Command::new("pkill")
- .args(["-f", "clear-status-interface"])
- .spawn();
-}
-
-fn read_fuzzel_size() -> Option<u16> {
- let ini = fs::read_to_string("/home/lsgalante/.config/fuzzel/fuzzel.ini").ok()?;
- for line in ini.lines() {
- let trimmed = line.trim();
- if trimmed.starts_with("font") {
- if let Some(pos) = trimmed.find("size=") {
- let size_str = &trimmed[pos + 5..];
- let end_pos = size_str.find(|c: char| !c.is_ascii_digit()).unwrap_or(size_str.len());
- return size_str[..end_pos].parse::<u16>().ok();
- }
- }
- }
- None
-}
-
-fn write_fuzzel_size(size: u16) {
- let path = "/home/lsgalante/.config/fuzzel/fuzzel.ini";
- let ini = fs::read_to_string(path).unwrap_or_default();
- let mut new_lines = Vec::new();
- for line in ini.lines() {
- let trimmed = line.trim();
- if trimmed.starts_with("font") {
- if let Some(pos) = line.find("size=") {
- let mut new_line = line[..pos + 5].to_string();
- new_line.push_str(&size.to_string());
- let size_str = &line[pos + 5..];
- let skip = size_str.find(|c: char| !c.is_ascii_digit()).unwrap_or(size_str.len());
- new_line.push_str(&size_str[skip..]);
- new_lines.push(new_line);
- } else {
- new_lines.push(line.to_string());
- }
- } else {
- new_lines.push(line.to_string());
- }
- }
- let _ = fs::write(path, new_lines.join("\n"));
-}
-
-fn read_terminal_size() -> Option<u16> {
- let ini = fs::read_to_string("/home/lsgalante/.config/foot/foot.ini").ok()?;
- for line in ini.lines() {
- let trimmed = line.trim();
- if trimmed.starts_with("font") {
- if let Some(pos) = trimmed.find("terminal:size=") {
- let size_str = &trimmed[pos + 14..];
- let end_pos = size_str.find(|c: char| !c.is_ascii_digit()).unwrap_or(size_str.len());
- return size_str[..end_pos].parse::<u16>().ok();
- }
- }
- }
- None
-}
-
-fn write_terminal_size(size: u16) {
- let path = "/home/lsgalante/.config/foot/foot.ini";
- let ini = fs::read_to_string(path).unwrap_or_default();
- let mut new_lines = Vec::new();
- for line in ini.lines() {
- let trimmed = line.trim();
- if trimmed.starts_with("font") {
- if let Some(pos) = line.find("terminal:size=") {
- let mut new_line = line[..pos + 14].to_string();
- new_line.push_str(&size.to_string());
- let size_str = &line[pos + 14..];
- let skip = size_str.find(|c: char| !c.is_ascii_digit()).unwrap_or(size_str.len());
- new_line.push_str(&size_str[skip..]);
- new_lines.push(new_line);
- } else {
- new_lines.push(line.to_string());
- }
- } else {
- new_lines.push(line.to_string());
- }
- }
- let _ = fs::write(path, new_lines.join("\n"));
-}
-
-fn read_paginator_size() -> Option<u16> {
- let content = fs::read_to_string("/home/lsgalante/.config/ccec/config.toml").ok()?;
- Some(parse_u16_from(&content, "paginator_font_size", 12))
-}
-
-fn write_paginator_size(size: u16) {
- write_config_value("paginator_font_size", &size.to_string());
-}
-
-fn parse_families(output: Option<std::process::Output>) -> Vec<String> {
- let mut families = Vec::new();
- if let Some(o) = output {
- let text = String::from_utf8_lossy(&o.stdout);
- for line in text.lines() {
- let trimmed = line.trim();
- if !trimmed.is_empty() {
- let family = trimmed.split(',').next().unwrap_or(trimmed).to_string();
- if !family.is_empty() && !families.contains(&family) {
- families.push(family);
- }
- }
- }
- }
- families.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase()));
- families
-}
-
-pub async fn fetch_typeface_state() -> TypefaceState {
- let (sans, serif, mono, borders, status, fuzzel_font, term, paginator_font) = read_preferred_fonts();
-
- let all_output = tokio::process::Command::new("fc-list")
- .args([":", "family"])
- .output().await.ok();
- let all_fonts = parse_families(all_output);
-
- let mono_output = tokio::process::Command::new("fc-list")
- .args([":spacing=100", "family"])
- .output().await.ok();
- let mono_fonts = parse_families(mono_output);
-
- let selected_font = all_fonts.first().cloned();
-
- let determine_dropdown_index = |font: &str, sans: &str, serif: &str, mono: &str| -> usize {
- if font == sans {
- 0
- } else if font == serif {
- 1
- } else if font == mono {
- 2
- } else {
- 3
- }
- };
-
- let borders_idx = determine_dropdown_index(&borders, &sans, &serif, &mono);
- let status_idx = determine_dropdown_index(&status, &sans, &serif, &mono);
- let fuzzel_idx = determine_dropdown_index(&fuzzel_font, &sans, &serif, &mono);
- let terminal_idx = determine_dropdown_index(&term, &sans, &serif, &mono);
- let paginator_idx = determine_dropdown_index(&paginator_font, &sans, &serif, &mono);
-
- let menu_options = vec![
- "Sans-Serif".to_string(),
- "Serif".to_string(),
- "Monospace".to_string(),
- "Other".to_string(),
- ];
-
- let mut borders_box = TextBox::new(borders.clone()).with_label("Window Borders").with_width(300.0);
- borders_box.disabled = borders_idx != 3;
-
- let mut status_box = TextBox::new(status.clone()).with_label("Status Interface").with_width(300.0);
- status_box.disabled = status_idx != 3;
-
- let mut fuzzel_box = TextBox::new(fuzzel_font.clone()).with_label("Fuzzel").with_width(300.0);
- fuzzel_box.disabled = fuzzel_idx != 3;
-
- let mut terminal_box = TextBox::new(term.clone()).with_label("Terminal").with_width(300.0);
- terminal_box.disabled = terminal_idx != 3;
-
- let mut paginator_box = TextBox::new(paginator_font.clone()).with_label("Paginator Tab Labels").with_width(300.0);
- paginator_box.disabled = paginator_idx != 3;
-
- let borders_size = read_border_font_size().unwrap_or(11);
- let status_size = read_status_size().unwrap_or(11);
- let fuzzel_size = read_fuzzel_size().unwrap_or(14);
- let terminal_size = read_terminal_size().unwrap_or(12);
- let paginator_size = read_paginator_size().unwrap_or(12);
-
- TypefaceState {
- loaded: true,
- sans_serif: sans.clone(),
- serif: serif.clone(),
- monospace: mono.clone(),
- window_borders: borders,
- status_interface: status,
- fuzzel: fuzzel_font,
- terminal: term,
- paginator: paginator_font,
- all_fonts,
- mono_fonts,
- sans_box: TextBox::new(sans).with_label("Sans-Serif"),
- serif_box: TextBox::new(serif).with_label("Serif"),
- mono_box: TextBox::new(mono).with_label("Monospace"),
- borders_box,
- status_box,
- fuzzel_box,
- terminal_box,
- paginator_box,
- search_box: TextBox::new(String::new()).with_label("Filter Fonts"),
- selected_font,
- list_box: ScrollingList::new(24.0, 4.0),
- borders_menu: Dropdown::new(menu_options.clone(), borders_idx),
- status_menu: Dropdown::new(menu_options.clone(), status_idx),
- fuzzel_menu: Dropdown::new(menu_options.clone(), fuzzel_idx),
- terminal_menu: Dropdown::new(menu_options.clone(), terminal_idx),
- paginator_menu: Dropdown::new(menu_options.clone(), paginator_idx),
- borders_size_box: Spinbox::new(borders_size as i32, 6, 72, 1),
- status_size_box: Spinbox::new(status_size as i32, 6, 72, 1),
- fuzzel_size_box: Spinbox::new(fuzzel_size as i32, 6, 72, 1),
- terminal_size_box: Spinbox::new(terminal_size as i32, 6, 72, 1),
- paginator_size_box: Spinbox::new(paginator_size as i32, 6, 72, 1),
- font_buttons: Vec::new(),
- copy_buttons: Vec::new(),
- }
-}
-
-const TEXT_DIM: [f32; 4] = [0.53, 0.53, 0.60, 1.0];
-
-pub fn view(state: &mut TypefaceState, cx: f32, cy: f32, cw: f32, ch: f32, sec_focused: &[bool], layout: &mut dyn LayoutStrategy) -> PageContent {
- let mut final_pc = PageContent::new();
- let sec_w = 320.0f32;
- let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(3);
-
- let widget_h = 26.0;
-
- // ── System Typefaces Section ──
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec = Section::new(pc, rx, ry, sec_w, "System Typefaces");
- sec.spacing(8.0);
-
- if !state.loaded {
- sec.text(pc, "Loading typefaces...", 12.0, 0.0, 12.0, TEXT_DIM);
- sec.spacing(18.0);
- } else {
- let inner_w = sec_w - 24.0;
- // Sans-Serif
- sec.widget(pc, &mut state.sans_box, 12.0, inner_w, widget_h);
- sec.spacing(12.0);
-
- // Serif
- sec.widget(pc, &mut state.serif_box, 12.0, inner_w, widget_h);
- sec.spacing(12.0);
-
- // Monospace
- sec.widget(pc, &mut state.mono_box, 12.0, inner_w, widget_h);
- sec.spacing(8.0);
- }
- let sys_focused = sec_focused.get(0).copied().unwrap_or(false);
- sec.finish_focused(pc, sys_focused)
- });
-
- // ── Program Typefaces Section ──
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec = Section::new(pc, rx, ry, sec_w, "Program Typefaces");
- sec.spacing(8.0);
-
- if !state.loaded {
- sec.text(pc, "Loading typefaces...", 12.0, 0.0, 12.0, TEXT_DIM);
- sec.spacing(18.0);
- } else {
- let inner_w = sec_w - 24.0;
-
- // Window Borders
- let start_y = sec.ay();
- sec.row(2, 10.0, widget_h, |idx, x, w| {
- if idx == 0 {
- state.borders_menu.set_row_rect(x, w);
- clear_ui::layout::render_widget(pc, &mut state.borders_menu, x, start_y, w, widget_h);
- } else {
- state.borders_size_box.set_row_rect(x, w);
- clear_ui::layout::render_widget(pc, &mut state.borders_size_box, x, start_y, w, widget_h);
- }
- });
- sec.widget(pc, &mut state.borders_box, 12.0, inner_w, widget_h);
- sec.spacing(16.0);
-
- // Status Interface
- let start_y = sec.ay();
- sec.row(2, 10.0, widget_h, |idx, x, w| {
- if idx == 0 {
- state.status_menu.set_row_rect(x, w);
- clear_ui::layout::render_widget(pc, &mut state.status_menu, x, start_y, w, widget_h);
- } else {
- state.status_size_box.set_row_rect(x, w);
- clear_ui::layout::render_widget(pc, &mut state.status_size_box, x, start_y, w, widget_h);
- }
- });
- sec.widget(pc, &mut state.status_box, 12.0, inner_w, widget_h);
- sec.spacing(16.0);
-
- // Fuzzel
- let start_y = sec.ay();
- sec.row(2, 10.0, widget_h, |idx, x, w| {
- if idx == 0 {
- state.fuzzel_menu.set_row_rect(x, w);
- clear_ui::layout::render_widget(pc, &mut state.fuzzel_menu, x, start_y, w, widget_h);
- } else {
- state.fuzzel_size_box.set_row_rect(x, w);
- clear_ui::layout::render_widget(pc, &mut state.fuzzel_size_box, x, start_y, w, widget_h);
- }
- });
- sec.widget(pc, &mut state.fuzzel_box, 12.0, inner_w, widget_h);
- sec.spacing(16.0);
-
- // Terminal
- let start_y = sec.ay();
- sec.row(2, 10.0, widget_h, |idx, x, w| {
- if idx == 0 {
- state.terminal_menu.set_row_rect(x, w);
- clear_ui::layout::render_widget(pc, &mut state.terminal_menu, x, start_y, w, widget_h);
- } else {
- state.terminal_size_box.set_row_rect(x, w);
- clear_ui::layout::render_widget(pc, &mut state.terminal_size_box, x, start_y, w, widget_h);
- }
- });
- sec.widget(pc, &mut state.terminal_box, 12.0, inner_w, widget_h);
- sec.spacing(16.0);
-
- // Paginator Tab Labels
- let start_y = sec.ay();
- sec.row(2, 10.0, widget_h, |idx, x, w| {
- if idx == 0 {
- state.paginator_menu.set_row_rect(x, w);
- clear_ui::layout::render_widget(pc, &mut state.paginator_menu, x, start_y, w, widget_h);
- } else {
- state.paginator_size_box.set_row_rect(x, w);
- clear_ui::layout::render_widget(pc, &mut state.paginator_size_box, x, start_y, w, widget_h);
- }
- });
- sec.widget(pc, &mut state.paginator_box, 12.0, inner_w, widget_h);
- sec.spacing(8.0);
- }
- let prog_focused = sec_focused.get(1).copied().unwrap_or(false);
- sec.finish_focused(pc, prog_focused)
- });
-
- // ── Typefaces Section (List & Preview) ──
- builder.add_section(&mut final_pc, |pc, rx, ry| {
- let mut sec = Section::new(pc, rx, ry, sec_w, "Typefaces");
- sec.spacing(12.0);
-
- if !state.loaded {
- sec.text(pc, "Loading installed fonts...", 12.0, 0.0, 12.0, TEXT_DIM);
- sec.spacing(18.0);
- } else {
- let inner_w = sec_w - 24.0;
-
- // 1. Search Box
- let top_room = state.search_box.top_room();
- state.search_box.set_row_rect(rx + 12.0, inner_w);
- clear_ui::layout::render_widget(
- pc,
- &mut state.search_box,
- rx + 12.0,
- sec.ay() + top_room,
- inner_w,
- widget_h,
- );
- sec.spacing(widget_h + top_room + 12.0);
-
- // 2. Scrolling List Box
- let list_box_y = sec.ay();
- let list_box_h = 200.0;
-
- clear_ui::layout::render_widget(pc, &mut state.list_box, rx + 12.0, list_box_y, inner_w, list_box_h);
-
- let query = state.search_box.text.to_lowercase();
- let matching_fonts: Vec<&String> = state.all_fonts.iter()
- .filter(|font| font.to_lowercase().contains(&query))
- .collect();
-
- if state.font_buttons.len() != matching_fonts.len() {
- state.font_buttons.clear();
- state.copy_buttons.clear();
- for _ in 0..matching_fonts.len() {
- state.font_buttons.push(Button::new_list_row(0.0, 0.0, 0.0, 0.0));
- state.copy_buttons.push(Button::new_copy_icon(0.0, 0.0, 0.0, 0.0));
- }
- }
-
- let btn_h = 24.0;
- let list_inner_x = rx + 16.0;
- let list_inner_w = inner_w - 16.0;
-
- state.list_box.update_bounds(matching_fonts.len(), list_box_y, list_box_h);
-
- for (idx, font_name) in matching_fonts.iter().enumerate() {
- if let Some(draw_y) = state.list_box.get_item_draw_y(idx, 0.0) {
- let is_selected = state.selected_font.as_ref() == Some(*font_name);
-
- let font_btn = &mut state.font_buttons[idx];
- font_btn.set_text(font_name);
- font_btn.selected = is_selected;
- clear_ui::layout::render_widget(pc, font_btn, list_inner_x, draw_y, list_inner_w - 44.0, btn_h);
-
- let copy_btn = &mut state.copy_buttons[idx];
- copy_btn.set_text("📋");
- copy_btn.selected = is_selected;
- clear_ui::layout::render_widget(pc, copy_btn, list_inner_x + list_inner_w - 40.0, draw_y, 40.0, btn_h);
- }
- }
-
- if matching_fonts.is_empty() {
- pc.text("No fonts match query", list_inner_x + 8.0, list_box_y + 16.0, 12.0, TEXT_DIM);
- }
-
- sec.spacing(list_box_h + 12.0);
-
- // 3. Info Box
- let info_h = 96.0;
- let info_bg = [0.12, 0.18, 0.28, 0.3];
- let info_border = [0.25, 0.40, 0.60, 0.5];
- let info_y = sec.ay();
-
- pc.rect(info_bg, rx + 12.0, info_y, inner_w, info_h);
- pc.rect(info_border, rx + 12.0, info_y, inner_w, 1.0);
- pc.rect(info_border, rx + 12.0, info_y + info_h - 1.0, inner_w, 1.0);
- pc.rect(info_border, rx + 12.0, info_y, 1.0, info_h);
- pc.rect(info_border, rx + 12.0 + inner_w - 1.0, info_y, 1.0, info_h);
-
- let text_padding_x = 16.0;
- let mut text_y = info_y + 12.0;
-
- pc.text("Font Directories & Installation", rx + 12.0 + text_padding_x, text_y, 12.0, [0.35, 0.65, 0.90, 1.0]);
- text_y += 20.0;
-
- pc.text("• Active Directory: ~/Dropbox/Fonts", rx + 12.0 + text_padding_x, text_y, 11.0, [0.80, 0.80, 0.85, 1.0]);
- text_y += 16.0;
-
- pc.text("• Place TTF/OTF files there to install new fonts.", rx + 12.0 + text_padding_x, text_y, 11.0, [0.80, 0.80, 0.85, 1.0]);
- text_y += 16.0;
-
- pc.text("• Changes will be cached automatically by fontconfig.", rx + 12.0 + text_padding_x, text_y, 11.0, [0.55, 0.55, 0.60, 1.0]);
-
- sec.spacing(info_h + 12.0);
-
- // 4. Preview Card
- if let Some(ref font_name) = state.selected_font {
- let card_h = 240.0;
- let card_y = sec.ay();
- pc.rect([0.10, 0.10, 0.14, 0.3], rx + 12.0, card_y, inner_w, card_h);
- pc.rect([0.25, 0.25, 0.35, 0.5], rx + 12.0, card_y, inner_w, 1.0);
- pc.rect([0.25, 0.25, 0.35, 0.5], rx + 12.0, card_y + card_h - 1.0, inner_w, 1.0);
- pc.rect([0.25, 0.25, 0.35, 0.5], rx + 12.0, card_y, 1.0, card_h);
- pc.rect([0.25, 0.25, 0.35, 0.5], rx + 12.0 + inner_w - 1.0, card_y, 1.0, card_h);
-
- let mut p_text_y = card_y + 16.0;
- pc.text(&format!("Family: {}", font_name), rx + 12.0 + text_padding_x, p_text_y, 15.0, [0.90, 0.90, 0.95, 1.0]);
- p_text_y += 28.0;
-
- pc.rect([0.22, 0.22, 0.30, 0.8], rx + 12.0 + text_padding_x, p_text_y, inner_w - (text_padding_x * 2.0), 1.0);
- p_text_y += 16.0;
-
- pc.text_with_font(
- "abcdefghijklmnopqrstuvwxyz",
- rx + 12.0 + text_padding_x,
- p_text_y,
- 13.0,
- [0.75, 0.75, 0.80, 1.0],
- font_name,
- );
- p_text_y += 22.0;
-
- pc.text_with_font(
- "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
- rx + 12.0 + text_padding_x,
- p_text_y,
- 13.0,
- [0.75, 0.75, 0.80, 1.0],
- font_name,
- );
- p_text_y += 22.0;
-
- pc.text_with_font(
- "0123456789 (!@#$%&*?)",
- rx + 12.0 + text_padding_x,
- p_text_y,
- 13.0,
- [0.75, 0.75, 0.80, 1.0],
- font_name,
- );
- p_text_y += 26.0;
-
- pc.text_with_font(
- "The quick brown fox jumps over the lazy dog.",
- rx + 12.0 + text_padding_x,
- p_text_y,
- 16.0,
- [0.90, 0.90, 0.95, 1.0],
- font_name,
- );
- p_text_y += 32.0;
-
- pc.text_with_font(
- "The five boxing wizards jump quickly.",
- rx + 12.0 + text_padding_x,
- p_text_y,
- 20.0,
- [0.95, 0.95, 1.0, 1.0],
- font_name,
- );
- sec.spacing(card_h + 8.0);
- } else {
- pc.text("Select a font to preview", rx + 24.0, sec.ay() + 20.0, 13.0, TEXT_DIM);
- sec.spacing(40.0);
- }
- }
- let list_focused = sec_focused.get(2).copied().unwrap_or(false);
- sec.finish_focused(pc, list_focused)
- });
-
- final_pc
-}
-
-pub fn update(state: &mut TypefaceState, msg: TypefaceMessage) {
- match msg {
- TypefaceMessage::Refreshed(new) => {
- state.loaded = new.loaded;
- state.all_fonts = new.all_fonts;
- state.mono_fonts = new.mono_fonts;
- if state.selected_font.is_none() {
- state.selected_font = new.selected_font.clone();
- }
- if !state.sans_box.editing {
- state.sans_serif = new.sans_serif.clone();
- state.sans_box = new.sans_box;
- }
- if !state.serif_box.editing {
- state.serif = new.serif.clone();
- state.serif_box = new.serif_box;
- }
- if !state.mono_box.editing {
- state.monospace = new.monospace.clone();
- state.mono_box = new.mono_box;
- }
- if !state.borders_box.editing {
- state.window_borders = new.window_borders.clone();
- state.borders_box = new.borders_box;
- state.borders_menu = new.borders_menu;
- }
- if !state.status_box.editing {
- state.status_interface = new.status_interface.clone();
- state.status_box = new.status_box;
- state.status_menu = new.status_menu;
- }
- if !state.fuzzel_box.editing {
- state.fuzzel = new.fuzzel.clone();
- state.fuzzel_box = new.fuzzel_box;
- state.fuzzel_menu = new.fuzzel_menu;
- }
- if !state.terminal_box.editing {
- state.terminal = new.terminal.clone();
- state.terminal_box = new.terminal_box;
- state.terminal_menu = new.terminal_menu;
- }
- if !state.paginator_box.editing {
- state.paginator = new.paginator.clone();
- state.paginator_box = new.paginator_box;
- state.paginator_menu = new.paginator_menu;
- }
- if !state.search_box.editing {
- state.search_box = new.search_box;
- }
- state.borders_size_box = new.borders_size_box;
- state.status_size_box = new.status_size_box;
- state.fuzzel_size_box = new.fuzzel_size_box;
- state.terminal_size_box = new.terminal_size_box;
- state.paginator_size_box = new.paginator_size_box;
- state.font_buttons = new.font_buttons;
- state.copy_buttons = new.copy_buttons;
- let old_scroll = state.list_box.scroll_y();
- state.list_box = new.list_box;
- state.list_box.set_scroll_y(old_scroll);
- }
- TypefaceMessage::SetSans(sans) => {
- state.sans_serif = sans.clone();
- state.sans_box.text = sans;
- if state.borders_menu.selected == 0 {
- state.window_borders = state.sans_serif.clone();
- state.borders_box.text = state.sans_serif.clone();
- }
- if state.status_menu.selected == 0 {
- state.status_interface = state.sans_serif.clone();
- state.status_box.text = state.sans_serif.clone();
- }
- if state.fuzzel_menu.selected == 0 {
- state.fuzzel = state.sans_serif.clone();
- state.fuzzel_box.text = state.sans_serif.clone();
- }
- if state.terminal_menu.selected == 0 {
- state.terminal = state.sans_serif.clone();
- state.terminal_box.text = state.sans_serif.clone();
- }
- if state.paginator_menu.selected == 0 {
- state.paginator = state.sans_serif.clone();
- state.paginator_box.text = state.sans_serif.clone();
- }
- save_preferred_fonts(
- &state.sans_serif,
- &state.serif,
- &state.monospace,
- &state.window_borders,
- &state.status_interface,
- &state.fuzzel,
- &state.terminal,
- &state.paginator,
- );
- }
- TypefaceMessage::SetSerif(serif) => {
- state.serif = serif.clone();
- state.serif_box.text = serif;
- if state.borders_menu.selected == 1 {
- state.window_borders = state.serif.clone();
- state.borders_box.text = state.serif.clone();
- }
- if state.status_menu.selected == 1 {
- state.status_interface = state.serif.clone();
- state.status_box.text = state.serif.clone();
- }
- if state.fuzzel_menu.selected == 1 {
- state.fuzzel = state.serif.clone();
- state.fuzzel_box.text = state.serif.clone();
- }
- if state.terminal_menu.selected == 1 {
- state.terminal = state.serif.clone();
- state.terminal_box.text = state.serif.clone();
- }
- if state.paginator_menu.selected == 1 {
- state.paginator = state.serif.clone();
- state.paginator_box.text = state.serif.clone();
- }
- save_preferred_fonts(
- &state.sans_serif,
- &state.serif,
- &state.monospace,
- &state.window_borders,
- &state.status_interface,
- &state.fuzzel,
- &state.terminal,
- &state.paginator,
- );
- }
- TypefaceMessage::SetMono(mono) => {
- state.monospace = mono.clone();
- state.mono_box.text = mono;
- if state.borders_menu.selected == 2 {
- state.window_borders = state.monospace.clone();
- state.borders_box.text = state.monospace.clone();
- }
- if state.status_menu.selected == 2 {
- state.status_interface = state.monospace.clone();
- state.status_box.text = state.monospace.clone();
- }
- if state.fuzzel_menu.selected == 2 {
- state.fuzzel = state.monospace.clone();
- state.fuzzel_box.text = state.monospace.clone();
- }
- if state.terminal_menu.selected == 2 {
- state.terminal = state.monospace.clone();
- state.terminal_box.text = state.monospace.clone();
- }
- if state.paginator_menu.selected == 2 {
- state.paginator = state.monospace.clone();
- state.paginator_box.text = state.monospace.clone();
- }
- save_preferred_fonts(
- &state.sans_serif,
- &state.serif,
- &state.monospace,
- &state.window_borders,
- &state.status_interface,
- &state.fuzzel,
- &state.terminal,
- &state.paginator,
- );
- }
- TypefaceMessage::SetBorders(borders) => {
- state.window_borders = borders.clone();
- state.borders_box.text = borders;
- save_preferred_fonts(
- &state.sans_serif,
- &state.serif,
- &state.monospace,
- &state.window_borders,
- &state.status_interface,
- &state.fuzzel,
- &state.terminal,
- &state.paginator,
- );
- }
- TypefaceMessage::SetStatus(status) => {
- state.status_interface = status.clone();
- state.status_box.text = status;
- save_preferred_fonts(
- &state.sans_serif,
- &state.serif,
- &state.monospace,
- &state.window_borders,
- &state.status_interface,
- &state.fuzzel,
- &state.terminal,
- &state.paginator,
- );
- }
- TypefaceMessage::SetFuzzel(fuzzel) => {
- state.fuzzel = fuzzel.clone();
- state.fuzzel_box.text = fuzzel;
- save_preferred_fonts(
- &state.sans_serif,
- &state.serif,
- &state.monospace,
- &state.window_borders,
- &state.status_interface,
- &state.fuzzel,
- &state.terminal,
- &state.paginator,
- );
- }
- TypefaceMessage::SetTerminal(term) => {
- state.terminal = term.clone();
- state.terminal_box.text = term;
- save_preferred_fonts(
- &state.sans_serif,
- &state.serif,
- &state.monospace,
- &state.window_borders,
- &state.status_interface,
- &state.fuzzel,
- &state.terminal,
- &state.paginator,
- );
- }
- TypefaceMessage::SetPaginator(paginator) => {
- state.paginator = paginator.clone();
- state.paginator_box.text = paginator;
- save_preferred_fonts(
- &state.sans_serif,
- &state.serif,
- &state.monospace,
- &state.window_borders,
- &state.status_interface,
- &state.fuzzel,
- &state.terminal,
- &state.paginator,
- );
- }
- TypefaceMessage::SetSearch(search) => {
- state.search_box.text = search;
- }
- TypefaceMessage::SelectFont(font) => {
- state.selected_font = Some(font);
- }
- TypefaceMessage::CopyFontName(font) => {
- use std::io::Write;
- std::thread::spawn({
- let text = font.clone();
- move || {
- let mut copied = false;
- let child = std::process::Command::new("wl-copy")
- .stdin(std::process::Stdio::piped())
- .stderr(std::process::Stdio::piped())
- .spawn();
- match child {
- Ok(mut child) => {
- if let Some(mut stdin) = child.stdin.take() {
- let _ = stdin.write_all(text.as_bytes());
- }
- match child.wait_with_output() {
- Ok(output) => {
- if output.status.success() {
- copied = true;
- } else {
- let err_msg = String::from_utf8_lossy(&output.stderr);
- eprintln!("wl-copy exited with error status: {:?}, stderr: {}", output.status, err_msg);
- }
- }
- Err(e) => {
- eprintln!("wl-copy wait failed: {:?}", e);
- }
- }
- }
- Err(e) => {
- eprintln!("wl-copy spawn failed: {:?}", e);
- }
- }
- if !copied {
- if let Ok(mut child) = std::process::Command::new("xclip")
- .arg("-selection")
- .arg("clipboard")
- .stdin(std::process::Stdio::piped())
- .spawn()
- {
- if let Some(mut stdin) = child.stdin.take() {
- let _ = stdin.write_all(text.as_bytes());
- }
- let _ = child.wait();
- }
- }
- }
- });
- }
- TypefaceMessage::SetBordersMenu(idx) => {
- state.borders_menu.selected = idx;
- state.borders_box.disabled = idx != 3;
- if idx == 0 {
- state.window_borders = state.sans_serif.clone();
- state.borders_box.text = state.sans_serif.clone();
- } else if idx == 1 {
- state.window_borders = state.serif.clone();
- state.borders_box.text = state.serif.clone();
- } else if idx == 2 {
- state.window_borders = state.monospace.clone();
- state.borders_box.text = state.monospace.clone();
- }
- save_preferred_fonts(
- &state.sans_serif,
- &state.serif,
- &state.monospace,
- &state.window_borders,
- &state.status_interface,
- &state.fuzzel,
- &state.terminal,
- &state.paginator,
- );
- }
- TypefaceMessage::SetStatusMenu(idx) => {
- state.status_menu.selected = idx;
- state.status_box.disabled = idx != 3;
- if idx == 0 {
- state.status_interface = state.sans_serif.clone();
- state.status_box.text = state.sans_serif.clone();
- } else if idx == 1 {
- state.status_interface = state.serif.clone();
- state.status_box.text = state.serif.clone();
- } else if idx == 2 {
- state.status_interface = state.monospace.clone();
- state.status_box.text = state.monospace.clone();
- }
- save_preferred_fonts(
- &state.sans_serif,
- &state.serif,
- &state.monospace,
- &state.window_borders,
- &state.status_interface,
- &state.fuzzel,
- &state.terminal,
- &state.paginator,
- );
- }
- TypefaceMessage::SetFuzzelMenu(idx) => {
- state.fuzzel_menu.selected = idx;
- state.fuzzel_box.disabled = idx != 3;
- if idx == 0 {
- state.fuzzel = state.sans_serif.clone();
- state.fuzzel_box.text = state.sans_serif.clone();
- } else if idx == 1 {
- state.fuzzel = state.serif.clone();
- state.fuzzel_box.text = state.serif.clone();
- } else if idx == 2 {
- state.fuzzel = state.monospace.clone();
- state.fuzzel_box.text = state.monospace.clone();
- }
- save_preferred_fonts(
- &state.sans_serif,
- &state.serif,
- &state.monospace,
- &state.window_borders,
- &state.status_interface,
- &state.fuzzel,
- &state.terminal,
- &state.paginator,
- );
- }
- TypefaceMessage::SetTerminalMenu(idx) => {
- state.terminal_menu.selected = idx;
- state.terminal_box.disabled = idx != 3;
- if idx == 0 {
- state.terminal = state.sans_serif.clone();
- state.terminal_box.text = state.sans_serif.clone();
- } else if idx == 1 {
- state.terminal = state.serif.clone();
- state.terminal_box.text = state.serif.clone();
- } else if idx == 2 {
- state.terminal = state.monospace.clone();
- state.terminal_box.text = state.monospace.clone();
- }
- save_preferred_fonts(
- &state.sans_serif,
- &state.serif,
- &state.monospace,
- &state.window_borders,
- &state.status_interface,
- &state.fuzzel,
- &state.terminal,
- &state.paginator,
- );
- }
- TypefaceMessage::SetPaginatorMenu(idx) => {
- state.paginator_menu.selected = idx;
- state.paginator_box.disabled = idx != 3;
- if idx == 0 {
- state.paginator = state.sans_serif.clone();
- state.paginator_box.text = state.sans_serif.clone();
- } else if idx == 1 {
- state.paginator = state.serif.clone();
- state.paginator_box.text = state.serif.clone();
- } else if idx == 2 {
- state.paginator = state.monospace.clone();
- state.paginator_box.text = state.monospace.clone();
- }
- save_preferred_fonts(
- &state.sans_serif,
- &state.serif,
- &state.monospace,
- &state.window_borders,
- &state.status_interface,
- &state.fuzzel,
- &state.terminal,
- &state.paginator,
- );
- }
- TypefaceMessage::SetBordersSize(val) => {
- state.borders_size_box.value = val;
- write_config_value("border_font_size", &val.to_string());
- send_ipc_command(&format!("layout border_font_size {}", val));
- }
- TypefaceMessage::SetStatusSize(val) => {
- state.status_size_box.value = val;
- write_status_size(val as u16);
- }
- TypefaceMessage::SetFuzzelSize(val) => {
- state.fuzzel_size_box.value = val;
- write_fuzzel_size(val as u16);
- }
- TypefaceMessage::SetTerminalSize(val) => {
- state.terminal_size_box.value = val;
- write_terminal_size(val as u16);
- }
- TypefaceMessage::SetPaginatorSize(val) => {
- state.paginator_size_box.value = val;
- write_paginator_size(val as u16);
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_parse_font_for_alias() {
- let content = r#"<?xml version="1.0"?>
-<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
-<fontconfig>
- <dir>~/Dropbox/Fonts</dir>
- <match target="pattern">
- <test qual="any" name="family"><string>sans-serif</string></test>
- <edit name="family" mode="assign" binding="same">
- <string>Adwaita Sans</string>
- </edit>
- </match>
- <match target="pattern">
- <test qual="any" name="family"><string>monospace</string></test>
- <edit name="family" mode="assign" binding="same">
- <string>Berkeley Mono</string>
- </edit>
- </match>
-</fontconfig>
-"#;
-
- assert_eq!(parse_font_for_alias(content, "sans-serif"), Some("Adwaita Sans".to_string()));
- assert_eq!(parse_font_for_alias(content, "monospace"), Some("Berkeley Mono".to_string()));
- assert_eq!(parse_font_for_alias(content, "serif"), None);
- }
-}
diff --git a/src/widgets.rs b/src/widgets.rs
index c0dd794..c88ee89 100644
--- a/src/widgets.rs
+++ b/src/widgets.rs
@@ -8,7 +8,7 @@ pub struct SectionStyle {
pub h: f32,
}
-pub fn section_rects(label: &str, x: f32, y: f32, w: f32, h: f32) -> Vec<([f32; 4], f32, f32, f32, f32)> {
+pub fn section_rects(_label: &str, x: f32, y: f32, w: f32, h: f32) -> Vec<([f32; 4], f32, f32, f32, f32)> {
let mut rects = Vec::new();
// Section background
rects.push((color::CONTENT_BG, x, y, w, h));