system settings
git clone https://git.lucas.co/cce-system-interface.git
style: prevent double rendering, clean up fonts and pages, overhaul packages master-detail layout, and implement global viewport clipping
src/main.rs | 5 +-
src/pages/accounts.rs | 32 +++---
src/pages/audio.rs | 95 +++++++++++-----
src/pages/fonts.rs | 46 +++++---
src/pages/network.rs | 36 +++---
src/pages/notifications.rs | 4 +
src/pages/packages.rs | 266 ++++++++++++++++++++++++++-------------------
src/pages/processes.rs | 34 +++---
src/pages/storage.rs | 48 ++++----
src/renderer.rs | 101 +++++++++++++----
10 files changed, 427 insertions(+), 240 deletions(-)
diff --git a/src/main.rs b/src/main.rs
index b872e0e..4ff1e4a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -138,8 +138,7 @@ impl cce_ui::engine::Application for SystemInterface {
let win_opacity = 1.0f32;
let win_radius = 12;
- let mut font_system = FontSystem::new();
- font_system.db_mut().load_fonts_dir("/home/lsgalante/Dropbox/Fonts");
+ let font_system = cce_ui::create_font_system();
let mut this = Self {
app: app_state,
@@ -348,7 +347,7 @@ impl cce_ui::engine::Application for SystemInterface {
}
fn clear_color(&self) -> [f32; 4] {
- [0.0, 0.0, 0.0, 0.0]
+ [0.039, 0.102, 0.055, 1.0]
}
fn handle_pointer_move(&mut self, pos: cce_ui::engine::LogicalPosition, needs_rebuild: &mut bool) {
diff --git a/src/pages/accounts.rs b/src/pages/accounts.rs
index 7c47011..21cf72d 100644
--- a/src/pages/accounts.rs
+++ b/src/pages/accounts.rs
@@ -840,20 +840,24 @@ impl crate::pages::AppPage for AccountsState {
fn get_section_containers(&self) -> Vec<cce_ui::widget::SectionContainer> {
vec![
- cce_ui::widget::SectionContainer::new("Accounts").with_layout(cce_ui::widget::AdaptiveGridLayout {
- min_col_width: 140.0,
- gap: 8.0,
- padding_x: 0.0,
- padding_y: 0.0,
- grid: None,
- }),
- cce_ui::widget::SectionContainer::new("Modify Accounts").with_layout(cce_ui::widget::AdaptiveGridLayout {
- min_col_width: 140.0,
- gap: 8.0,
- padding_x: 0.0,
- padding_y: 0.0,
- grid: None,
- }),
+ cce_ui::widget::SectionContainer::new("Accounts")
+ .with_draw_children(false)
+ .with_layout(cce_ui::widget::AdaptiveGridLayout {
+ min_col_width: 140.0,
+ gap: 8.0,
+ padding_x: 0.0,
+ padding_y: 0.0,
+ grid: None,
+ }),
+ cce_ui::widget::SectionContainer::new("Modify Accounts")
+ .with_draw_children(false)
+ .with_layout(cce_ui::widget::AdaptiveGridLayout {
+ min_col_width: 140.0,
+ gap: 8.0,
+ padding_x: 0.0,
+ padding_y: 0.0,
+ grid: None,
+ }),
]
}
diff --git a/src/pages/audio.rs b/src/pages/audio.rs
index 039f1e3..a41f04f 100644
--- a/src/pages/audio.rs
+++ b/src/pages/audio.rs
@@ -428,20 +428,24 @@ impl crate::pages::AppPage for AudioState {
fn get_section_containers(&self) -> Vec<cce_ui::widget::SectionContainer> {
vec![
- cce_ui::widget::SectionContainer::new("Output").with_layout(cce_ui::widget::AdaptiveGridLayout {
- min_col_width: 140.0,
- gap: 8.0,
- padding_x: 0.0,
- padding_y: 0.0,
- grid: None,
- }),
- cce_ui::widget::SectionContainer::new("Input").with_layout(cce_ui::widget::AdaptiveGridLayout {
- min_col_width: 140.0,
- gap: 8.0,
- padding_x: 0.0,
- padding_y: 0.0,
- grid: None,
- }),
+ cce_ui::widget::SectionContainer::new("Output")
+ .with_layout(cce_ui::widget::AdaptiveGridLayout {
+ min_col_width: 140.0,
+ gap: 8.0,
+ padding_x: 0.0,
+ padding_y: 0.0,
+ grid: None,
+ })
+ .with_draw_children(false),
+ cce_ui::widget::SectionContainer::new("Input")
+ .with_layout(cce_ui::widget::AdaptiveGridLayout {
+ min_col_width: 140.0,
+ gap: 8.0,
+ padding_x: 0.0,
+ padding_y: 0.0,
+ grid: None,
+ })
+ .with_draw_children(false),
]
}
@@ -454,17 +458,26 @@ impl crate::pages::AppPage for AudioState {
cce_ui::widget::link_parent_child(page_root, &mut sec_containers[0], ctx);
cce_ui::widget::link_parent_child(page_root, &mut sec_containers[1], ctx);
- for sb in &mut self.sink_spinboxes {
- cce_ui::widget::link_parent_child(&mut sec_containers[0], &mut **sb, ctx);
- }
- for sb in &mut self.source_spinboxes {
- cce_ui::widget::link_parent_child(&mut sec_containers[1], &mut **sb, ctx);
- }
- for slider in &mut self.sink_sliders {
- cce_ui::widget::link_parent_child(&mut sec_containers[0], &mut **slider, ctx);
+ for (i, sink) in self.sinks.iter().enumerate() {
+ if sink.active {
+ if i < self.sink_spinboxes.len() {
+ cce_ui::widget::link_parent_child(&mut sec_containers[0], &mut *self.sink_spinboxes[i], ctx);
+ }
+ if i < self.sink_sliders.len() {
+ cce_ui::widget::link_parent_child(&mut sec_containers[0], &mut *self.sink_sliders[i], ctx);
+ }
+ }
}
- for slider in &mut self.source_sliders {
- cce_ui::widget::link_parent_child(&mut sec_containers[1], &mut **slider, ctx);
+
+ for (i, src) in self.sources.iter().enumerate() {
+ if src.active {
+ if i < self.source_spinboxes.len() {
+ cce_ui::widget::link_parent_child(&mut sec_containers[1], &mut *self.source_spinboxes[i], ctx);
+ }
+ if i < self.source_sliders.len() {
+ cce_ui::widget::link_parent_child(&mut sec_containers[1], &mut *self.source_sliders[i], ctx);
+ }
+ }
}
}
@@ -583,9 +596,41 @@ mod tests {
#[test]
fn test_view_layout_grid() {
- let mut state = AudioState::default();
+ use cce_ui::widget::{Spinbox, Slider};
+ let mut state = AudioState {
+ loaded: true,
+ sinks: vec![
+ AudioSink { id: 71, name: "Speaker".to_string(), volume: 0.57, muted: false, active: true },
+ AudioSink { id: 70, name: "HDMI1".to_string(), volume: 0.5, muted: false, active: false },
+ AudioSink { id: 69, name: "HDMI2".to_string(), volume: 0.5, muted: false, active: false },
+ AudioSink { id: 68, name: "HDMI3".to_string(), volume: 0.5, muted: false, active: false },
+ ],
+ sources: vec![],
+ sink_spinboxes: vec![
+ Box::new(Spinbox::new(57, 0, 100, 1)),
+ Box::new(Spinbox::new(50, 0, 100, 1)),
+ Box::new(Spinbox::new(50, 0, 100, 1)),
+ Box::new(Spinbox::new(50, 0, 100, 1)),
+ ],
+ source_spinboxes: vec![],
+ sink_sliders: vec![
+ Box::new(Slider::new()),
+ Box::new(Slider::new()),
+ Box::new(Slider::new()),
+ Box::new(Slider::new()),
+ ],
+ source_sliders: vec![],
+ sink_dragging: None,
+ source_dragging: None,
+ };
let mut layout = AdaptiveGrid::new(260.0, 20.0);
let pc = view(&mut state, 10.0, 20.0, 800.0, 600.0, &[false, false], &mut layout, &mut cce_ui::context::UiContext::new());
+ for (i, (c, x, y, w, h, r, _)) in pc.rects.iter().enumerate() {
+ println!("TEST_PC_RECT {}: color={:?}, x={}, y={}, w={}, h={}, r={}", i, c, x, y, w, h, r);
+ }
+ for (i, (t, sz, x, y, c, _, _)) in pc.texts.iter().enumerate() {
+ println!("TEST_PC_TEXT {}: text='{}', size={}, x={}, y={}, color={:?}", i, t, sz, x, y, c);
+ }
assert!(!pc.rects.is_empty() || !pc.texts.is_empty());
}
diff --git a/src/pages/fonts.rs b/src/pages/fonts.rs
index 1d3c590..5330159 100644
--- a/src/pages/fonts.rs
+++ b/src/pages/fonts.rs
@@ -120,11 +120,11 @@ impl AppPage for FontsState {
fn get_section_containers(&self) -> Vec<cce_ui::widget::SectionContainer> {
vec![
- cce_ui::widget::SectionContainer::new("Preferred Fonts"),
- cce_ui::widget::SectionContainer::new("Borders"),
- cce_ui::widget::SectionContainer::new("Status Interface"),
- cce_ui::widget::SectionContainer::new("Fuzzel"),
- cce_ui::widget::SectionContainer::new("Terminal"),
+ cce_ui::widget::SectionContainer::new("Preferred Fonts").with_draw_children(false),
+ cce_ui::widget::SectionContainer::new("Borders").with_draw_children(false),
+ cce_ui::widget::SectionContainer::new("Status Interface").with_draw_children(false),
+ cce_ui::widget::SectionContainer::new("Fuzzel").with_draw_children(false),
+ cce_ui::widget::SectionContainer::new("Terminal").with_draw_children(false),
]
}
@@ -173,39 +173,49 @@ impl AppPage for FontsState {
let mut builder = cce_ui::layout::PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(5);
builder.add_section(&mut final_pc, "Preferred Fonts", sec_focused.get(0).copied().unwrap_or(false), |sec| {
- sec.widget_full(&mut self.sans_box, 44.0, ctx);
- sec.widget_full(&mut self.serif_box, 44.0, ctx);
- sec.widget_full(&mut self.mono_box, 44.0, ctx);
+ let mut stack = sec.vstack(8.0);
+ let sec_w = stack.context.cw;
+ stack.add_widget(&mut self.sans_box, sec_w - 28.0, 44.0, ctx);
+ stack.add_widget(&mut self.serif_box, sec_w - 28.0, 44.0, ctx);
+ stack.add_widget(&mut self.mono_box, sec_w - 28.0, 44.0, ctx);
});
builder.add_section(&mut final_pc, "Borders", sec_focused.get(1).copied().unwrap_or(false), |sec| {
- sec.widget_full(&mut self.borders_menu, 44.0, ctx);
+ let mut stack = sec.vstack(8.0);
+ let sec_w = stack.context.cw;
+ stack.add_widget(&mut self.borders_menu, sec_w - 28.0, 44.0, ctx);
if self.borders_menu.selected == 3 {
- sec.widget_full(&mut self.borders_box, 44.0, ctx);
+ stack.add_widget(&mut self.borders_box, sec_w - 28.0, 44.0, ctx);
}
});
builder.add_section(&mut final_pc, "Status Interface", sec_focused.get(2).copied().unwrap_or(false), |sec| {
- sec.widget_full(&mut self.status_menu, 44.0, ctx);
+ let mut stack = sec.vstack(8.0);
+ let sec_w = stack.context.cw;
+ stack.add_widget(&mut self.status_menu, sec_w - 28.0, 44.0, ctx);
if self.status_menu.selected == 3 {
- sec.widget_full(&mut self.status_box, 44.0, ctx);
+ stack.add_widget(&mut self.status_box, sec_w - 28.0, 44.0, ctx);
}
});
builder.add_section(&mut final_pc, "Fuzzel", sec_focused.get(3).copied().unwrap_or(false), |sec| {
- sec.widget_full(&mut self.fuzzel_menu, 44.0, ctx);
+ let mut stack = sec.vstack(8.0);
+ let sec_w = stack.context.cw;
+ stack.add_widget(&mut self.fuzzel_menu, sec_w - 28.0, 44.0, ctx);
if self.fuzzel_menu.selected == 3 {
- sec.widget_full(&mut self.fuzzel_box, 44.0, ctx);
+ stack.add_widget(&mut self.fuzzel_box, sec_w - 28.0, 44.0, ctx);
}
- sec.widget_full(&mut self.fuzzel_size_box, 44.0, ctx);
+ stack.add_widget(&mut self.fuzzel_size_box, sec_w - 28.0, 44.0, ctx);
});
builder.add_section(&mut final_pc, "Terminal", sec_focused.get(4).copied().unwrap_or(false), |sec| {
- sec.widget_full(&mut self.terminal_menu, 44.0, ctx);
+ let mut stack = sec.vstack(8.0);
+ let sec_w = stack.context.cw;
+ stack.add_widget(&mut self.terminal_menu, sec_w - 28.0, 44.0, ctx);
if self.terminal_menu.selected == 3 {
- sec.widget_full(&mut self.terminal_box, 44.0, ctx);
+ stack.add_widget(&mut self.terminal_box, sec_w - 28.0, 44.0, ctx);
}
- sec.widget_full(&mut self.terminal_size_box, 44.0, ctx);
+ stack.add_widget(&mut self.terminal_size_box, sec_w - 28.0, 44.0, ctx);
});
final_pc
diff --git a/src/pages/network.rs b/src/pages/network.rs
index 57e10f5..d420ee3 100644
--- a/src/pages/network.rs
+++ b/src/pages/network.rs
@@ -526,20 +526,24 @@ impl crate::pages::AppPage for NetworkState {
fn get_section_containers(&self) -> Vec<cce_ui::widget::SectionContainer> {
vec![
- cce_ui::widget::SectionContainer::new("WiFi").with_layout(cce_ui::widget::AdaptiveGridLayout {
- min_col_width: 140.0,
- gap: 8.0,
- padding_x: 0.0,
- padding_y: 0.0,
- grid: None,
- }),
- cce_ui::widget::SectionContainer::new("Bluetooth").with_layout(cce_ui::widget::AdaptiveGridLayout {
- min_col_width: 140.0,
- gap: 8.0,
- padding_x: 0.0,
- padding_y: 0.0,
- grid: None,
- }),
+ cce_ui::widget::SectionContainer::new("WiFi")
+ .with_draw_children(false)
+ .with_layout(cce_ui::widget::AdaptiveGridLayout {
+ min_col_width: 140.0,
+ gap: 8.0,
+ padding_x: 0.0,
+ padding_y: 0.0,
+ grid: None,
+ }),
+ cce_ui::widget::SectionContainer::new("Bluetooth")
+ .with_draw_children(false)
+ .with_layout(cce_ui::widget::AdaptiveGridLayout {
+ min_col_width: 140.0,
+ gap: 8.0,
+ padding_x: 0.0,
+ padding_y: 0.0,
+ grid: None,
+ }),
]
}
@@ -553,9 +557,7 @@ impl crate::pages::AppPage for NetworkState {
cce_ui::widget::link_parent_child(page_root, &mut sec_containers[1], ctx);
cce_ui::widget::link_parent_child(&mut sec_containers[0], &mut self.wifi_toggle, ctx);
- if self.wifi_enabled && !self.available.is_empty() {
- cce_ui::widget::link_parent_child(&mut sec_containers[0], &mut self.wifi_list_box.scroll_box, ctx);
- }
+ cce_ui::widget::link_parent_child(&mut sec_containers[0], &mut self.wifi_list_box.scroll_box, ctx);
cce_ui::widget::link_parent_child(&mut sec_containers[1], &mut self.bt_toggle, ctx);
}
diff --git a/src/pages/notifications.rs b/src/pages/notifications.rs
index f7c32ee..ec87eee 100644
--- a/src/pages/notifications.rs
+++ b/src/pages/notifications.rs
@@ -195,6 +195,7 @@ impl AppPage for NotificationsState {
fn get_section_containers(&self) -> Vec<cce_ui::widget::SectionContainer> {
vec![
cce_ui::widget::SectionContainer::new("Notifications Settings")
+ .with_draw_children(false)
.with_layout(cce_ui::widget::AdaptiveGridLayout {
min_col_width: 140.0,
gap: 8.0,
@@ -214,6 +215,9 @@ impl AppPage for NotificationsState {
for sec in sec_containers.iter_mut() {
cce_ui::widget::link_parent_child(page_root, sec, ctx);
}
+ cce_ui::widget::link_parent_child(&mut sec_containers[0], &mut self.enable_toggle, ctx);
+ cce_ui::widget::link_parent_child(&mut sec_containers[0], &mut self.bell_menu, ctx);
+ cce_ui::widget::link_parent_child(&mut sec_containers[0], &mut self.duration_spinbox, ctx);
}
fn view(
diff --git a/src/pages/packages.rs b/src/pages/packages.rs
index 3ef981b..ecaf534 100644
--- a/src/pages/packages.rs
+++ b/src/pages/packages.rs
@@ -327,8 +327,8 @@ pub fn view(
ctx: &mut cce_ui::context::UiContext,
) -> PageContent {
let mut final_pc = PageContent::new();
- let sec_w = 320.0f32;
- let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(2);
+ let sec_w = 260.0f32;
+ let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(3);
// Section 1: Packages List
builder.add_section(&mut final_pc, "Packages", sec_focused.first().copied().unwrap_or(false), |sec| {
@@ -398,6 +398,7 @@ pub fn view(
match state.active_tab {
PackageTab::Installed => {
+ state.installed_list_box.clear_children(ctx);
render_widget(sec.pc, &mut state.installed_list_box, list_box_x, list_box_y, list_box_w, list_box_h, ctx);
let filtered: Vec<&PackageInfo> = state.installed.iter()
@@ -421,6 +422,7 @@ pub fn view(
item.title = pkg.name.clone();
item.subtitle = Some(format!("Version: {}", pkg.version));
item.selected = Some(&pkg.name) == state.selected_package.as_ref();
+ cce_ui::widget::link_parent_child(&mut state.installed_list_box.scroll_box, item, ctx);
render_widget(sec.pc, item, list_box_x + 24.0, draw_y, list_box_w - 44.0, item_h, ctx);
}
}
@@ -431,6 +433,7 @@ pub fn view(
}
}
PackageTab::Updates => {
+ state.updates_list_box.clear_children(ctx);
render_widget(sec.pc, &mut state.updates_list_box, list_box_x, list_box_y, list_box_w, list_box_h, ctx);
let filtered: Vec<&UpdateInfo> = state.updates.iter()
@@ -454,6 +457,7 @@ pub fn view(
item.title = pkg.name.clone();
item.subtitle = Some(format!("{} -> {}", pkg.old_version, pkg.new_version));
item.selected = Some(&pkg.name) == state.selected_package.as_ref();
+ cce_ui::widget::link_parent_child(&mut state.updates_list_box.scroll_box, item, ctx);
render_widget(sec.pc, item, list_box_x + 24.0, draw_y, list_box_w - 44.0, item_h, ctx);
}
}
@@ -466,103 +470,102 @@ pub fn view(
}
sec.content_y += list_box_h;
+ }
+ });
- sec.add_section("Package Info", false, |subsec| {
- if state.loading_info {
- subsec.text("Loading package details...", 12.0, 0.0, 12.0, TEXT_DIM);
- } else if let Some(ref pkg_name) = state.selected_package {
- if let Some(ref info_raw) = state.selected_package_info {
- let mut parsed = parse_package_info(info_raw);
- if parsed.name.is_empty() {
- parsed.name = pkg_name.clone();
- }
+ // Section 2: Package Details Info
+ builder.add_section(&mut final_pc, "Package Info", sec_focused.get(1).copied().unwrap_or(false), |sec1| {
+ if state.loading_info {
+ sec1.text("Loading package details...", 12.0, 0.0, 12.0, TEXT_DIM);
+ } else if let Some(ref pkg_name) = state.selected_package {
+ if let Some(ref info_raw) = state.selected_package_info {
+ let mut parsed = parse_package_info(info_raw);
+ if parsed.name.is_empty() {
+ parsed.name = pkg_name.clone();
+ }
- subsec.text(&parsed.name, 12.0, 0.0, 13.0, WHITE);
+ sec1.text(&parsed.name, 12.0, 0.0, 13.0, WHITE);
- let render_detail = |sub: &mut SectionContext<'_, PageContent>, key: &str, val: &str| {
- sub.text(key, 12.0, 0.0, 11.0, TEXT_DIM);
- let val_start_x = 90.0f32;
- let usable_w = sub.cw - val_start_x - 12.0;
- let char_w = 6.0f32;
- let max_chars = (usable_w / char_w).max(15.0) as usize;
+ let render_detail = |sub: &mut SectionContext<'_, PageContent>, key: &str, val: &str| {
+ sub.text(key, 12.0, 0.0, 11.0, TEXT_DIM);
+ let val_start_x = 90.0f32;
+ let usable_w = sub.cw - val_start_x - 12.0;
+ let char_w = 6.0f32;
+ let max_chars = (usable_w / char_w).max(15.0) as usize;
- let lines = wrap_text(val, max_chars);
- for line in &lines {
- sub.text(line, val_start_x, 0.0, 11.0, TEXT_FG);
- }
- if lines.is_empty() {
- }
- };
+ let lines = wrap_text(val, max_chars);
+ for line in &lines {
+ sub.text(line, val_start_x, 0.0, 11.0, TEXT_FG);
+ }
+ };
- render_detail(subsec, "Version:", &parsed.version);
- if !parsed.size.is_empty() {
- render_detail(subsec, "Size:", &parsed.size);
- }
- if !parsed.licenses.is_empty() {
- render_detail(subsec, "Licenses:", &parsed.licenses);
- }
- if !parsed.website.is_empty() {
- render_detail(subsec, "Website:", &parsed.website);
- }
- if !parsed.packager.is_empty() {
- render_detail(subsec, "Packager:", &parsed.packager);
- }
- if !parsed.build_date.is_empty() {
- render_detail(subsec, "Build Date:", &parsed.build_date);
- }
- if !parsed.description.is_empty() {
- subsec.separator();
- render_detail(subsec, "Description:", &parsed.description);
- }
- if !parsed.required_by.is_empty() && parsed.required_by != "None" {
- subsec.separator();
- subsec.text("Required By:", 12.0, 0.0, 11.0, TEXT_DIM);
-
- let reqs: Vec<&str> = parsed.required_by.split_whitespace().collect();
- let cols_count = 3;
- let gap = 6.0;
- let btn_h = 24.0;
-
- for chunk in reqs.chunks(cols_count) {
- let btn_y = subsec.ay();
- let cols = subsec.row_layout(cols_count, gap);
- for (i, &pkg) in chunk.iter().enumerate() {
- if let Some(&(x, w)) = cols.get(i) {
- let action = AppAction::Packages(PackagesMessage::SelectAndScrollPackage(pkg.to_string()));
- subsec.button(pkg, x, btn_y, w, btn_h, TOGGLE_OFF, BTN_HOVER, TEXT_FG, action);
- }
- }
+ render_detail(sec1, "Version:", &parsed.version);
+ if !parsed.size.is_empty() {
+ render_detail(sec1, "Size:", &parsed.size);
+ }
+ if !parsed.licenses.is_empty() {
+ render_detail(sec1, "Licenses:", &parsed.licenses);
+ }
+ if !parsed.website.is_empty() {
+ render_detail(sec1, "Website:", &parsed.website);
+ }
+ if !parsed.packager.is_empty() {
+ render_detail(sec1, "Packager:", &parsed.packager);
+ }
+ if !parsed.build_date.is_empty() {
+ render_detail(sec1, "Build Date:", &parsed.build_date);
+ }
+ if !parsed.description.is_empty() {
+ sec1.separator();
+ render_detail(sec1, "Description:", &parsed.description);
+ }
+ if !parsed.required_by.is_empty() && parsed.required_by != "None" {
+ sec1.separator();
+ sec1.text("Required By:", 12.0, 0.0, 11.0, TEXT_DIM);
+
+ let reqs: Vec<&str> = parsed.required_by.split_whitespace().collect();
+ let cols_count = 3;
+ let gap = 6.0;
+ let btn_h = 24.0;
+
+ for chunk in reqs.chunks(cols_count) {
+ let btn_y = sec1.ay();
+ let cols = sec1.row_layout(cols_count, gap);
+ for (i, &pkg) in chunk.iter().enumerate() {
+ if let Some(&(x, w)) = cols.get(i) {
+ let action = AppAction::Packages(PackagesMessage::SelectAndScrollPackage(pkg.to_string()));
+ sec1.button(pkg, x, btn_y, w, btn_h, TOGGLE_OFF, BTN_HOVER, TEXT_FG, action);
}
}
- if !parsed.commands.is_empty() {
- subsec.separator();
- render_detail(subsec, "Commands:", &parsed.commands);
- }
+ }
+ }
+ if !parsed.commands.is_empty() {
+ sec1.separator();
+ render_detail(sec1, "Commands:", &parsed.commands);
+ }
- if state.active_tab == PackageTab::Installed {
- let mut stack = subsec.vstack(8.0);
- let btn_h = 32.0;
- let (btn_lbl, bg, hover, action) = if state.uninstalling {
- ("Uninstalling...", TOGGLE_OFF, TOGGLE_OFF, AppAction::Packages(PackagesMessage::StartUninstall(pkg_name.clone())))
- } else {
- ("Uninstall Package", RED, BTN_HOVER, AppAction::Packages(PackagesMessage::StartUninstall(pkg_name.clone())))
- };
- stack.add_row(1, 0.0, btn_h, |ctx, _, x, w| {
- ctx.button(btn_lbl, x, ctx.ay(), w, btn_h, bg, hover, WHITE, action.clone());
- });
- }
+ if state.active_tab == PackageTab::Installed {
+ let mut stack = sec1.vstack(8.0);
+ let btn_h = 32.0;
+ let (btn_lbl, bg, hover, action) = if state.uninstalling {
+ ("Uninstalling...", TOGGLE_OFF, TOGGLE_OFF, AppAction::Packages(PackagesMessage::StartUninstall(pkg_name.clone())))
} else {
- subsec.text("No details available.", 12.0, 0.0, 12.0, TEXT_DIM);
- }
- } else {
- subsec.text("Select a package to view details.", 12.0, 0.0, 12.0, TEXT_DIM);
+ ("Uninstall Package", RED, BTN_HOVER, AppAction::Packages(PackagesMessage::StartUninstall(pkg_name.clone())))
+ };
+ stack.add_row(1, 0.0, btn_h, |ctx, _, x, w| {
+ ctx.button(btn_lbl, x, ctx.ay(), w, btn_h, bg, hover, WHITE, action.clone());
+ });
}
- });
+ } else {
+ sec1.text("No details available.", 12.0, 0.0, 12.0, TEXT_DIM);
+ }
+ } else {
+ sec1.text("Select a package to view details.", 12.0, 0.0, 12.0, TEXT_DIM);
}
});
- // Section 2: Update Actions / Status
- builder.add_section(&mut final_pc, "System Update", sec_focused.get(1).copied().unwrap_or(false), |sec2| {
+ // Section 3: Update Actions / Status
+ builder.add_section(&mut final_pc, "System Update", sec_focused.get(2).copied().unwrap_or(false), |sec2| {
if !state.loaded {
sec2.text("Loading update status...", 12.0, 0.0, 12.0, TEXT_DIM);
} else {
@@ -715,20 +718,33 @@ impl crate::pages::AppPage for PackagesState {
fn get_section_containers(&self) -> Vec<cce_ui::widget::SectionContainer> {
vec![
- cce_ui::widget::SectionContainer::new("Packages").with_layout(cce_ui::widget::AdaptiveGridLayout {
- min_col_width: 140.0,
- gap: 8.0,
- padding_x: 0.0,
- padding_y: 0.0,
- grid: None,
- }),
- cce_ui::widget::SectionContainer::new("System Update").with_layout(cce_ui::widget::AdaptiveGridLayout {
- min_col_width: 140.0,
- gap: 8.0,
- padding_x: 0.0,
- padding_y: 0.0,
- grid: None,
- }),
+ cce_ui::widget::SectionContainer::new("Packages")
+ .with_draw_children(false)
+ .with_layout(cce_ui::widget::AdaptiveGridLayout {
+ min_col_width: 140.0,
+ gap: 8.0,
+ padding_x: 0.0,
+ padding_y: 0.0,
+ grid: None,
+ }),
+ cce_ui::widget::SectionContainer::new("Package Info")
+ .with_draw_children(false)
+ .with_layout(cce_ui::widget::AdaptiveGridLayout {
+ min_col_width: 140.0,
+ gap: 8.0,
+ padding_x: 0.0,
+ padding_y: 0.0,
+ grid: None,
+ }),
+ cce_ui::widget::SectionContainer::new("System Update")
+ .with_draw_children(false)
+ .with_layout(cce_ui::widget::AdaptiveGridLayout {
+ min_col_width: 140.0,
+ gap: 8.0,
+ padding_x: 0.0,
+ padding_y: 0.0,
+ grid: None,
+ }),
]
}
@@ -740,16 +756,11 @@ impl crate::pages::AppPage for PackagesState {
) {
cce_ui::widget::link_parent_child(page_root, &mut sec_containers[0], ctx);
cce_ui::widget::link_parent_child(page_root, &mut sec_containers[1], ctx);
+ cce_ui::widget::link_parent_child(page_root, &mut sec_containers[2], ctx);
cce_ui::widget::link_parent_child(&mut sec_containers[0], &mut self.search_box, ctx);
- match self.active_tab {
- PackageTab::Installed => {
- cce_ui::widget::link_parent_child(&mut sec_containers[0], &mut self.installed_list_box.scroll_box, ctx);
- }
- PackageTab::Updates => {
- cce_ui::widget::link_parent_child(&mut sec_containers[0], &mut self.updates_list_box.scroll_box, ctx);
- }
- }
+ cce_ui::widget::link_parent_child(&mut sec_containers[0], &mut self.installed_list_box.scroll_box, ctx);
+ cce_ui::widget::link_parent_child(&mut sec_containers[0], &mut self.updates_list_box.scroll_box, ctx);
}
fn view(
@@ -766,7 +777,44 @@ impl crate::pages::AppPage for PackagesState {
view(self, cx, cy, cw, ch, sec_focused, layout, ctx)
}
- fn propagate_widget_changes(&mut self, _actions: &mut Vec<crate::app::AppAction>) {}
+ fn propagate_widget_changes(&mut self, actions: &mut Vec<crate::app::AppAction>) {
+ let query = if self.search_box.editing {
+ self.search_box.edit_buffer.to_lowercase()
+ } else {
+ self.search_box.text.to_lowercase()
+ };
+
+ match self.active_tab {
+ PackageTab::Installed => {
+ let filtered: Vec<&PackageInfo> = self.installed.iter()
+ .filter(|p| p.name.to_lowercase().contains(&query) || p.version.to_lowercase().contains(&query))
+ .collect();
+ for (idx, item) in self.installed_items.iter_mut().enumerate() {
+ if item.just_clicked {
+ item.just_clicked = false;
+ if idx < filtered.len() {
+ let pkg = filtered[idx];
+ actions.push(AppAction::Packages(PackagesMessage::SelectPackage(Some(pkg.name.clone()))));
+ }
+ }
+ }
+ }
+ PackageTab::Updates => {
+ let filtered: Vec<&UpdateInfo> = self.updates.iter()
+ .filter(|p| p.name.to_lowercase().contains(&query))
+ .collect();
+ for (idx, item) in self.updates_items.iter_mut().enumerate() {
+ if item.just_clicked {
+ item.just_clicked = false;
+ if idx < filtered.len() {
+ let pkg = filtered[idx];
+ actions.push(AppAction::Packages(PackagesMessage::SelectPackage(Some(pkg.name.clone()))));
+ }
+ }
+ }
+ }
+ }
+ }
}
#[cfg(test)]
diff --git a/src/pages/processes.rs b/src/pages/processes.rs
index 588bed5..befa3f1 100644
--- a/src/pages/processes.rs
+++ b/src/pages/processes.rs
@@ -230,6 +230,7 @@ pub fn view(state: &mut ProcessesState, cx: f32, cy: f32, cw: f32, ch: f32, root
let list_box_w = sec_w - 24.0;
let list_box_h = 360.0;
+ state.services_list_box.clear_children(ctx);
render_widget(sec.pc, &mut state.services_list_box, list_box_x, list_box_y, list_box_w, list_box_h, ctx);
// Filter services
@@ -288,6 +289,7 @@ pub fn view(state: &mut ProcessesState, cx: f32, cy: f32, cw: f32, ch: f32, root
let item_btn = &mut state.service_items[idx];
item_btn.title = service.name.clone();
item_btn.subtitle = Some(desc_truncated);
+ cce_ui::widget::link_parent_child(&mut state.services_list_box.scroll_box, item_btn, ctx);
render_widget(sec.pc, item_btn, list_box_x + 24.0, draw_y, list_box_w - 44.0, item_h, ctx);
// Render StatusDot
@@ -489,20 +491,24 @@ impl crate::pages::AppPage for ProcessesState {
fn get_section_containers(&self) -> Vec<cce_ui::widget::SectionContainer> {
vec![
- cce_ui::widget::SectionContainer::new("Processes").with_layout(cce_ui::widget::AdaptiveGridLayout {
- min_col_width: 140.0,
- gap: 8.0,
- padding_x: 0.0,
- padding_y: 0.0,
- grid: None,
- }),
- cce_ui::widget::SectionContainer::new("Services").with_layout(cce_ui::widget::AdaptiveGridLayout {
- min_col_width: 140.0,
- gap: 8.0,
- padding_x: 0.0,
- padding_y: 0.0,
- grid: None,
- }),
+ cce_ui::widget::SectionContainer::new("Processes")
+ .with_draw_children(false)
+ .with_layout(cce_ui::widget::AdaptiveGridLayout {
+ min_col_width: 140.0,
+ gap: 8.0,
+ padding_x: 0.0,
+ padding_y: 0.0,
+ grid: None,
+ }),
+ cce_ui::widget::SectionContainer::new("Services")
+ .with_draw_children(false)
+ .with_layout(cce_ui::widget::AdaptiveGridLayout {
+ min_col_width: 140.0,
+ gap: 8.0,
+ padding_x: 0.0,
+ padding_y: 0.0,
+ grid: None,
+ }),
]
}
diff --git a/src/pages/storage.rs b/src/pages/storage.rs
index 87a9bb2..429f8f6 100644
--- a/src/pages/storage.rs
+++ b/src/pages/storage.rs
@@ -309,27 +309,33 @@ impl crate::pages::AppPage for StorageState {
fn get_section_containers(&self) -> Vec<cce_ui::widget::SectionContainer> {
vec![
- cce_ui::widget::SectionContainer::new("Local Storage").with_layout(cce_ui::widget::AdaptiveGridLayout {
- min_col_width: 140.0,
- gap: 8.0,
- padding_x: 0.0,
- padding_y: 0.0,
- grid: None,
- }),
- cce_ui::widget::SectionContainer::new("Memory").with_layout(cce_ui::widget::AdaptiveGridLayout {
- min_col_width: 140.0,
- gap: 8.0,
- padding_x: 0.0,
- padding_y: 0.0,
- grid: None,
- }),
- cce_ui::widget::SectionContainer::new("Full System Backup").with_layout(cce_ui::widget::AdaptiveGridLayout {
- min_col_width: 140.0,
- gap: 8.0,
- padding_x: 0.0,
- padding_y: 0.0,
- grid: None,
- }),
+ cce_ui::widget::SectionContainer::new("Local Storage")
+ .with_draw_children(false)
+ .with_layout(cce_ui::widget::AdaptiveGridLayout {
+ min_col_width: 140.0,
+ gap: 8.0,
+ padding_x: 0.0,
+ padding_y: 0.0,
+ grid: None,
+ }),
+ cce_ui::widget::SectionContainer::new("Memory")
+ .with_draw_children(false)
+ .with_layout(cce_ui::widget::AdaptiveGridLayout {
+ min_col_width: 140.0,
+ gap: 8.0,
+ padding_x: 0.0,
+ padding_y: 0.0,
+ grid: None,
+ }),
+ cce_ui::widget::SectionContainer::new("Full System Backup")
+ .with_draw_children(false)
+ .with_layout(cce_ui::widget::AdaptiveGridLayout {
+ min_col_width: 140.0,
+ gap: 8.0,
+ padding_x: 0.0,
+ padding_y: 0.0,
+ grid: None,
+ }),
]
}
diff --git a/src/renderer.rs b/src/renderer.rs
index abf8542..81c8922 100644
--- a/src/renderer.rs
+++ b/src/renderer.rs
@@ -194,6 +194,8 @@ 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;
@@ -259,9 +261,23 @@ impl SystemInterface {
for (idx, (c, x, y, w, h, r, corners)) in pc.rects.iter().enumerate() {
eprintln!("PAGE_PC_RECT idx={}: color={:?}, x={}, y={}, w={}, h={}", idx, c, x, y, w, h);
let wx = *x * s;
- let wy = (*y - scroll_offset_y) * s;
+ let mut wy = (*y - scroll_offset_y) * s;
let ww = *w * s;
- let wh = *h * s;
+ let mut wh = *h * s;
+
+ let viewport_bottom = logical_sh - self.status_height;
+ if wy >= viewport_bottom || wy + wh <= 0.0 {
+ continue;
+ }
+ if wy < 0.0 {
+ let diff = 0.0 - wy;
+ wy = 0.0;
+ wh = (wh - diff).max(0.0);
+ }
+ if wy + wh > viewport_bottom {
+ wh = (viewport_bottom - wy).max(0.0);
+ }
+
widgets.push(AppWidget {
x: wx, y: wy, w: ww, h: wh,
color: *c, hover_color: *c,
@@ -276,6 +292,22 @@ impl SystemInterface {
[bl, bt - scroll_offset_y, br, bb - scroll_offset_y]
});
+ let viewport_bottom = logical_sh - self.status_height;
+ let final_bounds = match shifted_bounds {
+ Some(b) => Some([
+ b[0],
+ b[1].max(0.0),
+ b[2],
+ b[3].min(viewport_bottom),
+ ]),
+ None => Some([
+ 0.0,
+ 0.0,
+ logical_sw,
+ viewport_bottom,
+ ]),
+ };
+
let matched = self.search_open && !self.search_query.is_empty() && t.to_lowercase().contains(&self.search_query.to_lowercase());
if matched {
@@ -299,20 +331,31 @@ impl SystemInterface {
let rect_h = *size + 2.0 * pad_y;
let wx = rect_x * s;
- let wy = (rect_y - scroll_offset_y) * s;
+ let mut wy = (rect_y - scroll_offset_y) * s;
let ww = rect_w * s;
- let wh = rect_h * s;
- widgets.push(AppWidget {
- x: wx,
- y: wy,
- w: ww,
- h: wh,
- color: [0.65, 0.45, 0.05, 0.4],
- hover_color: [0.65, 0.45, 0.05, 0.4],
- hovering: check_hover(wx, wy, ww, wh),
- radius: 3.0 * s,
- corners: (true, true, true, true),
- });
+ let mut wh = rect_h * s;
+
+ if wy < viewport_bottom && wy + wh > 0.0 {
+ if wy < 0.0 {
+ let diff = 0.0 - wy;
+ wy = 0.0;
+ wh = (wh - diff).max(0.0);
+ }
+ if wy + wh > viewport_bottom {
+ wh = (viewport_bottom - wy).max(0.0);
+ }
+ widgets.push(AppWidget {
+ x: wx,
+ y: wy,
+ w: ww,
+ h: wh,
+ color: [0.65, 0.45, 0.05, 0.4],
+ hover_color: [0.65, 0.45, 0.05, 0.4],
+ hovering: check_hover(wx, wy, ww, wh),
+ radius: 3.0 * s,
+ corners: (true, true, true, true),
+ });
+ }
}
let text_color = if self.search_open && !self.search_query.is_empty() {
@@ -339,7 +382,7 @@ impl SystemInterface {
color: glyphon::Color::rgb(
(text_color[0] * 255.0) as u8, (text_color[1] * 255.0) as u8, (text_color[2] * 255.0) as u8,
),
- bounds: shifted_bounds,
+ bounds: final_bounds,
});
}
for (btn, action) in &pc.buttons {
@@ -347,9 +390,23 @@ impl SystemInterface {
let bg = btn.bg.unwrap_or([0.16, 0.16, 0.24, 1.0]);
let hover_bg = btn.hover_bg.unwrap_or([0.25, 0.30, 0.26, 1.0]);
let wx = base.x * s;
- let wy = (base.y - scroll_offset_y) * s;
+ let mut wy = (base.y - scroll_offset_y) * s;
let ww = base.w * s;
- let wh = base.h * s;
+ let mut wh = base.h * s;
+
+ let viewport_bottom = logical_sh - self.status_height;
+ if wy >= viewport_bottom || wy + wh <= 0.0 {
+ continue;
+ }
+ if wy < 0.0 {
+ let diff = 0.0 - wy;
+ wy = 0.0;
+ wh = (wh - diff).max(0.0);
+ }
+ if wy + wh > viewport_bottom {
+ wh = (viewport_bottom - wy).max(0.0);
+ }
+
widgets.push(AppWidget {
x: wx, y: wy, w: ww, h: wh,
color: bg, hover_color: hover_bg,
@@ -400,6 +457,12 @@ impl SystemInterface {
};
let label_color = btn.label_color.unwrap_or([0.83, 0.83, 0.83, 1.0]);
+ let button_bounds = Some([
+ 0.0,
+ 0.0,
+ logical_sw,
+ viewport_bottom,
+ ]);
text_items.push(TextItem {
buffer: buf,
x: text_x, y: (base.y - scroll_offset_y) + (base.h - logical_lh) / 2.0,
@@ -408,7 +471,7 @@ impl SystemInterface {
(label_color[1] * 255.0) as u8,
(label_color[2] * 255.0) as u8,
),
- bounds: None,
+ bounds: button_bounds,
});
let mut btn_clone = btn.clone();
if let Some(base_mut) = btn_clone.base_mut() {