Make the Rust implementation the only one: crate at the root, Go tree removed
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
//! A scrollable list with a cursor, shared by the list-style views.
|
||||
|
||||
use super::canvas::{rgb, Canvas, Color, Style, BOLD};
|
||||
use super::model::{Cmd, Hit, HitAct, Model};
|
||||
use super::{pad_left, trunc, trunc_left};
|
||||
use crate::inventory::Tree;
|
||||
use crate::textutil;
|
||||
use crate::treemap::Rect;
|
||||
|
||||
/// Describes one table column.
|
||||
pub(crate) struct Column {
|
||||
pub title: &'static str,
|
||||
pub width: i32, // 0 = flexible (takes the remaining space)
|
||||
pub right: bool,
|
||||
pub clip_left: bool, // truncate from the left (paths: the tail matters)
|
||||
}
|
||||
|
||||
pub(crate) const fn col(title: &'static str, width: i32, right: bool, clip_left: bool) -> Column {
|
||||
Column { title, width, right, clip_left }
|
||||
}
|
||||
|
||||
/// One rendered row: cell strings plus optional per-cell styles.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct TableRow {
|
||||
pub cells: Vec<String>,
|
||||
pub styles: Vec<Option<Style>>, // None = default
|
||||
pub bar: f64, // 0..1 for an optional bar column (-1 = none)
|
||||
pub bar_col: Color,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Table {
|
||||
pub cursor: i32,
|
||||
pub offset: i32,
|
||||
pub height: i32, // rows visible in the last paint
|
||||
}
|
||||
|
||||
impl Table {
|
||||
/// Moves the cursor three rows for a mouse wheel step.
|
||||
pub fn scroll(&mut self, up: bool, n: usize) {
|
||||
self.mv(if up { -3 } else { 3 }, n);
|
||||
}
|
||||
|
||||
fn mv(&mut self, d: i32, n: usize) {
|
||||
if n == 0 {
|
||||
self.cursor = 0;
|
||||
return;
|
||||
}
|
||||
self.cursor = (self.cursor + d).max(0).min(n as i32 - 1);
|
||||
}
|
||||
|
||||
/// Handles navigation keys common to all tables.
|
||||
pub fn key(&mut self, k: &str, n: usize) -> bool {
|
||||
let page = (self.height - 1).max(1);
|
||||
match k {
|
||||
"up" | "k" => self.mv(-1, n),
|
||||
"down" | "j" => self.mv(1, n),
|
||||
"pgup" | "ctrl+b" => self.mv(-page, n),
|
||||
"pgdown" | "ctrl+f" => self.mv(page, n),
|
||||
"end" | "G" => self.mv(n as i32, n),
|
||||
"g" => self.cursor = 0,
|
||||
_ => return false,
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// The cursor as an index (it is never negative).
|
||||
pub fn at(&self) -> usize {
|
||||
self.cursor.max(0) as usize
|
||||
}
|
||||
|
||||
pub fn paint(&mut self, m: &mut Model, cv: &mut Canvas, r: Rect, cols: &[Column], rows: &[TableRow], title: &str) {
|
||||
let t = m.theme.clone();
|
||||
cv.fill(r, " ", t.base());
|
||||
if r.h < 3 {
|
||||
return;
|
||||
}
|
||||
let mut y = r.y;
|
||||
if !title.is_empty() {
|
||||
cv.text(r.x + 1, y, title, r.w - 2, Style::new(t.accent, t.bg).with(BOLD));
|
||||
y += 1;
|
||||
}
|
||||
// Resolve flexible width.
|
||||
let fixed: i32 = cols.iter().map(|c| c.width + 1).sum();
|
||||
let flex = (r.w - 2 - fixed).max(8);
|
||||
let width = |c: &Column| if c.width == 0 { flex } else { c.width };
|
||||
let mut x = r.x + 1;
|
||||
let hdr = Style::new(t.muted, t.bg).with(BOLD);
|
||||
for c in cols {
|
||||
let w = width(c);
|
||||
let s = if c.right { pad_left(c.title, w) } else { c.title.to_string() };
|
||||
cv.text(x, y, &s, w, hdr);
|
||||
x += w + 1;
|
||||
}
|
||||
y += 1;
|
||||
cv.hline(r.x + 1, y, r.w - 2, "─", Style::new(t.faint, t.bg));
|
||||
y += 1;
|
||||
self.height = r.y + r.h - y;
|
||||
let n = rows.len() as i32;
|
||||
if self.cursor >= n {
|
||||
self.cursor = (n - 1).max(0);
|
||||
}
|
||||
if self.cursor < self.offset {
|
||||
self.offset = self.cursor;
|
||||
}
|
||||
if self.cursor >= self.offset + self.height {
|
||||
self.offset = self.cursor - self.height + 1;
|
||||
}
|
||||
self.offset = 0.max(self.offset.min(n - self.height));
|
||||
let mut i = self.offset;
|
||||
while i < n && y < r.y + r.h {
|
||||
let row = &rows[i as usize];
|
||||
let sel = i == self.cursor;
|
||||
let mut base = t.base();
|
||||
if sel {
|
||||
base = Style::new(t.text_on(t.sel), t.sel).with(BOLD | m.mono_rev());
|
||||
cv.fill(Rect { x: r.x, y, w: r.w, h: 1 }, " ", base);
|
||||
}
|
||||
let mut x = r.x + 1;
|
||||
for (ci, c) in cols.iter().enumerate() {
|
||||
let w = width(c);
|
||||
let s = row.cells.get(ci).map_or("", |s| s.as_str());
|
||||
let st = match row.styles.get(ci) {
|
||||
Some(Some(st)) if !sel => *st,
|
||||
_ => base,
|
||||
};
|
||||
if c.title.is_empty() && row.bar >= 0.0 {
|
||||
m.paint_bar(cv, x, y, w, row.bar, row.bar_col, sel);
|
||||
} else if c.right {
|
||||
cv.text(x, y, &pad_left(s, w), w, st);
|
||||
} else if c.clip_left {
|
||||
cv.text(x, y, &trunc_left(s, w), w, st);
|
||||
} else {
|
||||
cv.text(x, y, &trunc(s, w), w, st);
|
||||
}
|
||||
x += w + 1;
|
||||
}
|
||||
m.hits.push(Hit { r: Rect { x: r.x, y, w: r.w, h: 1 }, act: HitAct::Row(i as usize) });
|
||||
y += 1;
|
||||
i += 1;
|
||||
}
|
||||
if rows.is_empty() {
|
||||
cv.text_centered(r.x, r.y + r.h / 2, r.w, "Nothing to show", t.muted());
|
||||
}
|
||||
if n > self.height && self.height > 0 {
|
||||
let pos = format!("{}/{}", textutil::count(self.cursor as i64 + 1), textutil::count(n as i64));
|
||||
cv.text_right(r.x, r.y, r.w - 1, &pos, t.muted());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Model {
|
||||
/// Draws a proportional bar with eighth-block precision.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn paint_bar(&self, cv: &mut Canvas, x: i32, y: i32, w: i32, frac: f64, col: Color, sel: bool) {
|
||||
let t = &self.theme;
|
||||
let col = if col.is_default() { t.accent } else { col };
|
||||
let bg = if sel { t.sel.mix(rgb(0, 0, 0), 0.25) } else { t.bg.mix(t.fg, 0.08) };
|
||||
let mut eighths = (frac * (w * 8) as f64 + 0.5) as i64;
|
||||
const PARTS: [&str; 8] = ["", "▏", "▎", "▍", "▌", "▋", "▊", "▉"];
|
||||
for i in 0..w {
|
||||
let mut ch = if eighths >= 8 {
|
||||
"█"
|
||||
} else if eighths > 0 {
|
||||
PARTS[eighths as usize]
|
||||
} else {
|
||||
" "
|
||||
};
|
||||
if t.ascii {
|
||||
ch = if eighths >= 4 { "#" } else { " " };
|
||||
}
|
||||
eighths -= 8;
|
||||
let st = if t.mono { Style::default() } else { Style::new(col, bg) };
|
||||
cv.text(x + i, y, ch, 1, st);
|
||||
}
|
||||
}
|
||||
|
||||
/// Lets mouse actions reuse key handling (lock already held).
|
||||
pub fn handle_key_name(&mut self, tr: &Tree, k: &str) -> Cmd {
|
||||
let (handled, cmd) = self.view_key(tr, k);
|
||||
if handled {
|
||||
return cmd;
|
||||
}
|
||||
match k {
|
||||
"enter" => self.inspect_selection(tr),
|
||||
"space" => self.zoom_into_selection(tr),
|
||||
_ => Cmd::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user