git.lucas.co / cce-system-interface
system settings
git clone https://git.lucas.co/cce-system-interface.git

commit9d4463256bdfb34a6e7c92fe40ca97af1581ba0a
parent08a66910fb
authorLucas Galante <[email protected]>
date2026-05-24 17:04
Update components and system settings

 src/main.rs                | 242 ++++++++++++++++++++++++++++++++++++++-------
 src/pages/display.rs       | 113 +++++++++++++++++----
 src/pages/input.rs         | 152 +++++++++++++++++++++++++---
 src/pages/notifications.rs |  84 +++++++++++++---
 src/pages/status.rs        |  55 ++++++++---
 5 files changed, 549 insertions(+), 97 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index 1bbf4b3..5960ebf 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -139,6 +139,8 @@ struct SystemInterface {
     width: u32,
     height: u32,
     needs_rebuild: bool,
+    scroll_y: f32,
+    max_scroll_y: f32,
 }
 
 impl SystemInterface {
@@ -301,7 +303,7 @@ impl SystemInterface {
             app,
             font_system, swash_cache, text_atlas, text_renderer, text_viewport,
             widgets: Vec::new(), text_items: Vec::new(), page_buttons: Vec::new(),
-            sidebar_width: 140.0, header_height: 0.0, status_height: 28.0,
+            sidebar_width: 140.0, header_height: 0.0, status_height: 0.0,
             cursor_x: 0.0, cursor_y: 0.0,
             scale_factor,
             rx_power, rx_audio, rx_display, rx_network, rx_layout, rx_input,
@@ -310,6 +312,8 @@ impl SystemInterface {
             tx_color_selector, rx_color_selector,
             width: size.width, height: size.height,
             needs_rebuild: true,
+            scroll_y: 0.0,
+            max_scroll_y: 0.0,
         };
         this.rebuild_layout(sw, sh);
         this
@@ -379,9 +383,24 @@ impl SystemInterface {
         // Page content in LOGICAL coordinates, then scale to physical
         let pc = self.render_page_content(lcx, lcy, lcw, lch);
 
+        let mut max_y = 0.0f32;
+        for (_, _, y, _, h) in &pc.rects {
+            max_y = max_y.max(y + h);
+        }
+        for (_, size, _, y, _) in &pc.texts {
+            max_y = max_y.max(y + size);
+        }
+        for btn in &pc.buttons {
+            max_y = max_y.max(btn.y + btn.h);
+        }
+        self.max_scroll_y = (max_y - lch).max(0.0);
+        self.scroll_y = self.scroll_y.min(self.max_scroll_y);
+
+        let scroll_offset_y = self.scroll_y;
+
         for (c, x, y, w, h) in &pc.rects {
             widgets.push(AppWidget {
-                x: *x * s, y: *y * s, w: *w * s, h: *h * s,
+                x: *x * s, y: (*y - scroll_offset_y) * s, w: *w * s, h: *h * s,
                 color: *c, hover_color: *c,
                 hovering: false, kind: WidgetKind::Static,
             });
@@ -389,7 +408,7 @@ impl SystemInterface {
         for (t, size, x, y, tc) in &pc.texts {
             text_items.push(TextItem {
                 buffer: make_text_buffer(&mut self.font_system, t, *size * s),
-                x: *x * s, y: *y * s,
+                x: *x * s, y: (*y - scroll_offset_y) * s,
                 color: glyphon::Color::rgb(
                     (tc[0] * 255.0) as u8, (tc[1] * 255.0) as u8, (tc[2] * 255.0) as u8,
                 ),
@@ -397,7 +416,7 @@ impl SystemInterface {
         }
         for btn in &pc.buttons {
             widgets.push(AppWidget {
-                x: btn.x * s, y: btn.y * s, w: btn.w * s, h: btn.h * s,
+                x: btn.x * s, y: (btn.y - scroll_offset_y) * s, w: btn.w * s, h: btn.h * s,
                 color: btn.bg, hover_color: btn.hover_bg,
                 hovering: false,
                 kind: WidgetKind::ActionButton(btn.action.clone()),
@@ -407,7 +426,7 @@ impl SystemInterface {
             let lh = btn.label_size * s * 1.4;
             text_items.push(TextItem {
                 buffer: buf,
-                x: btn.x * s + (btn.w * s - tw) / 2.0, y: btn.y * s + (btn.h * s - lh) / 2.0,
+                x: btn.x * s + (btn.w * s - tw) / 2.0, y: (btn.y - scroll_offset_y) * s + (btn.h * s - lh) / 2.0,
                 color: glyphon::Color::rgb(
                     (btn.label_color[0] * 255.0) as u8,
                     (btn.label_color[1] * 255.0) as u8,
@@ -415,16 +434,11 @@ impl SystemInterface {
                 ),
             });
             let mut cb = btn.clone();
-            cb.x *= s; cb.y *= s; cb.w *= s; cb.h *= s;
+            cb.x *= s; cb.y = (cb.y - scroll_offset_y) * s; cb.w *= s; cb.h *= s;
             page_buttons.push(cb);
         }
 
-        // Status bar
-        widgets.push(AppWidget {
-            x: 0.0, y: sh - st_h, w: sw, h: st_h,
-            color: color::STATUS_BG, hover_color: color::STATUS_BG,
-            hovering: false, kind: WidgetKind::Static,
-        });
+
 
         self.widgets = widgets;
         self.text_items = text_items;
@@ -443,7 +457,7 @@ impl SystemInterface {
             Page::Processors => processors::view(&self.app.processors, cx, cy, cw, ch),
             Page::Input => input::view(&mut self.app.input, cx, cy, cw, ch),
             Page::System => system_info::view(&self.app.system_info, cx, cy, cw, ch),
-            Page::Status => status::view(&self.app.status, 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),
@@ -644,9 +658,29 @@ impl SystemInterface {
 
     fn handle_event(&mut self, event: &WindowEvent) -> bool {
         match event {
+            WindowEvent::MouseWheel { delta, .. } => {
+                let s = self.scale_factor as f32;
+                if self.cursor_x >= self.sidebar_width * s {
+                    let scroll_speed = 24.0;
+                    let dy = match delta {
+                        winit::event::MouseScrollDelta::LineDelta(_, y) => -y * scroll_speed,
+                        winit::event::MouseScrollDelta::PixelDelta(pos) => -pos.y as f32,
+                    };
+                    let old_scroll = self.scroll_y;
+                    self.scroll_y = (self.scroll_y + dy).max(0.0).min(self.max_scroll_y);
+                    if (self.scroll_y - old_scroll).abs() > 0.01 {
+                        self.needs_rebuild = true;
+                        return true;
+                    }
+                }
+                false
+            }
             WindowEvent::CursorMoved { position, .. } => {
                 self.cursor_x = position.x as f32;
                 self.cursor_y = position.y as f32;
+                let s = self.scale_factor as f32;
+                let lx = self.cursor_x / s;
+                let ly = self.cursor_y / s + self.scroll_y;
                 let mut changed = false;
                 for w in &mut self.widgets {
                     let was = w.hovering;
@@ -658,76 +692,115 @@ impl SystemInterface {
                     }
                 }
                 if self.app.current_page == Page::Layout {
-                    let s = self.scale_factor as f32;
                     for sb in &mut self.app.layout.spinboxes {
-                        if sb.cursor_moved(self.cursor_x / s, self.cursor_y / s) {
+                        if sb.cursor_moved(lx, ly) {
                             changed = true;
                         }
                     }
-                    if self.app.layout.cascade_offset_spinbox.cursor_moved(self.cursor_x / s, self.cursor_y / s) {
+                    if self.app.layout.cascade_offset_spinbox.cursor_moved(lx, ly) {
                         changed = true;
                     }
-                    if self.app.layout.edge_gap_spinbox.cursor_moved(self.cursor_x / s, self.cursor_y / s) {
+                    if self.app.layout.edge_gap_spinbox.cursor_moved(lx, ly) {
                         changed = true;
                     }
-                    if self.app.layout.top_gap_spinbox.cursor_moved(self.cursor_x / s, self.cursor_y / s) {
+                    if self.app.layout.top_gap_spinbox.cursor_moved(lx, ly) {
                         changed = true;
                     }
                     for cp in &mut self.app.layout.color_selectors {
-                        if cp.cursor_moved(self.cursor_x / s, self.cursor_y / s) {
+                        if cp.cursor_moved(lx, ly) {
                             changed = true;
                         }
                     }
                 }
                 if self.app.current_page == Page::Input {
-                    let s = self.scale_factor as f32;
-                    if self.app.input.rate_spinbox.cursor_moved(self.cursor_x / s, self.cursor_y / s) {
+                    if self.app.input.rate_spinbox.cursor_moved(lx, ly) {
+                        changed = true;
+                    }
+                    if self.app.input.delay_spinbox.cursor_moved(lx, ly) {
+                        changed = true;
+                    }
+                    if self.app.input.tap_toggle.cursor_moved(lx, ly) {
+                        changed = true;
+                    }
+                    if self.app.input.scroll_toggle.cursor_moved(lx, ly) {
+                        changed = true;
+                    }
+                    if self.app.input.scroll_friction_spinbox.cursor_moved(lx, ly) {
+                        changed = true;
+                    }
+                    if self.app.input.pointer_toggle.cursor_moved(lx, ly) {
+                        changed = true;
+                    }
+                    if self.app.input.pointer_friction_spinbox.cursor_moved(lx, ly) {
                         changed = true;
                     }
-                    if self.app.input.delay_spinbox.cursor_moved(self.cursor_x / s, self.cursor_y / s) {
+                    if self.app.input.trackpad_toggle.cursor_moved(lx, ly) {
                         changed = true;
                     }
-                    if self.app.input.tap_toggle.cursor_moved(self.cursor_x / s, self.cursor_y / s) {
+                    if self.app.input.trackpad_friction_spinbox.cursor_moved(lx, ly) {
                         changed = true;
                     }
                 }
                 if self.app.current_page == Page::Audio {
-                    let s = self.scale_factor as f32;
                     for sb in &mut self.app.audio.sink_spinboxes {
-                        if sb.cursor_moved(self.cursor_x / s, self.cursor_y / s) {
+                        if sb.cursor_moved(lx, ly) {
                             changed = true;
                         }
                     }
                     for sb in &mut self.app.audio.source_spinboxes {
-                        if sb.cursor_moved(self.cursor_x / s, self.cursor_y / s) {
+                        if sb.cursor_moved(lx, ly) {
                             changed = true;
                         }
                     }
                 }
                 if self.app.current_page == Page::Display {
-                    let s = self.scale_factor as f32;
-                    if self.app.display.brightness_spinbox.cursor_moved(self.cursor_x / s, self.cursor_y / s) {
+                    if self.app.display.brightness_spinbox.cursor_moved(lx, ly) {
+                        changed = true;
+                    }
+                    if self.app.display.night_light_label.cursor_moved(lx, ly) {
                         changed = true;
                     }
+                    for out in &mut self.app.display.outputs {
+                        if out.name_label.cursor_moved(lx, ly) {
+                            changed = true;
+                        }
+                        if out.resolution_label.cursor_moved(lx, ly) {
+                            changed = true;
+                        }
+                        if let Some(ref mut scale_lbl) = out.scale_label {
+                            if scale_lbl.cursor_moved(lx, ly) {
+                                changed = true;
+                            }
+                        }
+                    }
                 }
                 if self.app.current_page == Page::Notifications {
-                    let s = self.scale_factor as f32;
-                    if self.app.notifications.enable_toggle.cursor_moved(self.cursor_x / s, self.cursor_y / s) {
+                    if self.app.notifications.enable_toggle.cursor_moved(lx, ly) {
+                        changed = true;
+                    }
+                    if self.app.notifications.bell_toggle.cursor_moved(lx, ly) {
                         changed = true;
                     }
-                    if self.app.notifications.bell_toggle.cursor_moved(self.cursor_x / s, self.cursor_y / s) {
+                    if self.app.notifications.duration_spinbox.cursor_moved(lx, ly) {
+                        changed = true;
+                    }
+                }
+                if self.app.current_page == Page::Status {
+                    if self.app.status.status_label.cursor_moved(lx, ly) {
+                        changed = true;
+                    }
+                    if self.app.status.size_label.cursor_moved(lx, ly) {
                         changed = true;
                     }
                 }
                 if self.app.current_page == Page::Typeface {
-                    let s = self.scale_factor as f32;
-                    if self.app.typeface.sans_box.cursor_moved(self.cursor_x / s, self.cursor_y / s) {
+                    if self.app.typeface.sans_box.cursor_moved(lx, ly) {
                         changed = true;
                     }
-                    if self.app.typeface.serif_box.cursor_moved(self.cursor_x / s, self.cursor_y / s) {
+                    if self.app.typeface.serif_box.cursor_moved(lx, ly) {
                         changed = true;
                     }
-                    if self.app.typeface.mono_box.cursor_moved(self.cursor_x / s, self.cursor_y / s) {
+                    if self.app.typeface.mono_box.cursor_moved(lx, ly) {
                         changed = true;
                     }
                 }
@@ -802,6 +875,19 @@ impl SystemInterface {
                         return true;
                     }
                 }
+                if self.app.current_page == Page::Notifications {
+                    let sb = &mut self.app.notifications.duration_spinbox;
+                    let old = sb.value;
+                    if sb.keyboard_input(event) {
+                        let new_val = sb.value;
+                        drop(sb);
+                        if new_val != old {
+                            self.handle_action(&AppAction::Notifications(pages::notifications::NotificationsMessage::SetDuration(new_val)));
+                        }
+                        self.needs_rebuild = true;
+                        return true;
+                    }
+                }
                 if self.app.current_page == Page::Input {
                     if self.app.input.rate_spinbox.keyboard_input(event) {
                         self.handle_action(&AppAction::Input(pages::input::InputMessage::ApplyRepeat));
@@ -813,6 +899,21 @@ impl SystemInterface {
                         self.needs_rebuild = true;
                         return true;
                     }
+                    if self.app.input.scroll_friction_spinbox.keyboard_input(event) {
+                        self.handle_action(&AppAction::Input(pages::input::InputMessage::ApplyScrollFriction));
+                        self.needs_rebuild = true;
+                        return true;
+                    }
+                    if self.app.input.pointer_friction_spinbox.keyboard_input(event) {
+                        self.handle_action(&AppAction::Input(pages::input::InputMessage::ApplyPointerFriction));
+                        self.needs_rebuild = true;
+                        return true;
+                    }
+                    if self.app.input.trackpad_friction_spinbox.keyboard_input(event) {
+                        self.handle_action(&AppAction::Input(pages::input::InputMessage::ApplyTrackpadFriction));
+                        self.needs_rebuild = true;
+                        return true;
+                    }
                 }
                 if self.app.current_page == Page::Audio {
                     let mut actions = Vec::new();
@@ -909,6 +1010,7 @@ impl SystemInterface {
                             if let WidgetKind::PageButton(p) = &w.kind {
                                 if self.app.current_page != *p {
                                     self.app.current_page = *p;
+                                    self.scroll_y = 0.0;
                                     self.needs_rebuild = true;
                                     return true;
                                 }
@@ -918,7 +1020,7 @@ impl SystemInterface {
                 }
                 let s = self.scale_factor as f32;
                 let lx = self.cursor_x / s;
-                let ly = self.cursor_y / s;
+                let ly = self.cursor_y / s + self.scroll_y;
                 let mut actions = Vec::new();
                 if *state == ElementState::Pressed && self.app.current_page == Page::Layout {
                     for (i, sb) in self.app.layout.spinboxes.iter_mut().enumerate() {
@@ -988,6 +1090,32 @@ impl SystemInterface {
                     if sb.mouse_input(*button, *state, lx, ly) && sb.value != old {
                         actions.push(AppAction::Input(pages::input::InputMessage::ApplyRepeat));
                     }
+                    let sb = &mut self.app.input.scroll_friction_spinbox;
+                    if !sb.hit_test(lx, ly) { sb.unfocus(); }
+                    let old = sb.value;
+                    if sb.mouse_input(*button, *state, lx, ly) && sb.value != old {
+                        actions.push(AppAction::Input(pages::input::InputMessage::ApplyScrollFriction));
+                    }
+                    let sb = &mut self.app.input.pointer_friction_spinbox;
+                    if !sb.hit_test(lx, ly) { sb.unfocus(); }
+                    let old = sb.value;
+                    if sb.mouse_input(*button, *state, lx, ly) && sb.value != old {
+                        actions.push(AppAction::Input(pages::input::InputMessage::ApplyPointerFriction));
+                    }
+                    let sb = &mut self.app.input.trackpad_friction_spinbox;
+                    if !sb.hit_test(lx, ly) { sb.unfocus(); }
+                    let old = sb.value;
+                    if sb.mouse_input(*button, *state, lx, ly) && sb.value != old {
+                        actions.push(AppAction::Input(pages::input::InputMessage::ApplyTrackpadFriction));
+                    }
+                }
+                if *state == ElementState::Pressed && self.app.current_page == Page::Notifications {
+                    let sb = &mut self.app.notifications.duration_spinbox;
+                    if !sb.hit_test(lx, ly) { sb.unfocus(); }
+                    let old = sb.value;
+                    if sb.mouse_input(*button, *state, lx, ly) && sb.value != old {
+                        actions.push(AppAction::Notifications(pages::notifications::NotificationsMessage::SetDuration(sb.value)));
+                    }
                 }
                 if self.app.current_page == Page::Input {
                     let toggle = &mut self.app.input.tap_toggle;
@@ -995,6 +1123,21 @@ impl SystemInterface {
                     if toggle.take_click() {
                         actions.push(AppAction::Input(pages::input::InputMessage::ToggleTapToClick));
                     }
+                    let toggle = &mut self.app.input.scroll_toggle;
+                    toggle.mouse_input(*button, *state, lx, ly);
+                    if toggle.take_click() {
+                        actions.push(AppAction::Input(pages::input::InputMessage::ToggleInertialScroll));
+                    }
+                    let toggle = &mut self.app.input.pointer_toggle;
+                    toggle.mouse_input(*button, *state, lx, ly);
+                    if toggle.take_click() {
+                        actions.push(AppAction::Input(pages::input::InputMessage::ToggleInertialPointer));
+                    }
+                    let toggle = &mut self.app.input.trackpad_toggle;
+                    toggle.mouse_input(*button, *state, lx, ly);
+                    if toggle.take_click() {
+                        actions.push(AppAction::Input(pages::input::InputMessage::ToggleInertialTrackpad));
+                    }
                 }
                 if self.app.current_page == Page::Notifications {
                     let toggle = &mut self.app.notifications.enable_toggle;
@@ -1033,6 +1176,33 @@ impl SystemInterface {
                     if sb.mouse_input(*button, *state, lx, ly) && sb.value != old {
                         actions.push(AppAction::Display(pages::display::DisplayMessage::BrightnessSet(sb.value as u32)));
                     }
+                    let lbl = &mut self.app.display.night_light_label;
+                    if !lbl.hit_test(lx, ly) { lbl.unfocus(); }
+                    lbl.mouse_input(*button, *state, lx, ly);
+
+                    for out in &mut self.app.display.outputs {
+                        let lbl = &mut out.name_label;
+                        if !lbl.hit_test(lx, ly) { lbl.unfocus(); }
+                        lbl.mouse_input(*button, *state, lx, ly);
+
+                        let lbl2 = &mut out.resolution_label;
+                        if !lbl2.hit_test(lx, ly) { lbl2.unfocus(); }
+                        lbl2.mouse_input(*button, *state, lx, ly);
+
+                        if let Some(ref mut scale_lbl) = out.scale_label {
+                            if !scale_lbl.hit_test(lx, ly) { scale_lbl.unfocus(); }
+                            scale_lbl.mouse_input(*button, *state, lx, ly);
+                        }
+                    }
+                }
+                if *state == ElementState::Pressed && self.app.current_page == Page::Status {
+                    let lbl1 = &mut self.app.status.status_label;
+                    if !lbl1.hit_test(lx, ly) { lbl1.unfocus(); }
+                    lbl1.mouse_input(*button, *state, lx, ly);
+
+                    let lbl2 = &mut self.app.status.size_label;
+                    if !lbl2.hit_test(lx, ly) { lbl2.unfocus(); }
+                    lbl2.mouse_input(*button, *state, lx, ly);
                 }
                 if *state == ElementState::Pressed && self.app.current_page == Page::Typeface {
                     let tb = &mut self.app.typeface.sans_box;
diff --git a/src/pages/display.rs b/src/pages/display.rs
index dc350fb..20d96d9 100644
--- a/src/pages/display.rs
+++ b/src/pages/display.rs
@@ -1,6 +1,6 @@
 use crate::app::PageContent;
 use clear_ui::layout::{render_widget, Section};
-use clear_ui::widget::{Spinbox, Widget};
+use clear_ui::widget::{Spinbox, Label, Widget};
 
 #[derive(Debug, Clone)]
 pub struct DisplayOutput {
@@ -9,6 +9,37 @@ pub struct DisplayOutput {
     pub refresh: String,
     pub scale: f32,
     pub connected: bool,
+    pub name_label: Label,
+    pub resolution_label: Label,
+    pub scale_label: Option<Label>,
+}
+
+impl DisplayOutput {
+    pub fn update_labels(&mut self) {
+        if self.connected {
+            self.name_label.set_text(&self.name);
+            self.resolution_label.set_text(&format!("{} @ {}Hz", self.resolution, self.refresh));
+            if self.scale > 1.0 {
+                let scale_str = if let Some((w_str, h_str)) = self.resolution.rsplit_once('x') {
+                    if let (Ok(w), Ok(h)) = (w_str.parse::<u32>(), h_str.parse::<u32>()) {
+                        format!("logical {:.0}x{:.0} | scale {:.0}x", w as f32 / self.scale, h as f32 / self.scale, self.scale)
+                    } else { format!("scale {:.0}x", self.scale) }
+                } else { format!("scale {:.0}x", self.scale) };
+                
+                self.scale_label = Some(Label::new(&scale_str)
+                    .with_font_size(11.0)
+                    .with_color([135, 135, 150]));
+            } else {
+                self.scale_label = None;
+            }
+        } else {
+            self.name_label.set_text(&self.name);
+            self.name_label.set_color([135, 135, 150]);
+            self.resolution_label.set_text("(disconnected)");
+            self.resolution_label.set_color([135, 135, 150]);
+            self.scale_label = None;
+        }
+    }
 }
 
 #[derive(Debug, Clone)]
@@ -19,6 +50,7 @@ pub struct DisplayState {
     pub outputs: Vec<DisplayOutput>,
     pub night_light: bool,
     pub brightness_spinbox: Spinbox,
+    pub night_light_label: Label,
 }
 
 impl Default for DisplayState {
@@ -30,6 +62,7 @@ impl Default for DisplayState {
             outputs: Vec::new(),
             night_light: false,
             brightness_spinbox: Spinbox::new(50, 0, 100, 5),
+            night_light_label: Label::new("Night Light: OFF").with_font_size(13.0).with_color([0xd4, 0xd4, 0xd4]),
         }
     }
 }
@@ -51,6 +84,9 @@ pub async fn fetch_display_state() -> DisplayState {
         loaded: true,
         brightness, max_brightness, outputs, night_light,
         brightness_spinbox: Spinbox::new(pct, 0, 100, 5),
+        night_light_label: Label::new(if night_light { "Night Light: ON" } else { "Night Light: OFF" })
+            .with_font_size(13.0)
+            .with_color([0xd4, 0xd4, 0xd4]),
     }
 }
 
@@ -78,11 +114,19 @@ async fn fetch_outputs() -> Vec<DisplayOutput> {
     for line in output.lines() {
         let trimmed = line.trim();
         if !trimmed.starts_with(' ') && trimmed.contains('"') {
-            if let Some(prev) = current.take() { displays.push(prev); }
+            if let Some(mut prev) = current.take() {
+                prev.update_labels();
+                displays.push(prev);
+            }
             let name = trimmed.split_whitespace().next().unwrap_or("").to_string();
+            let name_label = Label::new(&name).with_font_size(12.0).with_color([212, 212, 212]);
+            let resolution_label = Label::new("").with_font_size(12.0).with_color([212, 212, 212]);
             current = Some(DisplayOutput {
                 name, resolution: String::new(), refresh: String::new(),
                 scale: 1.0, connected: true,
+                name_label,
+                resolution_label,
+                scale_label: None,
             });
             continue;
         }
@@ -100,7 +144,10 @@ async fn fetch_outputs() -> Vec<DisplayOutput> {
             }
         }
     }
-    if let Some(prev) = current.take() { displays.push(prev); }
+    if let Some(mut prev) = current.take() {
+        prev.update_labels();
+        displays.push(prev);
+    }
     displays
 }
 
@@ -160,9 +207,12 @@ pub fn view(state: &mut DisplayState, cx: f32, cy: f32, cw: f32, _ch: f32) -> Pa
     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" };
-        sec.text(&mut pc, nl_label, 12.0, 0.0, 13.0, TEXT_FG);
+        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);
 
@@ -173,21 +223,22 @@ pub fn view(state: &mut DisplayState, cx: f32, cy: f32, cw: f32, _ch: f32) -> Pa
         sec.text(&mut pc, "Loading outputs...", 12.0, 0.0, 12.0, TEXT_DIM);
         sec.spacing(18.0);
     } else {
-        for out in &state.outputs {
-            if out.connected {
-                let scale_info = if out.scale > 1.0 {
-                    if let Some((w_str, h_str)) = out.resolution.rsplit_once('x') {
-                        if let (Ok(w), Ok(h)) = (w_str.parse::<u32>(), h_str.parse::<u32>()) {
-                            format!("  logical {:.0}x{:.0} | scale {:.0}x", w as f32 / out.scale, h as f32 / out.scale, out.scale)
-                        } else { format!("  scale {:.0}x", out.scale) }
-                    } else { String::new() }
-                } else { String::new() };
-                sec.text(&mut pc, &format!("{}  {} @ {}Hz{}", out.name, out.resolution, out.refresh, scale_info),
-                    14.0, 0.0, 12.0, TEXT_FG);
-            } else {
-                sec.text(&mut pc, &format!("{}  (disconnected)", out.name), 14.0, 0.0, 12.0, TEXT_DIM);
+        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);
             }
-            sec.spacing(18.0);
+            
+            sec.content_y += 20.0;
+            sec.spacing(12.0);
         }
     }
     sec.finish(&mut pc);
@@ -197,7 +248,31 @@ pub fn view(state: &mut DisplayState, cx: f32, cy: f32, cw: f32, _ch: f32) -> Pa
 
 pub fn update(state: &mut DisplayState, msg: DisplayMessage) {
     match msg {
-        DisplayMessage::Refreshed(new) => { *state = new; }
+        DisplayMessage::Refreshed(new) => {
+            let was_nl_hovered = state.night_light_label.hovered();
+            
+            let mut hovers = std::collections::HashMap::new();
+            for out in &state.outputs {
+                hovers.insert(out.name.clone(), (
+                    out.name_label.hovered(),
+                    out.resolution_label.hovered(),
+                    out.scale_label.as_ref().map(|l| l.hovered()).unwrap_or(false)
+                ));
+            }
+            
+            *state = new;
+            state.night_light_label.set_hovered(was_nl_hovered);
+            
+            for out in &mut state.outputs {
+                if let Some(&(name_h, res_h, scale_h)) = hovers.get(&out.name) {
+                    out.name_label.set_hovered(name_h);
+                    out.resolution_label.set_hovered(res_h);
+                    if let Some(ref mut scale_lbl) = out.scale_label {
+                        scale_lbl.set_hovered(scale_h);
+                    }
+                }
+            }
+        }
         DisplayMessage::BrightnessSet(pct) => {
             let pct = pct.clamp(0, 100);
             state.brightness = pct as f32 / 100.0 * state.max_brightness;
diff --git a/src/pages/input.rs b/src/pages/input.rs
index b9a82fd..3cade7f 100644
--- a/src/pages/input.rs
+++ b/src/pages/input.rs
@@ -1,9 +1,9 @@
 use std::fs;
 use std::io::Write;
 
-use crate::app::{AppAction, PageContent};
-use clear_ui::layout::{render_widget, Section};
-use clear_ui::widget::{Spinbox, Toggle, Widget};
+use crate::app::PageContent;
+use clear_ui::layout::Section;
+use clear_ui::widget::{Spinbox, Toggle};
 
 const CONFIG_PATH: &str = "/home/lsgalante/.config/clearwm/config.toml";
 const CLEARWM_SOCK: &str = "/tmp/clearwm.sock";
@@ -25,6 +25,21 @@ pub struct InputState {
     pub delay_spinbox: Spinbox,
     pub tap_toggle: Toggle,
     pub keybinds: Vec<Keybind>,
+
+    // Inertial settings
+    pub inertial_scroll: bool,
+    pub scroll_friction: u16,
+    pub inertial_pointer: bool,
+    pub pointer_friction: u16,
+    pub inertial_trackpad: bool,
+    pub trackpad_friction: u16,
+
+    pub scroll_toggle: Toggle,
+    pub scroll_friction_spinbox: Spinbox,
+    pub pointer_toggle: Toggle,
+    pub pointer_friction_spinbox: Spinbox,
+    pub trackpad_toggle: Toggle,
+    pub trackpad_friction_spinbox: Spinbox,
 }
 
 impl Default for InputState {
@@ -35,8 +50,22 @@ impl Default for InputState {
             repeat_delay: 300,
             rate_spinbox: Spinbox::new(50, 1, 100, 1).with_label("Repeat Rate").with_unit("ms"),
             delay_spinbox: Spinbox::new(300, 100, 2000, 10).with_label("Repeat Delay").with_unit("ms"),
-        tap_toggle: Toggle::new().with_label("Tap to Click"),
+            tap_toggle: Toggle::new().with_label("Tap to Click"),
             keybinds: Vec::new(),
+
+            inertial_scroll: true,
+            scroll_friction: 90,
+            inertial_pointer: false,
+            pointer_friction: 95,
+            inertial_trackpad: false,
+            trackpad_friction: 95,
+
+            scroll_toggle: Toggle::new().with_label("Inertial Scroll"),
+            scroll_friction_spinbox: Spinbox::new(90, 50, 99, 1).with_label("Scroll Friction").with_unit("%"),
+            pointer_toggle: Toggle::new().with_label("Inertial Pointer (Trackpoint)"),
+            pointer_friction_spinbox: Spinbox::new(95, 50, 99, 1).with_label("Pointer Friction").with_unit("%"),
+            trackpad_toggle: Toggle::new().with_label("Inertial Pointer (Trackpad)"),
+            trackpad_friction_spinbox: Spinbox::new(95, 50, 99, 1).with_label("Trackpad Friction").with_unit("%"),
         }
     }
 }
@@ -46,6 +75,13 @@ pub enum InputMessage {
     ToggleTapToClick,
     ApplyRepeat,
     Refreshed(InputState),
+
+    ToggleInertialScroll,
+    ApplyScrollFriction,
+    ToggleInertialPointer,
+    ApplyPointerFriction,
+    ToggleInertialTrackpad,
+    ApplyTrackpadFriction,
 }
 
 pub fn read_input_config() -> InputState {
@@ -53,6 +89,14 @@ pub fn read_input_config() -> InputState {
     let rate = parse_u16_key(&content, "rate", 50);
     let delay = parse_u16_key(&content, "delay", 300);
     let tap = parse_bool_from(&content, "tap_to_click");
+
+    let inertial_scroll = parse_bool_from_default(&content, "inertial_scroll", true);
+    let scroll_friction = parse_u16_key(&content, "scroll_friction", 90);
+    let inertial_pointer = parse_bool_from_default(&content, "inertial_pointer", false);
+    let pointer_friction = parse_u16_key(&content, "pointer_friction", 95);
+    let inertial_trackpad = parse_bool_from_default(&content, "inertial_trackpad", false);
+    let trackpad_friction = parse_u16_key(&content, "trackpad_friction", 95);
+
     InputState {
         tap_to_click: tap,
         repeat_rate: rate,
@@ -61,6 +105,20 @@ pub fn read_input_config() -> InputState {
         delay_spinbox: Spinbox::new(delay as i32, 100, 2000, 10).with_label("Repeat Delay").with_unit("ms"),
         tap_toggle: Toggle::new().with_label("Tap to Click"),
         keybinds: parse_keybinds(&content),
+
+        inertial_scroll,
+        scroll_friction,
+        inertial_pointer,
+        pointer_friction,
+        inertial_trackpad,
+        trackpad_friction,
+
+        scroll_toggle: Toggle::new().with_label("Inertial Scroll"),
+        scroll_friction_spinbox: Spinbox::new(scroll_friction as i32, 50, 99, 1).with_label("Scroll Friction").with_unit("%"),
+        pointer_toggle: Toggle::new().with_label("Inertial Pointer (Trackpoint)"),
+        pointer_friction_spinbox: Spinbox::new(pointer_friction as i32, 50, 99, 1).with_label("Pointer Friction").with_unit("%"),
+        trackpad_toggle: Toggle::new().with_label("Inertial Pointer (Trackpad)"),
+        trackpad_friction_spinbox: Spinbox::new(trackpad_friction as i32, 50, 99, 1).with_label("Trackpad Friction").with_unit("%"),
     }
 }
 
@@ -71,6 +129,13 @@ fn parse_bool_from(content: &str, key: &str) -> bool {
         .unwrap_or(false)
 }
 
+fn parse_bool_from_default(content: &str, key: &str, default: bool) -> bool {
+    content.lines().find(|l| l.trim().starts_with(key))
+        .and_then(|l| l.split('=').nth(1))
+        .map(|v| v.trim() == "true")
+        .unwrap_or(default)
+}
+
 fn parse_u16_key(content: &str, key: &str, default: u16) -> u16 {
     content.lines().find(|l| l.trim().starts_with(key))
         .and_then(|l| l.split('=').nth(1))
@@ -124,7 +189,16 @@ fn write_config_value(key: &str, value: &str) {
         .join("\n");
 
     if !found {
-        let section = if key == "tap_to_click" { "[input]" } else { "[repeat]" };
+        let section = if key == "tap_to_click" {
+            "[input]"
+        } else if key == "inertial_scroll" || key == "scroll_friction"
+               || key == "inertial_pointer" || key == "pointer_friction"
+               || key == "inertial_trackpad" || key == "trackpad_friction" {
+            "[inertial]"
+        } else {
+            "[repeat]"
+        };
+
         let mut result = String::new();
         let mut in_section = false;
         let mut inserted = false;
@@ -162,11 +236,6 @@ 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];
-const ACCENT: [f32; 4] = [0.36, 0.56, 0.38, 1.0];
-const BTN_HOVER: [f32; 4] = [0.25, 0.30, 0.26, 1.0];
-const TOGGLE_ON: [f32; 4] = [0.16, 0.41, 0.18, 1.0];
-const TOGGLE_OFF: [f32; 4] = [0.16, 0.16, 0.24, 1.0];
-const WHITE: [f32; 4] = [1.0, 1.0, 1.0, 1.0];
 
 pub fn view(state: &mut InputState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
     let mut pc = PageContent::new();
@@ -175,13 +244,11 @@ pub fn view(state: &mut InputState, cx: f32, cy: f32, cw: f32, _ch: f32) -> Page
     // ── Touchpad ──
     let mut sec = Section::new(&mut pc, cx, y, cw, "Touchpad");
 
-    let yt = sec.ay();
     let toggle_w = 48.0;
     let toggle_h = 24.0;
     state.tap_toggle.set_toggled(state.tap_to_click);
-    state.tap_toggle.set_row_rect(sec.ax(8.0), cw - 16.0);
-    render_widget(&mut pc, &mut state.tap_toggle, sec.ax(100.0), yt, toggle_w, toggle_h);
-    sec.content_y += toggle_h + 12.0;
+    sec.widget(&mut pc, &mut state.tap_toggle, 14.0, toggle_w, toggle_h);
+    sec.spacing(4.0);
     y = sec.finish(&mut pc);
 
     // ── Keyboard ──
@@ -194,6 +261,32 @@ pub fn view(state: &mut InputState, cx: f32, cy: f32, cw: f32, _ch: f32) -> Page
     sec.spacing(8.0);
     y = sec.finish(&mut pc);
 
+    // ── Inertial Input ──
+    let mut sec = Section::new(&mut pc, cx, y, cw, "Inertial Input");
+
+    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);
+
+    sec.widget(&mut pc, &mut state.scroll_friction_spinbox, 14.0, 200.0, 26.0);
+    sec.spacing(16.0);
+
+    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);
+
+    sec.widget(&mut 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);
+
+    sec.widget(&mut pc, &mut state.trackpad_friction_spinbox, 14.0, 200.0, 26.0);
+    sec.spacing(8.0);
+
+    y = sec.finish(&mut pc);
+
     // ── Keybindings ──
     let mut sec = Section::new(&mut pc, cx, y, cw, "Keyboard Bindings");
 
@@ -231,6 +324,35 @@ pub fn update(state: &mut InputState, msg: InputMessage) {
             state.repeat_delay = delay;
             apply_repeat_config(rate, delay);
         }
-        InputMessage::Refreshed(new) => { *state = new; }
+        InputMessage::ToggleInertialScroll => {
+            state.inertial_scroll = !state.inertial_scroll;
+            write_config_value("inertial_scroll", &state.inertial_scroll.to_string());
+        }
+        InputMessage::ApplyScrollFriction => {
+            let friction = state.scroll_friction_spinbox.value.max(50).min(99) as u16;
+            state.scroll_friction = friction;
+            write_config_value("scroll_friction", &friction.to_string());
+        }
+        InputMessage::ToggleInertialPointer => {
+            state.inertial_pointer = !state.inertial_pointer;
+            write_config_value("inertial_pointer", &state.inertial_pointer.to_string());
+        }
+        InputMessage::ApplyPointerFriction => {
+            let friction = state.pointer_friction_spinbox.value.max(50).min(99) as u16;
+            state.pointer_friction = friction;
+            write_config_value("pointer_friction", &friction.to_string());
+        }
+        InputMessage::ToggleInertialTrackpad => {
+            state.inertial_trackpad = !state.inertial_trackpad;
+            write_config_value("inertial_trackpad", &state.inertial_trackpad.to_string());
+        }
+        InputMessage::ApplyTrackpadFriction => {
+            let friction = state.trackpad_friction_spinbox.value.max(50).min(99) as u16;
+            state.trackpad_friction = friction;
+            write_config_value("trackpad_friction", &friction.to_string());
+        }
+        InputMessage::Refreshed(new) => {
+            *state = new;
+        }
     }
 }
diff --git a/src/pages/notifications.rs b/src/pages/notifications.rs
index 441dd5b..db56d2c 100644
--- a/src/pages/notifications.rs
+++ b/src/pages/notifications.rs
@@ -2,8 +2,8 @@ use std::fs;
 use std::io::Write;
 
 use crate::app::{AppAction, PageContent};
-use clear_ui::layout::{render_widget, Section};
-use clear_ui::widget::{Toggle, Widget};
+use clear_ui::layout::Section;
+use clear_ui::widget::{Toggle, Spinbox};
 
 const CONFIG_PATH: &str = "/home/lsgalante/.config/clearwm/config.toml";
 const CLEARWM_SOCK: &str = "/tmp/clearwm.sock";
@@ -14,6 +14,8 @@ pub struct NotificationsState {
     pub enable_toggle: Toggle,
     pub bell: bool,
     pub bell_toggle: Toggle,
+    pub duration: i32,
+    pub duration_spinbox: Spinbox,
 }
 
 impl Default for NotificationsState {
@@ -23,6 +25,10 @@ impl Default for NotificationsState {
             enable_toggle: Toggle::new().with_label("Enable Notifications"),
             bell: false,
             bell_toggle: Toggle::new().with_label("Play Bell Sound"),
+            duration: 5,
+            duration_spinbox: Spinbox::new(5, 1, 60, 1)
+                .with_label("Notification Duration")
+                .with_unit("s"),
         }
     }
 }
@@ -31,6 +37,7 @@ impl Default for NotificationsState {
 pub enum NotificationsMessage {
     ToggleEnable,
     ToggleBell,
+    SetDuration(i32),
     SendTestNotification,
     Refreshed(NotificationsState),
 }
@@ -39,11 +46,16 @@ pub fn read_notifications_config() -> NotificationsState {
     let content = fs::read_to_string(CONFIG_PATH).unwrap_or_default();
     let enable = parse_notifications_enable(&content);
     let bell = parse_notifications_bell(&content);
+    let duration = parse_notifications_duration(&content);
     NotificationsState {
         enable,
         enable_toggle: Toggle::new().with_label("Enable Notifications"),
         bell,
         bell_toggle: Toggle::new().with_label("Play Bell Sound"),
+        duration,
+        duration_spinbox: Spinbox::new(duration, 1, 60, 1)
+            .with_label("Notification Duration")
+            .with_unit("s"),
     }
 }
 
@@ -87,6 +99,28 @@ fn parse_notifications_bell(content: &str) -> bool {
     false // default to false
 }
 
+fn parse_notifications_duration(content: &str) -> i32 {
+    let mut in_section = false;
+    for line in content.lines() {
+        let trimmed = line.trim();
+        if trimmed == "[notifications]" {
+            in_section = true;
+            continue;
+        }
+        if trimmed.starts_with('[') && in_section {
+            break;
+        }
+        if in_section && trimmed.starts_with("duration") {
+            if let Some(val) = trimmed.split('=').nth(1) {
+                if let Ok(d) = val.trim().parse::<i32>() {
+                    return d;
+                }
+            }
+        }
+    }
+    5 // default to 5 seconds
+}
+
 fn send_ipc_command(cmd: &str) {
     if let Ok(mut stream) = std::os::unix::net::UnixStream::connect(CLEARWM_SOCK) {
         let _ = stream.write_all(format!("{}\n", cmd).as_bytes());
@@ -164,9 +198,9 @@ fn write_enable_notifications(enabled: bool) {
     send_ipc_command("reload");
 }
 
-const TEXT_FG: [f32; 4] = [0.83, 0.83, 0.83, 1.0];
-const ACCENT: [f32; 4] = [0.36, 0.56, 0.38, 1.0];
-const BTN_HOVER: [f32; 4] = [0.25, 0.30, 0.26, 1.0];
+const BTN_BG: [f32; 4] = [0.20, 0.40, 0.65, 1.0];
+const BTN_HOVER: [f32; 4] = [0.28, 0.50, 0.78, 1.0];
+const WHITE: [f32; 4] = [1.0, 1.0, 1.0, 1.0];
 
 pub fn view(state: &mut NotificationsState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
     let mut pc = PageContent::new();
@@ -174,19 +208,20 @@ pub fn view(state: &mut NotificationsState, cx: f32, cy: f32, cw: f32, _ch: f32)
 
     let mut sec = Section::new(&mut pc, cx, y, cw, "System Notifications");
 
-    let yt = sec.ay();
     let toggle_w = 48.0;
     let toggle_h = 24.0;
     state.enable_toggle.set_toggled(state.enable);
-    state.enable_toggle.set_row_rect(sec.ax(8.0), cw - 16.0);
-    render_widget(&mut pc, &mut state.enable_toggle, sec.ax(100.0), yt, toggle_w, toggle_h);
-    sec.content_y += toggle_h + 12.0;
+    sec.widget(&mut pc, &mut state.enable_toggle, 14.0, toggle_w, toggle_h);
+    sec.spacing(8.0);
 
-    let yt2 = sec.ay();
     state.bell_toggle.set_toggled(state.bell);
-    state.bell_toggle.set_row_rect(sec.ax(8.0), cw - 16.0);
-    render_widget(&mut pc, &mut state.bell_toggle, sec.ax(100.0), yt2, toggle_w, toggle_h);
-    sec.content_y += toggle_h + 24.0;
+    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;
@@ -198,9 +233,9 @@ pub fn view(state: &mut NotificationsState, cx: f32, cy: f32, cw: f32, _ch: f32)
             btn_y,
             btn_w,
             btn_h,
-            ACCENT,
+            BTN_BG,
             BTN_HOVER,
-            TEXT_FG,
+            WHITE,
             AppAction::Notifications(NotificationsMessage::SendTestNotification),
         );
     });
@@ -220,6 +255,10 @@ pub fn update(state: &mut NotificationsState, msg: NotificationsMessage) {
             state.bell = !state.bell;
             write_config_value("bell", &state.bell.to_string());
         }
+        NotificationsMessage::SetDuration(d) => {
+            state.duration = d;
+            write_config_value("duration", &state.duration.to_string());
+        }
         NotificationsMessage::SendTestNotification => {
             send_ipc_command("notify \"clearwm\" \"System notifications are working correctly!\"");
         }
@@ -280,4 +319,19 @@ enable = true
 ";
         assert!(!parse_notifications_enable(content));
     }
+
+    #[test]
+    fn test_parse_notifications_duration_default() {
+        assert_eq!(parse_notifications_duration(""), 5);
+        assert_eq!(parse_notifications_duration("[notifications]\n"), 5);
+    }
+
+    #[test]
+    fn test_parse_notifications_duration_explicit() {
+        let content = "\
+[notifications]
+duration = 10
+";
+        assert_eq!(parse_notifications_duration(content), 10);
+    }
 }
diff --git a/src/pages/status.rs b/src/pages/status.rs
index 353c07a..d47da52 100644
--- a/src/pages/status.rs
+++ b/src/pages/status.rs
@@ -1,11 +1,26 @@
 use crate::app::{AppAction, PageContent};
 use clear_ui::layout::Section;
+use clear_ui::widget::{Label, Widget};
 
-#[derive(Debug, Clone, Default)]
+#[derive(Debug, Clone)]
 pub struct StatusState {
     pub font_size: u16,
     pub running: bool,
     pub loaded: bool,
+    pub status_label: Label,
+    pub size_label: Label,
+}
+
+impl Default for StatusState {
+    fn default() -> Self {
+        Self {
+            font_size: 13,
+            running: false,
+            loaded: false,
+            status_label: Label::new("Waybar: Stopped").with_font_size(14.0).with_color([170, 51, 51]),
+            size_label: Label::new("Font size: 13px").with_font_size(13.0).with_color([212, 212, 212]),
+        }
+    }
 }
 
 #[derive(Debug, Clone)]
@@ -23,7 +38,18 @@ pub async fn fetch_status_state() -> StatusState {
         .unwrap_or(false);
 
     let font_size = read_waybar_font_size().unwrap_or(13);
-    StatusState { font_size, running, loaded: true }
+    let status_color = if running { [92, 143, 97] } else { [170, 51, 51] };
+    StatusState {
+        font_size,
+        running,
+        loaded: true,
+        status_label: Label::new(&format!("Waybar: {}", if running { "Running" } else { "Stopped" }))
+            .with_font_size(14.0)
+            .with_color(status_color),
+        size_label: Label::new(&format!("Font size: {}px", font_size))
+            .with_font_size(13.0)
+            .with_color([212, 212, 212]),
+    }
 }
 
 fn read_waybar_font_size() -> Option<u16> {
@@ -66,10 +92,9 @@ const TEXT_FG: [f32; 4] = [0.83, 0.83, 0.83, 1.0];
 const BTN_ACTIVE: [f32; 4] = [0.20, 0.40, 0.22, 1.0];
 const BTN_INACTIVE: [f32; 4] = [0.13, 0.18, 0.14, 1.0];
 const BTN_HOVER: [f32; 4] = [0.25, 0.30, 0.26, 1.0];
-const ACCENT: [f32; 4] = [0.36, 0.56, 0.38, 1.0];
 const WHITE: [f32; 4] = [1.0, 1.0, 1.0, 1.0];
 
-pub fn view(state: &StatusState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
+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;
 
@@ -80,15 +105,15 @@ pub fn view(state: &StatusState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageCon
         sec.spacing(18.0);
     } else {
         // Status
-        let status_color = if state.running { ACCENT } else { [0.67, 0.20, 0.20, 1.0] };
-        let status_text = if state.running { "Running" } else { "Stopped" };
-        sec.text(&mut pc, "Waybar", 12.0, 0.0, 14.0, TEXT_FG);
-        sec.text(&mut pc, status_text, 80.0, 0.0, 14.0, status_color);
-        sec.spacing(22.0);
+        let status_text = if state.running { "Waybar: Running" } else { "Waybar: 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
-        sec.text(&mut pc, &format!("Font size: {}px", state.font_size), 12.0, 0.0, 13.0, TEXT_FG);
-        sec.spacing(20.0);
+        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();
@@ -115,7 +140,13 @@ pub fn view(state: &StatusState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageCon
 
 pub fn update(state: &mut StatusState, msg: StatusMessage) {
     match msg {
-        StatusMessage::Refreshed(new) => { *state = new; }
+        StatusMessage::Refreshed(new) => {
+            let was_status_hovered = state.status_label.hovered();
+            let was_size_hovered = state.size_label.hovered();
+            *state = new;
+            state.status_label.set_hovered(was_status_hovered);
+            state.size_label.set_hovered(was_size_hovered);
+        }
         StatusMessage::FontSizeUp => {
             if state.font_size < 28 {
                 state.font_size += 1;