git.lucas.co / cce-fonts
font browser

commit5195ab81a8746db6de3fce13d83e5d27c97a786d
parent615183df4d
authorLucas Galante <[email protected]>
date2026-06-28 06:49
Convert middle panel to ScrollBox and implement precise boundary clipping

 src/main.rs | 229 +++++++++++++++++++++++++++++++++++++++---------------------
 1 file changed, 149 insertions(+), 80 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index 05794b8..8211941 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -5,7 +5,7 @@ use glyphon::{FontSystem, Buffer, Metrics, Attrs};
 use cce_ui::engine::{Application, EngineState, LogicalPosition, LogicalSize, WindowSettings};
 use cce_ui::widget::{
     MouseButton, ElementState, MouseScrollDelta, KeyEvent, TextItem, Element,
-    TextBox, Button, TextLabel, Key, NamedKey, ScrollingList, Dropdown, Slider,
+    TextBox, Button, TextLabel, Key, NamedKey, ScrollingList, ScrollBox, Dropdown, Slider,
     Backplate, Plate
 };
 use cce_ui::widget::focus::link_parent_child;
@@ -78,8 +78,7 @@ struct TypefaceApp {
     // Containers
     root_window: Backplate,
     left_panel: Plate,
-    mid_panel: Plate,
-    right_panel: Plate,
+    mid_panel: ScrollBox,
     bottom_bar: Plate,
     ui_context: cce_ui::context::UiContext,
 }
@@ -134,13 +133,11 @@ impl TypefaceApp {
         self.root_window.clear_children(ctx);
         self.left_panel.clear_children(ctx);
         self.mid_panel.clear_children(ctx);
-        self.right_panel.clear_children(ctx);
         self.bottom_bar.clear_children(ctx);
 
         // 1. Link active page-level containers to root
         link_parent_child(&mut self.root_window, &mut self.left_panel, ctx);
         link_parent_child(&mut self.root_window, &mut self.mid_panel, ctx);
-        link_parent_child(&mut self.root_window, &mut self.right_panel, ctx);
         
         if self.select_mode {
             link_parent_child(&mut self.root_window, &mut self.bottom_bar, ctx);
@@ -159,15 +156,19 @@ impl TypefaceApp {
 
         // Middle Panel (Preview)
         if self.selected_family.is_some() {
-            link_parent_child(&mut self.mid_panel, &mut self.style_dropdown, ctx);
-            link_parent_child(&mut self.mid_panel, &mut self.size_slider, ctx);
-            link_parent_child(&mut self.mid_panel, &mut self.preview_box, ctx);
-        }
-
-        // Right Panel (Details)
-        if self.selected_family.is_some() {
-            link_parent_child(&mut self.right_panel, &mut self.btn_open_folder, ctx);
-            link_parent_child(&mut self.right_panel, &mut self.btn_remove_font, ctx);
+            if self.btn_open_folder.rect().0 > -9000.0 {
+                link_parent_child(&mut self.mid_panel, &mut self.btn_open_folder, ctx);
+                link_parent_child(&mut self.mid_panel, &mut self.btn_remove_font, ctx);
+            }
+            if self.style_dropdown.rect().0 > -9000.0 {
+                link_parent_child(&mut self.mid_panel, &mut self.style_dropdown, ctx);
+            }
+            if self.size_slider.rect().0 > -9000.0 {
+                link_parent_child(&mut self.mid_panel, &mut self.size_slider, ctx);
+            }
+            if self.preview_box.rect().0 > -9000.0 {
+                link_parent_child(&mut self.mid_panel, &mut self.preview_box, ctx);
+            }
         }
 
         // Bottom bar
@@ -312,8 +313,30 @@ impl TypefaceApp {
 
         // Page Content
         labels.extend(self.left_panel.text_labels_with_bounds(&self.ui_context));
-        labels.extend(self.mid_panel.text_labels_with_bounds(&self.ui_context));
-        labels.extend(self.right_panel.text_labels_with_bounds(&self.ui_context));
+
+        // Clamped middle panel labels
+        let mid_panel_x = self.mid_panel.base.x;
+        let mid_panel_w = self.mid_panel.base.w;
+        let mid_panel_h = self.mid_panel.base.h;
+        let mid_viewport = [mid_panel_x, 10.0, mid_panel_x + mid_panel_w, 10.0 + mid_panel_h];
+        let mid_labels = self.mid_panel.text_labels_with_bounds(&self.ui_context);
+        for (label, bounds) in mid_labels {
+            let clamped_bounds = match bounds {
+                Some(b) => {
+                    let x0 = b[0].max(mid_viewport[0]);
+                    let y0 = b[1].max(mid_viewport[1]);
+                    let x1 = b[2].min(mid_viewport[2]);
+                    let y1 = b[3].min(mid_viewport[3]);
+                    if x0 < x1 && y0 < y1 {
+                        Some([x0, y0, x1, y1])
+                    } else {
+                        continue; // Completely clipped
+                    }
+                }
+                None => Some(mid_viewport),
+            };
+            labels.push((label, clamped_bounds));
+        }
 
         if self.select_mode {
             labels.extend(self.bottom_bar.text_labels_with_bounds(&self.ui_context));
@@ -329,13 +352,16 @@ impl TypefaceApp {
                     (color[1] * 255.0).clamp(0.0, 255.0) as u8,
                     (color[2] * 255.0).clamp(0.0, 255.0) as u8,
                 ];
+                let pop_x = tx;
+                let pop_y = ty;
+                let popover_bounds = Some(mid_viewport);
                 labels.push((TextLabel {
                     text: content,
-                    x: tx,
-                    y: ty,
+                    x: pop_x,
+                    y: pop_y,
                     font_size: size,
                     color: color_u8,
-                }, None));
+                }, popover_bounds));
             }
         }
 
