file manager
git clone https://git.lucas.co/cce-files.git
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,
};
}
}