Rust · 27598 bytes Raw Blame History
1 //! List view component for displaying directory contents.
2
3 use crate::core::{EntryType, FileEntry, SortDirection, SortOrder};
4 use crate::ui::tab::RenameState;
5 use gartk_core::{Color, Modifiers, Point, Rect};
6 use gartk_render::{Renderer, TextStyle};
7 use std::collections::HashSet;
8
9 /// Height of each row in the list view.
10 pub const ROW_HEIGHT: u32 = 28;
11
12 /// Height of the header row.
13 pub const HEADER_HEIGHT: u32 = 28;
14
15 /// Minimum column width.
16 const MIN_COLUMN_WIDTH: u32 = 60;
17
18 /// Column identifier.
19 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
20 pub enum Column {
21 Name,
22 Size,
23 Modified,
24 }
25
26 impl Column {
27 /// Convert to SortOrder.
28 fn to_sort_order(self) -> SortOrder {
29 match self {
30 Column::Name => SortOrder::Name,
31 Column::Size => SortOrder::Size,
32 Column::Modified => SortOrder::Modified,
33 }
34 }
35 }
36
37 /// List view for displaying file entries.
38 pub struct ListView {
39 /// Entries to display.
40 entries: Vec<FileEntry>,
41 /// Currently focused index (for keyboard nav).
42 focused: usize,
43 /// Selected indices (for multi-select).
44 selected: HashSet<usize>,
45 /// Anchor index for shift-selection.
46 selection_anchor: Option<usize>,
47 /// Scroll offset (first visible row).
48 scroll_offset: usize,
49 /// View bounds.
50 bounds: Rect,
51 /// Show hidden files.
52 show_hidden: bool,
53 /// Current sort order.
54 sort_order: SortOrder,
55 /// Current sort direction.
56 sort_direction: SortDirection,
57 /// Column widths (name, size, modified).
58 column_widths: [u32; 3],
59 /// Column being resized (if any).
60 resizing_column: Option<usize>,
61 /// Hovered header column (if any).
62 hovered_header: Option<Column>,
63 }
64
65 impl ListView {
66 /// Create a new list view.
67 pub fn new(bounds: Rect) -> Self {
68 // Initial column widths: 50% for name, 100px size, rest for date
69 let name_width = (bounds.width as f64 * 0.5) as u32;
70 let size_width = 100;
71 let date_width = bounds.width.saturating_sub(name_width + size_width + 32);
72
73 Self {
74 entries: Vec::new(),
75 focused: 0,
76 selected: HashSet::new(),
77 selection_anchor: None,
78 scroll_offset: 0,
79 bounds,
80 show_hidden: false,
81 sort_order: SortOrder::Name,
82 sort_direction: SortDirection::Ascending,
83 column_widths: [name_width, size_width, date_width],
84 resizing_column: None,
85 hovered_header: None,
86 }
87 }
88
89 /// Set the entries to display.
90 pub fn set_entries(&mut self, entries: Vec<FileEntry>) {
91 self.entries = entries;
92 self.focused = 0;
93 self.selected.clear();
94 self.selected.insert(0);
95 self.selection_anchor = Some(0);
96 self.scroll_offset = 0;
97 }
98
99 /// Get visible entries (respecting hidden filter).
100 pub fn visible_entries(&self) -> Vec<&FileEntry> {
101 self.entries
102 .iter()
103 .filter(|e| self.show_hidden || !e.hidden)
104 .collect()
105 }
106
107 /// Get the currently focused entry.
108 pub fn selected_entry(&self) -> Option<&FileEntry> {
109 let visible = self.visible_entries();
110 visible.get(self.focused).copied()
111 }
112
113 /// Get the focused index.
114 pub fn focused_index(&self) -> usize {
115 self.focused
116 }
117
118 /// Set the focused index and select it.
119 pub fn set_focused(&mut self, index: usize) {
120 if index < self.entries.len() {
121 self.focused = index;
122 self.selected.clear();
123 self.selected.insert(index);
124 }
125 }
126
127 /// Get all selected entries.
128 pub fn selected_entries(&self) -> Vec<&FileEntry> {
129 let visible = self.visible_entries();
130 self.selected
131 .iter()
132 .filter_map(|&i| visible.get(i).copied())
133 .collect()
134 }
135
136 /// Get selection count.
137 pub fn selection_count(&self) -> usize {
138 self.selected.len()
139 }
140
141 /// Check if an index is selected.
142 pub fn is_selected(&self, index: usize) -> bool {
143 self.selected.contains(&index)
144 }
145
146 /// Get the number of visible rows that fit in the view.
147 fn visible_rows(&self) -> usize {
148 let content_height = self.bounds.height.saturating_sub(HEADER_HEIGHT);
149 (content_height / ROW_HEIGHT).max(1) as usize
150 }
151
152 /// Get current sort settings.
153 pub fn sort_settings(&self) -> (SortOrder, SortDirection) {
154 (self.sort_order, self.sort_direction)
155 }
156
157 /// Toggle hidden files visibility.
158 pub fn toggle_hidden(&mut self) {
159 self.show_hidden = !self.show_hidden;
160 let visible_count = self.visible_entries().len();
161 if self.focused >= visible_count && visible_count > 0 {
162 self.focused = visible_count - 1;
163 }
164 // Revalidate selection
165 self.selected.retain(|&i| i < visible_count);
166 if self.selected.is_empty() && visible_count > 0 {
167 self.selected.insert(self.focused);
168 }
169 }
170
171 /// Move selection up.
172 pub fn select_prev(&mut self) {
173 if self.focused > 0 {
174 self.focused -= 1;
175 self.selected.clear();
176 self.selected.insert(self.focused);
177 self.selection_anchor = Some(self.focused);
178 if self.focused < self.scroll_offset {
179 self.scroll_offset = self.focused;
180 }
181 }
182 }
183
184 /// Move selection down.
185 pub fn select_next(&mut self) {
186 let visible_count = self.visible_entries().len();
187 if self.focused + 1 < visible_count {
188 self.focused += 1;
189 self.selected.clear();
190 self.selected.insert(self.focused);
191 self.selection_anchor = Some(self.focused);
192 let visible_rows = self.visible_rows();
193 if self.focused >= self.scroll_offset + visible_rows {
194 self.scroll_offset = self.focused - visible_rows + 1;
195 }
196 }
197 }
198
199 /// Jump to first entry.
200 pub fn select_first(&mut self) {
201 self.focused = 0;
202 self.selected.clear();
203 self.selected.insert(0);
204 self.selection_anchor = Some(0);
205 self.scroll_offset = 0;
206 }
207
208 /// Jump to last entry.
209 pub fn select_last(&mut self) {
210 let visible_count = self.visible_entries().len();
211 if visible_count > 0 {
212 self.focused = visible_count - 1;
213 self.selected.clear();
214 self.selected.insert(self.focused);
215 self.selection_anchor = Some(self.focused);
216 let visible_rows = self.visible_rows();
217 if self.focused >= visible_rows {
218 self.scroll_offset = self.focused - visible_rows + 1;
219 }
220 }
221 }
222
223 /// Page up.
224 pub fn page_up(&mut self) {
225 let page_size = self.visible_rows();
226 if self.focused >= page_size {
227 self.focused -= page_size;
228 } else {
229 self.focused = 0;
230 }
231 self.selected.clear();
232 self.selected.insert(self.focused);
233 self.selection_anchor = Some(self.focused);
234 if self.focused < self.scroll_offset {
235 self.scroll_offset = self.focused;
236 }
237 }
238
239 /// Page down.
240 pub fn page_down(&mut self) {
241 let visible_count = self.visible_entries().len();
242 let page_size = self.visible_rows();
243 self.focused = (self.focused + page_size).min(visible_count.saturating_sub(1));
244 self.selected.clear();
245 self.selected.insert(self.focused);
246 self.selection_anchor = Some(self.focused);
247 if self.focused >= self.scroll_offset + page_size {
248 self.scroll_offset = self.focused - page_size + 1;
249 }
250 }
251
252 /// Select all entries (Ctrl+A).
253 pub fn select_all(&mut self) {
254 let visible_count = self.visible_entries().len();
255 self.selected = (0..visible_count).collect();
256 }
257
258 /// Update bounds.
259 pub fn set_bounds(&mut self, bounds: Rect) {
260 self.bounds = bounds;
261 // Recalculate column widths proportionally
262 let total_width = bounds.width.saturating_sub(32);
263 let old_total: u32 = self.column_widths.iter().sum();
264 if old_total > 0 {
265 for width in &mut self.column_widths {
266 *width = (*width as f64 / old_total as f64 * total_width as f64) as u32;
267 }
268 }
269 }
270
271 /// Get header bounds.
272 fn header_bounds(&self) -> Rect {
273 Rect::new(self.bounds.x, self.bounds.y, self.bounds.width, HEADER_HEIGHT)
274 }
275
276 /// Get content bounds (below header).
277 fn content_bounds(&self) -> Rect {
278 Rect::new(
279 self.bounds.x,
280 self.bounds.y + HEADER_HEIGHT as i32,
281 self.bounds.width,
282 self.bounds.height.saturating_sub(HEADER_HEIGHT),
283 )
284 }
285
286 /// Get column header bounds for a specific column.
287 fn column_header_bounds(&self, col: Column) -> Rect {
288 let header = self.header_bounds();
289 match col {
290 Column::Name => Rect::new(header.x, header.y, self.column_widths[0] + 8, HEADER_HEIGHT),
291 Column::Size => Rect::new(
292 header.x + self.column_widths[0] as i32 + 16,
293 header.y,
294 self.column_widths[1],
295 HEADER_HEIGHT,
296 ),
297 Column::Modified => Rect::new(
298 header.x + self.column_widths[0] as i32 + self.column_widths[1] as i32 + 24,
299 header.y,
300 self.column_widths[2],
301 HEADER_HEIGHT,
302 ),
303 }
304 }
305
306 /// Get the X position of a column divider.
307 fn divider_x(&self, divider_index: usize) -> i32 {
308 match divider_index {
309 0 => self.bounds.x + self.column_widths[0] as i32 + 12,
310 1 => self.bounds.x + self.column_widths[0] as i32 + self.column_widths[1] as i32 + 20,
311 _ => 0,
312 }
313 }
314
315 /// Check if position is near a column divider. Returns divider index (0 or 1) if so.
316 pub fn divider_at(&self, pos: Point) -> Option<usize> {
317 let header = self.header_bounds();
318 if !header.contains_point(pos) {
319 return None;
320 }
321
322 for i in 0..2 {
323 let divider_x = self.divider_x(i);
324 if (pos.x - divider_x).abs() < 4 {
325 return Some(i);
326 }
327 }
328 None
329 }
330
331 /// Start resizing a column divider.
332 pub fn start_resize(&mut self, divider_index: usize) {
333 self.resizing_column = Some(divider_index);
334 }
335
336 /// Stop resizing.
337 pub fn stop_resize(&mut self) {
338 self.resizing_column = None;
339 }
340
341 /// Check if currently resizing.
342 pub fn is_resizing(&self) -> bool {
343 self.resizing_column.is_some()
344 }
345
346 /// Handle mouse move (for hover and resize).
347 pub fn on_mouse_move(&mut self, pos: Point) {
348 // Handle active resize
349 if let Some(divider_index) = self.resizing_column {
350 let new_x = pos.x;
351 match divider_index {
352 0 => {
353 // Resizing between Name and Size columns
354 let new_name_width = (new_x - self.bounds.x - 8).max(MIN_COLUMN_WIDTH as i32) as u32;
355 let total = self.column_widths[0] + self.column_widths[1];
356 let new_size_width = total.saturating_sub(new_name_width).max(MIN_COLUMN_WIDTH);
357 let adjusted_name = total.saturating_sub(new_size_width);
358 if adjusted_name >= MIN_COLUMN_WIDTH {
359 self.column_widths[0] = adjusted_name;
360 self.column_widths[1] = new_size_width;
361 }
362 }
363 1 => {
364 // Resizing between Size and Modified columns
365 let divider0_x = self.divider_x(0);
366 let new_size_width = (new_x - divider0_x - 8).max(MIN_COLUMN_WIDTH as i32) as u32;
367 let total = self.column_widths[1] + self.column_widths[2];
368 let new_date_width = total.saturating_sub(new_size_width).max(MIN_COLUMN_WIDTH);
369 let adjusted_size = total.saturating_sub(new_date_width);
370 if adjusted_size >= MIN_COLUMN_WIDTH {
371 self.column_widths[1] = adjusted_size;
372 self.column_widths[2] = new_date_width;
373 }
374 }
375 _ => {}
376 }
377 return;
378 }
379
380 if !self.bounds.contains_point(pos) {
381 self.hovered_header = None;
382 return;
383 }
384
385 let header = self.header_bounds();
386 if header.contains_point(pos) {
387 // Check for column resize zones
388 if self.divider_at(pos).is_some() {
389 self.hovered_header = None;
390 return;
391 }
392
393 // Check column headers for hover
394 for col in [Column::Name, Column::Size, Column::Modified] {
395 if self.column_header_bounds(col).contains_point(pos) {
396 self.hovered_header = Some(col);
397 return;
398 }
399 }
400 }
401
402 self.hovered_header = None;
403 }
404
405 /// Handle header click for sorting. Returns (new_order, new_direction) if sort changed.
406 /// Note: Call divider_at() first to check for resize initiation.
407 pub fn on_header_click(&mut self, pos: Point) -> Option<(SortOrder, SortDirection)> {
408 let header = self.header_bounds();
409 if !header.contains_point(pos) {
410 return None;
411 }
412
413 // Don't sort if clicking on a divider
414 if self.divider_at(pos).is_some() {
415 return None;
416 }
417
418 for col in [Column::Name, Column::Size, Column::Modified] {
419 if self.column_header_bounds(col).contains_point(pos) {
420 let new_order = col.to_sort_order();
421 if self.sort_order == new_order {
422 // Toggle direction
423 self.sort_direction = match self.sort_direction {
424 SortDirection::Ascending => SortDirection::Descending,
425 SortDirection::Descending => SortDirection::Ascending,
426 };
427 } else {
428 self.sort_order = new_order;
429 self.sort_direction = SortDirection::Ascending;
430 }
431 return Some((self.sort_order, self.sort_direction));
432 }
433 }
434
435 None
436 }
437
438 /// Get the entry at the given position (for drag detection).
439 pub fn entry_at_point(&self, pos: Point) -> Option<&FileEntry> {
440 let content = self.content_bounds();
441 if !content.contains_point(pos) {
442 return None;
443 }
444
445 let relative_y = pos.y - content.y;
446 let row_index = self.scroll_offset + (relative_y / ROW_HEIGHT as i32) as usize;
447
448 let visible = self.visible_entries();
449 visible.get(row_index).copied()
450 }
451
452 /// Handle row click. Returns index of clicked row if valid.
453 pub fn on_row_click(&mut self, pos: Point, modifiers: &Modifiers) -> Option<usize> {
454 let content = self.content_bounds();
455 if !content.contains_point(pos) {
456 return None;
457 }
458
459 let relative_y = pos.y - content.y;
460 let row_index = self.scroll_offset + (relative_y / ROW_HEIGHT as i32) as usize;
461
462 let visible_count = self.visible_entries().len();
463 if row_index >= visible_count {
464 return None;
465 }
466
467 if modifiers.ctrl {
468 // Ctrl+click: toggle selection
469 if self.selected.contains(&row_index) {
470 self.selected.remove(&row_index);
471 } else {
472 self.selected.insert(row_index);
473 }
474 self.focused = row_index;
475 self.selection_anchor = Some(row_index);
476 } else if modifiers.shift {
477 // Shift+click: range selection
478 if let Some(anchor) = self.selection_anchor {
479 let (start, end) = if anchor <= row_index {
480 (anchor, row_index)
481 } else {
482 (row_index, anchor)
483 };
484 self.selected = (start..=end).collect();
485 } else {
486 self.selected.clear();
487 self.selected.insert(row_index);
488 self.selection_anchor = Some(row_index);
489 }
490 self.focused = row_index;
491 } else {
492 // Plain click: single selection
493 self.selected.clear();
494 self.selected.insert(row_index);
495 self.focused = row_index;
496 self.selection_anchor = Some(row_index);
497 }
498
499 Some(row_index)
500 }
501
502 /// Clear hover state.
503 pub fn clear_hover(&mut self) {
504 self.hovered_header = None;
505 }
506
507 /// Render the list view.
508 pub fn render(&self, renderer: &Renderer, rename_state: Option<&RenameState>) -> anyhow::Result<()> {
509 let theme = renderer.theme();
510 let visible = self.visible_entries();
511 let visible_rows = self.visible_rows();
512
513 // Draw header
514 self.render_header(renderer)?;
515
516 // Draw entries
517 let content = self.content_bounds();
518 for (i, entry) in visible
519 .iter()
520 .skip(self.scroll_offset)
521 .take(visible_rows)
522 .enumerate()
523 {
524 let y = content.y + (i as i32 * ROW_HEIGHT as i32);
525 let row_rect = Rect::new(content.x, y, content.width, ROW_HEIGHT);
526
527 let actual_index = self.scroll_offset + i;
528 let is_selected = self.selected.contains(&actual_index);
529 let is_focused = actual_index == self.focused;
530 let is_renaming = rename_state.map_or(false, |s| s.index == actual_index);
531
532 // Row background
533 if is_selected {
534 renderer.fill_rounded_rect(row_rect, 4.0, theme.item_selected_background)?;
535 } else if i % 2 == 1 {
536 renderer.fill_rect(row_rect, theme.item_background.with_alpha(0.3))?;
537 }
538
539 // Focus indicator (subtle border)
540 if is_focused && self.selected.len() > 1 {
541 renderer.stroke_rect(row_rect, theme.selection_background.with_alpha(0.5), 1.0)?;
542 }
543
544 // Determine colors
545 let text_color = if is_selected {
546 theme.selection_foreground
547 } else if entry.hidden {
548 theme.item_foreground.with_alpha(0.5)
549 } else {
550 theme.item_foreground
551 };
552
553 let name_color = match entry.entry_type {
554 EntryType::Directory => Color::from_hex("#5c9fd8").unwrap_or(text_color),
555 EntryType::Symlink => Color::from_hex("#c678dd").unwrap_or(text_color),
556 _ => text_color,
557 };
558
559 let text_style = TextStyle::new()
560 .font_family(&theme.font_family)
561 .font_size(theme.font_size)
562 .color(text_color);
563
564 let name_style = TextStyle::new()
565 .font_family(&theme.font_family)
566 .font_size(theme.font_size)
567 .color(if is_selected {
568 theme.selection_foreground
569 } else {
570 name_color
571 });
572
573 // Name with icon prefix
574 let icon = match entry.entry_type {
575 EntryType::Directory => "\u{1F4C1} ",
576 EntryType::Symlink => "\u{1F517} ",
577 _ => "\u{1F4C4} ",
578 };
579
580 let name_rect =
581 Rect::new(row_rect.x + 8, row_rect.y, self.column_widths[0], ROW_HEIGHT);
582
583 if is_renaming {
584 // Render rename text field
585 if let Some(state) = rename_state {
586 self.render_rename_field(renderer, name_rect, state, &icon)?;
587 }
588 } else {
589 let display_name = if entry.is_symlink {
590 if let Some(target) = &entry.symlink_target {
591 let target_str = target.to_string_lossy();
592 // Truncate long targets
593 let target_display = if target_str.len() > 30 {
594 format!("...{}", &target_str[target_str.len()-27..])
595 } else {
596 target_str.to_string()
597 };
598 format!("{}{} -> {}", icon, entry.name, target_display)
599 } else {
600 format!("{}{}", icon, entry.name)
601 }
602 } else {
603 format!("{}{}", icon, entry.name)
604 };
605
606 renderer.text_in_rect(&display_name, name_rect, &name_style)?;
607 }
608
609 // Size
610 let size_rect = Rect::new(
611 row_rect.x + self.column_widths[0] as i32 + 16,
612 row_rect.y,
613 self.column_widths[1],
614 ROW_HEIGHT,
615 );
616 renderer.text_in_rect(&entry.format_size(), size_rect, &text_style)?;
617
618 // Modified date
619 let date_rect = Rect::new(
620 row_rect.x + self.column_widths[0] as i32 + self.column_widths[1] as i32 + 24,
621 row_rect.y,
622 self.column_widths[2],
623 ROW_HEIGHT,
624 );
625 renderer.text_in_rect(&entry.format_modified(), date_rect, &text_style)?;
626 }
627
628 Ok(())
629 }
630
631 /// Render the inline rename text field.
632 fn render_rename_field(&self, renderer: &Renderer, rect: Rect, state: &RenameState, icon: &str) -> anyhow::Result<()> {
633 let theme = renderer.theme();
634
635 // Background for text field (slightly lighter)
636 let field_rect = Rect::new(
637 rect.x + 24, // After icon
638 rect.y + 2,
639 rect.width.saturating_sub(28),
640 rect.height - 4,
641 );
642 renderer.fill_rounded_rect(field_rect, 2.0, theme.background)?;
643 renderer.stroke_rounded_rect(field_rect, 2.0, theme.selection_background, 1.0)?;
644
645 // Draw icon
646 let icon_style = TextStyle::new()
647 .font_family(&theme.font_family)
648 .font_size(theme.font_size)
649 .color(theme.item_foreground);
650 renderer.text(icon, (rect.x + 4) as f64, (rect.y + 4) as f64, &icon_style)?;
651
652 // Text style for the editable text
653 let text_style = TextStyle::new()
654 .font_family(&theme.font_family)
655 .font_size(theme.font_size)
656 .color(theme.foreground);
657
658 // Draw the text
659 let text_x = field_rect.x + 4;
660 let text_y = field_rect.y + 3;
661 renderer.text(&state.text, text_x as f64, text_y as f64, &text_style)?;
662
663 // Draw cursor
664 let cursor_x = if state.cursor == 0 {
665 text_x as f64
666 } else {
667 let prefix = &state.text[..state.cursor];
668 let prefix_width = renderer.measure_text(prefix, &text_style)?.width;
669 text_x as f64 + prefix_width as f64
670 };
671 renderer.line(
672 cursor_x,
673 (field_rect.y + 2) as f64,
674 cursor_x,
675 (field_rect.y + field_rect.height as i32 - 2) as f64,
676 theme.foreground,
677 1.0,
678 )?;
679
680 // Draw selection highlight if any
681 if let Some(sel_start) = state.selection_start {
682 let (from, to) = if sel_start < state.cursor {
683 (sel_start, state.cursor)
684 } else {
685 (state.cursor, sel_start)
686 };
687
688 let from_x = if from == 0 {
689 text_x as f64
690 } else {
691 let prefix = &state.text[..from];
692 text_x as f64 + renderer.measure_text(prefix, &text_style)?.width as f64
693 };
694
695 let to_x = if to == 0 {
696 text_x as f64
697 } else {
698 let prefix = &state.text[..to];
699 text_x as f64 + renderer.measure_text(prefix, &text_style)?.width as f64
700 };
701
702 let sel_rect = Rect::new(
703 from_x as i32,
704 field_rect.y + 2,
705 (to_x - from_x) as u32,
706 field_rect.height - 4,
707 );
708 renderer.fill_rect(sel_rect, theme.selection_background.with_alpha(0.3))?;
709 }
710
711 Ok(())
712 }
713
714 /// Render column headers.
715 fn render_header(&self, renderer: &Renderer) -> anyhow::Result<()> {
716 let theme = renderer.theme();
717 let header = self.header_bounds();
718
719 renderer.fill_rect(header, theme.item_background.darken(0.1))?;
720
721 let header_style = TextStyle::new()
722 .font_family(&theme.font_family)
723 .font_size(theme.font_size)
724 .color(theme.item_foreground.with_alpha(0.7));
725
726 let hover_style = TextStyle::new()
727 .font_family(&theme.font_family)
728 .font_size(theme.font_size)
729 .color(theme.selection_background);
730
731 // Sort indicator
732 let sort_indicator = match self.sort_direction {
733 SortDirection::Ascending => " \u{25B2}", // ▲
734 SortDirection::Descending => " \u{25BC}", // ▼
735 };
736
737 // Name header
738 let name_bounds = self.column_header_bounds(Column::Name);
739 let name_style = if self.hovered_header == Some(Column::Name) {
740 &hover_style
741 } else {
742 &header_style
743 };
744 let name_label = if self.sort_order == SortOrder::Name {
745 format!("Name{}", sort_indicator)
746 } else {
747 "Name".to_string()
748 };
749 renderer.text_in_rect(&name_label, name_bounds, name_style)?;
750
751 // Size header
752 let size_bounds = self.column_header_bounds(Column::Size);
753 let size_style = if self.hovered_header == Some(Column::Size) {
754 &hover_style
755 } else {
756 &header_style
757 };
758 let size_label = if self.sort_order == SortOrder::Size {
759 format!("Size{}", sort_indicator)
760 } else {
761 "Size".to_string()
762 };
763 renderer.text_in_rect(&size_label, size_bounds, size_style)?;
764
765 // Modified header
766 let modified_bounds = self.column_header_bounds(Column::Modified);
767 let modified_style = if self.hovered_header == Some(Column::Modified) {
768 &hover_style
769 } else {
770 &header_style
771 };
772 let modified_label = if self.sort_order == SortOrder::Modified {
773 format!("Modified{}", sort_indicator)
774 } else {
775 "Modified".to_string()
776 };
777 renderer.text_in_rect(&modified_label, modified_bounds, modified_style)?;
778
779 // Draw column dividers
780 let divider_color = theme.border.with_alpha(0.3);
781 let divider1_x = (header.x + self.column_widths[0] as i32 + 12) as f64;
782 renderer.line(
783 divider1_x,
784 (header.y + 4) as f64,
785 divider1_x,
786 (header.y + header.height as i32 - 4) as f64,
787 divider_color,
788 1.0,
789 )?;
790
791 let divider2_x = divider1_x + self.column_widths[1] as f64 + 8.0;
792 renderer.line(
793 divider2_x,
794 (header.y + 4) as f64,
795 divider2_x,
796 (header.y + header.height as i32 - 4) as f64,
797 divider_color,
798 1.0,
799 )?;
800
801 Ok(())
802 }
803 }
804