@@ -368,19 +394,27 @@ impl TypefaceApp {
                 weight_val,
             );
             
-            let mid_panel_w = self.right_panel.base.base.x - 12.0 - self.mid_panel.base.base.x;
-            let preview_box_x = self.mid_panel.base.base.x + 10.0;
+            let preview_box_x = mid_panel_x + 10.0;
             let preview_box_w = mid_panel_w - 20.0;
-            let alphabet_box_y = if self.select_mode { 320.0 } else { 380.0 };
+            
+            let alphabet_virtual_y = if self.select_mode { 320.0 } else { 380.0 };
             let alphabet_box_h = if self.select_mode { 102.0 } else { 120.0 };
-
-            self.text_items.push(TextItem {
-                buffer: preview_text_buffer_clamped(alph_buf, font_system, mid_panel_w - 40.0),
-                x: self.mid_panel.base.base.x + 20.0,
-                y: alphabet_box_y + 12.0,
-                color: glyphon::Color::rgb(0x88, 0x88, 0x99),
-                bounds: Some([preview_box_x, alphabet_box_y, preview_box_x + preview_box_w, alphabet_box_y + alphabet_box_h]),
-            });
+            let scroll_y = self.mid_panel.scroll_y;
+            let alphabet_draw_y = 10.0 + alphabet_virtual_y - scroll_y;
+
+            let viewport_top = 10.0;
+            let viewport_bottom = 10.0 + mid_panel_h;
+            if alphabet_draw_y + alphabet_box_h >= viewport_top && alphabet_draw_y <= viewport_bottom {
+                let bounds_y_start = alphabet_draw_y.max(viewport_top);
+                let bounds_y_end = (alphabet_draw_y + alphabet_box_h).min(viewport_bottom);
+                self.text_items.push(TextItem {
+                    buffer: preview_text_buffer_clamped(alph_buf, font_system, mid_panel_w - 40.0),
+                    x: mid_panel_x + 20.0,
+                    y: alphabet_draw_y + 12.0,
+                    color: glyphon::Color::rgb(0x88, 0x88, 0x99),
+                    bounds: Some([preview_box_x, bounds_y_start, preview_box_x + preview_box_w, bounds_y_end]),
+                });
+            }
         }
 
         if self.select_mode {
@@ -508,8 +542,7 @@ impl Application for TypefaceApp {
                     .with_radius(win_radius)
             },
             left_panel: Plate::new(0.0, 0.0, 0.0, 0.0).with_blur(false).with_draggable(false),
-            mid_panel: Plate::new(0.0, 0.0, 0.0, 0.0).with_blur(false).with_draggable(false),
-            right_panel: Plate::new(0.0, 0.0, 0.0, 0.0).with_blur(false).with_draggable(false),
+            mid_panel: ScrollBox::new(),
             bottom_bar: Plate::new(0.0, 0.0, 0.0, 0.0).with_blur(false).with_draggable(false),
             ui_context: cce_ui::context::UiContext::new(),
         };
