Draw the board as the canvas draws boxes, and drag cards live
The user found the board's look off from the canvas's: a block's C was gray around a white card, borders ignored the node's style, a folded column said nothing, and dragging felt wrong. Columns and cards now go through the canvas's line layer, with the same rules as Scene::add_borders: the node's border style and color, thick in the accent color for the cursor, thick in the visual color for the selection. A block's bracket and foot are drawn in its card's own line and color, so the C is one shape and its joint comes out as ├, ┣ or ╠. Tags and badges sit in the bottom border through a footer() shared with the canvas renderer. Columns show their tags and badge too, so a folded column says +N, and a column's title can be typed in its top border (it wasn't shown as typed before). A drag now moves the card live, as on the canvas (the user's pick), all one undo step: by the row under the mouse, before a card on its top half, after it or into its block on its bottom half, out at a foot, first on a column's title, last below the cards. Columns keep their widths while dragging, or a column that narrowed would slide the one under the mouse away and the card would chase it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
3716919db3
commit
38eca3c8c3
8 files changed
+424
-233
No files matched your search
@@ -343,9 +343,15 @@ lines.
|
||||
left past its cards (two cells further in), and a foot closing it. Blocks
|
||||
nest as deep as the tree does. The columns by tag or by color the first
|
||||
design had are dropped (the user's pick).
|
||||
- A card is a node's box, as on the canvas: its title, its tags and badge
|
||||
in the bottom border, its color on the border, thick for the cursor.
|
||||
Folds fold here too (a folded block is a card with `+N`).
|
||||
- A card is a node's box, and a column a node's box with the title in its
|
||||
top border, drawn exactly as the canvas draws them (the user asked for
|
||||
them to match): its border as its style says (rounded for the root,
|
||||
dashed for a question, double…), in its color, thick in the accent color
|
||||
for the cursor; the tags in its color and the badge in its bottom border.
|
||||
The borders go through the canvas's line layer, so a block's "C" is in
|
||||
its card's own line and color, joined where they meet (`├`, `┣`, `╠`).
|
||||
Folds fold here too: a folded block is a card with `+N`, and a folded
|
||||
column says `+N` in its bottom border.
|
||||
- `j` and `k` go down and up a column in reading order (into the blocks),
|
||||
`h` and `l` to the column beside, onto the card nearest the cursor's
|
||||
height. Titles are typed right in their cards, and `<Space>vb` or
|
||||
@@ -354,9 +360,15 @@ lines.
|
||||
the end of the column beside, `j` and `k` past the cards around it, into
|
||||
a block (as its first card going down, its last going up) and out of one
|
||||
at its end; a column's own node trades places with the column beside
|
||||
(the root's stays first). Enter is done, one undo step. 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. Every move goes through the tree.
|
||||
(the root's stays first). Enter is done, one undo step. Every move goes
|
||||
through the tree.
|
||||
- A drag moves the card live, as the canvas does (the user's pick): it goes
|
||||
through the board as the mouse goes, by the row the mouse is on in the
|
||||
column under it. On a card's top half it goes before that card; on its
|
||||
bottom half after it, or into its block as the first card; on a block's
|
||||
foot, after the block; on a column's title, first; below the cards,
|
||||
last. The whole drag is one undo step. The columns keep their widths
|
||||
while a card is dragged, so the one under the mouse stays put.
|
||||
- In visual mode the board selects in reading order, as the outline does.
|
||||
|
||||
**Views, panes and popups** (settled in stage 7)
|
||||
@@ -556,8 +568,9 @@ pans up and down, and sideways with a wheel that tilts, a touchpad, or the
|
||||
wheel with Shift or Alt held (Alt for terminals that keep Shift for selecting
|
||||
text), Ctrl+drag selects a rectangle, Ctrl+click adds or removes one node,
|
||||
right-click opens a menu built from the same actions as the keys. Zoom is on
|
||||
the `+` and `-` keys. On the board, dragging a card moves it in the tree:
|
||||
after the card it's dropped on, into a block, or to the end of a column.
|
||||
the `+` and `-` keys. On the board, dragging a card moves it through the
|
||||
tree as the mouse goes: before or after the card under it (by which half),
|
||||
into a block, out at its foot, or to a column's top or end.
|
||||
|
||||
## Modes and keys
|
||||
|
||||
|
||||
+109
-18
@@ -1,17 +1,39 @@
|
||||
//! 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.
|
||||
//! cards in them, blocks wrapping their cards, all drawn as the canvas draws
|
||||
//! boxes; getting around, move mode, dragging, and typing in a card.
|
||||
|
||||
use ratatui::style::Color;
|
||||
|
||||
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 });
|
||||
/// Picks up the card showing `title`.
|
||||
fn pick_up(h: &mut Harness, title: &str) -> Cell {
|
||||
let (col, row) = h.find(title);
|
||||
let at = Cell { col, row };
|
||||
h.mouse(Gesture::Press { at, ctrl: false });
|
||||
at
|
||||
}
|
||||
|
||||
/// Moves the mouse to a cell, holding the card picked up at `from`.
|
||||
fn drag_to(h: &mut Harness, from: Cell, (col, row): (u16, u16)) {
|
||||
h.mouse(Gesture::Drag { from, at: Cell { col, row }, ctrl: false });
|
||||
}
|
||||
|
||||
fn let_go(h: &mut Harness, (col, row): (u16, u16)) {
|
||||
h.mouse(Gesture::Release { at: Cell { col, row }, dragged: true });
|
||||
}
|
||||
|
||||
/// A cell on the bottom half of the card showing `title`: its bottom
|
||||
/// border. Its title's row is on the top half.
|
||||
fn bottom_half(h: &Harness, title: &str) -> (u16, u16) {
|
||||
let (col, row) = h.find(title);
|
||||
(col, row + 1)
|
||||
}
|
||||
|
||||
/// The color a cell is drawn in.
|
||||
fn color_at(h: &Harness, (col, row): (u16, u16)) -> Color {
|
||||
h.terminal.backend().buffer()[(col, row)].fg
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -20,7 +42,9 @@ fn space_vb_shows_the_board() {
|
||||
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 "] {
|
||||
// The cursor's column is thick, as the cursor's box is on the canvas;
|
||||
// ask about compost is dashed, as its style says.
|
||||
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.
|
||||
@@ -28,7 +52,7 @@ fn space_vb_shows_the_board() {
|
||||
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}");
|
||||
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");
|
||||
@@ -44,6 +68,40 @@ fn space_vb_shows_the_board() {
|
||||
assert!(h.screen().contains("┌ board · Garden plans"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cards_and_columns_are_drawn_as_the_canvas_draws_boxes() {
|
||||
let mut h = Harness::new();
|
||||
// Drip line opened: a block with no color of its own, around a card
|
||||
// with a double border.
|
||||
h.type_keys("<Space>vb:find drip<CR>za");
|
||||
let screen = h.screen();
|
||||
assert!(screen.contains("╔══════════════════╗"), "Timer on the tap's double border:\n{screen}");
|
||||
assert!(screen.contains("└─ #todo ─┘"), "Soil's tags in its bottom border:\n{screen}");
|
||||
// The C is one shape with its card: the same line, the same color.
|
||||
h.type_keys("k");
|
||||
let (col, row) = h.find("│ Drip line │");
|
||||
let card = color_at(&h, (col, row));
|
||||
assert_eq!(card, Color::Reset, "a card without a color is in the text's color, as on the canvas");
|
||||
assert_eq!(h.rows()[usize::from(row) + 1].chars().nth(usize::from(col)), Some('├'));
|
||||
for below in 1..=4 {
|
||||
assert_eq!(color_at(&h, (col, row + below)), card, "the bracket, {below} below:\n{screen}");
|
||||
}
|
||||
let (foot, foot_row) = h.find_in("└───────", usize::from(row)..=h.rows().len() - 3).unwrap();
|
||||
assert_eq!((foot, color_at(&h, (foot, foot_row))), (col, card), "the foot");
|
||||
// A folded column says what it hides, in its bottom border.
|
||||
h.type_keys(":find seeds<CR>za");
|
||||
let (_, top) = h.find("Seeds to order");
|
||||
assert!(!h.screen().contains("Tomatoes"), "{}", h.screen());
|
||||
assert!(h.rows()[usize::from(top) + 2].contains(" +2 ━┛"), "{}", h.screen());
|
||||
// A column's title is typed in its top border.
|
||||
h.type_keys("A, soon");
|
||||
let (col, row) = h.find("Seeds to order, soon");
|
||||
assert_eq!(row, top, "in the top border:\n{}", h.screen());
|
||||
assert_eq!(h.text_cursor(), Some((col + 20, top)), "the text cursor after it");
|
||||
h.type_keys("<Esc><Esc>");
|
||||
assert_eq!(h.title(h.node("Seeds to order, soon")), "Seeds to order, soon");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_mode_moves_cards_through_the_tree() {
|
||||
let mut h = Harness::new();
|
||||
@@ -70,15 +128,48 @@ fn move_mode_moves_cards_through_the_tree() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_card_drags_to_where_it_drops() {
|
||||
fn a_dragged_card_moves_as_the_mouse_goes() {
|
||||
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");
|
||||
// Over the bottom half of Tomatoes: after it, before letting go.
|
||||
let soil = pick_up(&mut h, "Soil");
|
||||
let below = bottom_half(&h, "Tomatoes");
|
||||
drag_to(&mut h, soil, below);
|
||||
assert_eq!(h.children("Seeds to order"), ["Tomatoes", "Soil", "Basil"]);
|
||||
// Over its top half: before it.
|
||||
let above = h.find("Tomatoes");
|
||||
drag_to(&mut h, soil, above);
|
||||
assert_eq!(h.children("Seeds to order"), ["Soil", "Tomatoes", "Basil"]);
|
||||
// Over itself, it stays put; letting go leaves it there.
|
||||
let itself = h.find("Soil");
|
||||
drag_to(&mut h, soil, itself);
|
||||
let_go(&mut h, itself);
|
||||
assert_eq!(h.children("Seeds to order"), ["Soil", "Tomatoes", "Basil"]);
|
||||
assert_eq!(h.cursor(), "Soil");
|
||||
h.type_keys("u");
|
||||
assert_eq!(h.children("Garden plans")[0], "Soil", "the whole drag is one undo step");
|
||||
assert_eq!(h.children("Seeds to order"), ["Tomatoes", "Basil"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_dragged_card_goes_into_blocks_out_of_them_and_onto_columns() {
|
||||
let mut h = Harness::new();
|
||||
h.type_keys("<Space>vb");
|
||||
let kitchen = pick_up(&mut h, "» Kitchen");
|
||||
// A block's bottom half: its first card.
|
||||
let watering = bottom_half(&h, "Watering");
|
||||
drag_to(&mut h, kitchen, watering);
|
||||
assert_eq!(h.children("Watering"), ["Kitchen", "Rain barrel", "Drip line"]);
|
||||
// Its foot, under its last card (folded): after the block, out of it.
|
||||
let (col, row) = h.find("Drip line");
|
||||
drag_to(&mut h, kitchen, (col, row + 2));
|
||||
assert_eq!(h.children("Watering"), ["Rain barrel", "Drip line"]);
|
||||
assert_eq!(h.children("Garden plans").last().map(String::as_str), Some("Kitchen"));
|
||||
// A column's title: its first card.
|
||||
let compost = h.find("ask about compost");
|
||||
drag_to(&mut h, kitchen, compost);
|
||||
let_go(&mut h, compost);
|
||||
assert_eq!(h.children("ask about compost"), ["Kitchen"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -24,6 +24,21 @@ pub enum Border {
|
||||
Hidden,
|
||||
}
|
||||
|
||||
impl Border {
|
||||
/// The line a border is drawn with (rounded corners aside), so whatever
|
||||
/// goes on from a box, like a board block's bracket, looks like it.
|
||||
/// `None` for no border.
|
||||
pub fn stroke(self) -> Option<Stroke> {
|
||||
match self {
|
||||
Border::Rounded | Border::Square => Some(Stroke::Solid),
|
||||
Border::Double => Some(Stroke::Double),
|
||||
Border::Thick => Some(Stroke::Thick),
|
||||
Border::Dashed => Some(Stroke::Dashed),
|
||||
Border::Hidden => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What a line looks like along its length.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum Stroke {
|
||||
|
||||
@@ -176,25 +176,6 @@ impl Glyphs {
|
||||
}
|
||||
}
|
||||
|
||||
/// A card's border on the board: thick for the cursor's card and the
|
||||
/// selected ones.
|
||||
pub fn card(self, thick: bool) -> border::Set<'static> {
|
||||
match (self, thick) {
|
||||
(Glyphs::Unicode, true) => border::THICK,
|
||||
(Glyphs::Unicode, false) => border::PLAIN,
|
||||
(Glyphs::Ascii, _) => ASCII_FRAME,
|
||||
}
|
||||
}
|
||||
|
||||
/// A board card's bottom left corner when its block goes on below it.
|
||||
pub fn card_joint(self, thick: bool) -> &'static str {
|
||||
match (self, thick) {
|
||||
(Glyphs::Unicode, true) => "┣",
|
||||
(Glyphs::Unicode, false) => "├",
|
||||
(Glyphs::Ascii, _) => "+",
|
||||
}
|
||||
}
|
||||
|
||||
/// What stands between the parts of the bars.
|
||||
pub fn separator(self) -> &'static str {
|
||||
match self {
|
||||
|
||||
+231
-163
@@ -4,7 +4,7 @@
|
||||
//! wrap the blocks inside them (a "C"):
|
||||
//!
|
||||
//! ```text
|
||||
//! ┌ Garden plans ──────────┐ ┌ Seeds to order ─┐
|
||||
//! ╭ Garden plans ──────────╮ ┌ Seeds to order ─┐
|
||||
//! │ ┌──────┐ │ │ ┌──────────┐ │
|
||||
//! │ │ Soil │ │ │ │ Tomatoes │ │
|
||||
//! │ └──────┘ │ │ └──────────┘ │
|
||||
@@ -15,34 +15,45 @@
|
||||
//! │ │ │ Rain barrel │ │
|
||||
//! │ │ └─────────────┘ │
|
||||
//! │ └─────── │
|
||||
//! └────────────────────────┘
|
||||
//! ╰────────────────────────╯
|
||||
//! ```
|
||||
//!
|
||||
//! A column and a card are drawn as the node's box is on the canvas (the
|
||||
//! user asked for them to match): its border and color, the tags and badge
|
||||
//! in the bottom border, thick for the cursor. The borders go through the
|
||||
//! canvas's line layer, so a block's "C" is in its card's line, joined
|
||||
//! where they meet.
|
||||
//!
|
||||
//! `j` and `k` go through a column in reading order, `h` and `l` to the
|
||||
//! column beside. In move mode (`M`) the card moves instead (the user's
|
||||
//! pick): `h` and `l` take it to the end of the column beside (a column
|
||||
//! itself trades places with it), `j` and `k` past the cards around it,
|
||||
//! into and out of the blocks, and a drag drops it where it lands. Every
|
||||
//! move goes through the tree, so every other view follows.
|
||||
//! into and out of the blocks. A drag moves it live, as the canvas does
|
||||
//! (the user's pick). Every move goes through the tree, so every other view
|
||||
//! follows.
|
||||
|
||||
use ratatui::Frame;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::{Position, Rect};
|
||||
use ratatui::layout::{Position, Rect, Size};
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use super::canvas::boxes::{NodeBox, draft_box};
|
||||
use super::canvas::boxes::{NodeBox, draft_box, footer};
|
||||
use super::canvas::lines::{LineLayer, Paint};
|
||||
use super::canvas::{LineOwner, ScreenPos, ScreenRect};
|
||||
use crate::app::action::{Action, AppAction, Message};
|
||||
use crate::app::connect;
|
||||
use crate::app::mode::{Grab, Mode};
|
||||
use crate::app::popup::MenuFor;
|
||||
use crate::app::text_edit::{EditAt, StartEdit};
|
||||
use crate::app::text_edit::{EditAt, StartEdit, TextEdit};
|
||||
use crate::app::visual::selection;
|
||||
use crate::input::bindings::Context;
|
||||
use crate::input::command::{Command, Motion};
|
||||
use crate::input::engine::Typed;
|
||||
use crate::input::mouse::{Cell, Gesture};
|
||||
use crate::model::{CanvasPos, CanvasRect, CanvasSize, Direction, EditOp, NodeId, Spot, Visible};
|
||||
use crate::model::{
|
||||
Border, CanvasPos, CanvasRect, CanvasSize, Direction, Document, EditOp, Group, Node, NodeId, Spot,
|
||||
Visible,
|
||||
};
|
||||
use crate::ui::text_editor::layout;
|
||||
use crate::view::{View, ViewContext, ViewKind, contexts_for};
|
||||
|
||||
@@ -62,13 +73,18 @@ const SCROLL_OFF: i32 = 2;
|
||||
const SCROLL_COLS: i32 = 6;
|
||||
const WHEEL_ROWS: i32 = 3;
|
||||
|
||||
/// A column: a node without a parent, and where it is on the board.
|
||||
/// A column: a node without a parent, where it is on the board, and how
|
||||
/// it's drawn.
|
||||
#[derive(Debug, Clone)]
|
||||
struct Column {
|
||||
id: NodeId,
|
||||
rect: CanvasRect,
|
||||
/// Its nodes on show in reading order, itself first.
|
||||
order: Vec<NodeId>,
|
||||
/// Its title, on one line in the top border.
|
||||
title: String,
|
||||
tags: String,
|
||||
badge: String,
|
||||
}
|
||||
|
||||
/// A card: a node in a column, and how it's drawn.
|
||||
@@ -83,22 +99,26 @@ struct Card {
|
||||
foot: Option<i32>,
|
||||
}
|
||||
|
||||
/// A title being typed in its card: what the text is, how it wraps, and
|
||||
/// where its cursor is.
|
||||
/// A title being typed in its card or column: what the text is, how it
|
||||
/// wraps, where its cursor is, and the board cell the text starts on.
|
||||
#[derive(Debug, Clone)]
|
||||
struct Editing {
|
||||
id: NodeId,
|
||||
text: String,
|
||||
rows: Vec<std::ops::Range<usize>>,
|
||||
cursor: (usize, usize),
|
||||
at: (i32, i32),
|
||||
}
|
||||
|
||||
/// A drag in progress: the card picked up, and whether it has moved yet.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
/// A card being dragged. It moves through the board as the mouse goes, all
|
||||
/// one undo step, and the columns keep at least the widths they had when
|
||||
/// it was picked up: when a card left a column and it narrowed, the columns
|
||||
/// beside would slide under the mouse, and the card would chase them.
|
||||
#[derive(Debug, Clone)]
|
||||
struct Drag {
|
||||
id: NodeId,
|
||||
from: (i32, i32),
|
||||
moved: bool,
|
||||
group: Group,
|
||||
widths: Vec<(NodeId, i32)>,
|
||||
}
|
||||
|
||||
/// The board view.
|
||||
@@ -132,21 +152,50 @@ impl BoardView {
|
||||
let mut x = 0;
|
||||
let mut bottom = 0;
|
||||
for top in doc.top_level() {
|
||||
let Some(node) = doc.node(top) else { continue };
|
||||
let first_card = self.cards.len();
|
||||
let (end, right) = self.place(ctx, &visible, top, x + 2, 1);
|
||||
let title_width = doc.node(top).map_or(0, |node| node.title().as_str().width() as i32);
|
||||
let width = (right - x + 2).max(title_width + 6).max(MIN_WIDTH);
|
||||
// The column's box, for its title (one line, however long) and
|
||||
// its bottom border: a folded column says what it hides there.
|
||||
let badge = badge(ctx, &visible, top, node);
|
||||
let node_box = match ctx.mode.title_edit().filter(|edit| edit.node == top) {
|
||||
Some(edit) => self.draft(node, edit, badge, u16::MAX, (x + 2, 0)),
|
||||
None => NodeBox::new(node, badge, u16::MAX, ctx.theme.glyphs),
|
||||
};
|
||||
let kept = self.drag.as_ref().and_then(|drag| drag.widths.iter().find(|(id, _)| *id == top));
|
||||
let width = (right - x + 2)
|
||||
.max(i32::from(node_box.rect.size.width) + 2)
|
||||
.max(MIN_WIDTH)
|
||||
.max(kept.map_or(0, |&(_, width)| width));
|
||||
let height = end.max(2) + 1;
|
||||
let mut order = vec![top];
|
||||
order.extend(self.cards[first_card..].iter().map(|card| card.id));
|
||||
let rect = CanvasRect::new(CanvasPos::new(x, 0), size(width, height));
|
||||
self.columns.push(Column { id: top, rect, order });
|
||||
let NodeBox { lines, tags, badge, .. } = node_box;
|
||||
self.columns.push(Column { id: top, rect, order, title: lines.join(" "), tags, badge });
|
||||
bottom = bottom.max(height);
|
||||
x += width + GAP;
|
||||
}
|
||||
self.size = (x, bottom);
|
||||
}
|
||||
|
||||
/// The box of the node whose title is being typed, showing the text as
|
||||
/// typed. Remembers how the text wraps, and that it starts at `at`.
|
||||
fn draft(
|
||||
&mut self,
|
||||
node: &Node,
|
||||
edit: &TextEdit,
|
||||
badge: String,
|
||||
max_width: u16,
|
||||
at: (i32, i32),
|
||||
) -> NodeBox {
|
||||
let text = edit.editor.text();
|
||||
let draft = draft_box(node, text, edit.editor.cursor(), badge, max_width);
|
||||
let (rows, cursor) = (draft.rows, draft.cursor);
|
||||
self.editing = Some(Editing { id: edit.node, text: text.to_string(), rows, cursor, at });
|
||||
draft.node_box
|
||||
}
|
||||
|
||||
/// Lays out the cards of `parent`'s children from (`x`, `y`), each under
|
||||
/// the last. A card with children on show wraps them: they go a little
|
||||
/// further in, and a foot closes the block. Returns the row after the
|
||||
@@ -166,18 +215,9 @@ impl BoardView {
|
||||
if !visible.contains(id) {
|
||||
continue;
|
||||
}
|
||||
let mut badge = ctx.badge(id, node).unwrap_or_default();
|
||||
if let Some(count) = visible.hidden.get(&id) {
|
||||
badge = if badge.is_empty() { format!("+{count}") } else { format!("{badge} +{count}") };
|
||||
}
|
||||
let edit = ctx.mode.title_edit().filter(|edit| edit.node == id);
|
||||
let node_box = match edit {
|
||||
Some(edit) => {
|
||||
let draft = draft_box(node, edit.editor.text(), edit.editor.cursor(), badge, max_width);
|
||||
let text = edit.editor.text().to_string();
|
||||
self.editing = Some(Editing { id, text, rows: draft.rows, cursor: draft.cursor });
|
||||
draft.node_box
|
||||
}
|
||||
let badge = badge(ctx, visible, id, node);
|
||||
let node_box = match ctx.mode.title_edit().filter(|edit| edit.node == id) {
|
||||
Some(edit) => self.draft(node, edit, badge, max_width, (x + 2, y + 1)),
|
||||
None => NodeBox::new(node, badge, max_width, ctx.theme.glyphs),
|
||||
};
|
||||
let rect = CanvasRect::new(CanvasPos::new(x, y), node_box.rect.size);
|
||||
@@ -356,122 +396,119 @@ impl BoardView {
|
||||
Some(vec![Action::edit_in(EditOp::MoveTo { id, spot }, grab.group)])
|
||||
}
|
||||
|
||||
/// Where a dragged card goes, dropped on `target`: into a block, as its
|
||||
/// first card; after another card; at the end of a column.
|
||||
fn drop_spot(&self, target: Target) -> Spot {
|
||||
match target {
|
||||
Target::Card(id) if self.cards.iter().any(|card| card.id == id && card.foot.is_some()) => {
|
||||
Spot::FirstChildOf(id)
|
||||
}
|
||||
Target::Card(id) => Spot::After(id),
|
||||
Target::Column(id) => Spot::LastChildOf(id),
|
||||
/// Where the `dragged` card goes with the mouse on a board cell. It goes
|
||||
/// by the row, in the column the mouse is over: on a card's top half,
|
||||
/// before it; on its bottom half, after it, or into its block as the
|
||||
/// first card; on a block's foot, after the block; on the column's
|
||||
/// title, first; below its cards, last. Nowhere over the card itself or
|
||||
/// its own cards, so a card that's caught up with the mouse stays put.
|
||||
fn drop_spot(&self, doc: &Document, dragged: NodeId, (x, y): (i32, i32)) -> Option<Spot> {
|
||||
let column =
|
||||
self.columns.iter().find(|column| (column.rect.left()..column.rect.right()).contains(&x))?;
|
||||
if y <= column.rect.top() {
|
||||
return Some(Spot::FirstChildOf(column.id));
|
||||
}
|
||||
for card in self.cards.iter().filter(|card| column.order.contains(&card.id)) {
|
||||
let its_own = card.id == dragged || doc.is_inside(card.id, dragged);
|
||||
let rect = card.rect;
|
||||
if (rect.top()..rect.bottom()).contains(&y) {
|
||||
let top_half = (y - rect.top()) * 2 < i32::from(rect.size.height);
|
||||
return (!its_own).then_some(match card.foot {
|
||||
_ if top_half => Spot::Before(card.id),
|
||||
Some(_) => Spot::FirstChildOf(card.id),
|
||||
None => Spot::After(card.id),
|
||||
});
|
||||
}
|
||||
if card.foot == Some(y) {
|
||||
return (!its_own).then_some(Spot::After(card.id));
|
||||
}
|
||||
}
|
||||
Some(Spot::LastChildOf(column.id))
|
||||
}
|
||||
|
||||
/// Where a click lands in the title being typed, if it's on its text.
|
||||
fn text_offset_at(&self, (x, y): (i32, i32)) -> Option<usize> {
|
||||
let editing = self.editing.as_ref()?;
|
||||
let rect = self.rect_of(editing.id)?;
|
||||
let row = usize::try_from(y - rect.top() - 1).ok().filter(|&row| row < editing.rows.len())?;
|
||||
let column = usize::try_from(x - rect.left() - 2).unwrap_or(0);
|
||||
if !(rect.left()..rect.right()).contains(&x) {
|
||||
return None;
|
||||
}
|
||||
let row = usize::try_from(y - editing.at.1).ok().filter(|&row| row < editing.rows.len())?;
|
||||
let column = usize::try_from(x - editing.at.0).unwrap_or(0);
|
||||
Some(layout::offset(&editing.text, &editing.rows, row, column))
|
||||
}
|
||||
|
||||
/// Draws a column: its frame, with its node's title in the top border.
|
||||
fn draw_column(&self, buf: &mut Buffer, column: &Column, ctx: &ViewContext<'_>, picked: &[NodeId]) {
|
||||
let Some(node) = ctx.doc.node(column.id) else { return };
|
||||
let style = ctx.node_style(column.id, node);
|
||||
let here = column.id == ctx.cursor;
|
||||
let color = match (here, picked.contains(&column.id)) {
|
||||
(true, _) => ctx.theme.accent,
|
||||
(false, true) => ctx.theme.visual,
|
||||
_ if style.color == Color::Reset => ctx.theme.muted,
|
||||
_ => style.color,
|
||||
/// Adds a node's border to the line layer, the way the canvas draws its
|
||||
/// box: the cursor's thick in the accent color (the typing color while
|
||||
/// its title is typed), the selected ones thick in the visual color, the
|
||||
/// others as their style says. A block's card goes on down its left past
|
||||
/// its cards and closes with a foot, in the same line and color: the
|
||||
/// "C" is one shape with its card.
|
||||
fn add_border(
|
||||
&self,
|
||||
lines: &mut LineLayer,
|
||||
(id, rect, foot): (NodeId, CanvasRect, Option<i32>),
|
||||
ctx: &ViewContext<'_>,
|
||||
picked: &[NodeId],
|
||||
) {
|
||||
let Some(node) = ctx.doc.node(id) else { return };
|
||||
let typing = ctx.mode.title_edit().is_some_and(|edit| edit.editor.is_typing());
|
||||
let (border, color) = match (id == ctx.cursor, picked.contains(&id)) {
|
||||
(true, _) => (Border::Thick, if typing { ctx.theme.insert } else { ctx.theme.accent }),
|
||||
(false, true) => (Border::Thick, ctx.theme.visual),
|
||||
(false, false) => {
|
||||
let style = ctx.node_style(id, node);
|
||||
(style.border, style.color)
|
||||
}
|
||||
};
|
||||
let set = ctx.theme.glyphs.frame(column.id == ctx.doc.root());
|
||||
let rect = column.rect;
|
||||
let border = Style::new().fg(color);
|
||||
let (left, right, top, bottom) = (rect.left(), rect.right() - 1, rect.top(), rect.bottom() - 1);
|
||||
for x in left + 1..right {
|
||||
self.put(buf, x, top, set.horizontal_top, border);
|
||||
self.put(buf, x, bottom, set.horizontal_bottom, border);
|
||||
let paint = Paint { color, owner: LineOwner::Border(id) };
|
||||
let (left, top) = (rect.left() - self.scroll.0, rect.top() - self.scroll.1);
|
||||
let (width, height) = (i32::from(rect.size.width), i32::from(rect.size.height));
|
||||
lines.add_border(ScreenRect::new(left, top, width, height), border, paint, |_| true);
|
||||
let (Some(foot), Some(stroke)) = (foot, border.stroke()) else { return };
|
||||
let corner = ScreenPos::new(left, top + height - 1);
|
||||
let bend = ScreenPos::new(left, foot - self.scroll.1);
|
||||
let tip = ScreenPos::new(left + FOOT - 1, bend.row);
|
||||
lines.add_path(&[corner, bend, tip], stroke, paint, |_| false);
|
||||
// The foot runs right to its end, rather than tapering off.
|
||||
lines.add_arm(tip, Direction::Right, stroke, paint);
|
||||
if border == Border::Rounded {
|
||||
lines.round(bend);
|
||||
}
|
||||
for y in top + 1..bottom {
|
||||
self.put(buf, left, y, set.vertical_left, border);
|
||||
self.put(buf, right, y, set.vertical_right, border);
|
||||
}
|
||||
self.put(buf, left, top, set.top_left, border);
|
||||
self.put(buf, right, top, set.top_right, border);
|
||||
self.put(buf, left, bottom, set.bottom_left, border);
|
||||
self.put(buf, right, bottom, set.bottom_right, border);
|
||||
let mut title = Style::new().fg(color).add_modifier(Modifier::BOLD);
|
||||
if here {
|
||||
title = Style::new().fg(Color::Black).bg(ctx.theme.accent).add_modifier(Modifier::BOLD);
|
||||
}
|
||||
let text = format!(" {} ", node.title());
|
||||
let room = usize::try_from(right - left - 2).unwrap_or(0);
|
||||
self.put_text(buf, left + 1, top, &truncated(&text, room), title);
|
||||
}
|
||||
|
||||
/// Draws a card, and the bracket and foot of its block.
|
||||
fn draw_card(&self, buf: &mut Buffer, card: &Card, ctx: &ViewContext<'_>, picked: &[NodeId]) {
|
||||
let Some(node) = ctx.doc.node(card.id) else { return };
|
||||
let style = ctx.node_style(card.id, node);
|
||||
let (here, chosen) = (card.id == ctx.cursor, picked.contains(&card.id));
|
||||
let typing = ctx.mode.title_edit().is_some_and(|edit| edit.editor.is_typing());
|
||||
let color = match (here, chosen) {
|
||||
(true, _) if typing => ctx.theme.insert,
|
||||
(true, _) => ctx.theme.accent,
|
||||
(false, true) => ctx.theme.visual,
|
||||
_ => style.color,
|
||||
};
|
||||
let glyphs = ctx.theme.glyphs;
|
||||
let set = glyphs.card(here || chosen);
|
||||
let border = Style::new().fg(color);
|
||||
let rect = card.rect;
|
||||
let (left, right, top, bottom) = (rect.left(), rect.right() - 1, rect.top(), rect.bottom() - 1);
|
||||
for x in left + 1..right {
|
||||
self.put(buf, x, top, set.horizontal_top, border);
|
||||
self.put(buf, x, bottom, set.horizontal_bottom, border);
|
||||
}
|
||||
for y in top + 1..bottom {
|
||||
self.put(buf, left, y, set.vertical_left, border);
|
||||
self.put(buf, right, y, set.vertical_right, border);
|
||||
}
|
||||
self.put(buf, left, top, set.top_left, border);
|
||||
self.put(buf, right, top, set.top_right, border);
|
||||
let corner = if card.foot.is_some() { glyphs.card_joint(here || chosen) } else { set.bottom_left };
|
||||
self.put(buf, left, bottom, corner, border);
|
||||
self.put(buf, right, bottom, set.bottom_right, border);
|
||||
// The tags on the left of the bottom border, the badge on the right.
|
||||
/// Writes what's in a node's box, as the canvas does: its title (in the
|
||||
/// top border, for a column), bold under the cursor and quiet when its
|
||||
/// style fades it; the tags in its color on the left of the bottom
|
||||
/// border; the badge on the right.
|
||||
fn put_box_text(&self, buf: &mut Buffer, content: BoxText<'_>, ctx: &ViewContext<'_>) {
|
||||
let Some(node) = ctx.doc.node(content.id) else { return };
|
||||
let style = ctx.node_style(content.id, node);
|
||||
let muted = Style::new().fg(ctx.theme.muted);
|
||||
let width = right - left - 1;
|
||||
if !card.tags.is_empty() {
|
||||
let tags = truncated(&format!(" {} ", card.tags), usize::try_from(width - 2).unwrap_or(0));
|
||||
self.put_text(buf, left + 2, bottom, &tags, muted);
|
||||
}
|
||||
if !card.badge.is_empty() {
|
||||
let badge = format!(" {} ", card.badge);
|
||||
self.put_text(buf, right - 1 - badge.width() as i32, bottom, &badge, muted);
|
||||
}
|
||||
let title = if style.faded { muted } else { Style::new() };
|
||||
let title = if here { title.add_modifier(Modifier::BOLD) } else { title };
|
||||
for (line, y) in card.lines.iter().zip(top + 1..) {
|
||||
self.put_text(buf, left + 2, y, line, title);
|
||||
}
|
||||
// A block: the bracket down its left, closed by a foot.
|
||||
if let Some(foot) = card.foot {
|
||||
let line =
|
||||
Style::new().fg(if style.color == Color::Reset { ctx.theme.muted } else { style.color });
|
||||
let plain = glyphs.card(false);
|
||||
for y in rect.bottom()..foot {
|
||||
self.put(buf, left, y, plain.vertical_left, line);
|
||||
}
|
||||
self.put(buf, left, foot, plain.bottom_left, line);
|
||||
for x in left + 1..left + FOOT {
|
||||
self.put(buf, x, foot, plain.horizontal_bottom, line);
|
||||
let title = match (content.id == ctx.cursor, style.faded) {
|
||||
(true, _) => Style::new().add_modifier(Modifier::BOLD),
|
||||
(false, true) => muted,
|
||||
(false, false) => Style::new(),
|
||||
};
|
||||
let rect = content.rect;
|
||||
if content.in_border {
|
||||
let text = format!(" {} ", content.lines.join(" "));
|
||||
self.put_text(buf, rect.left() + 1, rect.top(), &text, title);
|
||||
} else {
|
||||
for (line, y) in content.lines.iter().zip(rect.top() + 1..) {
|
||||
self.put_text(buf, rect.left() + 2, y, line, title);
|
||||
}
|
||||
}
|
||||
let bottom = rect.bottom() - 1;
|
||||
let footer =
|
||||
footer(rect.left(), rect.right(), content.tags, content.badge, ctx.theme.glyphs.ellipsis());
|
||||
if let Some((x, text)) = footer.badge {
|
||||
self.put_text(buf, x, bottom, &text, muted);
|
||||
}
|
||||
if let Some((x, text)) = footer.tags {
|
||||
let color = if style.color == Color::Reset { ctx.theme.muted } else { style.color };
|
||||
self.put_text(buf, x, bottom, &text, Style::new().fg(color));
|
||||
}
|
||||
}
|
||||
|
||||
/// Puts a symbol on a board cell, if it's on screen.
|
||||
@@ -530,20 +567,37 @@ fn size(width: i32, height: i32) -> CanvasSize {
|
||||
)
|
||||
}
|
||||
|
||||
/// Text cut to `width` columns, with `…` when it's cut.
|
||||
fn truncated(text: &str, width: usize) -> String {
|
||||
if text.width() <= width {
|
||||
return text.to_string();
|
||||
/// What's written in a node's box on the board.
|
||||
struct BoxText<'a> {
|
||||
id: NodeId,
|
||||
rect: CanvasRect,
|
||||
lines: &'a [String],
|
||||
/// A column's title is in its top border; a card's is inside it.
|
||||
in_border: bool,
|
||||
tags: &'a str,
|
||||
badge: &'a str,
|
||||
}
|
||||
|
||||
/// What a node's box says in its bottom right corner, as on the canvas:
|
||||
/// its own badge, then how many nodes its fold hides.
|
||||
fn badge(ctx: &ViewContext<'_>, visible: &Visible, id: NodeId, node: &Node) -> String {
|
||||
let badge = ctx.badge(id, node).unwrap_or_default();
|
||||
match visible.hidden.get(&id) {
|
||||
Some(count) if badge.is_empty() => format!("+{count}"),
|
||||
Some(count) => format!("{badge} +{count}"),
|
||||
None => badge,
|
||||
}
|
||||
let mut out = String::new();
|
||||
for c in text.chars() {
|
||||
if out.width() + unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) + 1 > width {
|
||||
break;
|
||||
}
|
||||
out.push(c);
|
||||
}
|
||||
|
||||
/// Whether moving `id` to `spot` would leave it where it is.
|
||||
fn stays(doc: &Document, id: NodeId, spot: Spot) -> bool {
|
||||
match spot {
|
||||
Spot::Before(next) => next == id || doc.next_sibling(id) == Some(next),
|
||||
Spot::After(previous) => previous == id || doc.previous_sibling(id) == Some(previous),
|
||||
Spot::FirstChildOf(parent) => doc.children(parent).first() == Some(&id),
|
||||
Spot::LastChildOf(parent) => doc.children(parent).last() == Some(&id),
|
||||
Spot::Loose => false,
|
||||
}
|
||||
out.push('…');
|
||||
out
|
||||
}
|
||||
|
||||
impl View for BoardView {
|
||||
@@ -573,19 +627,31 @@ impl View for BoardView {
|
||||
|
||||
fn render(&self, frame: &mut Frame, ctx: &ViewContext<'_>) {
|
||||
let picked = BoardView::picked(ctx);
|
||||
let buf = frame.buffer_mut();
|
||||
let mut lines = LineLayer::new(Size::new(self.area.width, self.area.height));
|
||||
for column in &self.columns {
|
||||
self.draw_column(buf, column, ctx, &picked);
|
||||
self.add_border(&mut lines, (column.id, column.rect, None), ctx, &picked);
|
||||
}
|
||||
for card in &self.cards {
|
||||
self.draw_card(buf, card, ctx, &picked);
|
||||
self.add_border(&mut lines, (card.id, card.rect, card.foot), ctx, &picked);
|
||||
}
|
||||
if let Some(editing) = &self.editing
|
||||
&& let Some(rect) = self.rect_of(editing.id)
|
||||
{
|
||||
let buf = frame.buffer_mut();
|
||||
for (at, glyph, color) in lines.cells(ctx.theme.glyphs) {
|
||||
let (x, y) = (at.col + self.scroll.0, at.row + self.scroll.1);
|
||||
self.put(buf, x, y, glyph.encode_utf8(&mut [0; 4]), Style::new().fg(color));
|
||||
}
|
||||
for column in &self.columns {
|
||||
let (lines, tags, badge) = (std::slice::from_ref(&column.title), &column.tags, &column.badge);
|
||||
let (id, rect) = (column.id, column.rect);
|
||||
self.put_box_text(buf, BoxText { id, rect, lines, in_border: true, tags, badge }, ctx);
|
||||
}
|
||||
for card in &self.cards {
|
||||
let (id, rect, lines, tags, badge) =
|
||||
(card.id, card.rect, &card.lines[..], &card.tags, &card.badge);
|
||||
self.put_box_text(buf, BoxText { id, rect, lines, in_border: false, tags, badge }, ctx);
|
||||
}
|
||||
if let Some(editing) = &self.editing {
|
||||
let (row, col) = editing.cursor;
|
||||
let at = self.screen(rect.left() + 2 + col as i32, rect.top() + 1 + row as i32);
|
||||
if let Some((col, row)) = at {
|
||||
if let Some((col, row)) = self.screen(editing.at.0 + col as i32, editing.at.1 + row as i32) {
|
||||
frame.set_cursor_position(Position::new(col, row));
|
||||
}
|
||||
}
|
||||
@@ -645,26 +711,28 @@ impl View for BoardView {
|
||||
}
|
||||
// Cards can be dragged; in connect mode a click only aims.
|
||||
if matches!(target, Target::Card(_)) && !matches!(ctx.mode, Mode::Connect(_)) {
|
||||
self.drag = Some(Drag { id, from: at, moved: false });
|
||||
let widths = self
|
||||
.columns
|
||||
.iter()
|
||||
.map(|column| (column.id, i32::from(column.rect.size.width)))
|
||||
.collect();
|
||||
self.drag = Some(Drag { id, group: Group::new(), widths });
|
||||
}
|
||||
actions.push(Action::cursor(id));
|
||||
actions
|
||||
}
|
||||
Gesture::Drag { at, .. } => {
|
||||
let here = self.local(at);
|
||||
if let (Some(drag), Some(here)) = (&mut self.drag, here) {
|
||||
drag.moved |= here != drag.from;
|
||||
let (Some(drag), Some(at)) = (&self.drag, self.local(at)) else { return Vec::new() };
|
||||
match self.drop_spot(ctx.doc, drag.id, at) {
|
||||
Some(spot) if !stays(ctx.doc, drag.id, spot) => {
|
||||
vec![Action::edit_in(EditOp::MoveTo { id: drag.id, spot }, drag.group)]
|
||||
}
|
||||
_ => Vec::new(),
|
||||
}
|
||||
Vec::new()
|
||||
}
|
||||
Gesture::Release { at, .. } => {
|
||||
let Some(drag) = self.drag.take().filter(|drag| drag.moved) else { return Vec::new() };
|
||||
let Some(target) = self.local(at).and_then(|at| self.at(at)) else { return Vec::new() };
|
||||
if target == Target::Card(drag.id) {
|
||||
return Vec::new();
|
||||
}
|
||||
let spot = self.drop_spot(target);
|
||||
vec![Action::edit(EditOp::MoveTo { id: drag.id, spot }), Action::cursor(drag.id)]
|
||||
Gesture::Release { .. } => {
|
||||
self.drag = None;
|
||||
Vec::new()
|
||||
}
|
||||
Gesture::DoublePress { at } => {
|
||||
let Some(at) = self.local(at) else { return Vec::new() };
|
||||
|
||||
@@ -89,6 +89,35 @@ pub fn box_rects(doc: &Document, max_width: u16, glyphs: Glyphs) -> BTreeMap<Nod
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// What a box writes into its bottom border, each part with the column it
|
||||
/// starts at: `─ #todo ─ +3 ─`.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct Footer {
|
||||
pub tags: Option<(i32, String)>,
|
||||
pub badge: Option<(i32, String)>,
|
||||
}
|
||||
|
||||
/// Where the tags and the badge go in the bottom border of a box from column
|
||||
/// `left` up to `right` (not included), as the canvas and the board both
|
||||
/// draw them: the badge ends two cells in from the right corner, and the
|
||||
/// tags start two in from the left, cut short to end before the badge.
|
||||
pub fn footer(left: i32, right: i32, tags: &str, badge: &str, ellipsis: char) -> Footer {
|
||||
let mut footer = Footer::default();
|
||||
let badge_start = if badge.is_empty() {
|
||||
right - 1
|
||||
} else {
|
||||
let text = format!(" {badge} ");
|
||||
let start = right - 2 - text.width() as i32;
|
||||
footer.badge = Some((start, text));
|
||||
start - 1
|
||||
};
|
||||
if !tags.is_empty() {
|
||||
let room = usize::try_from(badge_start - (left + 2) - 2).unwrap_or(0);
|
||||
footer.tags = Some((left + 2, format!(" {} ", truncate(tags, room, ellipsis))));
|
||||
}
|
||||
footer
|
||||
}
|
||||
|
||||
/// How many cells the bottom border needs for the tags and the badge,
|
||||
/// between its corners: `─ #todo ─ +3 ─`.
|
||||
fn footer_width(tags: &str, badge: &str) -> usize {
|
||||
|
||||
+11
-11
@@ -74,13 +74,7 @@ impl LineLayer {
|
||||
paint: Paint,
|
||||
shown: impl Fn(ScreenPos) -> bool,
|
||||
) {
|
||||
let stroke = match border {
|
||||
Border::Rounded | Border::Square => Stroke::Solid,
|
||||
Border::Double => Stroke::Double,
|
||||
Border::Thick => Stroke::Thick,
|
||||
Border::Dashed => Stroke::Dashed,
|
||||
Border::Hidden => return,
|
||||
};
|
||||
let Some(stroke) = border.stroke() else { return };
|
||||
if rect.width < 2 || rect.height < 2 {
|
||||
return;
|
||||
}
|
||||
@@ -95,10 +89,8 @@ impl LineLayer {
|
||||
if shown(at) {
|
||||
self.add_arm(at, one, stroke, paint);
|
||||
self.add_arm(at, other, stroke, paint);
|
||||
if border == Border::Rounded
|
||||
&& let Some(cell) = self.cell_mut(at)
|
||||
{
|
||||
cell.rounded = true;
|
||||
if border == Border::Rounded {
|
||||
self.round(at);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -174,6 +166,14 @@ impl LineLayer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws the light corner on a cell rounded, as a rounded box's corners
|
||||
/// are.
|
||||
pub fn round(&mut self, at: ScreenPos) {
|
||||
if let Some(cell) = self.cell_mut(at) {
|
||||
cell.rounded = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Every cell a line runs through, with its character and color.
|
||||
pub fn cells(&self, glyphs: Glyphs) -> impl Iterator<Item = (ScreenPos, char, Color)> + '_ {
|
||||
self.cells.iter().enumerate().filter(|(_, cell)| !cell.arms.is_empty()).map(move |(index, cell)| {
|
||||
|
||||
@@ -5,9 +5,9 @@ use ratatui::buffer::{Buffer, Cell};
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::widgets::Widget;
|
||||
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
|
||||
use super::boxes::truncate;
|
||||
use super::boxes::footer;
|
||||
use super::camera::ScreenPos;
|
||||
use super::scene::{Scene, SceneNode, Shape};
|
||||
|
||||
@@ -73,19 +73,13 @@ impl Scene {
|
||||
|
||||
// The bottom border: ─ #tags ─ … ─ badge ─
|
||||
let bottom = rect.bottom() - 1;
|
||||
let badge_start = if badge.is_empty() {
|
||||
rect.right() - 1
|
||||
} else {
|
||||
let text = format!(" {badge} ");
|
||||
let start = rect.right() - 2 - text.width() as i32;
|
||||
painter.text(ScreenPos::new(start, bottom), &text, Style::new().fg(self.muted), &mine);
|
||||
start - 1
|
||||
};
|
||||
if !tags.is_empty() {
|
||||
let room = usize::try_from(badge_start - (rect.left() + 2) - 2).unwrap_or(0);
|
||||
let text = format!(" {} ", truncate(tags, room, self.glyphs.ellipsis()));
|
||||
let footer = footer(rect.left(), rect.right(), tags, badge, self.glyphs.ellipsis());
|
||||
if let Some((col, text)) = footer.badge {
|
||||
painter.text(ScreenPos::new(col, bottom), &text, Style::new().fg(self.muted), &mine);
|
||||
}
|
||||
if let Some((col, text)) = footer.tags {
|
||||
let color = if node.color == Color::Reset { self.muted } else { node.color };
|
||||
painter.text(ScreenPos::new(rect.left() + 2, bottom), &text, Style::new().fg(color), &mine);
|
||||
painter.text(ScreenPos::new(col, bottom), &text, Style::new().fg(color), &mine);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user