system settings
git clone https://git.lucas.co/cce-system-interface.git
Implement page search functionality and update Interface page sections to use AdaptiveGridLayout
src/input_handler.rs | 82 ++++++++++++++++++++++++-
src/main.rs | 8 +++
src/renderer.rs | 168 +++++++++++++++++++++++++++++++++++++++++++++++----
3 files changed, 244 insertions(+), 14 deletions(-)
diff --git a/src/input_handler.rs b/src/input_handler.rs
index 081dc87..3a6c71e 100644
--- a/src/input_handler.rs
+++ b/src/input_handler.rs
@@ -11,6 +11,15 @@ impl SystemInterface {
let s = 1.0f32;
let lx_no_scroll = x / s;
let ly_no_scroll = y / s;
+
+ let sh_logical = self.height as f32 / s;
+ if self.search_open && ly_no_scroll >= (sh_logical - 42.0) {
+ let changed = self.search_box.cursor_moved(lx_no_scroll, ly_no_scroll, &mut self.ui_context);
+ if changed {
+ self.needs_rebuild = true;
+ }
+ return true;
+ }
if cce_ui::widget::context_menu::is_visible() {
if cce_ui::widget::context_menu::cursor_moved(lx_no_scroll, ly_no_scroll) {
@@ -99,6 +108,23 @@ impl SystemInterface {
let lx_no_scroll = self.cursor_x / s;
let ly_no_scroll = self.cursor_y / s;
+ let sh_logical = self.height as f32 / s;
+ if self.search_open && ly_no_scroll >= (sh_logical - 42.0) {
+ let handled = self.search_box.mouse_input(button, state, lx_no_scroll, ly_no_scroll, &mut self.ui_context);
+ if handled {
+ self.needs_rebuild = true;
+ }
+ return true;
+ }
+
+ if self.search_open && state == cce_ui::widget::ElementState::Pressed && ly_no_scroll < (sh_logical - 42.0) {
+ self.search_open = false;
+ self.search_query.clear();
+ self.search_box.set_value_string("");
+ self.ui_context.clear_focus();
+ self.needs_rebuild = true;
+ }
+
// CSD Close Button Interaction removed
if cce_ui::widget::context_menu::is_visible() {
@@ -761,6 +787,44 @@ impl SystemInterface {
}
}
+ if self.search_open {
+ if event.state == cce_ui::widget::ElementState::Pressed
+ && event.logical_key == cce_ui::widget::Key::Named(cce_ui::widget::NamedKey::Escape)
+ {
+ self.search_open = false;
+ self.search_query.clear();
+ self.search_box.set_value_string("");
+ self.ui_context.clear_focus();
+ self.needs_rebuild = true;
+ return true;
+ }
+ }
+
+ let is_text_box_focused = if let Some(focused) = self.ui_context.focused_widget {
+ unsafe { (*focused).as_any().is::<cce_ui::widget::input::TextBox>() }
+ } else {
+ false
+ };
+
+ if !self.search_open && !is_text_box_focused {
+ if event.state == cce_ui::widget::ElementState::Pressed && !event.repeat {
+ if let cce_ui::widget::Key::Character(ref c) = event.logical_key {
+ if c == "/" {
+ self.search_open = true;
+ self.search_box.set_value_string("");
+ self.search_query.clear();
+ let search_box_ptr = &mut self.search_box as *mut cce_ui::widget::input::TextBox;
+ unsafe {
+ (*search_box_ptr).focus();
+ }
+ self.ui_context.set_focused(&mut self.search_box);
+ self.needs_rebuild = true;
+ return true;
+ }
+ }
+ }
+ }
+
if event.state == cce_ui::widget::ElementState::Pressed && !event.repeat {
let is_nav_key = match (&event.logical_key, event.ctrl) {
(cce_ui::widget::Key::Character(c), true) if c == "j" || c == "J" || c == "k" || c == "K" || c == "u" || c == "U" || c == "i" || c == "I" => true,
@@ -787,6 +851,7 @@ impl SystemInterface {
}
let event_wrapper = cce_ui::widget::Event::KeyInput(event.clone());
+ let mut key_handled = false;
if let Some(root) = self.get_page_root_widget() {
if self.ui_context.propagate_event(&event_wrapper, root) {
let mut actions = Vec::new();
@@ -795,11 +860,24 @@ impl SystemInterface {
self.handle_action(&a);
}
self.needs_rebuild = true;
- return true;
+ key_handled = true;
}
}
- false
+ if self.search_open {
+ if self.search_box.take_change() {
+ self.search_query = self.search_box.text.clone();
+ self.needs_rebuild = true;
+ }
+ if !self.ui_context.is_focused(&self.search_box) {
+ self.search_open = false;
+ self.search_query.clear();
+ self.search_box.set_value_string("");
+ self.needs_rebuild = true;
+ }
+ }
+
+ key_handled
}
}
diff --git a/src/main.rs b/src/main.rs
index 1459322..36c5c28 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -216,6 +216,9 @@ struct SystemInterface {
sender: calloop::channel::Sender<AppAction>,
ui_context: cce_ui::context::UiContext,
scroll_logs: Vec<String>,
+ search_open: bool,
+ search_query: String,
+ search_box: cce_ui::widget::input::TextBox,
}
impl cce_ui::engine::Application for SystemInterface {
@@ -326,6 +329,11 @@ impl cce_ui::engine::Application for SystemInterface {
sender,
ui_context: cce_ui::context::UiContext::new(),
scroll_logs: Vec::new(),
+ search_open: false,
+ search_query: String::new(),
+ search_box: cce_ui::widget::input::TextBox::new(String::new())
+ .with_placeholder("Search sections & parameters...")
+ .with_draw_bg_border(false),
};
for page in &mut this.pages {
diff --git a/src/renderer.rs b/src/renderer.rs
index aa61a33..2f9208c 100644
--- a/src/renderer.rs
+++ b/src/renderer.rs
@@ -25,6 +25,7 @@ impl SystemInterface {
}
// Clear all widgets' hierarchy links
+ self.search_box.clear_children(&mut self.ui_context); self.search_box.set_parent(None, &mut self.ui_context);
self.app.processes.services_search_box.clear_children(&mut self.ui_context); self.app.processes.services_search_box.set_parent(None, &mut self.ui_context);
self.app.packages.search_box.clear_children(&mut self.ui_context); self.app.packages.search_box.set_parent(None, &mut self.ui_context);
self.app.packages.installed_list_box.scroll_box.clear_children(&mut self.ui_context); self.app.packages.installed_list_box.scroll_box.set_parent(None, &mut self.ui_context);
@@ -319,16 +320,16 @@ impl SystemInterface {
}
Page::Interface => {
self.page_sec_containers.clear();
- self.page_sec_containers.push(cce_ui::widget::SectionContainer::new("Custom Parameters").with_layout(cce_ui::widget::VerticalLayout::default()));
- self.page_sec_containers.push(cce_ui::widget::SectionContainer::new("Layout").with_layout(cce_ui::widget::VerticalLayout::default()));
- self.page_sec_containers.push(cce_ui::widget::SectionContainer::new("Status").with_layout(cce_ui::widget::VerticalLayout::default()));
- self.page_sec_containers.push(cce_ui::widget::SectionContainer::new("Controls").with_layout(cce_ui::widget::VerticalLayout::default()));
- self.page_sec_containers.push(cce_ui::widget::SectionContainer::new("Indicators").with_layout(cce_ui::widget::VerticalLayout::default()));
- self.page_sec_containers.push(cce_ui::widget::SectionContainer::new("Notification").with_layout(cce_ui::widget::VerticalLayout::default()));
- self.page_sec_containers.push(cce_ui::widget::SectionContainer::new("Surfaces").with_layout(cce_ui::widget::VerticalLayout::default()));
- self.page_sec_containers.push(cce_ui::widget::SectionContainer::new("Fonts").with_layout(cce_ui::widget::VerticalLayout::default()));
+ self.page_sec_containers.push(cce_ui::widget::SectionContainer::new("Custom Parameters").with_layout(cce_ui::widget::AdaptiveGridLayout { min_col_width: 140.0, gap: 8.0, padding_x: 0.0, padding_y: 0.0 }));
+ self.page_sec_containers.push(cce_ui::widget::SectionContainer::new("Layout").with_layout(cce_ui::widget::AdaptiveGridLayout { min_col_width: 140.0, gap: 8.0, padding_x: 0.0, padding_y: 0.0 }));
+ self.page_sec_containers.push(cce_ui::widget::SectionContainer::new("Status").with_layout(cce_ui::widget::AdaptiveGridLayout { min_col_width: 140.0, gap: 8.0, padding_x: 0.0, padding_y: 0.0 }));
+ self.page_sec_containers.push(cce_ui::widget::SectionContainer::new("Controls").with_layout(cce_ui::widget::AdaptiveGridLayout { min_col_width: 140.0, gap: 8.0, padding_x: 0.0, padding_y: 0.0 }));
+ self.page_sec_containers.push(cce_ui::widget::SectionContainer::new("Indicators").with_layout(cce_ui::widget::AdaptiveGridLayout { min_col_width: 140.0, gap: 8.0, padding_x: 0.0, padding_y: 0.0 }));
+ self.page_sec_containers.push(cce_ui::widget::SectionContainer::new("Notification").with_layout(cce_ui::widget::AdaptiveGridLayout { min_col_width: 140.0, gap: 8.0, padding_x: 0.0, padding_y: 0.0 }));
+ self.page_sec_containers.push(cce_ui::widget::SectionContainer::new("Surfaces").with_layout(cce_ui::widget::AdaptiveGridLayout { min_col_width: 140.0, gap: 8.0, padding_x: 0.0, padding_y: 0.0 }));
+ self.page_sec_containers.push(cce_ui::widget::SectionContainer::new("Fonts").with_layout(cce_ui::widget::AdaptiveGridLayout { min_col_width: 140.0, gap: 8.0, padding_x: 0.0, padding_y: 0.0 }));
self.page_sec_containers.push(cce_ui::widget::SectionContainer::new("Containers").with_layout(cce_ui::widget::AdaptiveGridLayout { min_col_width: 140.0, gap: 8.0, padding_x: 0.0, padding_y: 0.0 }));
- self.page_sec_containers.push(cce_ui::widget::SectionContainer::new("Windows").with_layout(cce_ui::widget::VerticalLayout::default()));
+ self.page_sec_containers.push(cce_ui::widget::SectionContainer::new("Windows").with_layout(cce_ui::widget::AdaptiveGridLayout { min_col_width: 140.0, gap: 8.0, padding_x: 0.0, padding_y: 0.0 }));
for i in 0..10 {
link_parent_child(page_root, &mut self.page_sec_containers[i], &mut self.ui_context);
@@ -646,7 +647,10 @@ impl SystemInterface {
let lcx = self.sidebar_width;
let lcy = self.header_height;
let lcw = sw / s - self.sidebar_width;
- let lch = sh / s - self.header_height - self.status_height;
+ let mut lch = sh / s - self.header_height - self.status_height;
+ if self.search_open {
+ lch -= 42.0;
+ }
let page_idx = Page::ALL.iter().position(|&p| p == self.app.current_page).unwrap_or(0);
self.menubar.set_selected_page(page_idx);
@@ -663,16 +667,56 @@ impl SystemInterface {
self.root_window.clear_children(&mut self.ui_context);
self.root_window.add_child(self.menubar.as_ptr(), &mut self.ui_context);
self.root_window.add_child(self.switcher.as_ptr(), &mut self.ui_context);
+ if self.search_open {
+ use cce_ui::widget::focus::link_parent_child;
+ link_parent_child(&mut self.root_window, &mut self.search_box, &mut self.ui_context);
+ }
// Position sidebar and switcher below the titlebar
let mut dummy_pc = PageContent::new();
cce_ui::layout::render_widget(&mut dummy_pc, &mut self.menubar, 0.0, self.header_height, self.sidebar_width, sh / s - self.header_height, &mut self.ui_context);
- cce_ui::layout::render_widget(&mut dummy_pc, &mut self.switcher, self.sidebar_width, self.header_height, sw / s - self.sidebar_width, sh / s - self.header_height, &mut self.ui_context);
+ let switcher_h = if self.search_open {
+ sh / s - self.header_height - 42.0
+ } else {
+ sh / s - self.header_height
+ };
+ cce_ui::layout::render_widget(&mut dummy_pc, &mut self.switcher, self.sidebar_width, self.header_height, sw / s - self.sidebar_width, switcher_h, &mut self.ui_context);
// Render root window recursively
let mut window_pc = PageContent::new();
cce_ui::layout::render_widget(&mut window_pc, &mut self.root_window, 0.0, 0.0, sw / s, sh / s, &mut self.ui_context);
+ let mut search_pc = PageContent::new();
+ if self.search_open {
+ search_pc.rects.push((
+ [0.08, 0.08, 0.12, 1.0],
+ self.sidebar_width,
+ sh / s - 42.0,
+ sw / s - self.sidebar_width,
+ 42.0,
+ 0.0,
+ (false, false, false, false),
+ ));
+ search_pc.rects.push((
+ [0.18, 0.18, 0.24, 1.0],
+ self.sidebar_width,
+ sh / s - 42.0,
+ sw / s - self.sidebar_width,
+ 1.0,
+ 0.0,
+ (false, false, false, false),
+ ));
+ cce_ui::layout::render_widget(
+ &mut search_pc,
+ &mut self.search_box,
+ self.sidebar_width + 12.0,
+ sh / s - 36.0,
+ sw / s - self.sidebar_width - 24.0,
+ 30.0,
+ &mut self.ui_context,
+ );
+ }
+
// CSD Titlebar removed
for pc_part in &[window_pc] {
@@ -712,6 +756,32 @@ impl SystemInterface {
// Page content in LOGICAL coordinates, then scale to physical
let pc = self.render_page_content(lcx, lcy, lcw, lch);
+ if self.search_open && !self.search_query.is_empty() && !self.pages[page_idx].scroll_bar.dragging {
+ let query_lower = self.search_query.to_lowercase();
+ let mut first_match_y = None;
+ for (t, _, _, y, _, _, _) in &pc.texts {
+ if t.to_lowercase().contains(&query_lower) {
+ first_match_y = Some(*y);
+ break;
+ }
+ }
+ if let Some(y) = first_match_y {
+ 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 {
+ let base = btn.base().unwrap();
+ max_y = max_y.max(base.y + base.h);
+ }
+ let local_max_scroll_y = (max_y - lch).max(0.0);
+ self.scroll_y = (y - 100.0).clamp(0.0, local_max_scroll_y);
+ }
+ }
+
let mut popovers = Vec::new();
Self::collect_popover_rects(&self.pages[page_idx], &mut popovers, &self.ui_context);
@@ -761,6 +831,52 @@ impl SystemInterface {
let shifted_bounds = bounds.map(|[bl, bt, br, bb]| {
[bl, bt - scroll_offset_y, br, bb - scroll_offset_y]
});
+
+ let matched = self.search_open && !self.search_query.is_empty() && t.to_lowercase().contains(&self.search_query.to_lowercase());
+
+ if matched {
+ let text_buf = 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,
+ );
+ let scale = cce_ui::scale::scale_factor();
+ let text_w = text_buf.layout_runs().next().map(|r| r.line_w).unwrap_or(0.0) / scale;
+
+ let pad_x = 4.0;
+ let pad_y = 2.0;
+ let rect_x = *x - pad_x;
+ let rect_y = *y - pad_y;
+ let rect_w = text_w + 2.0 * pad_x;
+ let rect_h = *size + 2.0 * pad_y;
+
+ widgets.push(AppWidget {
+ x: rect_x * s,
+ y: (rect_y - scroll_offset_y) * s,
+ w: rect_w * s,
+ h: rect_h * s,
+ color: [0.65, 0.45, 0.05, 0.4],
+ hover_color: [0.65, 0.45, 0.05, 0.4],
+ hovering: false,
+ radius: 3.0 * s,
+ corners: (true, true, true, true),
+ });
+ }
+
+ let text_color = if self.search_open && !self.search_query.is_empty() {
+ if matched {
+ [1.0, 0.95, 0.80, 1.0]
+ } else {
+ [tc[0] * 0.25, tc[1] * 0.25, tc[2] * 0.25, tc[3] * 0.25]
+ }
+ } else {
+ *tc
+ };
+
text_items.push(TextItem {
buffer: make_text_buffer_with_font(
&mut self.font_system,
@@ -773,7 +889,7 @@ impl SystemInterface {
),
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,
+ (text_color[0] * 255.0) as u8, (text_color[1] * 255.0) as u8, (text_color[2] * 255.0) as u8,
),
bounds: shifted_bounds,
});
@@ -883,6 +999,34 @@ impl SystemInterface {
+ for (c, x, y, w, h, r, corners) in &search_pc.rects {
+ widgets.push(AppWidget {
+ x: *x * s, y: *y * s, w: *w * s, h: *h * s,
+ color: *c, hover_color: *c,
+ hovering: false,
+ radius: *r * s,
+ corners: *corners,
+ });
+ }
+ for (t, size, x, y, tc, font_opt, bounds) in &search_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 * s,
+ color: glyphon::Color::rgb(
+ (tc[0] * 255.0) as u8, (tc[1] * 255.0) as u8, (tc[2] * 255.0) as u8,
+ ),
+ bounds: *bounds,
+ });
+ }
+
self.widgets = widgets;
self.text_items = text_items;
self.page_buttons = page_buttons;