system settings
git clone https://git.lucas.co/cce-system-interface.git
Update system configuration and interface modules
src/app.rs | 4 +
src/main.rs | 606 +++++++++++++++++++++++++++++++++++++++++++++---
src/pages/audio.rs | 6 +-
src/pages/colors.rs | 316 +++++++++++++++++++++++++
src/pages/input.rs | 6 +-
src/pages/layout.rs | 103 +-------
src/pages/mod.rs | 12 +-
src/pages/network.rs | 43 ++--
src/pages/processors.rs | 27 +--
src/pages/services.rs | 53 +++--
src/pages/typeface.rs | 468 +++++++++++++++++++++++++++++++++----
11 files changed, 1420 insertions(+), 224 deletions(-)
diff --git a/src/app.rs b/src/app.rs
index 353ea6c..c3e0548 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -14,6 +14,7 @@ use crate::pages::system_info;
use crate::pages::backup;
use crate::pages::typeface;
use crate::pages::services;
+use crate::pages::colors;
use crate::pages::Page;
pub struct AppState {
@@ -32,6 +33,7 @@ pub struct AppState {
pub backup: backup::BackupState,
pub typeface: typeface::TypefaceState,
pub services: services::ServicesState,
+ pub colors: colors::ColorsState,
}
impl Default for AppState {
@@ -52,6 +54,7 @@ impl Default for AppState {
backup: backup::BackupState::default(),
typeface: typeface::TypefaceState::default(),
services: services::ServicesState::default(),
+ colors: colors::ColorsState::default(),
}
}
}
@@ -72,6 +75,7 @@ pub enum AppAction {
Backup(backup::BackupMessage),
Typeface(typeface::TypefaceMessage),
Services(services::ServicesMessage),
+ Colors(colors::ColorsMessage),
}
pub struct PageContent {
diff --git a/src/main.rs b/src/main.rs
index 6d71f67..3cb4510 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -166,6 +166,7 @@ struct SystemInterface {
rx_backup_state: std::sync::mpsc::Receiver<pages::backup::BackupState>,
rx_typeface: std::sync::mpsc::Receiver<pages::typeface::TypefaceState>,
rx_services: std::sync::mpsc::Receiver<Vec<pages::services::ServiceInfo>>,
+ rx_colors: std::sync::mpsc::Receiver<pages::colors::ColorsState>,
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>,
@@ -177,6 +178,8 @@ struct SystemInterface {
needs_rebuild: bool,
scroll_y: f32,
max_scroll_y: f32,
+ page_root_container: clear_ui::widget::Container,
+ page_sec_containers: Vec<clear_ui::widget::Container>,
}
impl SystemInterface {
@@ -369,6 +372,17 @@ impl SystemInterface {
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_services = spawn_bg(3, || pages::services::fetch_services());
+ let rx_colors = {
+ let (tx, rx) = std::sync::mpsc::channel::<pages::colors::ColorsState>();
+ tokio::spawn(async move {
+ loop {
+ let val = tokio::task::spawn_blocking(|| pages::colors::read_colors_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 (tx_backup, rx_backup) = std::sync::mpsc::channel();
let (tx_color_selector, rx_color_selector) = std::sync::mpsc::channel();
@@ -385,12 +399,14 @@ impl SystemInterface {
scale_factor,
rx_power, rx_audio, rx_display, rx_network, rx_layout, rx_input, rx_fingers,
rx_processors, rx_system, rx_status, rx_storage, rx_notifications,
- rx_backup_state, rx_typeface, rx_services, tx_backup, rx_backup,
+ rx_backup_state, rx_typeface, rx_services, rx_colors, tx_backup, rx_backup,
tx_color_selector, rx_color_selector,
width, height,
needs_rebuild: true,
scroll_y: 0.0,
max_scroll_y: 0.0,
+ page_root_container: clear_ui::widget::Container::new(),
+ page_sec_containers: Vec::new(),
};
this.rebuild_layout(width as f32, height as f32);
this
@@ -503,7 +519,7 @@ impl SystemInterface {
// 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::Typeface {
+ if self.app.current_page == Page::Typefaces {
let sb = &self.app.typeface.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
@@ -547,6 +563,169 @@ impl SystemInterface {
page_buttons.push(cb);
}
+ // ── Rebuild Widget 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.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.services.list_box.scroll_box.clear_children(); self.app.services.list_box.scroll_box.set_parent(None);
+
+ self.app.processors.cpu_list_box.scroll_box.clear_children(); self.app.processors.cpu_list_box.scroll_box.set_parent(None);
+
+ self.app.network.wifi_list_box.scroll_box.clear_children(); self.app.network.wifi_list_box.scroll_box.set_parent(None);
+
+ for sb in &mut self.app.layout.spinboxes {
+ sb.clear_children();
+ sb.set_parent(None);
+ }
+ self.app.layout.cascade_offset_spinbox.clear_children(); self.app.layout.cascade_offset_spinbox.set_parent(None);
+ self.app.layout.edge_gap_spinbox.clear_children(); self.app.layout.edge_gap_spinbox.set_parent(None);
+ self.app.layout.top_gap_spinbox.clear_children(); self.app.layout.top_gap_spinbox.set_parent(None);
+
+ for cs in &mut self.app.colors.color_selectors {
+ cs.clear_children();
+ cs.set_parent(None);
+ }
+
+ self.app.notifications.duration_spinbox.clear_children(); self.app.notifications.duration_spinbox.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);
+ self.app.input.scroll_friction_spinbox.clear_children(); self.app.input.scroll_friction_spinbox.set_parent(None);
+ self.app.input.pointer_friction_spinbox.clear_children(); self.app.input.pointer_friction_spinbox.set_parent(None);
+ self.app.input.trackpad_friction_spinbox.clear_children(); self.app.input.trackpad_friction_spinbox.set_parent(None);
+
+ for sb in &mut self.app.audio.sink_spinboxes {
+ sb.clear_children();
+ sb.set_parent(None);
+ }
+ for sb in &mut self.app.audio.source_spinboxes {
+ sb.clear_children();
+ sb.set_parent(None);
+ }
+
+ self.app.display.brightness_spinbox.clear_children(); self.app.display.brightness_spinbox.set_parent(None);
+
+ use clear_ui::widget::focus::link_parent_child;
+ match self.app.current_page {
+ 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[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);
+ }
+ Page::Processors => {
+ link_parent_child(&mut self.page_root_container, &mut self.app.processors.cpu_list_box.scroll_box);
+ }
+ 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(6, clear_ui::widget::Container::new);
+
+ for i in 0..6 {
+ link_parent_child(&mut self.page_root_container, &mut self.page_sec_containers[i]);
+ }
+
+ if !self.app.layout.spinboxes.is_empty() {
+ link_parent_child(&mut self.page_sec_containers[0], &mut self.app.layout.spinboxes[0]);
+ }
+ if self.app.layout.spinboxes.len() > 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);
+
+ if self.app.layout.spinboxes.len() > 2 {
+ link_parent_child(&mut self.page_sec_containers[2], &mut self.app.layout.spinboxes[2]);
+ }
+ if self.app.layout.spinboxes.len() > 3 {
+ link_parent_child(&mut self.page_sec_containers[3], &mut self.app.layout.spinboxes[3]);
+ }
+ if self.app.layout.spinboxes.len() > 4 {
+ link_parent_child(&mut self.page_sec_containers[4], &mut self.app.layout.spinboxes[4]);
+ }
+ if self.app.layout.spinboxes.len() > 5 {
+ link_parent_child(&mut self.page_sec_containers[5], &mut self.app.layout.spinboxes[5]);
+ }
+ }
+ Page::Colors => {
+ for cs in &mut self.app.colors.color_selectors {
+ link_parent_child(&mut self.page_root_container, cs);
+ }
+ }
+ Page::Notifications => {
+ link_parent_child(&mut self.page_root_container, &mut self.app.notifications.duration_spinbox);
+ }
+ Page::Input => {
+ self.page_sec_containers.resize_with(2, 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_sec_containers[0], &mut self.app.input.rate_spinbox);
+ link_parent_child(&mut self.page_sec_containers[0], &mut self.app.input.delay_spinbox);
+
+ link_parent_child(&mut self.page_sec_containers[1], &mut self.app.input.scroll_friction_spinbox);
+ link_parent_child(&mut self.page_sec_containers[1], &mut self.app.input.pointer_friction_spinbox);
+ link_parent_child(&mut self.page_sec_containers[1], &mut self.app.input.trackpad_friction_spinbox);
+ }
+ Page::Audio => {
+ self.page_sec_containers.resize_with(2, 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]);
+
+ for sb in &mut self.app.audio.sink_spinboxes {
+ link_parent_child(&mut self.page_sec_containers[0], sb);
+ }
+ for sb in &mut self.app.audio.source_spinboxes {
+ link_parent_child(&mut self.page_sec_containers[1], sb);
+ }
+ }
+ Page::Display => {
+ link_parent_child(&mut self.page_root_container, &mut self.app.display.brightness_spinbox);
+ }
+ _ => {}
+ }
+
self.widgets = widgets;
self.text_items = text_items;
self.page_buttons = page_buttons;
@@ -555,21 +734,26 @@ impl SystemInterface {
fn render_page_content(&mut self, cx: f32, cy: f32, cw: f32, ch: f32) -> PageContent {
use pages::*;
+ let root_focused = clear_ui::widget::focus::is_focused(&self.page_root_container);
+ let sec_focused: Vec<bool> = self.page_sec_containers.iter()
+ .map(|c| clear_ui::widget::focus::is_focused(c))
+ .collect();
match self.app.current_page {
Page::Power => power::view(&self.app.power, cx, cy, cw, ch),
- Page::Audio => audio::view(&mut self.app.audio, cx, cy, cw, ch),
+ Page::Audio => audio::view(&mut self.app.audio, cx, cy, cw, ch, &sec_focused),
Page::Display => display::view(&mut self.app.display, cx, cy, cw, ch),
- Page::Radios => network::view(&self.app.network, cx, cy, cw, ch),
- Page::Layout => layout::view(&mut self.app.layout, cx, cy, cw, ch),
- Page::Processors => processors::view(&mut self.app.processors, cx, cy, cw, ch),
- Page::Input => input::view(&mut self.app.input, cx, cy, cw, ch),
+ Page::Radios => network::view(&mut self.app.network, cx, cy, cw, ch, root_focused),
+ Page::Layout => layout::view(&mut self.app.layout, cx, cy, cw, ch, &sec_focused),
+ Page::Processors => processors::view(&mut self.app.processors, cx, cy, cw, ch, root_focused),
+ Page::Input => input::view(&mut self.app.input, cx, cy, cw, ch, &sec_focused),
Page::System => system_info::view(&self.app.system_info, cx, cy, cw, ch),
Page::Status => status::view(&mut self.app.status, cx, cy, cw, ch),
Page::Storage => storage::view(&self.app.storage, cx, cy, cw, ch),
Page::Notifications => notifications::view(&mut self.app.notifications, cx, cy, cw, ch),
Page::Backup => backup::view(&self.app.backup, cx, cy, cw, ch),
- Page::Typeface => typeface::view(&mut self.app.typeface, cx, cy, cw, ch),
- Page::Services => services::view(&mut self.app.services, cx, cy, cw, ch),
+ Page::Typefaces => typeface::view(&mut self.app.typeface, cx, cy, cw, ch, &sec_focused),
+ Page::Services => services::view(&mut self.app.services, cx, cy, cw, ch, root_focused),
+ Page::Colors => colors::view(&mut self.app.colors, cx, cy, cw, ch),
}
}
@@ -696,6 +880,10 @@ impl SystemInterface {
pages::services::update(&mut self.app.services, pages::services::ServicesMessage::Refreshed(s));
self.needs_rebuild = true;
}
+ while let Ok(s) = self.rx_colors.try_recv() {
+ colors::update(&mut self.app.colors, pages::colors::ColorsMessage::Refreshed(s));
+ self.needs_rebuild = true;
+ }
while let Ok(m) = self.rx_backup.try_recv() {
self.handle_action(&AppAction::Backup(m));
self.needs_rebuild = true;
@@ -703,10 +891,10 @@ impl SystemInterface {
while let Ok(action) = self.rx_color_selector.try_recv() {
match action {
ColorSelectorAction::Background(rgb) => {
- layout::update(&mut self.app.layout, layout::LayoutMessage::SetBackground(rgb));
+ colors::update(&mut self.app.colors, pages::colors::ColorsMessage::SetLowColor(rgb));
}
ColorSelectorAction::Border(rgb) => {
- layout::update(&mut self.app.layout, layout::LayoutMessage::SetBorderColor(rgb));
+ colors::update(&mut self.app.colors, pages::colors::ColorsMessage::SetHighColor(rgb));
}
}
self.needs_rebuild = true;
@@ -729,6 +917,7 @@ impl SystemInterface {
AppAction::Notifications(m) => notifications::update(&mut self.app.notifications, m.clone()),
AppAction::Typeface(m) => typeface::update(&mut self.app.typeface, m.clone()),
AppAction::Services(m) => services::update(&mut self.app.services, m.clone()),
+ AppAction::Colors(m) => colors::update(&mut self.app.colors, m.clone()),
AppAction::Backup(m) => match m {
pages::backup::BackupMessage::StartBackup => {
pages::backup::update(&mut self.app.backup, pages::backup::BackupMessage::StartBackup);
@@ -774,7 +963,9 @@ impl SystemInterface {
if self.app.layout.top_gap_spinbox.cursor_moved(lx, ly) {
changed = true;
}
- for cp in &mut self.app.layout.color_selectors {
+ }
+ if self.app.current_page == Page::Colors {
+ for cp in &mut self.app.colors.color_selectors {
if cp.cursor_moved(lx, ly) {
changed = true;
}
@@ -874,7 +1065,7 @@ impl SystemInterface {
changed = true;
}
}
- if self.app.current_page == Page::Typeface {
+ if self.app.current_page == Page::Typefaces {
if self.app.typeface.sans_box.cursor_moved(lx, ly) {
changed = true;
}
@@ -884,15 +1075,27 @@ impl SystemInterface {
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;
}
@@ -931,6 +1134,7 @@ impl SystemInterface {
if px >= w.x && px <= w.x + w.w && py >= w.y && py <= w.y + w.h {
if let WidgetKind::PageButton(p) = &w.kind {
if self.app.current_page != *p {
+ clear_ui::widget::focus::clear_focus();
self.app.current_page = *p;
self.scroll_y = 0.0;
self.needs_rebuild = true;
@@ -943,6 +1147,93 @@ impl SystemInterface {
let lx = self.cursor_x / s;
let ly = self.cursor_y / s + self.scroll_y;
let mut actions = Vec::new();
+
+ if state == clear_ui::widget::ElementState::Pressed {
+ let mut clicked_any_focusable = false;
+ match self.app.current_page {
+ Page::Layout => {
+ for sb in &mut self.app.layout.spinboxes {
+ if sb.hit_test(lx, ly) { clicked_any_focusable = true; }
+ }
+ if self.app.layout.cascade_offset_spinbox.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if self.app.layout.edge_gap_spinbox.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if self.app.layout.top_gap_spinbox.hit_test(lx, ly) { clicked_any_focusable = true; }
+ }
+ Page::Colors => {
+ for cp in &mut self.app.colors.color_selectors {
+ if cp.hit_test(lx, ly) { clicked_any_focusable = true; }
+ }
+ }
+ Page::Input => {
+ if self.app.input.rate_spinbox.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if self.app.input.delay_spinbox.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if self.app.input.scroll_friction_spinbox.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if self.app.input.pointer_friction_spinbox.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if self.app.input.trackpad_friction_spinbox.hit_test(lx, ly) { clicked_any_focusable = true; }
+ }
+ Page::Notifications => {
+ if self.app.notifications.duration_spinbox.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; }
+ }
+ for sb in &mut self.app.audio.source_spinboxes {
+ if sb.hit_test(lx, ly) { clicked_any_focusable = true; }
+ }
+ }
+ Page::Display => {
+ if self.app.display.brightness_spinbox.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if self.app.display.night_light_label.hit_test(lx, ly) { clicked_any_focusable = true; }
+ for out in &mut self.app.display.outputs {
+ if out.name_label.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if out.resolution_label.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if let Some(ref mut scale_lbl) = out.scale_label {
+ 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; }
+ }
+ 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.search_box.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if tf.list_box.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; }
+ }
+ Page::Processors => {
+ let proc = &mut self.app.processors;
+ if proc.cpu_list_box.hit_test(lx, ly) { clicked_any_focusable = true; }
+ }
+ Page::Radios => {
+ let net = &mut self.app.network;
+ if net.wifi_list_box.hit_test(lx, ly) { clicked_any_focusable = true; }
+ }
+ _ => {}
+ }
+
+ if !clicked_any_focusable {
+ clear_ui::widget::focus::clear_focus();
+ }
+ }
+
if state == clear_ui::widget::ElementState::Pressed && self.app.current_page == Page::Layout {
for (i, sb) in self.app.layout.spinboxes.iter_mut().enumerate() {
if !sb.hit_test(lx, ly) { sb.unfocus(); }
@@ -980,20 +1271,24 @@ impl SystemInterface {
pages::layout::LayoutMessage::SetTopGap(sb.value as u16)
));
}
- for (i, cp) in self.app.layout.color_selectors.iter_mut().enumerate() {
+ }
+ if state == clear_ui::widget::ElementState::Pressed && self.app.current_page == Page::Colors {
+ for (i, cp) in self.app.colors.color_selectors.iter_mut().enumerate() {
let old = cp.color;
if !cp.hit_test(lx, ly) { cp.unfocus(); }
cp.mouse_input(button, state, lx, ly);
if cp.take_click() {
- actions.push(AppAction::Layout(match i {
- 0 => pages::layout::LayoutMessage::PickBackgroundColor,
- _ => pages::layout::LayoutMessage::PickBorderColor,
+ actions.push(AppAction::Colors(match i {
+ 0 => pages::colors::ColorsMessage::PickLowColor,
+ 1 => pages::colors::ColorsMessage::PickHighColor,
+ _ => pages::colors::ColorsMessage::PickDisabledColor,
}));
}
if cp.color != old {
- actions.push(AppAction::Layout(match i {
- 0 => pages::layout::LayoutMessage::SetBackground(cp.color),
- _ => pages::layout::LayoutMessage::SetBorderColor(cp.color),
+ actions.push(AppAction::Colors(match i {
+ 0 => pages::colors::ColorsMessage::SetLowColor(cp.color),
+ 1 => pages::colors::ColorsMessage::SetHighColor(cp.color),
+ _ => pages::colors::ColorsMessage::SetDisabledColor(cp.color),
}));
}
}
@@ -1125,7 +1420,7 @@ impl SystemInterface {
if !lbl2.hit_test(lx, ly) { lbl2.unfocus(); }
lbl2.mouse_input(button, state, lx, ly);
}
- if state == clear_ui::widget::ElementState::Pressed && self.app.current_page == Page::Typeface {
+ if state == clear_ui::widget::ElementState::Pressed && self.app.current_page == Page::Typefaces {
let tb = &mut self.app.typeface.sans_box;
if !tb.hit_test(lx, ly) { tb.unfocus(); }
if tb.mouse_input(button, state, lx, ly) {
@@ -1153,6 +1448,15 @@ impl SystemInterface {
actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetMono(tb.text.clone())));
}
+ let menu = &mut self.app.typeface.borders_menu;
+ if !menu.hit_test(lx, ly) { menu.unfocus(); }
+ if menu.mouse_input(button, state, lx, ly) {
+ self.needs_rebuild = true;
+ }
+ if menu.take_change() {
+ actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetBordersMenu(menu.selected)));
+ }
+
let tb = &mut self.app.typeface.borders_box;
if !tb.hit_test(lx, ly) { tb.unfocus(); }
if tb.mouse_input(button, state, lx, ly) {
@@ -1162,6 +1466,15 @@ impl SystemInterface {
actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetBorders(tb.text.clone())));
}
+ let menu = &mut self.app.typeface.status_menu;
+ if !menu.hit_test(lx, ly) { menu.unfocus(); }
+ if menu.mouse_input(button, state, lx, ly) {
+ self.needs_rebuild = true;
+ }
+ if menu.take_change() {
+ actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetStatusMenu(menu.selected)));
+ }
+
let tb = &mut self.app.typeface.status_box;
if !tb.hit_test(lx, ly) { tb.unfocus(); }
if tb.mouse_input(button, state, lx, ly) {
@@ -1171,6 +1484,15 @@ impl SystemInterface {
actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetStatus(tb.text.clone())));
}
+ let menu = &mut self.app.typeface.fuzzel_menu;
+ if !menu.hit_test(lx, ly) { menu.unfocus(); }
+ if menu.mouse_input(button, state, lx, ly) {
+ self.needs_rebuild = true;
+ }
+ if menu.take_change() {
+ actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetFuzzelMenu(menu.selected)));
+ }
+
let tb = &mut self.app.typeface.fuzzel_box;
if !tb.hit_test(lx, ly) { tb.unfocus(); }
if tb.mouse_input(button, state, lx, ly) {
@@ -1180,6 +1502,15 @@ impl SystemInterface {
actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetFuzzel(tb.text.clone())));
}
+ let menu = &mut self.app.typeface.terminal_menu;
+ if !menu.hit_test(lx, ly) { menu.unfocus(); }
+ if menu.mouse_input(button, state, lx, ly) {
+ self.needs_rebuild = true;
+ }
+ if menu.take_change() {
+ actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetTerminalMenu(menu.selected)));
+ }
+
let tb = &mut self.app.typeface.terminal_box;
if !tb.hit_test(lx, ly) { tb.unfocus(); }
if tb.mouse_input(button, state, lx, ly) {
@@ -1197,6 +1528,11 @@ impl SystemInterface {
if tb.take_change() {
actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetSearch(tb.text.clone())));
}
+
+ let tf = &mut self.app.typeface;
+ if tf.list_box.mouse_input(button, state, lx, ly) {
+ self.needs_rebuild = true;
+ }
}
if state == clear_ui::widget::ElementState::Pressed && self.app.current_page == Page::Services {
let tb = &mut self.app.services.search_box;
@@ -1204,6 +1540,22 @@ impl SystemInterface {
if tb.mouse_input(button, state, lx, ly) {
self.needs_rebuild = true;
}
+ let srv = &mut self.app.services;
+ if srv.list_box.mouse_input(button, state, lx, ly) {
+ self.needs_rebuild = true;
+ }
+ }
+ if state == clear_ui::widget::ElementState::Pressed && self.app.current_page == Page::Processors {
+ let proc = &mut self.app.processors;
+ if proc.cpu_list_box.mouse_input(button, state, lx, ly) {
+ self.needs_rebuild = true;
+ }
+ }
+ if state == clear_ui::widget::ElementState::Pressed && self.app.current_page == Page::Radios {
+ let net = &mut self.app.network;
+ if net.wifi_list_box.mouse_input(button, state, lx, ly) {
+ self.needs_rebuild = true;
+ }
}
for a in &actions {
self.handle_action(a);
@@ -1222,7 +1574,7 @@ impl SystemInterface {
let lx = self.cursor_x / s;
let ly = self.cursor_y / s + self.scroll_y;
- if self.app.current_page == Page::Typeface {
+ if self.app.current_page == Page::Typefaces {
let tf = &mut self.app.typeface;
if tf.list_box.mouse_wheel(delta, lx, ly) {
self.needs_rebuild = true;
@@ -1243,6 +1595,13 @@ impl SystemInterface {
return true;
}
}
+ if self.app.current_page == Page::Radios {
+ let net = &mut self.app.network;
+ if net.wifi_list_box.mouse_wheel(delta, lx, ly) {
+ self.needs_rebuild = true;
+ return true;
+ }
+ }
let scroll_speed = 24.0;
let dy = match delta {
@@ -1259,7 +1618,47 @@ impl SystemInterface {
false
}
+ fn get_page_root_widget(&mut self) -> Option<*mut (dyn clear_ui::widget::Widget + 'static)> {
+ match self.app.current_page {
+ Page::Typefaces | Page::Services | Page::Processors | Page::Radios |
+ Page::Layout | Page::Colors | Page::Notifications | 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 static_ptr = unsafe {
+ std::mem::transmute::<*mut dyn clear_ui::widget::Widget, *mut (dyn clear_ui::widget::Widget + 'static)>(ptr)
+ };
+ Some(static_ptr)
+ }
+ _ => None,
+ }
+ }
+
fn handle_key_input(&mut self, event: &clear_ui::widget::KeyEvent) -> bool {
+ if event.state == clear_ui::widget::ElementState::Pressed && !event.repeat {
+ let is_nav_key = match (&event.logical_key, event.ctrl) {
+ (clear_ui::widget::Key::Character(c), true) if c == "j" || c == "J" || c == "k" || c == "K" || c == "u" || c == "U" || c == "i" || c == "I" => true,
+ _ => false,
+ };
+ if is_nav_key {
+ if clear_ui::widget::focus::has_focus() {
+ if clear_ui::widget::focus::navigate_focus(&event.logical_key, event.ctrl) {
+ self.needs_rebuild = true;
+ return true;
+ }
+ } else {
+ if let Some(root_ptr) = self.get_page_root_widget() {
+ unsafe {
+ let root_ref = &mut *root_ptr;
+ clear_ui::widget::focus::set_focused(root_ref);
+ root_ref.focus();
+ self.needs_rebuild = true;
+ return true;
+ }
+ }
+ }
+ }
+ }
+
if self.app.current_page == Page::Layout {
let mut changed = false;
let mut actions = Vec::new();
@@ -1307,13 +1706,25 @@ impl SystemInterface {
}
changed = true;
}
- for (i, cp) in self.app.layout.color_selectors.iter_mut().enumerate() {
+ for a in &actions {
+ self.handle_action(a);
+ }
+ if changed {
+ self.needs_rebuild = true;
+ return true;
+ }
+ }
+ if self.app.current_page == Page::Colors {
+ let mut changed = false;
+ let mut actions = Vec::new();
+ for (i, cp) in self.app.colors.color_selectors.iter_mut().enumerate() {
let old = cp.color;
if cp.keyboard_input(event) {
if cp.color != old {
- actions.push(AppAction::Layout(match i {
- 0 => pages::layout::LayoutMessage::SetBackground(cp.color),
- _ => pages::layout::LayoutMessage::SetBorderColor(cp.color),
+ actions.push(AppAction::Colors(match i {
+ 0 => pages::colors::ColorsMessage::SetLowColor(cp.color),
+ 1 => pages::colors::ColorsMessage::SetHighColor(cp.color),
+ _ => pages::colors::ColorsMessage::SetDisabledColor(cp.color),
}));
}
changed = true;
@@ -1408,11 +1819,114 @@ impl SystemInterface {
return true;
}
}
- if self.app.current_page == Page::Typeface {
+ 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,
+ (clear_ui::widget::Key::Named(clear_ui::widget::NamedKey::ArrowDown), false) => true,
+ _ => false,
+ };
+ let is_up = match (&event.logical_key, event.ctrl) {
+ (clear_ui::widget::Key::Character(c), true) if c == "p" || c == "P" => true,
+ (clear_ui::widget::Key::Named(clear_ui::widget::NamedKey::ArrowUp), false) => true,
+ _ => false,
+ };
+ if is_down {
+ if !clear_ui::widget::focus::is_focused(&self.app.typeface.list_box.scroll_box) {
+ return false;
+ }
+ let next_idx_font_scroll = {
+ let tf = &self.app.typeface;
+ let query = tf.search_box.text.to_lowercase();
+ let matching_fonts: Vec<&String> = tf.all_fonts.iter()
+ .filter(|font| font.to_lowercase().contains(&query))
+ .collect();
+ if !matching_fonts.is_empty() {
+ let current_idx = tf.selected_font.as_ref()
+ .and_then(|f| matching_fonts.iter().position(|&x| x == f));
+ let next_idx = match current_idx {
+ Some(idx) => (idx + 1).min(matching_fonts.len() - 1),
+ None => 0,
+ };
+ let font = matching_fonts[next_idx].clone();
+
+ // Compute scroll
+ let btn_h = 24.0;
+ let btn_gap = 4.0;
+ let item_height_full = btn_h + btn_gap;
+ let item_y = next_idx as f32 * item_height_full;
+ let list_box_h = 320.0;
+
+ let mut scroll_y = tf.list_box.scroll_y();
+ if item_y < scroll_y {
+ scroll_y = item_y;
+ } else if item_y + btn_h > scroll_y + list_box_h {
+ scroll_y = item_y + btn_h - list_box_h;
+ }
+ Some((font, scroll_y))
+ } else {
+ None
+ }
+ };
+
+ 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.needs_rebuild = true;
+ return true;
+ }
+ } else if is_up {
+ if !clear_ui::widget::focus::is_focused(&self.app.typeface.list_box.scroll_box) {
+ return false;
+ }
+ let next_idx_font_scroll = {
+ let tf = &self.app.typeface;
+ let query = tf.search_box.text.to_lowercase();
+ let matching_fonts: Vec<&String> = tf.all_fonts.iter()
+ .filter(|font| font.to_lowercase().contains(&query))
+ .collect();
+ if !matching_fonts.is_empty() {
+ let current_idx = tf.selected_font.as_ref()
+ .and_then(|f| matching_fonts.iter().position(|&x| x == f));
+ let next_idx = match current_idx {
+ Some(idx) => idx.saturating_sub(1),
+ None => 0,
+ };
+ let font = matching_fonts[next_idx].clone();
+
+ // Compute scroll
+ let btn_h = 24.0;
+ let btn_gap = 4.0;
+ let item_height_full = btn_h + btn_gap;
+ let item_y = next_idx as f32 * item_height_full;
+ let list_box_h = 320.0;
+
+ let mut scroll_y = tf.list_box.scroll_y();
+ if item_y < scroll_y {
+ scroll_y = item_y;
+ } else if item_y + btn_h > scroll_y + list_box_h {
+ scroll_y = item_y + btn_h - list_box_h;
+ }
+ Some((font, scroll_y))
+ } else {
+ None
+ }
+ };
+
+ 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.needs_rebuild = true;
+ return true;
+ }
+ }
+ }
+
let mut actions = Vec::new();
let mut consumed = false;
- let tb = &mut self.app.typeface.sans_box;
+ let tf = &mut self.app.typeface;
+ 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())));
@@ -1485,13 +1999,32 @@ impl SystemInterface {
}
}
if self.app.current_page == Page::Services {
- let tb = &mut self.app.services.search_box;
+ let srv = &mut self.app.services;
+ if srv.list_box.keyboard_input(event) {
+ self.needs_rebuild = true;
+ return true;
+ }
+ let tb = &mut srv.search_box;
if tb.keyboard_input(event) {
tb.take_change();
self.needs_rebuild = true;
return true;
}
}
+ if self.app.current_page == Page::Processors {
+ let proc = &mut self.app.processors;
+ if proc.cpu_list_box.keyboard_input(event) {
+ self.needs_rebuild = true;
+ return true;
+ }
+ }
+ if self.app.current_page == Page::Radios {
+ let net = &mut self.app.network;
+ if net.wifi_list_box.keyboard_input(event) {
+ self.needs_rebuild = true;
+ return true;
+ }
+ }
false
}
@@ -1570,6 +2103,7 @@ struct App {
initial_page: Page,
exit: bool,
redraw: bool,
+ ctrl_pressed: bool,
}
@@ -1804,9 +2338,11 @@ impl KeyboardHandler for App {
_qh: &QueueHandle<Self>,
_keyboard: &wl_keyboard::WlKeyboard,
_serial: u32,
- _modifiers: smithay_client_toolkit::seat::keyboard::Modifiers,
+ modifiers: smithay_client_toolkit::seat::keyboard::Modifiers,
_layout: u32,
- ) {}
+ ) {
+ self.ctrl_pressed = modifiers.ctrl;
+ }
}
impl App {
@@ -1823,6 +2359,12 @@ impl App {
xkeysym::Keysym::Tab => Key::Named(NamedKey::Tab),
xkeysym::Keysym::Delete => Key::Named(NamedKey::Delete),
xkeysym::Keysym::space => Key::Named(NamedKey::Space),
+ xkeysym::Keysym::j | xkeysym::Keysym::J => Key::Character("j".to_string()),
+ xkeysym::Keysym::k | xkeysym::Keysym::K => Key::Character("k".to_string()),
+ xkeysym::Keysym::u | xkeysym::Keysym::U => Key::Character("u".to_string()),
+ xkeysym::Keysym::i | xkeysym::Keysym::I => Key::Character("i".to_string()),
+ xkeysym::Keysym::n | xkeysym::Keysym::N => Key::Character("n".to_string()),
+ xkeysym::Keysym::p | xkeysym::Keysym::P => Key::Character("p".to_string()),
_ => {
if let Some(ref text) = event.utf8 {
Key::Character(text.clone())
@@ -1837,6 +2379,7 @@ impl App {
logical_key,
text: event.utf8.clone(),
repeat: false,
+ ctrl: self.ctrl_pressed,
};
if let Some(st) = &mut self.state {
@@ -1949,6 +2492,7 @@ fn main() {
initial_page,
exit: false,
redraw: true,
+ ctrl_pressed: false,
};
// Perform a roundtrip to populate output_state with active output scales
diff --git a/src/pages/audio.rs b/src/pages/audio.rs
index 7b32a46..c53dde2 100644
--- a/src/pages/audio.rs
+++ b/src/pages/audio.rs
@@ -207,7 +207,7 @@ const FILL_BAR: [f32; 4] = [0.30, 0.50, 0.32, 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];
-pub fn view(state: &mut AudioState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
+pub fn view(state: &mut AudioState, cx: f32, cy: f32, cw: f32, _ch: f32, sec_focused: &[bool]) -> PageContent {
let mut pc = PageContent::new();
let mut y = cy + 12.0;
@@ -266,7 +266,7 @@ pub fn view(state: &mut AudioState, cx: f32, cy: f32, cw: f32, _ch: f32) -> Page
}
}
- y = sec.finish(&mut pc);
+ y = sec.finish_focused(&mut pc, sec_focused.get(0).copied().unwrap_or(false));
// ── Input section ──
let mut sec = Section::new(&mut pc, cx, y, cw, "Input");
@@ -323,7 +323,7 @@ pub fn view(state: &mut AudioState, cx: f32, cy: f32, cw: f32, _ch: f32) -> Page
}
}
- sec.finish(&mut pc);
+ sec.finish_focused(&mut pc, sec_focused.get(1).copied().unwrap_or(false));
pc
}
diff --git a/src/pages/colors.rs b/src/pages/colors.rs
new file mode 100644
index 0000000..c1d41a5
--- /dev/null
+++ b/src/pages/colors.rs
@@ -0,0 +1,316 @@
+use std::fs;
+use std::io::Write;
+use crate::app::PageContent;
+use clear_ui::layout::Section;
+use clear_ui::widget::ColorSelector;
+
+const CONFIG_PATH: &str = "/home/lsgalante/.config/clearwm/config.toml";
+const CLEARWM_SOCK: &str = "/tmp/clearwm.sock";
+
+#[derive(Debug, Clone)]
+pub struct ColorsState {
+ pub low_color: [u8; 3],
+ pub high_color: [u8; 3],
+ pub disabled_color: [u8; 3],
+ pub color_selectors: Vec<ColorSelector>,
+ pub preset_colors: Vec<(&'static str, [u8; 3])>,
+}
+
+impl Default for ColorsState {
+ fn default() -> Self {
+ Self {
+ low_color: [0x0a, 0x1a, 0x0e],
+ high_color: [0x3e, 0x3e, 0x3e],
+ disabled_color: [0x55, 0x55, 0x55],
+ color_selectors: vec![
+ ColorSelector::new([0x0a, 0x1a, 0x0e]).with_label("Low Color"),
+ ColorSelector::new([0x3e, 0x3e, 0x3e]).with_label("High Color"),
+ ColorSelector::new([0x55, 0x55, 0x55]).with_label("Disabled"),
+ ],
+ preset_colors: preset_colors(),
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+pub enum ColorsMessage {
+ SetLowColor([u8; 3]),
+ SetHighColor([u8; 3]),
+ SetDisabledColor([u8; 3]),
+ PickLowColor,
+ PickHighColor,
+ PickDisabledColor,
+ Refreshed(ColorsState),
+}
+
+fn preset_colors() -> Vec<(&'static str, [u8; 3])> {
+ vec![
+ ("Black", [0x00, 0x00, 0x00]),
+ ("Dark Gray", [0x1a, 0x1a, 0x2e]),
+ ("Slate", [0x2d, 0x2d, 0x3d]),
+ ("Dark Forest", [0x0a, 0x1a, 0x0e]),
+ ("Forest", [0x1a, 0x2a, 0x1c]),
+ ("Dark Teal", [0x0a, 0x1a, 0x1e]),
+ ("Navy", [0x0a, 0x0f, 0x2e]),
+ ("Dark Wine", [0x1e, 0x0a, 0x14]),
+ ("Dark Brown", [0x1e, 0x16, 0x0e]),
+ ("Charcoal", [0x22, 0x22, 0x22]),
+ ("Midnight", [0x10, 0x10, 0x20]),
+ ("Deep Sea", [0x06, 0x14, 0x1e]),
+ ]
+}
+
+pub fn read_colors_config() -> ColorsState {
+ let content = fs::read_to_string(CONFIG_PATH).unwrap_or_default();
+ let has_low = content.lines().any(|l| l.trim().starts_with("low_color"));
+ let bg = if has_low {
+ parse_color_from_key(&content, "low_color", [0x0a, 0x1a, 0x0e])
+ } else {
+ parse_color_from_key(&content, "background_color", [0x0a, 0x1a, 0x0e])
+ };
+
+ let has_high = content.lines().any(|l| l.trim().starts_with("high_color"));
+ let border = if has_high {
+ parse_color_from_key(&content, "high_color", [0x3e, 0x3e, 0x3e])
+ } else {
+ parse_color_from_key(&content, "border_color", [0x3e, 0x3e, 0x3e])
+ };
+
+ let disabled = parse_color_from_key(&content, "disabled_color", [0x55, 0x55, 0x55]);
+
+ ColorsState {
+ low_color: bg,
+ high_color: border,
+ disabled_color: disabled,
+ color_selectors: vec![
+ ColorSelector::new(bg).with_label("Low Color"),
+ ColorSelector::new(border).with_label("High Color"),
+ ColorSelector::new(disabled).with_label("Disabled"),
+ ],
+ preset_colors: preset_colors(),
+ }
+}
+
+fn parse_color_from_key(content: &str, key: &str, default: [u8; 3]) -> [u8; 3] {
+ 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 hex = rest.trim_end_matches('"').trim();
+ return parse_hex(hex);
+ }
+ }
+ default
+}
+
+fn parse_hex(s: &str) -> [u8; 3] {
+ let s = s.trim_start_matches('#');
+ if s.len() >= 6 {
+ let r = u8::from_str_radix(&s[0..2], 16).unwrap_or(0x0a);
+ let g = u8::from_str_radix(&s[2..4], 16).unwrap_or(0x1a);
+ let b = u8::from_str_radix(&s[4..6], 16).unwrap_or(0x0e);
+ [r, g, b]
+ } else { [0x0a, 0x1a, 0x0e] }
+}
+
+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 {
+ let content = fs::read_to_string(path).unwrap_or_default();
+ let old_key = match key {
+ "low_color" => "background_color",
+ "high_color" => "border_color",
+ _ => "",
+ };
+ let new_line = format!("{} = {}", key, value);
+ let mut found = false;
+ let updated: String = content.lines()
+ .map(|line| {
+ let trimmed = line.trim();
+ if trimmed.starts_with(key) {
+ found = true;
+ new_line.clone()
+ } else if !old_key.is_empty() && trimmed.starts_with(old_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 send_ipc_command(cmd: &str) {
+ if let Ok(mut stream) = std::os::unix::net::UnixStream::connect(CLEARWM_SOCK) {
+ let _ = stream.write_all(format!("{}\n", cmd).as_bytes());
+ }
+}
+
+fn apply_background(rgb: [u8; 3]) {
+ let _ = std::process::Command::new("pkill").args(["-x", "swaybg"]).status();
+ std::thread::sleep(std::time::Duration::from_millis(100));
+ let hex = format!("{:02x}{:02x}{:02x}", rgb[0], rgb[1], rgb[2]);
+ let _ = std::process::Command::new("swaybg").arg("-c").arg(&hex).spawn();
+ write_config_value("low_color", &format!("\"#{}\"", hex));
+ send_ipc_command(&format!("layout low_color #{}", hex));
+}
+
+fn apply_border_color(rgb: [u8; 3]) {
+ let hex = format!("\"#{:02x}{:02x}{:02x}\"", rgb[0], rgb[1], rgb[2]);
+ write_config_value("high_color", &hex);
+ send_ipc_command(&format!("layout high_color #{:02x}{:02x}{:02x}", rgb[0], rgb[1], rgb[2]));
+}
+
+fn apply_disabled_color(rgb: [u8; 3]) {
+ let hex = format!("\"#{:02x}{:02x}{:02x}\"", rgb[0], rgb[1], rgb[2]);
+ write_config_value("disabled_color", &hex);
+ send_ipc_command(&format!("layout disabled_color #{:02x}{:02x}{:02x}", rgb[0], rgb[1], rgb[2]));
+}
+
+pub fn view(state: &mut ColorsState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
+ let mut pc = PageContent::new();
+ let mut y = cy + 12.0;
+
+ // 1. Layout Section
+ let mut sec = Section::new(&mut pc, cx, y, cw, "Layout");
+ sec.spacing(8.0);
+ state.color_selectors[0].color = state.low_color;
+ sec.widget(&mut pc, &mut state.color_selectors[0], 12.0, 220.0, 22.0);
+ sec.spacing(8.0);
+ state.color_selectors[1].color = state.high_color;
+ sec.widget(&mut pc, &mut state.color_selectors[1], 12.0, 220.0, 22.0);
+ sec.spacing(8.0);
+ y = sec.finish(&mut pc);
+
+ // 2. Status Section
+ let mut sec = Section::new(&mut pc, cx, y, cw, "Status");
+ sec.spacing(8.0);
+ state.color_selectors[2].color = state.disabled_color;
+ sec.widget(&mut pc, &mut state.color_selectors[2], 12.0, 220.0, 22.0);
+ sec.spacing(8.0);
+ y = sec.finish(&mut pc);
+
+ // 3. Preset Background Colors Grid
+ let mut sec = Section::new(&mut pc, cx, y, cw, "Preset Backgrounds");
+ let cols = 4;
+ let gap = 8.0;
+ let btn_w = (cw - 24.0 - (gap * (cols - 1) as f32)) / cols as f32;
+ let btn_h = 28.0;
+
+ for (i, (name, rgb)) in state.preset_colors.iter().enumerate() {
+ let col = i % cols;
+ let row = i / cols;
+ let bx = cx + 12.0 + col as f32 * (btn_w + gap);
+ let by = sec.ay() + row as f32 * (btn_h + gap);
+
+ let r = rgb[0] as f32 / 255.0;
+ let g = rgb[1] as f32 / 255.0;
+ let b = rgb[2] as f32 / 255.0;
+ let luminance = 0.299 * r + 0.587 * g + 0.114 * b;
+ let text_color = if luminance > 0.5 { [0.08, 0.08, 0.12, 1.0] } else { [0.90, 0.90, 0.95, 1.0] };
+
+ pc.button(
+ name,
+ bx,
+ by,
+ btn_w,
+ btn_h,
+ [r, g, b, 0.8],
+ [r, g, b, 1.0],
+ text_color,
+ crate::app::AppAction::Colors(ColorsMessage::SetLowColor(*rgb)),
+ );
+ }
+
+ let rows = (state.preset_colors.len() + cols - 1) / cols;
+ sec.content_y += rows as f32 * (btn_h + gap) + 4.0;
+ sec.finish(&mut pc);
+
+ pc
+}
+
+pub fn update(state: &mut ColorsState, msg: ColorsMessage) {
+ match msg {
+ ColorsMessage::SetLowColor(rgb) => {
+ state.low_color = rgb;
+ apply_background(rgb);
+ }
+ ColorsMessage::SetHighColor(rgb) => {
+ state.high_color = rgb;
+ apply_border_color(rgb);
+ }
+ ColorsMessage::SetDisabledColor(rgb) => {
+ state.disabled_color = rgb;
+ apply_disabled_color(rgb);
+ }
+ ColorsMessage::PickLowColor | ColorsMessage::PickHighColor | ColorsMessage::PickDisabledColor => {}
+ ColorsMessage::Refreshed(new) => {
+ *state = new;
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_parse_hex() {
+ assert_eq!(parse_hex("#ffffff"), [255, 255, 255]);
+ assert_eq!(parse_hex("000000"), [0, 0, 0]);
+ assert_eq!(parse_hex("#123456"), [18, 52, 86]);
+ assert_eq!(parse_hex("invalid"), [0x0a, 0x1a, 0x0e]);
+ }
+
+ #[test]
+ fn test_parse_color_from_key() {
+ let content = "\n[layout]\nlow_color = \"#112233\"\nhigh_color = \"#445566\"\ndisabled_color = \"#778899\"\n";
+ assert_eq!(parse_color_from_key(content, "low_color", [0, 0, 0]), [17, 34, 51]);
+ assert_eq!(parse_color_from_key(content, "high_color", [0, 0, 0]), [68, 85, 102]);
+ assert_eq!(parse_color_from_key(content, "disabled_color", [0, 0, 0]), [119, 136, 153]);
+ assert_eq!(parse_color_from_key(content, "non_existent", [1, 2, 3]), [1, 2, 3]);
+ }
+
+ #[test]
+ fn test_write_config_value_path() {
+ let dir = std::env::temp_dir();
+ let path = dir.join("test_config.toml");
+ let path_str = path.to_str().unwrap();
+
+ // 1. Write initial file content with [layout] and other keys
+ let initial_content = "[layout]\ngap = 18\nborder_color = \"#374673\"\n\n[output]\nscale = 2\n";
+ fs::write(path_str, initial_content).unwrap();
+
+ // 2. Write disabled_color which does not exist yet (key not found case)
+ assert!(write_config_value_path(path_str, "disabled_color", "\"#555555\""));
+ let updated = fs::read_to_string(path_str).unwrap();
+ assert!(updated.contains("disabled_color = \"#555555\""));
+ // Check it was inserted before [output]
+ assert!(updated.find("disabled_color = \"#555555\"").unwrap() < updated.find("[output]").unwrap());
+
+ // 3. Update disabled_color (key found case)
+ assert!(write_config_value_path(path_str, "disabled_color", "\"#666666\""));
+ let updated2 = fs::read_to_string(path_str).unwrap();
+ assert!(updated2.contains("disabled_color = \"#666666\""));
+ assert!(!updated2.contains("disabled_color = \"#555555\""));
+
+ // Clean up
+ let _ = fs::remove_file(path_str);
+ }
+}
diff --git a/src/pages/input.rs b/src/pages/input.rs
index f007262..247658c 100644
--- a/src/pages/input.rs
+++ b/src/pages/input.rs
@@ -247,7 +247,7 @@ fn apply_repeat_config(rate: u16, delay: u16) {
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];
-pub fn view(state: &mut InputState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
+pub fn view(state: &mut InputState, cx: f32, cy: f32, cw: f32, _ch: f32, sec_focused: &[bool]) -> PageContent {
let mut pc = PageContent::new();
let mut y = cy + 12.0;
@@ -304,7 +304,7 @@ pub fn view(state: &mut InputState, cx: f32, cy: f32, cw: f32, _ch: f32) -> Page
sec.widget(&mut pc, &mut state.delay_spinbox, 14.0, 200.0, 26.0);
sec.spacing(8.0);
- y = sec.finish(&mut pc);
+ y = sec.finish_focused(&mut pc, sec_focused.get(0).copied().unwrap_or(false));
// ── Inertial Input ──
let mut sec = Section::new(&mut pc, cx, y, cw, "Inertial Input");
@@ -330,7 +330,7 @@ pub fn view(state: &mut InputState, cx: f32, cy: f32, cw: f32, _ch: f32) -> Page
sec.widget(&mut pc, &mut state.trackpad_friction_spinbox, 14.0, 200.0, 26.0);
sec.spacing(8.0);
- y = sec.finish(&mut pc);
+ y = sec.finish_focused(&mut pc, sec_focused.get(1).copied().unwrap_or(false));
// ── Keybindings ──
let mut sec = Section::new(&mut pc, cx, y, cw, "Keyboard Bindings");
diff --git a/src/pages/layout.rs b/src/pages/layout.rs
index cfd0d2a..c0da47e 100644
--- a/src/pages/layout.rs
+++ b/src/pages/layout.rs
@@ -3,7 +3,7 @@ use std::io::Write;
use crate::app::PageContent;
use clear_ui::layout::Section;
-use clear_ui::widget::{ColorSelector, Spinbox};
+use clear_ui::widget::Spinbox;
const CONFIG_PATH: &str = "/home/lsgalante/.config/clearwm/config.toml";
const CLEARWM_SOCK: &str = "/tmp/clearwm.sock";
@@ -53,8 +53,6 @@ fn make_spinboxes(fs: u16, ca: u16, g: u16, v: u16, h: u16, fl: u16) -> Vec<Spin
#[derive(Debug, Clone)]
pub struct LayoutState {
- pub background_color: [u8; 3],
- pub border_color: [u8; 3],
pub fullscreen_border_width: u16,
pub cascade_border_width: u16,
pub grid_border_width: u16,
@@ -64,19 +62,15 @@ pub struct LayoutState {
pub cascade_offset: u16,
pub edge_gap: u16,
pub top_gap: u16,
- pub color_options: Vec<(&'static str, [u8; 3])>,
pub spinboxes: Vec<Spinbox>,
pub cascade_offset_spinbox: Spinbox,
pub edge_gap_spinbox: Spinbox,
pub top_gap_spinbox: Spinbox,
- pub color_selectors: Vec<ColorSelector>,
}
impl Default for LayoutState {
fn default() -> Self {
Self {
- background_color: [0x0a, 0x1a, 0x0e],
- border_color: [0x3e, 0x3e, 0x3e],
fullscreen_border_width: 0,
cascade_border_width: 6,
grid_border_width: 6,
@@ -86,25 +80,16 @@ impl Default for LayoutState {
cascade_offset: 20,
edge_gap: 48,
top_gap: 48,
- color_options: preset_colors(),
spinboxes: make_spinboxes(0, 6, 6, 6, 6, 6),
cascade_offset_spinbox: Spinbox::new(20, 0, 200, 1),
edge_gap_spinbox: Spinbox::new(48, 0, 200, 1),
top_gap_spinbox: Spinbox::new(48, 0, 200, 1),
- color_selectors: vec![
- ColorSelector::new([0x0a, 0x1a, 0x0e]).with_label("Desktop Background"),
- ColorSelector::new([0x3e, 0x3e, 0x3e]).with_label("Border Color"),
- ],
}
}
}
#[derive(Debug, Clone)]
pub enum LayoutMessage {
- SetBackground([u8; 3]),
- SetBorderColor([u8; 3]),
- PickBackgroundColor,
- PickBorderColor,
SetWidth(WidthParam, u16),
SetCascadeOffset(u16),
SetEdgeGap(u16),
@@ -112,23 +97,6 @@ pub enum LayoutMessage {
Refreshed(LayoutState),
}
-fn preset_colors() -> Vec<(&'static str, [u8; 3])> {
- vec![
- ("Black", [0x00, 0x00, 0x00]),
- ("Dark Gray", [0x1a, 0x1a, 0x2e]),
- ("Slate", [0x2d, 0x2d, 0x3d]),
- ("Dark Forest", [0x0a, 0x1a, 0x0e]),
- ("Forest", [0x1a, 0x2a, 0x1c]),
- ("Dark Teal", [0x0a, 0x1a, 0x1e]),
- ("Navy", [0x0a, 0x0f, 0x2e]),
- ("Dark Wine", [0x1e, 0x0a, 0x14]),
- ("Dark Brown", [0x1e, 0x16, 0x0e]),
- ("Charcoal", [0x22, 0x22, 0x22]),
- ("Midnight", [0x10, 0x10, 0x20]),
- ("Deep Sea", [0x06, 0x14, 0x1e]),
- ]
-}
-
pub fn read_layout_config() -> LayoutState {
let content = fs::read_to_string(CONFIG_PATH).unwrap_or_default();
let fs = parse_u16_from(&content, "fullscreen_border_width", 0);
@@ -141,8 +109,6 @@ pub fn read_layout_config() -> LayoutState {
let gl = parse_u16_from(&content, "gap_left", 48);
let gt = parse_u16_from(&content, "gap_top", 48);
LayoutState {
- background_color: parse_color_from_key(&content, "background_color", [0x0a, 0x1a, 0x0e]),
- border_color: parse_color_from_key(&content, "border_color", [0x3e, 0x3e, 0x3e]),
fullscreen_border_width: fs,
cascade_border_width: ca,
grid_border_width: g,
@@ -152,32 +118,13 @@ pub fn read_layout_config() -> LayoutState {
cascade_offset: co,
edge_gap: gl,
top_gap: gt,
- color_options: preset_colors(),
spinboxes: make_spinboxes(fs, ca, g, v, h, fl),
cascade_offset_spinbox: Spinbox::new(co as i32, 0, 200, 1),
edge_gap_spinbox: Spinbox::new(gl as i32, 0, 200, 1),
top_gap_spinbox: Spinbox::new(gt as i32, 0, 200, 1),
- color_selectors: vec![
- ColorSelector::new(parse_color_from_key(&content, "background_color", [0x0a, 0x1a, 0x0e]))
- .with_label("Desktop Background"),
- ColorSelector::new(parse_color_from_key(&content, "border_color", [0x3e, 0x3e, 0x3e]))
- .with_label("Border Color"),
- ],
}
}
-fn parse_color_from_key(content: &str, key: &str, default: [u8; 3]) -> [u8; 3] {
- 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 hex = rest.trim_end_matches('"').trim();
- return parse_hex(hex);
- }
- }
- default
-}
-
fn parse_u16_from(content: &str, key: &str, default: u16) -> u16 {
for line in content.lines() {
let trimmed = line.trim();
@@ -189,16 +136,6 @@ fn parse_u16_from(content: &str, key: &str, default: u16) -> u16 {
default
}
-fn parse_hex(s: &str) -> [u8; 3] {
- let s = s.trim_start_matches('#');
- if s.len() >= 6 {
- let r = u8::from_str_radix(&s[0..2], 16).unwrap_or(0x0a);
- let g = u8::from_str_radix(&s[2..4], 16).unwrap_or(0x1a);
- let b = u8::from_str_radix(&s[4..6], 16).unwrap_or(0x0e);
- [r, g, b]
- } else { [0x0a, 0x1a, 0x0e] }
-}
-
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);
@@ -231,21 +168,6 @@ fn send_ipc_command(cmd: &str) {
}
}
-fn apply_background(rgb: [u8; 3]) {
- let _ = std::process::Command::new("pkill").args(["-x", "swaybg"]).status();
- std::thread::sleep(std::time::Duration::from_millis(100));
- let hex = format!("{:02x}{:02x}{:02x}", rgb[0], rgb[1], rgb[2]);
- let _ = std::process::Command::new("swaybg").arg("-c").arg(&hex).spawn();
- write_config_value("background_color", &format!("\"#{}\"", hex));
- send_ipc_command(&format!("layout background_color #{}", hex));
-}
-
-fn apply_border_color(rgb: [u8; 3]) {
- let hex = format!("\"#{:02x}{:02x}{:02x}\"", rgb[0], rgb[1], rgb[2]);
- write_config_value("border_color", &hex);
- send_ipc_command(&format!("layout border_color #{:02x}{:02x}{:02x}", rgb[0], rgb[1], rgb[2]));
-}
-
fn apply_all_widths(s: &LayoutState) {
let w = |k: &str, v: u16| { write_config_value(k, &v.to_string()); send_ipc_command(&format!("layout {} {}", k, v)); };
w("fullscreen_border_width", s.fullscreen_border_width);
@@ -261,20 +183,10 @@ fn apply_all_widths(s: &LayoutState) {
w("gap_top", s.top_gap);
}
-pub fn view(state: &mut LayoutState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
+pub fn view(state: &mut LayoutState, cx: f32, cy: f32, cw: f32, _ch: f32, sec_focused: &[bool]) -> PageContent {
let mut pc = PageContent::new();
let mut y = cy + 12.0;
- let mut sec = Section::new(&mut pc, cx, y, cw, "Desktop Background");
- state.color_selectors[0].color = state.background_color;
- sec.widget(&mut pc, &mut state.color_selectors[0], 12.0, 220.0, 22.0);
- y = sec.finish(&mut pc);
-
- let mut sec = Section::new(&mut pc, cx, y, cw, "Border Color");
- state.color_selectors[1].color = state.border_color;
- sec.widget(&mut pc, &mut state.color_selectors[1], 12.0, 220.0, 22.0);
- y = sec.finish(&mut pc);
-
for (i, param) in WidthParam::ALL.iter().enumerate() {
let mut sec = Section::new(&mut pc, cx, y, cw, param.label());
sec.spacing(8.0);
@@ -292,7 +204,7 @@ pub fn view(state: &mut LayoutState, cx: f32, cy: f32, cw: f32, _ch: f32) -> Pag
sec.widget(&mut pc, &mut state.top_gap_spinbox, 14.0, 200.0, 26.0);
}
sec.spacing(8.0);
- y = sec.finish(&mut pc);
+ y = sec.finish_focused(&mut pc, sec_focused.get(i).copied().unwrap_or(false));
}
pc
@@ -325,15 +237,6 @@ fn param_idx(p: WidthParam) -> usize {
pub fn update(state: &mut LayoutState, msg: LayoutMessage) {
match msg {
- LayoutMessage::SetBackground(rgb) => {
- state.background_color = rgb;
- apply_background(rgb);
- }
- LayoutMessage::SetBorderColor(rgb) => {
- state.border_color = rgb;
- apply_border_color(rgb);
- }
- LayoutMessage::PickBackgroundColor | LayoutMessage::PickBorderColor => {}
LayoutMessage::SetWidth(p, v) => set_width(state, p, v),
LayoutMessage::SetCascadeOffset(v) => {
let val = v.min(200);
diff --git a/src/pages/mod.rs b/src/pages/mod.rs
index 80e3046..56878b4 100644
--- a/src/pages/mod.rs
+++ b/src/pages/mod.rs
@@ -13,6 +13,7 @@ pub mod notifications;
pub mod backup;
pub mod typeface;
pub mod services;
+pub mod colors;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Page {
@@ -29,13 +30,15 @@ pub enum Page {
Status,
Notifications,
Backup,
- Typeface,
+ Typefaces,
+ Colors,
}
impl Page {
- pub const ALL: [Page; 14] = [
+ pub const ALL: [Page; 15] = [
Page::Audio,
Page::Backup,
+ Page::Colors,
Page::Display,
Page::Input,
Page::Layout,
@@ -47,7 +50,7 @@ impl Page {
Page::Status,
Page::Storage,
Page::System,
- Page::Typeface,
+ Page::Typefaces,
];
pub fn label(self) -> &'static str {
@@ -65,7 +68,8 @@ impl Page {
Page::Status => "Status",
Page::Notifications => "Notifications",
Page::Backup => "Backup",
- Page::Typeface => "Typeface",
+ Page::Typefaces => "Typefaces",
+ Page::Colors => "Colors",
}
}
diff --git a/src/pages/network.rs b/src/pages/network.rs
index 9e1dcdd..a55db88 100644
--- a/src/pages/network.rs
+++ b/src/pages/network.rs
@@ -1,5 +1,6 @@
use crate::app::{AppAction, PageContent};
use clear_ui::layout::Section;
+use clear_ui::widget::{Widget, TextLabel, ScrollBox, ScrollingList};
#[derive(Debug, Clone)]
pub struct WifiNetwork {
@@ -29,6 +30,7 @@ pub struct NetworkState {
pub bt_enabled: bool,
pub bt_devices: Vec<BluetoothDevice>,
pub bt_scanning: bool,
+ pub wifi_list_box: ScrollingList,
}
#[derive(Debug, Clone)]
@@ -90,6 +92,7 @@ pub async fn fetch_network_state() -> NetworkState {
wifi_enabled, connected_ssid, signal_strength: signal,
ip_address, device, available,
bt_enabled, bt_devices, bt_scanning: false,
+ wifi_list_box: ScrollingList::new(26.0, 4.0),
}
}
@@ -205,7 +208,7 @@ const NET_BTN: [f32; 4] = [0.13, 0.20, 0.27, 1.0];
const ACT_BTN: [f32; 4] = [0.16, 0.29, 0.18, 1.0];
const WHITE: [f32; 4] = [1.0, 1.0, 1.0, 1.0];
-pub fn view(state: &NetworkState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
+pub fn view(state: &mut NetworkState, cx: f32, cy: f32, cw: f32, _ch: f32, root_focused: bool) -> PageContent {
let mut pc = PageContent::new();
let mut y = cy + 12.0;
@@ -235,21 +238,31 @@ pub fn view(state: &NetworkState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageCo
}
if state.wifi_enabled && !state.available.is_empty() {
- for net in &state.available {
- let prefix = if net.in_use { ">" } else { " " };
- let label = format!("{} {} ({}%)", prefix, net.ssid, net.signal);
- let active = net.in_use;
- let yt = sec.ay();
- pc.button(&label, sec.ax(14.0), yt, cw - 28.0, 26.0,
- if active { ACT_BTN } else { NET_BTN }, BTN_HOVER,
- if active { ACCENT } else { TEXT_FG },
- AppAction::Radios(NetworkMessage::ConnectWifi(net.ssid.clone())));
- sec.content_y += 30.0;
+ let list_box_x = cx + 12.0;
+ let list_box_y = sec.ay();
+ let list_box_w = cw - 24.0;
+ let list_box_h = 160.0;
+
+ clear_ui::layout::render_widget(&mut 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);
+
+ for (idx, net) in state.available.iter().enumerate() {
+ if let Some(draw_y) = state.wifi_list_box.get_item_draw_y(idx, 4.0) {
+ let prefix = if net.in_use { ">" } else { " " };
+ let label = format!("{} {} ({}%)", prefix, net.ssid, net.signal);
+ let active = net.in_use;
+ pc.button(&label, list_box_x + 4.0, draw_y, list_box_w - 24.0, 26.0,
+ if active { ACT_BTN } else { NET_BTN }, BTN_HOVER,
+ if active { ACCENT } else { TEXT_FG },
+ AppAction::Radios(NetworkMessage::ConnectWifi(net.ssid.clone())));
+ }
}
+ sec.content_y += list_box_h + 8.0;
}
}
- y = sec.finish(&mut pc);
+ y = sec.finish_focused(&mut pc, root_focused);
// ── Bluetooth ──
let mut sec = Section::new(&mut pc, cx, y, cw, "Bluetooth");
@@ -297,7 +310,11 @@ pub fn view(state: &NetworkState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageCo
pub fn update(state: &mut NetworkState, msg: NetworkMessage) {
match msg {
- NetworkMessage::Refreshed(new) => { *state = new; }
+ NetworkMessage::Refreshed(new) => {
+ let old_scroll = state.wifi_list_box.scroll_y();
+ *state = new;
+ state.wifi_list_box.set_scroll_y(old_scroll);
+ }
NetworkMessage::ToggleWifi => {
state.wifi_enabled = !state.wifi_enabled;
wifi_toggle(state.wifi_enabled);
diff --git a/src/pages/processors.rs b/src/pages/processors.rs
index 2c7f855..a225900 100644
--- a/src/pages/processors.rs
+++ b/src/pages/processors.rs
@@ -1,6 +1,6 @@
use crate::app::PageContent;
use clear_ui::layout::Section;
-use clear_ui::widget::{Label, ScrollBox};
+use clear_ui::widget::{Label, ScrollingList};
#[derive(Debug, Clone)]
pub struct ProcessorsState {
@@ -12,7 +12,7 @@ pub struct ProcessorsState {
pub cpu_label: Label,
pub gpu_labels: Vec<Label>,
pub processes: Vec<(String, String, String)>, // (pid, cpu, comm)
- pub cpu_list_box: ScrollBox,
+ pub cpu_list_box: ScrollingList,
}
impl Default for ProcessorsState {
@@ -26,7 +26,7 @@ impl Default for ProcessorsState {
cpu_label: Label::new("CPU Info"),
gpu_labels: Vec::new(),
processes: Vec::new(),
- cpu_list_box: ScrollBox::new(),
+ cpu_list_box: ScrollingList::new(24.0, 2.0),
}
}
}
@@ -197,14 +197,14 @@ pub async fn fetch_processors_state() -> ProcessorsState {
cpu_label: Label::new(&cpu_label_text).with_font_size(12.0).with_color([212, 212, 212]),
gpu_labels,
processes,
- cpu_list_box: ScrollBox::new(),
+ cpu_list_box: ScrollingList::new(24.0, 2.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];
-pub fn view(state: &mut ProcessorsState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
+pub fn view(state: &mut ProcessorsState, cx: f32, cy: f32, cw: f32, _ch: f32, root_focused: bool) -> PageContent {
let mut pc = PageContent::new();
let mut y = cy + 12.0;
@@ -238,18 +238,13 @@ pub fn view(state: &mut ProcessorsState, cx: f32, cy: f32, cw: f32, _ch: f32) ->
let row_h = 24.0;
let row_gap = 2.0;
- let item_height_full = row_h + row_gap;
- let content_h = state.processes.len() as f32 * item_height_full;
-
- // Update ScrollBox bounds for the scrollable viewport (which starts below the header)
- state.cpu_list_box.update_bounds(content_h, list_box_y + header_h, list_box_h - header_h - 6.0);
+ // Update ScrollingList bounds for the scrollable viewport (which starts below the header)
+ state.cpu_list_box.update_bounds(state.processes.len(), list_box_y + header_h, list_box_h - header_h - 6.0);
// Visible process rows rendering (virtualized/clipped)
for (idx, (pid, cpu, comm)) in state.processes.iter().enumerate() {
- let virtual_y = idx as f32 * item_height_full + 4.0;
-
- if let Some(draw_y) = state.cpu_list_box.get_item_draw_y(virtual_y, row_h) {
+ 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(
"",
@@ -275,7 +270,7 @@ pub fn view(state: &mut ProcessorsState, cx: f32, cy: f32, cw: f32, _ch: f32) ->
sec.content_y += list_box_h;
}
- y = sec.finish(&mut pc);
+ y = sec.finish_focused(&mut pc, root_focused);
// ── GPU Section ──
let mut sec_gpu = Section::new(&mut pc, cx, y, cw, "GPU");
@@ -304,9 +299,9 @@ pub fn update(state: &mut ProcessorsState, msg: ProcessorsMessage) {
state.cpu_label = new.cpu_label;
state.gpu_labels = new.gpu_labels;
state.processes = new.processes;
- let old_scroll = state.cpu_list_box.scroll_y;
+ let old_scroll = state.cpu_list_box.scroll_y();
state.cpu_list_box = new.cpu_list_box;
- state.cpu_list_box.scroll_y = old_scroll;
+ state.cpu_list_box.set_scroll_y(old_scroll);
}
ProcessorsMessage::None => {}
}
diff --git a/src/pages/services.rs b/src/pages/services.rs
index 91f2f7a..e7fdb0a 100644
--- a/src/pages/services.rs
+++ b/src/pages/services.rs
@@ -1,6 +1,6 @@
use crate::app::PageContent;
use clear_ui::layout::Section;
-use clear_ui::widget::{Widget, TextLabel, ScrollBox};
+use clear_ui::widget::{Widget, TextLabel, ScrollBox, ScrollingList};
use clear_ui::widget::{ElementState, KeyEvent, MouseButton, Key, NamedKey};
// ── TextBox Widget ──
@@ -16,6 +16,8 @@ pub struct TextBox {
label: Option<String>,
row_x: f32,
row_w: f32,
+ pub parent: Option<*mut (dyn Widget + 'static)>,
+ pub children: Vec<*mut (dyn Widget + 'static)>,
}
impl TextBox {
@@ -30,6 +32,8 @@ impl TextBox {
label: None,
row_x: 0.0,
row_w: 0.0,
+ parent: None,
+ children: Vec::new(),
}
}
@@ -89,7 +93,15 @@ impl Widget for TextBox {
fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
if button != MouseButton::Left { return false; }
if state != ElementState::Pressed { return false; }
- if !self.hit_test(px, py) { return false; }
+ let (x, y, w, h) = self.rect();
+ let (hy, hh) = if self.label.is_some() {
+ (y - 18.0, h + 18.0)
+ } else {
+ (y, h)
+ };
+ if !(px >= x && px <= x + w && py >= hy && py <= hy + hh) {
+ return false;
+ }
self.focus();
true
}
@@ -98,6 +110,7 @@ impl Widget for TextBox {
if !self.editing {
self.editing = true;
self.edit_buffer = self.text.clone();
+ clear_ui::widget::focus::set_focused(self);
}
}
@@ -202,8 +215,23 @@ impl Widget for TextBox {
});
labels
}
+
+ fn parent(&self) -> Option<*mut (dyn Widget + 'static)> { self.parent }
+ fn set_parent(&mut self, parent: Option<*mut (dyn Widget + 'static)>) { self.parent = parent; }
+ fn children(&self) -> Vec<*mut (dyn Widget + 'static)> { self.children.clone() }
+ fn add_child(&mut self, child: *mut (dyn Widget + 'static)) { self.children.push(child); }
+ fn clear_children(&mut self) { self.children.clear(); }
+}
+
+impl Drop for TextBox {
+ fn drop(&mut self) {
+ clear_ui::widget::focus::clear_if_matches(self);
+ }
}
+unsafe impl Send for TextBox {}
+unsafe impl Sync for TextBox {}
+
// ── Service Types and Page State ──
#[derive(Debug, Clone)]
@@ -233,7 +261,7 @@ pub struct ServicesState {
pub services: Vec<ServiceInfo>,
pub active_tab: ServiceTab,
pub search_box: TextBox,
- pub list_box: ScrollBox,
+ pub list_box: ScrollingList,
}
impl Default for ServicesState {
@@ -243,7 +271,7 @@ impl Default for ServicesState {
services: Vec::new(),
active_tab: ServiceTab::System,
search_box: TextBox::new(String::new()).with_label("Filter Services"),
- list_box: ScrollBox::new(),
+ list_box: ScrollingList::new(36.0, 6.0),
}
}
}
@@ -337,7 +365,7 @@ 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) -> PageContent {
+pub fn view(state: &mut ServicesState, cx: f32, cy: f32, cw: f32, _ch: f32, root_focused: bool) -> PageContent {
let mut pc = PageContent::new();
let y = cy + 12.0;
@@ -415,16 +443,13 @@ pub fn view(state: &mut ServicesState, cx: f32, cy: f32, cw: f32, _ch: f32) -> P
.filter(|s| s.name.to_lowercase().contains(&query) || s.description.to_lowercase().contains(&query))
.collect();
- let item_h = 36.0;
- let item_gap = 6.0;
- let item_height_full = item_h + item_gap;
- let content_h = filtered_services.len() as f32 * item_height_full;
+ // Update ScrollingList bounds
+ state.list_box.update_bounds(filtered_services.len(), list_box_y, list_box_h);
- state.list_box.update_bounds(content_h, list_box_y, list_box_h);
+ let item_h = state.list_box.item_height;
for (idx, service) in filtered_services.iter().enumerate() {
- let virtual_y = idx as f32 * item_height_full + 4.0;
- if let Some(draw_y) = state.list_box.get_item_draw_y(virtual_y, item_h) {
+ 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);
@@ -514,7 +539,7 @@ pub fn view(state: &mut ServicesState, cx: f32, cy: f32, cw: f32, _ch: f32) -> P
sec.content_y += list_box_h;
}
- sec.finish(&mut pc);
+ sec.finish_focused(&mut pc, root_focused);
pc
}
@@ -526,7 +551,7 @@ pub fn update(state: &mut ServicesState, msg: ServicesMessage) {
}
ServicesMessage::SetTab(tab) => {
state.active_tab = tab;
- state.list_box.scroll_y = 0.0;
+ state.list_box.set_scroll_y(0.0);
}
ServicesMessage::Start(name, is_system) => {
if let Some(srv) = state.services.iter_mut().find(|s| s.name == name && s.is_system == is_system) {
diff --git a/src/pages/typeface.rs b/src/pages/typeface.rs
index 0696163..9bdab1c 100644
--- a/src/pages/typeface.rs
+++ b/src/pages/typeface.rs
@@ -1,7 +1,7 @@
use std::fs;
use crate::app::PageContent;
use clear_ui::layout::Section;
-use clear_ui::widget::{Widget, TextLabel, ScrollBox};
+use clear_ui::widget::{Widget, TextLabel, ScrollBox, ScrollingList, Dropdown};
use clear_ui::widget::{ElementState, KeyEvent, MouseButton, Key, NamedKey};
const FONTS_CONF_PATH: &str = "/home/lsgalante/.config/fontconfig/fonts.conf";
@@ -19,6 +19,9 @@ pub struct TextBox {
label: Option<String>,
row_x: f32,
row_w: f32,
+ pub disabled: bool,
+ pub parent: Option<*mut (dyn Widget + 'static)>,
+ pub children: Vec<*mut (dyn Widget + 'static)>,
}
impl TextBox {
@@ -33,6 +36,9 @@ impl TextBox {
label: None,
row_x: 0.0,
row_w: 0.0,
+ disabled: false,
+ parent: None,
+ children: Vec::new(),
}
}
@@ -84,23 +90,39 @@ impl Widget for TextBox {
fn top_room(&self) -> f32 { if self.label.is_some() { 18.0 } else { 0.0 } }
fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
+ if self.disabled {
+ let was = self.hovered;
+ self.hovered = false;
+ return was;
+ }
let was = self.hovered;
self.hovered = self.hit_test(px, py);
was != self.hovered
}
fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
+ if self.disabled { return false; }
if button != MouseButton::Left { return false; }
if state != ElementState::Pressed { return false; }
- if !self.hit_test(px, py) { return false; }
+ let (x, y, w, h) = self.rect();
+ let (hy, hh) = if self.label.is_some() {
+ (y - 18.0, h + 18.0)
+ } else {
+ (y, h)
+ };
+ if !(px >= x && px <= x + w && py >= hy && py <= hy + hh) {
+ return false;
+ }
self.focus();
true
}
fn focus(&mut self) {
+ if self.disabled { return; }
if !self.editing {
self.editing = true;
self.edit_buffer = self.text.clone();
+ clear_ui::widget::focus::set_focused(self);
}
}
@@ -115,6 +137,7 @@ impl Widget for TextBox {
}
fn keyboard_input(&mut self, event: &KeyEvent) -> bool {
+ if self.disabled { return false; }
if !self.editing { return false; }
if event.state != ElementState::Pressed { return false; }
match &event.logical_key {
@@ -153,6 +176,11 @@ impl Widget for TextBox {
fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
let mut quads = Vec::new();
+ if self.disabled {
+ quads.push((self.x, self.y, self.w, self.h, [0.12, 0.12, 0.16, 1.0])); // border
+ quads.push((self.x + 1.0, self.y + 1.0, self.w - 2.0, self.h - 2.0, [0.06, 0.06, 0.08, 1.0])); // bg
+ return quads;
+ }
if self.hovered {
let (hy, hh) = if self.label.is_some() {
(self.y - 18.0, self.h + 18.0)
@@ -201,12 +229,27 @@ impl Widget for TextBox {
x: self.x + 8.0,
y: self.y + (self.h - 12.0) / 2.0,
font_size: 13.0,
- color: if self.editing { [0xee, 0xee, 0xf5] } else { [0xcc, 0xcc, 0xd4] },
+ color: if self.disabled { [0x53, 0x53, 0x5a] } else if self.editing { [0xee, 0xee, 0xf5] } else { [0xcc, 0xcc, 0xd4] },
});
labels
}
+
+ fn parent(&self) -> Option<*mut (dyn Widget + 'static)> { self.parent }
+ fn set_parent(&mut self, parent: Option<*mut (dyn Widget + 'static)>) { self.parent = parent; }
+ fn children(&self) -> Vec<*mut (dyn Widget + 'static)> { self.children.clone() }
+ fn add_child(&mut self, child: *mut (dyn Widget + 'static)) { self.children.push(child); }
+ fn clear_children(&mut self) { self.children.clear(); }
+}
+
+impl Drop for TextBox {
+ fn drop(&mut self) {
+ clear_ui::widget::focus::clear_if_matches(self);
+ }
}
+unsafe impl Send for TextBox {}
+unsafe impl Sync for TextBox {}
+
// ── TypefaceState and TypefaceMessage ──
#[derive(Debug, Clone)]
@@ -230,7 +273,11 @@ pub struct TypefaceState {
pub terminal_box: TextBox,
pub search_box: TextBox,
pub selected_font: Option<String>,
- pub list_box: ScrollBox,
+ pub list_box: ScrollingList,
+ pub borders_menu: Dropdown,
+ pub status_menu: Dropdown,
+ pub fuzzel_menu: Dropdown,
+ pub terminal_menu: Dropdown,
}
impl Default for TypefaceState {
@@ -255,7 +302,11 @@ impl Default for TypefaceState {
terminal_box: TextBox::default(),
search_box: TextBox::default(),
selected_font: None,
- list_box: ScrollBox::new(),
+ list_box: ScrollingList::new(24.0, 4.0),
+ borders_menu: Dropdown::default(),
+ status_menu: Dropdown::default(),
+ fuzzel_menu: Dropdown::default(),
+ terminal_menu: Dropdown::default(),
}
}
}
@@ -272,6 +323,11 @@ pub enum TypefaceMessage {
SetTerminal(String),
SetSearch(String),
SelectFont(String),
+ CopyFontName(String),
+ SetBordersMenu(usize),
+ SetStatusMenu(usize),
+ SetFuzzelMenu(usize),
+ SetTerminalMenu(usize),
}
fn parse_font_for_alias(content: &str, alias: &str) -> Option<String> {
@@ -445,40 +501,82 @@ pub async fn fetch_typeface_state() -> TypefaceState {
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 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");
+ borders_box.disabled = borders_idx != 3;
+
+ let mut status_box = TextBox::new(status.clone()).with_label("Status Interface");
+ status_box.disabled = status_idx != 3;
+
+ let mut fuzzel_box = TextBox::new(fuzzel_font.clone()).with_label("Fuzzel");
+ fuzzel_box.disabled = fuzzel_idx != 3;
+
+ let mut terminal_box = TextBox::new(term.clone()).with_label("Terminal");
+ terminal_box.disabled = terminal_idx != 3;
+
TypefaceState {
loaded: true,
sans_serif: sans.clone(),
serif: serif.clone(),
monospace: mono.clone(),
- window_borders: borders.clone(),
- status_interface: status.clone(),
- fuzzel: fuzzel_font.clone(),
- terminal: term.clone(),
+ window_borders: borders,
+ status_interface: status,
+ fuzzel: fuzzel_font,
+ terminal: term,
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: TextBox::new(borders).with_label("Window Borders"),
- status_box: TextBox::new(status).with_label("Status Interface"),
- fuzzel_box: TextBox::new(fuzzel_font).with_label("Fuzzel"),
- terminal_box: TextBox::new(term).with_label("Terminal"),
+ borders_box,
+ status_box,
+ fuzzel_box,
+ terminal_box,
search_box: TextBox::new(String::new()).with_label("Filter Fonts"),
selected_font,
- list_box: ScrollBox::new(),
+ 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),
}
}
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) -> PageContent {
+pub fn view(state: &mut TypefaceState, cx: f32, cy: f32, cw: f32, _ch: f32, sec_focused: &[bool]) -> PageContent {
let mut pc = PageContent::new();
let mut y = cy + 12.0;
let widget_w = cw - 24.0;
let widget_h = 26.0;
- let mut sec = Section::new(&mut pc, cx, y, cw, "Typeface Settings");
+ // ── System Typefaces Section ──
+ let mut sec = Section::new(&mut pc, cx, y, cw, "System Typefaces");
sec.spacing(8.0);
if !state.loaded {
@@ -495,25 +593,60 @@ pub fn view(state: &mut TypefaceState, cx: f32, cy: f32, cw: f32, _ch: f32) -> P
// Monospace
sec.widget(&mut pc, &mut state.mono_box, 12.0, widget_w, widget_h);
- sec.spacing(12.0);
+ sec.spacing(8.0);
+ }
+ let sys_focused = sec_focused.get(0).copied().unwrap_or(false);
+ y = sec.finish_focused(&mut pc, sys_focused);
+
+ // ── Program Typefaces Section ──
+ let mut sec = Section::new(&mut pc, cx, y, cw, "Program Typefaces");
+ sec.spacing(8.0);
+
+ if !state.loaded {
+ sec.text(&mut pc, "Loading typefaces...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(18.0);
+ } else {
+ let dropdown_w = 120.0;
+ let textbox_w = widget_w - dropdown_w - 12.0;
// Window Borders
- sec.widget(&mut pc, &mut state.borders_box, 12.0, widget_w, widget_h);
- sec.spacing(12.0);
+ let start_y = sec.ay();
+ let top_room = state.borders_box.top_room();
+ state.borders_menu.set_row_rect(cx + 8.0, cw - 16.0);
+ clear_ui::layout::render_widget(&mut pc, &mut state.borders_menu, cx + 12.0, start_y + top_room, dropdown_w, widget_h);
+ state.borders_box.set_row_rect(cx + 8.0, cw - 16.0);
+ clear_ui::layout::render_widget(&mut pc, &mut state.borders_box, cx + 12.0 + dropdown_w + 12.0, start_y + top_room, textbox_w, widget_h);
+ sec.spacing(widget_h + top_room + 12.0);
// Status Interface
- sec.widget(&mut pc, &mut state.status_box, 12.0, widget_w, widget_h);
- sec.spacing(12.0);
+ let start_y = sec.ay();
+ let top_room = state.status_box.top_room();
+ state.status_menu.set_row_rect(cx + 8.0, cw - 16.0);
+ clear_ui::layout::render_widget(&mut pc, &mut state.status_menu, cx + 12.0, start_y + top_room, dropdown_w, widget_h);
+ state.status_box.set_row_rect(cx + 8.0, cw - 16.0);
+ clear_ui::layout::render_widget(&mut pc, &mut state.status_box, cx + 12.0 + dropdown_w + 12.0, start_y + top_room, textbox_w, widget_h);
+ sec.spacing(widget_h + top_room + 12.0);
// Fuzzel
- sec.widget(&mut pc, &mut state.fuzzel_box, 12.0, widget_w, widget_h);
- sec.spacing(12.0);
+ let start_y = sec.ay();
+ let top_room = state.fuzzel_box.top_room();
+ state.fuzzel_menu.set_row_rect(cx + 8.0, cw - 16.0);
+ clear_ui::layout::render_widget(&mut pc, &mut state.fuzzel_menu, cx + 12.0, start_y + top_room, dropdown_w, widget_h);
+ state.fuzzel_box.set_row_rect(cx + 8.0, cw - 16.0);
+ clear_ui::layout::render_widget(&mut pc, &mut state.fuzzel_box, cx + 12.0 + dropdown_w + 12.0, start_y + top_room, textbox_w, widget_h);
+ sec.spacing(widget_h + top_room + 12.0);
// Terminal
- sec.widget(&mut pc, &mut state.terminal_box, 12.0, widget_w, widget_h);
- sec.spacing(8.0);
+ let start_y = sec.ay();
+ let top_room = state.terminal_box.top_room();
+ state.terminal_menu.set_row_rect(cx + 8.0, cw - 16.0);
+ clear_ui::layout::render_widget(&mut pc, &mut state.terminal_menu, cx + 12.0, start_y + top_room, dropdown_w, widget_h);
+ state.terminal_box.set_row_rect(cx + 8.0, cw - 16.0);
+ clear_ui::layout::render_widget(&mut pc, &mut state.terminal_box, cx + 12.0 + dropdown_w + 12.0, start_y + top_room, textbox_w, widget_h);
+ sec.spacing(widget_h + top_room + 8.0);
}
- y = sec.finish(&mut pc);
+ let prog_focused = sec_focused.get(1).copied().unwrap_or(false);
+ y = sec.finish_focused(&mut pc, prog_focused);
// ── Typefaces Section (List & Preview) ──
let mut sec = Section::new(&mut pc, cx, y, cw, "Typefaces");
@@ -551,7 +684,7 @@ pub fn view(state: &mut TypefaceState, cx: f32, cy: f32, cw: f32, _ch: f32) -> P
let list_box_y = left_y;
let list_box_h = 320.0;
- // Render the standardized ScrollBox widget
+ // Render the standardized ScrollingList widget
clear_ui::layout::render_widget(&mut pc, &mut state.list_box, left_x, list_box_y, left_w, list_box_h);
let query = state.search_box.text.to_lowercase();
@@ -560,22 +693,17 @@ pub fn view(state: &mut TypefaceState, cx: f32, cy: f32, cw: f32, _ch: f32) -> P
.collect();
let btn_h = 24.0;
- let btn_gap = 4.0;
let inner_x = left_x + 4.0;
let inner_w = left_w - 16.0; // leave room for scrollbar
- let item_height_full = btn_h + btn_gap;
- let content_h = matching_fonts.len() as f32 * item_height_full;
- // Update ScrollBox bounds to clamp and render correctly
- state.list_box.update_bounds(content_h, list_box_y, list_box_h);
+ // Update ScrollingList bounds to clamp and render correctly
+ state.list_box.update_bounds(matching_fonts.len(), list_box_y, list_box_h);
// Render visible buttons inside scroll region
for (idx, font_name) in matching_fonts.iter().enumerate() {
- let virtual_y = idx as f32 * item_height_full;
-
// Only render buttons that are completely within the visible area
- if let Some(draw_y) = state.list_box.get_item_draw_y(virtual_y, btn_h) {
+ 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 (bg, hover_bg, text_color) = if is_selected {
@@ -588,13 +716,31 @@ pub fn view(state: &mut TypefaceState, cx: f32, cy: f32, cw: f32, _ch: f32) -> P
font_name,
inner_x,
draw_y,
- inner_w,
+ inner_w - 44.0,
btn_h,
bg,
hover_bg,
text_color,
crate::app::AppAction::Typeface(TypefaceMessage::SelectFont((*font_name).clone())),
);
+
+ let (copy_bg, copy_hover_bg, copy_text_color) = if is_selected {
+ ([0.20, 0.40, 0.65, 0.2], [0.30, 0.52, 0.78, 0.5], [0.90, 0.90, 0.95, 1.0])
+ } else {
+ ([0.0, 0.0, 0.0, 0.0], [0.20, 0.20, 0.25, 0.25], [0.70, 0.70, 0.75, 1.0])
+ };
+
+ pc.button(
+ "📋",
+ inner_x + inner_w - 40.0,
+ draw_y,
+ 40.0,
+ btn_h,
+ copy_bg,
+ copy_hover_bg,
+ copy_text_color,
+ crate::app::AppAction::Typeface(TypefaceMessage::CopyFontName((*font_name).clone())),
+ );
}
}
@@ -607,6 +753,33 @@ pub fn view(state: &mut TypefaceState, cx: f32, cy: f32, cw: f32, _ch: f32) -> P
// 2. Render Right Column (Live Preview)
let mut right_y = start_y;
+ // Render Info Box
+ let info_h = 96.0;
+ let info_bg = [0.12, 0.18, 0.28, 0.3]; // Sleek translucent blue-ish background
+ let info_border = [0.25, 0.40, 0.60, 0.5]; // Soft blue border
+
+ pc.rect(info_bg, right_x, right_y, right_w, info_h);
+ pc.rect(info_border, right_x, right_y, right_w, 1.0);
+ pc.rect(info_border, right_x, right_y + info_h - 1.0, right_w, 1.0);
+ pc.rect(info_border, right_x, right_y, 1.0, info_h);
+ pc.rect(info_border, right_x + right_w - 1.0, right_y, 1.0, info_h);
+
+ let text_padding_x = 16.0;
+ let mut text_y = right_y + 12.0;
+
+ pc.text("Font Directories & Installation", right_x + 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", right_x + 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.", right_x + 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.", right_x + text_padding_x, text_y, 11.0, [0.55, 0.55, 0.60, 1.0]);
+
+ right_y += info_h + 12.0;
+
if let Some(ref font_name) = state.selected_font {
let card_h = 240.0;
pc.rect([0.10, 0.10, 0.14, 0.3], right_x, right_y, right_w, card_h);
@@ -665,7 +838,7 @@ pub fn view(state: &mut TypefaceState, cx: f32, cy: f32, cw: f32, _ch: f32) -> P
text_y += 32.0;
pc.text_with_font(
- "Pack my box with five dozen liquor jugs.",
+ "The five boxing wizards jump quickly.",
right_x + text_padding_x,
text_y,
24.0,
@@ -681,7 +854,45 @@ pub fn view(state: &mut TypefaceState, cx: f32, cy: f32, cw: f32, _ch: f32) -> P
sec.content_y = left_y.max(right_y);
}
- sec.finish(&mut pc);
+ let list_focused = sec_focused.get(2).copied().unwrap_or(false);
+ sec.finish_focused(&mut pc, list_focused);
+
+ // Filter out base text items covered by any open popover to prevent showing through
+ let mut popovers = Vec::new();
+ if state.borders_menu.open {
+ let (x, y, w, h) = state.borders_menu.rect();
+ popovers.push((x, y + h, w, state.borders_menu.options.len() as f32 * 24.0));
+ }
+ if state.status_menu.open {
+ let (x, y, w, h) = state.status_menu.rect();
+ popovers.push((x, y + h, w, state.status_menu.options.len() as f32 * 24.0));
+ }
+ if state.fuzzel_menu.open {
+ let (x, y, w, h) = state.fuzzel_menu.rect();
+ popovers.push((x, y + h, w, state.fuzzel_menu.options.len() as f32 * 24.0));
+ }
+ if state.terminal_menu.open {
+ let (x, y, w, h) = state.terminal_menu.rect();
+ popovers.push((x, y + h, w, state.terminal_menu.options.len() as f32 * 24.0));
+ }
+
+ if !popovers.is_empty() {
+ pc.texts.retain(|(_, _, tx, ty, _, _)| {
+ for &(px, py, pw, ph) in &popovers {
+ if *tx >= px && *tx <= px + pw && *ty >= py && *ty <= py + ph {
+ return false;
+ }
+ }
+ true
+ });
+ }
+
+ // Render dropdown popovers on top of all other widgets
+ state.borders_menu.render_popover(&mut pc);
+ state.status_menu.render_popover(&mut pc);
+ state.fuzzel_menu.render_popover(&mut pc);
+ state.terminal_menu.render_popover(&mut pc);
+
pc
}
@@ -709,29 +920,49 @@ pub fn update(state: &mut TypefaceState, msg: TypefaceMessage) {
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.search_box.editing {
state.search_box = new.search_box;
}
- let old_scroll = state.list_box.scroll_y;
+ let old_scroll = state.list_box.scroll_y();
state.list_box = new.list_box;
- state.list_box.scroll_y = old_scroll;
+ 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();
+ }
save_preferred_fonts(
&state.sans_serif,
&state.serif,
@@ -745,6 +976,22 @@ pub fn update(state: &mut TypefaceState, msg: TypefaceMessage) {
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();
+ }
save_preferred_fonts(
&state.sans_serif,
&state.serif,
@@ -758,6 +1005,22 @@ pub fn update(state: &mut TypefaceState, msg: TypefaceMessage) {
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();
+ }
save_preferred_fonts(
&state.sans_serif,
&state.serif,
@@ -826,6 +1089,131 @@ pub fn update(state: &mut TypefaceState, msg: TypefaceMessage) {
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;
+ if let Ok(mut child) = std::process::Command::new("wl-copy")
+ .stdin(std::process::Stdio::piped())
+ .spawn()
+ {
+ if let Some(mut stdin) = child.stdin.take() {
+ if stdin.write_all(text.as_bytes()).is_ok() {
+ copied = true;
+ }
+ }
+ let _ = child.wait();
+ }
+ 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,
+ );
+ }
+ 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,
+ );
+ }
+ 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,
+ );
+ }
+ 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,
+ );
+ }
}
}