system settings
git clone https://git.lucas.co/cce-system-interface.git
Update system configuration and interface modules
Cargo.lock | 1 +
src/main.rs | 367 +++++++++++++++--------
src/pages/colors.rs | 27 +-
src/pages/input.rs | 73 ++++-
src/pages/layout.rs | 812 +++++++++++++++++++++++++++++++++++++++++++++++++-
src/pages/typeface.rs | 5 +-
6 files changed, 1144 insertions(+), 141 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index ce5956a..addd3cb 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -352,6 +352,7 @@ dependencies = [
"calloop",
"calloop-wayland-source",
"glyphon",
+ "libc",
"pollster",
"raw-window-handle",
"resvg",
diff --git a/src/main.rs b/src/main.rs
index 6c3fc8f..3de63e3 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -194,6 +194,7 @@ struct SystemInterface {
rx_display: std::sync::mpsc::Receiver<pages::display::DisplayState>,
rx_network: std::sync::mpsc::Receiver<pages::network::NetworkState>,
rx_layout: std::sync::mpsc::Receiver<pages::layout::LayoutState>,
+ rx_wm_events: std::sync::mpsc::Receiver<()>,
rx_input: std::sync::mpsc::Receiver<pages::input::InputState>,
rx_fingers: std::sync::mpsc::Receiver<Vec<Finger>>,
rx_hardware: std::sync::mpsc::Receiver<pages::hardware::HardwareState>,
@@ -267,6 +268,38 @@ impl clear_ui::engine::Application for SystemInterface {
});
rx
};
+ let rx_wm_events = {
+ let (tx, rx) = std::sync::mpsc::channel::<()>();
+ tokio::spawn(async move {
+ let display = std::env::var("WAYLAND_DISPLAY").unwrap_or_else(|_| "wayland-0".to_string());
+ let windows_path = format!("/tmp/ccec-windows-{}", display);
+ let tags_path = format!("/tmp/ccec-tags-{}", display);
+ let title_path = format!("/tmp/ccec-title-{}", display);
+
+ let mut last_mod = std::time::SystemTime::UNIX_EPOCH;
+
+ let check_mtime = |path: &str| -> Option<std::time::SystemTime> {
+ std::fs::metadata(path).and_then(|m| m.modified()).ok()
+ };
+
+ loop {
+ let mut changed = false;
+ for p in &[&windows_path, &tags_path, &title_path] {
+ if let Some(mtime) = check_mtime(p) {
+ if mtime > last_mod {
+ last_mod = mtime;
+ changed = true;
+ }
+ }
+ }
+ if changed {
+ if tx.send(()).is_err() { break; }
+ }
+ tokio::time::sleep(std::time::Duration::from_millis(500)).await;
+ }
+ });
+ rx
+ };
let rx_input = {
let (tx, rx) = std::sync::mpsc::channel::<pages::input::InputState>();
tokio::spawn(async move {
@@ -358,6 +391,7 @@ impl clear_ui::engine::Application for SystemInterface {
rx_display,
rx_network,
rx_layout,
+ rx_wm_events,
rx_input,
rx_fingers,
rx_hardware,
@@ -495,6 +529,7 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
let mut page_buttons = Vec::new();
clear_ui::widget::hover_animation::reset_frame_registration();
+ clear_ui::widget::popovers::clear();
clear_ui::widget::hover_animation::set_scroll_offset(self.scroll_y);
clear_ui::widget::hover_animation::set_cursor_pos(self.cursor_x / s, self.cursor_y / s);
@@ -536,120 +571,7 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
// Page content in LOGICAL coordinates, then scale to physical
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 {
- 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 - scroll_offset_y) * s, w: *w * s, h: *h * s,
- color: *c, hover_color: *c,
- hovering: false, kind: WidgetKind::Static,
- });
- }
- for (t, size, x, y, tc, font_opt) in &pc.texts {
- text_items.push(TextItem {
- buffer: make_text_buffer_with_font(
- &mut self.font_system,
- t,
- *size * s,
- font_opt.as_deref(),
- &self.sans_serif_family,
- &self.serif_family,
- &self.monospace_family,
- ),
- 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,
- ),
- });
- }
- for btn in &pc.buttons {
- widgets.push(AppWidget {
- x: btn.x * s, y: (btn.y - scroll_offset_y) * s, w: btn.w * s, h: btn.h * s,
- color: btn.bg, hover_color: btn.hover_bg,
- hovering: false,
- kind: WidgetKind::ActionButton(btn.action.clone()),
- });
- let buf = make_text_buffer(&mut self.font_system, &btn.label, btn.label_size * s);
- let tw = buf.layout_runs().next().map(|r| r.line_w).unwrap_or(0.0);
- let lh = btn.label_size * s * 1.4;
- let mut left_align = btn.left_align;
-
- // Auto-detect if inside a ScrollBox to apply left alignment by default
- if !left_align && btn.w >= 60.0 {
- if self.app.current_page == Page::Typefaces {
- let sb = &self.app.typeface.list_box;
- let (sb_x, sb_y, sb_w, sb_h) = sb.rect();
- if btn.x >= sb_x - 1.0 && btn.x + btn.w <= sb_x + sb_w + 1.0
- && btn.y >= sb_y - 1.0 && btn.y + btn.h <= sb_y + sb_h + 1.0 {
- left_align = true;
- }
- } else if self.app.current_page == Page::Hardware {
- let sb = &self.app.hardware.cpu_list_box;
- let (sb_x, sb_y, sb_w, sb_h) = sb.rect();
- if btn.x >= sb_x - 1.0 && btn.x + btn.w <= sb_x + sb_w + 1.0
- && btn.y >= sb_y - 1.0 && btn.y + btn.h <= sb_y + sb_h + 1.0 {
- left_align = true;
- }
- } else if self.app.current_page == Page::Services {
- let sb = &self.app.services.list_box;
- let (sb_x, sb_y, sb_w, sb_h) = sb.rect();
- if btn.x >= sb_x - 1.0 && btn.x + btn.w <= sb_x + sb_w + 1.0
- && btn.y >= sb_y - 1.0 && btn.y + btn.h <= sb_y + sb_h + 1.0 {
- left_align = true;
- }
- }
- }
-
- let text_x = if left_align {
- btn.x * s + 8.0 * s
- } else {
- btn.x * s + (btn.w * s - tw) / 2.0
- };
-
- text_items.push(TextItem {
- buffer: buf,
- x: text_x, 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,
- (btn.label_color[2] * 255.0) as u8,
- ),
- });
- let mut cb = btn.clone();
- cb.x *= s; cb.y = (cb.y - scroll_offset_y) * s; cb.w *= s; cb.h *= s;
- page_buttons.push(cb);
- }
+ clear_ui::layout::render_popovers(&mut pc);
// ── Rebuild Widget Focus Hierarchy ──
self.page_root_container.clear_children();
@@ -686,6 +608,9 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
self.app.layout.cascade_offset_spinbox.clear_children(); self.app.layout.cascade_offset_spinbox.set_parent(None);
self.app.layout.edge_gap_spinbox.clear_children(); self.app.layout.edge_gap_spinbox.set_parent(None);
self.app.layout.top_gap_spinbox.clear_children(); self.app.layout.top_gap_spinbox.set_parent(None);
+ self.app.layout.grid_gap_spinbox.clear_children(); self.app.layout.grid_gap_spinbox.set_parent(None);
+ 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 {
cs.clear_children();
@@ -697,11 +622,19 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
self.app.input.rate_spinbox.clear_children(); self.app.input.rate_spinbox.set_parent(None);
self.app.input.delay_spinbox.clear_children(); self.app.input.delay_spinbox.set_parent(None);
+ self.app.input.scroll_toggle.clear_children(); self.app.input.scroll_toggle.set_parent(None);
self.app.input.scroll_friction_spinbox.clear_children(); self.app.input.scroll_friction_spinbox.set_parent(None);
+ self.app.input.natural_toggle.clear_children(); self.app.input.natural_toggle.set_parent(None);
+ self.app.input.scroll_speed_spinbox.clear_children(); self.app.input.scroll_speed_spinbox.set_parent(None);
+ self.app.input.pointer_toggle.clear_children(); self.app.input.pointer_toggle.set_parent(None);
self.app.input.pointer_friction_spinbox.clear_children(); self.app.input.pointer_friction_spinbox.set_parent(None);
+ self.app.input.trackpad_toggle.clear_children(); self.app.input.trackpad_toggle.set_parent(None);
self.app.input.trackpad_friction_spinbox.clear_children(); self.app.input.trackpad_friction_spinbox.set_parent(None);
+ self.app.input.dwtp_toggle.clear_children(); self.app.input.dwtp_toggle.set_parent(None);
self.app.input.trackpoint_accel_speed_spinbox.clear_children(); self.app.input.trackpoint_accel_speed_spinbox.set_parent(None);
self.app.input.trackpoint_accel_profile_menu.clear_children(); self.app.input.trackpoint_accel_profile_menu.set_parent(None);
+ self.app.input.cursor_theme_menu.clear_children(); self.app.input.cursor_theme_menu.set_parent(None);
+ self.app.input.cursor_size_spinbox.clear_children(); self.app.input.cursor_size_spinbox.set_parent(None);
for sb in &mut self.app.audio.sink_spinboxes {
sb.clear_children();
@@ -756,9 +689,9 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
link_parent_child(&mut self.page_root_container, &mut self.app.network.wifi_list_box.scroll_box);
}
Page::Layout => {
- self.page_sec_containers.resize_with(4, clear_ui::widget::Container::new);
+ self.page_sec_containers.resize_with(5, clear_ui::widget::Container::new);
- for i in 0..4 {
+ for i in 0..5 {
link_parent_child(&mut self.page_root_container, &mut self.page_sec_containers[i]);
}
@@ -769,11 +702,14 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
link_parent_child(&mut self.page_sec_containers[1], &mut self.app.layout.cascade_offset_spinbox);
link_parent_child(&mut self.page_sec_containers[1], &mut self.app.layout.edge_gap_spinbox);
link_parent_child(&mut self.page_sec_containers[1], &mut self.app.layout.top_gap_spinbox);
+ link_parent_child(&mut self.page_sec_containers[1], &mut self.app.layout.grid_gap_spinbox);
link_parent_child(&mut self.page_sec_containers[2], &mut self.app.layout.transition_duration_spinbox);
+
+ link_parent_child(&mut self.page_sec_containers[3], &mut self.app.layout.status_height_spinbox);
for menu in &mut self.app.layout.tag_layout_menus {
- link_parent_child(&mut self.page_sec_containers[3], menu);
+ link_parent_child(&mut self.page_sec_containers[4], menu);
}
}
Page::Colors => {
@@ -786,12 +722,14 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
link_parent_child(&mut self.page_root_container, &mut self.app.notifications.opacity_slider);
}
Page::Input => {
- self.page_sec_containers.resize_with(4, clear_ui::widget::Container::new);
+ self.page_sec_containers.resize_with(5, clear_ui::widget::Container::new);
link_parent_child(&mut self.page_root_container, &mut self.page_sec_containers[0]);
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_root_container, &mut self.page_sec_containers[4]);
+ link_parent_child(&mut self.page_sec_containers[0], &mut self.app.input.dwtp_toggle);
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);
@@ -801,9 +739,15 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
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[3], &mut self.app.input.scroll_toggle);
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);
+ link_parent_child(&mut self.page_sec_containers[3], &mut self.app.input.natural_toggle);
+ link_parent_child(&mut self.page_sec_containers[3], &mut self.app.input.scroll_speed_spinbox);
+
+ link_parent_child(&mut self.page_sec_containers[4], &mut self.app.input.pointer_toggle);
+ link_parent_child(&mut self.page_sec_containers[4], &mut self.app.input.pointer_friction_spinbox);
+ link_parent_child(&mut self.page_sec_containers[4], &mut self.app.input.trackpad_toggle);
+ link_parent_child(&mut self.page_sec_containers[4], &mut self.app.input.trackpad_friction_spinbox);
}
Page::Audio => {
self.page_sec_containers.resize_with(2, clear_ui::widget::Container::new);
@@ -826,6 +770,120 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
_ => {}
}
+ 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 {
+ 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 - scroll_offset_y) * s, w: *w * s, h: *h * s,
+ color: *c, hover_color: *c,
+ hovering: false, kind: WidgetKind::Static,
+ });
+ }
+ for (t, size, x, y, tc, font_opt) in &pc.texts {
+ text_items.push(TextItem {
+ buffer: make_text_buffer_with_font(
+ &mut self.font_system,
+ t,
+ *size * s,
+ font_opt.as_deref(),
+ &self.sans_serif_family,
+ &self.serif_family,
+ &self.monospace_family,
+ ),
+ 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,
+ ),
+ });
+ }
+ for btn in &pc.buttons {
+ widgets.push(AppWidget {
+ x: btn.x * s, y: (btn.y - scroll_offset_y) * s, w: btn.w * s, h: btn.h * s,
+ color: btn.bg, hover_color: btn.hover_bg,
+ hovering: false,
+ kind: WidgetKind::ActionButton(btn.action.clone()),
+ });
+ let buf = make_text_buffer(&mut self.font_system, &btn.label, btn.label_size * s);
+ let tw = buf.layout_runs().next().map(|r| r.line_w).unwrap_or(0.0);
+ let lh = btn.label_size * s * 1.4;
+ let mut left_align = btn.left_align;
+
+ // Auto-detect if inside a ScrollBox to apply left alignment by default
+ if !left_align && btn.w >= 60.0 {
+ if self.app.current_page == Page::Typefaces {
+ let sb = &self.app.typeface.list_box;
+ let (sb_x, sb_y, sb_w, sb_h) = sb.rect();
+ if btn.x >= sb_x - 1.0 && btn.x + btn.w <= sb_x + sb_w + 1.0
+ && btn.y >= sb_y - 1.0 && btn.y + btn.h <= sb_y + sb_h + 1.0 {
+ left_align = true;
+ }
+ } else if self.app.current_page == Page::Hardware {
+ let sb = &self.app.hardware.cpu_list_box;
+ let (sb_x, sb_y, sb_w, sb_h) = sb.rect();
+ if btn.x >= sb_x - 1.0 && btn.x + btn.w <= sb_x + sb_w + 1.0
+ && btn.y >= sb_y - 1.0 && btn.y + btn.h <= sb_y + sb_h + 1.0 {
+ left_align = true;
+ }
+ } else if self.app.current_page == Page::Services {
+ let sb = &self.app.services.list_box;
+ let (sb_x, sb_y, sb_w, sb_h) = sb.rect();
+ if btn.x >= sb_x - 1.0 && btn.x + btn.w <= sb_x + sb_w + 1.0
+ && btn.y >= sb_y - 1.0 && btn.y + btn.h <= sb_y + sb_h + 1.0 {
+ left_align = true;
+ }
+ }
+ }
+
+ let text_x = if left_align {
+ btn.x * s + 8.0 * s
+ } else {
+ btn.x * s + (btn.w * s - tw) / 2.0
+ };
+
+ text_items.push(TextItem {
+ buffer: buf,
+ x: text_x, 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,
+ (btn.label_color[2] * 255.0) as u8,
+ ),
+ });
+ let mut cb = btn.clone();
+ cb.x *= s; cb.y = (cb.y - scroll_offset_y) * s; cb.w *= s; cb.h *= s;
+ page_buttons.push(cb);
+ }
+
// Draw global hover highlight if active
clear_ui::widget::hover_animation::post_render_check();
if let Some((qx, qy, qw, qh, qc)) = clear_ui::widget::hover_animation::get_quad() {
@@ -959,6 +1017,9 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
layout::update(&mut self.app.layout, layout::LayoutMessage::Refreshed(s));
self.needs_rebuild = true;
}
+ while let Ok(_) = self.rx_wm_events.try_recv() {
+ self.needs_rebuild = true;
+ }
while let Ok(s) = self.rx_input.try_recv() {
input::update(&mut self.app.input, input::InputMessage::Refreshed(s));
self.needs_rebuild = true;
@@ -1108,9 +1169,15 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
if self.app.layout.top_gap_spinbox.cursor_moved(lx, ly) {
changed = true;
}
+ if self.app.layout.grid_gap_spinbox.cursor_moved(lx, ly) {
+ changed = true;
+ }
if self.app.layout.transition_duration_spinbox.cursor_moved(lx, ly) {
changed = true;
}
+ if self.app.layout.status_height_spinbox.cursor_moved(lx, ly) {
+ changed = true;
+ }
for menu in &mut self.app.layout.tag_layout_menus {
if menu.cursor_moved(lx, ly) {
changed = true;
@@ -1370,7 +1437,9 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
if self.app.layout.cascade_offset_spinbox.hit_test(lx, ly) { clicked_any_focusable = true; }
if self.app.layout.edge_gap_spinbox.hit_test(lx, ly) { clicked_any_focusable = true; }
if self.app.layout.top_gap_spinbox.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if self.app.layout.grid_gap_spinbox.hit_test(lx, ly) { clicked_any_focusable = true; }
if self.app.layout.transition_duration_spinbox.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if self.app.layout.status_height_spinbox.hit_test(lx, ly) { clicked_any_focusable = true; }
for menu in &mut self.app.layout.tag_layout_menus {
if menu.hit_test(lx, ly) { clicked_any_focusable = true; }
}
@@ -1491,6 +1560,14 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
pages::layout::LayoutMessage::SetTopGap(sb.value as u16)
));
}
+ let sb = &mut self.app.layout.grid_gap_spinbox;
+ if !sb.hit_test(lx, ly) { sb.unfocus(); }
+ let old = sb.value;
+ if sb.mouse_input(button, state, lx, ly) && sb.value != old {
+ actions.push(AppAction::Layout(
+ pages::layout::LayoutMessage::SetGridGap(sb.value as u16)
+ ));
+ }
let sb = &mut self.app.layout.transition_duration_spinbox;
if !sb.hit_test(lx, ly) { sb.unfocus(); }
let old = sb.value;
@@ -1499,6 +1576,14 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
pages::layout::LayoutMessage::SetTransitionDuration(sb.value as u16)
));
}
+ let sb = &mut self.app.layout.status_height_spinbox;
+ if !sb.hit_test(lx, ly) { sb.unfocus(); }
+ let old = sb.value;
+ if sb.mouse_input(button, state, lx, ly) && sb.value != old {
+ actions.push(AppAction::Layout(
+ pages::layout::LayoutMessage::SetStatusHeight(sb.value as u16)
+ ));
+ }
}
if self.app.current_page == Page::Layout {
for (idx, menu) in self.app.layout.tag_layout_menus.iter_mut().enumerate() {
@@ -1525,6 +1610,8 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
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,
_ => pages::colors::ColorsMessage::PickLowColor,
}));
}
@@ -1537,6 +1624,8 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
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),
_ => pages::colors::ColorsMessage::SetLowColor(cp.color),
}));
}
@@ -1561,6 +1650,12 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
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.scroll_speed_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::ApplyScrollSpeed));
+ }
let sb = &mut self.app.input.pointer_friction_spinbox;
if !sb.hit_test(lx, ly) { sb.unfocus(); }
let old = sb.value;
@@ -1605,6 +1700,11 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
if toggle.take_click() {
actions.push(AppAction::Input(pages::input::InputMessage::ToggleInertialScroll));
}
+ let toggle = &mut self.app.input.natural_toggle;
+ toggle.mouse_input(button, state, lx, ly);
+ if toggle.take_click() {
+ actions.push(AppAction::Input(pages::input::InputMessage::ToggleNaturalScroll));
+ }
let toggle = &mut self.app.input.pointer_toggle;
toggle.mouse_input(button, state, lx, ly);
if toggle.take_click() {
@@ -2112,6 +2212,16 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
}
changed = true;
}
+ let sb = &mut self.app.layout.grid_gap_spinbox;
+ let old = sb.value;
+ if sb.keyboard_input(event) {
+ if sb.value != old {
+ actions.push(AppAction::Layout(
+ pages::layout::LayoutMessage::SetGridGap(sb.value as u16)
+ ));
+ }
+ changed = true;
+ }
let sb = &mut self.app.layout.transition_duration_spinbox;
let old = sb.value;
if sb.keyboard_input(event) {
@@ -2122,6 +2232,16 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
}
changed = true;
}
+ let sb = &mut self.app.layout.status_height_spinbox;
+ let old = sb.value;
+ if sb.keyboard_input(event) {
+ if sb.value != old {
+ actions.push(AppAction::Layout(
+ pages::layout::LayoutMessage::SetStatusHeight(sb.value as u16)
+ ));
+ }
+ changed = true;
+ }
for a in &actions {
self.handle_action(a);
}
@@ -2145,6 +2265,8 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
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),
_ => pages::colors::ColorsMessage::SetLowColor(cp.color),
}));
}
@@ -2188,6 +2310,11 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
self.needs_rebuild = true;
return true;
}
+ if self.app.input.scroll_speed_spinbox.keyboard_input(event) {
+ self.handle_action(&AppAction::Input(pages::input::InputMessage::ApplyScrollSpeed));
+ self.needs_rebuild = true;
+ return true;
+ }
if self.app.input.pointer_friction_spinbox.keyboard_input(event) {
self.handle_action(&AppAction::Input(pages::input::InputMessage::ApplyPointerFriction));
self.needs_rebuild = true;
diff --git a/src/pages/colors.rs b/src/pages/colors.rs
index 169547b..c8c05dd 100644
--- a/src/pages/colors.rs
+++ b/src/pages/colors.rs
@@ -23,6 +23,7 @@ pub struct ColorsState {
pub slider_track_color: [u8; 3],
pub page_low_color: [u8; 3],
pub color_borders_color: [u8; 3],
+ pub normal_color: [u8; 3],
pub color_selectors: Vec<ColorSelector>,
}
@@ -37,6 +38,7 @@ impl Default for ColorsState {
slider_track_color: [116, 116, 128],
page_low_color: [71, 71, 81],
color_borders_color: [124, 124, 137],
+ normal_color: [0xcc, 0xcc, 0xd8],
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
@@ -46,6 +48,7 @@ impl Default for ColorsState {
ColorSelector::new([116, 116, 128]).with_label("Slider Track"), // 5: Controls - Slider Track
ColorSelector::new([124, 124, 137]).with_label("Borders"), // 6: Controls - Borders
ColorSelector::new([0x0a, 0x1a, 0x0e]).with_label("Low Color"), // 7: Layout - Low Color
+ ColorSelector::new([0xcc, 0xcc, 0xd8]).with_label("Normal"), // 8: Status - Normal
],
}
}
@@ -61,6 +64,7 @@ pub enum ColorsMessage {
SetSliderTrackColor([u8; 3]),
SetPageLowColor([u8; 3]),
SetColorBordersColor([u8; 3]),
+ SetNormalColor([u8; 3]),
PickLowColor,
PickHighColor,
PickDisabledColor,
@@ -69,6 +73,7 @@ pub enum ColorsMessage {
PickSliderTrackColor,
PickPageLowColor,
PickColorBordersColor,
+ PickNormalColor,
Refreshed(ColorsState),
}
@@ -101,6 +106,8 @@ pub fn read_colors_config() -> ColorsState {
let page_low = parse_color_from_key(&content, "page_low_color", [71, 71, 81]);
let color_borders = parse_color_from_key(&content, "color_borders_color", [124, 124, 137]);
+
+ let normal = parse_color_from_key(&content, "status_normal_color", [0xcc, 0xcc, 0xd8]);
ColorsState {
low_color: bg,
@@ -111,6 +118,7 @@ pub fn read_colors_config() -> ColorsState {
slider_track_color: slider_track,
page_low_color: page_low,
color_borders_color: color_borders,
+ normal_color: normal,
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
@@ -120,6 +128,7 @@ pub fn read_colors_config() -> ColorsState {
ColorSelector::new(slider_track).with_label("Slider Track"), // 5: Controls - Slider Track
ColorSelector::new(color_borders).with_label("Borders"), // 6: Controls - Borders
ColorSelector::new(bg).with_label("Low Color"), // 7: Layout - Low Color
+ ColorSelector::new(normal).with_label("Normal"), // 8: Status - Normal
],
}
}
@@ -263,6 +272,12 @@ fn apply_color_borders_color(rgb: [u8; 3]) {
clear_ui::color::set_color_borders_color([r, g, b, 1.0]);
}
+fn apply_normal_color(rgb: [u8; 3]) {
+ let hex = format!("\"#{:02x}{:02x}{:02x}\"", rgb[0], rgb[1], rgb[2]);
+ write_config_value("status_normal_color", &hex);
+ status_interface_reload();
+}
+
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;
@@ -292,6 +307,9 @@ pub fn view(state: &mut ColorsState, cx: f32, cy: f32, cw: f32, _ch: f32) -> Pag
// 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);
@@ -348,7 +366,11 @@ pub fn update(state: &mut ColorsState, msg: ColorsMessage) {
state.color_borders_color = rgb;
apply_color_borders_color(rgb);
}
- ColorsMessage::PickLowColor | ColorsMessage::PickHighColor | ColorsMessage::PickDisabledColor | ColorsMessage::PickSeparatorColor | ColorsMessage::PickVisualGuides | ColorsMessage::PickSliderTrackColor | ColorsMessage::PickPageLowColor | ColorsMessage::PickColorBordersColor => {}
+ ColorsMessage::SetNormalColor(rgb) => {
+ state.normal_color = rgb;
+ apply_normal_color(rgb);
+ }
+ ColorsMessage::PickLowColor | ColorsMessage::PickHighColor | ColorsMessage::PickDisabledColor | ColorsMessage::PickSeparatorColor | ColorsMessage::PickVisualGuides | ColorsMessage::PickSliderTrackColor | ColorsMessage::PickPageLowColor | ColorsMessage::PickColorBordersColor | ColorsMessage::PickNormalColor => {}
ColorsMessage::Refreshed(new) => {
*state = new;
}
@@ -369,7 +391,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\"\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\"\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]);
@@ -378,6 +400,7 @@ mod tests {
assert_eq!(parse_color_from_key(content, "slider_track_color", [0, 0, 0]), [18, 52, 86]);
assert_eq!(parse_color_from_key(content, "page_low_color", [0, 0, 0]), [71, 71, 81]);
assert_eq!(parse_color_from_key(content, "color_borders_color", [0, 0, 0]), [171, 205, 239]);
+ assert_eq!(parse_color_from_key(content, "status_normal_color", [0, 0, 0]), [204, 204, 216]);
assert_eq!(parse_color_from_key(content, "non_existent", [1, 2, 3]), [1, 2, 3]);
}
diff --git a/src/pages/input.rs b/src/pages/input.rs
index 0a69bef..4cb1159 100644
--- a/src/pages/input.rs
+++ b/src/pages/input.rs
@@ -49,6 +49,12 @@ pub struct InputState {
pub trackpad_toggle: Toggle,
pub trackpad_friction_spinbox: Spinbox,
+ // Scrolling settings
+ pub natural_scroll: bool,
+ pub scroll_speed: f32,
+ pub natural_toggle: Toggle,
+ pub scroll_speed_spinbox: Spinbox,
+
// Trackpoint settings
pub dwtp: bool,
pub trackpoint_accel_speed: f32,
@@ -124,6 +130,11 @@ impl Default for InputState {
trackpad_toggle: Toggle::new().with_label("Inertial Pointer (Trackpad)"),
trackpad_friction_spinbox: Spinbox::new(95, 50, 99, 1).with_label("Trackpad Friction").with_unit("%"),
+ natural_scroll: false,
+ scroll_speed: 1.0,
+ natural_toggle: Toggle::new().with_label("Natural Scroll"),
+ scroll_speed_spinbox: Spinbox::new(10, 1, 100, 1).with_label("Scroll Speed").with_unit("x").with_decimals(1),
+
dwtp: true,
trackpoint_accel_speed: 0.5,
trackpoint_accel_profile: "flat".to_string(),
@@ -161,6 +172,8 @@ pub enum InputMessage {
ApplyPointerFriction,
ToggleInertialTrackpad,
ApplyTrackpadFriction,
+ ToggleNaturalScroll,
+ ApplyScrollSpeed,
ToggleDwtp,
ApplyTrackpointAccelSpeed,
@@ -182,7 +195,11 @@ pub fn read_input_config() -> InputState {
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);
+ let natural_scroll = parse_bool_from_default(&content, "natural_scroll", false);
+ let scroll_speed = parse_f32_key(&content, "scroll_speed", 1.0);
+ let scroll_speed_val = (scroll_speed * 10.0).round() as i32;
+ let _dwt = parse_bool_from_default(&content, "dwt", true);
let dwtp = parse_bool_from_default(&content, "dwtp", true);
let trackpoint_accel_speed = parse_f32_key(&content, "trackpoint_accel_speed", 0.5);
let trackpoint_accel_profile = parse_string_key(&content, "trackpoint_accel_profile", "flat");
@@ -220,6 +237,11 @@ pub fn read_input_config() -> InputState {
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("%"),
+ natural_scroll,
+ scroll_speed,
+ natural_toggle: Toggle::new().with_label("Natural Scroll"),
+ scroll_speed_spinbox: Spinbox::new(scroll_speed_val, 1, 100, 1).with_label("Scroll Speed").with_unit("x").with_decimals(1),
+
dwtp,
trackpoint_accel_speed,
trackpoint_accel_profile,
@@ -320,11 +342,11 @@ 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 == "cursor_theme" || key == "cursor_size" {
+ || key == "cursor_theme" || key == "cursor_size" || key == "natural_scroll" {
"[input]"
} else if key == "inertial_scroll" || key == "scroll_friction"
|| key == "inertial_pointer" || key == "pointer_friction"
- || key == "inertial_trackpad" || key == "trackpad_friction" {
+ || key == "inertial_trackpad" || key == "trackpad_friction" || key == "scroll_speed" {
"[inertial]"
} else {
"[repeat]"
@@ -423,15 +445,27 @@ pub fn view(state: &mut InputState, cx: f32, cy: f32, cw: f32, _ch: f32, sec_foc
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");
+ // ── Scrolling ──
+ let mut sec = Section::new(&mut pc, cx, y, cw, "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);
sec.widget(&mut pc, &mut state.scroll_friction_spinbox, 14.0, 200.0, 26.0);
- sec.spacing(16.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);
+
+ sec.widget(&mut 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));
+
+ // ── Inertial Input ──
+ let mut sec = Section::new(&mut pc, cx, y, cw, "Inertial Input");
state.pointer_toggle.set_toggled(state.inertial_pointer);
sec.widget(&mut pc, &mut state.pointer_toggle, 14.0, toggle_w, toggle_h);
@@ -447,7 +481,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(3).copied().unwrap_or(false));
+ y = sec.finish_focused(&mut pc, sec_focused.get(4).copied().unwrap_or(false));
// ── Keybindings ──
let mut sec = Section::new(&mut pc, cx, y, cw, "Keyboard Bindings");
@@ -470,8 +504,7 @@ pub fn view(state: &mut InputState, cx: f32, cy: f32, cw: f32, _ch: f32, sec_foc
}
sec.finish(&mut pc);
- state.trackpoint_accel_profile_menu.render_popover(&mut pc);
- state.cursor_theme_menu.render_popover(&mut pc);
+
pc
}
@@ -492,11 +525,24 @@ pub fn update(state: &mut InputState, msg: InputMessage) {
InputMessage::ToggleInertialScroll => {
state.inertial_scroll = !state.inertial_scroll;
write_config_value("inertial_scroll", &state.inertial_scroll.to_string());
+ send_ipc_command("reload");
}
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());
+ send_ipc_command("reload");
+ }
+ InputMessage::ToggleNaturalScroll => {
+ state.natural_scroll = !state.natural_scroll;
+ write_config_value("natural_scroll", &state.natural_scroll.to_string());
+ send_ipc_command(&format!("input natural-scroll {}", state.natural_scroll));
+ }
+ InputMessage::ApplyScrollSpeed => {
+ let val = state.scroll_speed_spinbox.value as f32 / 10.0;
+ state.scroll_speed = val;
+ write_config_value("scroll_speed", &val.to_string());
+ send_ipc_command("reload");
}
InputMessage::ToggleInertialPointer => {
state.inertial_pointer = !state.inertial_pointer;
@@ -582,5 +628,16 @@ mod tests {
assert!(!state.is_over_trackpad(150.0, 199.0));
assert!(!state.is_over_trackpad(150.0, 351.0));
}
+
+ #[test]
+ fn test_parse_scrolling_params() {
+ let content = "[input]\nnatural_scroll = true\n[inertial]\nscroll_speed = 2.5\n";
+ assert_eq!(parse_bool_from_default(content, "natural_scroll", false), true);
+ assert_eq!(parse_f32_key(content, "scroll_speed", 1.0), 2.5);
+
+ let empty_content = "";
+ 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);
+ }
}
diff --git a/src/pages/layout.rs b/src/pages/layout.rs
index e2ab962..ff12bdd 100644
--- a/src/pages/layout.rs
+++ b/src/pages/layout.rs
@@ -60,11 +60,15 @@ pub struct LayoutState {
pub cascade_offset: u16,
pub edge_gap: u16,
pub top_gap: u16,
+ pub grid_gap: u16,
+ pub status_height: u16,
pub transition_duration: u16,
pub spinboxes: Vec<Spinbox>,
pub cascade_offset_spinbox: Spinbox,
pub edge_gap_spinbox: Spinbox,
pub top_gap_spinbox: Spinbox,
+ pub grid_gap_spinbox: Spinbox,
+ pub status_height_spinbox: Spinbox,
pub transition_duration_spinbox: Spinbox,
pub tag_layout_menus: Vec<Dropdown>,
}
@@ -79,11 +83,15 @@ impl Default for LayoutState {
cascade_offset: 20,
edge_gap: 48,
top_gap: 48,
+ grid_gap: 6,
+ status_height: 24,
transition_duration: 300,
spinboxes: make_spinboxes(0, 6, 6, 6),
cascade_offset_spinbox: Spinbox::new(20, 0, 200, 1),
edge_gap_spinbox: Spinbox::new(48, 0, 200, 1),
top_gap_spinbox: Spinbox::new(48, 0, 200, 1),
+ grid_gap_spinbox: Spinbox::new(6, 0, 200, 1),
+ status_height_spinbox: Spinbox::new(24, 0, 100, 1),
transition_duration_spinbox: Spinbox::new(300, 0, 2000, 50),
tag_layout_menus: (1..=4).map(|i| {
Dropdown::new(
@@ -107,7 +115,9 @@ pub enum LayoutMessage {
SetCascadeOffset(u16),
SetEdgeGap(u16),
SetTopGap(u16),
+ SetGridGap(u16),
SetTransitionDuration(u16),
+ SetStatusHeight(u16),
SetTagLayout(usize, usize),
Refreshed(LayoutState),
}
@@ -121,6 +131,8 @@ pub fn read_layout_config() -> LayoutState {
let co = parse_u16_from(&content, "cascade_offset", 20);
let gl = parse_u16_from(&content, "gap_left", 48);
let gt = parse_u16_from(&content, "gap_top", 48);
+ let gg = parse_u16_from(&content, "grid_gap", 6);
+ let sh = parse_u16_from(&content, "bar_height", 24);
let td = parse_u16_from(&content, "transition_duration", 300);
let tag_modes = parse_tag_layouts_from_config(&content);
@@ -145,11 +157,15 @@ pub fn read_layout_config() -> LayoutState {
cascade_offset: co,
edge_gap: gl,
top_gap: gt,
+ grid_gap: gg,
+ status_height: sh,
transition_duration: td,
spinboxes: make_spinboxes(fs, ca, g, fl),
cascade_offset_spinbox: Spinbox::new(co as i32, 0, 200, 1),
edge_gap_spinbox: Spinbox::new(gl as i32, 0, 200, 1),
top_gap_spinbox: Spinbox::new(gt as i32, 0, 200, 1),
+ grid_gap_spinbox: Spinbox::new(gg as i32, 0, 200, 1),
+ status_height_spinbox: Spinbox::new(sh as i32, 0, 100, 1),
transition_duration_spinbox: Spinbox::new(td as i32, 0, 2000, 50),
tag_layout_menus,
}
@@ -305,10 +321,782 @@ fn apply_all_widths(s: &LayoutState) {
w("transition_duration", s.transition_duration);
}
+fn apply_single_layout_param(key: &str, val: u16) {
+ write_config_value(key, &val.to_string());
+ send_ipc_command(&format!("layout {} {}", key, val));
+}
+
+fn apply_edge_gap(val: u16) {
+ let val_str = val.to_string();
+ write_config_value("gap_left", &val_str);
+ write_config_value("gap_right", &val_str);
+ write_config_value("gap_bottom", &val_str);
+ send_ipc_command(&format!("layout gap_left {}", val));
+ send_ipc_command(&format!("layout gap_right {}", val));
+ send_ipc_command(&format!("layout gap_bottom {}", val));
+}
+
+#[derive(Debug, Clone)]
+struct PreviewWindow {
+ app_id: String,
+ title: String,
+ x: f32,
+ y: f32,
+ w: f32,
+ h: f32,
+ tags: u32,
+ _minimized: bool,
+ has_parent: bool,
+ layout_mode: String,
+}
+
+struct LayoutStatusInfo {
+ active_tags: u32,
+ focused_tags: u32,
+ _num_tags: u32,
+ windows: Vec<PreviewWindow>,
+ focused_title: String,
+ focused_layout_mode: String,
+}
+
+fn read_current_layout_status() -> LayoutStatusInfo {
+ let mut active_tags = 1;
+ let mut focused_tags = 1;
+ let mut _num_tags = 4;
+
+ let display = std::env::var("WAYLAND_DISPLAY").unwrap_or_else(|_| "wayland-0".to_string());
+
+ let tags_path = format!("/tmp/ccec-tags-{}", display);
+ let tags_fallback = "/tmp/ccec-tags".to_string();
+ let tags_content = fs::read_to_string(&tags_path)
+ .or_else(|_| fs::read_to_string(&tags_fallback))
+ .unwrap_or_default();
+
+ if let Some(line) = tags_content.lines().next() {
+ let parts: Vec<&str> = line.split_whitespace().collect();
+ if parts.len() >= 3 {
+ active_tags = parts[0].parse().unwrap_or(1);
+ focused_tags = parts[1].parse().unwrap_or(1);
+ _num_tags = parts[2].parse().unwrap_or(4);
+ }
+ }
+
+ let title_path = format!("/tmp/ccec-title-{}", display);
+ let title_fallback = "/tmp/ccec-title".to_string();
+ let focused_title = fs::read_to_string(&title_path)
+ .or_else(|_| fs::read_to_string(&title_fallback))
+ .unwrap_or_default()
+ .trim()
+ .to_string();
+
+ let layout_path = format!("/tmp/ccec-layout-{}", display);
+ let layout_fallback = "/tmp/ccec-layout".to_string();
+ let focused_layout_mode = fs::read_to_string(&layout_path)
+ .or_else(|_| fs::read_to_string(&layout_fallback))
+ .unwrap_or_else(|_| "Cascade".to_string())
+ .trim()
+ .to_string();
+
+ let windows_path = format!("/tmp/ccec-windows-{}", display);
+ let windows_fallback = "/tmp/ccec-windows".to_string();
+ let windows_content = fs::read_to_string(&windows_path)
+ .or_else(|_| fs::read_to_string(&windows_fallback))
+ .unwrap_or_default();
+
+ let mut windows = Vec::new();
+ for line in windows_content.lines() {
+ if !line.starts_with("window ") { continue; }
+
+ let mut app_id = String::new();
+ let mut title = String::new();
+ let mut x = 0.0;
+ let mut y = 0.0;
+ let mut w = 0.0;
+ let mut h = 0.0;
+ let mut tags = 0;
+ let mut minimized = false;
+ let mut has_parent = false;
+ let mut layout_mode = "Cascade".to_string();
+
+ let parts = line.strip_prefix("window ").unwrap_or(line);
+
+ let get_val = |p: &str, k: &str| -> Option<String> {
+ if let Some(idx) = p.find(k) {
+ let start = idx + k.len();
+ let mut end = p.len();
+ let next_keys = [
+ " app_id=", " title=", " mode=", " decoration=", " presentation=",
+ " tags=", " x=", " y=", " w=", " h=", " has_parent=", " minimized="
+ ];
+ for nk in next_keys {
+ if nk != k {
+ if let Some(nidx) = p[start..].find(nk) {
+ end = end.min(start + nidx);
+ }
+ }
+ }
+ Some(p[start..end].trim().to_string())
+ } else {
+ None
+ }
+ };
+
+ if let Some(val) = get_val(parts, "app_id=") { app_id = val; }
+ if let Some(val) = get_val(parts, "title=") { title = val; }
+ if let Some(val) = get_val(parts, "x=") { x = val.parse().unwrap_or(0.0); }
+ if let Some(val) = get_val(parts, "y=") { y = val.parse().unwrap_or(0.0); }
+ if let Some(val) = get_val(parts, "w=") { w = val.parse().unwrap_or(0.0); }
+ if let Some(val) = get_val(parts, "h=") { h = val.parse().unwrap_or(0.0); }
+ if let Some(val) = get_val(parts, "tags=") { tags = val.parse().unwrap_or(0); }
+ if let Some(val) = get_val(parts, "minimized=") { minimized = val == "true"; }
+ if let Some(val) = get_val(parts, "has_parent=") { has_parent = val == "true"; }
+ if let Some(val) = get_val(parts, "mode=") { layout_mode = val; }
+
+ windows.push(PreviewWindow {
+ app_id,
+ title,
+ x,
+ y,
+ w,
+ h,
+ tags,
+ _minimized: minimized,
+ has_parent,
+ layout_mode,
+ });
+ }
+
+ LayoutStatusInfo {
+ active_tags,
+ focused_tags,
+ _num_tags,
+ windows,
+ focused_title,
+ focused_layout_mode,
+ }
+}
+
+fn get_short_app_name(app_id: &str) -> String {
+ let lower = app_id.to_lowercase();
+ if lower.contains("foot") || lower.contains("terminal") || lower.contains("kitty") || lower.contains("alacritty") {
+ "Term".to_string()
+ } else if lower.contains("firefox") || lower.contains("chrome") || lower.contains("qutebrowser") || lower.contains("browser") {
+ "Web".to_string()
+ } else if lower.contains("code") || lower.contains("vscodium") || lower.contains("neovim") || lower.contains("nvim") {
+ "Code".to_string()
+ } else if lower.contains("spotify") || lower.contains("music") {
+ "Musc".to_string()
+ } else if lower.contains("discord") {
+ "Disc".to_string()
+ } else if lower.contains("interface") {
+ "Intf".to_string()
+ } else if lower.is_empty() {
+ "Win".to_string()
+ } else {
+ let mut s = lower;
+ s.truncate(4);
+ if let Some(first) = s.chars().next() {
+ let first_upper = first.to_uppercase().to_string();
+ format!("{}{}", first_upper, &s[first.len_utf8()..])
+ } else {
+ "Win".to_string()
+ }
+ }
+}
+
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;
+ // 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);
+
+ let is_active = (info.active_tags & (1 << tag_idx)) != 0;
+ let is_focused = (info.focused_tags & (1 << tag_idx)) != 0;
+
+ // 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 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();
+
+ // 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));
+
+ 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]
+ };
+
+ 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 {
+ 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 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,
+ });
+ }
+ } 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,
+ });
+ }
+
+ // 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,
+ });
+ }
+ } 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;
+
+ // 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,
+ });
+
+ // 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,
+ });
+
+ 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,
+ });
+ }
+ }
+
+ 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);
+
// 1. Border Width Section
let mut sec_bw = Section::new(&mut pc, cx, y, cw, "Border Width");
sec_bw.spacing(8.0);
@@ -350,9 +1138,7 @@ pub fn view(state: &mut LayoutState, cx: f32, cy: f32, cw: f32, _ch: f32, sec_fo
}
default_layouts_sec.finish_focused(&mut pc, sec_focused.get(3).copied().unwrap_or(false));
- for menu in &mut state.tag_layout_menus {
- menu.render_popover(&mut pc);
- }
+
pc
}
@@ -385,25 +1171,37 @@ pub fn update(state: &mut LayoutState, msg: LayoutMessage) {
let val = v.min(200);
state.cascade_offset = val;
state.cascade_offset_spinbox.value = val as i32;
- apply_all_widths(state);
+ apply_single_layout_param("cascade_offset", val);
}
LayoutMessage::SetEdgeGap(v) => {
let val = v.min(200);
state.edge_gap = val;
state.edge_gap_spinbox.value = val as i32;
- apply_all_widths(state);
+ apply_edge_gap(val);
}
LayoutMessage::SetTopGap(v) => {
let val = v.min(200);
state.top_gap = val;
state.top_gap_spinbox.value = val as i32;
- apply_all_widths(state);
+ apply_single_layout_param("gap_top", val);
+ }
+ LayoutMessage::SetGridGap(v) => {
+ let val = v.min(200);
+ state.grid_gap = val;
+ state.grid_gap_spinbox.value = val as i32;
+ apply_single_layout_param("grid_gap", val);
}
LayoutMessage::SetTransitionDuration(v) => {
let val = v.min(2000);
state.transition_duration = val;
state.transition_duration_spinbox.value = val as i32;
- apply_all_widths(state);
+ apply_single_layout_param("transition_duration", val);
+ }
+ LayoutMessage::SetStatusHeight(v) => {
+ let val = v.min(100);
+ state.status_height = val;
+ state.status_height_spinbox.value = val as i32;
+ apply_single_layout_param("bar_height", val);
}
LayoutMessage::SetTagLayout(tag, idx) => {
if tag >= 1 && tag <= 4 && idx < 5 {
diff --git a/src/pages/typeface.rs b/src/pages/typeface.rs
index 9e799f0..e53f283 100644
--- a/src/pages/typeface.rs
+++ b/src/pages/typeface.rs
@@ -794,10 +794,7 @@ pub fn view(state: &mut TypefaceState, cx: f32, cy: f32, cw: f32, _ch: f32, sec_
// Render dropdown popovers on top of all other widgets
- state.borders_menu.render_popover(&mut pc);
- state.status_menu.render_popover(&mut pc);
- state.fuzzel_menu.render_popover(&mut pc);
- state.terminal_menu.render_popover(&mut pc);
+
pc
}