git.lucas.co / cce-cloud
cloud storage client
git clone https://git.lucas.co/cce-cloud.git

commitfe26c25f24c65f0abbfddfb0118dd940d7c18db5
parent71af078dad
authorLucas Galante <[email protected]>
date2026-05-25 00:27
Integrate clear-ui ScrollBox into clear-cloud for trackpad and mouse scrolling

 src/main.rs | 144 +++++++++++++++++++++++++++++++++++++-----------------------
 1 file changed, 88 insertions(+), 56 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index cc93656..ad57dcc 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -310,7 +310,7 @@ pub struct FuzzelWidget {
     all_items: Vec<String>,
     filtered_items: Vec<String>,
     selected: usize,
-    scroll_offset: usize,
+    pub scroll_box: clear_ui::widget::ScrollBox,
 }
 
 impl FuzzelWidget {
@@ -325,7 +325,7 @@ impl FuzzelWidget {
             all_items: Vec::new(),
             filtered_items: Vec::new(),
             selected: 0,
-            scroll_offset: 0,
+            scroll_box: clear_ui::widget::ScrollBox::new(),
         }
     }
 
@@ -340,15 +340,39 @@ impl FuzzelWidget {
             self.selected = self.filtered_items.len().saturating_sub(1);
         }
         self.update_scroll();
+        self.snap_to_selected();
     }
 
     pub fn update_scroll(&mut self) {
-        let visible_items = 29;
-        if self.selected >= self.scroll_offset + visible_items {
-            self.scroll_offset = self.selected - visible_items + 1;
-        } else if self.selected < self.scroll_offset {
-            self.scroll_offset = self.selected;
+        let item_h = 25.0;
+        let pad = 15.0;
+        let search_h = 35.0;
+        let viewport_y = self.y + pad + search_h + 10.0;
+        let viewport_h = self.h - (pad + search_h + 10.0) - pad;
+        let content_h = self.filtered_items.len() as f32 * item_h;
+        self.scroll_box.update_bounds(content_h, viewport_y, viewport_h);
+    }
+
+    pub fn snap_to_selected(&mut self) {
+        let item_h = 25.0;
+        let pad = 15.0;
+        let search_h = 35.0;
+        let viewport_h = self.h - (pad + search_h + 10.0) - pad;
+        let content_h = self.filtered_items.len() as f32 * item_h;
+
+        if self.filtered_items.is_empty() {
+            return;
+        }
+
+        let virtual_selected_y = self.selected as f32 * item_h;
+        if virtual_selected_y + item_h > self.scroll_box.scroll_y + viewport_h {
+            self.scroll_box.scroll_y = virtual_selected_y + item_h - viewport_h;
+        } else if virtual_selected_y < self.scroll_box.scroll_y {
+            self.scroll_box.scroll_y = virtual_selected_y;
         }
+
+        let max_scroll = (content_h - viewport_h).max(0.0);
+        self.scroll_box.scroll_y = self.scroll_box.scroll_y.clamp(0.0, max_scroll);
     }
 }
 
