system settings
git clone https://git.lucas.co/cce-system-interface.git
refactor: rename colors page to interface, supporting general interface settings and dynamic scale factor
src/app.rs | 14 +-
src/main.rs | 340 ++++++++++-----
src/pages/accounts.rs | 535 ++++++++++++-----------
src/pages/audio.rs | 226 +++++-----
src/pages/backup.rs | 125 +++---
src/pages/display.rs | 145 +++---
src/pages/hardware.rs | 381 ++++++++--------
src/pages/input.rs | 203 +++++----
src/pages/{colors.rs => interface.rs} | 329 ++++++++++----
src/pages/layout.rs | 799 ++++++++--------------------------
src/pages/mod.rs | 8 +-
src/pages/network.rs | 328 ++++++++++----
src/pages/notifications.rs | 106 +++--
src/pages/screensaver.rs | 77 ++--
src/pages/services.rs | 372 ++++++++--------
src/pages/status.rs | 125 +++---
src/pages/storage.rs | 102 ++---
src/pages/system_info.rs | 87 ++--
src/pages/typeface.rs | 565 ++++++++++++------------
19 files changed, 2513 insertions(+), 2354 deletions(-)
diff --git a/src/app.rs b/src/app.rs
index b2f764d..9a45c1e 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -1,4 +1,4 @@
-use clear_ui::layout::RenderTarget;
+use clear_ui::layout::{RenderTarget, Radial};
use crate::pages::audio;
use crate::pages::display;
@@ -13,7 +13,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::interface;
use crate::pages::screensaver;
use crate::pages::accounts;
use crate::pages::Page;
@@ -33,7 +33,7 @@ pub struct AppState {
pub backup: backup::BackupState,
pub typeface: typeface::TypefaceState,
pub services: services::ServicesState,
- pub colors: colors::ColorsState,
+ pub interface: interface::InterfaceState,
pub screensaver: screensaver::ScreensaverState,
pub accounts: accounts::AccountsState,
}
@@ -55,7 +55,7 @@ impl Default for AppState {
backup: backup::BackupState::default(),
typeface: typeface::TypefaceState::default(),
services: services::ServicesState::default(),
- colors: colors::ColorsState::default(),
+ interface: interface::InterfaceState::default(),
screensaver: screensaver::read_screensaver_config(),
accounts: accounts::AccountsState::default_mock(),
}
@@ -77,12 +77,13 @@ pub enum AppAction {
Backup(backup::BackupMessage),
Typeface(typeface::TypefaceMessage),
Services(services::ServicesMessage),
- Colors(colors::ColorsMessage),
+ Interface(interface::InterfaceMessage),
Screensaver(screensaver::ScreensaverMessage),
Accounts(accounts::AccountsMessage),
}
+#[derive(Default)]
pub struct PageContent {
pub rects: Vec<([f32; 4], f32, f32, f32, f32)>,
pub texts: Vec<(String, f32, f32, f32, [f32; 4], Option<String>, Option<[f32; 4]>)>,
@@ -162,3 +163,6 @@ impl RenderTarget for PageContent {
self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string()), bounds));
}
}
+
+
+
diff --git a/src/main.rs b/src/main.rs
index af2af1a..c60d28c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -206,7 +206,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>,
+ rx_interface: std::sync::mpsc::Receiver<pages::interface::InterfaceState>,
rx_accounts: std::sync::mpsc::Receiver<Vec<pages::accounts::AccountInfo>>,
tx_backup: std::sync::mpsc::Sender<pages::backup::BackupMessage>,
rx_backup: std::sync::mpsc::Receiver<pages::backup::BackupMessage>,
@@ -233,6 +233,7 @@ impl clear_ui::engine::Application for SystemInterface {
type Message = AppAction;
fn new(_qh: &wayland_client::QueueHandle<clear_ui::engine::EngineState<Self>>, sender: calloop::channel::Sender<Self::Message>) -> Self {
+ clear_ui::scale::set_scale_factor(1.0);
let app = AppState {
layout: pages::layout::read_layout_config(),
input: pages::input::read_input_config(),
@@ -365,11 +366,11 @@ impl clear_ui::engine::Application for SystemInterface {
let rx_typeface = spawn_bg(30, || pages::typeface::fetch_typeface_state());
let rx_services = spawn_bg(3, || pages::services::fetch_services());
let rx_accounts = spawn_bg(3, || pages::accounts::fetch_accounts());
- let rx_colors = {
- let (tx, rx) = std::sync::mpsc::channel::<pages::colors::ColorsState>();
+ let rx_interface = {
+ let (tx, rx) = std::sync::mpsc::channel::<pages::interface::InterfaceState>();
tokio::spawn(async move {
loop {
- let val = tokio::task::spawn_blocking(|| pages::colors::read_colors_config()).await;
+ let val = tokio::task::spawn_blocking(|| pages::interface::read_interface_config()).await;
if let Ok(val) = val { if tx.send(val).is_err() { break; } }
tokio::time::sleep(std::time::Duration::from_secs(30)).await;
}
@@ -383,7 +384,8 @@ impl clear_ui::engine::Application for SystemInterface {
let pages_names = Page::ALL.iter().map(|p| p.label().to_string()).collect::<Vec<_>>();
let paginator = clear_ui::widget::Paginator::new(56.0, pages_names)
- .with_tabs_rotated(true);
+ .with_tabs_rotated(true)
+ .with_sidebar_label("SYSTEM");
let initial_page_idx = INITIAL_PAGE_INDEX.load(std::sync::atomic::Ordering::SeqCst);
let mut app_state = app;
@@ -397,7 +399,7 @@ impl clear_ui::engine::Application for SystemInterface {
widgets: Vec::new(),
text_items: Vec::new(),
page_buttons: Vec::new(),
- sidebar_width: 56.0,
+ sidebar_width: paginator.sidebar_w(),
header_height: 0.0,
status_height: 0.0,
cursor_x: 0.0,
@@ -418,7 +420,7 @@ impl clear_ui::engine::Application for SystemInterface {
rx_backup_state,
rx_typeface,
rx_services,
- rx_colors,
+ rx_interface,
rx_accounts,
tx_backup,
rx_backup,
@@ -477,7 +479,7 @@ impl clear_ui::engine::Application for SystemInterface {
self.width = width as u32;
self.height = height as u32;
self.scale_factor = scale;
- self.paginator.set_scale_factor(scale as f32);
+ clear_ui::scale::set_scale_factor(scale as f32);
self.rebuild_layout(width, height);
}
for w in &self.widgets {
@@ -491,9 +493,9 @@ impl clear_ui::engine::Application for SystemInterface {
fn clear_color(&self) -> [f32; 4] {
let mut color = [
- self.app.colors.page_low_color[0] as f32 / 255.0,
- self.app.colors.page_low_color[1] as f32 / 255.0,
- self.app.colors.page_low_color[2] as f32 / 255.0,
+ self.app.interface.page_low_color[0] as f32 / 255.0,
+ self.app.interface.page_low_color[1] as f32 / 255.0,
+ self.app.interface.page_low_color[2] as f32 / 255.0,
1.0,
];
if let Some(opacity) = clear_ui::color::read_opacity_if_configured() {
@@ -545,6 +547,7 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
}
fn rebuild_layout(&mut self, sw: f32, sh: f32) {
+ self.sidebar_width = self.paginator.sidebar_w();
let s = 1.0f32;
let mut widgets = Vec::new();
let mut text_items = Vec::new();
@@ -643,10 +646,18 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
self.app.layout.transition_duration_spinbox.clear_children(); self.app.layout.transition_duration_spinbox.set_parent(None);
self.app.layout.status_height_spinbox.clear_children(); self.app.layout.status_height_spinbox.set_parent(None);
- for cs in &mut self.app.colors.color_selectors {
+ for cs in &mut self.app.interface.color_selectors {
cs.clear_children();
cs.set_parent(None);
}
+ self.app.interface.tab_margin_spinbox_x.clear_children();
+ self.app.interface.tab_margin_spinbox_x.set_parent(None);
+ self.app.interface.tab_margin_spinbox_y.clear_children();
+ self.app.interface.tab_margin_spinbox_y.set_parent(None);
+ self.app.interface.tab_padding_spinbox_x.clear_children();
+ self.app.interface.tab_padding_spinbox_x.set_parent(None);
+ self.app.interface.tab_padding_spinbox_y.clear_children();
+ self.app.interface.tab_padding_spinbox_y.set_parent(None);
self.app.notifications.duration_spinbox.clear_children(); self.app.notifications.duration_spinbox.set_parent(None);
self.app.notifications.opacity_slider.clear_children(); self.app.notifications.opacity_slider.set_parent(None);
@@ -756,10 +767,14 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
link_parent_child(&mut self.page_sec_containers[4], menu);
}
}
- Page::Colors => {
- for cs in &mut self.app.colors.color_selectors {
+ Page::Interface => {
+ for cs in &mut self.app.interface.color_selectors {
link_parent_child(&mut self.page_root_container, cs);
}
+ link_parent_child(&mut self.page_root_container, &mut self.app.interface.tab_margin_spinbox_x);
+ link_parent_child(&mut self.page_root_container, &mut self.app.interface.tab_margin_spinbox_y);
+ link_parent_child(&mut self.page_root_container, &mut self.app.interface.tab_padding_spinbox_x);
+ link_parent_child(&mut self.page_root_container, &mut self.app.interface.tab_padding_spinbox_y);
}
Page::Notifications => {
link_parent_child(&mut self.page_root_container, &mut self.app.notifications.duration_spinbox);
@@ -1008,27 +1023,29 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
fn render_page_content(&mut self, cx: f32, cy: f32, cw: f32, ch: f32) -> PageContent {
use pages::*;
+ use clear_ui::layout::{LayoutStrategy, GridLayout};
+ let mut layout = GridLayout::new(320.0, 20.0);
let root_focused = clear_ui::widget::focus::is_focused(&self.page_root_container);
let sec_focused: Vec<bool> = self.page_sec_containers.iter()
.map(|c| clear_ui::widget::focus::is_focused(c))
.collect();
match self.app.current_page {
- Page::Accounts => accounts::view(&mut self.app.accounts, 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(&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::Hardware => hardware::view(&mut self.app.hardware, 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::Screensaver => screensaver::view(&mut self.app.screensaver, 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),
+ Page::Accounts => accounts::view(&mut self.app.accounts, cx, cy, cw, ch, &mut layout),
+ Page::Audio => audio::view(&mut self.app.audio, cx, cy, cw, ch, &sec_focused, &mut layout),
+ Page::Display => display::view(&mut self.app.display, cx, cy, cw, ch, &mut layout),
+ Page::Radios => network::view(&mut self.app.network, cx, cy, cw, ch, root_focused, &mut layout),
+ Page::Layout => layout::view(&mut self.app.layout, cx, cy, cw, ch, &sec_focused, &mut layout),
+ Page::Hardware => hardware::view(&mut self.app.hardware, cx, cy, cw, ch, root_focused, &mut layout),
+ Page::Input => input::view(&mut self.app.input, cx, cy, cw, ch, &sec_focused, &mut layout),
+ Page::System => system_info::view(&self.app.system_info, cx, cy, cw, ch, &mut layout),
+ Page::Status => status::view(&mut self.app.status, cx, cy, cw, ch, &mut layout),
+ Page::Storage => storage::view(&self.app.storage, cx, cy, cw, ch, &mut layout),
+ Page::Notifications => notifications::view(&mut self.app.notifications, cx, cy, cw, ch, &mut layout),
+ Page::Backup => backup::view(&self.app.backup, cx, cy, cw, ch, &mut layout),
+ Page::Screensaver => screensaver::view(&mut self.app.screensaver, cx, cy, cw, ch, &mut layout),
+ Page::Typefaces => typeface::view(&mut self.app.typeface, cx, cy, cw, ch, &sec_focused, &mut layout),
+ Page::Services => services::view(&mut self.app.services, cx, cy, cw, ch, root_focused, &mut layout),
+ Page::Interface => interface::view(&mut self.app.interface, cx, cy, cw, ch, &mut layout),
}
}
@@ -1055,41 +1072,45 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
// Asynchronously check color selector changes (e.g. Zenity process exit)
let mut color_changed = false;
let mut color_actions = Vec::new();
- for (i, cp) in self.app.colors.color_selectors.iter_mut().enumerate() {
+ for (i, cp) in self.app.interface.color_selectors.iter_mut().enumerate() {
if cp.tick(dt) {
needs_redraw = true;
self.needs_rebuild = true;
}
let state_color = match i {
- 0 => self.app.colors.page_low_color,
- 1 => self.app.colors.high_color,
- 2 => self.app.colors.visual_guides_color,
- 3 => self.app.colors.disabled_color,
- 4 => self.app.colors.separator_color,
- 5 => self.app.colors.slider_track_color,
- 6 => self.app.colors.color_borders_color,
- 7 => self.app.colors.low_color,
- 8 => self.app.colors.normal_color,
- 9 => self.app.colors.paginator_sidebar_color,
- 10 => self.app.colors.primary_highlight_color,
- 11 => self.app.colors.paginator_tab_label_color,
- _ => self.app.colors.low_color,
+ 0 => self.app.interface.page_low_color,
+ 1 => self.app.interface.high_color,
+ 2 => self.app.interface.visual_guides_color,
+ 3 => self.app.interface.disabled_color,
+ 4 => self.app.interface.separator_color,
+ 5 => self.app.interface.slider_track_color,
+ 6 => self.app.interface.color_borders_color,
+ 7 => self.app.interface.low_color,
+ 8 => self.app.interface.normal_color,
+ 9 => self.app.interface.paginator_sidebar_color,
+ 10 => self.app.interface.primary_highlight_color,
+ 11 => self.app.interface.paginator_tab_label_color,
+ 12 => self.app.interface.toggle_enabled_color,
+ 13 => self.app.interface.toggle_disabled_color,
+ _ => self.app.interface.low_color,
};
if cp.color != state_color {
- color_actions.push(AppAction::Colors(match i {
- 0 => pages::colors::ColorsMessage::SetPageLowColor(cp.color),
- 1 => pages::colors::ColorsMessage::SetHighColor(cp.color),
- 2 => pages::colors::ColorsMessage::SetVisualGuidesColor(cp.color),
- 3 => pages::colors::ColorsMessage::SetDisabledColor(cp.color),
- 4 => pages::colors::ColorsMessage::SetSeparatorColor(cp.color),
- 5 => pages::colors::ColorsMessage::SetSliderTrackColor(cp.color),
- 6 => pages::colors::ColorsMessage::SetColorBordersColor(cp.color),
- 7 => pages::colors::ColorsMessage::SetLowColor(cp.color),
- 8 => pages::colors::ColorsMessage::SetNormalColor(cp.color),
- 9 => pages::colors::ColorsMessage::SetPaginatorSidebarColor(cp.color),
- 10 => pages::colors::ColorsMessage::SetPrimaryHighlightColor(cp.color),
- 11 => pages::colors::ColorsMessage::SetPaginatorTabLabelColor(cp.color),
- _ => pages::colors::ColorsMessage::SetLowColor(cp.color),
+ color_actions.push(AppAction::Interface(match i {
+ 0 => pages::interface::InterfaceMessage::SetPageLowColor(cp.color),
+ 1 => pages::interface::InterfaceMessage::SetHighColor(cp.color),
+ 2 => pages::interface::InterfaceMessage::SetVisualGuidesColor(cp.color),
+ 3 => pages::interface::InterfaceMessage::SetDisabledColor(cp.color),
+ 4 => pages::interface::InterfaceMessage::SetSeparatorColor(cp.color),
+ 5 => pages::interface::InterfaceMessage::SetSliderTrackColor(cp.color),
+ 6 => pages::interface::InterfaceMessage::SetColorBordersColor(cp.color),
+ 7 => pages::interface::InterfaceMessage::SetLowColor(cp.color),
+ 8 => pages::interface::InterfaceMessage::SetNormalColor(cp.color),
+ 9 => pages::interface::InterfaceMessage::SetPaginatorSidebarColor(cp.color),
+ 10 => pages::interface::InterfaceMessage::SetPrimaryHighlightColor(cp.color),
+ 11 => pages::interface::InterfaceMessage::SetPaginatorTabLabelColor(cp.color),
+ 12 => pages::interface::InterfaceMessage::SetToggleEnabledColor(cp.color),
+ 13 => pages::interface::InterfaceMessage::SetToggleDisabledColor(cp.color),
+ _ => pages::interface::InterfaceMessage::SetLowColor(cp.color),
}));
color_changed = true;
}
@@ -1177,8 +1198,8 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
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));
+ while let Ok(s) = self.rx_interface.try_recv() {
+ interface::update(&mut self.app.interface, pages::interface::InterfaceMessage::Refreshed(s));
self.needs_rebuild = true;
}
while let Ok(s) = self.rx_accounts.try_recv() {
@@ -1192,10 +1213,10 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
while let Ok(action) = self.rx_color_selector.try_recv() {
match action {
ColorSelectorAction::Background(rgb) => {
- colors::update(&mut self.app.colors, pages::colors::ColorsMessage::SetLowColor(rgb));
+ interface::update(&mut self.app.interface, pages::interface::InterfaceMessage::SetLowColor(rgb));
}
ColorSelectorAction::Border(rgb) => {
- colors::update(&mut self.app.colors, pages::colors::ColorsMessage::SetHighColor(rgb));
+ interface::update(&mut self.app.interface, pages::interface::InterfaceMessage::SetHighColor(rgb));
}
}
self.needs_rebuild = true;
@@ -1222,7 +1243,7 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
self.monospace_family = self.app.typeface.monospace.clone();
}
AppAction::Services(m) => services::update(&mut self.app.services, m.clone()),
- AppAction::Colors(m) => colors::update(&mut self.app.colors, m.clone()),
+ AppAction::Interface(m) => interface::update(&mut self.app.interface, m.clone()),
AppAction::Screensaver(m) => screensaver::update(&mut self.app.screensaver, m.clone()),
AppAction::Backup(m) => match m {
pages::backup::BackupMessage::StartBackup => {
@@ -1309,12 +1330,24 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
}
}
}
- if self.app.current_page == Page::Colors {
- for cp in &mut self.app.colors.color_selectors {
+ if self.app.current_page == Page::Interface {
+ for cp in &mut self.app.interface.color_selectors {
if cp.cursor_moved(lx, ly) {
changed = true;
}
}
+ if self.app.interface.tab_margin_spinbox_x.cursor_moved(lx, ly) {
+ changed = true;
+ }
+ if self.app.interface.tab_margin_spinbox_y.cursor_moved(lx, ly) {
+ changed = true;
+ }
+ if self.app.interface.tab_padding_spinbox_x.cursor_moved(lx, ly) {
+ changed = true;
+ }
+ if self.app.interface.tab_padding_spinbox_y.cursor_moved(lx, ly) {
+ changed = true;
+ }
}
if self.app.current_page == Page::Input {
if self.app.input.rate_spinbox.cursor_moved(lx, ly) {
@@ -1615,10 +1648,14 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
if menu.hit_test(lx, ly) { clicked_any_focusable = true; }
}
}
- Page::Colors => {
- for cp in &mut self.app.colors.color_selectors {
+ Page::Interface => {
+ for cp in &mut self.app.interface.color_selectors {
if cp.hit_test(lx, ly) { clicked_any_focusable = true; }
}
+ if self.app.interface.tab_margin_spinbox_x.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if self.app.interface.tab_margin_spinbox_y.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if self.app.interface.tab_padding_spinbox_x.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if self.app.interface.tab_padding_spinbox_y.hit_test(lx, ly) { clicked_any_focusable = true; }
}
Page::Input => {
if self.app.input.rate_spinbox.hit_test(lx, ly) { clicked_any_focusable = true; }
@@ -1773,46 +1810,74 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
}
}
}
- 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() {
+ if state == clear_ui::widget::ElementState::Pressed && self.app.current_page == Page::Interface {
+ for (i, cp) in self.app.interface.color_selectors.iter_mut().enumerate() {
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::Colors(match i {
- 0 => pages::colors::ColorsMessage::PickPageLowColor,
- 1 => pages::colors::ColorsMessage::PickHighColor,
- 2 => pages::colors::ColorsMessage::PickVisualGuides,
- 3 => pages::colors::ColorsMessage::PickDisabledColor,
- 4 => pages::colors::ColorsMessage::PickSeparatorColor,
- 5 => pages::colors::ColorsMessage::PickSliderTrackColor,
- 6 => pages::colors::ColorsMessage::PickColorBordersColor,
- 7 => pages::colors::ColorsMessage::PickLowColor,
- 8 => pages::colors::ColorsMessage::PickNormalColor,
- 9 => pages::colors::ColorsMessage::PickPaginatorSidebarColor,
- 10 => pages::colors::ColorsMessage::PickPrimaryHighlightColor,
- 11 => pages::colors::ColorsMessage::PickPaginatorTabLabelColor,
- _ => pages::colors::ColorsMessage::PickLowColor,
+ actions.push(AppAction::Interface(match i {
+ 0 => pages::interface::InterfaceMessage::PickPageLowColor,
+ 1 => pages::interface::InterfaceMessage::PickHighColor,
+ 2 => pages::interface::InterfaceMessage::PickVisualGuides,
+ 3 => pages::interface::InterfaceMessage::PickDisabledColor,
+ 4 => pages::interface::InterfaceMessage::PickSeparatorColor,
+ 5 => pages::interface::InterfaceMessage::PickSliderTrackColor,
+ 6 => pages::interface::InterfaceMessage::PickColorBordersColor,
+ 7 => pages::interface::InterfaceMessage::PickLowColor,
+ 8 => pages::interface::InterfaceMessage::PickNormalColor,
+ 9 => pages::interface::InterfaceMessage::PickPaginatorSidebarColor,
+ 10 => pages::interface::InterfaceMessage::PickPrimaryHighlightColor,
+ 11 => pages::interface::InterfaceMessage::PickPaginatorTabLabelColor,
+ 12 => pages::interface::InterfaceMessage::PickToggleEnabledColor,
+ 13 => pages::interface::InterfaceMessage::PickToggleDisabledColor,
+ _ => pages::interface::InterfaceMessage::PickLowColor,
}));
}
if cp.color != old {
- actions.push(AppAction::Colors(match i {
- 0 => pages::colors::ColorsMessage::SetPageLowColor(cp.color),
- 1 => pages::colors::ColorsMessage::SetHighColor(cp.color),
- 2 => pages::colors::ColorsMessage::SetVisualGuidesColor(cp.color),
- 3 => pages::colors::ColorsMessage::SetDisabledColor(cp.color),
- 4 => pages::colors::ColorsMessage::SetSeparatorColor(cp.color),
- 5 => pages::colors::ColorsMessage::SetSliderTrackColor(cp.color),
- 6 => pages::colors::ColorsMessage::SetColorBordersColor(cp.color),
- 7 => pages::colors::ColorsMessage::SetLowColor(cp.color),
- 8 => pages::colors::ColorsMessage::SetNormalColor(cp.color),
- 9 => pages::colors::ColorsMessage::SetPaginatorSidebarColor(cp.color),
- 10 => pages::colors::ColorsMessage::SetPrimaryHighlightColor(cp.color),
- 11 => pages::colors::ColorsMessage::SetPaginatorTabLabelColor(cp.color),
- _ => pages::colors::ColorsMessage::SetLowColor(cp.color),
+ actions.push(AppAction::Interface(match i {
+ 0 => pages::interface::InterfaceMessage::SetPageLowColor(cp.color),
+ 1 => pages::interface::InterfaceMessage::SetHighColor(cp.color),
+ 2 => pages::interface::InterfaceMessage::SetVisualGuidesColor(cp.color),
+ 3 => pages::interface::InterfaceMessage::SetDisabledColor(cp.color),
+ 4 => pages::interface::InterfaceMessage::SetSeparatorColor(cp.color),
+ 5 => pages::interface::InterfaceMessage::SetSliderTrackColor(cp.color),
+ 6 => pages::interface::InterfaceMessage::SetColorBordersColor(cp.color),
+ 7 => pages::interface::InterfaceMessage::SetLowColor(cp.color),
+ 8 => pages::interface::InterfaceMessage::SetNormalColor(cp.color),
+ 9 => pages::interface::InterfaceMessage::SetPaginatorSidebarColor(cp.color),
+ 10 => pages::interface::InterfaceMessage::SetPrimaryHighlightColor(cp.color),
+ 11 => pages::interface::InterfaceMessage::SetPaginatorTabLabelColor(cp.color),
+ 12 => pages::interface::InterfaceMessage::SetToggleEnabledColor(cp.color),
+ 13 => pages::interface::InterfaceMessage::SetToggleDisabledColor(cp.color),
+ _ => pages::interface::InterfaceMessage::SetLowColor(cp.color),
}));
}
}
+ let sb = &mut self.app.interface.tab_margin_spinbox_x;
+ if !sb.hit_test(lx, ly) { sb.unfocus(); }
+ let old = sb.value;
+ if sb.mouse_input(button, state, lx, ly) && sb.value != old {
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetTabMarginX(sb.value as u16)));
+ }
+ let sb = &mut self.app.interface.tab_margin_spinbox_y;
+ if !sb.hit_test(lx, ly) { sb.unfocus(); }
+ let old = sb.value;
+ if sb.mouse_input(button, state, lx, ly) && sb.value != old {
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetTabMarginY(sb.value as u16)));
+ }
+ let sb = &mut self.app.interface.tab_padding_spinbox_x;
+ if !sb.hit_test(lx, ly) { sb.unfocus(); }
+ let old = sb.value;
+ if sb.mouse_input(button, state, lx, ly) && sb.value != old {
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetTabPaddingX(sb.value as u16)));
+ }
+ let sb = &mut self.app.interface.tab_padding_spinbox_y;
+ if !sb.hit_test(lx, ly) { sb.unfocus(); }
+ let old = sb.value;
+ if sb.mouse_input(button, state, lx, ly) && sb.value != old {
+ actions.push(AppAction::Interface(pages::interface::InterfaceMessage::SetTabPaddingY(sb.value as u16)));
+ }
}
if state == clear_ui::widget::ElementState::Pressed && self.app.current_page == Page::Input {
let sb = &mut self.app.input.rate_spinbox;
@@ -1981,6 +2046,7 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
actions.push(AppAction::Screensaver(pages::screensaver::ScreensaverMessage::SetStyle(menu.selected)));
}
}
+
if state == clear_ui::widget::ElementState::Pressed && self.app.current_page == Page::Audio {
for (i, sb) in self.app.audio.sink_spinboxes.iter_mut().enumerate() {
if !sb.hit_test(lx, ly) { sb.unfocus(); }
@@ -2438,7 +2504,7 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
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::Hardware | Page::Radios |
- Page::Layout | Page::Colors | Page::Notifications | Page::Input |
+ Page::Layout | Page::Interface | 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 {
@@ -2561,27 +2627,29 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
return true;
}
}
- if self.app.current_page == Page::Colors {
+ if self.app.current_page == Page::Interface {
let mut changed = false;
let mut actions = Vec::new();
- for (i, cp) in self.app.colors.color_selectors.iter_mut().enumerate() {
+ for (i, cp) in self.app.interface.color_selectors.iter_mut().enumerate() {
let old = cp.color;
if cp.keyboard_input(event) {
if cp.color != old {
- actions.push(AppAction::Colors(match i {
- 0 => pages::colors::ColorsMessage::SetPageLowColor(cp.color),
- 1 => pages::colors::ColorsMessage::SetHighColor(cp.color),
- 2 => pages::colors::ColorsMessage::SetVisualGuidesColor(cp.color),
- 3 => pages::colors::ColorsMessage::SetDisabledColor(cp.color),
- 4 => pages::colors::ColorsMessage::SetSeparatorColor(cp.color),
- 5 => pages::colors::ColorsMessage::SetSliderTrackColor(cp.color),
- 6 => pages::colors::ColorsMessage::SetColorBordersColor(cp.color),
- 7 => pages::colors::ColorsMessage::SetLowColor(cp.color),
- 8 => pages::colors::ColorsMessage::SetNormalColor(cp.color),
- 9 => pages::colors::ColorsMessage::SetPaginatorSidebarColor(cp.color),
- 10 => pages::colors::ColorsMessage::SetPrimaryHighlightColor(cp.color),
- 11 => pages::colors::ColorsMessage::SetPaginatorTabLabelColor(cp.color),
- _ => pages::colors::ColorsMessage::SetLowColor(cp.color),
+ actions.push(AppAction::Interface(match i {
+ 0 => pages::interface::InterfaceMessage::SetPageLowColor(cp.color),
+ 1 => pages::interface::InterfaceMessage::SetHighColor(cp.color),
+ 2 => pages::interface::InterfaceMessage::SetVisualGuidesColor(cp.color),
+ 3 => pages::interface::InterfaceMessage::SetDisabledColor(cp.color),
+ 4 => pages::interface::InterfaceMessage::SetSeparatorColor(cp.color),
+ 5 => pages::interface::InterfaceMessage::SetSliderTrackColor(cp.color),
+ 6 => pages::interface::InterfaceMessage::SetColorBordersColor(cp.color),
+ 7 => pages::interface::InterfaceMessage::SetLowColor(cp.color),
+ 8 => pages::interface::InterfaceMessage::SetNormalColor(cp.color),
+ 9 => pages::interface::InterfaceMessage::SetPaginatorSidebarColor(cp.color),
+ 10 => pages::interface::InterfaceMessage::SetPrimaryHighlightColor(cp.color),
+ 11 => pages::interface::InterfaceMessage::SetPaginatorTabLabelColor(cp.color),
+ 12 => pages::interface::InterfaceMessage::SetToggleEnabledColor(cp.color),
+ 13 => pages::interface::InterfaceMessage::SetToggleDisabledColor(cp.color),
+ _ => pages::interface::InterfaceMessage::SetLowColor(cp.color),
}));
}
changed = true;
@@ -2594,6 +2662,50 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
self.needs_rebuild = true;
return true;
}
+ let sb = &mut self.app.interface.tab_margin_spinbox_x;
+ let old = sb.value;
+ if sb.keyboard_input(event) {
+ let new_val = sb.value;
+ drop(sb);
+ if new_val != old {
+ self.handle_action(&AppAction::Interface(pages::interface::InterfaceMessage::SetTabMarginX(new_val as u16)));
+ }
+ self.needs_rebuild = true;
+ return true;
+ }
+ let sb = &mut self.app.interface.tab_margin_spinbox_y;
+ let old = sb.value;
+ if sb.keyboard_input(event) {
+ let new_val = sb.value;
+ drop(sb);
+ if new_val != old {
+ self.handle_action(&AppAction::Interface(pages::interface::InterfaceMessage::SetTabMarginY(new_val as u16)));
+ }
+ self.needs_rebuild = true;
+ return true;
+ }
+ let sb = &mut self.app.interface.tab_padding_spinbox_x;
+ let old = sb.value;
+ if sb.keyboard_input(event) {
+ let new_val = sb.value;
+ drop(sb);
+ if new_val != old {
+ self.handle_action(&AppAction::Interface(pages::interface::InterfaceMessage::SetTabPaddingX(new_val as u16)));
+ }
+ self.needs_rebuild = true;
+ return true;
+ }
+ let sb = &mut self.app.interface.tab_padding_spinbox_y;
+ let old = sb.value;
+ if sb.keyboard_input(event) {
+ let new_val = sb.value;
+ drop(sb);
+ if new_val != old {
+ self.handle_action(&AppAction::Interface(pages::interface::InterfaceMessage::SetTabPaddingY(new_val as u16)));
+ }
+ self.needs_rebuild = true;
+ return true;
+ }
}
if self.app.current_page == Page::Screensaver {
let sb = &mut self.app.screensaver.timeout_spinbox;
diff --git a/src/pages/accounts.rs b/src/pages/accounts.rs
index 3440982..1507aee 100644
--- a/src/pages/accounts.rs
+++ b/src/pages/accounts.rs
@@ -1,5 +1,5 @@
use crate::app::{AppAction, PageContent};
-use clear_ui::layout::{render_widget, Section};
+use clear_ui::layout::{Section, PageLayoutBuilder, LayoutStrategy};
use clear_ui::widget::{TextBox, Widget};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
@@ -303,298 +303,301 @@ pub async fn exchange_code_for_tokens(code: String, sender: calloop::channel::Se
const TEXT_DIM: [f32; 4] = [0.53, 0.53, 0.60, 1.0];
-pub fn view(state: &mut AccountsState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
- let mut pc = PageContent::new();
- let y = cy + 12.0;
-
- let mut sec = Section::new(&mut pc, cx, y, cw, "Online Accounts");
-
- if !state.loaded {
- sec.text(&mut pc, "Loading online accounts...", 12.0, 0.0, 12.0, TEXT_DIM);
- sec.spacing(18.0);
- } else {
- let usable_w = cw - 24.0;
- let gap = 24.0;
- let left_w = (usable_w - gap) * 0.40;
- let right_w = (usable_w - gap) * 0.60;
- let left_x = cx + 12.0;
- let right_x = left_x + left_w + gap;
-
- let mut left_y = sec.ay();
- let row_h = 28.0;
- let row_gap = 8.0;
-
- if state.accounts.is_empty() {
- pc.text("No accounts configured.", left_x + 8.0, left_y, 12.0, TEXT_DIM);
- left_y += 20.0;
+pub fn view(state: &mut AccountsState, cx: f32, cy: f32, cw: f32, ch: f32, layout: &mut dyn LayoutStrategy) -> PageContent {
+ let mut final_pc = PageContent::new();
+ let sec_w = 320.0f32;
+ let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(2);
+
+ // ── Accounts Section ──
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec_accounts = Section::new(pc, rx, ry, sec_w, "Accounts");
+ if !state.loaded {
+ sec_accounts.text(pc, "Loading online accounts...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec_accounts.spacing(18.0);
} else {
- for (idx, acc) in state.accounts.iter().enumerate() {
- let label = if acc.is_default {
- format!("{} [Default]", acc.email)
- } else {
- acc.email.clone()
- };
- let is_selected = state.selected_idx == Some(idx) && !state.adding_new && !state.editing_oauth_creds;
- let bg_col = if is_selected { [0.20, 0.40, 0.65, 0.4] } else { [0.10, 0.10, 0.16, 0.3] };
- pc.button(
- &label,
- left_x,
- left_y,
- left_w,
- row_h,
- bg_col,
- [0.20, 0.20, 0.25, 0.15],
- [0.90, 0.90, 0.95, 1.0],
- AppAction::Accounts(AccountsMessage::SelectAccount(idx)),
- );
- left_y += row_h + row_gap;
- }
- }
+ let row_h = 28.0;
+ let row_gap = 8.0;
+ let item_w = sec_w - 40.0;
- left_y += 12.0;
-
- let add_bg = if state.adding_new { [0.20, 0.40, 0.65, 0.4] } else { [0.13, 0.18, 0.14, 1.0] };
- pc.button(
- "Add Account",
- left_x,
- left_y,
- left_w,
- row_h,
- add_bg,
- [0.25, 0.30, 0.26, 1.0],
- [1.0, 1.0, 1.0, 1.0],
- AppAction::Accounts(AccountsMessage::AddAccountStart),
- );
- left_y += row_h + row_gap;
-
- let oauth_bg = if state.editing_oauth_creds { [0.20, 0.40, 0.65, 0.4] } else { [0.15, 0.15, 0.20, 1.0] };
- pc.button(
- "Google API Settings",
- left_x,
- left_y,
- left_w,
- row_h,
- oauth_bg,
- [0.25, 0.25, 0.30, 1.0],
- [1.0, 1.0, 1.0, 1.0],
- AppAction::Accounts(AccountsMessage::EditOAuthCredsStart),
- );
- left_y += row_h + row_gap;
-
- if let Some(selected_idx) = state.selected_idx {
- if selected_idx < state.accounts.len() && !state.adding_new && !state.editing_oauth_creds {
- let acc = &state.accounts[selected_idx];
- if !acc.is_default {
+ if state.accounts.is_empty() {
+ sec_accounts.text(pc, "No accounts configured.", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec_accounts.spacing(20.0);
+ } else {
+ for (idx, acc) in state.accounts.iter().enumerate() {
+ let label = if acc.is_default {
+ format!("{} [Default]", acc.email)
+ } else {
+ acc.email.clone()
+ };
+ let is_selected = state.selected_idx == Some(idx) && !state.adding_new && !state.editing_oauth_creds;
+ let bg_col = if is_selected { [0.20, 0.40, 0.65, 0.4] } else { [0.10, 0.10, 0.16, 0.3] };
pc.button(
- "Make Default",
- left_x,
- left_y,
- left_w,
+ &label,
+ sec_accounts.ax(12.0),
+ sec_accounts.ay(),
+ item_w,
row_h,
- [0.15, 0.15, 0.25, 1.0],
- [0.25, 0.25, 0.35, 1.0],
- [1.0, 1.0, 1.0, 1.0],
- AppAction::Accounts(AccountsMessage::MakeDefault(selected_idx)),
+ bg_col,
+ [0.20, 0.20, 0.25, 0.15],
+ [0.90, 0.90, 0.95, 1.0],
+ AppAction::Accounts(AccountsMessage::SelectAccount(idx)),
);
- left_y += row_h + row_gap;
+ sec_accounts.spacing(row_h + row_gap);
}
- pc.button(
- "Delete Account",
- left_x,
- left_y,
- left_w,
- row_h,
- [0.33, 0.20, 0.20, 1.0],
- [0.45, 0.25, 0.25, 1.0],
- [1.0, 0.33, 0.33, 1.0],
- AppAction::Accounts(AccountsMessage::DeleteAccount(selected_idx)),
- );
- left_y += row_h + row_gap;
}
- }
-
- let mut right_y = sec.ay();
-
- if state.adding_new {
- pc.text("Add New Account", right_x, right_y, 14.0, [0.35, 0.65, 0.90, 1.0]);
- right_y += 24.0;
-
- pc.text("Note: Gmail uses Google Login. iCloud requires App PW.", right_x, right_y, 11.0, TEXT_DIM);
- right_y += 18.0;
-
- let widget_h = 26.0;
- let field_gap = 14.0;
- // Email Address textbox
- let email_top = state.email_box.top_room();
- state.email_box.set_row_rect(right_x, right_w);
- clear_ui::layout::render_widget(&mut pc, &mut state.email_box, right_x, right_y + email_top, right_w, widget_h);
- right_y += widget_h + email_top + field_gap;
+ sec_accounts.spacing(12.0);
- // Password textbox
- let password_top = state.password_box.top_room();
- state.password_box.set_row_rect(right_x, right_w);
- clear_ui::layout::render_widget(&mut pc, &mut state.password_box, right_x, right_y + password_top, right_w, widget_h);
- right_y += widget_h + password_top + field_gap;
-
- // IMAP Server textbox
- let imap_top = state.imap_box.top_room();
- state.imap_box.set_row_rect(right_x, right_w);
- clear_ui::layout::render_widget(&mut pc, &mut state.imap_box, right_x, right_y + imap_top, right_w, widget_h);
- right_y += widget_h + imap_top + field_gap;
-
- // SMTP Server textbox
- let smtp_top = state.smtp_box.top_room();
- state.smtp_box.set_row_rect(right_x, right_w);
- clear_ui::layout::render_widget(&mut pc, &mut state.smtp_box, right_x, right_y + smtp_top, right_w, widget_h);
- right_y += widget_h + smtp_top + field_gap;
-
- let helper_w = (right_w - 8.0) / 2.0;
- pc.button(
- "Login (Google)",
- right_x,
- right_y,
- helper_w,
- row_h,
- [0.15, 0.15, 0.25, 1.0],
- [0.25, 0.25, 0.35, 1.0],
- [1.0, 1.0, 1.0, 1.0],
- AppAction::Accounts(AccountsMessage::GoogleLoginInit),
- );
- pc.button(
- "Login (iCloud)",
- right_x + helper_w + 8.0,
- right_y,
- helper_w,
- row_h,
- [0.15, 0.15, 0.25, 1.0],
- [0.25, 0.25, 0.35, 1.0],
- [1.0, 1.0, 1.0, 1.0],
- AppAction::Accounts(AccountsMessage::ICloudLoginHelp),
- );
- right_y += row_h + 16.0;
-
- pc.button(
- "Save Account",
- right_x,
- right_y,
- helper_w,
- row_h,
- [0.13, 0.18, 0.14, 1.0],
- [0.25, 0.30, 0.26, 1.0],
- [1.0, 1.0, 1.0, 1.0],
- AppAction::Accounts(AccountsMessage::AddAccountSave),
- );
- pc.button(
- "Cancel",
- right_x + helper_w + 8.0,
- right_y,
- helper_w,
- row_h,
- [0.33, 0.20, 0.20, 1.0],
- [0.45, 0.25, 0.25, 1.0],
- [1.0, 1.0, 1.0, 1.0],
- AppAction::Accounts(AccountsMessage::AddAccountCancel),
- );
- right_y += row_h + 12.0;
- } else if state.editing_oauth_creds {
- pc.text("Google OAuth Credentials", right_x, right_y, 14.0, [0.35, 0.65, 0.90, 1.0]);
- right_y += 24.0;
-
- pc.text("Configures client ID & secret from your Google Cloud Console.", right_x, right_y, 11.0, TEXT_DIM);
- right_y += 18.0;
- pc.text("Required: Gmail API enabled & redirect URI set to http://127.0.0.1:8080", right_x, right_y, 11.0, TEXT_DIM);
- right_y += 18.0;
-
- let widget_h = 26.0;
- let field_gap = 14.0;
-
- // Client ID textbox
- let client_id_top = state.oauth_client_id_box.top_room();
- state.oauth_client_id_box.set_row_rect(right_x, right_w);
- clear_ui::layout::render_widget(&mut pc, &mut state.oauth_client_id_box, right_x, right_y + client_id_top, right_w, widget_h);
- right_y += widget_h + client_id_top + field_gap;
-
- // Client Secret textbox
- let client_secret_top = state.oauth_client_secret_box.top_room();
- state.oauth_client_secret_box.set_row_rect(right_x, right_w);
- clear_ui::layout::render_widget(&mut pc, &mut state.oauth_client_secret_box, right_x, right_y + client_secret_top, right_w, widget_h);
- right_y += widget_h + client_secret_top + field_gap;
-
- let helper_w = (right_w - 8.0) / 2.0;
+ let add_bg = if state.adding_new { [0.20, 0.40, 0.65, 0.4] } else { [0.13, 0.18, 0.14, 1.0] };
pc.button(
- "Save Credentials",
- right_x,
- right_y,
- helper_w,
+ "Add Account",
+ sec_accounts.ax(12.0),
+ sec_accounts.ay(),
+ item_w,
row_h,
- [0.13, 0.18, 0.14, 1.0],
+ add_bg,
[0.25, 0.30, 0.26, 1.0],
[1.0, 1.0, 1.0, 1.0],
- AppAction::Accounts(AccountsMessage::EditOAuthCredsSave),
+ AppAction::Accounts(AccountsMessage::AddAccountStart),
);
+ sec_accounts.spacing(row_h + row_gap);
+
+ let oauth_bg = if state.editing_oauth_creds { [0.20, 0.40, 0.65, 0.4] } else { [0.15, 0.15, 0.20, 1.0] };
pc.button(
- "Cancel",
- right_x + helper_w + 8.0,
- right_y,
- helper_w,
+ "Google API Settings",
+ sec_accounts.ax(12.0),
+ sec_accounts.ay(),
+ item_w,
row_h,
- [0.33, 0.20, 0.20, 1.0],
- [0.45, 0.25, 0.25, 1.0],
+ oauth_bg,
+ [0.25, 0.25, 0.30, 1.0],
[1.0, 1.0, 1.0, 1.0],
- AppAction::Accounts(AccountsMessage::EditOAuthCredsCancel),
+ AppAction::Accounts(AccountsMessage::EditOAuthCredsStart),
);
- right_y += row_h + 12.0;
- } else if let Some(selected_idx) = state.selected_idx {
- if selected_idx < state.accounts.len() {
- let acc = &state.accounts[selected_idx];
-
- pc.text("Account Details", right_x, right_y, 14.0, [0.35, 0.65, 0.90, 1.0]);
- right_y += 28.0;
-
- pc.text(&format!("Email Address: {}", acc.email), right_x + 8.0, right_y, 12.0, [0.90, 0.90, 0.95, 1.0]);
- right_y += 18.0;
-
- let auth_type = if acc.is_oauth { "OAuth2 (Google)" } else { "Password-based" };
- pc.text(&format!("Authentication: {}", auth_type), right_x + 8.0, right_y, 12.0, [0.83, 0.83, 0.83, 1.0]);
- right_y += 18.0;
-
- pc.text(&format!("IMAP Server: {}", acc.imap), right_x + 8.0, right_y, 12.0, [0.83, 0.83, 0.83, 1.0]);
- right_y += 18.0;
-
- pc.text(&format!("SMTP Server: {}", acc.smtp), right_x + 8.0, right_y, 12.0, [0.83, 0.83, 0.83, 1.0]);
- right_y += 24.0;
-
- if acc.is_oauth {
+ sec_accounts.spacing(row_h + row_gap);
+
+ if let Some(selected_idx) = state.selected_idx {
+ if selected_idx < state.accounts.len() && !state.adding_new && !state.editing_oauth_creds {
+ let acc = &state.accounts[selected_idx];
+ if !acc.is_default {
+ pc.button(
+ "Make Default",
+ sec_accounts.ax(12.0),
+ sec_accounts.ay(),
+ item_w,
+ row_h,
+ [0.15, 0.15, 0.25, 1.0],
+ [0.25, 0.25, 0.35, 1.0],
+ [1.0, 1.0, 1.0, 1.0],
+ AppAction::Accounts(AccountsMessage::MakeDefault(selected_idx)),
+ );
+ sec_accounts.spacing(row_h + row_gap);
+ }
pc.button(
- "Click to Login (Browser)",
- right_x + 8.0,
- right_y,
- 200.0,
+ "Delete Account",
+ sec_accounts.ax(12.0),
+ sec_accounts.ay(),
+ item_w,
row_h,
- [0.15, 0.15, 0.25, 1.0],
- [0.25, 0.25, 0.35, 1.0],
- [1.0, 1.0, 1.0, 1.0],
- AppAction::Accounts(AccountsMessage::GoogleLoginInit),
+ [0.33, 0.20, 0.20, 1.0],
+ [0.45, 0.25, 0.25, 1.0],
+ [1.0, 0.33, 0.33, 1.0],
+ AppAction::Accounts(AccountsMessage::DeleteAccount(selected_idx)),
);
- right_y += row_h + 12.0;
+ sec_accounts.spacing(row_h + row_gap);
}
}
- } else {
- pc.text("Select an account to view details, or click Add Account.", right_x, right_y, 12.0, TEXT_DIM);
- right_y += 20.0;
}
+ sec_accounts.finish(pc)
+ });
- if let Some(ref msg) = state.status_msg {
- pc.text(msg, right_x, right_y + 12.0, 12.0, [0.56, 0.83, 0.56, 1.0]);
- right_y += 24.0;
- }
+ // ── Modify Accounts Section ──
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec_modify = Section::new(pc, rx, ry, sec_w, "Modify Accounts");
+ let item_w = sec_w - 40.0;
+ let row_h = 28.0;
- sec.content_y = left_y.max(right_y) + 24.0;
- }
+ if state.loaded {
+ if state.adding_new {
+ sec_modify.text(pc, "Add New Account", 12.0, 0.0, 14.0, [0.35, 0.65, 0.90, 1.0]);
+ sec_modify.spacing(24.0);
+
+ sec_modify.text(pc, "Note: Gmail uses Google Login. iCloud requires App PW.", 12.0, 0.0, 11.0, TEXT_DIM);
+ sec_modify.spacing(18.0);
+
+ let widget_h = 26.0;
+ let field_gap = 14.0;
+
+ // Email Address textbox
+ let email_top = state.email_box.top_room();
+ state.email_box.set_row_rect(rx + 12.0, item_w);
+ clear_ui::layout::render_widget(pc, &mut state.email_box, rx + 12.0, sec_modify.ay() + email_top, item_w, widget_h);
+ sec_modify.spacing(widget_h + email_top + field_gap);
+
+ // Password textbox
+ let password_top = state.password_box.top_room();
+ state.password_box.set_row_rect(rx + 12.0, item_w);
+ clear_ui::layout::render_widget(pc, &mut state.password_box, rx + 12.0, sec_modify.ay() + password_top, item_w, widget_h);
+ sec_modify.spacing(widget_h + password_top + field_gap);
+
+ // IMAP Server textbox
+ let imap_top = state.imap_box.top_room();
+ state.imap_box.set_row_rect(rx + 12.0, item_w);
+ clear_ui::layout::render_widget(pc, &mut state.imap_box, rx + 12.0, sec_modify.ay() + imap_top, item_w, widget_h);
+ sec_modify.spacing(widget_h + imap_top + field_gap);
+
+ // SMTP Server textbox
+ let smtp_top = state.smtp_box.top_room();
+ state.smtp_box.set_row_rect(rx + 12.0, item_w);
+ clear_ui::layout::render_widget(pc, &mut state.smtp_box, rx + 12.0, sec_modify.ay() + smtp_top, item_w, widget_h);
+ sec_modify.spacing(widget_h + smtp_top + field_gap);
+
+ let helper_w = (item_w - 8.0) / 2.0;
+ pc.button(
+ "Login (Google)",
+ sec_modify.ax(12.0),
+ sec_modify.ay(),
+ helper_w,
+ row_h,
+ [0.15, 0.15, 0.25, 1.0],
+ [0.25, 0.25, 0.35, 1.0],
+ [1.0, 1.0, 1.0, 1.0],
+ AppAction::Accounts(AccountsMessage::GoogleLoginInit),
+ );
+ pc.button(
+ "Login (iCloud)",
+ sec_modify.ax(12.0) + helper_w + 8.0,
+ sec_modify.ay(),
+ helper_w,
+ row_h,
+ [0.15, 0.15, 0.25, 1.0],
+ [0.25, 0.25, 0.35, 1.0],
+ [1.0, 1.0, 1.0, 1.0],
+ AppAction::Accounts(AccountsMessage::ICloudLoginHelp),
+ );
+ sec_modify.spacing(row_h + 16.0);
+
+ pc.button(
+ "Save Account",
+ sec_modify.ax(12.0),
+ sec_modify.ay(),
+ helper_w,
+ row_h,
+ [0.13, 0.18, 0.14, 1.0],
+ [0.25, 0.30, 0.26, 1.0],
+ [1.0, 1.0, 1.0, 1.0],
+ AppAction::Accounts(AccountsMessage::AddAccountSave),
+ );
+ pc.button(
+ "Cancel",
+ sec_modify.ax(12.0) + helper_w + 8.0,
+ sec_modify.ay(),
+ helper_w,
+ row_h,
+ [0.33, 0.20, 0.20, 1.0],
+ [0.45, 0.25, 0.25, 1.0],
+ [1.0, 1.0, 1.0, 1.0],
+ AppAction::Accounts(AccountsMessage::AddAccountCancel),
+ );
+ sec_modify.spacing(row_h + 12.0);
+ } else if state.editing_oauth_creds {
+ sec_modify.text(pc, "Google OAuth Credentials", 12.0, 0.0, 14.0, [0.35, 0.65, 0.90, 1.0]);
+ sec_modify.spacing(24.0);
+
+ sec_modify.text(pc, "Configures client ID & secret from your Google Cloud Console.", 12.0, 0.0, 11.0, TEXT_DIM);
+ sec_modify.spacing(18.0);
+ sec_modify.text(pc, "Required: Gmail API enabled & redirect URI set to http://127.0.0.1:8080", 12.0, 0.0, 11.0, TEXT_DIM);
+ sec_modify.spacing(18.0);
+
+ let widget_h = 26.0;
+ let field_gap = 14.0;
+
+ // Client ID textbox
+ let client_id_top = state.oauth_client_id_box.top_room();
+ state.oauth_client_id_box.set_row_rect(rx + 12.0, item_w);
+ clear_ui::layout::render_widget(pc, &mut state.oauth_client_id_box, rx + 12.0, sec_modify.ay() + client_id_top, item_w, widget_h);
+ sec_modify.spacing(widget_h + client_id_top + field_gap);
+
+ // Client Secret textbox
+ let client_secret_top = state.oauth_client_secret_box.top_room();
+ state.oauth_client_secret_box.set_row_rect(rx + 12.0, item_w);
+ clear_ui::layout::render_widget(pc, &mut state.oauth_client_secret_box, rx + 12.0, sec_modify.ay() + client_secret_top, item_w, widget_h);
+ sec_modify.spacing(widget_h + client_secret_top + field_gap);
+
+ let helper_w = (item_w - 8.0) / 2.0;
+ pc.button(
+ "Save Credentials",
+ sec_modify.ax(12.0),
+ sec_modify.ay(),
+ helper_w,
+ row_h,
+ [0.13, 0.18, 0.14, 1.0],
+ [0.25, 0.30, 0.26, 1.0],
+ [1.0, 1.0, 1.0, 1.0],
+ AppAction::Accounts(AccountsMessage::EditOAuthCredsSave),
+ );
+ pc.button(
+ "Cancel",
+ sec_modify.ax(12.0) + helper_w + 8.0,
+ sec_modify.ay(),
+ helper_w,
+ row_h,
+ [0.33, 0.20, 0.20, 1.0],
+ [0.45, 0.25, 0.25, 1.0],
+ [1.0, 1.0, 1.0, 1.0],
+ AppAction::Accounts(AccountsMessage::EditOAuthCredsCancel),
+ );
+ sec_modify.spacing(row_h + 12.0);
+ } else if let Some(selected_idx) = state.selected_idx {
+ if selected_idx < state.accounts.len() {
+ let acc = &state.accounts[selected_idx];
+
+ sec_modify.text(pc, "Account Details", 12.0, 0.0, 14.0, [0.35, 0.65, 0.90, 1.0]);
+ sec_modify.spacing(28.0);
+
+ sec_modify.text(pc, &format!("Email Address: {}", acc.email), 12.0, 0.0, 12.0, [0.90, 0.90, 0.95, 1.0]);
+ sec_modify.spacing(18.0);
+
+ let auth_type = if acc.is_oauth { "OAuth2 (Google)" } else { "Password-based" };
+ sec_modify.text(pc, &format!("Authentication: {}", auth_type), 12.0, 0.0, 12.0, [0.83, 0.83, 0.83, 1.0]);
+ sec_modify.spacing(18.0);
+
+ sec_modify.text(pc, &format!("IMAP Server: {}", acc.imap), 12.0, 0.0, 12.0, [0.83, 0.83, 0.83, 1.0]);
+ sec_modify.spacing(18.0);
+
+ sec_modify.text(pc, &format!("SMTP Server: {}", acc.smtp), 12.0, 0.0, 12.0, [0.83, 0.83, 0.83, 1.0]);
+ sec_modify.spacing(24.0);
+
+ if acc.is_oauth {
+ pc.button(
+ "Click to Login (Browser)",
+ sec_modify.ax(12.0),
+ sec_modify.ay(),
+ 200.0,
+ row_h,
+ [0.15, 0.15, 0.25, 1.0],
+ [0.25, 0.25, 0.35, 1.0],
+ [1.0, 1.0, 1.0, 1.0],
+ AppAction::Accounts(AccountsMessage::GoogleLoginInit),
+ );
+ sec_modify.spacing(row_h + 12.0);
+ }
+ }
+ } else {
+ sec_modify.text(pc, "Select an account to view details, or click Add Account.", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec_modify.spacing(20.0);
+ }
+
+ if let Some(ref msg) = state.status_msg {
+ sec_modify.spacing(12.0);
+ sec_modify.text(pc, msg, 12.0, 0.0, 12.0, [0.56, 0.83, 0.56, 1.0]);
+ sec_modify.spacing(24.0);
+ }
+ }
+ sec_modify.finish(pc)
+ });
- sec.finish(&mut pc);
- pc
+ final_pc
}
pub fn update(state: &mut AccountsState, msg: AccountsMessage) {
diff --git a/src/pages/audio.rs b/src/pages/audio.rs
index c53dde2..9a2ff9f 100644
--- a/src/pages/audio.rs
+++ b/src/pages/audio.rs
@@ -1,5 +1,5 @@
use crate::app::{AppAction, PageContent};
-use clear_ui::layout::{render_widget, Section};
+use clear_ui::layout::{render_widget, Section, PageLayoutBuilder, LayoutStrategy, GridLayout};
use clear_ui::widget::{Spinbox, Widget};
#[derive(Debug, Clone)]
@@ -207,127 +207,133 @@ 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, sec_focused: &[bool]) -> PageContent {
- let mut pc = PageContent::new();
- let mut y = cy + 12.0;
+pub fn view(state: &mut AudioState, cx: f32, cy: f32, cw: f32, ch: f32, sec_focused: &[bool], layout: &mut dyn LayoutStrategy) -> PageContent {
+ let mut final_pc = PageContent::new();
+ let sec_w = 320.0f32;
+ let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(2);
// ── Output section ──
- let mut sec = Section::new(&mut pc, cx, y, cw, "Output");
-
- if !state.loaded {
- sec.text(&mut pc, "Loading output devices...", 12.0, 0.0, 12.0, TEXT_DIM);
- sec.spacing(18.0);
- } else if state.sinks.is_empty() {
- sec.text(&mut pc, "No output devices found", 12.0, 0.0, 12.0, TEXT_DIM);
- sec.spacing(18.0);
- }
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec = Section::new(pc, rx, ry, sec_w, "Output");
- if state.loaded {
- for (idx, sink) in state.sinks.iter().enumerate() {
- let label = if !sink.active {
- format!("{} (inactive)", sink.name)
- } else if sink.muted {
- format!("{} {:.0}% (muted)", sink.name, sink.volume * 100.0)
- } else {
- format!("{} {:.0}%", sink.name, sink.volume * 100.0)
- };
- let lc = if sink.muted { RED } else { TEXT_FG };
- sec.text(&mut pc, &label, 14.0, 0.0, 13.0, lc);
+ if !state.loaded {
+ sec.text(pc, "Loading output devices...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(18.0);
+ } else if state.sinks.is_empty() {
+ sec.text(pc, "No output devices found", 12.0, 0.0, 12.0, TEXT_DIM);
sec.spacing(18.0);
+ }
- if sink.active {
- let bar_w = cw - 100.0;
- let bar_x = 14.0;
- let yt = sec.ay();
- pc.rect(BLANK_BAR, sec.ax(bar_x), yt, bar_w, 8.0);
- pc.rect(FILL_BAR, sec.ax(bar_x), yt, bar_w * sink.volume, 8.0);
- sec.text(&mut pc, &format!("{:.0}%", sink.volume * 100.0), bar_x + bar_w + 8.0, -2.0, 11.0, TEXT_DIM);
-
- let row_y = sec.ay() + 12.0;
- let sb_w = 100.0;
- let sb_h = 26.0;
- let mute_w = 60.0;
- let gap = 8.0;
-
- state.sink_spinboxes[idx].value = (sink.volume * 100.0).round() as i32;
- state.sink_spinboxes[idx].set_row_rect(sec.ax(8.0), cw - 16.0);
- render_widget(&mut pc, &mut state.sink_spinboxes[idx], sec.ax(bar_x), row_y, sb_w, sb_h);
-
- let mute_label = if sink.muted { "Unmute" } else { "Mute" };
- let mute_col = if sink.muted { MUTED_BG } else { BTN_INACTIVE };
- pc.button(mute_label, sec.ax(bar_x) + sb_w + gap, row_y, mute_w, sb_h,
- mute_col, BTN_HOVER, WHITE,
- AppAction::Audio(AudioMessage::SinkMute(sink.id)));
-
- sec.content_y += 12.0 + sb_h + 6.0;
- } else {
- sec.content_y += 6.0;
+ if state.loaded {
+ for (idx, sink) in state.sinks.iter().enumerate() {
+ let label = if !sink.active {
+ format!("{} (inactive)", sink.name)
+ } else if sink.muted {
+ format!("{} {:.0}% (muted)", sink.name, sink.volume * 100.0)
+ } else {
+ format!("{} {:.0}%", sink.name, sink.volume * 100.0)
+ };
+ let lc = if sink.muted { RED } else { TEXT_FG };
+ sec.text(pc, &label, 14.0, 0.0, 13.0, lc);
+ sec.spacing(18.0);
+
+ if sink.active {
+ let bar_w = sec_w - 100.0;
+ let bar_x = 14.0;
+ let yt = sec.ay();
+ pc.rect(BLANK_BAR, sec.ax(bar_x), yt, bar_w, 8.0);
+ pc.rect(FILL_BAR, sec.ax(bar_x), yt, bar_w * sink.volume, 8.0);
+ sec.text(pc, &format!("{:.0}%", sink.volume * 100.0), bar_x + bar_w + 8.0, -2.0, 11.0, TEXT_DIM);
+
+ let row_y = sec.ay() + 12.0;
+ let sb_w = 100.0;
+ let sb_h = 26.0;
+ let mute_w = 60.0;
+ let gap = 8.0;
+
+ state.sink_spinboxes[idx].value = (sink.volume * 100.0).round() as i32;
+ state.sink_spinboxes[idx].set_row_rect(sec.ax(8.0), sec_w - 16.0);
+ render_widget(pc, &mut state.sink_spinboxes[idx], sec.ax(bar_x), row_y, sb_w, sb_h);
+
+ let mute_label = if sink.muted { "Unmute" } else { "Mute" };
+ let mute_col = if sink.muted { MUTED_BG } else { BTN_INACTIVE };
+ pc.button(mute_label, sec.ax(bar_x) + sb_w + gap, row_y, mute_w, sb_h,
+ mute_col, BTN_HOVER, WHITE,
+ AppAction::Audio(AudioMessage::SinkMute(sink.id)));
+
+ sec.content_y += 12.0 + sb_h + 6.0;
+ } else {
+ sec.content_y += 6.0;
+ }
}
}
- }
- y = sec.finish_focused(&mut pc, sec_focused.get(0).copied().unwrap_or(false));
+ sec.finish_focused(pc, sec_focused.first().copied().unwrap_or(false))
+ });
// ── Input section ──
- let mut sec = Section::new(&mut pc, cx, y, cw, "Input");
-
- if !state.loaded {
- sec.text(&mut pc, "Loading input devices...", 12.0, 0.0, 12.0, TEXT_DIM);
- sec.spacing(18.0);
- } else if state.sources.is_empty() {
- sec.text(&mut pc, "No input devices found", 12.0, 0.0, 12.0, TEXT_DIM);
- sec.spacing(18.0);
- }
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec = Section::new(pc, rx, ry, sec_w, "Input");
- if state.loaded {
- for (idx, src) in state.sources.iter().enumerate() {
- let label = if !src.active {
- format!("{} (inactive)", src.name)
- } else if src.muted {
- format!("{} {:.0}% (muted)", src.name, src.volume * 100.0)
- } else {
- format!("{} {:.0}%", src.name, src.volume * 100.0)
- };
- let lc = if src.muted { RED } else { TEXT_FG };
- sec.text(&mut pc, &label, 14.0, 0.0, 13.0, lc);
+ if !state.loaded {
+ sec.text(pc, "Loading input devices...", 12.0, 0.0, 12.0, TEXT_DIM);
sec.spacing(18.0);
+ } else if state.sources.is_empty() {
+ sec.text(pc, "No input devices found", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(18.0);
+ }
- if src.active {
- let bar_w = cw - 100.0;
- let bar_x = 14.0;
- let yt = sec.ay();
- pc.rect(BLANK_BAR, sec.ax(bar_x), yt, bar_w, 8.0);
- pc.rect(FILL_BAR, sec.ax(bar_x), yt, bar_w * src.volume, 8.0);
- sec.text(&mut pc, &format!("{:.0}%", src.volume * 100.0), bar_x + bar_w + 8.0, -2.0, 11.0, TEXT_DIM);
-
- let row_y = sec.ay() + 12.0;
- let sb_w = 100.0;
- let sb_h = 26.0;
- let mute_w = 60.0;
- let gap = 8.0;
-
- state.source_spinboxes[idx].value = (src.volume * 100.0).round() as i32;
- state.source_spinboxes[idx].set_row_rect(sec.ax(8.0), cw - 16.0);
- render_widget(&mut pc, &mut state.source_spinboxes[idx], sec.ax(bar_x), row_y, sb_w, sb_h);
-
- let mute_label = if src.muted { "Unmute" } else { "Mute" };
- let mute_col = if src.muted { MUTED_BG } else { BTN_INACTIVE };
- pc.button(mute_label, sec.ax(bar_x) + sb_w + gap, row_y, mute_w, sb_h,
- mute_col, BTN_HOVER, WHITE,
- AppAction::Audio(AudioMessage::SourceMute(src.id)));
-
- sec.content_y += 12.0 + sb_h + 6.0;
- } else {
- sec.content_y += 6.0;
+ if state.loaded {
+ for (idx, src) in state.sources.iter().enumerate() {
+ let label = if !src.active {
+ format!("{} (inactive)", src.name)
+ } else if src.muted {
+ format!("{} {:.0}% (muted)", src.name, src.volume * 100.0)
+ } else {
+ format!("{} {:.0}%", src.name, src.volume * 100.0)
+ };
+ let lc = if src.muted { RED } else { TEXT_FG };
+ sec.text(pc, &label, 14.0, 0.0, 13.0, lc);
+ sec.spacing(18.0);
+
+ if src.active {
+ let bar_w = sec_w - 100.0;
+ let bar_x = 14.0;
+ let yt = sec.ay();
+ pc.rect(BLANK_BAR, sec.ax(bar_x), yt, bar_w, 8.0);
+ pc.rect(FILL_BAR, sec.ax(bar_x), yt, bar_w * src.volume, 8.0);
+ sec.text(pc, &format!("{:.0}%", src.volume * 100.0), bar_x + bar_w + 8.0, -2.0, 11.0, TEXT_DIM);
+
+ let row_y = sec.ay() + 12.0;
+ let sb_w = 100.0;
+ let sb_h = 26.0;
+ let mute_w = 60.0;
+ let gap = 8.0;
+
+ state.source_spinboxes[idx].value = (src.volume * 100.0).round() as i32;
+ state.source_spinboxes[idx].set_row_rect(sec.ax(8.0), sec_w - 16.0);
+ render_widget(pc, &mut state.source_spinboxes[idx], sec.ax(bar_x), row_y, sb_w, sb_h);
+
+ let mute_label = if src.muted { "Unmute" } else { "Mute" };
+ let mute_col = if src.muted { MUTED_BG } else { BTN_INACTIVE };
+ pc.button(mute_label, sec.ax(bar_x) + sb_w + gap, row_y, mute_w, sb_h,
+ mute_col, BTN_HOVER, WHITE,
+ AppAction::Audio(AudioMessage::SourceMute(src.id)));
+
+ sec.content_y += 12.0 + sb_h + 6.0;
+ } else {
+ sec.content_y += 6.0;
+ }
}
}
- }
- sec.finish_focused(&mut pc, sec_focused.get(1).copied().unwrap_or(false));
+ sec.finish_focused(pc, sec_focused.get(1).copied().unwrap_or(false))
+ });
- pc
+ final_pc
}
+
pub fn update(state: &mut AudioState, msg: AudioMessage) {
match msg {
AudioMessage::Refreshed(new) => {
@@ -361,3 +367,17 @@ pub fn update(state: &mut AudioState, msg: AudioMessage) {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_view_layout_grid() {
+ let mut state = AudioState::default();
+ let mut layout = GridLayout::new(320.0, 20.0);
+ let pc = view(&mut state, 10.0, 20.0, 800.0, 600.0, &[false, false], &mut layout);
+ assert!(!pc.rects.is_empty() || !pc.texts.is_empty());
+ }
+}
+
diff --git a/src/pages/backup.rs b/src/pages/backup.rs
index 1664c71..04a21d4 100644
--- a/src/pages/backup.rs
+++ b/src/pages/backup.rs
@@ -1,5 +1,5 @@
use crate::app::{AppAction, PageContent};
-use clear_ui::layout::Section;
+use clear_ui::layout::{Section, PageLayoutBuilder, LayoutStrategy};
use std::fs;
#[derive(Debug, Clone, Default)]
@@ -96,69 +96,72 @@ const BTN_HOVER: [f32; 4] = [0.28, 0.50, 0.78, 1.0];
const BTN_DISABLED: [f32; 4] = [0.15, 0.18, 0.22, 1.0];
const WHITE: [f32; 4] = [1.0, 1.0, 1.0, 1.0];
-pub fn view(state: &BackupState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
- let mut pc = PageContent::new();
- let y = cy + 12.0;
-
- let mut sec = Section::new(&mut pc, cx, y, cw, "Full System Backup");
-
- if !state.loaded {
- sec.text(&mut pc, "Loading backup state...", 12.0, 0.0, 12.0, TEXT_DIM);
- sec.spacing(18.0);
- } else {
- // Status Row
- sec.text(&mut pc, "Backup Status", 12.0, 0.0, 12.0, LABEL_FG);
- let status_text = if state.in_progress { "Backing up..." } else { "Idle" };
- let status_color = if state.in_progress { GREEN } else { TEXT_FG };
- sec.text(&mut pc, status_text, 120.0, 0.0, 12.0, status_color);
- sec.spacing(18.0);
-
- // Last Backup Row
- sec.text(&mut pc, "Last Backup", 12.0, 0.0, 12.0, LABEL_FG);
- sec.text(&mut pc, &state.last_backup_time, 120.0, 0.0, 12.0, TEXT_FG);
- sec.spacing(18.0);
-
- // Backup Size Row
- sec.text(&mut pc, "Archive Size", 12.0, 0.0, 12.0, LABEL_FG);
- sec.text(&mut pc, &state.backup_size, 120.0, 0.0, 12.0, TEXT_FG);
- sec.spacing(18.0);
-
- // Target Directories Row
- sec.text(&mut pc, "Backup Targets", 12.0, 0.0, 12.0, LABEL_FG);
- sec.text(&mut pc, "Entire Filesystem (/) [Preserving attributes]", 120.0, 0.0, 12.0, TEXT_DIM);
- sec.spacing(18.0);
-
- // Destination Archive Row
- sec.text(&mut pc, "Destination", 12.0, 0.0, 12.0, LABEL_FG);
- sec.text(&mut pc, "USB Drive (/mnt/usb or /run/media/...)", 120.0, 0.0, 12.0, TEXT_DIM);
- sec.spacing(24.0);
-
- // Error message if present
- if let Some(ref err) = state.error_message {
- sec.text(&mut pc, "Error:", 12.0, 0.0, 12.0, RED);
- sec.text(&mut pc, err, 60.0, 0.0, 11.0, RED);
- sec.spacing(18.0);
- }
+pub fn view(state: &BackupState, cx: f32, cy: f32, cw: f32, ch: f32, layout: &mut dyn LayoutStrategy) -> PageContent {
+ let mut final_pc = PageContent::new();
+ let sec_w = 320.0f32;
+ let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(1);
- // Action Button
- let btn_w = 120.0;
- let btn_h = 32.0;
- let yt = sec.ay();
-
- let (btn_label, bg, hover, action) = if state.in_progress {
- ("Backing up...", BTN_DISABLED, BTN_DISABLED, AppAction::Backup(BackupMessage::StartBackup))
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec = Section::new(pc, rx, ry, sec_w, "Full System Backup");
+
+ if !state.loaded {
+ sec.text(pc, "Loading backup state...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(18.0);
} else {
- ("Run Backup", BTN_BG, BTN_HOVER, AppAction::Backup(BackupMessage::StartBackup))
- };
-
- sec.row(1, 0.0, btn_h, |_, x, _| {
- pc.button(btn_label, x, yt, btn_w, btn_h, bg, hover, WHITE, action.clone());
- });
- sec.spacing(12.0);
- }
- sec.finish(&mut pc);
+ // Status Row
+ sec.text(pc, "Backup Status", 12.0, 0.0, 12.0, LABEL_FG);
+ let status_text = if state.in_progress { "Backing up..." } else { "Idle" };
+ let status_color = if state.in_progress { GREEN } else { TEXT_FG };
+ sec.text(pc, status_text, 120.0, 0.0, 12.0, status_color);
+ sec.spacing(18.0);
+
+ // Last Backup Row
+ sec.text(pc, "Last Backup", 12.0, 0.0, 12.0, LABEL_FG);
+ sec.text(pc, &state.last_backup_time, 120.0, 0.0, 12.0, TEXT_FG);
+ sec.spacing(18.0);
+
+ // Backup Size Row
+ sec.text(pc, "Archive Size", 12.0, 0.0, 12.0, LABEL_FG);
+ sec.text(pc, &state.backup_size, 120.0, 0.0, 12.0, TEXT_FG);
+ sec.spacing(18.0);
+
+ // Target Directories Row
+ sec.text(pc, "Backup Targets", 12.0, 0.0, 12.0, LABEL_FG);
+ sec.text(pc, "Entire Filesystem (/) [Preserving attributes]", 120.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(18.0);
+
+ // Destination Archive Row
+ sec.text(pc, "Destination", 12.0, 0.0, 12.0, LABEL_FG);
+ sec.text(pc, "USB Drive (/mnt/usb or /run/media/...)", 120.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(24.0);
+
+ // Error message if present
+ if let Some(ref err) = state.error_message {
+ sec.text(pc, "Error:", 12.0, 0.0, 12.0, RED);
+ sec.text(pc, err, 60.0, 0.0, 11.0, RED);
+ sec.spacing(18.0);
+ }
+
+ // Action Button
+ let btn_w = 120.0;
+ let btn_h = 32.0;
+ let yt = sec.ay();
+
+ let (btn_label, bg, hover, action) = if state.in_progress {
+ ("Backing up...", BTN_DISABLED, BTN_DISABLED, AppAction::Backup(BackupMessage::StartBackup))
+ } else {
+ ("Run Backup", BTN_BG, BTN_HOVER, AppAction::Backup(BackupMessage::StartBackup))
+ };
+
+ sec.row(1, 0.0, btn_h, |_, x, _| {
+ pc.button(btn_label, x, yt, btn_w, btn_h, bg, hover, WHITE, action.clone());
+ });
+ sec.spacing(12.0);
+ }
+ sec.finish(pc)
+ });
- pc
+ final_pc
}
pub fn update(state: &mut BackupState, msg: BackupMessage) {
diff --git a/src/pages/display.rs b/src/pages/display.rs
index 804c258..eca7ba9 100644
--- a/src/pages/display.rs
+++ b/src/pages/display.rs
@@ -1,5 +1,5 @@
use crate::app::PageContent;
-use clear_ui::layout::{render_widget, Section};
+use clear_ui::layout::{render_widget, Section, PageLayoutBuilder, LayoutStrategy, Subsection};
use clear_ui::widget::{Spinbox, Label, Widget};
#[derive(Debug, Clone)]
@@ -171,81 +171,90 @@ const TEXT_DIM: [f32; 4] = [0.53, 0.53, 0.60, 1.0];
const BLANK_BAR: [f32; 4] = [0.15, 0.15, 0.24, 1.0];
const FILL_BAR: [f32; 4] = [0.30, 0.50, 0.32, 1.0];
-pub fn view(state: &mut DisplayState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
- let mut pc = PageContent::new();
- let mut y = cy + 12.0;
+pub fn view(state: &mut DisplayState, cx: f32, cy: f32, cw: f32, ch: f32, layout: &mut dyn LayoutStrategy) -> PageContent {
+ let mut final_pc = PageContent::new();
+ let sec_w = 320.0f32;
+ let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(3);
// ── Brightness ──
- let mut sec = Section::new(&mut pc, cx, y, cw, "Brightness");
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec = Section::new(pc, rx, ry, sec_w, "Brightness");
- if !state.loaded {
- sec.text(&mut pc, "Loading display settings...", 12.0, 0.0, 12.0, TEXT_DIM);
- sec.spacing(18.0);
- } else {
- let bright_pct = if state.max_brightness > 0.0 {
- (state.brightness / state.max_brightness * 100.0).round() as i32
- } else { 0 };
+ if !state.loaded {
+ sec.text(pc, "Loading display settings...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(18.0);
+ } else {
+ let bright_pct = if state.max_brightness > 0.0 {
+ (state.brightness / state.max_brightness * 100.0).round() as i32
+ } else { 0 };
- let bar_w = cw - 100.0;
- let yt = sec.ay();
- pc.rect(BLANK_BAR, sec.ax(12.0), yt, bar_w, 8.0);
- pc.rect(FILL_BAR, sec.ax(12.0), yt, bar_w * bright_pct as f32 / 100.0, 8.0);
- pc.text(&format!("{}%", bright_pct), sec.ax(16.0 + bar_w), yt - 2.0, 11.0, TEXT_DIM);
- sec.content_y += 14.0;
+ let bar_w = sec_w - 100.0;
+ let yt = sec.ay();
+ pc.rect(BLANK_BAR, sec.ax(12.0), yt, bar_w, 8.0);
+ pc.rect(FILL_BAR, sec.ax(12.0), yt, bar_w * bright_pct as f32 / 100.0, 8.0);
+ pc.text(&format!("{}%", bright_pct), sec.ax(16.0 + bar_w), yt - 2.0, 11.0, TEXT_DIM);
+ sec.content_y += 14.0;
- let yt = sec.ay();
- let sb_w = 100.0;
- let sb_h = 26.0;
- state.brightness_spinbox.value = bright_pct;
- state.brightness_spinbox.set_row_rect(sec.ax(8.0), cw - 16.0);
- render_widget(&mut pc, &mut state.brightness_spinbox, sec.ax(12.0), yt, sb_w, sb_h);
- sec.content_y += sb_h + 12.0;
- }
- y = sec.finish(&mut pc);
+ let yt = sec.ay();
+ let sb_w = 100.0;
+ let sb_h = 26.0;
+ state.brightness_spinbox.value = bright_pct;
+ state.brightness_spinbox.set_row_rect(sec.ax(8.0), sec_w - 16.0);
+ render_widget(pc, &mut state.brightness_spinbox, sec.ax(12.0), yt, sb_w, sb_h);
+ sec.content_y += sb_h + 12.0;
+ }
+ sec.finish(pc)
+ });
// ── Night Light ──
- let mut sec = Section::new(&mut pc, cx, y, cw, "Night Light");
- if !state.loaded {
- sec.text(&mut pc, "Loading...", 12.0, 0.0, 12.0, TEXT_DIM);
- sec.spacing(18.0);
- } else {
- let nl_label = if state.night_light { "Night Light: ON" } else { "Night Light: OFF" };
- state.night_light_label.set_text(nl_label);
- sec.widget(&mut pc, &mut state.night_light_label, 12.0, cw - 24.0, 20.0);
- sec.spacing(8.0);
- }
- y = sec.finish(&mut pc);
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec = Section::new(pc, rx, ry, sec_w, "Night Light");
+ if !state.loaded {
+ sec.text(pc, "Loading...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(18.0);
+ } else {
+ let nl_label = if state.night_light { "Night Light: ON" } else { "Night Light: OFF" };
+ state.night_light_label.set_text(nl_label);
+ sec.widget(pc, &mut state.night_light_label, 12.0, sec_w - 24.0, 20.0);
+ sec.spacing(8.0);
+ }
+ sec.finish(pc)
+ });
// ── Outputs ──
- let mut sec = Section::new(&mut pc, cx, y, cw, "Outputs");
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec = Section::new(pc, rx, ry, sec_w, "Outputs");
- if !state.loaded {
- sec.text(&mut pc, "Loading outputs...", 12.0, 0.0, 12.0, TEXT_DIM);
- sec.spacing(18.0);
- } else {
- for out in &mut state.outputs {
- let yt = sec.ay();
-
- // Name label at x = 14.0
- render_widget(&mut pc, &mut out.name_label, sec.ax(14.0), yt, 100.0, 20.0);
-
- // Resolution label at x = 120.0
- render_widget(&mut pc, &mut out.resolution_label, sec.ax(120.0), yt, 160.0, 20.0);
-
- // Scale label at x = 290.0 if present
- if let Some(ref mut scale_lbl) = out.scale_label {
- render_widget(&mut pc, scale_lbl, sec.ax(290.0), yt, cw - 304.0, 20.0);
+ if !state.loaded {
+ sec.text(pc, "Loading outputs...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(18.0);
+ } else {
+ for out in &mut state.outputs {
+ let mut subsec = Subsection::new(
+ pc,
+ sec.ax(0.0) + Section::ROW_PADDING_X,
+ sec.content_y,
+ sec.cw - 2.0 * Section::ROW_PADDING_X,
+ &out.name,
+ );
+
+ subsec.widget(pc, &mut out.resolution_label, 12.0, 240.0, 20.0);
+
+ if let Some(ref mut scale_lbl) = out.scale_label {
+ subsec.widget(pc, scale_lbl, 12.0, 240.0, 20.0);
+ }
+
+ let sub_h = subsec.finish(pc);
+ sec.content_y = sub_h;
}
-
- sec.content_y += 20.0;
- sec.spacing(12.0);
}
- }
- sec.finish(&mut pc);
+ sec.finish(pc)
+ });
- pc
+ final_pc
}
+
pub fn update(state: &mut DisplayState, msg: DisplayMessage) {
match msg {
DisplayMessage::Refreshed(new) => {
@@ -281,3 +290,17 @@ pub fn update(state: &mut DisplayState, msg: DisplayMessage) {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_view_layout_grid() {
+ let mut state = DisplayState::default();
+ let mut layout = clear_ui::layout::GridLayout::new(320.0, 20.0);
+ let pc = view(&mut state, 10.0, 20.0, 800.0, 600.0, &mut layout);
+ assert!(!pc.rects.is_empty() || !pc.texts.is_empty());
+ }
+}
+
diff --git a/src/pages/hardware.rs b/src/pages/hardware.rs
index 53d73d6..26013c5 100644
--- a/src/pages/hardware.rs
+++ b/src/pages/hardware.rs
@@ -1,5 +1,5 @@
use crate::app::{AppAction, PageContent};
-use clear_ui::layout::Section;
+use clear_ui::layout::{Section, PageLayoutBuilder, LayoutStrategy};
use clear_ui::widget::{Label, ScrollingList};
#[derive(Debug, Clone, Default)]
@@ -352,209 +352,220 @@ const WHITE: [f32; 4] = [1.0, 1.0, 1.0, 1.0];
const RED: [f32; 4] = [1.0, 0.33, 0.33, 1.0];
const ORANGE: [f32; 4] = [1.0, 0.73, 0.20, 1.0];
-pub fn view(state: &mut HardwareState, cx: f32, cy: f32, cw: f32, _ch: f32, root_focused: bool) -> PageContent {
- let mut pc = PageContent::new();
- let mut y = cy + 12.0;
+pub fn view(state: &mut HardwareState, cx: f32, cy: f32, cw: f32, ch: f32, root_focused: bool, layout: &mut dyn LayoutStrategy) -> PageContent {
+ let mut final_pc = PageContent::new();
+ let sec_w = 320.0f32;
+ let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(5);
// ── CPU Section ──
- let mut sec = Section::new(&mut pc, cx, y, cw, "CPU");
- if !state.loaded {
- sec.text(&mut pc, "Loading CPU model and utilization...", 12.0, 0.0, 12.0, TEXT_FG);
- sec.spacing(10.0);
- } else {
- // CPU Info Label
- sec.widget(&mut pc, &mut state.cpu_label, 12.0, cw - 24.0, 26.0);
- sec.spacing(12.0);
-
- // Scrolling box configuration for process list
- let list_box_x = cx + 12.0;
- let list_box_y = sec.ay();
- let list_box_w = cw - 24.0;
- let list_box_h = 220.0;
-
- // Render the standardized ScrollBox widget
- clear_ui::layout::render_widget(&mut pc, &mut state.cpu_list_box, list_box_x, list_box_y, list_box_w, list_box_h);
-
- // Header for process list columns (drawn static on top of the ScrollBox background)
- let header_h = 22.0;
- pc.rect([0.12, 0.12, 0.16, 0.5], list_box_x + 1.0, list_box_y + 1.0, list_box_w - 2.0, header_h);
- pc.rect([0.18, 0.18, 0.24, 1.0], list_box_x + 1.0, list_box_y + header_h, list_box_w - 2.0, 1.0); // Divider
-
- pc.text("PID", list_box_x + 12.0, list_box_y + 5.0, 11.0, [0.53, 0.53, 0.60, 1.0]);
- pc.text("COMMAND", list_box_x + 80.0, list_box_y + 5.0, 11.0, [0.53, 0.53, 0.60, 1.0]);
- pc.text("CPU %", list_box_x + list_box_w - 60.0, list_box_y + 5.0, 11.0, [0.53, 0.53, 0.60, 1.0]);
-
- let row_h = 24.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() {
- 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(
- "",
- list_box_x + 2.0,
- draw_y,
- list_box_w - 16.0,
- row_h,
- [0.0, 0.0, 0.0, 0.0],
- [1.0, 1.0, 1.0, 0.06],
- [0.0, 0.0, 0.0, 0.0],
- AppAction::Hardware(HardwareMessage::None),
- );
-
- pc.text(pid, list_box_x + 12.0, draw_y + 6.0, 12.0, [0.80, 0.80, 0.85, 1.0]);
- pc.text(comm, list_box_x + 80.0, draw_y + 6.0, 12.0, [0.80, 0.80, 0.85, 1.0]);
- pc.text(&format!("{}%", cpu), list_box_x + list_box_w - 60.0, draw_y + 6.0, 12.0, [0.56, 0.83, 0.56, 1.0]);
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec = Section::new(pc, rx, ry, sec_w, "CPU");
+ if !state.loaded {
+ sec.text(pc, "Loading CPU model and utilization...", 12.0, 0.0, 12.0, TEXT_FG);
+ sec.spacing(10.0);
+ } else {
+ // CPU Info Label
+ sec.widget(pc, &mut state.cpu_label, 12.0, sec_w - 24.0, 26.0);
+ sec.spacing(12.0);
+
+ // Scrolling box configuration for process list
+ let list_box_x = rx + 12.0;
+ let list_box_y = sec.ay();
+ let list_box_w = sec_w - 24.0;
+ let list_box_h = 220.0;
+
+ // Render the standardized ScrollBox widget
+ clear_ui::layout::render_widget(pc, &mut state.cpu_list_box, list_box_x, list_box_y, list_box_w, list_box_h);
+
+ // Header for process list columns (drawn static on top of the ScrollBox background)
+ let header_h = 22.0;
+ pc.rect([0.12, 0.12, 0.16, 0.5], list_box_x + 1.0, list_box_y + 1.0, list_box_w - 2.0, header_h);
+ pc.rect([0.18, 0.18, 0.24, 1.0], list_box_x + 1.0, list_box_y + header_h, list_box_w - 2.0, 1.0); // Divider
+
+ pc.text("PID", list_box_x + 12.0, list_box_y + 5.0, 11.0, [0.53, 0.53, 0.60, 1.0]);
+ pc.text("COMMAND", list_box_x + 80.0, list_box_y + 5.0, 11.0, [0.53, 0.53, 0.60, 1.0]);
+ pc.text("CPU %", list_box_x + list_box_w - 60.0, list_box_y + 5.0, 11.0, [0.53, 0.53, 0.60, 1.0]);
+
+ let row_h = 24.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() {
+ 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(
+ "",
+ list_box_x + 2.0,
+ draw_y,
+ list_box_w - 16.0,
+ row_h,
+ [0.0, 0.0, 0.0, 0.0],
+ [1.0, 1.0, 1.0, 0.06],
+ [0.0, 0.0, 0.0, 0.0],
+ AppAction::Hardware(HardwareMessage::None),
+ );
+
+ pc.text(pid, list_box_x + 12.0, draw_y + 6.0, 12.0, [0.80, 0.80, 0.85, 1.0]);
+ pc.text(comm, list_box_x + 80.0, draw_y + 6.0, 12.0, [0.80, 0.80, 0.85, 1.0]);
+ pc.text(&format!("{}%", cpu), list_box_x + list_box_w - 60.0, draw_y + 6.0, 12.0, [0.56, 0.83, 0.56, 1.0]);
+ }
+ }
+
+ if state.processes.is_empty() {
+ pc.text("No active processes", list_box_x + 12.0, list_box_y + header_h + 16.0, 12.0, TEXT_DIM);
}
- }
-
- if state.processes.is_empty() {
- pc.text("No active processes", list_box_x + 12.0, list_box_y + header_h + 16.0, 12.0, TEXT_DIM);
- }
- sec.content_y += list_box_h;
- }
- y = sec.finish_focused(&mut pc, root_focused);
+ sec.content_y += list_box_h;
+ }
+ sec.finish_focused(pc, root_focused)
+ });
// ── GPU Section ──
- let mut sec_gpu = Section::new(&mut pc, cx, y, cw, "GPU");
- if !state.loaded {
- sec_gpu.text(&mut pc, "Loading GPU models...", 12.0, 0.0, 12.0, TEXT_FG);
- sec_gpu.spacing(10.0);
- } else {
- for (i, gpu_lbl) in state.gpu_labels.iter_mut().enumerate() {
- if i > 0 { sec_gpu.spacing(12.0); }
- sec_gpu.widget(&mut pc, gpu_lbl, 12.0, cw - 24.0, 26.0);
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec_gpu = Section::new(pc, rx, ry, sec_w, "GPU");
+ if !state.loaded {
+ sec_gpu.text(pc, "Loading GPU models...", 12.0, 0.0, 12.0, TEXT_FG);
+ sec_gpu.spacing(10.0);
+ } else {
+ for (i, gpu_lbl) in state.gpu_labels.iter_mut().enumerate() {
+ if i > 0 { sec_gpu.spacing(12.0); }
+ sec_gpu.widget(pc, gpu_lbl, 12.0, sec_w - 24.0, 26.0);
+ }
}
- }
- y = sec_gpu.finish(&mut pc);
+ sec_gpu.finish(pc)
+ });
// ── Battery Section ──
- let mut sec_bat = Section::new(&mut pc, cx, y, cw, "Battery");
- if !state.loaded {
- sec_bat.text(&mut pc, "Loading battery status...", 12.0, 0.0, 12.0, TEXT_DIM);
- sec_bat.spacing(18.0);
- } else {
- let bat = &state.battery;
- let bat_icon = match bat.state.as_str() {
- "charging" => "+",
- "fully-charged" => "=",
- _ => "",
- };
-
- let pct_color = if bat.percentage < 20.0 { RED }
- else if bat.percentage < 50.0 { ORANGE }
- else { ACCENT };
-
- let pct_str = format!("{} {:.0}%", bat_icon, bat.percentage);
- sec_bat.text(&mut pc, &pct_str, 12.0, 0.0, 24.0, pct_color);
- sec_bat.spacing(30.0);
-
- let state_str = format!("{} • {:.1}W • {:.1}/{:.1} Wh",
- bat.state, bat.energy_rate, bat.energy, bat.energy_full);
- sec_bat.text(&mut pc, &state_str, 12.0, 0.0, 12.0, TEXT_DIM);
- sec_bat.spacing(18.0);
-
- let time_str = if bat.time_to_empty > 0 {
- format!("Time remaining: {}", format_duration(bat.time_to_empty))
- } else if bat.time_to_full > 0 {
- format!("Time to full: {}", format_duration(bat.time_to_full))
- } else { String::new() };
- if !time_str.is_empty() {
- sec_bat.text(&mut pc, &time_str, 12.0, 0.0, 12.0, TEXT_DIM);
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec_bat = Section::new(pc, rx, ry, sec_w, "Battery");
+ if !state.loaded {
+ sec_bat.text(pc, "Loading battery status...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec_bat.spacing(18.0);
+ } else {
+ let bat = &state.battery;
+ let bat_icon = match bat.state.as_str() {
+ "charging" => "+",
+ "fully-charged" => "=",
+ _ => "",
+ };
+
+ let pct_color = if bat.percentage < 20.0 { RED }
+ else if bat.percentage < 50.0 { ORANGE }
+ else { ACCENT };
+
+ let pct_str = format!("{} {:.0}%", bat_icon, bat.percentage);
+ sec_bat.text(pc, &pct_str, 12.0, 0.0, 24.0, pct_color);
+ sec_bat.spacing(30.0);
+
+ let state_str = format!("{} • {:.1}W • {:.1}/{:.1} Wh",
+ bat.state, bat.energy_rate, bat.energy, bat.energy_full);
+ sec_bat.text(pc, &state_str, 12.0, 0.0, 12.0, TEXT_DIM);
sec_bat.spacing(18.0);
- }
- let detail_str = format!("{} {}", bat.vendor, bat.model);
- sec_bat.text(&mut pc, &detail_str, 12.0, 0.0, 11.0, TEXT_DIM);
- sec_bat.spacing(20.0);
+ let time_str = if bat.time_to_empty > 0 {
+ format!("Time remaining: {}", format_duration(bat.time_to_empty))
+ } else if bat.time_to_full > 0 {
+ format!("Time to full: {}", format_duration(bat.time_to_full))
+ } else { String::new() };
+ if !time_str.is_empty() {
+ sec_bat.text(pc, &time_str, 12.0, 0.0, 12.0, TEXT_DIM);
+ sec_bat.spacing(18.0);
+ }
- let ac_str = if state.on_ac { "On AC Power" } else { "On Battery" };
- sec_bat.text(&mut pc, ac_str, 12.0, 0.0, 14.0, TEXT_FG);
- }
- y = sec_bat.finish(&mut pc);
+ let detail_str = format!("{} {}", bat.vendor, bat.model);
+ sec_bat.text(pc, &detail_str, 12.0, 0.0, 11.0, TEXT_DIM);
+ sec_bat.spacing(20.0);
+
+ let ac_str = if state.on_ac { "On AC Power" } else { "On Battery" };
+ sec_bat.text(pc, ac_str, 12.0, 0.0, 14.0, TEXT_FG);
+ }
+ sec_bat.finish(pc)
+ });
// ── CPU Governor section ──
- let mut sec_gov = Section::new(&mut pc, cx, y, cw, "CPU Governor");
- if !state.loaded {
- sec_gov.text(&mut pc, "Loading CPU governor...", 12.0, 0.0, 12.0, TEXT_DIM);
- sec_gov.spacing(18.0);
- } else {
- let btn_h = 44.0;
- let yt = sec_gov.ay();
-
- sec_gov.row(2, 8.0, btn_h, |i, x, w| {
- if i == 0 {
- let perf_active = !state.cpu_powersave;
- let (perf_bg, perf_desc, perf_desc_color) = if perf_active {
- (BTN_ACTIVE, "Governor set to performance", ACCENT)
- } else {
- (BTN_INACTIVE, "Switch to performance governor", TEXT_DIM)
- };
-
- pc.button("Performance", x, yt, w, btn_h,
- perf_bg, BTN_HOVER, WHITE,
- AppAction::Hardware(HardwareMessage::SetCpuPerformance));
- pc.text(perf_desc, x + 4.0, yt + 26.0, 10.0, perf_desc_color);
- } else {
- let (save_bg, save_desc, save_desc_color) = if state.cpu_powersave {
- (BTN_ACTIVE, "Governor set to powersave — lower power, slower burst", ACCENT)
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec_gov = Section::new(pc, rx, ry, sec_w, "CPU Governor");
+ if !state.loaded {
+ sec_gov.text(pc, "Loading CPU governor...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec_gov.spacing(18.0);
+ } else {
+ let btn_h = 44.0;
+ let yt = sec_gov.ay();
+
+ sec_gov.row(2, 8.0, btn_h, |i, x, w| {
+ if i == 0 {
+ let perf_active = !state.cpu_powersave;
+ let (perf_bg, perf_desc, perf_desc_color) = if perf_active {
+ (BTN_ACTIVE, "Governor set to performance", ACCENT)
+ } else {
+ (BTN_INACTIVE, "Switch to performance governor", TEXT_DIM)
+ };
+
+ pc.button("Performance", x, yt, w, btn_h,
+ perf_bg, BTN_HOVER, WHITE,
+ AppAction::Hardware(HardwareMessage::SetCpuPerformance));
+ pc.text(perf_desc, x + 4.0, yt + 26.0, 10.0, perf_desc_color);
} else {
- (BTN_INACTIVE, "Switch to powersave governor (requires auth)", TEXT_DIM)
- };
-
- pc.button("Powersave", x, yt, w, btn_h,
- save_bg, BTN_HOVER, WHITE,
- AppAction::Hardware(HardwareMessage::SetCpuPowersave));
- pc.text(save_desc, x + 4.0, yt + 26.0, 10.0, save_desc_color);
- }
- });
- sec_gov.spacing(12.0);
- }
- y = sec_gov.finish(&mut pc);
+ let (save_bg, save_desc, save_desc_color) = if state.cpu_powersave {
+ (BTN_ACTIVE, "Governor set to powersave — lower power, slower burst", ACCENT)
+ } else {
+ (BTN_INACTIVE, "Switch to powersave governor (requires auth)", TEXT_DIM)
+ };
+
+ pc.button("Powersave", x, yt, w, btn_h,
+ save_bg, BTN_HOVER, WHITE,
+ AppAction::Hardware(HardwareMessage::SetCpuPowersave));
+ pc.text(save_desc, x + 4.0, yt + 26.0, 10.0, save_desc_color);
+ }
+ });
+ sec_gov.spacing(12.0);
+ }
+ sec_gov.finish(pc)
+ });
// ── GPU Power section ──
- let mut sec_gpow = Section::new(&mut pc, cx, y, cw, "GPU Power");
- if !state.loaded {
- sec_gpow.text(&mut pc, "Loading GPU power status...", 12.0, 0.0, 12.0, TEXT_DIM);
- sec_gpow.spacing(18.0);
- } else {
- let btn_h = 44.0;
- let yt = sec_gpow.ay();
-
- sec_gpow.row(2, 8.0, btn_h, |i, x, w| {
- if i == 0 {
- let gpu_def_active = !state.gpu_powersave;
- let (gpu_def_bg, gpu_def_desc, gpu_def_desc_c) = if gpu_def_active {
- (BTN_ACTIVE, "NVIDIA running at default power limit", ACCENT)
- } else {
- (BTN_INACTIVE, "Restore default power limit (requires auth)", TEXT_DIM)
- };
-
- pc.button("80W Default", x, yt, w, btn_h,
- gpu_def_bg, BTN_HOVER, WHITE,
- AppAction::Hardware(HardwareMessage::SetGpuDefault));
- pc.text(gpu_def_desc, x + 4.0, yt + 26.0, 10.0, gpu_def_desc_c);
- } else {
- let (gpu_cap_bg, gpu_cap_desc, gpu_cap_desc_c) = if state.gpu_powersave {
- (BTN_ACTIVE, "NVIDIA power limit capped at 5W — minimal draw", ACCENT)
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec_gpow = Section::new(pc, rx, ry, sec_w, "GPU Power");
+ if !state.loaded {
+ sec_gpow.text(pc, "Loading GPU power status...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec_gpow.spacing(18.0);
+ } else {
+ let btn_h = 44.0;
+ let yt = sec_gpow.ay();
+
+ sec_gpow.row(2, 8.0, btn_h, |i, x, w| {
+ if i == 0 {
+ let gpu_def_active = !state.gpu_powersave;
+ let (gpu_def_bg, gpu_def_desc, gpu_def_desc_c) = if gpu_def_active {
+ (BTN_ACTIVE, "NVIDIA running at default power limit", ACCENT)
+ } else {
+ (BTN_INACTIVE, "Restore default power limit (requires auth)", TEXT_DIM)
+ };
+
+ pc.button("80W Default", x, yt, w, btn_h,
+ gpu_def_bg, BTN_HOVER, WHITE,
+ AppAction::Hardware(HardwareMessage::SetGpuDefault));
+ pc.text(gpu_def_desc, x + 4.0, yt + 26.0, 10.0, gpu_def_desc_c);
} else {
- (BTN_INACTIVE, "Cap NVIDIA to 5W power limit (requires auth)", TEXT_DIM)
- };
-
- pc.button("5W Cap", x, yt, w, btn_h,
- gpu_cap_bg, BTN_HOVER, WHITE,
- AppAction::Hardware(HardwareMessage::SetGpuPowersave));
- pc.text(gpu_cap_desc, x + 4.0, yt + 26.0, 10.0, gpu_cap_desc_c);
- }
- });
- sec_gpow.spacing(12.0);
- }
- sec_gpow.finish(&mut pc);
+ let (gpu_cap_bg, gpu_cap_desc, gpu_cap_desc_c) = if state.gpu_powersave {
+ (BTN_ACTIVE, "NVIDIA power limit capped at 5W — minimal draw", ACCENT)
+ } else {
+ (BTN_INACTIVE, "Cap NVIDIA to 5W power limit (requires auth)", TEXT_DIM)
+ };
+
+ pc.button("5W Cap", x, yt, w, btn_h,
+ gpu_cap_bg, BTN_HOVER, WHITE,
+ AppAction::Hardware(HardwareMessage::SetGpuPowersave));
+ pc.text(gpu_cap_desc, x + 4.0, yt + 26.0, 10.0, gpu_cap_desc_c);
+ }
+ });
+ sec_gpow.spacing(12.0);
+ }
+ sec_gpow.finish(pc)
+ });
- pc
+ final_pc
}
pub fn update(state: &mut HardwareState, msg: HardwareMessage) {
diff --git a/src/pages/input.rs b/src/pages/input.rs
index 4cb1159..11e30a4 100644
--- a/src/pages/input.rs
+++ b/src/pages/input.rs
@@ -2,8 +2,8 @@ use std::fs;
use std::io::Write;
use crate::app::PageContent;
-use clear_ui::layout::Section;
-use clear_ui::widget::{Dropdown, Spinbox, Toggle, Widget, Finger, Trackpad};
+use clear_ui::layout::{Section, PageLayoutBuilder, LayoutStrategy};
+use clear_ui::widget::{Spinbox, Toggle, Trackpad, Dropdown, Finger, Widget};
const CONFIG_PATH: &str = "/home/lsgalante/.config/ccec/config.toml";
@@ -389,126 +389,146 @@ 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, sec_focused: &[bool]) -> PageContent {
- let mut pc = PageContent::new();
- let mut y = cy + 12.0;
+pub fn view(state: &mut InputState, cx: f32, cy: f32, cw: f32, ch: f32, sec_focused: &[bool], layout: &mut dyn LayoutStrategy) -> PageContent {
+ let mut final_pc = PageContent::new();
+ let sec_w = 320.0f32;
+ let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(7);
// ── Touchpad ──
- let mut sec = Section::new(&mut pc, cx, y, cw, "Touchpad");
-
- let toggle_w = 48.0;
- let toggle_h = 24.0;
- state.tap_toggle.set_toggled(state.tap_to_click);
- sec.widget(&mut pc, &mut state.tap_toggle, 14.0, toggle_w, toggle_h);
- sec.spacing(8.0);
-
- // Built-in trackpad visualizer widget
- let pad_w = 280.0;
- let pad_h = 140.0;
- state.trackpad.set_fingers(state.fingers.clone());
- sec.widget(&mut pc, &mut state.trackpad, 14.0, pad_w, pad_h);
- sec.spacing(12.0);
- y = sec.finish(&mut pc);
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec = Section::new(pc, rx, ry, sec_w, "Touchpad");
+
+ let toggle_w = 48.0;
+ let toggle_h = 24.0;
+ state.tap_toggle.set_toggled(state.tap_to_click);
+ sec.widget(pc, &mut state.tap_toggle, 14.0, toggle_w, toggle_h);
+ sec.spacing(8.0);
+
+ // Built-in trackpad visualizer widget
+ let pad_w = 280.0;
+ let pad_h = 140.0;
+ state.trackpad.set_fingers(state.fingers.clone());
+ sec.widget(pc, &mut state.trackpad, 14.0, pad_w, pad_h);
+ sec.spacing(12.0);
+ sec.finish(pc)
+ });
// ── Trackpoint ──
- let mut sec = Section::new(&mut pc, cx, y, cw, "Trackpoint");
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec = Section::new(pc, rx, ry, sec_w, "Trackpoint");
- state.dwtp_toggle.set_toggled(state.dwtp);
- sec.widget(&mut pc, &mut state.dwtp_toggle, 14.0, toggle_w, toggle_h);
- sec.spacing(12.0);
+ let toggle_w = 48.0;
+ let toggle_h = 24.0;
+ state.dwtp_toggle.set_toggled(state.dwtp);
+ sec.widget(pc, &mut state.dwtp_toggle, 14.0, toggle_w, toggle_h);
+ sec.spacing(12.0);
- sec.widget(&mut pc, &mut state.trackpoint_accel_speed_spinbox, 14.0, 200.0, 26.0);
- sec.spacing(12.0);
+ sec.widget(pc, &mut state.trackpoint_accel_speed_spinbox, 14.0, 200.0, 26.0);
+ sec.spacing(12.0);
- sec.widget(&mut pc, &mut state.trackpoint_accel_profile_menu, 14.0, 200.0, 26.0);
- sec.spacing(8.0);
- y = sec.finish_focused(&mut pc, sec_focused.get(0).copied().unwrap_or(false));
+ sec.widget(pc, &mut state.trackpoint_accel_profile_menu, 14.0, 200.0, 26.0);
+ sec.spacing(8.0);
+ sec.finish_focused(pc, sec_focused.first().copied().unwrap_or(false))
+ });
// ── Keyboard ──
- let mut sec = Section::new(&mut pc, cx, y, cw, "Keyboard");
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec = Section::new(pc, rx, ry, sec_w, "Keyboard");
- sec.widget(&mut pc, &mut state.rate_spinbox, 14.0, 200.0, 26.0);
- sec.spacing(8.0);
+ sec.widget(pc, &mut state.rate_spinbox, 14.0, 200.0, 26.0);
+ sec.spacing(8.0);
- sec.widget(&mut pc, &mut state.delay_spinbox, 14.0, 200.0, 26.0);
- sec.spacing(8.0);
- y = sec.finish_focused(&mut pc, sec_focused.get(1).copied().unwrap_or(false));
+ sec.widget(pc, &mut state.delay_spinbox, 14.0, 200.0, 26.0);
+ sec.spacing(8.0);
+ sec.finish_focused(pc, sec_focused.get(1).copied().unwrap_or(false))
+ });
// ── Cursor ──
- let mut sec = Section::new(&mut pc, cx, y, cw, "Cursor");
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec = Section::new(pc, rx, ry, sec_w, "Cursor");
- sec.widget(&mut pc, &mut state.cursor_theme_menu, 14.0, 200.0, 26.0);
- sec.spacing(12.0);
+ sec.widget(pc, &mut state.cursor_theme_menu, 14.0, 200.0, 26.0);
+ sec.spacing(12.0);
- sec.widget(&mut pc, &mut state.cursor_size_spinbox, 14.0, 200.0, 26.0);
- sec.spacing(8.0);
+ sec.widget(pc, &mut state.cursor_size_spinbox, 14.0, 200.0, 26.0);
+ sec.spacing(8.0);
- y = sec.finish_focused(&mut pc, sec_focused.get(2).copied().unwrap_or(false));
+ sec.finish_focused(pc, sec_focused.get(2).copied().unwrap_or(false))
+ });
// ── Scrolling ──
- let mut sec = Section::new(&mut pc, cx, y, cw, "Scrolling");
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec = Section::new(pc, rx, ry, sec_w, "Scrolling");
- state.scroll_toggle.set_toggled(state.inertial_scroll);
- sec.widget(&mut pc, &mut state.scroll_toggle, 14.0, toggle_w, toggle_h);
- sec.spacing(12.0);
+ let toggle_w = 48.0;
+ let toggle_h = 24.0;
+ state.scroll_toggle.set_toggled(state.inertial_scroll);
+ sec.widget(pc, &mut state.scroll_toggle, 14.0, toggle_w, toggle_h);
+ sec.spacing(12.0);
- sec.widget(&mut pc, &mut state.scroll_friction_spinbox, 14.0, 200.0, 26.0);
- sec.spacing(12.0);
+ sec.widget(pc, &mut state.scroll_friction_spinbox, 14.0, 200.0, 26.0);
+ sec.spacing(12.0);
- state.natural_toggle.set_toggled(state.natural_scroll);
- sec.widget(&mut pc, &mut state.natural_toggle, 14.0, toggle_w, toggle_h);
- sec.spacing(12.0);
+ state.natural_toggle.set_toggled(state.natural_scroll);
+ sec.widget(pc, &mut state.natural_toggle, 14.0, toggle_w, toggle_h);
+ sec.spacing(12.0);
- sec.widget(&mut pc, &mut state.scroll_speed_spinbox, 14.0, 200.0, 26.0);
- sec.spacing(8.0);
+ sec.widget(pc, &mut state.scroll_speed_spinbox, 14.0, 200.0, 26.0);
+ sec.spacing(8.0);
- y = sec.finish_focused(&mut pc, sec_focused.get(3).copied().unwrap_or(false));
+ sec.finish_focused(pc, sec_focused.get(3).copied().unwrap_or(false))
+ });
// ── Inertial Input ──
- let mut sec = Section::new(&mut pc, cx, y, cw, "Inertial Input");
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec = Section::new(pc, rx, ry, sec_w, "Inertial Input");
- state.pointer_toggle.set_toggled(state.inertial_pointer);
- sec.widget(&mut pc, &mut state.pointer_toggle, 14.0, toggle_w, toggle_h);
- sec.spacing(12.0);
+ let toggle_w = 48.0;
+ let toggle_h = 24.0;
+ state.pointer_toggle.set_toggled(state.inertial_pointer);
+ sec.widget(pc, &mut state.pointer_toggle, 14.0, toggle_w, toggle_h);
+ sec.spacing(12.0);
- sec.widget(&mut pc, &mut state.pointer_friction_spinbox, 14.0, 200.0, 26.0);
- sec.spacing(16.0);
+ sec.widget(pc, &mut state.pointer_friction_spinbox, 14.0, 200.0, 26.0);
+ sec.spacing(16.0);
- state.trackpad_toggle.set_toggled(state.inertial_trackpad);
- sec.widget(&mut pc, &mut state.trackpad_toggle, 14.0, toggle_w, toggle_h);
- sec.spacing(12.0);
+ state.trackpad_toggle.set_toggled(state.inertial_trackpad);
+ sec.widget(pc, &mut state.trackpad_toggle, 14.0, toggle_w, toggle_h);
+ sec.spacing(12.0);
- sec.widget(&mut pc, &mut state.trackpad_friction_spinbox, 14.0, 200.0, 26.0);
- sec.spacing(8.0);
+ sec.widget(pc, &mut state.trackpad_friction_spinbox, 14.0, 200.0, 26.0);
+ sec.spacing(8.0);
- y = sec.finish_focused(&mut pc, sec_focused.get(4).copied().unwrap_or(false));
+ sec.finish_focused(pc, sec_focused.get(4).copied().unwrap_or(false))
+ });
// ── Keybindings ──
- let mut sec = Section::new(&mut pc, cx, y, cw, "Keyboard Bindings");
-
- for kb in &state.keybinds {
- let binding = if kb.mods.is_empty() {
- kb.key.clone()
- } else {
- format!("{}+{}", kb.mods, kb.key)
- };
- let action_label = if kb.command.is_empty() {
- kb.action.clone()
- } else {
- format!("{}: {}", kb.action, kb.command)
- };
- sec.text(&mut pc, &binding, 14.0, 0.0, 12.0, TEXT_FG);
- let label_w = cw - 200.0;
- sec.text(&mut pc, &action_label, 14.0 + label_w.min(180.0), 0.0, 12.0, TEXT_DIM);
- sec.spacing(18.0);
- }
- sec.finish(&mut pc);
-
-
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec = Section::new(pc, rx, ry, sec_w, "Keyboard Bindings");
+
+ for kb in &state.keybinds {
+ let binding = if kb.mods.is_empty() {
+ kb.key.clone()
+ } else {
+ format!("{}+{}", kb.mods, kb.key)
+ };
+ let action_label = if kb.command.is_empty() {
+ kb.action.clone()
+ } else {
+ format!("{}: {}", kb.action, kb.command)
+ };
+ sec.text(pc, &binding, 14.0, 0.0, 12.0, TEXT_FG);
+ let label_w = sec_w - 200.0;
+ sec.text(pc, &action_label, 14.0 + label_w.min(180.0), 0.0, 12.0, TEXT_DIM);
+ sec.spacing(18.0);
+ }
+ sec.finish(pc)
+ });
- pc
+ final_pc
}
+
pub fn update(state: &mut InputState, msg: InputMessage) {
match msg {
InputMessage::ToggleTapToClick => {
@@ -639,5 +659,14 @@ mod tests {
assert_eq!(parse_bool_from_default(empty_content, "natural_scroll", false), false);
assert_eq!(parse_f32_key(empty_content, "scroll_speed", 1.0), 1.0);
}
+
+ #[test]
+ fn test_view_layout_grid() {
+ let mut state = InputState::default();
+ let mut layout = clear_ui::layout::ColumnLayout::new(20.0);
+ let pc = view(&mut state, 10.0, 20.0, 800.0, 600.0, &[false, false, false, false, false], &mut layout);
+ assert!(!pc.rects.is_empty() || !pc.texts.is_empty());
+ }
}
+
diff --git a/src/pages/colors.rs b/src/pages/interface.rs
similarity index 59%
rename from src/pages/colors.rs
rename to src/pages/interface.rs
index 0f58310..9ea2489 100644
--- a/src/pages/colors.rs
+++ b/src/pages/interface.rs
@@ -1,8 +1,9 @@
use std::fs;
use std::io::Write;
use crate::app::PageContent;
-use clear_ui::layout::Section;
-use clear_ui::widget::ColorSelector;
+use crate::pages::typeface::parse_u16_from;
+use clear_ui::layout::{Section, PageLayoutBuilder, LayoutStrategy};
+use clear_ui::widget::{ColorSelector, Spinbox, Widget};
const CONFIG_PATH: &str = "/home/lsgalante/.config/ccec/config.toml";
@@ -14,7 +15,7 @@ fn get_socket_path() -> String {
}
#[derive(Debug, Clone)]
-pub struct ColorsState {
+pub struct InterfaceState {
pub low_color: [u8; 3],
pub high_color: [u8; 3],
pub disabled_color: [u8; 3],
@@ -27,10 +28,20 @@ pub struct ColorsState {
pub paginator_sidebar_color: [u8; 3],
pub primary_highlight_color: [u8; 3],
pub paginator_tab_label_color: [u8; 3],
+ pub toggle_enabled_color: [u8; 3],
+ pub toggle_disabled_color: [u8; 3],
pub color_selectors: Vec<ColorSelector>,
+ pub paginator_tab_margin_x: u16,
+ pub paginator_tab_margin_y: u16,
+ pub tab_margin_spinbox_x: Spinbox,
+ pub tab_margin_spinbox_y: Spinbox,
+ pub paginator_tab_padding_x: u16,
+ pub paginator_tab_padding_y: u16,
+ pub tab_padding_spinbox_x: Spinbox,
+ pub tab_padding_spinbox_y: Spinbox,
}
-impl Default for ColorsState {
+impl Default for InterfaceState {
fn default() -> Self {
Self {
low_color: [0x0a, 0x1a, 0x0e],
@@ -45,6 +56,8 @@ impl Default for ColorsState {
paginator_sidebar_color: [90, 90, 101],
primary_highlight_color: [255, 255, 255],
paginator_tab_label_color: [230, 230, 242],
+ toggle_enabled_color: [104, 217, 165],
+ toggle_disabled_color: [135, 135, 148],
color_selectors: vec![
ColorSelector::new([71, 71, 81]).with_label("Low Color"), // 0: Pages - Low Color
ColorSelector::new([0x3e, 0x3e, 0x3e]).with_label("High Color"), // 1: Layout - High Color
@@ -58,13 +71,23 @@ impl Default for ColorsState {
ColorSelector::new([90, 90, 101]).with_label("Paginator Sidebar"), // 9: Controls - Paginator Sidebar
ColorSelector::new([255, 255, 255]).with_label("Primary Highlight"), // 10: Controls - Primary Highlight
ColorSelector::new([230, 230, 242]).with_label("Paginator Tab Label"), // 11: Controls - Paginator Tab Label
+ ColorSelector::new([104, 217, 165]).with_label("Enabled"), // 12: Toggles - Enabled
+ ColorSelector::new([135, 135, 148]).with_label("Disabled"), // 13: Toggles - Disabled
],
+ paginator_tab_margin_x: 5,
+ paginator_tab_margin_y: 10,
+ tab_margin_spinbox_x: Spinbox::new(5, 0, 100, 1).with_label("Tab Margin X").with_unit("px"),
+ tab_margin_spinbox_y: Spinbox::new(10, 0, 100, 1).with_label("Tab Margin Y").with_unit("px"),
+ paginator_tab_padding_x: 10,
+ paginator_tab_padding_y: 14,
+ tab_padding_spinbox_x: Spinbox::new(10, 0, 100, 1).with_label("Tab Padding X").with_unit("px"),
+ tab_padding_spinbox_y: Spinbox::new(14, 0, 100, 1).with_label("Tab Padding Y").with_unit("px"),
}
}
}
#[derive(Debug, Clone)]
-pub enum ColorsMessage {
+pub enum InterfaceMessage {
SetLowColor([u8; 3]),
SetHighColor([u8; 3]),
SetDisabledColor([u8; 3]),
@@ -77,6 +100,12 @@ pub enum ColorsMessage {
SetPaginatorSidebarColor([u8; 3]),
SetPrimaryHighlightColor([u8; 3]),
SetPaginatorTabLabelColor([u8; 3]),
+ SetToggleEnabledColor([u8; 3]),
+ SetToggleDisabledColor([u8; 3]),
+ SetTabMarginX(u16),
+ SetTabMarginY(u16),
+ SetTabPaddingX(u16),
+ SetTabPaddingY(u16),
PickLowColor,
PickHighColor,
PickDisabledColor,
@@ -89,12 +118,12 @@ pub enum ColorsMessage {
PickPaginatorSidebarColor,
PickPrimaryHighlightColor,
PickPaginatorTabLabelColor,
- Refreshed(ColorsState),
+ PickToggleEnabledColor,
+ PickToggleDisabledColor,
+ Refreshed(InterfaceState),
}
-
-
-pub fn read_colors_config() -> ColorsState {
+pub fn read_interface_config() -> InterfaceState {
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 {
@@ -129,8 +158,18 @@ pub fn read_colors_config() -> ColorsState {
let primary_highlight = parse_color_from_key(&content, "primary_highlight_color", [255, 255, 255]);
let paginator_tab_label = parse_color_from_key(&content, "paginator_tab_label_color", [230, 230, 242]);
+
+ let toggle_enabled = parse_color_from_key(&content, "toggle_enabled_color", [104, 217, 165]);
+
+ let toggle_disabled = parse_color_from_key(&content, "toggle_disabled_color", [135, 135, 148]);
+
+ let paginator_tab_margin_general = parse_u16_from(&content, "paginator_tab_margin", 999);
+ let paginator_tab_margin_x = parse_u16_from(&content, "paginator_tab_margin_x", if paginator_tab_margin_general != 999 { paginator_tab_margin_general } else { 5 });
+ let paginator_tab_margin_y = parse_u16_from(&content, "paginator_tab_margin_y", if paginator_tab_margin_general != 999 { paginator_tab_margin_general } else { 10 });
+ let paginator_tab_padding_x = parse_u16_from(&content, "paginator_tab_padding_x", 10);
+ let paginator_tab_padding_y = parse_u16_from(&content, "paginator_tab_padding_y", 14);
- ColorsState {
+ InterfaceState {
low_color: bg,
high_color: border,
disabled_color: disabled,
@@ -143,6 +182,8 @@ pub fn read_colors_config() -> ColorsState {
paginator_sidebar_color: paginator_sidebar,
primary_highlight_color: primary_highlight,
paginator_tab_label_color: paginator_tab_label,
+ toggle_enabled_color: toggle_enabled,
+ toggle_disabled_color: toggle_disabled,
color_selectors: vec![
ColorSelector::new(page_low).with_label("Low Color"), // 0: Pages - Low Color
ColorSelector::new(border).with_label("High Color"), // 1: Layout - High Color
@@ -156,7 +197,17 @@ pub fn read_colors_config() -> ColorsState {
ColorSelector::new(paginator_sidebar).with_label("Paginator Sidebar"), // 9: Controls - Paginator Sidebar
ColorSelector::new(primary_highlight).with_label("Primary Highlight"), // 10: Controls - Primary Highlight
ColorSelector::new(paginator_tab_label).with_label("Paginator Tab Label"), // 11: Controls - Paginator Tab Label
+ ColorSelector::new(toggle_enabled).with_label("Enabled"), // 12: Toggles - Enabled
+ ColorSelector::new(toggle_disabled).with_label("Disabled"), // 13: Toggles - Disabled
],
+ paginator_tab_margin_x,
+ paginator_tab_margin_y,
+ tab_margin_spinbox_x: Spinbox::new(paginator_tab_margin_x as i32, 0, 100, 1).with_label("Tab Margin X").with_unit("px"),
+ tab_margin_spinbox_y: Spinbox::new(paginator_tab_margin_y as i32, 0, 100, 1).with_label("Tab Margin Y").with_unit("px"),
+ paginator_tab_padding_x,
+ paginator_tab_padding_y,
+ tab_padding_spinbox_x: Spinbox::new(paginator_tab_padding_x as i32, 0, 100, 1).with_label("Tab Padding X").with_unit("px"),
+ tab_padding_spinbox_y: Spinbox::new(paginator_tab_padding_y as i32, 0, 100, 1).with_label("Tab Padding Y").with_unit("px"),
}
}
@@ -165,7 +216,7 @@ fn parse_color_from_key(content: &str, key: &str, default: [u8; 3]) -> [u8; 3] {
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();
+ let hex = rest.trim_end_matches('"').trim().trim_start_matches('#');
return parse_hex(hex);
}
}
@@ -332,122 +383,234 @@ fn apply_paginator_tab_label_color(rgb: [u8; 3]) {
clear_ui::color::set_paginator_tab_label_color([r, g, b, 1.0]);
}
-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;
+fn apply_toggle_enabled_color(rgb: [u8; 3]) {
+ let hex = format!("\"#{:02x}{:02x}{:02x}\"", rgb[0], rgb[1], rgb[2]);
+ write_config_value("toggle_enabled_color", &hex);
+ let r = clear_ui::color::srgb_to_linear(rgb[0] as f32 / 255.0);
+ let g = clear_ui::color::srgb_to_linear(rgb[1] as f32 / 255.0);
+ let b = clear_ui::color::srgb_to_linear(rgb[2] as f32 / 255.0);
+ clear_ui::color::set_toggle_on_color([r, g, b, 1.0]);
+}
+
+fn apply_toggle_disabled_color(rgb: [u8; 3]) {
+ let hex = format!("\"#{:02x}{:02x}{:02x}\"", rgb[0], rgb[1], rgb[2]);
+ write_config_value("toggle_disabled_color", &hex);
+ let r = clear_ui::color::srgb_to_linear(rgb[0] as f32 / 255.0);
+ let g = clear_ui::color::srgb_to_linear(rgb[1] as f32 / 255.0);
+ let b = clear_ui::color::srgb_to_linear(rgb[2] as f32 / 255.0);
+ clear_ui::color::set_toggle_off_color([r, g, b, 1.0]);
+}
+
+fn apply_paginator_tab_margin_x(margin: u16) {
+ write_config_value("paginator_tab_margin_x", &margin.to_string());
+ send_ipc_command(&format!("layout paginator_tab_margin_x {}", margin));
+}
+
+fn apply_paginator_tab_margin_y(margin: u16) {
+ write_config_value("paginator_tab_margin_y", &margin.to_string());
+ send_ipc_command(&format!("layout paginator_tab_margin_y {}", margin));
+}
+
+fn apply_paginator_tab_padding_x(padding: u16) {
+ write_config_value("paginator_tab_padding_x", &padding.to_string());
+ send_ipc_command(&format!("layout paginator_tab_padding_x {}", padding));
+}
+
+fn apply_paginator_tab_padding_y(padding: u16) {
+ write_config_value("paginator_tab_padding_y", &padding.to_string());
+ send_ipc_command(&format!("layout paginator_tab_padding_y {}", padding));
+}
+
+pub fn view(state: &mut InterfaceState, cx: f32, cy: f32, cw: f32, ch: f32, layout: &mut dyn LayoutStrategy) -> PageContent {
+ let mut final_pc = PageContent::new();
+ let sec_w = 320.0f32;
+ let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(6);
// 1. Pages Section
- let mut sec = Section::new(&mut pc, cx, y, cw, "Pages");
- sec.spacing(8.0);
- state.color_selectors[0].color = state.page_low_color;
- sec.widget(&mut pc, &mut state.color_selectors[0], 12.0, 220.0, 22.0);
- sec.spacing(8.0);
- y = sec.finish(&mut pc);
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec = Section::new(pc, rx, ry, sec_w, "Pages");
+ sec.spacing(8.0);
+ state.color_selectors[0].color = state.page_low_color;
+ sec.widget(pc, &mut state.color_selectors[0], 12.0, 220.0, 22.0);
+ sec.spacing(8.0);
+ sec.finish(pc)
+ });
// 2. Layout Section
- let mut sec = Section::new(&mut pc, cx, y, cw, "Layout");
- sec.spacing(8.0);
- state.color_selectors[7].color = state.low_color;
- sec.widget(&mut pc, &mut state.color_selectors[7], 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);
- state.color_selectors[2].color = state.visual_guides_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);
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec = Section::new(pc, rx, ry, sec_w, "Layout");
+ sec.spacing(8.0);
+ state.color_selectors[7].color = state.low_color;
+ sec.widget(pc, &mut state.color_selectors[7], 12.0, 220.0, 22.0);
+ sec.spacing(8.0);
+ state.color_selectors[1].color = state.high_color;
+ sec.widget(pc, &mut state.color_selectors[1], 12.0, 220.0, 22.0);
+ sec.spacing(8.0);
+ state.color_selectors[2].color = state.visual_guides_color;
+ sec.widget(pc, &mut state.color_selectors[2], 12.0, 220.0, 22.0);
+ sec.spacing(8.0);
+ sec.finish(pc)
+ });
// 3. Status Section
- let mut sec = Section::new(&mut pc, cx, y, cw, "Status");
- sec.spacing(8.0);
- state.color_selectors[8].color = state.normal_color;
- sec.widget(&mut pc, &mut state.color_selectors[8], 12.0, 220.0, 22.0);
- sec.spacing(8.0);
- state.color_selectors[3].color = state.disabled_color;
- sec.widget(&mut pc, &mut state.color_selectors[3], 12.0, 220.0, 22.0);
- sec.spacing(8.0);
- state.color_selectors[4].color = state.separator_color;
- sec.widget(&mut pc, &mut state.color_selectors[4], 12.0, 220.0, 22.0);
- sec.spacing(8.0);
- y = sec.finish(&mut pc);
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec = Section::new(pc, rx, ry, sec_w, "Status");
+ sec.spacing(8.0);
+ state.color_selectors[8].color = state.normal_color;
+ sec.widget(pc, &mut state.color_selectors[8], 12.0, 220.0, 22.0);
+ sec.spacing(8.0);
+ state.color_selectors[3].color = state.disabled_color;
+ sec.widget(pc, &mut state.color_selectors[3], 12.0, 220.0, 22.0);
+ sec.spacing(8.0);
+ state.color_selectors[4].color = state.separator_color;
+ sec.widget(pc, &mut state.color_selectors[4], 12.0, 220.0, 22.0);
+ sec.spacing(8.0);
+ sec.finish(pc)
+ });
// 4. Controls Section
- let mut sec = Section::new(&mut pc, cx, y, cw, "Controls");
- sec.spacing(8.0);
- state.color_selectors[5].color = state.slider_track_color;
- sec.widget(&mut pc, &mut state.color_selectors[5], 12.0, 220.0, 22.0);
- sec.spacing(8.0);
- state.color_selectors[6].color = state.color_borders_color;
- sec.widget(&mut pc, &mut state.color_selectors[6], 12.0, 220.0, 22.0);
- sec.spacing(8.0);
- state.color_selectors[9].color = state.paginator_sidebar_color;
- sec.widget(&mut pc, &mut state.color_selectors[9], 12.0, 220.0, 22.0);
- sec.spacing(8.0);
- state.color_selectors[10].color = state.primary_highlight_color;
- sec.widget(&mut pc, &mut state.color_selectors[10], 12.0, 220.0, 22.0);
- sec.spacing(8.0);
- state.color_selectors[11].color = state.paginator_tab_label_color;
- sec.widget(&mut pc, &mut state.color_selectors[11], 12.0, 220.0, 22.0);
- sec.spacing(8.0);
- sec.finish(&mut pc);
-
- pc
-}
-
-pub fn update(state: &mut ColorsState, msg: ColorsMessage) {
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec = Section::new(pc, rx, ry, sec_w, "Controls");
+ sec.spacing(8.0);
+ state.color_selectors[5].color = state.slider_track_color;
+ sec.widget(pc, &mut state.color_selectors[5], 12.0, 220.0, 22.0);
+ sec.spacing(8.0);
+ state.color_selectors[6].color = state.color_borders_color;
+ sec.widget(pc, &mut state.color_selectors[6], 12.0, 220.0, 22.0);
+ sec.spacing(8.0);
+ state.color_selectors[10].color = state.primary_highlight_color;
+ sec.widget(pc, &mut state.color_selectors[10], 12.0, 220.0, 22.0);
+ sec.spacing(8.0);
+ sec.finish(pc)
+ });
+
+ // 5. Paginator Section
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec = Section::new(pc, rx, ry, sec_w, "Paginator");
+ sec.spacing(8.0);
+ state.color_selectors[9].color = state.paginator_sidebar_color;
+ sec.widget(pc, &mut state.color_selectors[9], 12.0, 220.0, 22.0);
+ sec.spacing(8.0);
+ state.color_selectors[11].color = state.paginator_tab_label_color;
+ sec.widget(pc, &mut state.color_selectors[11], 12.0, 220.0, 22.0);
+ sec.spacing(8.0);
+ state.tab_margin_spinbox_x.value = state.paginator_tab_margin_x as i32;
+ sec.widget(pc, &mut state.tab_margin_spinbox_x, 12.0, 200.0, 26.0);
+ sec.spacing(8.0);
+ state.tab_margin_spinbox_y.value = state.paginator_tab_margin_y as i32;
+ sec.widget(pc, &mut state.tab_margin_spinbox_y, 12.0, 200.0, 26.0);
+ sec.spacing(8.0);
+ state.tab_padding_spinbox_x.value = state.paginator_tab_padding_x as i32;
+ sec.widget(pc, &mut state.tab_padding_spinbox_x, 12.0, 200.0, 26.0);
+ sec.spacing(8.0);
+ state.tab_padding_spinbox_y.value = state.paginator_tab_padding_y as i32;
+ sec.widget(pc, &mut state.tab_padding_spinbox_y, 12.0, 200.0, 26.0);
+ sec.spacing(8.0);
+ sec.finish(pc)
+ });
+
+ // 6. Toggles Section
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec = Section::new(pc, rx, ry, sec_w, "Toggles");
+ sec.spacing(8.0);
+ state.color_selectors[12].color = state.toggle_enabled_color;
+ sec.widget(pc, &mut state.color_selectors[12], 12.0, 220.0, 22.0);
+ sec.spacing(8.0);
+ state.color_selectors[13].color = state.toggle_disabled_color;
+ sec.widget(pc, &mut state.color_selectors[13], 12.0, 220.0, 22.0);
+ sec.spacing(8.0);
+ sec.finish(pc)
+ });
+
+ final_pc
+}
+
+
+pub fn update(state: &mut InterfaceState, msg: InterfaceMessage) {
match msg {
- ColorsMessage::SetLowColor(rgb) => {
+ InterfaceMessage::SetLowColor(rgb) => {
state.low_color = rgb;
apply_background(rgb);
}
- ColorsMessage::SetPageLowColor(rgb) => {
+ InterfaceMessage::SetPageLowColor(rgb) => {
state.page_low_color = rgb;
apply_page_low_color(rgb);
}
- ColorsMessage::SetHighColor(rgb) => {
+ InterfaceMessage::SetHighColor(rgb) => {
state.high_color = rgb;
apply_border_color(rgb);
}
- ColorsMessage::SetDisabledColor(rgb) => {
+ InterfaceMessage::SetDisabledColor(rgb) => {
state.disabled_color = rgb;
apply_disabled_color(rgb);
}
- ColorsMessage::SetSeparatorColor(rgb) => {
+ InterfaceMessage::SetSeparatorColor(rgb) => {
state.separator_color = rgb;
apply_separator_color(rgb);
}
- ColorsMessage::SetVisualGuidesColor(rgb) => {
+ InterfaceMessage::SetVisualGuidesColor(rgb) => {
state.visual_guides_color = rgb;
apply_visual_guides_color(rgb);
}
- ColorsMessage::SetSliderTrackColor(rgb) => {
+ InterfaceMessage::SetSliderTrackColor(rgb) => {
state.slider_track_color = rgb;
apply_slider_track_color(rgb);
}
- ColorsMessage::SetColorBordersColor(rgb) => {
+ InterfaceMessage::SetColorBordersColor(rgb) => {
state.color_borders_color = rgb;
apply_color_borders_color(rgb);
}
- ColorsMessage::SetNormalColor(rgb) => {
+ InterfaceMessage::SetNormalColor(rgb) => {
state.normal_color = rgb;
apply_normal_color(rgb);
}
- ColorsMessage::SetPaginatorSidebarColor(rgb) => {
+ InterfaceMessage::SetPaginatorSidebarColor(rgb) => {
state.paginator_sidebar_color = rgb;
apply_paginator_sidebar_color(rgb);
}
- ColorsMessage::SetPrimaryHighlightColor(rgb) => {
+ InterfaceMessage::SetPrimaryHighlightColor(rgb) => {
state.primary_highlight_color = rgb;
apply_primary_highlight_color(rgb);
}
- ColorsMessage::SetPaginatorTabLabelColor(rgb) => {
+ InterfaceMessage::SetPaginatorTabLabelColor(rgb) => {
state.paginator_tab_label_color = rgb;
apply_paginator_tab_label_color(rgb);
}
- ColorsMessage::PickLowColor | ColorsMessage::PickHighColor | ColorsMessage::PickDisabledColor | ColorsMessage::PickSeparatorColor | ColorsMessage::PickVisualGuides | ColorsMessage::PickSliderTrackColor | ColorsMessage::PickPageLowColor | ColorsMessage::PickColorBordersColor | ColorsMessage::PickNormalColor | ColorsMessage::PickPaginatorSidebarColor | ColorsMessage::PickPrimaryHighlightColor | ColorsMessage::PickPaginatorTabLabelColor => {}
- ColorsMessage::Refreshed(new) => {
+ InterfaceMessage::SetToggleEnabledColor(rgb) => {
+ state.toggle_enabled_color = rgb;
+ apply_toggle_enabled_color(rgb);
+ }
+ InterfaceMessage::SetToggleDisabledColor(rgb) => {
+ state.toggle_disabled_color = rgb;
+ apply_toggle_disabled_color(rgb);
+ }
+ InterfaceMessage::SetTabMarginX(margin) => {
+ state.paginator_tab_margin_x = margin;
+ apply_paginator_tab_margin_x(margin);
+ }
+ InterfaceMessage::SetTabMarginY(margin) => {
+ state.paginator_tab_margin_y = margin;
+ apply_paginator_tab_margin_y(margin);
+ }
+ InterfaceMessage::SetTabPaddingX(padding) => {
+ state.paginator_tab_padding_x = padding;
+ apply_paginator_tab_padding_x(padding);
+ }
+ InterfaceMessage::SetTabPaddingY(padding) => {
+ state.paginator_tab_padding_y = padding;
+ apply_paginator_tab_padding_y(padding);
+ }
+ InterfaceMessage::PickLowColor | InterfaceMessage::PickHighColor | InterfaceMessage::PickDisabledColor | InterfaceMessage::PickSeparatorColor | InterfaceMessage::PickVisualGuides | InterfaceMessage::PickSliderTrackColor | InterfaceMessage::PickPageLowColor | InterfaceMessage::PickColorBordersColor | InterfaceMessage::PickNormalColor | InterfaceMessage::PickPaginatorSidebarColor | InterfaceMessage::PickPrimaryHighlightColor | InterfaceMessage::PickPaginatorTabLabelColor | InterfaceMessage::PickToggleEnabledColor | InterfaceMessage::PickToggleDisabledColor => {}
+ InterfaceMessage::Refreshed(new) => {
+ let was_mx_hovered = state.tab_margin_spinbox_x.hovered();
+ let was_my_hovered = state.tab_margin_spinbox_y.hovered();
+ let was_px_hovered = state.tab_padding_spinbox_x.hovered();
+ let was_py_hovered = state.tab_padding_spinbox_y.hovered();
*state = new;
+ state.tab_margin_spinbox_x.set_hovered(was_mx_hovered);
+ state.tab_margin_spinbox_y.set_hovered(was_my_hovered);
+ state.tab_padding_spinbox_x.set_hovered(was_px_hovered);
+ state.tab_padding_spinbox_y.set_hovered(was_py_hovered);
}
}
}
@@ -466,7 +629,7 @@ mod tests {
#[test]
fn test_parse_color_from_key() {
- let content = "\n[layout]\nlow_color = \"#112233\"\nhigh_color = \"#445566\"\ndisabled_color = \"#778899\"\nstatus_separator_color = \"#aabbcc\"\nvisual_guides_color = \"#ddeeff\"\nslider_track_color = \"#123456\"\npage_low_color = \"#474751\"\ncolor_borders_color = \"#abcdef\"\nstatus_normal_color = \"#ccccd8\"\npaginator_sidebar_color = \"#5a5a65\"\nprimary_highlight_color = \"#ffffff\"\npaginator_tab_label_color = \"#e6e6f2\"\n";
+ let content = "\n[layout]\nlow_color = \"#112233\"\nhigh_color = \"#445566\"\ndisabled_color = \"#778899\"\nstatus_separator_color = \"#aabbcc\"\nvisual_guides_color = \"#ddeeff\"\nslider_track_color = \"#123456\"\npage_low_color = \"#474751\"\ncolor_borders_color = \"#abcdef\"\nstatus_normal_color = \"#ccccd8\"\npaginator_sidebar_color = \"#5a5a65\"\nprimary_highlight_color = \"#ffffff\"\npaginator_tab_label_color = \"#e6e6f2\"\ntoggle_enabled_color = \"#68d8a5\"\ntoggle_disabled_color = \"#878794\"\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]);
@@ -479,6 +642,8 @@ mod tests {
assert_eq!(parse_color_from_key(content, "paginator_sidebar_color", [0, 0, 0]), [90, 90, 101]);
assert_eq!(parse_color_from_key(content, "primary_highlight_color", [0, 0, 0]), [255, 255, 255]);
assert_eq!(parse_color_from_key(content, "paginator_tab_label_color", [0, 0, 0]), [230, 230, 242]);
+ assert_eq!(parse_color_from_key(content, "toggle_enabled_color", [0, 0, 0]), [104, 216, 165]);
+ assert_eq!(parse_color_from_key(content, "toggle_disabled_color", [0, 0, 0]), [135, 135, 148]);
assert_eq!(parse_color_from_key(content, "non_existent", [1, 2, 3]), [1, 2, 3]);
}
diff --git a/src/pages/layout.rs b/src/pages/layout.rs
index ff12bdd..c38c5b6 100644
--- a/src/pages/layout.rs
+++ b/src/pages/layout.rs
@@ -2,8 +2,8 @@ use std::fs;
use std::io::Write;
use crate::app::PageContent;
-use clear_ui::layout::Section;
-use clear_ui::widget::{Dropdown, Spinbox};
+use clear_ui::layout::{render_widget, Section, PageLayoutBuilder, LayoutStrategy};
+use clear_ui::widget::{Spinbox, Dropdown};
const CONFIG_PATH: &str = "/home/lsgalante/.config/ccec/config.toml";
@@ -504,643 +504,220 @@ fn get_short_app_name(app_id: &str) -> String {
}
}
-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;
+struct SimNode {
+ x: f32,
+ y: f32,
+ w: f32,
+ h: f32,
+ label: String,
+}
+
+pub fn view(state: &mut LayoutState, cx: f32, cy: f32, cw: f32, ch: f32, sec_focused: &[bool], layout: &mut dyn LayoutStrategy) -> PageContent {
+ let mut final_pc = PageContent::new();
+ let sec_w = 320.0f32;
+ let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(5);
// Current Layout Section (Read-only visual preview)
- let mut sec_cl = Section::new(&mut pc, cx, y, cw, "Current Layout");
- sec_cl.spacing(8.0);
-
- let info = read_current_layout_status();
-
- let card_w = (cw - 24.0) / 2.0;
- let card_h = 135.0;
-
- for tag_idx in 0..4 {
- let col = tag_idx % 2;
- let row = tag_idx / 2;
- let tx = cx + 8.0 + col as f32 * (card_w + 8.0);
- let ty = sec_cl.ay() + row as f32 * (card_h + 8.0);
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec_cl = Section::new(pc, rx, ry, sec_w, "Current Layout");
+ sec_cl.spacing(8.0);
- let is_active = (info.active_tags & (1 << tag_idx)) != 0;
- let is_focused = (info.focused_tags & (1 << tag_idx)) != 0;
+ let info = read_current_layout_status();
- // Draw card border and background
- let border_color = if is_focused {
- [0.2, 0.6, 1.0, 1.0]
- } else if is_active {
- [0.28, 0.28, 0.32, 1.0]
- } else {
- [0.16, 0.16, 0.18, 1.0]
- };
+ let card_w = (sec_w - 24.0) / 2.0;
+ let card_h = 135.0;
- let bg_color = if is_active {
- [0.08, 0.08, 0.11, 0.9]
- } else {
- [0.05, 0.05, 0.07, 0.9]
- };
-
- pc.rect(border_color, tx, ty, card_w, card_h);
- pc.rect(bg_color, tx + 1.0, ty + 1.0, card_w - 2.0, card_h - 2.0);
-
- // Render tag label in top-left of the card
- let tag_label = format!("T{}", tag_idx + 1);
- let tag_label_color = if is_focused {
- [1.0, 1.0, 1.0, 1.0]
- } else if is_active {
- [0.8, 0.8, 0.85, 1.0]
- } else {
- [0.4, 0.4, 0.45, 1.0]
- };
- pc.text(&tag_label, tx + 8.0, ty + 6.0, 9.5, tag_label_color);
-
- // Render miniature screen preview inside the box (on the left side)
- let px = tx + 8.0;
- let py = ty + 24.0;
- let p_w = 70.0;
- let p_h = 44.0;
-
- // Screen background
- pc.rect([0.04, 0.04, 0.06, 1.0], px, py, p_w, p_h);
- pc.rect([0.16, 0.16, 0.18, 1.0], px, py, p_w, 1.0); // Top border
- pc.rect([0.16, 0.16, 0.18, 1.0], px, py + p_h - 1.0, p_w, 1.0); // Bottom border
- pc.rect([0.16, 0.16, 0.18, 1.0], px, py, 1.0, p_h); // Left border
- pc.rect([0.16, 0.16, 0.18, 1.0], px + p_w - 1.0, py, 1.0, p_h); // Right border
-
- // Find windows for this tag
- let tag_windows: Vec<&PreviewWindow> = info.windows.iter()
- .filter(|w| w.app_id != "clear-status-interface" && ((w.tags & (1 << tag_idx)) != 0 || w.tags == u32::MAX))
- .collect();
+ for tag_idx in 0..4 {
+ let col = tag_idx % 2;
+ let row = tag_idx / 2;
+ let tx = rx + 8.0 + col as f32 * (card_w + 8.0);
+ let ty = sec_cl.ay() + row as f32 * (card_h + 8.0);
- // Reference screen size
- let screen_w = 1920.0;
- let screen_h = 1200.0;
- let scale_x = p_w / screen_w;
- let scale_y = p_h / screen_h;
-
- for win in &tag_windows {
- let wx = (px + win.x * scale_x).max(px).min(px + p_w);
- let wy = (py + win.y * scale_y).max(py).min(py + p_h);
- let ww = (win.w * scale_x).min(p_w - (wx - px));
- let wh = (win.h * scale_y).min(p_h - (wy - py));
+ // Draw card background
+ let is_active = (info.active_tags & (1 << tag_idx)) != 0;
+ let bg_col = if is_active { [0.12, 0.24, 0.14, 0.55] } else { [0.08, 0.08, 0.12, 0.35] };
+ let border_col = if is_active { [0.36, 0.56, 0.38, 0.95] } else { [0.24, 0.24, 0.28, 0.45] };
+ pc.rect(bg_col, tx, ty, card_w, card_h);
+ // Card border
+ pc.rect(border_col, tx, ty, card_w, 1.0);
+ pc.rect(border_col, tx, ty + card_h - 1.0, card_w, 1.0);
+ pc.rect(border_col, tx, ty, 1.0, card_h);
+ pc.rect(border_col, tx + card_w - 1.0, ty, 1.0, card_h);
- let is_win_focused = !win.title.is_empty() && win.title == info.focused_title;
- let color = if is_win_focused {
- [0.2, 0.6, 1.0, 1.0]
- } else {
- [0.4, 0.4, 0.45, 1.0]
- };
- let fill = if is_win_focused {
- [0.10, 0.32, 0.55, 0.4]
- } else {
- [0.12, 0.12, 0.15, 0.4]
- };
+ // Tag index text
+ pc.text(&format!("TAG {}", tag_idx + 1), tx + 8.0, ty + 8.0, 10.0, [0.55, 0.55, 0.60, 1.0]);
- pc.rect(color, wx, wy, ww, wh);
- pc.rect(fill, wx + 0.5, wy + 0.5, ww - 1.0, wh - 1.0);
- }
-
- // Determine Tag layout mode
- let layout_name = if is_focused {
- info.focused_layout_mode.clone()
- } else if let Some(win) = tag_windows.first() {
- win.layout_mode.clone()
- } else {
+ // Layout Name
let layout_idx = state.tag_layout_menus.get(tag_idx).map(|m| m.selected).unwrap_or(0);
- let layout_modes = ["Cascade", "Grid", "Fullscreen", "Floating", "Popup"];
- layout_modes.get(layout_idx).copied().unwrap_or("Cascade").to_string()
- };
-
- // Render Visual Focus Hierarchy Tree on the right side of the card
- let tx_tree = tx + 84.0;
- let ty_tree = ty + 18.0;
- let t_w = card_w - 92.0;
- let t_h = card_h - 26.0;
-
- struct TreeNode {
- label: String,
- is_focused: bool,
- is_layout: bool,
- is_role: bool,
- x: f32,
- y: f32,
- w: f32,
- h: f32,
- }
-
- struct TreeLine {
- x0: f32,
- y0: f32,
- x1: f32,
- y1: f32,
- is_dotted: bool,
- }
-
- let mut nodes = Vec::new();
- let mut lines = Vec::new();
-
- // 1. Root Node: Layout mode
- let layout_label = match layout_name.as_str() {
- "Grid" => "GRID",
- "Cascade" => "CASC",
- "Fullscreen" => "FULL",
- "Floating" => "FLOT",
- "Popup" => "POP",
- _ => "CASC",
- };
- nodes.push(TreeNode {
- label: layout_label.to_string(),
- is_focused: false,
- is_layout: true,
- is_role: false,
- x: tx_tree + 2.0,
- y: ty_tree + (t_h - 14.0) / 2.0,
- w: 28.0,
- h: 14.0,
- });
-
- // 2. Window hierarchy
- if tag_windows.is_empty() {
- nodes.push(TreeNode {
- label: "(empty)".to_string(),
- is_focused: false,
- is_layout: false,
- is_role: true,
- x: tx_tree + 46.0,
- y: ty_tree + (t_h - 12.0) / 2.0,
- w: 42.0,
- h: 12.0,
- });
- lines.push(TreeLine {
- x0: tx_tree + 30.0,
- y0: ty_tree + t_h / 2.0,
- x1: tx_tree + 46.0,
- y1: ty_tree + t_h / 2.0,
- is_dotted: true,
- });
- } else {
- // Group transient/child windows under their parent
- struct WinNode {
- win: PreviewWindow,
- children: Vec<PreviewWindow>,
- }
- let mut groups: Vec<WinNode> = Vec::new();
- for w in &tag_windows {
- if w.has_parent && !groups.is_empty() {
- groups.last_mut().unwrap().children.push((*w).clone());
- } else {
- groups.push(WinNode {
- win: (*w).clone(),
- children: Vec::new(),
- });
- }
- }
+ let layout_name = match layout_idx {
+ 1 => "Cascade",
+ 2 => "Stack",
+ 3 => "Grid",
+ 4 => "L-Tiled",
+ 5 => "R-Tiled",
+ 6 => "Equal",
+ 7 => "Spiral",
+ 8 => "Floating",
+ _ => "Fullscreen",
+ };
+ pc.text(layout_name, tx + 8.0, ty + 20.0, 13.0, [0.90, 0.90, 0.95, 1.0]);
- let num_roots = groups.len();
- if num_roots == 1 {
- // One root group: Layout -> Root -> Children
- let root_g = &groups[0];
- let rx = tx_tree + 44.0;
- let ry = ty_tree + (t_h - 14.0) / 2.0;
- let is_root_focused = !root_g.win.title.is_empty() && root_g.win.title == info.focused_title;
- nodes.push(TreeNode {
- label: get_short_app_name(&root_g.win.app_id),
- is_focused: is_root_focused,
- is_layout: false,
- is_role: false,
- x: rx,
- y: ry,
- w: 42.0,
- h: 14.0,
- });
- lines.push(TreeLine {
- x0: tx_tree + 30.0,
- y0: ty_tree + t_h / 2.0,
- x1: rx,
- y1: ty_tree + t_h / 2.0,
- is_dotted: false,
- });
-
- let child_count = root_g.children.len();
- for (ci, child) in root_g.children.iter().enumerate() {
- let cx = tx_tree + 104.0;
- let cy = if child_count == 1 {
- ty_tree + (t_h - 12.0) / 2.0
- } else {
- ty_tree + 6.0 + (ci as f32 / (child_count - 1) as f32) * (t_h - 24.0)
- };
- let is_child_focused = !child.title.is_empty() && child.title == info.focused_title;
- nodes.push(TreeNode {
- label: get_short_app_name(&child.app_id),
- is_focused: is_child_focused,
- is_layout: false,
- is_role: false,
- x: cx,
- y: cy,
- w: 32.0,
- h: 12.0,
- });
- lines.push(TreeLine {
- x0: rx + 42.0,
- y0: ry + 7.0,
- x1: cx,
- y1: cy + 6.0,
- is_dotted: true,
- });
+ // Visual nodes layout preview inside card
+ let preview_x = tx + 8.0;
+ let preview_y = ty + 38.0;
+ let preview_w = card_w - 16.0;
+ let preview_h = card_h - 46.0;
+
+ // Gray border for preview box
+ pc.rect([0.16, 0.16, 0.20, 0.6], preview_x, preview_y, preview_w, preview_h);
+ pc.rect([0.22, 0.22, 0.26, 0.8], preview_x, preview_y, preview_w, 1.0);
+ pc.rect([0.22, 0.22, 0.26, 0.8], preview_x, preview_y + preview_h - 1.0, preview_w, 1.0);
+ pc.rect([0.22, 0.22, 0.26, 0.8], preview_x, preview_y, 1.0, preview_h);
+ pc.rect([0.22, 0.22, 0.26, 0.8], preview_x + preview_w - 1.0, preview_y, 1.0, preview_h);
+
+ // Simulate layout windows preview
+ let mut nodes = Vec::new();
+ match layout_idx {
+ 0 => { // Fullscreen
+ nodes.push(SimNode { x: 2.0, y: 2.0, w: preview_w - 4.0, h: preview_h - 4.0, label: "F".to_string() });
}
- } else if num_roots == 2 {
- // Two root groups (Master & Stack): C -> M & S -> Windows -> Children
- let m_y = ty_tree + (t_h / 2.0) - 20.0;
- let s_y = ty_tree + (t_h / 2.0) + 20.0;
-
- // M indicator
- nodes.push(TreeNode {
- label: "M".to_string(),
- is_focused: false,
- is_layout: false,
- is_role: true,
- x: tx_tree + 44.0,
- y: m_y - 6.0,
- w: 12.0,
- h: 12.0,
- });
- lines.push(TreeLine {
- x0: tx_tree + 30.0,
- y0: ty_tree + t_h / 2.0,
- x1: tx_tree + 44.0,
- y1: m_y,
- is_dotted: false,
- });
-
- // Master Window
- let m_win = &groups[0];
- let is_m_focused = !m_win.win.title.is_empty() && m_win.win.title == info.focused_title;
- nodes.push(TreeNode {
- label: get_short_app_name(&m_win.win.app_id),
- is_focused: is_m_focused,
- is_layout: false,
- is_role: false,
- x: tx_tree + 68.0,
- y: m_y - 7.0,
- w: 36.0,
- h: 14.0,
- });
- lines.push(TreeLine {
- x0: tx_tree + 56.0,
- y0: m_y,
- x1: tx_tree + 68.0,
- y1: m_y,
- is_dotted: false,
- });
-
- // S indicator
- nodes.push(TreeNode {
- label: "S".to_string(),
- is_focused: false,
- is_layout: false,
- is_role: true,
- x: tx_tree + 44.0,
- y: s_y - 6.0,
- w: 12.0,
- h: 12.0,
- });
- lines.push(TreeLine {
- x0: tx_tree + 30.0,
- y0: ty_tree + t_h / 2.0,
- x1: tx_tree + 44.0,
- y1: s_y,
- is_dotted: false,
- });
-
- // Stack Window
- let s_win = &groups[1];
- let is_s_focused = !s_win.win.title.is_empty() && s_win.win.title == info.focused_title;
- nodes.push(TreeNode {
- label: get_short_app_name(&s_win.win.app_id),
- is_focused: is_s_focused,
- is_layout: false,
- is_role: false,
- x: tx_tree + 68.0,
- y: s_y - 7.0,
- w: 36.0,
- h: 14.0,
- });
- lines.push(TreeLine {
- x0: tx_tree + 56.0,
- y0: s_y,
- x1: tx_tree + 68.0,
- y1: s_y,
- is_dotted: false,
- });
-
- // Master children
- let m_child_count = m_win.children.len();
- for (ci, child) in m_win.children.iter().enumerate() {
- let cx = tx_tree + 114.0;
- let cy = if m_child_count == 1 {
- m_y - 6.0
- } else {
- m_y - 20.0 + (ci as f32 / (m_child_count - 1) as f32) * 30.0
- };
- let is_child_focused = !child.title.is_empty() && child.title == info.focused_title;
- nodes.push(TreeNode {
- label: get_short_app_name(&child.app_id),
- is_focused: is_child_focused,
- is_layout: false,
- is_role: false,
- x: cx,
- y: cy,
- w: 28.0,
- h: 11.0,
- });
- lines.push(TreeLine {
- x0: tx_tree + 104.0,
- y0: m_y,
- x1: cx,
- y1: cy + 5.5,
- is_dotted: true,
- });
+ 1 => { // Cascade
+ nodes.push(SimNode { x: 2.0, y: 2.0, w: preview_w - 12.0, h: preview_h - 12.0, label: "1".to_string() });
+ nodes.push(SimNode { x: 6.0, y: 6.0, w: preview_w - 12.0, h: preview_h - 12.0, label: "2".to_string() });
+ nodes.push(SimNode { x: 10.0, y: 10.0, w: preview_w - 12.0, h: preview_h - 12.0, label: "3".to_string() });
}
-
- // Stack children
- let s_child_count = s_win.children.len();
- for (ci, child) in s_win.children.iter().enumerate() {
- let cx = tx_tree + 114.0;
- let cy = if s_child_count == 1 {
- s_y - 6.0
- } else {
- s_y - 20.0 + (ci as f32 / (s_child_count - 1) as f32) * 30.0
- };
- let is_child_focused = !child.title.is_empty() && child.title == info.focused_title;
- nodes.push(TreeNode {
- label: get_short_app_name(&child.app_id),
- is_focused: is_child_focused,
- is_layout: false,
- is_role: false,
- x: cx,
- y: cy,
- w: 28.0,
- h: 11.0,
- });
- lines.push(TreeLine {
- x0: tx_tree + 104.0,
- y0: s_y,
- x1: cx,
- y1: cy + 5.5,
- is_dotted: true,
- });
+ 2 => { // Stack
+ nodes.push(SimNode { x: 2.0, y: 2.0, w: preview_w - 4.0, h: preview_h - 4.0, label: "Stack".to_string() });
}
- } else {
- // More than 2 roots: Master & Stacks (list of stack items)
- let m_y = ty_tree + (t_h / 2.0) - 24.0;
- let s_y = ty_tree + (t_h / 2.0) + 20.0;
+ 3 => { // Grid
+ let hw = (preview_w - 6.0) / 2.0;
+ let hh = (preview_h - 6.0) / 2.0;
+ nodes.push(SimNode { x: 2.0, y: 2.0, w: hw, h: hh, label: "1".to_string() });
+ nodes.push(SimNode { x: 4.0 + hw, y: 2.0, w: hw, h: hh, label: "2".to_string() });
+ nodes.push(SimNode { x: 2.0, y: 4.0 + hh, w: hw, h: hh, label: "3".to_string() });
+ nodes.push(SimNode { x: 4.0 + hw, y: 4.0 + hh, w: hw, h: hh, label: "4".to_string() });
+ }
+ 4 => { // Left Tiled (Main window on left, stack on right)
+ let mw = (preview_w - 6.0) * 0.55;
+ let sw = (preview_w - 6.0) - mw;
+ let sh = (preview_h - 6.0) / 2.0;
+ nodes.push(SimNode { x: 2.0, y: 2.0, w: mw, h: preview_h - 4.0, label: "M".to_string() });
+ nodes.push(SimNode { x: 4.0 + mw, y: 2.0, w: sw, h: sh, label: "1".to_string() });
+ nodes.push(SimNode { x: 4.0 + mw, y: 4.0 + sh, w: sw, h: sh, label: "2".to_string() });
+ }
+ 5 => { // Right Tiled (Main window on right, stack on left)
+ let mw = (preview_w - 6.0) * 0.55;
+ let sw = (preview_w - 6.0) - mw;
+ let sh = (preview_h - 6.0) / 2.0;
+ nodes.push(SimNode { x: 2.0, y: 2.0, w: sw, h: sh, label: "1".to_string() });
+ nodes.push(SimNode { x: 2.0, y: 4.0 + sh, w: sw, h: sh, label: "2".to_string() });
+ nodes.push(SimNode { x: 4.0 + sw, y: 2.0, w: mw, h: preview_h - 4.0, label: "M".to_string() });
+ }
+ 6 => { // Equal (Split evenly horizontally)
+ let ew = (preview_w - 8.0) / 3.0;
+ nodes.push(SimNode { x: 2.0, y: 2.0, w: ew, h: preview_h - 4.0, label: "1".to_string() });
+ nodes.push(SimNode { x: 4.0 + ew, y: 2.0, w: ew, h: preview_h - 4.0, label: "2".to_string() });
+ nodes.push(SimNode { x: 6.0 + 2.0 * ew, y: 2.0, w: ew, h: preview_h - 4.0, label: "3".to_string() });
+ }
+ 7 => { // Spiral (Fibonacci layout)
+ let w1 = (preview_w - 6.0) * 0.5;
+ let w2 = (preview_w - 6.0) - w1;
+ let h2 = (preview_h - 6.0) * 0.5;
+ nodes.push(SimNode { x: 2.0, y: 2.0, w: w1, h: preview_h - 4.0, label: "1".to_string() });
+ nodes.push(SimNode { x: 4.0 + w1, y: 2.0, w: w2, h: h2, label: "2".to_string() });
+ nodes.push(SimNode { x: 4.0 + w1, y: 4.0 + h2, w: w2 * 0.5, h: h2, label: "3".to_string() });
+ nodes.push(SimNode { x: 4.0 + w1 + w2 * 0.5, y: 4.0 + h2, w: w2 * 0.5, h: h2, label: "4".to_string() });
+ }
+ 8 => { // Floating (Scatter windows randomly)
+ nodes.push(SimNode { x: 4.0, y: 6.0, w: preview_w * 0.45, h: preview_h * 0.5, label: "1".to_string() });
+ nodes.push(SimNode { x: preview_w * 0.4, y: 12.0, w: preview_w * 0.5, h: preview_h * 0.45, label: "2".to_string() });
+ nodes.push(SimNode { x: 8.0, y: preview_h * 0.4, w: preview_w * 0.55, h: preview_h * 0.5, label: "3".to_string() });
+ }
+ _ => {}
+ }
+
+ // Draw simulated layout preview rectangles
+ for node in nodes {
+ let rect_x = preview_x + node.x;
+ let rect_y = preview_y + node.y;
- // M indicator
- nodes.push(TreeNode {
- label: "M".to_string(),
- is_focused: false,
- is_layout: false,
- is_role: true,
- x: tx_tree + 44.0,
- y: m_y - 6.0,
- w: 12.0,
- h: 12.0,
- });
- lines.push(TreeLine {
- x0: tx_tree + 30.0,
- y0: ty_tree + t_h / 2.0,
- x1: tx_tree + 44.0,
- y1: m_y,
- is_dotted: false,
- });
+ // Semi-transparent blue for node backgrounds, slightly highlighted if active tag
+ let node_bg = if is_active { [0.30, 0.45, 0.65, 0.45] } else { [0.20, 0.24, 0.30, 0.25] };
+ let node_border = if is_active { [0.45, 0.65, 0.90, 0.85] } else { [0.35, 0.40, 0.45, 0.55] };
- // Master Window
- let m_win = &groups[0];
- let is_m_focused = !m_win.win.title.is_empty() && m_win.win.title == info.focused_title;
- nodes.push(TreeNode {
- label: get_short_app_name(&m_win.win.app_id),
- is_focused: is_m_focused,
- is_layout: false,
- is_role: false,
- x: tx_tree + 68.0,
- y: m_y - 7.0,
- w: 36.0,
- h: 14.0,
- });
- lines.push(TreeLine {
- x0: tx_tree + 56.0,
- y0: m_y,
- x1: tx_tree + 68.0,
- y1: m_y,
- is_dotted: false,
- });
+ pc.rect(node_bg, rect_x, rect_y, node.w, node.h);
- // S indicator
- nodes.push(TreeNode {
- label: "S".to_string(),
- is_focused: false,
- is_layout: false,
- is_role: true,
- x: tx_tree + 44.0,
- y: s_y - 6.0,
- w: 12.0,
- h: 12.0,
- });
- lines.push(TreeLine {
- x0: tx_tree + 30.0,
- y0: ty_tree + t_h / 2.0,
- x1: tx_tree + 44.0,
- y1: s_y,
- is_dotted: false,
- });
+ // Draw node border lines
+ pc.rect(node_border, rect_x, rect_y, node.w, 1.0);
+ pc.rect(node_border, rect_x, rect_y + node.h - 1.0, node.w, 1.0);
+ pc.rect(node_border, rect_x, rect_y, 1.0, node.h);
+ pc.rect(node_border, rect_x + node.w - 1.0, rect_y, 1.0, node.h);
- // Render first 2 Stack items vertically spaced
- let s1_win = &groups[1];
- let is_s1_focused = !s1_win.win.title.is_empty() && s1_win.win.title == info.focused_title;
- let s1_y = s_y - 14.0;
- nodes.push(TreeNode {
- label: get_short_app_name(&s1_win.win.app_id),
- is_focused: is_s1_focused,
- is_layout: false,
- is_role: false,
- x: tx_tree + 68.0,
- y: s1_y - 7.0,
- w: 36.0,
- h: 14.0,
- });
- lines.push(TreeLine {
- x0: tx_tree + 56.0,
- y0: s_y,
- x1: tx_tree + 68.0,
- y1: s1_y,
- is_dotted: false,
- });
+ // Center the label text inside the simulated node
+ let text_sz = 9.0;
+ let text_w = node.label.len() as f32 * 6.0;
+ let text_color = if is_active { [0.95, 0.95, 1.0, 0.95] } else { [0.70, 0.70, 0.75, 0.75] };
+ let tx_offset = ((node.w - text_w) / 2.0).max(1.0);
+ let ty_offset = ((node.h - text_sz) / 2.0).max(1.0);
- let s2_win = &groups[2];
- let is_s2_focused = !s2_win.win.title.is_empty() && s2_win.win.title == info.focused_title;
- let s2_y = s_y + 14.0;
- nodes.push(TreeNode {
- label: get_short_app_name(&s2_win.win.app_id),
- is_focused: is_s2_focused,
- is_layout: false,
- is_role: false,
- x: tx_tree + 68.0,
- y: s2_y - 7.0,
- w: 36.0,
- h: 14.0,
- });
- lines.push(TreeLine {
- x0: tx_tree + 56.0,
- y0: s_y,
- x1: tx_tree + 68.0,
- y1: s2_y,
- is_dotted: false,
- });
+ pc.text(&node.label, rect_x + tx_offset, rect_y + ty_offset, text_sz, text_color);
}
}
- let line_color = if is_active {
- [0.30, 0.30, 0.35, 0.8]
- } else {
- [0.18, 0.18, 0.20, 0.6]
- };
- let draw_line = |pc: &mut PageContent, x0: f32, y0: f32, x1: f32, y1: f32, is_dotted: bool, color: [f32; 4]| {
- if is_dotted {
- if (x0 - x1).abs() < 0.1 {
- let sy = y0.min(y1);
- let ey = y0.max(y1);
- let mut curr_y = sy;
- while curr_y <= ey {
- pc.rect(color, x0 - 0.5, curr_y, 1.0, 1.0);
- curr_y += 3.0;
- }
- } else if (y0 - y1).abs() < 0.1 {
- let sx = x0.min(x1);
- let ex = x0.max(x1);
- let mut curr_x = sx;
- while curr_x <= ex {
- pc.rect(color, curr_x, y0 - 0.5, 1.0, 1.0);
- curr_x += 3.0;
- }
- } else {
- pc.rect(color, x0.min(x1), y0.min(y1), (x0 - x1).abs().max(1.0), (y0 - y1).abs().max(1.0));
- }
- } else {
- if (x0 - x1).abs() < 0.1 {
- pc.rect(color, x0 - 0.5, y0.min(y1), 1.0, (y0 - y1).abs());
- } else if (y0 - y1).abs() < 0.1 {
- pc.rect(color, x0.min(x1), y0 - 0.5, (x0 - x1).abs(), 1.0);
- } else {
- pc.rect(color, x0.min(x1), y0.min(y1), (x0 - x1).abs().max(1.0), (y0 - y1).abs().max(1.0));
- }
- }
- };
-
- for line in &lines {
- let mid_x = (line.x0 + line.x1) / 2.0;
- draw_line(&mut pc, line.x0, line.y0, mid_x, line.y0, line.is_dotted, line_color);
- draw_line(&mut pc, mid_x, line.y0, mid_x, line.y1, line.is_dotted, line_color);
- draw_line(&mut pc, mid_x, line.y1, line.x1, line.y1, line.is_dotted, line_color);
- }
-
- // Draw nodes
- for node in nodes {
- let border = if node.is_layout {
- [0.32, 0.32, 0.38, 1.0]
- } else if node.is_role {
- [0.20, 0.20, 0.24, 0.8]
- } else if node.is_focused {
- [0.2, 0.6, 1.0, 1.0]
- } else {
- [0.22, 0.22, 0.26, 1.0]
- };
-
- let bg = if node.is_layout {
- [0.14, 0.14, 0.18, 1.0]
- } else if node.is_role {
- [0.09, 0.09, 0.11, 0.9]
- } else if node.is_focused {
- [0.10, 0.32, 0.55, 1.0]
- } else {
- [0.11, 0.11, 0.15, 1.0]
- };
-
- pc.rect(border, node.x, node.y, node.w, node.h);
- pc.rect(bg, node.x + 1.0, node.y + 1.0, node.w - 2.0, node.h - 2.0);
-
- let text_color = if node.is_focused {
- [1.0, 1.0, 1.0, 1.0]
- } else if node.is_role {
- [0.48, 0.48, 0.52, 1.0]
- } else {
- [0.78, 0.78, 0.82, 1.0]
- };
-
- let text_sz = if node.is_layout {
- 7.5
- } else if node.is_role {
- 7.0
- } else {
- 7.0
- };
-
- let char_width = text_sz * 0.52;
- let text_w = node.label.len() as f32 * char_width;
- let tx_offset = ((node.w - text_w) / 2.0).max(1.0);
- let ty_offset = ((node.h - text_sz) / 2.0).max(1.0);
-
- pc.text(&node.label, node.x + tx_offset, node.y + ty_offset, text_sz, text_color);
- }
- }
-
- sec_cl.content_y += 2.0 * (card_h + 8.0) + 4.0;
- y = sec_cl.finish(&mut pc);
+ sec_cl.content_y += 2.0 * (card_h + 8.0) + 4.0;
+ sec_cl.finish(pc)
+ });
// 1. Border Width Section
- let mut sec_bw = Section::new(&mut pc, cx, y, cw, "Border Width");
- sec_bw.spacing(8.0);
- for (i, param) in WidthParam::ALL.iter().enumerate() {
- state.spinboxes[i].set_label(param.label());
- sec_bw.widget(&mut pc, &mut state.spinboxes[i], 14.0, 200.0, 26.0);
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec_bw = Section::new(pc, rx, ry, sec_w, "Border Width");
sec_bw.spacing(8.0);
- }
- y = sec_bw.finish_focused(&mut pc, sec_focused.get(0).copied().unwrap_or(false));
+ for (i, param) in WidthParam::ALL.iter().enumerate() {
+ state.spinboxes[i].set_label(param.label());
+ sec_bw.widget(pc, &mut state.spinboxes[i], 14.0, 200.0, 26.0);
+ sec_bw.spacing(8.0);
+ }
+ sec_bw.finish_focused(pc, sec_focused.first().copied().unwrap_or(false))
+ });
// 2. Cascade Section
- let mut sec_cascade = Section::new(&mut pc, cx, y, cw, "Cascade");
- sec_cascade.spacing(8.0);
- state.cascade_offset_spinbox.set_label("Offset");
- sec_cascade.widget(&mut pc, &mut state.cascade_offset_spinbox, 14.0, 200.0, 26.0);
- sec_cascade.spacing(8.0);
- state.edge_gap_spinbox.set_label("Edge Gap");
- sec_cascade.widget(&mut pc, &mut state.edge_gap_spinbox, 14.0, 200.0, 26.0);
- sec_cascade.spacing(8.0);
- state.top_gap_spinbox.set_label("Top Gap");
- sec_cascade.widget(&mut pc, &mut state.top_gap_spinbox, 14.0, 200.0, 26.0);
- sec_cascade.spacing(8.0);
- y = sec_cascade.finish_focused(&mut pc, sec_focused.get(1).copied().unwrap_or(false));
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec_cascade = Section::new(pc, rx, ry, sec_w, "Cascade");
+ sec_cascade.spacing(8.0);
+ state.cascade_offset_spinbox.set_label("Offset");
+ sec_cascade.widget(pc, &mut state.cascade_offset_spinbox, 14.0, 200.0, 26.0);
+ sec_cascade.spacing(8.0);
+ state.edge_gap_spinbox.set_label("Edge Gap");
+ sec_cascade.widget(pc, &mut state.edge_gap_spinbox, 14.0, 200.0, 26.0);
+ sec_cascade.spacing(8.0);
+ state.top_gap_spinbox.set_label("Top Gap");
+ sec_cascade.widget(pc, &mut state.top_gap_spinbox, 14.0, 200.0, 26.0);
+ sec_cascade.spacing(8.0);
+ sec_cascade.finish_focused(pc, sec_focused.get(1).copied().unwrap_or(false))
+ });
// 3. Movement Section
- let mut movement_sec = Section::new(&mut pc, cx, y, cw, "Movement");
- movement_sec.spacing(8.0);
- state.transition_duration_spinbox.set_label("Duration (ms)");
- movement_sec.widget(&mut pc, &mut state.transition_duration_spinbox, 14.0, 200.0, 26.0);
- movement_sec.spacing(8.0);
- y = movement_sec.finish_focused(&mut pc, sec_focused.get(2).copied().unwrap_or(false));
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut movement_sec = Section::new(pc, rx, ry, sec_w, "Movement");
+ movement_sec.spacing(8.0);
+ state.transition_duration_spinbox.set_label("Duration (ms)");
+ movement_sec.widget(pc, &mut state.transition_duration_spinbox, 14.0, 200.0, 26.0);
+ movement_sec.spacing(8.0);
+ movement_sec.finish_focused(pc, sec_focused.get(2).copied().unwrap_or(false))
+ });
// 4. Default Layouts Section
- let mut default_layouts_sec = Section::new(&mut pc, cx, y, cw, "Default Layouts");
- default_layouts_sec.spacing(8.0);
- for i in 0..4 {
- default_layouts_sec.widget(&mut pc, &mut state.tag_layout_menus[i], 14.0, 200.0, 26.0);
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut default_layouts_sec = Section::new(pc, rx, ry, sec_w, "Default Layouts");
default_layouts_sec.spacing(8.0);
- }
- default_layouts_sec.finish_focused(&mut pc, sec_focused.get(3).copied().unwrap_or(false));
-
-
+ for i in 0..4 {
+ default_layouts_sec.widget(pc, &mut state.tag_layout_menus[i], 14.0, 200.0, 26.0);
+ default_layouts_sec.spacing(8.0);
+ }
+ default_layouts_sec.finish_focused(pc, sec_focused.get(3).copied().unwrap_or(false))
+ });
- pc
+ final_pc
}
fn set_width(state: &mut LayoutState, param: WidthParam, val: u16) {
@@ -1270,4 +847,12 @@ mode = "popup"
let modes = parse_tag_layouts_from_config(content);
assert_eq!(modes, vec!["cascade", "cascade", "cascade", "cascade"]);
}
+
+ #[test]
+ fn test_view_layout_grid() {
+ let mut state = LayoutState::default();
+ let mut layout = clear_ui::layout::ColumnLayout::new(20.0);
+ let pc = view(&mut state, 10.0, 20.0, 800.0, 600.0, &[false, false, false, false], &mut layout);
+ assert!(!pc.rects.is_empty() || !pc.texts.is_empty());
+ }
}
diff --git a/src/pages/mod.rs b/src/pages/mod.rs
index de38e69..6a4f46d 100644
--- a/src/pages/mod.rs
+++ b/src/pages/mod.rs
@@ -12,7 +12,7 @@ pub mod notifications;
pub mod backup;
pub mod typeface;
pub mod services;
-pub mod colors;
+pub mod interface;
pub mod screensaver;
pub mod accounts;
@@ -32,7 +32,7 @@ pub enum Page {
Notifications,
Backup,
Typefaces,
- Colors,
+ Interface,
Screensaver,
}
@@ -41,7 +41,7 @@ impl Page {
Page::Accounts,
Page::Audio,
Page::Backup,
- Page::Colors,
+ Page::Interface,
Page::Display,
Page::Input,
Page::Layout,
@@ -72,7 +72,7 @@ impl Page {
Page::Notifications => "Notifications",
Page::Backup => "Backup",
Page::Typefaces => "Typefaces",
- Page::Colors => "Colors",
+ Page::Interface => "Interface",
Page::Screensaver => "Screensaver",
}
}
diff --git a/src/pages/network.rs b/src/pages/network.rs
index a55db88..798bce4 100644
--- a/src/pages/network.rs
+++ b/src/pages/network.rs
@@ -1,6 +1,6 @@
use crate::app::{AppAction, PageContent};
-use clear_ui::layout::Section;
-use clear_ui::widget::{Widget, TextLabel, ScrollBox, ScrollingList};
+use clear_ui::layout::{Section, PageLayoutBuilder, LayoutStrategy};
+use clear_ui::widget::ScrollingList;
#[derive(Debug, Clone)]
pub struct WifiNetwork {
@@ -27,6 +27,8 @@ pub struct NetworkState {
pub ip_address: String,
pub device: String,
pub available: Vec<WifiNetwork>,
+ pub bt_installed: bool,
+ pub bt_service_active: bool,
pub bt_enabled: bool,
pub bt_devices: Vec<BluetoothDevice>,
pub bt_scanning: bool,
@@ -42,6 +44,8 @@ pub enum NetworkMessage {
BtConnect(String),
BtDisconnect(String),
BtScan,
+ InstallBtTools,
+ StartBtService,
}
pub async fn fetch_network_state() -> NetworkState {
@@ -85,12 +89,13 @@ pub async fn fetch_network_state() -> NetworkState {
}).unwrap_or_default();
let available = if wifi_enabled { fetch_wifi_list().await } else { Vec::new() };
- let (bt_enabled, bt_devices) = fetch_bluetooth_state().await;
+ let (bt_installed, bt_service_active, bt_enabled, bt_devices) = fetch_bluetooth_state().await;
NetworkState {
loaded: true,
wifi_enabled, connected_ssid, signal_strength: signal,
ip_address, device, available,
+ bt_installed, bt_service_active,
bt_enabled, bt_devices, bt_scanning: false,
wifi_list_box: ScrollingList::new(26.0, 4.0),
}
@@ -124,14 +129,35 @@ async fn fetch_wifi_list() -> Vec<WifiNetwork> {
networks
}
-async fn fetch_bluetooth_state() -> (bool, Vec<BluetoothDevice>) {
+async fn fetch_bluetooth_state() -> (bool, bool, bool, Vec<BluetoothDevice>) {
+ let bt_installed = tokio::process::Command::new("bluetoothctl")
+ .arg("--version")
+ .output()
+ .await
+ .is_ok();
+
+ if !bt_installed {
+ return (false, false, false, Vec::new());
+ }
+
+ let bt_service_active = tokio::process::Command::new("systemctl")
+ .args(["is-active", "bluetooth"])
+ .output()
+ .await
+ .map(|o| String::from_utf8_lossy(&o.stdout).trim() == "active")
+ .unwrap_or(false);
+
+ if !bt_service_active {
+ return (true, false, false, Vec::new());
+ }
+
let bt_enabled = tokio::process::Command::new("bluetoothctl")
.args(["show"]).output().await.ok()
.map(|o| String::from_utf8_lossy(&o.stdout).lines().any(|l| l.contains("Powered: yes")))
.unwrap_or(false);
let devices = if bt_enabled { fetch_bt_devices().await } else { Vec::new() };
- (bt_enabled, devices)
+ (true, true, bt_enabled, devices)
}
async fn fetch_bt_devices() -> Vec<BluetoothDevice> {
@@ -148,13 +174,23 @@ async fn fetch_bt_devices() -> Vec<BluetoothDevice> {
let parts: Vec<&str> = rest.splitn(2, ' ').collect();
if parts.len() < 2 || parts[0].is_empty() || parts[1].is_empty() { continue; }
let mac = parts[0].to_string();
- let name = parts[1].to_string();
+ let default_name = parts[1].to_string();
let info = tokio::process::Command::new("bluetoothctl")
.args(["info", &mac]).output().await.ok()
.map(|o| String::from_utf8_lossy(&o.stdout).to_string())
.unwrap_or_default();
+ let info_name = info.lines()
+ .find(|l| l.contains("Name:"))
+ .and_then(|l| l.splitn(2, ':').nth(1).map(|s| s.trim().to_string()));
+
+ let info_alias = info.lines()
+ .find(|l| l.contains("Alias:"))
+ .and_then(|l| l.splitn(2, ':').nth(1).map(|s| s.trim().to_string()));
+
+ let name = info_name.or(info_alias).unwrap_or(default_name);
+
let connected = info.lines().any(|l| l.contains("Connected: yes"));
let icon = info.lines()
.find(|l| l.contains("Icon:"))
@@ -208,104 +244,193 @@ 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: &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;
+pub fn view(state: &mut NetworkState, cx: f32, cy: f32, cw: f32, ch: f32, root_focused: bool, layout: &mut dyn LayoutStrategy) -> PageContent {
+ let mut final_pc = PageContent::new();
+ let sec_w = 320.0f32;
+ let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(2);
// ── WiFi ──
- let mut sec = Section::new(&mut pc, cx, y, cw, "WiFi");
-
- if !state.loaded {
- sec.text(&mut pc, "Loading WiFi interfaces...", 12.0, 0.0, 12.0, TEXT_DIM);
- sec.spacing(18.0);
- } else {
- let yt = sec.ay();
- pc.button(if state.wifi_enabled { "ON" } else { "OFF" },
- sec.ax(cw - 80.0), yt, 60.0, 28.0,
- if state.wifi_enabled { TOGGLE_ON } else { TOGGLE_OFF }, BTN_HOVER, WHITE,
- AppAction::Radios(NetworkMessage::ToggleWifi));
- sec.content_y += 34.0;
-
- if !state.connected_ssid.is_empty() {
- sec.text(&mut pc, &format!("Connected: {}", state.connected_ssid), 14.0, 0.0, 13.0, ACCENT);
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec = Section::new(pc, rx, ry, sec_w, "WiFi");
+
+ if !state.loaded {
+ sec.text(pc, "Loading WiFi interfaces...", 12.0, 0.0, 12.0, TEXT_DIM);
sec.spacing(18.0);
- sec.text(&mut pc, &format!("Signal: {}% IP: {}", state.signal_strength, state.ip_address),
- 14.0, 0.0, 12.0, TEXT_DIM);
- sec.spacing(16.0);
- } else if state.wifi_enabled {
- sec.text(&mut pc, "Not connected", 14.0, 0.0, 12.0, TEXT_DIM);
- sec.spacing(16.0);
- }
+ } else {
+ let yt = sec.ay();
+ let wifi_btn_w = if sec_w < 200.0 { 40.0 } else { 60.0 };
+ let wifi_btn_x = sec_w - wifi_btn_w - 12.0;
+
+ pc.button(if state.wifi_enabled { "ON" } else { "OFF" },
+ sec.ax(wifi_btn_x), yt, wifi_btn_w, 28.0,
+ if state.wifi_enabled { TOGGLE_ON } else { TOGGLE_OFF }, BTN_HOVER, WHITE,
+ AppAction::Radios(NetworkMessage::ToggleWifi));
+ sec.content_y += 34.0;
+
+ if !state.connected_ssid.is_empty() {
+ let ssid_max_chars = ((sec_w - 24.0) / 7.0) as usize;
+ let ssid_truncated = if state.connected_ssid.len() > ssid_max_chars {
+ format!("{}...", &state.connected_ssid[..ssid_max_chars.saturating_sub(3)])
+ } else {
+ state.connected_ssid.clone()
+ };
+ sec.text(pc, &format!("Connected: {}", ssid_truncated), 14.0, 0.0, 13.0, ACCENT);
+ sec.spacing(18.0);
+
+ if sec_w < 220.0 {
+ sec.text(pc, &format!("Signal: {}%", state.signal_strength), 14.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(16.0);
+ if !state.ip_address.is_empty() {
+ sec.text(pc, &format!("IP: {}", state.ip_address), 14.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(16.0);
+ }
+ } else {
+ sec.text(pc, &format!("Signal: {}% IP: {}", state.signal_strength, state.ip_address),
+ 14.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(16.0);
+ }
+ } else if state.wifi_enabled {
+ sec.text(pc, "Not connected", 14.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(16.0);
+ }
- if state.wifi_enabled && !state.available.is_empty() {
- 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())));
+ if state.wifi_enabled && !state.available.is_empty() {
+ let list_box_x = rx + 12.0;
+ let list_box_y = sec.ay();
+ let list_box_w = sec_w - 24.0;
+ let list_box_h = 160.0;
+
+ clear_ui::layout::render_widget(pc, &mut state.wifi_list_box, list_box_x, list_box_y, list_box_w, list_box_h);
+
+ state.wifi_list_box.update_bounds(state.available.len(), list_box_y, list_box_h);
+
+ let btn_w = list_box_w - 24.0;
+ let max_chars = ((btn_w / 6.5) as usize).saturating_sub(10).max(5);
+
+ 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 ssid_truncated = if net.ssid.len() > max_chars {
+ format!("{}...", &net.ssid[..max_chars.saturating_sub(3)])
+ } else {
+ net.ssid.clone()
+ };
+ let label = format!("{} {} ({}%)", prefix, ssid_truncated, net.signal);
+ let active = net.in_use;
+ pc.button(&label, list_box_x + 4.0, draw_y, btn_w, 26.0,
+ if active { ACT_BTN } else { NET_BTN }, BTN_HOVER,
+ if active { ACCENT } else { TEXT_FG },
+ AppAction::Radios(NetworkMessage::ConnectWifi(net.ssid.clone())));
+ }
}
+ sec.content_y += list_box_h + 8.0;
}
- sec.content_y += list_box_h + 8.0;
}
- }
-
- y = sec.finish_focused(&mut pc, root_focused);
+ sec.finish_focused(pc, root_focused)
+ });
// ── Bluetooth ──
- let mut sec = Section::new(&mut pc, cx, y, cw, "Bluetooth");
-
- if !state.loaded {
- sec.text(&mut pc, "Loading Bluetooth status...", 12.0, 0.0, 12.0, TEXT_DIM);
- sec.spacing(18.0);
- } else {
- let yt = sec.ay();
- pc.button(if state.bt_enabled { "ON" } else { "OFF" },
- sec.ax(cw - 140.0), yt, 60.0, 28.0,
- if state.bt_enabled { TOGGLE_ON } else { TOGGLE_OFF }, BTN_HOVER, WHITE,
- AppAction::Radios(NetworkMessage::ToggleBluetooth));
- pc.button("Scan", sec.ax(cw - 72.0), yt, 52.0, 28.0,
- TOGGLE_OFF, BTN_HOVER, WHITE,
- AppAction::Radios(NetworkMessage::BtScan));
- sec.content_y += 34.0;
-
- if state.bt_devices.is_empty() {
- if state.bt_enabled {
- sec.text(&mut pc, "No paired devices found", 14.0, 0.0, 12.0, TEXT_DIM);
- }
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let bt_sec_w = sec_w * 2.0;
+ let mut sec = Section::new(pc, rx, ry, bt_sec_w, "Bluetooth");
+
+ if !state.loaded {
+ sec.text(pc, "Loading Bluetooth status...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(18.0);
+ } else if !state.bt_installed {
+ sec.text(pc, "Bluetooth tools (bluez) not installed", 14.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(18.0);
+ let btn_w = if bt_sec_w < 200.0 { 100.0 } else { 120.0 };
+ let yt = sec.ay();
+ pc.button("Install Tools", rx + 12.0, yt, btn_w, 28.0,
+ TOGGLE_ON, BTN_HOVER, WHITE,
+ AppAction::Radios(NetworkMessage::InstallBtTools));
+ sec.content_y += 34.0;
+ } else if !state.bt_service_active {
+ sec.text(pc, "Bluetooth service is stopped", 14.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(18.0);
+ let btn_w = if bt_sec_w < 200.0 { 100.0 } else { 120.0 };
+ let yt = sec.ay();
+ pc.button("Start Service", rx + 12.0, yt, btn_w, 28.0,
+ TOGGLE_ON, BTN_HOVER, WHITE,
+ AppAction::Radios(NetworkMessage::StartBtService));
+ sec.content_y += 34.0;
} else {
- for dev in &state.bt_devices {
- let status = if dev.connected { ">" } else { " " };
- let label = format!("{} {} ({})", status, dev.name, dev.mac);
- let action_label = if dev.connected { "Disconnect" } else { "Connect" };
- let yt = sec.ay();
- sec.text(&mut pc, &label, 14.0, 0.0, 12.0, if dev.connected { ACCENT } else { TEXT_FG });
- pc.button(action_label, sec.ax(cw - 90.0), yt - 2.0, 70.0, 22.0,
- if dev.connected { TOGGLE_OFF } else { TOGGLE_ON }, BTN_HOVER, WHITE,
- if dev.connected {
- AppAction::Radios(NetworkMessage::BtDisconnect(dev.mac.clone()))
+ let yt = sec.ay();
+ let bt_btn_w = if bt_sec_w < 200.0 { 40.0 } else { 60.0 };
+ let scan_btn_w = if bt_sec_w < 200.0 { 40.0 } else { 52.0 };
+ let bt_btn_x = bt_sec_w - bt_btn_w - scan_btn_w - 20.0;
+ let scan_btn_x = bt_sec_w - scan_btn_w - 12.0;
+
+ pc.button(if state.bt_enabled { "ON" } else { "OFF" },
+ sec.ax(bt_btn_x), yt, bt_btn_w, 28.0,
+ if state.bt_enabled { TOGGLE_ON } else { TOGGLE_OFF }, BTN_HOVER, WHITE,
+ AppAction::Radios(NetworkMessage::ToggleBluetooth));
+ pc.button("Scan", sec.ax(scan_btn_x), yt, scan_btn_w, 28.0,
+ TOGGLE_OFF, BTN_HOVER, WHITE,
+ AppAction::Radios(NetworkMessage::BtScan));
+ sec.content_y += 34.0;
+
+ if state.bt_devices.is_empty() {
+ if state.bt_enabled {
+ let no_devices_msg = if bt_sec_w < 200.0 { "No paired devices" } else { "No paired devices found" };
+ sec.text(pc, no_devices_msg, 14.0, 0.0, 12.0, TEXT_DIM);
+ }
+ } else {
+ for dev in &state.bt_devices {
+ let status = if dev.connected { ">" } else { " " };
+ let btn_w = if bt_sec_w < 250.0 { 42.0 } else { 70.0 };
+ let action_label = if dev.connected {
+ if bt_sec_w < 250.0 { "Disc" } else { "Disconnect" }
+ } else {
+ if bt_sec_w < 250.0 { "Conn" } else { "Connect" }
+ };
+
+ let label_max_w = (bt_sec_w - btn_w - 20.0 - 14.0 - 8.0).max(20.0);
+ let label_max_chars = ((label_max_w / 6.0) as usize).max(5);
+
+ let is_unknown = dev.name.replace('-', ":").eq_ignore_ascii_case(&dev.mac);
+ let label = if is_unknown {
+ if bt_sec_w < 350.0 {
+ format!("{} {}", status, dev.mac)
+ } else {
+ format!("{} Unknown Device ({})", status, dev.mac)
+ }
} else {
- AppAction::Radios(NetworkMessage::BtConnect(dev.mac.clone()))
- });
- sec.content_y += 24.0;
+ if bt_sec_w < 350.0 {
+ let name_truncated = if dev.name.len() > label_max_chars {
+ format!("{}...", &dev.name[..label_max_chars.saturating_sub(3)])
+ } else {
+ dev.name.clone()
+ };
+ format!("{} {}", status, name_truncated)
+ } else {
+ let full_label = format!("{} {} ({})", status, dev.name, dev.mac);
+ if full_label.len() > label_max_chars {
+ format!("{}...", &full_label[..label_max_chars.saturating_sub(3)])
+ } else {
+ full_label
+ }
+ }
+ };
+
+ let yt = sec.ay();
+ sec.text(pc, &label, 14.0, 0.0, 12.0, if dev.connected { ACCENT } else { TEXT_FG });
+ pc.button(action_label, sec.ax(bt_sec_w - btn_w - 20.0), yt - 2.0, btn_w, 22.0,
+ if dev.connected { TOGGLE_OFF } else { TOGGLE_ON }, BTN_HOVER, WHITE,
+ if dev.connected {
+ AppAction::Radios(NetworkMessage::BtDisconnect(dev.mac.clone()))
+ } else {
+ AppAction::Radios(NetworkMessage::BtConnect(dev.mac.clone()))
+ });
+ sec.content_y += 24.0;
+ }
}
}
- }
- sec.finish(&mut pc);
+ sec.finish(pc)
+ });
- pc
+ final_pc
}
pub fn update(state: &mut NetworkState, msg: NetworkMessage) {
@@ -327,5 +452,28 @@ pub fn update(state: &mut NetworkState, msg: NetworkMessage) {
NetworkMessage::BtConnect(mac) => { bt_connect(&mac); }
NetworkMessage::BtDisconnect(mac) => { bt_disconnect(&mac); }
NetworkMessage::BtScan => { bt_scan(); }
+ NetworkMessage::InstallBtTools => {
+ let _ = tokio::process::Command::new("pkexec")
+ .args(["sh", "-c", "pacman -S --noconfirm bluez bluez-utils && systemctl enable --now bluetooth"])
+ .spawn();
+ }
+ NetworkMessage::StartBtService => {
+ let _ = tokio::process::Command::new("pkexec")
+ .args(["systemctl", "enable", "--now", "bluetooth"])
+ .spawn();
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_view_layout_grid() {
+ let mut state = NetworkState::default();
+ let mut layout = clear_ui::layout::ColumnLayout::new(20.0);
+ let pc = view(&mut state, 10.0, 20.0, 800.0, 600.0, false, &mut layout);
+ assert!(!pc.rects.is_empty() || !pc.texts.is_empty() || !pc.buttons.is_empty());
}
}
diff --git a/src/pages/notifications.rs b/src/pages/notifications.rs
index 531d766..cc70ceb 100644
--- a/src/pages/notifications.rs
+++ b/src/pages/notifications.rs
@@ -2,7 +2,7 @@ use std::fs;
use std::io::Write;
use crate::app::{AppAction, PageContent};
-use clear_ui::layout::Section;
+use clear_ui::layout::{Section, PageLayoutBuilder, LayoutStrategy};
use clear_ui::widget::{Toggle, Spinbox, Slider};
const CONFIG_PATH: &str = "/home/lsgalante/.config/ccec/config.toml";
@@ -308,53 +308,60 @@ fn write_transparency_config_value(key: &str, value: &str) {
let _ = fs::write(CONFIG_PATH, updated);
}
-pub fn view(state: &mut NotificationsState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
- let mut pc = PageContent::new();
- let y = cy + 12.0;
-
- let mut sec = Section::new(&mut pc, cx, y, cw, "System Notifications");
-
- let toggle_w = 48.0;
- let toggle_h = 24.0;
- state.enable_toggle.set_toggled(state.enable);
- sec.widget(&mut pc, &mut state.enable_toggle, 14.0, toggle_w, toggle_h);
- sec.spacing(8.0);
-
- state.bell_toggle.set_toggled(state.bell);
- sec.widget(&mut pc, &mut state.bell_toggle, 14.0, toggle_w, toggle_h);
- sec.spacing(16.0);
-
- state.duration_spinbox.value = state.duration;
- state.duration_spinbox.set_label("Notification Duration");
- sec.widget(&mut pc, &mut state.duration_spinbox, 14.0, 200.0, 26.0);
- sec.spacing(16.0);
-
- let btn_w = 160.0;
- let btn_h = 32.0;
- let btn_y = sec.ay();
- sec.row(1, 0.0, btn_h, |_, x, _| {
- pc.button(
- "Send Test Notification",
- x,
- btn_y,
- btn_w,
- btn_h,
- BTN_BG,
- BTN_HOVER,
- WHITE,
- AppAction::Notifications(NotificationsMessage::SendTestNotification),
- );
+pub fn view(state: &mut NotificationsState, cx: f32, cy: f32, cw: f32, ch: f32, layout: &mut dyn LayoutStrategy) -> PageContent {
+ let mut final_pc = PageContent::new();
+ let sec_w = 320.0f32;
+ let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(2);
+
+ // ── System Notifications ──
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec = Section::new(pc, rx, ry, sec_w, "System Notifications");
+
+ let toggle_w = 48.0;
+ let toggle_h = 24.0;
+ state.enable_toggle.set_toggled(state.enable);
+ sec.widget(pc, &mut state.enable_toggle, 14.0, toggle_w, toggle_h);
+ sec.spacing(8.0);
+
+ state.bell_toggle.set_toggled(state.bell);
+ sec.widget(pc, &mut state.bell_toggle, 14.0, toggle_w, toggle_h);
+ sec.spacing(16.0);
+
+ state.duration_spinbox.value = state.duration;
+ state.duration_spinbox.set_label("Notification Duration");
+ sec.widget(pc, &mut state.duration_spinbox, 14.0, 200.0, 26.0);
+ sec.spacing(16.0);
+
+ let btn_w = 160.0;
+ let btn_h = 32.0;
+ let btn_y = sec.ay();
+ sec.row(1, 0.0, btn_h, |_, x, _| {
+ pc.button(
+ "Send Test Notification",
+ x,
+ btn_y,
+ btn_w,
+ btn_h,
+ BTN_BG,
+ BTN_HOVER,
+ WHITE,
+ AppAction::Notifications(NotificationsMessage::SendTestNotification),
+ );
+ });
+ sec.spacing(12.0);
+ sec.finish(pc)
});
- sec.spacing(12.0);
- sec.finish(&mut pc);
- let mut sec2 = Section::new(&mut pc, cx, sec.ay() + 24.0, cw, "Transparency");
- state.opacity_slider.set_value(state.opacity);
- sec2.widget(&mut pc, &mut state.opacity_slider, 14.0, 300.0, 20.0);
- sec2.spacing(12.0);
- sec2.finish(&mut pc);
+ // ── Transparency ──
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec2 = Section::new(pc, rx, ry, sec_w, "Transparency");
+ state.opacity_slider.set_value(state.opacity);
+ sec2.widget(pc, &mut state.opacity_slider, 14.0, 300.0, 20.0);
+ sec2.spacing(12.0);
+ sec2.finish(pc)
+ });
- pc
+ final_pc
}
pub fn update(state: &mut NotificationsState, msg: NotificationsMessage) {
@@ -451,4 +458,13 @@ duration = 10
";
assert_eq!(parse_notifications_duration(content), 10);
}
+
+ #[test]
+ fn test_view_layout_grid() {
+ let mut state = NotificationsState::default();
+ let mut layout = clear_ui::layout::ColumnLayout::new(20.0);
+ let pc = view(&mut state, 10.0, 20.0, 800.0, 600.0, &mut layout);
+ assert!(!pc.rects.is_empty() || !pc.texts.is_empty());
+ }
}
+
diff --git a/src/pages/screensaver.rs b/src/pages/screensaver.rs
index 6f14b46..9f17ef5 100644
--- a/src/pages/screensaver.rs
+++ b/src/pages/screensaver.rs
@@ -1,6 +1,6 @@
use std::fs;
use crate::app::{AppAction, PageContent};
-use clear_ui::layout::Section;
+use clear_ui::layout::{Section, PageLayoutBuilder, LayoutStrategy};
use clear_ui::widget::{Toggle, Spinbox, Dropdown};
const CONFIG_PATH: &str = "/home/lsgalante/.config/ccec/config.toml";
@@ -230,50 +230,53 @@ const BTN_BG: [f32; 4] = [0.20, 0.40, 0.65, 1.0];
const BTN_HOVER: [f32; 4] = [0.28, 0.50, 0.78, 1.0];
const WHITE: [f32; 4] = [1.0, 1.0, 1.0, 1.0];
-pub fn view(state: &mut ScreensaverState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
- let mut pc = PageContent::new();
- let y = cy + 12.0;
+pub fn view(state: &mut ScreensaverState, cx: f32, cy: f32, cw: f32, ch: f32, layout: &mut dyn LayoutStrategy) -> PageContent {
+ let mut final_pc = PageContent::new();
+ let sec_w = 320.0f32;
+ let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(1);
- let mut sec = Section::new(&mut pc, cx, y, cw, "Screensaver Settings");
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec = Section::new(pc, rx, ry, sec_w, "Screensaver Settings");
- let toggle_w = 48.0;
- let toggle_h = 24.0;
-
- state.enable_toggle.set_toggled(state.enable);
- sec.widget(&mut pc, &mut state.enable_toggle, 14.0, toggle_w, toggle_h);
- sec.spacing(8.0);
+ let toggle_w = 48.0;
+ let toggle_h = 24.0;
+
+ state.enable_toggle.set_toggled(state.enable);
+ sec.widget(pc, &mut state.enable_toggle, 14.0, toggle_w, toggle_h);
+ sec.spacing(8.0);
- state.lock_screen_toggle.set_toggled(state.lock_screen);
- sec.widget(&mut pc, &mut state.lock_screen_toggle, 14.0, toggle_w, toggle_h);
- sec.spacing(16.0);
+ state.lock_screen_toggle.set_toggled(state.lock_screen);
+ sec.widget(pc, &mut state.lock_screen_toggle, 14.0, toggle_w, toggle_h);
+ sec.spacing(16.0);
- state.timeout_spinbox.value = state.timeout;
- sec.widget(&mut pc, &mut state.timeout_spinbox, 14.0, 200.0, 26.0);
- sec.spacing(16.0);
+ state.timeout_spinbox.value = state.timeout;
+ sec.widget(pc, &mut state.timeout_spinbox, 14.0, 200.0, 26.0);
+ sec.spacing(16.0);
- sec.widget(&mut pc, &mut state.style_menu, 14.0, 200.0, 26.0);
- sec.spacing(24.0);
+ sec.widget(pc, &mut state.style_menu, 14.0, 200.0, 26.0);
+ sec.spacing(24.0);
- let btn_w = 160.0;
- let btn_h = 32.0;
- let btn_y = sec.ay();
- sec.row(1, 0.0, btn_h, |_, x, _| {
- pc.button(
- "Preview Screensaver",
- x,
- btn_y,
- btn_w,
- btn_h,
- BTN_BG,
- BTN_HOVER,
- WHITE,
- AppAction::Screensaver(ScreensaverMessage::StartPreview),
- );
+ let btn_w = 160.0;
+ let btn_h = 32.0;
+ let btn_y = sec.ay();
+ sec.row(1, 0.0, btn_h, |_, x, _| {
+ pc.button(
+ "Preview Screensaver",
+ x,
+ btn_y,
+ btn_w,
+ btn_h,
+ BTN_BG,
+ BTN_HOVER,
+ WHITE,
+ AppAction::Screensaver(ScreensaverMessage::StartPreview),
+ );
+ });
+ sec.spacing(12.0);
+ sec.finish(pc)
});
- sec.spacing(12.0);
- sec.finish(&mut pc);
- pc
+ final_pc
}
pub fn update(state: &mut ScreensaverState, msg: ScreensaverMessage) {
diff --git a/src/pages/services.rs b/src/pages/services.rs
index 47bdcb6..8e1092f 100644
--- a/src/pages/services.rs
+++ b/src/pages/services.rs
@@ -1,9 +1,10 @@
use crate::app::PageContent;
-use clear_ui::layout::Section;
+use clear_ui::layout::{Section, PageLayoutBuilder, LayoutStrategy};
use clear_ui::widget::{Widget, TextLabel, ScrollBox, ScrollingList, TextBox};
use clear_ui::widget::{ElementState, KeyEvent, MouseButton, Key, NamedKey};
+
// ── Service Types and Page State ──
#[derive(Debug, Clone)]
@@ -137,182 +138,199 @@ fn service_action(name: &str, action: &str, is_system: bool) {
const TEXT_DIM: [f32; 4] = [0.53, 0.53, 0.60, 1.0];
-pub fn view(state: &mut ServicesState, cx: f32, cy: f32, cw: f32, _ch: f32, root_focused: bool) -> PageContent {
- let mut pc = PageContent::new();
- let y = cy + 12.0;
+pub fn view(state: &mut ServicesState, cx: f32, cy: f32, cw: f32, ch: f32, root_focused: bool, layout: &mut dyn LayoutStrategy) -> PageContent {
+ let mut final_pc = PageContent::new();
+ let sec_w = 320.0f32;
+ let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(1);
- let mut sec = Section::new(&mut pc, cx, y, cw, "Services");
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec = Section::new(pc, rx, ry, sec_w, "Services");
- if !state.loaded {
- sec.text(&mut pc, "Loading systemd services...", 12.0, 0.0, 12.0, TEXT_DIM);
- sec.spacing(18.0);
- } else {
- // Tab header buttons: System Services, User Services
- let tab_w = 140.0;
- let tab_h = 28.0;
- let tab_y = sec.ay();
- let active_bg = [0.20, 0.40, 0.65, 0.4];
- let inactive_bg = [0.10, 0.10, 0.16, 0.3];
- let hover_bg = [0.20, 0.20, 0.25, 0.15];
-
- pc.button(
- "System Services",
- cx + 12.0,
- tab_y,
- tab_w,
- tab_h,
- if state.active_tab == ServiceTab::System { active_bg } else { inactive_bg },
- hover_bg,
- [0.90, 0.90, 0.95, 1.0],
- crate::app::AppAction::Services(ServicesMessage::SetTab(ServiceTab::System)),
- );
-
- pc.button(
- "User Services",
- cx + 12.0 + tab_w + 8.0,
- tab_y,
- tab_w,
- tab_h,
- if state.active_tab == ServiceTab::User { active_bg } else { inactive_bg },
- hover_bg,
- [0.90, 0.90, 0.95, 1.0],
- crate::app::AppAction::Services(ServicesMessage::SetTab(ServiceTab::User)),
- );
- sec.content_y += tab_h + 12.0;
-
- // Search textbox
- let search_y = sec.ay() + state.search_box.top_room();
- let search_w = cw - 24.0;
- let search_h = 28.0;
-
- state.search_box.set_row_rect(cx + 12.0, search_w);
- clear_ui::layout::render_widget(
- &mut pc,
- &mut state.search_box,
- cx + 12.0,
- search_y,
- search_w,
- search_h,
- );
- sec.content_y += search_h + state.search_box.top_room() + 16.0;
-
- // Scroll box list
- let list_box_x = cx + 12.0;
- let list_box_y = sec.ay();
- let list_box_w = cw - 24.0;
- let list_box_h = 360.0;
-
- clear_ui::layout::render_widget(&mut pc, &mut state.list_box, list_box_x, list_box_y, list_box_w, list_box_h);
-
- // Filter services
- let query = if state.search_box.editing {
- state.search_box.edit_buffer.to_lowercase()
+ if !state.loaded {
+ sec.text(pc, "Loading systemd services...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(18.0);
} else {
- state.search_box.text.to_lowercase()
- };
- let filtered_services: Vec<&ServiceInfo> = state.services.iter()
- .filter(|s| s.is_system == (state.active_tab == ServiceTab::System))
- .filter(|s| s.name.to_lowercase().contains(&query) || s.description.to_lowercase().contains(&query))
- .collect();
-
- // Update ScrollingList bounds
- state.list_box.update_bounds(filtered_services.len(), list_box_y, list_box_h);
-
- let item_h = state.list_box.item_height;
-
- for (idx, service) in filtered_services.iter().enumerate() {
- if let Some(draw_y) = state.list_box.get_item_draw_y(idx, 4.0) {
- // Item background
- let bg_color = [0.08, 0.08, 0.12, 0.2];
- pc.rect(bg_color, list_box_x + 4.0, draw_y, list_box_w - 24.0, item_h);
-
- // Status indicator color
- let is_active = service.active_state == "active" || service.sub_state == "running";
- let status_color = if service.active_state == "failed" {
- [0.85, 0.25, 0.25, 1.0] // failed = red
- } else if is_active {
- [0.25, 0.75, 0.35, 1.0] // active = green
- } else {
- [0.55, 0.55, 0.60, 1.0] // inactive/dead = gray
- };
-
- // Render status dot (small square)
- pc.rect(status_color, list_box_x + 14.0, draw_y + (item_h - 10.0) / 2.0, 10.0, 10.0);
-
- // Service Name
- pc.text(&service.name, list_box_x + 32.0, draw_y + 4.0, 13.0, [0.90, 0.90, 0.95, 1.0]);
-
- // Service Description
- let desc = if service.description.is_empty() { "No description" } else { &service.description };
- let desc_truncated = if desc.len() > 65 { format!("{}...", &desc[..62]) } else { desc.to_string() };
- pc.text(&desc_truncated, list_box_x + 32.0, draw_y + 19.0, 11.0, [0.55, 0.55, 0.60, 1.0]);
-
- // Control buttons: Start, Stop, Restart on the right
- let btn_w = 46.0;
- let r_btn_w = 54.0;
- let btn_gap = 6.0;
- let right_edge = list_box_x + list_box_w - 24.0 - 8.0;
-
- let restart_x = right_edge - r_btn_w;
- let stop_x = restart_x - btn_gap - btn_w;
- let start_x = stop_x - btn_gap - btn_w;
-
- let btn_y = draw_y + (item_h - 22.0) / 2.0;
- let btn_h = 22.0;
-
- let active_txt = [0.90, 0.90, 0.95, 1.0];
- let disabled_txt = [0.40, 0.40, 0.45, 1.0];
-
- // Start button
- pc.button(
- "Start",
- start_x,
- btn_y,
- btn_w,
- btn_h,
- if !is_active { [0.16, 0.35, 0.18, 0.4] } else { [0.12, 0.12, 0.16, 0.1] },
- [0.22, 0.45, 0.25, 0.6],
- if !is_active { active_txt } else { disabled_txt },
- crate::app::AppAction::Services(ServicesMessage::Start(service.name.clone(), service.is_system)),
- );
-
- // Stop button
- pc.button(
- "Stop",
- stop_x,
- btn_y,
- btn_w,
- btn_h,
- if is_active { [0.55, 0.16, 0.16, 0.3] } else { [0.12, 0.12, 0.16, 0.1] },
- [0.70, 0.22, 0.22, 0.5],
- if is_active { active_txt } else { disabled_txt },
- crate::app::AppAction::Services(ServicesMessage::Stop(service.name.clone(), service.is_system)),
- );
-
- // Restart button
- pc.button(
- "Restart",
- restart_x,
- btn_y,
- r_btn_w,
- btn_h,
- [0.15, 0.28, 0.45, 0.3],
- [0.20, 0.38, 0.58, 0.5],
- active_txt,
- crate::app::AppAction::Services(ServicesMessage::Restart(service.name.clone(), service.is_system)),
- );
+ // Tab header buttons: System Services, User Services
+ let tab_w = (sec_w - 24.0 - 8.0) / 2.0;
+ let tab_h = 28.0;
+ let tab_y = sec.ay();
+ let active_bg = [0.20, 0.40, 0.65, 0.4];
+ let inactive_bg = [0.10, 0.10, 0.16, 0.3];
+ let hover_bg = [0.20, 0.20, 0.25, 0.15];
+
+ let label1 = if tab_w < 110.0 { "System" } else { "System Services" };
+ let label2 = if tab_w < 110.0 { "User" } else { "User Services" };
+
+ pc.button(
+ label1,
+ rx + 12.0,
+ tab_y,
+ tab_w,
+ tab_h,
+ if state.active_tab == ServiceTab::System { active_bg } else { inactive_bg },
+ hover_bg,
+ [0.90, 0.90, 0.95, 1.0],
+ crate::app::AppAction::Services(ServicesMessage::SetTab(ServiceTab::System)),
+ );
+
+ pc.button(
+ label2,
+ rx + 12.0 + tab_w + 8.0,
+ tab_y,
+ tab_w,
+ tab_h,
+ if state.active_tab == ServiceTab::User { active_bg } else { inactive_bg },
+ hover_bg,
+ [0.90, 0.90, 0.95, 1.0],
+ crate::app::AppAction::Services(ServicesMessage::SetTab(ServiceTab::User)),
+ );
+ sec.content_y += tab_h + 12.0;
+
+ // Search textbox
+ let search_y = sec.ay() + state.search_box.top_room();
+ let search_w = sec_w - 24.0;
+ let search_h = 28.0;
+
+ state.search_box.set_row_rect(rx + 12.0, search_w);
+ clear_ui::layout::render_widget(
+ pc,
+ &mut state.search_box,
+ rx + 12.0,
+ search_y,
+ search_w,
+ search_h,
+ );
+ sec.content_y += search_h + state.search_box.top_room() + 16.0;
+
+ // Scroll box list
+ let list_box_x = rx + 12.0;
+ let list_box_y = sec.ay();
+ let list_box_w = sec_w - 24.0;
+ let list_box_h = 360.0;
+
+ clear_ui::layout::render_widget(pc, &mut state.list_box, list_box_x, list_box_y, list_box_w, list_box_h);
+
+ // Filter services
+ let query = if state.search_box.editing {
+ state.search_box.edit_buffer.to_lowercase()
+ } else {
+ state.search_box.text.to_lowercase()
+ };
+ let filtered_services: Vec<&ServiceInfo> = state.services.iter()
+ .filter(|s| s.is_system == (state.active_tab == ServiceTab::System))
+ .filter(|s| s.name.to_lowercase().contains(&query) || s.description.to_lowercase().contains(&query))
+ .collect();
+
+ // Update ScrollingList bounds
+ state.list_box.update_bounds(filtered_services.len(), list_box_y, list_box_h);
+
+ let item_h = state.list_box.item_height;
+
+ for (idx, service) in filtered_services.iter().enumerate() {
+ if let Some(draw_y) = state.list_box.get_item_draw_y(idx, 4.0) {
+ // Item background
+ let bg_color = [0.08, 0.08, 0.12, 0.2];
+ pc.rect(bg_color, list_box_x + 4.0, draw_y, list_box_w - 24.0, item_h);
+
+ // Status indicator color
+ let is_active = service.active_state == "active" || service.sub_state == "running";
+ let status_color = if service.active_state == "failed" {
+ [0.85, 0.25, 0.25, 1.0] // failed = red
+ } else if is_active {
+ [0.25, 0.75, 0.35, 1.0] // active = green
+ } else {
+ [0.55, 0.55, 0.60, 1.0] // inactive/dead = gray
+ };
+
+ // Render status dot (small square)
+ pc.rect(status_color, list_box_x + 14.0, draw_y + (item_h - 10.0) / 2.0, 10.0, 10.0);
+
+ // Control buttons: Start, Stop, Restart on the right
+ let is_small = sec_w < 350.0;
+ let btn_w = if is_small { 24.0 } else { 46.0 };
+ let r_btn_w = if is_small { 24.0 } else { 54.0 };
+ let btn_gap = if is_small { 4.0 } else { 6.0 };
+ let right_edge = list_box_x + list_box_w - 24.0 - 8.0;
+
+ let restart_x = right_edge - r_btn_w;
+ let stop_x = restart_x - btn_gap - btn_w;
+ let start_x = stop_x - btn_gap - btn_w;
+
+ let btn_y = draw_y + (item_h - 22.0) / 2.0;
+ let btn_h = 22.0;
+
+ // Service Name
+ pc.text(&service.name, list_box_x + 32.0, draw_y + 4.0, 13.0, [0.90, 0.90, 0.95, 1.0]);
+
+ // Service Description (Truncate dynamically based on remaining space before Start button)
+ let text_max_w = (start_x - 8.0) - (list_box_x + 32.0);
+ let max_chars = ((text_max_w / 6.0) as usize).max(10);
+ let desc = if service.description.is_empty() { "No description" } else { &service.description };
+ let desc_truncated = if desc.len() > max_chars {
+ format!("{}...", &desc[..max_chars.saturating_sub(3)])
+ } else {
+ desc.to_string()
+ };
+ pc.text(&desc_truncated, list_box_x + 32.0, draw_y + 19.0, 11.0, [0.55, 0.55, 0.60, 1.0]);
+
+ let active_txt = [0.90, 0.90, 0.95, 1.0];
+ let disabled_txt = [0.40, 0.40, 0.45, 1.0];
+
+ let start_lbl = if is_small { "▶" } else { "Start" };
+ let stop_lbl = if is_small { "■" } else { "Stop" };
+ let restart_lbl = if is_small { "⟳" } else { "Restart" };
+
+ // Start button
+ pc.button(
+ start_lbl,
+ start_x,
+ btn_y,
+ btn_w,
+ btn_h,
+ if !is_active { [0.16, 0.35, 0.18, 0.4] } else { [0.12, 0.12, 0.16, 0.1] },
+ [0.22, 0.45, 0.25, 0.6],
+ if !is_active { active_txt } else { disabled_txt },
+ crate::app::AppAction::Services(ServicesMessage::Start(service.name.clone(), service.is_system)),
+ );
+
+ // Stop button
+ pc.button(
+ stop_lbl,
+ stop_x,
+ btn_y,
+ btn_w,
+ btn_h,
+ if is_active { [0.55, 0.16, 0.16, 0.3] } else { [0.12, 0.12, 0.16, 0.1] },
+ [0.70, 0.22, 0.22, 0.5],
+ if is_active { active_txt } else { disabled_txt },
+ crate::app::AppAction::Services(ServicesMessage::Stop(service.name.clone(), service.is_system)),
+ );
+
+ // Restart button
+ pc.button(
+ restart_lbl,
+ restart_x,
+ btn_y,
+ r_btn_w,
+ btn_h,
+ [0.15, 0.28, 0.45, 0.3],
+ [0.20, 0.38, 0.58, 0.5],
+ active_txt,
+ crate::app::AppAction::Services(ServicesMessage::Restart(service.name.clone(), service.is_system)),
+ );
+ }
}
- }
- if filtered_services.is_empty() {
- pc.text("No services match the query", list_box_x + 16.0, list_box_y + 16.0, 12.0, TEXT_DIM);
- }
+ if filtered_services.is_empty() {
+ pc.text("No services match the query", list_box_x + 16.0, list_box_y + 16.0, 12.0, TEXT_DIM);
+ }
- sec.content_y += list_box_h;
- }
+ sec.content_y += list_box_h;
+ }
+ sec.finish_focused(pc, root_focused)
+ });
- sec.finish_focused(&mut pc, root_focused);
- pc
+ final_pc
}
pub fn update(state: &mut ServicesState, msg: ServicesMessage) {
@@ -348,3 +366,17 @@ pub fn update(state: &mut ServicesState, msg: ServicesMessage) {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_view_layout_grid() {
+ let mut state = ServicesState::default();
+ let mut layout = clear_ui::layout::ColumnLayout::new(20.0);
+ let pc = view(&mut state, 10.0, 20.0, 800.0, 600.0, false, &mut layout);
+ assert!(!pc.rects.is_empty() || !pc.texts.is_empty());
+ }
+}
+
diff --git a/src/pages/status.rs b/src/pages/status.rs
index 612f461..cc22d45 100644
--- a/src/pages/status.rs
+++ b/src/pages/status.rs
@@ -1,6 +1,7 @@
use crate::app::{AppAction, PageContent};
-use clear_ui::layout::Section;
+use clear_ui::layout::{Section, PageLayoutBuilder, LayoutStrategy};
use clear_ui::widget::{Label, Toggle, Widget, Spinbox};
+
use crate::pages::typeface::parse_u16_from;
#[derive(Debug, Clone)]
@@ -184,63 +185,65 @@ const BTN_INACTIVE: [f32; 4] = [0.13, 0.18, 0.14, 1.0];
const BTN_HOVER: [f32; 4] = [0.25, 0.30, 0.26, 1.0];
const WHITE: [f32; 4] = [1.0, 1.0, 1.0, 1.0];
-pub fn view(state: &mut StatusState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
- let mut pc = PageContent::new();
- let y = cy + 12.0;
-
- let mut sec = Section::new(&mut pc, cx, y, cw, "Status Interface");
-
- if !state.loaded {
- sec.text(&mut pc, "Loading Status Interface status...", 12.0, 0.0, 12.0, TEXT_FG);
- sec.spacing(18.0);
- } else {
- // Status
- let status_text = if state.running { "Status Interface: Running" } else { "Status Interface: Stopped" };
- state.status_label.set_text(status_text);
- sec.widget(&mut pc, &mut state.status_label, 12.0, cw - 24.0, 20.0);
- sec.spacing(12.0);
-
- // Font size
- state.size_label.set_text(&format!("Font size: {}px", state.font_size));
- sec.widget(&mut pc, &mut state.size_label, 12.0, cw - 24.0, 20.0);
- sec.spacing(12.0);
-
- let btn_h = 28.0;
- let yt = sec.ay();
- pc.button("-1", sec.ax(12.0), yt, 36.0, btn_h,
- BTN_INACTIVE, BTN_HOVER, WHITE,
- AppAction::Status(StatusMessage::FontSizeDown));
- pc.text(&format!(" {}px ", state.font_size), sec.ax(56.0), yt + 7.0, 13.0, TEXT_FG);
- pc.button("+1", sec.ax(12.0 + 36.0 + 8.0), yt, 36.0, btn_h,
- BTN_ACTIVE, BTN_HOVER, WHITE,
- AppAction::Status(StatusMessage::FontSizeUp));
- sec.content_y += btn_h + 16.0;
-
- // Separators toggle
- state.separators_toggle.set_toggled(state.separators);
- sec.widget(&mut pc, &mut state.separators_toggle, 12.0, 48.0, 24.0);
- sec.spacing(16.0);
-
- // Underline toggle
- state.underline_toggle.set_toggled(state.underline);
- sec.widget(&mut pc, &mut state.underline_toggle, 12.0, 48.0, 24.0);
- sec.spacing(16.0);
-
- // Padding spinbox
- state.padding_spinbox.value = state.padding as i32;
- sec.widget(&mut pc, &mut state.padding_spinbox, 12.0, 200.0, 26.0);
- sec.spacing(16.0);
-
- // Reload button
- let yt = sec.ay();
- let btn_w = (cw - 24.0).min(200.0);
- pc.button("Reload Status Interface", cx + cw / 2.0 - btn_w / 2.0, yt, btn_w, 32.0,
- BTN_INACTIVE, BTN_HOVER, WHITE,
- AppAction::Status(StatusMessage::ReloadStatus));
- }
- sec.finish(&mut pc);
+pub fn view(state: &mut StatusState, cx: f32, cy: f32, cw: f32, ch: f32, layout: &mut dyn LayoutStrategy) -> PageContent {
+ let mut final_pc = PageContent::new();
+ let sec_w = 320.0f32;
+ let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(1);
+
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec = Section::new(pc, rx, ry, sec_w, "Status Interface");
+ if !state.loaded {
+ sec.text(pc, "Loading Status Interface status...", 12.0, 0.0, 12.0, TEXT_FG);
+ sec.spacing(18.0);
+ } else {
+ // Status
+ let status_text = if state.running { "Status Interface: Running" } else { "Status Interface: Stopped" };
+ state.status_label.set_text(status_text);
+ sec.widget(pc, &mut state.status_label, 12.0, sec_w - 24.0, 20.0);
+ sec.spacing(12.0);
+
+ // Font size
+ state.size_label.set_text(&format!("Font size: {}px", state.font_size));
+ sec.widget(pc, &mut state.size_label, 12.0, sec_w - 24.0, 20.0);
+ sec.spacing(12.0);
+
+ let btn_h = 28.0;
+ let yt = sec.ay();
+ pc.button("-1", sec.ax(12.0), yt, 36.0, btn_h,
+ BTN_INACTIVE, BTN_HOVER, WHITE,
+ AppAction::Status(StatusMessage::FontSizeDown));
+ pc.text(&format!(" {}px ", state.font_size), sec.ax(56.0), yt + 7.0, 13.0, TEXT_FG);
+ pc.button("+1", sec.ax(12.0 + 36.0 + 8.0), yt, 36.0, btn_h,
+ BTN_ACTIVE, BTN_HOVER, WHITE,
+ AppAction::Status(StatusMessage::FontSizeUp));
+ sec.content_y += btn_h + 16.0;
+
+ // Separators toggle
+ state.separators_toggle.set_toggled(state.separators);
+ sec.widget(pc, &mut state.separators_toggle, 12.0, 48.0, 24.0);
+ sec.spacing(16.0);
+
+ // Underline toggle
+ state.underline_toggle.set_toggled(state.underline);
+ sec.widget(pc, &mut state.underline_toggle, 12.0, 48.0, 24.0);
+ sec.spacing(16.0);
+
+ // Padding spinbox
+ state.padding_spinbox.value = state.padding as i32;
+ sec.widget(pc, &mut state.padding_spinbox, 12.0, 200.0, 26.0);
+ sec.spacing(16.0);
+
+ // Reload button
+ let yt = sec.ay();
+ let btn_w = (sec_w - 24.0).min(200.0);
+ pc.button("Reload Status Interface", rx + sec_w / 2.0 - btn_w / 2.0, yt, btn_w, 32.0,
+ BTN_INACTIVE, BTN_HOVER, WHITE,
+ AppAction::Status(StatusMessage::ReloadStatus));
+ }
+ sec.finish(pc)
+ });
- pc
+ final_pc
}
pub fn update(state: &mut StatusState, msg: StatusMessage) {
@@ -349,4 +352,12 @@ mod tests {
let _ = fs::remove_file(path);
}
+
+ #[test]
+ fn test_view_layout_grid() {
+ let mut state = StatusState::default();
+ let mut layout = clear_ui::layout::ColumnLayout::new(20.0);
+ let pc = view(&mut state, 10.0, 20.0, 800.0, 600.0, &mut layout);
+ assert!(!pc.rects.is_empty() || !pc.texts.is_empty());
+ }
}
diff --git a/src/pages/storage.rs b/src/pages/storage.rs
index 1e3c6ed..740413d 100644
--- a/src/pages/storage.rs
+++ b/src/pages/storage.rs
@@ -1,5 +1,5 @@
use crate::app::PageContent;
-use clear_ui::layout::Section;
+use clear_ui::layout::{Section, PageLayoutBuilder, LayoutStrategy};
#[derive(Debug, Clone, Default)]
pub struct StorageState {
@@ -62,59 +62,61 @@ fn parse_mem(info: &str) -> (f64, f64) {
const LABEL_FG: [f32; 4] = [0.56, 0.83, 0.56, 1.0];
const TEXT_FG: [f32; 4] = [0.83, 0.83, 0.83, 1.0];
-pub fn view(state: &StorageState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
- let mut pc = PageContent::new();
- let y = cy + 12.0;
+pub fn view(state: &StorageState, cx: f32, cy: f32, cw: f32, ch: f32, layout: &mut dyn LayoutStrategy) -> PageContent {
+ let mut final_pc = PageContent::new();
+ let sec_w = 320.0f32;
+ let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(1);
- let mut sec = Section::new(&mut pc, cx, y, cw, "Local Storage");
-
- if !state.loaded {
- sec.text(&mut pc, "Loading storage and memory usage...", 12.0, 0.0, 12.0, TEXT_FG);
- sec.spacing(18.0);
- } else {
- let disk_pct = if state.disk_total > 0.0 {
- state.disk_used / state.disk_total * 100.0
- } else {
- 0.0
- };
-
- sec.text(&mut pc, "Disk", 12.0, 0.0, 12.0, LABEL_FG);
- sec.text(&mut pc,
- &format!("{:.0} / {:.0} GiB ({:.0}%)", state.disk_used, state.disk_total, disk_pct),
- 100.0, 0.0, 12.0, TEXT_FG,
- );
- sec.spacing(18.0);
-
- let bar_w = cw - 24.0;
- let yt = sec.ay();
- pc.rect([0.15, 0.15, 0.25, 1.0], sec.ax(12.0), yt, bar_w, 8.0);
- if disk_pct > 0.0 {
- pc.rect([0.36, 0.60, 0.36, 1.0], sec.ax(12.0), yt, bar_w * (disk_pct as f32 / 100.0).min(1.0), 8.0);
- }
- sec.content_y += 20.0;
-
- let ram_pct = if state.ram_total > 0.0 {
- state.ram_used / state.ram_total * 100.0
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec = Section::new(pc, rx, ry, sec_w, "Local Storage");
+ if !state.loaded {
+ sec.text(pc, "Loading storage and memory usage...", 12.0, 0.0, 12.0, TEXT_FG);
+ sec.spacing(18.0);
} else {
- 0.0
- };
-
- sec.text(&mut pc, "RAM", 12.0, 0.0, 12.0, LABEL_FG);
- sec.text(&mut pc,
- &format!("{:.1} / {:.1} GiB ({:.0}%)", state.ram_used, state.ram_total, ram_pct),
- 100.0, 0.0, 12.0, TEXT_FG,
- );
- sec.spacing(18.0);
-
- let yt = sec.ay();
- pc.rect([0.15, 0.15, 0.25, 1.0], sec.ax(12.0), yt, bar_w, 8.0);
- if ram_pct > 0.0 {
- pc.rect([0.50, 0.50, 0.65, 1.0], sec.ax(12.0), yt, bar_w * (ram_pct as f32 / 100.0).min(1.0), 8.0);
+ let disk_pct = if state.disk_total > 0.0 {
+ state.disk_used / state.disk_total * 100.0
+ } else {
+ 0.0
+ };
+
+ sec.text(pc, "Disk", 12.0, 0.0, 12.0, LABEL_FG);
+ sec.text(pc,
+ &format!("{:.0} / {:.0} GiB ({:.0}%)", state.disk_used, state.disk_total, disk_pct),
+ 100.0, 0.0, 12.0, TEXT_FG,
+ );
+ sec.spacing(18.0);
+
+ let bar_w = sec_w - 24.0;
+ let yt = sec.ay();
+ pc.rect([0.15, 0.15, 0.25, 1.0], sec.ax(12.0), yt, bar_w, 8.0);
+ if disk_pct > 0.0 {
+ pc.rect([0.36, 0.60, 0.36, 1.0], sec.ax(12.0), yt, bar_w * (disk_pct as f32 / 100.0).min(1.0), 8.0);
+ }
+ sec.content_y += 20.0;
+
+ let ram_pct = if state.ram_total > 0.0 {
+ state.ram_used / state.ram_total * 100.0
+ } else {
+ 0.0
+ };
+
+ sec.text(pc, "RAM", 12.0, 0.0, 12.0, LABEL_FG);
+ sec.text(pc,
+ &format!("{:.1} / {:.1} GiB ({:.0}%)", state.ram_used, state.ram_total, ram_pct),
+ 100.0, 0.0, 12.0, TEXT_FG,
+ );
+ sec.spacing(18.0);
+
+ let yt = sec.ay();
+ pc.rect([0.15, 0.15, 0.25, 1.0], sec.ax(12.0), yt, bar_w, 8.0);
+ if ram_pct > 0.0 {
+ pc.rect([0.50, 0.50, 0.65, 1.0], sec.ax(12.0), yt, bar_w * (ram_pct as f32 / 100.0).min(1.0), 8.0);
+ }
}
- }
- sec.finish(&mut pc);
+ sec.finish(pc)
+ });
- pc
+ final_pc
}
pub fn update(state: &mut StorageState, msg: StorageMessage) {
diff --git a/src/pages/system_info.rs b/src/pages/system_info.rs
index 1b9e1cf..5335ad8 100644
--- a/src/pages/system_info.rs
+++ b/src/pages/system_info.rs
@@ -1,5 +1,5 @@
use crate::app::{AppAction, PageContent};
-use clear_ui::layout::Section;
+use clear_ui::layout::{Section, PageLayoutBuilder, LayoutStrategy};
#[derive(Debug, Clone, Default)]
pub struct SystemState {
@@ -48,52 +48,57 @@ fn spawn_systemctl(action: &str) {
let _ = tokio::process::Command::new("systemctl").arg(action).spawn();
}
-pub fn view(state: &SystemState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
- let mut pc = PageContent::new();
- let mut y = cy + 12.0;
+pub fn view(state: &SystemState, cx: f32, cy: f32, cw: f32, ch: f32, layout: &mut dyn LayoutStrategy) -> PageContent {
+ let mut final_pc = PageContent::new();
+ let sec_w = 320.0f32;
+ let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(2);
- let mut sec = Section::new(&mut pc, cx, y, cw, "System");
- if !state.loaded {
- sec.text(&mut pc, "Loading system information...", 12.0, 0.0, 14.0, TEXT_FG);
- sec.spacing(10.0);
- } else {
- sec.text(&mut pc, &format!("{} — Linux {}", state.hostname, state.kernel), 12.0, 0.0, 14.0, TEXT_FG);
- sec.spacing(10.0);
- sec.text(&mut pc, &format!("Uptime: {}", state.uptime), 12.0, 0.0, 12.0, TEXT_DIM);
- }
- y = sec.finish(&mut pc);
-
- // ── System Actions section ──
- let mut sec_act = Section::new(&mut pc, cx, y, cw, "System Actions");
+ // 1. System Section
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec = Section::new(pc, rx, ry, sec_w, "System");
+ if !state.loaded {
+ sec.text(pc, "Loading system information...", 12.0, 0.0, 14.0, TEXT_FG);
+ sec.spacing(10.0);
+ } else {
+ sec.text(pc, &format!("{} — Linux {}", state.hostname, state.kernel), 12.0, 0.0, 14.0, TEXT_FG);
+ sec.spacing(10.0);
+ sec.text(pc, &format!("Uptime: {}", state.uptime), 12.0, 0.0, 12.0, TEXT_DIM);
+ }
+ sec.finish(pc)
+ });
- let yt = sec_act.ay();
- let act_btn_h = 32.0;
+ // 2. System Actions Section
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec_act = Section::new(pc, rx, ry, sec_w, "System Actions");
+ let yt = sec_act.ay();
+ let act_btn_h = 32.0;
- sec_act.row(4, 8.0, act_btn_h, |i, x, w| {
- match i {
- 0 => {
- pc.button("Suspend", x, yt, w, act_btn_h,
- SAFE_BG, BTN_HOVER, WHITE, AppAction::SystemInfo(SystemMessage::Suspend));
+ sec_act.row(4, 8.0, act_btn_h, |i, x, w| {
+ match i {
+ 0 => {
+ pc.button("Suspend", x, yt, w, act_btn_h,
+ SAFE_BG, BTN_HOVER, WHITE, AppAction::SystemInfo(SystemMessage::Suspend));
+ }
+ 1 => {
+ pc.button("Hibernate", x, yt, w, act_btn_h,
+ SAFE_BG, BTN_HOVER, WHITE, AppAction::SystemInfo(SystemMessage::Hibernate));
+ }
+ 2 => {
+ pc.button("Reboot", x, yt, w, act_btn_h,
+ DANGER_BG, BTN_HOVER, WHITE, AppAction::SystemInfo(SystemMessage::Reboot));
+ }
+ 3 => {
+ pc.button("Power Off", x, yt, w, act_btn_h,
+ DANGER_BG, BTN_HOVER, WHITE, AppAction::SystemInfo(SystemMessage::PowerOff));
+ }
+ _ => {}
}
- 1 => {
- pc.button("Hibernate", x, yt, w, act_btn_h,
- SAFE_BG, BTN_HOVER, WHITE, AppAction::SystemInfo(SystemMessage::Hibernate));
- }
- 2 => {
- pc.button("Reboot", x, yt, w, act_btn_h,
- DANGER_BG, BTN_HOVER, WHITE, AppAction::SystemInfo(SystemMessage::Reboot));
- }
- 3 => {
- pc.button("Power Off", x, yt, w, act_btn_h,
- DANGER_BG, BTN_HOVER, WHITE, AppAction::SystemInfo(SystemMessage::PowerOff));
- }
- _ => {}
- }
+ });
+ sec_act.spacing(12.0);
+ sec_act.finish(pc)
});
- sec_act.spacing(12.0);
- sec_act.finish(&mut pc);
- pc
+ final_pc
}
pub fn update(_state: &mut SystemState, msg: SystemMessage) {
diff --git a/src/pages/typeface.rs b/src/pages/typeface.rs
index 5520975..959bc41 100644
--- a/src/pages/typeface.rs
+++ b/src/pages/typeface.rs
@@ -1,6 +1,6 @@
use std::fs;
use crate::app::PageContent;
-use clear_ui::layout::Section;
+use clear_ui::layout::{Section, PageLayoutBuilder, LayoutStrategy};
use clear_ui::widget::{Widget, TextLabel, ScrollBox, ScrollingList, Dropdown, TextBox, Spinbox, Button};
use clear_ui::widget::{ElementState, KeyEvent, MouseButton, Key, NamedKey};
@@ -149,7 +149,7 @@ pub fn read_preferred_fonts() -> (String, String, String, String, String, String
let status = parse_font_for_alias(&content, "status-interface").unwrap_or_else(|| "Noto Sans".to_string());
let fuzzel_font = parse_font_for_alias(&content, "fuzzel").unwrap_or_else(|| "Noto Sans".to_string());
let term = parse_font_for_alias(&content, "terminal").unwrap_or_else(|| "Noto Sans Mono".to_string());
- let paginator = parse_font_for_alias(&content, "paginator-tab-labels").unwrap_or_else(|| "Noto Sans".to_string());
+ let paginator = parse_font_for_alias(&content, "paginator-tab-labels").unwrap_or_else(|| "Noto Sans Mono".to_string());
(sans, serif, mono, borders, status, fuzzel_font, term, paginator)
}
@@ -540,313 +540,300 @@ pub async fn fetch_typeface_state() -> TypefaceState {
const TEXT_DIM: [f32; 4] = [0.53, 0.53, 0.60, 1.0];
-pub fn view(state: &mut TypefaceState, cx: f32, cy: f32, cw: f32, _ch: f32, sec_focused: &[bool]) -> PageContent {
- let mut pc = PageContent::new();
- let mut y = cy + 12.0;
+pub fn view(state: &mut TypefaceState, cx: f32, cy: f32, cw: f32, ch: f32, sec_focused: &[bool], layout: &mut dyn LayoutStrategy) -> PageContent {
+ let mut final_pc = PageContent::new();
+ let sec_w = 320.0f32;
+ let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(3);
- let widget_w = cw - 24.0;
let widget_h = 26.0;
// ── System Typefaces Section ──
- let mut sec = Section::new(&mut pc, cx, y, cw, "System 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 {
- // Sans-Serif
- sec.widget(&mut pc, &mut state.sans_box, 12.0, widget_w, widget_h);
- sec.spacing(12.0);
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec = Section::new(pc, rx, ry, sec_w, "System Typefaces");
+ sec.spacing(8.0);
- // Serif
- sec.widget(&mut pc, &mut state.serif_box, 12.0, widget_w, widget_h);
- sec.spacing(12.0);
+ if !state.loaded {
+ sec.text(pc, "Loading typefaces...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(18.0);
+ } else {
+ let inner_w = sec_w - 24.0;
+ // Sans-Serif
+ sec.widget(pc, &mut state.sans_box, 12.0, inner_w, widget_h);
+ sec.spacing(12.0);
+
+ // Serif
+ sec.widget(pc, &mut state.serif_box, 12.0, inner_w, widget_h);
+ sec.spacing(12.0);
+
+ // Monospace
+ sec.widget(pc, &mut state.mono_box, 12.0, inner_w, widget_h);
+ sec.spacing(8.0);
+ }
+ let sys_focused = sec_focused.get(0).copied().unwrap_or(false);
+ sec.finish_focused(pc, sys_focused)
+ });
- // Monospace
- sec.widget(&mut pc, &mut state.mono_box, 12.0, widget_w, widget_h);
+ // ── Program Typefaces Section ──
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec = Section::new(pc, rx, ry, sec_w, "Program Typefaces");
sec.spacing(8.0);
- }
- 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 = 300.0;
- let spinbox_w = 90.0;
-
- let menu_row_w = dropdown_w + 10.0;
- let box_row_w = textbox_w + 10.0;
- let spinbox_row_w = spinbox_w + 10.0;
-
- let box_row_x = cx + 8.0 + menu_row_w;
- let spin_row_x = box_row_x + box_row_w;
- let spin_x = cx + 12.0 + dropdown_w + 12.0 + textbox_w + 12.0;
-
- // Window Borders
- let start_y = sec.ay();
- let top_room = state.borders_box.top_room();
- state.borders_menu.set_row_rect(cx + 8.0, menu_row_w);
- 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(box_row_x, box_row_w);
- 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);
- state.borders_size_box.set_row_rect(spin_row_x, spinbox_row_w);
- clear_ui::layout::render_widget(&mut pc, &mut state.borders_size_box, spin_x, start_y + top_room, spinbox_w, widget_h);
- sec.spacing(widget_h + top_room + 12.0);
-
- // Status Interface
- let start_y = sec.ay();
- let top_room = state.status_box.top_room();
- state.status_menu.set_row_rect(cx + 8.0, menu_row_w);
- 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(box_row_x, box_row_w);
- 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);
- state.status_size_box.set_row_rect(spin_row_x, spinbox_row_w);
- clear_ui::layout::render_widget(&mut pc, &mut state.status_size_box, spin_x, start_y + top_room, spinbox_w, widget_h);
- sec.spacing(widget_h + top_room + 12.0);
-
- // Fuzzel
- let start_y = sec.ay();
- let top_room = state.fuzzel_box.top_room();
- state.fuzzel_menu.set_row_rect(cx + 8.0, menu_row_w);
- 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(box_row_x, box_row_w);
- 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);
- state.fuzzel_size_box.set_row_rect(spin_row_x, spinbox_row_w);
- clear_ui::layout::render_widget(&mut pc, &mut state.fuzzel_size_box, spin_x, start_y + top_room, spinbox_w, widget_h);
- sec.spacing(widget_h + top_room + 12.0);
-
- // Terminal
- let start_y = sec.ay();
- let top_room = state.terminal_box.top_room();
- state.terminal_menu.set_row_rect(cx + 8.0, menu_row_w);
- 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(box_row_x, box_row_w);
- 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);
- state.terminal_size_box.set_row_rect(spin_row_x, spinbox_row_w);
- clear_ui::layout::render_widget(&mut pc, &mut state.terminal_size_box, spin_x, start_y + top_room, spinbox_w, widget_h);
- sec.spacing(widget_h + top_room + 12.0);
-
- // Paginator Tab Labels
- let start_y = sec.ay();
- let top_room = state.paginator_box.top_room();
- state.paginator_menu.set_row_rect(cx + 8.0, menu_row_w);
- clear_ui::layout::render_widget(&mut pc, &mut state.paginator_menu, cx + 12.0, start_y + top_room, dropdown_w, widget_h);
- state.paginator_box.set_row_rect(box_row_x, box_row_w);
- clear_ui::layout::render_widget(&mut pc, &mut state.paginator_box, cx + 12.0 + dropdown_w + 12.0, start_y + top_room, textbox_w, widget_h);
- state.paginator_size_box.set_row_rect(spin_row_x, spinbox_row_w);
- clear_ui::layout::render_widget(&mut pc, &mut state.paginator_size_box, spin_x, start_y + top_room, spinbox_w, widget_h);
- sec.spacing(widget_h + top_room + 8.0);
- }
- let prog_focused = sec_focused.get(1).copied().unwrap_or(false);
- y = sec.finish_focused(&mut pc, prog_focused);
+ if !state.loaded {
+ sec.text(pc, "Loading typefaces...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(18.0);
+ } else {
+ let inner_w = sec_w - 24.0;
+
+ // Window Borders
+ let start_y = sec.ay();
+ sec.row(2, 10.0, widget_h, |idx, x, w| {
+ if idx == 0 {
+ state.borders_menu.set_row_rect(x, w);
+ clear_ui::layout::render_widget(pc, &mut state.borders_menu, x, start_y, w, widget_h);
+ } else {
+ state.borders_size_box.set_row_rect(x, w);
+ clear_ui::layout::render_widget(pc, &mut state.borders_size_box, x, start_y, w, widget_h);
+ }
+ });
+ sec.widget(pc, &mut state.borders_box, 12.0, inner_w, widget_h);
+ sec.spacing(16.0);
+
+ // Status Interface
+ let start_y = sec.ay();
+ sec.row(2, 10.0, widget_h, |idx, x, w| {
+ if idx == 0 {
+ state.status_menu.set_row_rect(x, w);
+ clear_ui::layout::render_widget(pc, &mut state.status_menu, x, start_y, w, widget_h);
+ } else {
+ state.status_size_box.set_row_rect(x, w);
+ clear_ui::layout::render_widget(pc, &mut state.status_size_box, x, start_y, w, widget_h);
+ }
+ });
+ sec.widget(pc, &mut state.status_box, 12.0, inner_w, widget_h);
+ sec.spacing(16.0);
+
+ // Fuzzel
+ let start_y = sec.ay();
+ sec.row(2, 10.0, widget_h, |idx, x, w| {
+ if idx == 0 {
+ state.fuzzel_menu.set_row_rect(x, w);
+ clear_ui::layout::render_widget(pc, &mut state.fuzzel_menu, x, start_y, w, widget_h);
+ } else {
+ state.fuzzel_size_box.set_row_rect(x, w);
+ clear_ui::layout::render_widget(pc, &mut state.fuzzel_size_box, x, start_y, w, widget_h);
+ }
+ });
+ sec.widget(pc, &mut state.fuzzel_box, 12.0, inner_w, widget_h);
+ sec.spacing(16.0);
+
+ // Terminal
+ let start_y = sec.ay();
+ sec.row(2, 10.0, widget_h, |idx, x, w| {
+ if idx == 0 {
+ state.terminal_menu.set_row_rect(x, w);
+ clear_ui::layout::render_widget(pc, &mut state.terminal_menu, x, start_y, w, widget_h);
+ } else {
+ state.terminal_size_box.set_row_rect(x, w);
+ clear_ui::layout::render_widget(pc, &mut state.terminal_size_box, x, start_y, w, widget_h);
+ }
+ });
+ sec.widget(pc, &mut state.terminal_box, 12.0, inner_w, widget_h);
+ sec.spacing(16.0);
+
+ // Paginator Tab Labels
+ let start_y = sec.ay();
+ sec.row(2, 10.0, widget_h, |idx, x, w| {
+ if idx == 0 {
+ state.paginator_menu.set_row_rect(x, w);
+ clear_ui::layout::render_widget(pc, &mut state.paginator_menu, x, start_y, w, widget_h);
+ } else {
+ state.paginator_size_box.set_row_rect(x, w);
+ clear_ui::layout::render_widget(pc, &mut state.paginator_size_box, x, start_y, w, widget_h);
+ }
+ });
+ sec.widget(pc, &mut state.paginator_box, 12.0, inner_w, widget_h);
+ sec.spacing(8.0);
+ }
+ let prog_focused = sec_focused.get(1).copied().unwrap_or(false);
+ sec.finish_focused(pc, prog_focused)
+ });
// ── Typefaces Section (List & Preview) ──
- let mut sec = Section::new(&mut pc, cx, y, cw, "Typefaces");
- sec.spacing(12.0);
-
- if !state.loaded {
- sec.text(&mut pc, "Loading installed fonts...", 12.0, 0.0, 12.0, TEXT_DIM);
- sec.spacing(18.0);
- } else {
- let start_y = sec.ay();
- let usable_w = cw - 24.0;
- let gap = 24.0;
- let left_w = (usable_w - gap) * 0.40;
- let right_w = (usable_w - gap) * 0.60;
- let left_x = cx + 12.0;
- let right_x = left_x + left_w + gap;
-
- // 1. Render Left Column (Search + List Box)
- let mut left_y = start_y;
-
- let top_room = state.search_box.top_room();
- let search_box_h = widget_h + top_room;
- state.search_box.set_row_rect(left_x, left_w);
- clear_ui::layout::render_widget(
- &mut pc,
- &mut state.search_box,
- left_x,
- left_y + top_room,
- left_w,
- widget_h,
- );
- left_y += search_box_h + 12.0;
-
- // Scrolling box configuration
- let list_box_y = left_y;
- let list_box_h = 320.0;
-
- // 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();
- let matching_fonts: Vec<&String> = state.all_fonts.iter()
- .filter(|font| font.to_lowercase().contains(&query))
- .collect();
-
- // Ensure we have exactly matching_fonts.len() buttons of each type
- if state.font_buttons.len() != matching_fonts.len() {
- state.font_buttons.clear();
- state.copy_buttons.clear();
- for _ in 0..matching_fonts.len() {
- state.font_buttons.push(Button::new_list_row(0.0, 0.0, 0.0, 0.0));
- state.copy_buttons.push(Button::new_copy_icon(0.0, 0.0, 0.0, 0.0));
+ builder.add_section(&mut final_pc, |pc, rx, ry| {
+ let mut sec = Section::new(pc, rx, ry, sec_w, "Typefaces");
+ sec.spacing(12.0);
+
+ if !state.loaded {
+ sec.text(pc, "Loading installed fonts...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(18.0);
+ } else {
+ let inner_w = sec_w - 24.0;
+
+ // 1. Search Box
+ let top_room = state.search_box.top_room();
+ state.search_box.set_row_rect(rx + 12.0, inner_w);
+ clear_ui::layout::render_widget(
+ pc,
+ &mut state.search_box,
+ rx + 12.0,
+ sec.ay() + top_room,
+ inner_w,
+ widget_h,
+ );
+ sec.spacing(widget_h + top_room + 12.0);
+
+ // 2. Scrolling List Box
+ let list_box_y = sec.ay();
+ let list_box_h = 200.0;
+
+ clear_ui::layout::render_widget(pc, &mut state.list_box, rx + 12.0, list_box_y, inner_w, list_box_h);
+
+ let query = state.search_box.text.to_lowercase();
+ let matching_fonts: Vec<&String> = state.all_fonts.iter()
+ .filter(|font| font.to_lowercase().contains(&query))
+ .collect();
+
+ if state.font_buttons.len() != matching_fonts.len() {
+ state.font_buttons.clear();
+ state.copy_buttons.clear();
+ for _ in 0..matching_fonts.len() {
+ state.font_buttons.push(Button::new_list_row(0.0, 0.0, 0.0, 0.0));
+ state.copy_buttons.push(Button::new_copy_icon(0.0, 0.0, 0.0, 0.0));
+ }
+ }
+
+ let btn_h = 24.0;
+ let list_inner_x = rx + 16.0;
+ let list_inner_w = inner_w - 16.0;
+
+ state.list_box.update_bounds(matching_fonts.len(), list_box_y, list_box_h);
+
+ for (idx, font_name) in matching_fonts.iter().enumerate() {
+ if let Some(draw_y) = state.list_box.get_item_draw_y(idx, 0.0) {
+ let is_selected = state.selected_font.as_ref() == Some(*font_name);
+
+ let font_btn = &mut state.font_buttons[idx];
+ font_btn.set_text(font_name);
+ font_btn.selected = is_selected;
+ clear_ui::layout::render_widget(pc, font_btn, list_inner_x, draw_y, list_inner_w - 44.0, btn_h);
+
+ let copy_btn = &mut state.copy_buttons[idx];
+ copy_btn.set_text("📋");
+ copy_btn.selected = is_selected;
+ clear_ui::layout::render_widget(pc, copy_btn, list_inner_x + list_inner_w - 40.0, draw_y, 40.0, btn_h);
+ }
}
- }
- let btn_h = 24.0;
- let inner_x = left_x + 4.0;
- let inner_w = left_w - 16.0; // leave room for scrollbar
-
- // 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() {
- // Only render buttons that are completely within the visible area
- if let Some(draw_y) = state.list_box.get_item_draw_y(idx, 0.0) {
- let is_selected = state.selected_font.as_ref() == Some(*font_name);
-
- let font_btn = &mut state.font_buttons[idx];
- font_btn.set_text(font_name);
- font_btn.selected = is_selected;
- clear_ui::layout::render_widget(&mut pc, font_btn, inner_x, draw_y, inner_w - 44.0, btn_h);
-
- let copy_btn = &mut state.copy_buttons[idx];
- copy_btn.set_text("📋");
- copy_btn.selected = is_selected;
- clear_ui::layout::render_widget(&mut pc, copy_btn, inner_x + inner_w - 40.0, draw_y, 40.0, btn_h);
+ if matching_fonts.is_empty() {
+ pc.text("No fonts match query", list_inner_x + 8.0, list_box_y + 16.0, 12.0, TEXT_DIM);
}
- }
-
- if matching_fonts.is_empty() {
- pc.text("No fonts match query", inner_x + 8.0, list_box_y + 16.0, 12.0, TEXT_DIM);
- }
- left_y += list_box_h;
-
- // 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);
- pc.rect([0.25, 0.25, 0.35, 0.5], right_x, right_y, right_w, 1.0);
- pc.rect([0.25, 0.25, 0.35, 0.5], right_x, right_y + card_h - 1.0, right_w, 1.0);
- pc.rect([0.25, 0.25, 0.35, 0.5], right_x, right_y, 1.0, card_h);
- pc.rect([0.25, 0.25, 0.35, 0.5], right_x + right_w - 1.0, right_y, 1.0, card_h);
-
- let text_padding_x = 16.0;
- let mut text_y = right_y + 16.0;
-
- pc.text(&format!("Family: {}", font_name), right_x + text_padding_x, text_y, 15.0, [0.90, 0.90, 0.95, 1.0]);
- text_y += 28.0;
-
- pc.rect([0.22, 0.22, 0.30, 0.8], right_x + text_padding_x, text_y, right_w - (text_padding_x * 2.0), 1.0);
- text_y += 16.0;
-
- pc.text_with_font(
- "abcdefghijklmnopqrstuvwxyz",
- right_x + text_padding_x,
- text_y,
- 13.0,
- [0.75, 0.75, 0.80, 1.0],
- font_name,
- );
- text_y += 22.0;
-
- pc.text_with_font(
- "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
- right_x + text_padding_x,
- text_y,
- 13.0,
- [0.75, 0.75, 0.80, 1.0],
- font_name,
- );
- text_y += 22.0;
-
- pc.text_with_font(
- "0123456789 (!@#$%&*?)",
- right_x + text_padding_x,
- text_y,
- 13.0,
- [0.75, 0.75, 0.80, 1.0],
- font_name,
- );
- text_y += 26.0;
-
- pc.text_with_font(
- "The quick brown fox jumps over the lazy dog.",
- right_x + text_padding_x,
- text_y,
- 18.0,
- [0.90, 0.90, 0.95, 1.0],
- font_name,
- );
- text_y += 32.0;
-
- pc.text_with_font(
- "The five boxing wizards jump quickly.",
- right_x + text_padding_x,
- text_y,
- 24.0,
- [0.95, 0.95, 1.0, 1.0],
- font_name,
- );
-
- right_y += card_h;
- } else {
- pc.text("Select a font to preview", right_x + 12.0, right_y + 20.0, 13.0, TEXT_DIM);
- right_y += 40.0;
- }
+ sec.spacing(list_box_h + 12.0);
- sec.content_y = left_y.max(right_y);
- }
- let list_focused = sec_focused.get(2).copied().unwrap_or(false);
- sec.finish_focused(&mut pc, list_focused);
+ // 3. Info Box
+ let info_h = 96.0;
+ let info_bg = [0.12, 0.18, 0.28, 0.3];
+ let info_border = [0.25, 0.40, 0.60, 0.5];
+ let info_y = sec.ay();
+ pc.rect(info_bg, rx + 12.0, info_y, inner_w, info_h);
+ pc.rect(info_border, rx + 12.0, info_y, inner_w, 1.0);
+ pc.rect(info_border, rx + 12.0, info_y + info_h - 1.0, inner_w, 1.0);
+ pc.rect(info_border, rx + 12.0, info_y, 1.0, info_h);
+ pc.rect(info_border, rx + 12.0 + inner_w - 1.0, info_y, 1.0, info_h);
+ let text_padding_x = 16.0;
+ let mut text_y = info_y + 12.0;
- // Render dropdown popovers on top of all other widgets
+ pc.text("Font Directories & Installation", rx + 12.0 + text_padding_x, text_y, 12.0, [0.35, 0.65, 0.90, 1.0]);
+ text_y += 20.0;
+ pc.text("• Active Directory: ~/Dropbox/Fonts", rx + 12.0 + text_padding_x, text_y, 11.0, [0.80, 0.80, 0.85, 1.0]);
+ text_y += 16.0;
+
+ pc.text("• Place TTF/OTF files there to install new fonts.", rx + 12.0 + text_padding_x, text_y, 11.0, [0.80, 0.80, 0.85, 1.0]);
+ text_y += 16.0;
+
+ pc.text("• Changes will be cached automatically by fontconfig.", rx + 12.0 + text_padding_x, text_y, 11.0, [0.55, 0.55, 0.60, 1.0]);
+
+ sec.spacing(info_h + 12.0);
+
+ // 4. Preview Card
+ if let Some(ref font_name) = state.selected_font {
+ let card_h = 240.0;
+ let card_y = sec.ay();
+ pc.rect([0.10, 0.10, 0.14, 0.3], rx + 12.0, card_y, inner_w, card_h);
+ pc.rect([0.25, 0.25, 0.35, 0.5], rx + 12.0, card_y, inner_w, 1.0);
+ pc.rect([0.25, 0.25, 0.35, 0.5], rx + 12.0, card_y + card_h - 1.0, inner_w, 1.0);
+ pc.rect([0.25, 0.25, 0.35, 0.5], rx + 12.0, card_y, 1.0, card_h);
+ pc.rect([0.25, 0.25, 0.35, 0.5], rx + 12.0 + inner_w - 1.0, card_y, 1.0, card_h);
+
+ let mut p_text_y = card_y + 16.0;
+ pc.text(&format!("Family: {}", font_name), rx + 12.0 + text_padding_x, p_text_y, 15.0, [0.90, 0.90, 0.95, 1.0]);
+ p_text_y += 28.0;
+
+ pc.rect([0.22, 0.22, 0.30, 0.8], rx + 12.0 + text_padding_x, p_text_y, inner_w - (text_padding_x * 2.0), 1.0);
+ p_text_y += 16.0;
+
+ pc.text_with_font(
+ "abcdefghijklmnopqrstuvwxyz",
+ rx + 12.0 + text_padding_x,
+ p_text_y,
+ 13.0,
+ [0.75, 0.75, 0.80, 1.0],
+ font_name,
+ );
+ p_text_y += 22.0;
+
+ pc.text_with_font(
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
+ rx + 12.0 + text_padding_x,
+ p_text_y,
+ 13.0,
+ [0.75, 0.75, 0.80, 1.0],
+ font_name,
+ );
+ p_text_y += 22.0;
+
+ pc.text_with_font(
+ "0123456789 (!@#$%&*?)",
+ rx + 12.0 + text_padding_x,
+ p_text_y,
+ 13.0,
+ [0.75, 0.75, 0.80, 1.0],
+ font_name,
+ );
+ p_text_y += 26.0;
+
+ pc.text_with_font(
+ "The quick brown fox jumps over the lazy dog.",
+ rx + 12.0 + text_padding_x,
+ p_text_y,
+ 16.0,
+ [0.90, 0.90, 0.95, 1.0],
+ font_name,
+ );
+ p_text_y += 32.0;
+
+ pc.text_with_font(
+ "The five boxing wizards jump quickly.",
+ rx + 12.0 + text_padding_x,
+ p_text_y,
+ 20.0,
+ [0.95, 0.95, 1.0, 1.0],
+ font_name,
+ );
+ sec.spacing(card_h + 8.0);
+ } else {
+ pc.text("Select a font to preview", rx + 24.0, sec.ay() + 20.0, 13.0, TEXT_DIM);
+ sec.spacing(40.0);
+ }
+ }
+ let list_focused = sec_focused.get(2).copied().unwrap_or(false);
+ sec.finish_focused(pc, list_focused)
+ });
- pc
+ final_pc
}
pub fn update(state: &mut TypefaceState, msg: TypefaceMessage) {