git.lucas.co / cce-files
file manager
git clone https://git.lucas.co/cce-files.git

commit326a3644dee57b9c34d2650194244ff6e9b93be0
parenta658be06a4
authorLucas Galante <[email protected]>
date2026-06-13 21:29
Refactor page layout and add preview/network page functionality

 src/main.rs          | 147 ++++++++++++++++++++++++++++++++++++++-------------
 src/pages/mod.rs     |  56 +++++---------------
 src/pages/network.rs |   9 ++++
 src/pages/preview.rs | 109 ++++++++++++++++++++++++++++----------
 4 files changed, 215 insertions(+), 106 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index a71bc7f..705da62 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -44,7 +44,7 @@ struct FilesystemApp {
     height: u32,
     scale_factor: f64,
     sender: calloop::channel::Sender<Message>,
-    page_buttons: Vec<pages::ContentButton>,
+    page_buttons: Vec<(clear_ui::widget::Button, Message)>,
     cursor_x: f32,
     cursor_y: f32,
     paginator: clear_ui::widget::Paginator,
@@ -66,10 +66,22 @@ pub enum Message {
 
 // ── Helpers ─────────────────────────────────────────────────────────
 
-fn make_text_buffer(fs: &mut FontSystem, text: &str, size: f32) -> Buffer {
-    let metrics = Metrics::new(size, size * 1.4);
+fn make_text_buffer(fs: &mut FontSystem, text: &str, size: f32, font: Option<&str>) -> Buffer {
+    let scale = clear_ui::scale::scale_factor();
+    let physical_size = size * scale;
+    let metrics = Metrics::new(physical_size, physical_size * 1.4);
     let mut buf = Buffer::new(fs, metrics);
-    buf.set_text(fs, text, Attrs::new(), glyphon::Shaping::Advanced);
+    let mut attrs = Attrs::new();
+    if let Some(f) = font {
+        let family = match f {
+            "monospace" => glyphon::Family::Name(clear_ui::layout::get_system_monospace_font()),
+            "sans-serif" => glyphon::Family::SansSerif,
+            "serif" => glyphon::Family::Serif,
+            _ => glyphon::Family::Name(f),
+        };
+        attrs = attrs.family(family);
+    }
+    buf.set_text(fs, text, attrs, glyphon::Shaping::Advanced);
     buf.shape_until_scroll(fs, true);
     buf
 }
@@ -95,12 +107,12 @@ impl FilesystemApp {
         let mut paginator_pc = pages::PageContent::new();
         clear_ui::layout::render_widget(&mut paginator_pc, &mut self.paginator, 0.0, 0.0, self.width as f32, self.height as f32, &mut self.ui_context);
 
-        let has_sidebar = true;
+        let has_sidebar = !self.select_mode;
         let sidebar_w = if has_sidebar { self.paginator.sidebar_w() } else { 0.0 };
         let browse_x = if has_sidebar { sidebar_w + 17.0 } else { 16.0 };
         let usable_w = self.width as f32 - sidebar_w - (if has_sidebar { 1.0 } else { 0.0 }) - 32.0;
-        let browse_w = usable_w * 0.55;
-        let preview_w = usable_w * 0.45;
+        let browse_w = (usable_w - 12.0) * 0.5;
+        let preview_w = (usable_w - 12.0) * 0.5;
         let preview_x = browse_x + browse_w + 12.0;
         let content_y = 16.0;
 
@@ -127,6 +139,24 @@ impl FilesystemApp {
         // 3. Draw Page content
         match self.current_page {
             Page::Browse => {
+                // Draw background plates for columns
+                let plate_bg = [0.08, 0.13, 0.09, 0.45];
+                let border_color = [0.15, 0.23, 0.17, 0.7];
+
+                // Left column plate (Browse)
+                pc.rect(plate_bg, browse_x, content_y, browse_w, content_h);
+                pc.rect(border_color, browse_x, content_y, browse_w, 1.0);
+                pc.rect(border_color, browse_x, content_y + content_h - 1.0, browse_w, 1.0);
+                pc.rect(border_color, browse_x, content_y, 1.0, content_h);
+                pc.rect(border_color, browse_x + browse_w - 1.0, content_y, 1.0, content_h);
+
+                // Right column plate (Preview)
+                pc.rect(plate_bg, preview_x, content_y, preview_w, content_h);
+                pc.rect(border_color, preview_x, content_y, preview_w, 1.0);
+                pc.rect(border_color, preview_x, content_y + content_h - 1.0, preview_w, 1.0);
+                pc.rect(border_color, preview_x, content_y, 1.0, content_h);
+                pc.rect(border_color, preview_x + preview_w - 1.0, content_y, 1.0, content_h);
+
                 let browse_pc = pages::browse::view(&mut self.browse, browse_x, content_y, browse_w, content_h, self.select_mode, &mut self.ui_context);
                 let preview_pc = pages::preview::view(&self.preview, preview_x, content_y, preview_w, content_h);
 
@@ -238,48 +268,55 @@ impl FilesystemApp {
         }
 
         // Add page buttons
-        for btn in &pc.buttons {
-            let hovering = self.cursor_x >= btn.x && self.cursor_x <= btn.x + btn.w
-                && self.cursor_y >= btn.y && self.cursor_y <= btn.y + btn.h;
-            let col = if hovering { btn.hover_bg } else { btn.bg };
+        for (btn, action) in &pc.buttons {
+            let base = btn.base().unwrap();
+            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 label = base.label.as_deref().unwrap_or("");
+            let label_size = 12.0;
+            let label_color = btn.label_color.unwrap_or([0.83, 0.83, 0.83, 1.0]);
+
+            let hovering = self.cursor_x >= base.x && self.cursor_x <= base.x + base.w
+                && self.cursor_y >= base.y && self.cursor_y <= base.y + base.h;
+            let col = if hovering { hover_bg } else { bg };
 
             widgets.push(AppWidget {
-                x: btn.x,
-                y: btn.y,
-                w: btn.w,
-                h: btn.h,
+                x: base.x,
+                y: base.y,
+                w: base.w,
+                h: base.h,
                 color: col,
-                hover_color: btn.hover_bg,
+                hover_color: hover_bg,
                 hovering,
-                action: Some(btn.action.clone()),
+                action: Some(action.clone()),
             });
 
             // Draw button text
             let text_x = if btn.left_align {
-                btn.x + 8.0
+                base.x + 8.0
             } else {
-                let text_w = btn.label.chars().count() as f32 * btn.label_size * 0.65;
-                btn.x + (btn.w - text_w) / 2.0
+                let text_w = label.chars().count() as f32 * label_size * 0.65;
+                base.x + (base.w - text_w) / 2.0
             };
-            let text_y = btn.y + (btn.h - btn.label_size * 1.4) / 2.0;
+            let text_y = base.y + (base.h - label_size * 1.4) / 2.0;
 
             text_items.push(TextItem {
-                buffer: make_text_buffer(&mut self.font_system, &btn.label, btn.label_size),
+                buffer: make_text_buffer(&mut self.font_system, label, label_size, None),
                 x: text_x,
                 y: text_y,
                 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,
+                    (label_color[0] * 255.0) as u8,
+                    (label_color[1] * 255.0) as u8,
+                    (label_color[2] * 255.0) as u8,
                 ),
                 bounds: None,
             });
         }
 
         // Add raw texts
-        for (text, size, x, y, col, _font, bounds) in &pc.texts {
+        for (text, size, x, y, col, font, bounds) in &pc.texts {
             text_items.push(TextItem {
-                buffer: make_text_buffer(&mut self.font_system, text, *size),
+                buffer: make_text_buffer(&mut self.font_system, text, *size, font.as_deref()),
                 x: *x,
                 y: *y,
                 color: glyphon::Color::rgb(
@@ -333,7 +370,11 @@ impl Application for FilesystemApp {
             save_mode,
             widgets: Vec::new(),
             text_items: Vec::new(),
-            font_system: FontSystem::new(),
+            font_system: {
+                let mut fs = FontSystem::new();
+                fs.db_mut().load_fonts_dir("/home/lsgalante/Dropbox/Fonts");
+                fs
+            },
             needs_rebuild: true,
             width: if select_mode { 900 } else { 1200 },
             height: if select_mode { 500 } else { 720 },
@@ -573,7 +614,7 @@ impl Application for FilesystemApp {
 
         let mut changed = false;
 
-        if self.paginator.cursor_moved(pos.x, pos.y, &mut self.ui_context) {
+        if !self.select_mode && self.paginator.cursor_moved(pos.x, pos.y, &mut self.ui_context) {
             changed = true;
         }
 
@@ -612,9 +653,10 @@ impl Application for FilesystemApp {
         }
 
         // Always check if buttons hover state changed
-        for btn in &self.page_buttons {
-            let _hovering = self.cursor_x >= btn.x && self.cursor_x <= btn.x + btn.w
-                && self.cursor_y >= btn.y && self.cursor_y <= btn.y + btn.h;
+        for (btn, _action) in &self.page_buttons {
+            let base = btn.base().unwrap();
+            let _hovering = self.cursor_x >= base.x && self.cursor_x <= base.x + base.w
+                && self.cursor_y >= base.y && self.cursor_y <= base.y + base.h;
             // Trigger redraw on pointer moves so hover transitions are smooth
             changed = true;
         }
@@ -632,7 +674,7 @@ impl Application for FilesystemApp {
         let mut changed = false;
 
         eprintln!("[DEBUG] MOUSE INPUT: {:?} {:?} pos=({}, {})", button, state, pos.x, pos.y);
-        let pag_match = self.paginator.mouse_input(button, state, pos.x, pos.y, &mut self.ui_context);
+        let pag_match = !self.select_mode && self.paginator.mouse_input(button, state, pos.x, pos.y, &mut self.ui_context);
         eprintln!("[DEBUG] Paginator matched: {}", pag_match);
         if pag_match {
             if self.paginator.take_click() {
@@ -866,11 +908,12 @@ impl Application for FilesystemApp {
         }
 
         if button == MouseButton::Left && state == ElementState::Released {
-            for btn in &self.page_buttons {
-                if pos.x >= btn.x && pos.x <= btn.x + btn.w && pos.y >= btn.y && pos.y <= btn.y + btn.h {
+            for (btn, action) in &self.page_buttons {
+                let base = btn.base().unwrap();
+                if pos.x >= base.x && pos.x <= base.x + base.w && pos.y >= base.y && pos.y <= base.y + base.h {
                     *needs_rebuild = true;
                     self.needs_rebuild = true;
-                    return Some(btn.action.clone());
+                    return Some(action.clone());
                 }
             }
         }
@@ -879,6 +922,38 @@ impl Application for FilesystemApp {
     }
 
     fn handle_mouse_wheel(&mut self, delta: &MouseScrollDelta, pos: LogicalPosition, needs_rebuild: &mut bool) {
+        if self.current_page == Page::Browse || self.current_page == Page::Network {
+            let has_sidebar = !self.select_mode;
+            let sidebar_w = if has_sidebar { self.paginator.sidebar_w() } else { 0.0 };
+            let browse_x = if has_sidebar { sidebar_w + 17.0 } else { 16.0 };
+            let usable_w = self.width as f32 - sidebar_w - (if has_sidebar { 1.0 } else { 0.0 }) - 32.0;
+            let browse_w = (usable_w - 12.0) * 0.5;
+            let preview_w = (usable_w - 12.0) * 0.5;
+            let preview_x = browse_x + browse_w + 12.0;
+            let content_y = 16.0;
+
+            let select_bar_h = 48.0;
+            let content_h = if self.select_mode {
+                self.height as f32 - 32.0 - select_bar_h
+            } else {
+                self.height as f32 - 32.0
+            };
+
+            let half_h = content_h * 0.5;
+            let px = preview_x + 12.0;
+            let py = content_y + 32.0;
+            let pw = preview_w - 24.0;
+            let ph = half_h - 40.0;
+
+            if pos.x as f32 >= px && pos.x as f32 <= px + pw && pos.y as f32 >= py && pos.y as f32 <= py + ph {
+                if self.preview.handle_mouse_wheel(delta, content_h) {
+                    *needs_rebuild = true;
+                    self.needs_rebuild = true;
+                }
+                return;
+            }
+        }
+
         if self.current_page == Page::Browse {
             if self.browse.list_box.mouse_wheel(delta, pos.x, pos.y, &mut self.ui_context) {
                 *needs_rebuild = true;
diff --git a/src/pages/mod.rs b/src/pages/mod.rs
index e686be7..b1c229f 100644
--- a/src/pages/mod.rs
+++ b/src/pages/mod.rs
@@ -36,25 +36,10 @@ impl Page {
     }
 }
 
-#[derive(Clone)]
-pub struct ContentButton {
-    pub x: f32,
-    pub y: f32,
-    pub w: f32,
-    pub h: f32,
-    pub bg: [f32; 4],
-    pub hover_bg: [f32; 4],
-    pub label: String,
-    pub label_size: f32,
-    pub label_color: [f32; 4],
-    pub action: crate::Message,
-    pub left_align: bool,
-}
-
 pub struct PageContent {
     pub rects: Vec<([f32; 4], f32, f32, f32, f32)>,
     pub texts: Vec<(String, f32, f32, f32, [f32; 4], Option<String>, Option<[f32; 4]>)>,
-    pub buttons: Vec<ContentButton>,
+    pub buttons: Vec<(clear_ui::widget::Button, crate::Message)>,
 }
 
 impl PageContent {
@@ -90,19 +75,12 @@ impl PageContent {
         label_color: [f32; 4],
         action: crate::Message,
     ) {
-        self.buttons.push(ContentButton {
-            x,
-            y,
-            w,
-            h,
-            bg,
-            hover_bg,
-            label: label.to_string(),
-            label_size: 12.0,
-            label_color,
-            action,
-            left_align: false,
-        });
+        let btn = clear_ui::widget::Button::new(x, y, w, h)
+            .with_label(label)
+            .with_bg(bg)
+            .with_hover_bg(hover_bg)
+            .with_label_color(label_color);
+        self.buttons.push((btn, action));
     }
 
     pub fn button_left(
@@ -117,19 +95,13 @@ impl PageContent {
         label_color: [f32; 4],
         action: crate::Message,
     ) {
-        self.buttons.push(ContentButton {
-            x,
-            y,
-            w,
-            h,
-            bg,
-            hover_bg,
-            label: label.to_string(),
-            label_size: 12.0,
-            label_color,
-            action,
-            left_align: true,
-        });
+        let btn = clear_ui::widget::Button::new(x, y, w, h)
+            .with_label(label)
+            .with_bg(bg)
+            .with_hover_bg(hover_bg)
+            .with_label_color(label_color)
+            .with_left_align(true);
+        self.buttons.push((btn, action));
     }
 }
 
diff --git a/src/pages/network.rs b/src/pages/network.rs
index aea26ab..faf8e5e 100644
--- a/src/pages/network.rs
+++ b/src/pages/network.rs
@@ -53,6 +53,9 @@ impl NetworkState {
                 position: (3.0, 0.0),
                 parameters: Vec::new(),
                 geom_visible: true,
+                node_type: String::new(),
+                inputs: 0,
+                outputs: 1,
             });
             Some(name)
         } else {
@@ -78,6 +81,9 @@ impl NetworkState {
             position: (3.0, 1.0),
             parameters: current_params,
             geom_visible: true,
+            node_type: String::new(),
+            inputs: 1,
+            outputs: 1,
         });
 
         // 3. Children nodes
@@ -95,6 +101,9 @@ impl NetworkState {
                 position: (col, row),
                 parameters: vec![("input".to_string(), current_node_name.clone(), "string".to_string())],
                 geom_visible: true,
+                node_type: String::new(),
+                inputs: 1,
+                outputs: 1,
             });
         }
 
diff --git a/src/pages/preview.rs b/src/pages/preview.rs
index 877cf3e..22f7940 100644
--- a/src/pages/preview.rs
+++ b/src/pages/preview.rs
@@ -3,6 +3,7 @@ use std::os::unix::fs::PermissionsExt;
 use std::path::PathBuf;
 
 use crate::pages::PageContent;
+use clear_ui::layout::SectionContext;
 
 // ── Data ────────────────────────────────────────────────────────────
 
@@ -18,6 +19,7 @@ pub struct PreviewState {
     pub file_type: String,
     pub target: String, // for symlinks
     pub content_preview: Option<String>,
+    pub scroll_line: usize,
 }
 
 #[derive(Debug, Clone)]
@@ -88,6 +90,44 @@ fn infer_file_type(name: &str, is_dir: bool) -> String {
     }
 }
 
+impl PreviewState {
+    pub fn handle_mouse_wheel(&mut self, delta: &clear_ui::widget::MouseScrollDelta, ch: f32) -> bool {
+        let content = match &self.content_preview {
+            Some(c) => c,
+            None => return false,
+        };
+        let total_lines = content.lines().count();
+        let half_h = ch * 0.5;
+        let mut max_visible_lines = 0;
+        let mut text_y = 44.0;
+        while text_y + 14.0 <= half_h - 16.0 {
+            max_visible_lines += 1;
+            text_y += 15.0;
+        }
+        if total_lines <= max_visible_lines {
+            if self.scroll_line != 0 {
+                self.scroll_line = 0;
+                return true;
+            }
+            return false;
+        }
+        let max_scroll = total_lines.saturating_sub(max_visible_lines);
+        let scroll_speed = 3.0;
+        let diff = match delta {
+            clear_ui::widget::MouseScrollDelta::LineDelta(_, y) => {
+                -y * scroll_speed
+            }
+            clear_ui::widget::MouseScrollDelta::PixelDelta(pos) => {
+                -pos.y as f32 / 15.0
+            }
+        };
+        let prev_scroll = self.scroll_line;
+        let new_scroll = (self.scroll_line as f32 + diff).round() as isize;
+        self.scroll_line = new_scroll.clamp(0, max_scroll as isize) as usize;
+        self.scroll_line != prev_scroll
+    }
+}
+
 // ── View ────────────────────────────────────────────────────────────
 
 pub fn view(state: &PreviewState, cx: f32, cy: f32, cw: f32, ch: f32) -> PageContent {
@@ -103,50 +143,39 @@ pub fn view(state: &PreviewState, cx: f32, cy: f32, cw: f32, ch: f32) -> PageCon
 
     let half_h = ch * 0.5;
 
-    // 1. Top pane: File Preview
-    pc.text("Preview", cx + 12.0, cy + 12.0, 14.0, label_fg);
+    // 1. Top pane: File Preview Section
+    let mut preview_sec = SectionContext::new(&mut pc, cx + 4.0, cy + 12.0, cw - 8.0, "Preview", false, false);
+    preview_sec.content_y = cy + half_h - 20.0;
+    preview_sec.finish(); // Releases borrow on pc
 
     let bg_color = [0.07, 0.11, 0.08, 0.5];
     pc.rect(bg_color, cx + 12.0, cy + 32.0, cw - 24.0, half_h - 40.0);
 
     if let Some(content) = &state.content_preview {
         let mut text_y = cy + 44.0;
-        for line in content.lines() {
+        for line in content.lines().skip(state.scroll_line) {
             if text_y + 14.0 > cy + half_h - 16.0 {
                 break;
             }
-            let line_truncated = if line.chars().count() > 45 {
-                let mut s: String = line.chars().take(42).collect();
+            let limit = (((cw - 40.0) / 6.8).floor() as usize).max(20);
+            let line_truncated = if line.chars().count() > limit {
+                let mut s: String = line.chars().take(limit - 3).collect();
                 s.push_str("...");
                 s
             } else {
                 line.to_string()
             };
-            pc.text(&line_truncated, cx + 20.0, text_y, 11.0, text_fg);
+            pc.text_with_font(&line_truncated, cx + 20.0, text_y, 11.0, text_fg, "monospace");
             text_y += 15.0;
         }
     } else {
         pc.text("No preview available", cx + 20.0, cy + 44.0, 11.0, text_dim);
     }
 
-    // 2. Bottom pane: Details
+    // 2. Bottom pane: Details Section
     let bottom_y = cy + half_h + 12.0;
-
-    // Pane divider
-    pc.rect([0.15, 0.20, 0.16, 1.0], cx + 12.0, bottom_y - 6.0, cw - 24.0, 1.0);
-
     let icon = if state.is_dir { "📁" } else { "📄" };
 
-    // Details header
-    pc.text(icon, cx + 12.0, bottom_y + 6.0, 20.0, text_fg);
-    
-    let name_truncated = if state.name.len() > 30 {
-        format!("{}...", &state.name[..27])
-    } else {
-        state.name.clone()
-    };
-    pc.text(&name_truncated, cx + 42.0, bottom_y + 10.0, 16.0, text_fg);
-
     // Metadata details
     let details = [
         ("Path", &state.path_display),
@@ -156,7 +185,27 @@ pub fn view(state: &PreviewState, cx: f32, cy: f32, cw: f32, ch: f32) -> PageCon
         ("Modified", &state.modified),
     ];
 
-    let mut y = bottom_y + 36.0;
+    let details_content_start_y = bottom_y + 19.0;
+    let mut details_content_end_y = details_content_start_y + 36.0 + details.len() as f32 * 20.0;
+    if !state.target.is_empty() {
+        details_content_end_y += 24.0;
+    }
+
+    let mut details_sec = SectionContext::new(&mut pc, cx + 4.0, bottom_y, cw - 8.0, "Details", false, false);
+    details_sec.content_y = details_content_end_y;
+    details_sec.finish(); // Releases borrow on pc
+
+    let header_y = details_content_start_y + 6.0;
+    pc.text(icon, cx + 12.0, header_y, 20.0, text_fg);
+    
+    let name_truncated = if state.name.len() > 30 {
+        format!("{}...", &state.name[..27])
+    } else {
+        state.name.clone()
+    };
+    pc.text(&name_truncated, cx + 42.0, header_y + 4.0, 16.0, text_fg);
+
+    let mut y = details_content_start_y + 36.0;
     for (label, val) in &details {
         pc.text(label, cx + 12.0, y, 12.0, label_fg);
         
@@ -228,7 +277,7 @@ pub fn update(state: &mut PreviewState, msg: PreviewMessage) {
             let content_preview = if is_dir {
                 if let Ok(entries) = fs::read_dir(&path) {
                     let mut names = Vec::new();
-                    for entry in entries.flatten().take(15) {
+                    for entry in entries.flatten().take(100) {
                         let name = entry.file_name().to_string_lossy().to_string();
                         let is_sub_dir = entry.file_type().map(|t| t.is_dir()).unwrap_or(false);
                         let icon = if is_sub_dir { "📁" } else { "📄" };
@@ -244,13 +293,16 @@ pub fn update(state: &mut PreviewState, msg: PreviewMessage) {
                 }
             } else if let Ok(mut file) = fs::File::open(&path) {
                 use std::io::Read;
-                let mut buf = vec![0u8; 1024];
+                let mut buf = vec![0u8; 65536];
                 if let Ok(n) = file.read(&mut buf) {
                     buf.truncate(n);
-                    if let Ok(utf8_str) = String::from_utf8(buf) {
-                        let lines: Vec<&str> = utf8_str.lines().take(15).collect();
-                        let preview_text = lines.join("\n");
-                        Some(preview_text)
+                    let is_text = match std::str::from_utf8(&buf) {
+                        Ok(_) => true,
+                        Err(err) => err.error_len().is_none() && err.valid_up_to() > 0,
+                    };
+                    if is_text {
+                        let utf8_str = String::from_utf8_lossy(&buf).into_owned();
+                        Some(utf8_str)
                     } else {
                         Some("[Binary file content]".to_string())
                     }
@@ -272,6 +324,7 @@ pub fn update(state: &mut PreviewState, msg: PreviewMessage) {
                 file_type,
                 target,
                 content_preview,
+                scroll_line: 0,
             };
         }
     }