Add the board, and finish v1.1 (stage 9e)
The board is the user's design: the document as lists and blocks, the
way Scratch nests blocks inside a loop. Each node without a parent (the
root, then each loose node) is a column with its title in the top border;
its children are cards in it, one under the other; a card with children
on show wraps them in a C: a joint at its bottom left, a bracket down the
left two cells further in, and a foot closing it. Blocks nest as deep as
the tree does. The columns by tag or color the first design had are
dropped (the user's pick), and :board takes no arguments any more.
j and k go down and up a column in reading order, into the blocks; h and
l go to the column beside, onto the card nearest the cursor's height.
Titles are typed right in their cards, and visual mode selects in
reading order as the outline does. <Space>vb or :board shows it.
Move mode (M, the user's pick) moves the card through the tree: h and l
to the end of the column beside, j and k past the cards around it, into
an open block and out of one at its end. A column's own node trades
places with the loose column beside it; the root's column stays first.
A drag does the same with the mouse: dropped on a card it goes after it,
on a block's card into it, on a column at its end.
The fallback message for a key a view ignores now names the view ("does
nothing in the board view"). The version goes to 1.1.0: stage 9 is done.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
893edbda9f
commit
3716919db3
20 files changed
+946
-50
No files matched your search
+1
-5
@@ -40,7 +40,7 @@ pub fn actions_for(call: Call, ctx: &ViewContext<'_>, on: &[NodeId]) -> Vec<Acti
|
||||
Call::Split(view) => app(AppAction::Split { arrange: Arrange::Stacked, view }),
|
||||
Call::VSplit(view) => app(AppAction::Split { arrange: Arrange::SideBySide, view }),
|
||||
Call::Only => app(AppAction::OnlyPane),
|
||||
Call::Show(ViewKind::Board) | Call::Board(_) => later("the board comes in v1.1"),
|
||||
Call::Board => app(AppAction::ShowView(ViewKind::Board)),
|
||||
Call::Show(view) => app(AppAction::ShowView(view)),
|
||||
Call::Buffers => app(AppAction::OpenPicker(PickKind::Document)),
|
||||
Call::Buffer(name) => app(AppAction::SwitchDocument(DocumentChoice::Named(name))),
|
||||
@@ -109,10 +109,6 @@ fn say(message: Message) -> Vec<Action> {
|
||||
vec![Action::say(message)]
|
||||
}
|
||||
|
||||
fn later(text: &str) -> Vec<Action> {
|
||||
say(Message::info(text))
|
||||
}
|
||||
|
||||
/// `:in`: adds an idea under the Inbox, a loose node made the first time.
|
||||
/// The cursor stays where it is: capturing shouldn't break your flow.
|
||||
fn capture(ctx: &ViewContext<'_>, text: &str) -> Vec<Action> {
|
||||
|
||||
@@ -120,6 +120,7 @@ pub fn shared(typed: &Typed, ctx: &ViewContext<'_>) -> Option<Vec<Action>> {
|
||||
Command::ShowCanvas => app(AppAction::ShowView(ViewKind::Canvas)),
|
||||
Command::ShowOutline => app(AppAction::ShowView(ViewKind::Outline)),
|
||||
Command::ShowNote => app(AppAction::ShowView(ViewKind::Note)),
|
||||
Command::ShowBoard => app(AppAction::ShowView(ViewKind::Board)),
|
||||
Command::NextDocument => app(AppAction::SwitchDocument(DocumentChoice::Next)),
|
||||
Command::PreviousDocument => app(AppAction::SwitchDocument(DocumentChoice::Previous)),
|
||||
// ZZ saves (if there are changes) and closes the pane, as in vim: the
|
||||
|
||||
+2
-2
@@ -59,8 +59,8 @@ impl App {
|
||||
})
|
||||
.or_else(|| commands::shared(&typed, &ctx));
|
||||
let actions = actions.unwrap_or_else(|| {
|
||||
let later = format!("\"{}\" comes in a later part of the build", describe(typed.command));
|
||||
vec![Action::say(Message::info(later))]
|
||||
let nothing = format!("\"{}\" does nothing in the {} view", describe(typed.command), kind.name());
|
||||
vec![Action::say(Message::info(nothing))]
|
||||
});
|
||||
self.update(actions);
|
||||
}
|
||||
|
||||
+3
-1
@@ -66,6 +66,7 @@ use crate::input::mouse::MouseReader;
|
||||
use crate::model::DocUid;
|
||||
use crate::storage::memory::Memory;
|
||||
use crate::ui::theme::Theme;
|
||||
use crate::view::board::BoardView;
|
||||
use crate::view::canvas::{Camera, CanvasView};
|
||||
use crate::view::note::NoteView;
|
||||
use crate::view::outline::OutlineView;
|
||||
@@ -183,7 +184,8 @@ impl Pane {
|
||||
let mut view: Box<dyn View> = match kind {
|
||||
ViewKind::Outline => Box::new(OutlineView::new()),
|
||||
ViewKind::Note => Box::new(NoteView::new()),
|
||||
ViewKind::Canvas | ViewKind::Board => Box::new(CanvasView::new()),
|
||||
ViewKind::Board => Box::new(BoardView::new()),
|
||||
ViewKind::Canvas => Box::new(CanvasView::new()),
|
||||
};
|
||||
if let Some(camera) = camera {
|
||||
view.set_camera(camera);
|
||||
|
||||
+7
-1
@@ -182,13 +182,16 @@ fn hints(mode: &Mode, view: ViewKind) -> &'static str {
|
||||
(Mode::Normal, ViewKind::Note) => {
|
||||
"j/k parts · Enter edit or go · h/l siblings · a/o add · <C-w> panes · ? help"
|
||||
}
|
||||
(Mode::Normal, ViewKind::Board) => {
|
||||
"j/k cards · h/l columns · i edit · a/o add · dd delete · M move a card · K body · ? help"
|
||||
}
|
||||
(Mode::Normal, _) => {
|
||||
"hjkl move · i edit · a/o add · dd delete · K body · M move · <C-w> panes · <Space> menu · ? help"
|
||||
}
|
||||
(Mode::Text(edit), _) | (Mode::Popup(Popup::Card(Card { edit: Some(edit), .. })), _) => {
|
||||
text_hints(edit)
|
||||
}
|
||||
(Mode::Visual(_), ViewKind::Canvas | ViewKind::Board) => {
|
||||
(Mode::Visual(_), ViewKind::Canvas) => {
|
||||
"hjkl more · o other end · d delete · y yank · > < in/out · M move · <Space> color, align… · Esc stop"
|
||||
}
|
||||
(Mode::Visual(_), ViewKind::Note) => {
|
||||
@@ -197,6 +200,9 @@ fn hints(mode: &Mode, view: ViewKind) -> &'static str {
|
||||
(Mode::Visual(_), _) => {
|
||||
"j/k more · o other end · d delete · y yank · > < in/out · <Space>c color · Esc stop"
|
||||
}
|
||||
(Mode::Move(_), ViewKind::Board) => {
|
||||
"h/l to the column beside · j/k down or up, into and out of blocks · Enter done"
|
||||
}
|
||||
(Mode::Move(_), ViewKind::Outline | ViewKind::Note) => {
|
||||
"j/k swap with a sibling (J/K: 5) · h out a level · l under the one above · Enter done"
|
||||
}
|
||||
|
||||
+2
-3
@@ -4,6 +4,7 @@
|
||||
//! `tests/files.rs`, and the config folder in `tests/config.rs`.
|
||||
|
||||
mod arrange;
|
||||
mod board;
|
||||
mod branch_out;
|
||||
mod config;
|
||||
mod connect;
|
||||
@@ -507,10 +508,8 @@ fn indenting_and_outdenting_nodes() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v1_1_commands_say_so_and_portal_titles_stay() {
|
||||
fn portal_titles_stay_their_documents_names() {
|
||||
let mut h = Harness::new();
|
||||
h.type_keys(":board<CR>");
|
||||
assert_eq!(h.message(), "the board comes in v1.1");
|
||||
// Kitchen is a portal: its title is the other document's name.
|
||||
h.type_keys("glgjgjgj");
|
||||
assert_eq!(h.cursor(), "Kitchen");
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
//! The board (part 9e), end to end: a column per node without a parent,
|
||||
//! cards in them, blocks wrapping their cards; getting around, move mode,
|
||||
//! dragging, and typing in a card.
|
||||
|
||||
use super::*;
|
||||
use crate::input::mouse::{Cell, Gesture};
|
||||
|
||||
/// Drags the card showing `from` onto the one showing `to`.
|
||||
fn drag(h: &mut Harness, from: &str, to: &str) {
|
||||
let (start, end) = (h.find(from), h.find(to));
|
||||
let (start, end) = (Cell { col: start.0, row: start.1 }, Cell { col: end.0, row: end.1 });
|
||||
h.mouse(Gesture::Press { at: start, ctrl: false });
|
||||
h.mouse(Gesture::Drag { from: start, at: end, ctrl: false });
|
||||
h.mouse(Gesture::Release { at: end, dragged: true });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn space_vb_shows_the_board() {
|
||||
let mut h = Harness::new();
|
||||
h.type_keys("<Space>vb");
|
||||
let screen = h.screen();
|
||||
assert!(screen.contains("┌ board · Garden plans"), "{screen}");
|
||||
for column in ["╭ Garden plans ", "┌ Seeds to order ", "┌ ask about compost "] {
|
||||
assert!(screen.contains(column), "a column per node without a parent: {column}\n{screen}");
|
||||
}
|
||||
// Watering is a block: its card, its cards further in, and a foot.
|
||||
let row = h.find("│ Watering │").1;
|
||||
let rows = h.rows();
|
||||
assert!(rows[usize::from(row) + 1].contains("├──────────┘"), "{screen}");
|
||||
assert!(rows[usize::from(row) + 3].contains("│ │ Rain barrel │"), "{screen}");
|
||||
assert!(rows.iter().any(|row| row.contains("│ └───────")), "the foot:\n{screen}");
|
||||
// j and k go down and up the column, h and l to the one beside.
|
||||
h.type_keys("jj");
|
||||
assert_eq!(h.cursor(), "Raised beds");
|
||||
h.type_keys("j");
|
||||
assert_eq!(h.cursor(), "Cedar, untreated", "into the block, opened while the cursor's inside");
|
||||
h.type_keys("l");
|
||||
assert_eq!(h.parent(&h.cursor()), "Seeds to order", "the nearest card in the next column");
|
||||
h.type_keys("l");
|
||||
assert_eq!(h.cursor(), "ask about compost");
|
||||
h.type_keys("l");
|
||||
assert_eq!(h.message(), "that's the last column");
|
||||
h.type_keys(":canvas<CR>:board<CR>");
|
||||
assert!(h.screen().contains("┌ board · Garden plans"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_mode_moves_cards_through_the_tree() {
|
||||
let mut h = Harness::new();
|
||||
h.type_keys("<Space>vbjM");
|
||||
assert_eq!(h.label(), "MOVE");
|
||||
// Past Raised beds (folded), then into Watering's block, then past Rain barrel.
|
||||
h.type_keys("j");
|
||||
assert_eq!(h.children("Garden plans"), ["Raised beds", "Soil", "Watering", "Kitchen"]);
|
||||
h.type_keys("j");
|
||||
assert_eq!(h.children("Watering"), ["Soil", "Rain barrel", "Drip line"]);
|
||||
h.type_keys("j");
|
||||
assert_eq!(h.children("Watering"), ["Rain barrel", "Soil", "Drip line"]);
|
||||
// l: to the end of the next column.
|
||||
h.type_keys("l<CR>");
|
||||
assert_eq!(h.children("Seeds to order"), ["Tomatoes", "Basil", "Soil"]);
|
||||
h.type_keys("u");
|
||||
assert_eq!(h.children("Garden plans"), ["Soil", "Raised beds", "Watering", "Kitchen"], "one undo step");
|
||||
// A column trades places with the one beside it; the root's stays first.
|
||||
h.type_keys(":find seeds<CR>Ml<CR>");
|
||||
let loose: Vec<String> = h.doc().loose().iter().map(|&id| h.title(id)).collect();
|
||||
assert_eq!(loose, ["ask about compost", "Seeds to order"]);
|
||||
h.type_keys("ggMl");
|
||||
assert_eq!(h.message(), "the root's column stays first");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_card_drags_to_where_it_drops() {
|
||||
let mut h = Harness::new();
|
||||
h.type_keys("<Space>vb");
|
||||
drag(&mut h, "Soil", "Tomatoes");
|
||||
assert_eq!(h.children("Seeds to order"), ["Tomatoes", "Soil", "Basil"], "after the card it dropped on");
|
||||
drag(&mut h, "Kitchen", "Watering");
|
||||
assert_eq!(h.children("Watering")[0], "Kitchen", "dropped on a block: its first card");
|
||||
drag(&mut h, "Basil", "ask about compost");
|
||||
assert_eq!(h.children("ask about compost"), ["Basil"], "dropped on a column: its last card");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cards_are_typed_in_and_selected_in_reading_order() {
|
||||
let mut h = Harness::new();
|
||||
h.type_keys("<Space>vbjaMul");
|
||||
assert!(h.screen().contains("┃ Mul ┃"), "typed in its card:\n{}", h.screen());
|
||||
assert!(h.text_cursor().is_some());
|
||||
h.type_keys("ch<Esc><Esc>");
|
||||
assert_eq!(h.children("Soil"), ["Mulch"]);
|
||||
// v and j select down the column, in reading order.
|
||||
h.type_keys("ggjvjj");
|
||||
assert!(h.rows()[h.rows().len() - 2].contains("3 nodes selected"), "{}", h.screen());
|
||||
h.type_keys("d");
|
||||
assert_eq!(h.message(), "deleted 3 nodes · u to undo");
|
||||
}
|
||||
@@ -247,7 +247,6 @@ impl App {
|
||||
}
|
||||
}
|
||||
AppAction::EqualizePanes => self.panes.equalize(),
|
||||
AppAction::ShowView(ViewKind::Board) => return Some(Message::info("the board comes in v1.1")),
|
||||
AppAction::ShowView(kind) => {
|
||||
self.keep_cameras();
|
||||
let pane = self.panes.current_mut();
|
||||
@@ -288,9 +287,6 @@ impl App {
|
||||
}
|
||||
let pane = self.panes.current();
|
||||
let kind = view.unwrap_or(pane.view.kind());
|
||||
if kind == ViewKind::Board {
|
||||
return Some(Message::info("the board comes in v1.1"));
|
||||
}
|
||||
let buffer = pane.buffer;
|
||||
let camera = pane.view.camera().or(self.shared.docs.get(buffer).camera);
|
||||
// The new pane starts with a copy of this one's jump list, as in vim
|
||||
|
||||
+2
-2
@@ -28,7 +28,7 @@ pub fn selection(visual: &Visual, ctx: &ViewContext<'_>, kind: ViewKind) -> Vec<
|
||||
let doc = ctx.doc;
|
||||
let visible = Visible::new(doc, ctx.cursor, ctx.settings.focus);
|
||||
let mut chosen: BTreeSet<NodeId> = match kind {
|
||||
ViewKind::Canvas | ViewKind::Board => {
|
||||
ViewKind::Canvas => {
|
||||
let rects = ctx.box_rects();
|
||||
let ends: Vec<CanvasRect> =
|
||||
[visual.anchor, ctx.cursor].iter().filter_map(|id| rects.get(id).copied()).collect();
|
||||
@@ -42,7 +42,7 @@ pub fn selection(visual: &Visual, ctx: &ViewContext<'_>, kind: ViewKind) -> Vec<
|
||||
None => BTreeSet::from([ctx.cursor]),
|
||||
}
|
||||
}
|
||||
ViewKind::Outline | ViewKind::Note => {
|
||||
ViewKind::Outline | ViewKind::Note | ViewKind::Board => {
|
||||
let order = doc.outline();
|
||||
let at = |id: NodeId| order.iter().position(|&other| other == id);
|
||||
let (from, to) = match (at(visual.anchor), at(ctx.cursor)) {
|
||||
|
||||
Reference in new issue
Block a user