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

commit01f8452ec0fdc69ae9ee74bed5182b2cc5b9cb6d
parent55f92dbd6e
authorLucas Galante <[email protected]>
date2026-05-29 17:36
Update system configuration and interface modules

 Cargo.lock            |   1 +
 src/main.rs           |  95 +++++++++++++++++++++-----
 src/pages/input.rs    | 183 +++++++++++++++++++++++++++-----------------------
 src/pages/typeface.rs |  32 +--------
 4 files changed, 182 insertions(+), 129 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index b235c80..08b9097 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -355,6 +355,7 @@ dependencies = [
  "pollster",
  "raw-window-handle",
  "resvg",
+ "serde",
  "smithay-client-toolkit",
  "tokio",
  "wayland-client",
diff --git a/src/main.rs b/src/main.rs
index 03c603d..281a0f1 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,7 +1,7 @@
 use std::sync::Arc;
 
 use clear_ui::color;
-use clear_ui::widget::{Spinbox, Widget};
+use clear_ui::widget::{Spinbox, Widget, Finger};
 use glyphon::{
     Attrs, Buffer, Cache, FontSystem, Metrics, Resolution, SwashCache, TextArea, TextAtlas,
     TextBounds, TextRenderer, Viewport,
@@ -18,7 +18,7 @@ use smithay_client_toolkit::{
     output::{OutputHandler, OutputState},
     seat::{
         keyboard::KeyboardHandler,
-        pointer::PointerHandler,
+        pointer::{PointerHandler, ThemedPointer, ThemeSpec, CursorIcon},
         Capability, SeatHandler, SeatState,
     },
     shell::{
@@ -216,7 +216,7 @@ struct SystemInterface {
     rx_network: std::sync::mpsc::Receiver<pages::network::NetworkState>,
     rx_layout: std::sync::mpsc::Receiver<pages::layout::LayoutState>,
     rx_input: std::sync::mpsc::Receiver<pages::input::InputState>,
-    rx_fingers: std::sync::mpsc::Receiver<Vec<pages::input::Finger>>,
+    rx_fingers: std::sync::mpsc::Receiver<Vec<Finger>>,
     rx_hardware: std::sync::mpsc::Receiver<pages::hardware::HardwareState>,
     rx_system: std::sync::mpsc::Receiver<pages::system_info::SystemState>,
     rx_status: std::sync::mpsc::Receiver<pages::status::StatusState>,
@@ -394,7 +394,7 @@ impl SystemInterface {
             rx
         };
         let rx_fingers = {
-            let (tx, rx) = std::sync::mpsc::channel::<Vec<pages::input::Finger>>();
+            let (tx, rx) = std::sync::mpsc::channel::<Vec<Finger>>();
             tokio::spawn(async move {
                 let socket_path = "/tmp/clear-input-coords.sock";
                 loop {
@@ -403,7 +403,7 @@ impl SystemInterface {
                         let reader = tokio::io::BufReader::new(stream);
                         let mut lines = reader.lines();
                         while let Ok(Some(line)) = lines.next_line().await {
-                            if let Ok(fingers) = serde_json::from_str::<Vec<pages::input::Finger>>(&line) {
+                            if let Ok(fingers) = serde_json::from_str::<Vec<Finger>>(&line) {
                                 if tx.send(fingers).is_err() {
                                     return;
                                 }
@@ -478,6 +478,19 @@ impl SystemInterface {
         this
     }
 
+fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f32, f32, f32, f32)>) {
+    if let Some(rect) = w.popover_rect() {
+        popovers.push(rect);
+    }
+    for child_ptr in w.children() {
+        unsafe {
+            if let Some(child) = child_ptr.as_ref() {
+                Self::collect_popover_rects(child, popovers);
+            }
+        }
+    }
+}
+
     fn rebuild_layout(&mut self, sw: f32, sh: f32) {
         let s = self.scale_factor as f32;
         let mut widgets = Vec::new();
@@ -538,7 +551,24 @@ impl SystemInterface {
         });
 
         // Page content in LOGICAL coordinates, then scale to physical
-        let pc = self.render_page_content(lcx, lcy, lcw, lch);
+        let mut pc = self.render_page_content(lcx, lcy, lcw, lch);
+
+        let mut popovers = Vec::new();
+        Self::collect_popover_rects(&self.page_root_container, &mut popovers);
+
+        if !popovers.is_empty() {
+            pc.texts.retain(|(text, size, tx, ty, _, _)| {
+                let text_w = text.chars().count() as f32 * *size * 0.65;
+                for &(px, py, pw, ph) in &popovers {
+                    let x_overlap = *tx <= px + pw && (*tx + text_w) >= px;
+                    let y_overlap = *ty <= py + ph && (*ty + *size) >= py;
+                    if x_overlap && y_overlap {
+                        return false;
+                    }
+                }
+                true
+            });
+        }
 
         let mut max_y = 0.0f32;
         for (_, _, y, _, h) in &pc.rects {
@@ -773,20 +803,24 @@ impl SystemInterface {
                 link_parent_child(&mut self.page_root_container, &mut self.app.notifications.duration_spinbox);
             }
             Page::Input => {
-                self.page_sec_containers.resize_with(3, clear_ui::widget::Container::new);
+                self.page_sec_containers.resize_with(4, clear_ui::widget::Container::new);
                 link_parent_child(&mut self.page_root_container, &mut self.page_sec_containers[0]);
                 link_parent_child(&mut self.page_root_container, &mut self.page_sec_containers[1]);
                 link_parent_child(&mut self.page_root_container, &mut self.page_sec_containers[2]);
+                link_parent_child(&mut self.page_root_container, &mut self.page_sec_containers[3]);
                 
                 link_parent_child(&mut self.page_sec_containers[0], &mut self.app.input.trackpoint_accel_speed_spinbox);
                 link_parent_child(&mut self.page_sec_containers[0], &mut self.app.input.trackpoint_accel_profile_menu);
                 
                 link_parent_child(&mut self.page_sec_containers[1], &mut self.app.input.rate_spinbox);
                 link_parent_child(&mut self.page_sec_containers[1], &mut self.app.input.delay_spinbox);
+
+                link_parent_child(&mut self.page_sec_containers[2], &mut self.app.input.cursor_theme_menu);
+                link_parent_child(&mut self.page_sec_containers[2], &mut self.app.input.cursor_size_spinbox);
                 
-                link_parent_child(&mut self.page_sec_containers[2], &mut self.app.input.scroll_friction_spinbox);
-                link_parent_child(&mut self.page_sec_containers[2], &mut self.app.input.pointer_friction_spinbox);
-                link_parent_child(&mut self.page_sec_containers[2], &mut self.app.input.trackpad_friction_spinbox);
+                link_parent_child(&mut self.page_sec_containers[3], &mut self.app.input.scroll_friction_spinbox);
+                link_parent_child(&mut self.page_sec_containers[3], &mut self.app.input.pointer_friction_spinbox);
+                link_parent_child(&mut self.page_sec_containers[3], &mut self.app.input.trackpad_friction_spinbox);
             }
             Page::Audio => {
                 self.page_sec_containers.resize_with(2, clear_ui::widget::Container::new);
@@ -1514,6 +1548,12 @@ impl SystemInterface {
             if sb.mouse_input(button, state, lx, ly) && sb.value != old {
                 actions.push(AppAction::Input(pages::input::InputMessage::ApplyTrackpointAccelSpeed));
             }
+            let sb = &mut self.app.input.cursor_size_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::ApplyCursorSize));
+            }
         }
         if state == clear_ui::widget::ElementState::Pressed && self.app.current_page == Page::Notifications {
             let sb = &mut self.app.notifications.duration_spinbox;
@@ -1557,6 +1597,14 @@ impl SystemInterface {
             if state == clear_ui::widget::ElementState::Pressed && menu.take_change() {
                 actions.push(AppAction::Input(pages::input::InputMessage::ApplyTrackpointAccelProfile(menu.selected)));
             }
+            let menu = &mut self.app.input.cursor_theme_menu;
+            if state == clear_ui::widget::ElementState::Pressed && !menu.hit_test(lx, ly) { menu.unfocus(); }
+            if menu.mouse_input(button, state, lx, ly) {
+                self.needs_rebuild = true;
+            }
+            if state == clear_ui::widget::ElementState::Pressed && menu.take_change() {
+                actions.push(AppAction::Input(pages::input::InputMessage::ApplyCursorTheme(menu.selected)));
+            }
         }
         if self.app.current_page == Page::Notifications {
             let toggle = &mut self.app.notifications.enable_toggle;
@@ -1858,9 +1906,7 @@ impl SystemInterface {
             
             if self.app.current_page == Page::Input {
                 let input = &self.app.input;
-                if lx >= input.trackpad_x && lx <= input.trackpad_x + input.trackpad_w
-                    && ly >= input.trackpad_y && ly <= input.trackpad_y + input.trackpad_h
-                {
+                if input.is_over_trackpad(lx, ly) {
                     return true;
                 }
             }
@@ -2073,6 +2119,11 @@ impl SystemInterface {
                 self.needs_rebuild = true;
                 return true;
             }
+            if self.app.input.cursor_size_spinbox.keyboard_input(event) {
+                self.handle_action(&AppAction::Input(pages::input::InputMessage::ApplyCursorSize));
+                self.needs_rebuild = true;
+                return true;
+            }
         }
         if self.app.current_page == Page::Audio {
             let mut actions = Vec::new();
@@ -2430,7 +2481,7 @@ struct App {
     output_state: OutputState,
 
     seats: Vec<wl_seat::WlSeat>,
-    pointer: Option<wl_pointer::WlPointer>,
+    pointer: Option<ThemedPointer>,
     keyboard: Option<wl_keyboard::WlKeyboard>,
 
     window: Option<XdgWindow>,
@@ -2527,8 +2578,15 @@ impl SeatHandler for App {
         capability: Capability,
     ) {
         if capability == Capability::Pointer && self.pointer.is_none() {
-            let pointer = self.seat_state.get_pointer(qh, &seat).unwrap();
-            self.pointer = Some(pointer);
+            let surface = self.compositor_state.create_surface(qh);
+            let themed_pointer = self.seat_state.get_pointer_with_theme(
+                qh,
+                &seat,
+                self.shm_state.wl_shm(),
+                surface,
+                ThemeSpec::System,
+            ).unwrap();
+            self.pointer = Some(themed_pointer);
         }
         if capability == Capability::Keyboard && self.keyboard.is_none() {
             let keyboard = self.seat_state.get_keyboard(qh, &seat, None).unwrap();
@@ -2621,6 +2679,11 @@ impl PointerHandler for App {
                         }
                     }
                 }
+                PointerEventKind::Enter { .. } => {
+                    if let Some(ref themed_pointer) = self.pointer {
+                        let _ = themed_pointer.set_cursor(_conn, CursorIcon::Default);
+                    }
+                }
                 _ => {}
             }
         }
diff --git a/src/pages/input.rs b/src/pages/input.rs
index 7d0859b..2aeefb6 100644
--- a/src/pages/input.rs
+++ b/src/pages/input.rs
@@ -3,7 +3,7 @@ use std::io::Write;
 
 use crate::app::PageContent;
 use clear_ui::layout::Section;
-use clear_ui::widget::{Dropdown, Spinbox, Toggle, Widget};
+use clear_ui::widget::{Dropdown, Spinbox, Toggle, Widget, Finger, Trackpad};
 
 const CONFIG_PATH: &str = "/home/lsgalante/.config/clearwm/config.toml";
 
@@ -14,13 +14,6 @@ fn get_socket_path() -> String {
     }
 }
 
-#[derive(Debug, Clone, serde::Deserialize)]
-pub struct Finger {
-    pub slot: usize,
-    pub x: f32,
-    pub y: f32,
-}
-
 #[derive(Debug, Clone)]
 pub struct Keybind {
     pub mods: String,
@@ -39,10 +32,7 @@ pub struct InputState {
     pub tap_toggle: Toggle,
     pub keybinds: Vec<Keybind>,
     pub fingers: Vec<Finger>,
-    pub trackpad_x: f32,
-    pub trackpad_y: f32,
-    pub trackpad_w: f32,
-    pub trackpad_h: f32,
+    pub trackpad: Trackpad,
 
     // Inertial settings
     pub inertial_scroll: bool,
@@ -67,10 +57,48 @@ pub struct InputState {
     pub dwtp_toggle: Toggle,
     pub trackpoint_accel_speed_spinbox: Spinbox,
     pub trackpoint_accel_profile_menu: Dropdown,
+
+    // Cursor settings
+    pub cursor_theme: String,
+    pub cursor_size: u32,
+    pub cursor_theme_menu: Dropdown,
+    pub cursor_size_spinbox: Spinbox,
+    pub cursor_themes: Vec<String>,
+}
+
+fn scan_cursor_themes() -> Vec<String> {
+    let mut themes = vec!["default".to_string()];
+    let paths = [
+        "/usr/share/icons",
+        "/home/lsgalante/.icons",
+        "/home/lsgalante/.local/share/icons",
+    ];
+    for base_path in &paths {
+        if let Ok(entries) = std::fs::read_dir(base_path) {
+            for entry in entries.flatten() {
+                if let Ok(file_type) = entry.file_type() {
+                    if file_type.is_dir() {
+                        let path = entry.path();
+                        if path.join("cursors").is_dir() {
+                            if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
+                                let name_str = name.to_string();
+                                if !themes.contains(&name_str) {
+                                    themes.push(name_str);
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+    themes.sort();
+    themes
 }
 
 impl Default for InputState {
     fn default() -> Self {
+        let cursor_themes = vec!["default".to_string()];
         Self {
             tap_to_click: false,
             repeat_rate: 50,
@@ -80,10 +108,7 @@ impl Default for InputState {
             tap_toggle: Toggle::new().with_label("Tap to Click"),
             keybinds: Vec::new(),
             fingers: Vec::new(),
-            trackpad_x: 0.0,
-            trackpad_y: 0.0,
-            trackpad_w: 0.0,
-            trackpad_h: 0.0,
+            trackpad: Trackpad::new(),
 
             inertial_scroll: true,
             scroll_friction: 90,
@@ -106,14 +131,20 @@ impl Default for InputState {
             dwtp_toggle: Toggle::new().with_label("Disable While Trackpointing"),
             trackpoint_accel_speed_spinbox: Spinbox::new(5, -10, 10, 1).with_label("Acceleration Speed").with_decimals(1),
             trackpoint_accel_profile_menu: Dropdown::new(vec!["flat".to_string(), "adaptive".to_string()], 0).with_label("Acceleration Profile"),
+
+            // Cursor settings defaults
+            cursor_theme: "default".to_string(),
+            cursor_size: 24,
+            cursor_theme_menu: Dropdown::new(cursor_themes.clone(), 0).with_label("Cursor Theme"),
+            cursor_size_spinbox: Spinbox::new(24, 16, 64, 4).with_label("Cursor Size").with_unit("px"),
+            cursor_themes,
         }
     }
 }
 
 impl InputState {
     pub fn is_over_trackpad(&self, lx: f32, ly: f32) -> bool {
-        lx >= self.trackpad_x && lx <= self.trackpad_x + self.trackpad_w
-            && ly >= self.trackpad_y && ly <= self.trackpad_y + self.trackpad_h
+        self.trackpad.hit_test(lx, ly)
     }
 }
 
@@ -134,6 +165,9 @@ pub enum InputMessage {
     ToggleDwtp,
     ApplyTrackpointAccelSpeed,
     ApplyTrackpointAccelProfile(usize),
+
+    ApplyCursorTheme(usize),
+    ApplyCursorSize,
 }
 
 pub fn read_input_config() -> InputState {
@@ -156,6 +190,11 @@ pub fn read_input_config() -> InputState {
     let speed_val = (trackpoint_accel_speed * 10.0).round() as i32;
     let profile_idx = if trackpoint_accel_profile == "adaptive" { 1 } else { 0 };
 
+    let cursor_theme = parse_string_key(&content, "cursor_theme", "default");
+    let cursor_size = parse_u16_key(&content, "cursor_size", 24);
+    let cursor_themes = scan_cursor_themes();
+    let theme_idx = cursor_themes.iter().position(|t| t == &cursor_theme).unwrap_or(0);
+
     InputState {
         tap_to_click: tap,
         repeat_rate: rate,
@@ -165,10 +204,7 @@ pub fn read_input_config() -> InputState {
         tap_toggle: Toggle::new().with_label("Tap to Click"),
         keybinds: parse_keybinds(&content),
         fingers: Vec::new(),
-        trackpad_x: 0.0,
-        trackpad_y: 0.0,
-        trackpad_w: 0.0,
-        trackpad_h: 0.0,
+        trackpad: Trackpad::new(),
 
         inertial_scroll,
         scroll_friction,
@@ -191,6 +227,13 @@ pub fn read_input_config() -> InputState {
         dwtp_toggle: Toggle::new().with_label("Disable While Trackpointing"),
         trackpoint_accel_speed_spinbox: Spinbox::new(speed_val, -10, 10, 1).with_label("Acceleration Speed").with_decimals(1),
         trackpoint_accel_profile_menu: Dropdown::new(vec!["flat".to_string(), "adaptive".to_string()], profile_idx).with_label("Acceleration Profile"),
+
+        // Cursor settings
+        cursor_theme: cursor_theme.clone(),
+        cursor_size: cursor_size as u32,
+        cursor_theme_menu: Dropdown::new(cursor_themes.clone(), theme_idx).with_label("Cursor Theme"),
+        cursor_size_spinbox: Spinbox::new(cursor_size as i32, 16, 64, 4).with_label("Cursor Size").with_unit("px"),
+        cursor_themes,
     }
 }
 
@@ -276,7 +319,8 @@ fn write_config_value(key: &str, value: &str) {
 
     if !found {
         let section = if key == "tap_to_click" || key == "dwtp"
-                || key == "trackpoint_accel_speed" || key == "trackpoint_accel_profile" {
+                || key == "trackpoint_accel_speed" || key == "trackpoint_accel_profile"
+                || key == "cursor_theme" || key == "cursor_size" {
             "[input]"
         } else if key == "inertial_scroll" || key == "scroll_friction"
                || key == "inertial_pointer" || key == "pointer_friction"
@@ -336,45 +380,12 @@ pub fn view(state: &mut InputState, cx: f32, cy: f32, cw: f32, _ch: f32, sec_foc
     sec.widget(&mut pc, &mut state.tap_toggle, 14.0, toggle_w, toggle_h);
     sec.spacing(8.0);
 
-    // Left-aligned trackpad visualizer box
+    // Built-in trackpad visualizer widget
     let pad_w = 280.0;
     let pad_h = 140.0;
-    let pad_x = sec.ax(14.0);
-    let pad_y = sec.ay();
-
-    state.trackpad_x = pad_x;
-    state.trackpad_y = pad_y;
-    state.trackpad_w = pad_w;
-    state.trackpad_h = pad_h;
-
-    // Background of trackpad: sleek dark translucent blue/grey
-    pc.rect([0.11, 0.11, 0.16, 0.85], pad_x, pad_y, pad_w, pad_h);
-
-    // Border: clean border
-    let border_color = [0.28, 0.28, 0.38, 1.0];
-    pc.rect(border_color, pad_x, pad_y, pad_w, 1.0);
-    pc.rect(border_color, pad_x, pad_y + pad_h - 1.0, pad_w, 1.0);
-    pc.rect(border_color, pad_x, pad_y, 1.0, pad_h);
-    pc.rect(border_color, pad_x + pad_w - 1.0, pad_y, 1.0, pad_h);
-
-    // Sleek label in the touchpad area
-    pc.text("Touchpad Area", pad_x + 12.0, pad_y + pad_h - 22.0, 11.0, [0.45, 0.45, 0.55, 1.0]);
-
-    // Active fingers visualizer
-    for finger in &state.fingers {
-        let rx = finger.x.clamp(0.0, 1.0);
-        let ry = finger.y.clamp(0.0, 1.0);
-        let fx = pad_x + rx * pad_w;
-        let fy = pad_y + ry * pad_h;
-        let dot_size = 12.0;
-
-        // Render glow (outer light blue rectangle)
-        pc.rect([0.35, 0.55, 0.95, 0.4], fx - (dot_size + 6.0) / 2.0, fy - (dot_size + 6.0) / 2.0, dot_size + 6.0, dot_size + 6.0);
-        // Render core (solid blue/purple rectangle)
-        pc.rect([0.45, 0.65, 1.0, 1.0], fx - dot_size / 2.0, fy - dot_size / 2.0, dot_size, dot_size);
-    }
-
-    sec.spacing(pad_h + 12.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);
 
     // ── Trackpoint ──
@@ -401,6 +412,17 @@ pub fn view(state: &mut InputState, cx: f32, cy: f32, cw: f32, _ch: f32, sec_foc
     sec.spacing(8.0);
     y = sec.finish_focused(&mut pc, sec_focused.get(1).copied().unwrap_or(false));
 
+    // ── Cursor ──
+    let mut sec = Section::new(&mut pc, cx, y, cw, "Cursor");
+
+    sec.widget(&mut 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);
+
+    y = sec.finish_focused(&mut pc, sec_focused.get(2).copied().unwrap_or(false));
+
     // ── Inertial Input ──
     let mut sec = Section::new(&mut pc, cx, y, cw, "Inertial Input");
 
@@ -425,7 +447,7 @@ pub fn view(state: &mut InputState, cx: f32, cy: f32, cw: f32, _ch: f32, sec_foc
     sec.widget(&mut 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(2).copied().unwrap_or(false));
+    y = sec.finish_focused(&mut pc, sec_focused.get(3).copied().unwrap_or(false));
 
     // ── Keybindings ──
     let mut sec = Section::new(&mut pc, cx, y, cw, "Keyboard Bindings");
@@ -448,25 +470,8 @@ pub fn view(state: &mut InputState, cx: f32, cy: f32, cw: f32, _ch: f32, sec_foc
     }
     sec.finish(&mut pc);
 
-    // Filter out base text items covered by any open popover to prevent showing through
-    let mut popovers = Vec::new();
-    if state.trackpoint_accel_profile_menu.open {
-        let (x, y, w, h) = state.trackpoint_accel_profile_menu.rect();
-        popovers.push((x, y + h, w, state.trackpoint_accel_profile_menu.options.len() as f32 * 24.0));
-    }
-
-    if !popovers.is_empty() {
-        pc.texts.retain(|(_, _, tx, ty, _, _)| {
-            for &(px, py, pw, ph) in &popovers {
-                if *tx >= px && *tx <= px + pw && *ty >= py && *ty <= py + ph {
-                    return false;
-                }
-            }
-            true
-        });
-    }
-
     state.trackpoint_accel_profile_menu.render_popover(&mut pc);
+    state.cursor_theme_menu.render_popover(&mut pc);
 
     pc
 }
@@ -529,6 +534,21 @@ pub fn update(state: &mut InputState, msg: InputMessage) {
             write_config_value("trackpoint_accel_profile", &format!("\"{}\"", profile));
             send_ipc_command(&format!("input trackpoint-accel-profile {}", profile));
         }
+        InputMessage::ApplyCursorTheme(idx) => {
+            if idx < state.cursor_themes.len() {
+                let theme = state.cursor_themes[idx].clone();
+                state.cursor_theme = theme.clone();
+                state.cursor_theme_menu.selected = idx;
+                write_config_value("cursor_theme", &format!("\"{}\"", theme));
+                send_ipc_command(&format!("input cursor-theme {}", theme));
+            }
+        }
+        InputMessage::ApplyCursorSize => {
+            let size = state.cursor_size_spinbox.value.max(16).min(64) as u32;
+            state.cursor_size = size;
+            write_config_value("cursor_size", &size.to_string());
+            send_ipc_command(&format!("input cursor-size {}", size));
+        }
         InputMessage::Refreshed(new) => {
             let fingers = state.fingers.clone();
             *state = new;
@@ -547,10 +567,7 @@ mod tests {
     #[test]
     fn test_is_over_trackpad() {
         let mut state = InputState::default();
-        state.trackpad_x = 100.0;
-        state.trackpad_y = 200.0;
-        state.trackpad_w = 300.0;
-        state.trackpad_h = 150.0;
+        state.trackpad.set_rect(100.0, 200.0, 300.0, 150.0);
 
         // Inside
         assert!(state.is_over_trackpad(150.0, 250.0));
diff --git a/src/pages/typeface.rs b/src/pages/typeface.rs
index 1112d1c..2bab21d 100644
--- a/src/pages/typeface.rs
+++ b/src/pages/typeface.rs
@@ -663,12 +663,12 @@ pub fn view(state: &mut TypefaceState, cx: f32, cy: f32, cw: f32, _ch: f32, sec_
                 let is_selected = state.selected_font.as_ref() == Some(*font_name);
                 
                 let font_btn = &mut state.font_buttons[idx];
-                font_btn.label = Some((*font_name).clone());
+                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.label = Some("📋".to_string());
+                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);
             }
@@ -787,35 +787,7 @@ pub fn view(state: &mut TypefaceState, cx: f32, cy: f32, cw: f32, _ch: f32, sec_
     let list_focused = sec_focused.get(2).copied().unwrap_or(false);
     sec.finish_focused(&mut pc, list_focused);
 
-    // Filter out base text items covered by any open popover to prevent showing through
-    let mut popovers = Vec::new();
-    if state.borders_menu.open {
-        let (x, y, w, h) = state.borders_menu.rect();
-        popovers.push((x, y + h, w, state.borders_menu.options.len() as f32 * 24.0));
-    }
-    if state.status_menu.open {
-        let (x, y, w, h) = state.status_menu.rect();
-        popovers.push((x, y + h, w, state.status_menu.options.len() as f32 * 24.0));
-    }
-    if state.fuzzel_menu.open {
-        let (x, y, w, h) = state.fuzzel_menu.rect();
-        popovers.push((x, y + h, w, state.fuzzel_menu.options.len() as f32 * 24.0));
-    }
-    if state.terminal_menu.open {
-        let (x, y, w, h) = state.terminal_menu.rect();
-        popovers.push((x, y + h, w, state.terminal_menu.options.len() as f32 * 24.0));
-    }
 
-    if !popovers.is_empty() {
-        pc.texts.retain(|(_, _, tx, ty, _, _)| {
-            for &(px, py, pw, ph) in &popovers {
-                if *tx >= px && *tx <= px + pw && *ty >= py && *ty <= py + ph {
-                    return false;
-                }
-            }
-            true
-        });
-    }
 
     // Render dropdown popovers on top of all other widgets
     state.borders_menu.render_popover(&mut pc);