T13b Trees: Tree, TreeNode, TreeSelect, Outline, Disclosure (review pending: codex 401)
This commit is contained in:
22 files changed
+1969
-94
No files matched your search
@@ -0,0 +1,207 @@
|
||||
use gpui::{
|
||||
Context, Entity, IntoElement, Modifiers, MouseButton, Pixels, Point, Render, SharedString,
|
||||
Styled, TestAppContext, VisualTestContext, Window, point, px,
|
||||
};
|
||||
|
||||
use super::{DropAt, Tree, TreeNode};
|
||||
use crate::theme::Theme;
|
||||
|
||||
/// A small project tree that keeps what its tree reports.
|
||||
#[derive(Default)]
|
||||
struct Explorer {
|
||||
docs: Vec<TreeNode>,
|
||||
selected: Vec<SharedString>,
|
||||
checked: Option<Vec<SharedString>>,
|
||||
heard: Vec<String>,
|
||||
}
|
||||
|
||||
impl Render for Explorer {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let view = cx.entity();
|
||||
let heard = |view: &Entity<Explorer>| {
|
||||
let view = view.clone();
|
||||
move |line: String, cx: &mut gpui::App| {
|
||||
view.update(cx, |explorer, _| explorer.heard.push(line))
|
||||
}
|
||||
};
|
||||
let docs = match self.docs.is_empty() {
|
||||
true => TreeNode::new("docs", "docs").pending(),
|
||||
false => TreeNode::new("docs", "docs").children(self.docs.clone()),
|
||||
};
|
||||
let all = [
|
||||
TreeNode::new("src", "src").children([
|
||||
TreeNode::new("lib", "lib.rs"),
|
||||
TreeNode::new("ui", "ui").child(TreeNode::new("button", "button.rs")),
|
||||
]),
|
||||
docs,
|
||||
TreeNode::new("readme", "README.md"),
|
||||
];
|
||||
let (select, check, load, rename, moved) = (
|
||||
view.clone(),
|
||||
view,
|
||||
heard(&cx.entity()),
|
||||
heard(&cx.entity()),
|
||||
heard(&cx.entity()),
|
||||
);
|
||||
let tree = Tree::new("tree", all)
|
||||
.open(["src"])
|
||||
.selected(self.selected.clone())
|
||||
.on_select(move |keys, _, cx| {
|
||||
select.update(cx, |explorer, _| explorer.selected = keys.to_vec())
|
||||
})
|
||||
.on_load(move |key, _, cx| load(format!("load {key}"), cx))
|
||||
.on_rename(move |key, name, _, cx| rename(format!("rename {key} {name}"), cx))
|
||||
.on_move(move |key, target, at, _, cx| moved(format!("move {key} {at:?} {target}"), cx))
|
||||
.w(px(300.0))
|
||||
.h(px(400.0));
|
||||
match self.checked.clone() {
|
||||
Some(checked) => tree.checked(checked).on_check(move |keys, _, cx| {
|
||||
check.update(cx, |explorer, _| explorer.checked = Some(keys.to_vec()))
|
||||
}),
|
||||
None => tree,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn explorer(checked: bool, cx: &mut TestAppContext) -> (Entity<Explorer>, &mut VisualTestContext) {
|
||||
cx.update(|cx| {
|
||||
Theme::init(cx);
|
||||
Theme::update(cx, |theme| theme.reduced_motion = true);
|
||||
crate::forms::bind_keys(cx);
|
||||
});
|
||||
let (view, cx) = cx.add_window_view(move |_, _| Explorer {
|
||||
checked: checked.then(Vec::new),
|
||||
..Explorer::default()
|
||||
});
|
||||
settle(cx);
|
||||
(view, cx)
|
||||
}
|
||||
|
||||
/// Frames 2ms apart, past reduced motion's 1ms glides.
|
||||
fn settle(cx: &mut VisualTestContext) {
|
||||
for _ in 0..3 {
|
||||
std::thread::sleep(std::time::Duration::from_millis(2));
|
||||
cx.update(|window, _| window.refresh());
|
||||
cx.run_until_parked();
|
||||
}
|
||||
}
|
||||
|
||||
/// Row `ix`'s left padding: rows are 28 tall.
|
||||
fn row(ix: usize) -> Point<Pixels> {
|
||||
point(px(4.0), px(14.0 + 28.0 * ix as f32))
|
||||
}
|
||||
|
||||
fn read<T>(view: &Entity<Explorer>, cx: &mut VisualTestContext, f: impl Fn(&Explorer) -> T) -> T {
|
||||
view.read_with(cx, |explorer, _| f(explorer))
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn keys_walk_open_climb_and_close(cx: &mut TestAppContext) {
|
||||
let (view, cx) = explorer(false, cx);
|
||||
cx.simulate_click(row(0), Modifiers::none());
|
||||
settle(cx);
|
||||
let mut seen = Vec::new();
|
||||
for keys in ["right", "down", "right", "right", "left", "left"] {
|
||||
cx.simulate_keystrokes(keys);
|
||||
settle(cx);
|
||||
seen.push(read(&view, cx, |explorer| explorer.selected.join(",")));
|
||||
}
|
||||
assert_eq!(
|
||||
seen,
|
||||
["lib", "ui", "ui", "button", "ui", "ui"],
|
||||
"right enters, left climbs, then closes"
|
||||
);
|
||||
cx.simulate_keystrokes("down");
|
||||
settle(cx);
|
||||
assert_eq!(
|
||||
read(&view, cx, |explorer| explorer.selected.join(",")),
|
||||
"docs",
|
||||
"ui closed again"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_pending_node_asks_for_its_children_once(cx: &mut TestAppContext) {
|
||||
let (view, cx) = explorer(false, cx);
|
||||
cx.simulate_click(row(3), Modifiers::none());
|
||||
settle(cx);
|
||||
cx.simulate_keystrokes("right");
|
||||
settle(cx);
|
||||
settle(cx);
|
||||
assert_eq!(
|
||||
read(&view, cx, |explorer| explorer.heard.clone()),
|
||||
["load docs"],
|
||||
"asked once while it waits"
|
||||
);
|
||||
view.update(cx, |explorer, cx| {
|
||||
explorer.docs = vec![TreeNode::new("guide", "guide.md")];
|
||||
cx.notify();
|
||||
});
|
||||
settle(cx);
|
||||
cx.simulate_keystrokes("left right down");
|
||||
settle(cx);
|
||||
assert_eq!(
|
||||
read(&view, cx, |explorer| explorer.heard.clone()),
|
||||
["load docs"]
|
||||
);
|
||||
assert_eq!(
|
||||
read(&view, cx, |explorer| explorer.selected.join(",")),
|
||||
"guide",
|
||||
"the children came"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn space_checks_a_whole_branch_and_again_clears_it(cx: &mut TestAppContext) {
|
||||
let (view, cx) = explorer(true, cx);
|
||||
cx.simulate_click(row(0), Modifiers::none());
|
||||
settle(cx);
|
||||
cx.simulate_keystrokes("space");
|
||||
settle(cx);
|
||||
assert_eq!(
|
||||
read(&view, cx, |explorer| explorer.checked.clone()),
|
||||
Some(vec!["lib".into(), "button".into()])
|
||||
);
|
||||
cx.simulate_keystrokes("space");
|
||||
settle(cx);
|
||||
assert_eq!(
|
||||
read(&view, cx, |explorer| explorer.checked.clone()),
|
||||
Some(Vec::new())
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn f2_renames_a_node(cx: &mut TestAppContext) {
|
||||
let (view, cx) = explorer(false, cx);
|
||||
cx.simulate_click(row(1), Modifiers::none());
|
||||
settle(cx);
|
||||
cx.simulate_keystrokes("f2");
|
||||
settle(cx);
|
||||
cx.simulate_input("main.rs");
|
||||
cx.simulate_keystrokes("enter");
|
||||
settle(cx);
|
||||
assert_eq!(
|
||||
read(&view, cx, |explorer| explorer.heard.clone()),
|
||||
["rename lib main.rs"]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_drop_in_a_folders_middle_moves_the_node_inside(cx: &mut TestAppContext) {
|
||||
let (view, cx) = explorer(false, cx);
|
||||
cx.simulate_mouse_down(row(4), MouseButton::Left, Modifiers::none());
|
||||
for y in [120.0, 100.0, 80.0, 70.0] {
|
||||
cx.simulate_mouse_move(point(px(40.0), px(y)), MouseButton::Left, Modifiers::none());
|
||||
settle(cx);
|
||||
}
|
||||
cx.simulate_mouse_up(
|
||||
point(px(40.0), px(70.0)),
|
||||
MouseButton::Left,
|
||||
Modifiers::none(),
|
||||
);
|
||||
settle(cx);
|
||||
assert_eq!(
|
||||
read(&view, cx, |explorer| explorer.heard.clone()),
|
||||
[format!("move readme {:?} ui", DropAt::Inside)]
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user