Files: a narrow listing keeps its names; a column starts fresh for a new folder
The directory listing's size and date columns are fixed, so at 280px the name had no room and rows showed no names. The heading and rows now sit in a sideways scroll box whose child keeps the least width: the name's least, the fixed columns and each row's icon, gaps and padding. A column of the explorer was keyed by its depth, so a new folder there inherited the old list's cursor; columns and rows are keyed by their folder. The explorer asserts each folder on its path is a folder in its parent.
This commit is contained in:
1 parent
14de69fed6
commit
4717aad596
7 files changed
+169
-31
No files matched your search
+41
-25
@@ -2,14 +2,14 @@ use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
App, ElementId, InteractiveElement, IntoElement, MouseButton, ParentElement, RenderOnce,
|
||||
SharedString, StatefulInteractiveElement, Styled, Window, div, prelude::*,
|
||||
SharedString, StatefulInteractiveElement, Styled, Window, div, prelude::*, rems,
|
||||
};
|
||||
use jiff::Timestamp;
|
||||
|
||||
use super::{ListItem, SelectableList};
|
||||
use crate::{
|
||||
forms::OnValue,
|
||||
layout::seeded::use_seeded,
|
||||
layout::{on_axis, seeded::use_seeded},
|
||||
navigation::{Breadcrumb, Crumb},
|
||||
primitives::{Icon, IconName, file_icon},
|
||||
theme::{ActiveTheme, IconSize, TextSize},
|
||||
@@ -67,6 +67,9 @@ enum Column {
|
||||
Modified,
|
||||
}
|
||||
|
||||
/// Each row's side padding and three gaps, in rems, as `ListItem` lays them.
|
||||
const ROW_CHROME: f32 = 3.75;
|
||||
|
||||
/// Entries as a listing first shows them: folders first, then by name.
|
||||
pub(crate) fn listed(entries: Vec<DirEntry>) -> Vec<DirEntry> {
|
||||
sorted(entries, Column::Name, true)
|
||||
@@ -188,6 +191,11 @@ impl RenderOnce for DirectoryListing {
|
||||
let theme = cx.theme();
|
||||
let colors = &theme.colors;
|
||||
let (size_width, date_width) = (theme.label_width() * 0.5, theme.label_width() * 0.75);
|
||||
let least = theme.label_width()
|
||||
+ size_width
|
||||
+ date_width
|
||||
+ theme.icon_size(IconSize::Md)
|
||||
+ rems(ROW_CHROME);
|
||||
let now = Timestamp::now();
|
||||
let header = |label: &'static str, this: Column| {
|
||||
let order = order.clone();
|
||||
@@ -237,7 +245,12 @@ impl RenderOnce for DirectoryListing {
|
||||
.border_color(colors.border)
|
||||
.text_size(theme.text_size(TextSize::Xs))
|
||||
.text_color(colors.fg_muted)
|
||||
.child(div().flex_1().child(header("Name", Column::Name)))
|
||||
.child(
|
||||
div()
|
||||
.debug_selector(|| "listing-name".into())
|
||||
.flex_1()
|
||||
.child(header("Name", Column::Name)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w(size_width)
|
||||
@@ -303,29 +316,32 @@ impl RenderOnce for DirectoryListing {
|
||||
&self.path,
|
||||
self.on_climb,
|
||||
))
|
||||
.child(heading)
|
||||
.child(
|
||||
list.on_change(move |keys, window, cx| {
|
||||
let Some(name) = keys.first() else {
|
||||
return;
|
||||
};
|
||||
store.update(cx, |picked, cx| {
|
||||
picked.value = Some(name.clone());
|
||||
cx.notify();
|
||||
});
|
||||
if let Some(on_select) = &on_select {
|
||||
on_select(name, window, cx);
|
||||
}
|
||||
})
|
||||
.on_activate(move |name, window, cx| {
|
||||
let entry = shown
|
||||
.iter()
|
||||
.find(|entry| entry.name == *name)
|
||||
.expect("a listed entry");
|
||||
if let Some(on_open) = &on_open {
|
||||
on_open(entry, window, cx);
|
||||
}
|
||||
}),
|
||||
on_axis(div().id((self.id.clone(), "sideways")).overflow_x_scroll()).child(
|
||||
div().flex().flex_col().min_w(least).child(heading).child(
|
||||
list.on_change(move |keys, window, cx| {
|
||||
let Some(name) = keys.first() else {
|
||||
return;
|
||||
};
|
||||
store.update(cx, |picked, cx| {
|
||||
picked.value = Some(name.clone());
|
||||
cx.notify();
|
||||
});
|
||||
if let Some(on_select) = &on_select {
|
||||
on_select(name, window, cx);
|
||||
}
|
||||
})
|
||||
.on_activate(move |name, window, cx| {
|
||||
let entry = shown
|
||||
.iter()
|
||||
.find(|entry| entry.name == *name)
|
||||
.expect("a listed entry");
|
||||
if let Some(on_open) = &on_open {
|
||||
on_open(entry, window, cx);
|
||||
}
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+33
-2
@@ -10,8 +10,8 @@ use gpui::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
InfiniteList, ListItem, SelectableList, SortableList, SwipeAction, SwipeableListItem,
|
||||
VirtualList,
|
||||
DirEntry, DirectoryListing, InfiniteList, ListItem, SelectableList, SortableList, SwipeAction,
|
||||
SwipeableListItem, VirtualList,
|
||||
};
|
||||
use crate::{
|
||||
primitives::{IconName, Measure},
|
||||
@@ -400,3 +400,34 @@ fn the_cursor_stays_on_its_row_when_the_rows_reorder(cx: &mut TestAppContext) {
|
||||
assert_eq!(opened, ["a"], "enter opens the row the cursor was on");
|
||||
assert_eq!(picks(&view, cx), ["b"], "up moves from where that row went");
|
||||
}
|
||||
|
||||
/// A listing 280 wide.
|
||||
struct Narrow;
|
||||
|
||||
impl Render for Narrow {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
let then = jiff::Timestamp::from_second(1_700_000_000).expect("a time");
|
||||
let entries = [
|
||||
DirEntry::folder("Design", then),
|
||||
DirEntry::file("Brief.md", 4_800, then),
|
||||
];
|
||||
div()
|
||||
.w(px(280.0))
|
||||
.child(DirectoryListing::new("listing", ["Home"], entries))
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_narrow_listing_keeps_room_for_the_name(cx: &mut TestAppContext) {
|
||||
setup(cx);
|
||||
let (_, cx) = cx.add_window_view(|_, _| Narrow);
|
||||
settle(cx);
|
||||
let name = cx
|
||||
.debug_bounds("listing-name")
|
||||
.expect("the name heads its column");
|
||||
let least = cx.update(|window, cx| {
|
||||
use crate::theme::ActiveTheme;
|
||||
cx.theme().label_width().to_pixels(window.rem_size())
|
||||
});
|
||||
assert!(name.size.width >= least, "{name:?} under {least:?}");
|
||||
}
|
||||
Reference in new issue
Block a user