@@ -680,10 +713,8 @@ impl Application for TypefaceApp {
 
         let left_panel_x = 10.0;
         let left_panel_w = 270.0;
-        let right_panel_w = 286.0;
-        let right_panel_x = w_f32 - 10.0 - right_panel_w;
         let mid_panel_x = left_panel_x + left_panel_w + 12.0;
-        let mid_panel_w = right_panel_x - 12.0 - mid_panel_x;
+        let mid_panel_w = w_f32 - 10.0 - mid_panel_x;
 
         let select_bar_h = 48.0;
         let content_h = if self.select_mode {
@@ -701,15 +732,15 @@ impl Application for TypefaceApp {
             // Position panel Plates
             self.left_panel.set_rect(left_panel_x, 10.0, left_panel_w, content_h);
             self.mid_panel.set_rect(mid_panel_x, 10.0, mid_panel_w, content_h);
-            self.right_panel.set_rect(right_panel_x, 10.0, right_panel_w, content_h);
+
+            let mid_content_h = if self.select_mode { 440.0 } else { 520.0 };
+            self.mid_panel.update_bounds(mid_content_h, 10.0, content_h);
 
             let bar_y = h_f32 - select_bar_h - 10.0;
             self.bottom_bar.set_rect(left_panel_x, bar_y, w_f32 - 20.0, select_bar_h);
 
             // Configure Plate visibility
             self.left_panel.visible = true;
-            self.mid_panel.visible = true;
-            self.right_panel.visible = true;
             self.bottom_bar.visible = self.select_mode;
 
             // Search box
@@ -732,18 +763,48 @@ impl Application for TypefaceApp {
                 }
             }
 
-            // Preview panel widgets
+            // Preview panel widgets layout
             let style_dropdown_h = cce_ui::layout::dropdown_height() + cce_ui::widget::label_offset(&self.style_dropdown);
-            self.style_dropdown.set_rect(mid_panel_x + 10.0, 65.0, mid_panel_w - 20.0, style_dropdown_h);
-
             let size_slider_h = cce_ui::layout::slider_height() + cce_ui::widget::label_offset(&self.size_slider);
-            self.size_slider.set_rect(mid_panel_x + 10.0, 120.0, mid_panel_w - 20.0, size_slider_h);
             let preview_box_h = if self.select_mode { 120.0 } else { 180.0 };
-            self.preview_box.set_rect(mid_panel_x + 10.0, 190.0, mid_panel_w - 20.0, preview_box_h);
 
-            // Details panel buttons
-            self.btn_open_folder.set_rect(right_panel_x + 10.0, 200.0, 110.0, 28.0);
-            self.btn_remove_font.set_rect(right_panel_x + 130.0, 200.0, 110.0, 28.0);
+            let scroll_y = self.mid_panel.scroll_y;
+            let viewport_top = 10.0;
+            let viewport_bottom = 10.0 + content_h;
+
+            // Open Folder & Remove Font buttons (virtual_y = 20.0, h = 28.0)
+            let btn_draw_y = 10.0 + 20.0 - scroll_y;
+            if btn_draw_y + 28.0 >= viewport_top && btn_draw_y <= viewport_bottom {
+                self.btn_open_folder.set_rect(mid_panel_x + 10.0, btn_draw_y, 110.0, 28.0);
+                self.btn_remove_font.set_rect(mid_panel_x + 130.0, btn_draw_y, 110.0, 28.0);
+            } else {
+                self.btn_open_folder.set_rect(-9999.0, -9999.0, 0.0, 0.0);
+                self.btn_remove_font.set_rect(-9999.0, -9999.0, 0.0, 0.0);
+            }
+
+            // Style dropdown (virtual_y = 65.0, h = style_dropdown_h)
+            let dropdown_draw_y = 10.0 + 65.0 - scroll_y;
+            if dropdown_draw_y + style_dropdown_h >= viewport_top && dropdown_draw_y <= viewport_bottom {
+                self.style_dropdown.set_rect(mid_panel_x + 10.0, dropdown_draw_y, mid_panel_w - 20.0, style_dropdown_h);
+            } else {
+                self.style_dropdown.set_rect(-9999.0, -9999.0, 0.0, 0.0);
+            }
+
+            // Size slider (virtual_y = 120.0, h = size_slider_h)
+            let slider_draw_y = 10.0 + 120.0 - scroll_y;
+            if slider_draw_y + size_slider_h >= viewport_top && slider_draw_y <= viewport_bottom {
+                self.size_slider.set_rect(mid_panel_x + 10.0, slider_draw_y, mid_panel_w - 20.0, size_slider_h);
+            } else {
+                self.size_slider.set_rect(-9999.0, -9999.0, 0.0, 0.0);
+            }
+
+            // Preview box (virtual_y = 190.0, h = preview_box_h)
+            let preview_draw_y = 10.0 + 190.0 - scroll_y;
+            if preview_draw_y + preview_box_h >= viewport_top && preview_draw_y <= viewport_bottom {
+                self.preview_box.set_rect(mid_panel_x + 10.0, preview_draw_y, mid_panel_w - 20.0, preview_box_h);
+            } else {
+                self.preview_box.set_rect(-9999.0, -9999.0, 0.0, 0.0);
+            }
 
             if self.select_mode {
                 self.select_cancel_btn.set_rect(w_f32 - 190.0, bar_y + 10.0, 80.0, 28.0);
@@ -785,45 +846,53 @@ impl Application for TypefaceApp {
         quads.push((left_panel_x, 10.0, 1.0, content_h, border_col));
         quads.push((left_panel_x + left_panel_w, 10.0, 1.0, content_h, border_col));
 
-        // Middle Panel Borders
-        quads.push((mid_panel_x, 10.0, mid_panel_w, 1.0, border_col));
-        quads.push((mid_panel_x, 10.0 + content_h, mid_panel_w, 1.0, border_col));
-        quads.push((mid_panel_x, 10.0, 1.0, content_h, border_col));
-        quads.push((mid_panel_x + mid_panel_w, 10.0, 1.0, content_h, border_col));
+        // The ScrollBox now automatically draws its own borders and scrollbar.
 
         if self.selected_family.is_some() {
-            // Alphabet preview box background & borders
-            let preview_box_x = mid_panel_x + 10.0;
-            let preview_box_w = mid_panel_w - 20.0;
-            let alphabet_box_y = if self.select_mode { 320.0 } else { 380.0 };
+            let mid_panel_h = self.mid_panel.base.h;
+            let alphabet_virtual_y = if self.select_mode { 320.0 } else { 380.0 };
             let alphabet_box_h = if self.select_mode { 102.0 } else { 120.0 };
-
-            let alphabet_bg = [
-                base_low[0] * 0.9,
-                base_low[1] * 0.9,
-                base_low[2] * 0.9,
-                1.0,
-            ];
-            let alphabet_border = [
-                border_col[0] * 0.85,
-                border_col[1] * 0.85,
-                border_col[2] * 0.85,
-                1.0,
-            ];
-
-            quads.push((preview_box_x, alphabet_box_y, preview_box_w, alphabet_box_h, alphabet_bg)); // alphabet box bg
-            quads.push((preview_box_x, alphabet_box_y, preview_box_w, 1.0, alphabet_border));
-            quads.push((preview_box_x, alphabet_box_y + alphabet_box_h, preview_box_w, 1.0, alphabet_border));
-            quads.push((preview_box_x, alphabet_box_y, 1.0, alphabet_box_h, alphabet_border));
-            quads.push((preview_box_x + preview_box_w, alphabet_box_y, 1.0, alphabet_box_h, alphabet_border));
+            let scroll_y = self.mid_panel.scroll_y;
+            let alphabet_draw_y = 10.0 + alphabet_virtual_y - scroll_y;
+
+            let viewport_top = 10.0;
+            let viewport_bottom = 10.0 + mid_panel_h;
+
+            if alphabet_draw_y + alphabet_box_h >= viewport_top && alphabet_draw_y <= viewport_bottom {
+                let draw_y_start = alphabet_draw_y.max(viewport_top);
+                let draw_y_end = (alphabet_draw_y + alphabet_box_h).min(viewport_bottom);
+                let draw_h = draw_y_end - draw_y_start;
+
+                if draw_h > 0.0 {
+                    let preview_box_x = mid_panel_x + 10.0;
+                    let preview_box_w = mid_panel_w - 20.0;
+                    let alphabet_bg = [
+                        base_low[0] * 0.9,
+                        base_low[1] * 0.9,
+                        base_low[2] * 0.9,
+                        1.0,
+                    ];
+                    let alphabet_border = [
+                        border_col[0] * 0.85,
+                        border_col[1] * 0.85,
+                        border_col[2] * 0.85,
+                        1.0,
+                    ];
+
+                    quads.push((preview_box_x, draw_y_start, preview_box_w, draw_h, alphabet_bg)); // alphabet box bg
+                    
+                    if alphabet_draw_y >= viewport_top {
+                        quads.push((preview_box_x, alphabet_draw_y, preview_box_w, 1.0, alphabet_border)); // Top border
+                    }
+                    if alphabet_draw_y + alphabet_box_h <= viewport_bottom {
+                        quads.push((preview_box_x, alphabet_draw_y + alphabet_box_h, preview_box_w, 1.0, alphabet_border)); // Bottom border
+                    }
+                    quads.push((preview_box_x, draw_y_start, 1.0, draw_h, alphabet_border)); // Left border
+                    quads.push((preview_box_x + preview_box_w, draw_y_start, 1.0, draw_h, alphabet_border)); // Right border
+                }
+            }
         }
 
-        // Right Panel Borders
-        quads.push((right_panel_x, 10.0, right_panel_w, 1.0, border_col));
-        quads.push((right_panel_x, 10.0 + content_h, right_panel_w, 1.0, border_col));
-        quads.push((right_panel_x, 10.0, 1.0, content_h, border_col));
-        quads.push((right_panel_x + right_panel_w, 10.0, 1.0, content_h, border_col));
-
         if self.select_mode {
             let bar_y = h_f32 - select_bar_h - 10.0;
             // Draw bottom bar separator and side borders