@@ -362,6 +386,13 @@ impl Widget for FuzzelWidget {
         self.y = y;
         self.w = w;
         self.h = h;
+
+        let pad = 15.0;
+        let search_h = 35.0;
+        let viewport_y = y + pad + search_h + 10.0;
+        let viewport_h = h - (pad + search_h + 10.0) - pad;
+        self.scroll_box.set_rect(x + pad, viewport_y, w - pad * 2.0, viewport_h);
+        self.update_scroll();
     }
 
     fn color(&self) -> [f32; 4] {
@@ -393,20 +424,20 @@ impl Widget for FuzzelWidget {
         quads.push((bx, by, 1.0, bh, border_color));
         quads.push((bx + bw - 1.0, by, 1.0, bh, border_color));
 
-        // Items list background highlight
-        let list_y = self.y + pad + search_h + 10.0;
-        let row_h = 25.0;
-        let visible_items = 29;
+        // ScrollBox quads
+        quads.extend(self.scroll_box.extra_quads());
 
-        if !self.filtered_items.is_empty() && self.selected >= self.scroll_offset {
-            let relative_selected = self.selected - self.scroll_offset;
-            if relative_selected < visible_items {
-                let y = list_y + relative_selected as f32 * row_h;
+        // Selected Item Highlight
+        let item_h = 25.0;
+        if !self.filtered_items.is_empty() {
+            let virtual_selected_y = self.selected as f32 * item_h;
+            if let Some(draw_y) = self.scroll_box.get_item_draw_y(virtual_selected_y, item_h) {
+                let scrollbar_w = if self.scroll_box.content_h > self.scroll_box.viewport_h { 10.0 } else { 0.0 };
                 quads.push((
-                    self.x + pad,
-                    y,
-                    self.w - pad * 2.0,
-                    row_h - 2.0,
+                    self.x + pad + 2.0,
+                    draw_y,
+                    self.w - pad * 2.0 - 4.0 - scrollbar_w,
+                    item_h - 2.0,
                     [0.20, 0.35, 0.65, 0.9],
                 ));
             }
@@ -439,32 +470,28 @@ impl Widget for FuzzelWidget {
             color: query_color,
         });
 
-        let list_y = self.y + pad + search_h + 10.0;
-        let row_h = 25.0;
-        let visible_items = 29;
-
-        let start = self.scroll_offset;
-        let end = self.filtered_items.len().min(start + visible_items);
-
-        for (i, idx) in (start..end).enumerate() {
-            let item_text = &self.filtered_items[idx];
-            let y = list_y + i as f32 * row_h + 4.0;
-            let color = if idx == self.selected {
-                [0xff, 0xff, 0xff]
-            } else {
-                [0xbb, 0xbb, 0xc5]
-            };
-
-            labels.push(TextLabel {
-                text: item_text.clone(),
-                x: self.x + pad + 10.0,
-                y,
-                font_size: 13.0,
-                color,
-            });
+        let item_h = 25.0;
+        for (idx, item_text) in self.filtered_items.iter().enumerate() {
+            let virtual_y = idx as f32 * item_h;
+            if let Some(draw_y) = self.scroll_box.get_item_draw_y(virtual_y, item_h) {
+                let color = if idx == self.selected {
+                    [0xff, 0xff, 0xff]
+                } else {
+                    [0xbb, 0xbb, 0xc5]
+                };
+
+                labels.push(TextLabel {
+                    text: item_text.clone(),
+                    x: self.x + pad + 10.0,
+                    y: draw_y + 4.0,
+                    font_size: 13.0,
+                    color,
+                });
+            }
         }
 
         if self.filtered_items.is_empty() {
+            let list_y = self.y + pad + search_h + 10.0;
             labels.push(TextLabel {
                 text: "No matches found".to_string(),
                 x: self.x + pad + 10.0,
@@ -479,20 +506,13 @@ impl Widget for FuzzelWidget {
 
     fn mouse_input(&mut self, button: clear_ui::widget::MouseButton, state: clear_ui::widget::ElementState, px: f32, py: f32) -> bool {
         if button == clear_ui::widget::MouseButton::Left && state == clear_ui::widget::ElementState::Pressed {
-            let pad = 15.0;
-            let search_h = 35.0;
-            let list_y = self.y + pad + search_h + 10.0;
-            let row_h = 25.0;
-            let visible_items = 29;
-
-            if px >= self.x + pad && px <= self.x + self.w - pad {
-                if py >= list_y && py < list_y + visible_items as f32 * row_h {
-                    let clicked_row = ((py - list_y) / row_h).floor() as usize;
-                    let target_idx = self.scroll_offset + clicked_row;
-                    if target_idx < self.filtered_items.len() {
-                        self.selected = target_idx;
-                        return true;
-                    }
+            let item_h = 25.0;
+            if self.scroll_box.hit_test(px, py) {
+                let click_virtual_y = py - self.scroll_box.viewport_y + self.scroll_box.scroll_y;
+                let clicked_idx = (click_virtual_y / item_h).floor() as usize;
+                if clicked_idx < self.filtered_items.len() {
+                    self.selected = clicked_idx;
+                    return true;
                 }
             }
         }
@@ -1146,6 +1166,16 @@ impl PointerHandler for AppState {
                             }
                         }
                     }
+                    PointerEventKind::Axis { horizontal, vertical, .. } => {
+                        let h_scroll = horizontal.absolute as f32;
+                        let v_scroll = vertical.absolute as f32;
+                        let delta = clear_ui::widget::MouseScrollDelta::LineDelta(-h_scroll / 10.0, -v_scroll / 10.0);
+                        if st.fuzzel.scroll_box.mouse_wheel(&delta, st.cursor_x, st.cursor_y) {
+                            st.fuzzel.update_scroll();
+                            st.upload_vertices();
+                            self.redraw = true;
+                        }
+                    }
                     _ => {}
                 }
             }
@@ -1314,6 +1344,7 @@ impl AppState {
                     if !st.fuzzel.filtered_items.is_empty() {
                         st.fuzzel.selected = (st.fuzzel.selected + 1).min(st.fuzzel.filtered_items.len() - 1);
                         st.fuzzel.update_scroll();
+                        st.fuzzel.snap_to_selected();
                         st.upload_vertices();
                         self.redraw = true;
                     }
@@ -1322,6 +1353,7 @@ impl AppState {
                     if st.fuzzel.selected > 0 {
                         st.fuzzel.selected -= 1;
                         st.fuzzel.update_scroll();
+                        st.fuzzel.snap_to_selected();
                         st.upload_vertices();
                         self.redraw = true;
                     }