HumanInputRequest asks a question with answers to pick and a line to write one; CostBreakdown totals a session and splits it by part. InterruptButton, TokenUsageChart, TraceViewer and EvalResultTable point to chat::stop_button, a stacked BarChart, TimelineProfiler and DataTable. UsageBar shows amounts as its owner writes them. MemoryPanel and HumanInputRequest share agent::line::send_line; the chapter's bool callbacks take forms::OnFlag. DataTable scrolls columns wider than its box sideways, pinned or not, and leaves a plain wheel to the page; sortable headers are Tab stops. TimelineProfiler fits its ruler to its lane, its spans are Tab stops, and a picked span takes accent.
495 lines
20 KiB
Rust
495 lines
20 KiB
Rust
use std::{
|
||
collections::{HashMap, HashSet},
|
||
rc::Rc,
|
||
};
|
||
|
||
use gpui::{
|
||
AnyElement, App, Div, ElementId, Entity, FontWeight, InteractiveElement, IntoElement,
|
||
ParentElement, Pixels, RenderOnce, SharedString, StatefulInteractiveElement, StyleRefinement,
|
||
Styled, UniformListScrollHandle, Window, div, prelude::*, transparent_black, uniform_list,
|
||
};
|
||
|
||
use super::{
|
||
Aggregate, Cell, Column, FilterRule,
|
||
body::{Body, Detail, Line, OnEdit, Select, sideways, sized},
|
||
header::header,
|
||
model::{figure, filtered, page, range},
|
||
rules::{groups, kept, sorted_by},
|
||
};
|
||
use crate::{
|
||
forms::{CheckState, Editing, check_mark},
|
||
layout::seeded::use_seeded,
|
||
lists::{Pick, picked},
|
||
navigation::Pagination,
|
||
theme::{ActiveTheme, ControlSize, Density, TextSize},
|
||
typography::{Caption, format},
|
||
};
|
||
|
||
/// One row: its key, which a selection names it by, and a cell for each column.
|
||
#[derive(Clone, Debug, PartialEq)]
|
||
pub struct Row {
|
||
pub(crate) key: SharedString,
|
||
pub(crate) cells: Vec<Cell>,
|
||
}
|
||
|
||
impl Row {
|
||
pub fn new(key: impl Into<SharedString>, cells: impl IntoIterator<Item = Cell>) -> Self {
|
||
Self {
|
||
key: key.into(),
|
||
cells: cells.into_iter().collect(),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// A table's own state: the page, a Shift range's start, the scroll, column widths, edges and order, opened rows, folded groups, and the cell being edited.
|
||
#[derive(Default)]
|
||
pub(crate) struct View {
|
||
pub page: usize,
|
||
pub anchor: usize,
|
||
pub scroll: UniformListScrollHandle,
|
||
pub widths: HashMap<SharedString, Pixels>,
|
||
pub edges: HashMap<SharedString, Pixels>,
|
||
pub narrowest: Pixels,
|
||
pub order: Vec<SharedString>,
|
||
pub open: HashSet<SharedString>,
|
||
pub folded: HashSet<SharedString>,
|
||
pub editing: Option<(SharedString, SharedString)>,
|
||
}
|
||
|
||
pub(super) type OnSelect = Rc<dyn Fn(&[SharedString], &mut Window, &mut App)>;
|
||
|
||
/// Rows under headers. A press on a header sorts, rising then falling then off; Shift adds it to the sort. A header drags to move its column and its edge drags to resize it. A query and filter rules keep rows; pages split long results, or a long table draws only the rows in view. Rows can gather into folding groups, open a detail, or edit a cell in place. With a selection, a box leads each row. Footers show figures; tinted columns show where each number sits.
|
||
#[derive(IntoElement)]
|
||
pub struct DataTable {
|
||
pub(super) id: ElementId,
|
||
pub(super) base: Div,
|
||
pub(super) columns: Vec<Column>,
|
||
pub(super) rows: Rc<Vec<Row>>,
|
||
pub(super) query: SharedString,
|
||
pub(super) filters: Vec<FilterRule>,
|
||
pub(super) any: bool,
|
||
pub(super) sorts: Vec<(SharedString, bool)>,
|
||
pub(super) hidden: Vec<SharedString>,
|
||
pub(super) page_size: Option<usize>,
|
||
pub(super) virtualized: bool,
|
||
pub(super) selected: Option<Vec<SharedString>>,
|
||
pub(super) on_select: Option<OnSelect>,
|
||
pub(super) detail: Option<Detail>,
|
||
pub(super) group_by: Option<SharedString>,
|
||
pub(super) on_edit: Option<OnEdit>,
|
||
pub(super) density: Density,
|
||
}
|
||
|
||
impl Styled for DataTable {
|
||
fn style(&mut self) -> &mut StyleRefinement {
|
||
self.base.style()
|
||
}
|
||
}
|
||
|
||
impl RenderOnce for DataTable {
|
||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||
let id = self.id.clone();
|
||
let count = self.columns.len();
|
||
for row in self.rows.iter() {
|
||
assert_eq!(
|
||
row.cells.len(),
|
||
count,
|
||
"row {} needs a cell per column",
|
||
row.key
|
||
);
|
||
}
|
||
assert!(
|
||
!(self.detail.is_some() && self.virtualized),
|
||
"a virtualized table draws rows of one height, so rows cannot open"
|
||
);
|
||
assert!(
|
||
!(self.group_by.is_some() && self.page_size.is_some()),
|
||
"a grouped table is not paged"
|
||
);
|
||
let pinned_any = self.columns.iter().any(|column| column.pinned);
|
||
assert!(
|
||
!(pinned_any && (self.virtualized || self.detail.is_some())),
|
||
"pinned columns need rows of one height in one list"
|
||
);
|
||
assert!(
|
||
self.columns
|
||
.iter()
|
||
.all(|column| !column.pinned || column.width.is_some()),
|
||
"a pinned column needs a width"
|
||
);
|
||
let view: Entity<View> =
|
||
window.use_keyed_state((id.clone(), "view"), cx, |_, _| View::default());
|
||
let sorting = use_seeded((id.clone(), "sort"), self.sorts.clone(), window, cx);
|
||
let editor = window.use_keyed_state((id.clone(), "editor"), cx, |_, _| Editing::default());
|
||
let narrowest = cx.theme().label_width() * 0.5;
|
||
let rem = window.rem_size();
|
||
let keys_now: Vec<SharedString> = self
|
||
.columns
|
||
.iter()
|
||
.map(|column| column.key.clone())
|
||
.collect();
|
||
view.update(cx, |view, _| {
|
||
view.narrowest = narrowest.to_pixels(rem);
|
||
view.order.retain(|key| keys_now.contains(key));
|
||
for key in &keys_now {
|
||
if !view.order.contains(key) {
|
||
view.order.push(key.clone());
|
||
}
|
||
}
|
||
});
|
||
if view.read(cx).editing.is_some() && editor.read(cx).field().is_none() {
|
||
view.update(cx, |view, _| view.editing = None);
|
||
}
|
||
let rows = self.rows;
|
||
let columns = Rc::new(self.columns);
|
||
let index = |key: &SharedString| columns.iter().position(|column| column.key == *key);
|
||
let shown_columns: Vec<usize> = view
|
||
.read(cx)
|
||
.order
|
||
.iter()
|
||
.filter(|key| !self.hidden.contains(key))
|
||
.filter_map(index)
|
||
.collect();
|
||
let sort: Vec<(usize, bool)> = sorting
|
||
.read(cx)
|
||
.value
|
||
.iter()
|
||
.filter_map(|(key, rising)| index(key).map(|col| (col, *rising)))
|
||
.collect();
|
||
let mut order = kept(
|
||
&rows,
|
||
&columns,
|
||
filtered(&rows, &self.query),
|
||
&self.filters,
|
||
self.any,
|
||
);
|
||
if !sort.is_empty() {
|
||
order = sorted_by(&rows, order, &sort);
|
||
}
|
||
let total = order.len();
|
||
let (page_at, open, folded) = {
|
||
let view = view.read(cx);
|
||
(view.page, view.open.clone(), view.folded.clone())
|
||
};
|
||
let pages = self.page_size.map(|size| total.div_ceil(size).max(1));
|
||
let page_at = pages.map_or(0, |pages| page_at.min(pages - 1));
|
||
let opens = self.detail.is_some();
|
||
let mut lines = Vec::new();
|
||
let mut ordered: Vec<SharedString> = Vec::new();
|
||
let push_row = |ix: usize, position: usize, lines: &mut Vec<Line>| {
|
||
lines.push(Line::Row { ix, position });
|
||
if opens && open.contains(&rows[ix].key) {
|
||
lines.push(Line::Detail { ix });
|
||
}
|
||
};
|
||
match self
|
||
.group_by
|
||
.as_ref()
|
||
.map(|key| index(key).unwrap_or_else(|| panic!("no column {key} to group by")))
|
||
{
|
||
Some(col) => {
|
||
for (name, members) in groups(&rows, &order, col) {
|
||
let shut = folded.contains(&name);
|
||
lines.push(Line::Group {
|
||
name: name.clone(),
|
||
count: members.len(),
|
||
folded: shut,
|
||
});
|
||
if !shut {
|
||
for ix in members {
|
||
push_row(ix, ordered.len(), &mut lines);
|
||
ordered.push(rows[ix].key.clone());
|
||
}
|
||
}
|
||
}
|
||
}
|
||
None => {
|
||
ordered.extend(order.iter().map(|ix| rows[*ix].key.clone()));
|
||
let offset = self.page_size.map_or(0, |size| page_at * size);
|
||
let visible = match self.page_size {
|
||
Some(size) => page(&order, page_at, size).to_vec(),
|
||
None => order.clone(),
|
||
};
|
||
for (at, ix) in visible.into_iter().enumerate() {
|
||
push_row(ix, offset + at, &mut lines);
|
||
}
|
||
}
|
||
}
|
||
let ordered = Rc::new(ordered);
|
||
let selected = self.selected.map(Rc::new);
|
||
let select: Option<Select> = selected.clone().map(|selected| {
|
||
let (view, ordered, on_select, id) = (
|
||
view.clone(),
|
||
ordered.clone(),
|
||
self.on_select.clone(),
|
||
id.clone(),
|
||
);
|
||
Rc::new(
|
||
move |position: usize, how: Pick, window: &mut Window, cx: &mut App| {
|
||
let next = picked(&ordered, &selected, view.read(cx).anchor, position, how);
|
||
view.update(cx, |view, _| {
|
||
if how != Pick::Range {
|
||
view.anchor = position;
|
||
}
|
||
});
|
||
log::info!("data table {id:?}: {} selected", next.len());
|
||
if let Some(on_select) = &on_select {
|
||
on_select(&next, window, cx);
|
||
}
|
||
},
|
||
) as Select
|
||
});
|
||
let theme = cx.theme();
|
||
let height = theme.table_row(self.density);
|
||
let widths: Rc<Vec<Option<Pixels>>> = Rc::new(
|
||
columns
|
||
.iter()
|
||
.map(|column| view.read(cx).widths.get(&column.key).copied())
|
||
.collect(),
|
||
);
|
||
let body = Rc::new(Body {
|
||
id: id.clone(),
|
||
view: view.clone(),
|
||
columns: columns.clone(),
|
||
widths: widths.clone(),
|
||
rows: rows.clone(),
|
||
lines: Rc::new(lines),
|
||
selected: selected.clone(),
|
||
select,
|
||
detail: self.detail,
|
||
open: Rc::new(open),
|
||
editing: view.read(cx).editing.clone(),
|
||
editor,
|
||
on_edit: self.on_edit,
|
||
ranges: Rc::new(
|
||
columns
|
||
.iter()
|
||
.enumerate()
|
||
.map(|(col, column)| column.scale.then(|| range(&rows, col)).flatten())
|
||
.collect(),
|
||
),
|
||
height,
|
||
lead: theme.control_height(ControlSize::Md),
|
||
narrowest,
|
||
});
|
||
let all = selected.as_ref().map(|selected| {
|
||
match ordered.iter().filter(|key| selected.contains(key)).count() {
|
||
0 => CheckState::Off,
|
||
on if on == ordered.len() => CheckState::On,
|
||
_ => CheckState::Mixed,
|
||
}
|
||
});
|
||
let on_select = self.on_select;
|
||
let heads = |cols: &[usize], lead: bool, cx: &App| {
|
||
let theme = cx.theme();
|
||
div()
|
||
.flex()
|
||
.items_center()
|
||
.h(height)
|
||
.border_b_1()
|
||
.border_color(theme.colors.border)
|
||
.when(lead, |row| {
|
||
row.when_some(all.zip(selected.clone()), |row, (state, selected)| {
|
||
let (ordered, on_select) = (ordered.clone(), on_select.clone());
|
||
row.child(
|
||
div()
|
||
.id((id.clone(), "all"))
|
||
.flex_none()
|
||
.w(body.lead)
|
||
.flex()
|
||
.justify_center()
|
||
.cursor_pointer()
|
||
.on_click(move |_, window, cx| {
|
||
let next: Vec<SharedString> = if state == CheckState::On {
|
||
selected
|
||
.iter()
|
||
.filter(|key| !ordered.contains(key))
|
||
.cloned()
|
||
.collect()
|
||
} else {
|
||
let mut next = (*selected).clone();
|
||
next.extend(
|
||
ordered
|
||
.iter()
|
||
.filter(|key| !selected.contains(key))
|
||
.cloned(),
|
||
);
|
||
next
|
||
};
|
||
if let Some(on_select) = &on_select {
|
||
on_select(&next, window, cx);
|
||
}
|
||
})
|
||
.child(check_mark(state, false, false, 0, cx)),
|
||
)
|
||
})
|
||
.when(body.detail.is_some(), |row| {
|
||
row.child(div().flex_none().w(body.lead))
|
||
})
|
||
})
|
||
.children(cols.iter().map(|col| {
|
||
header(
|
||
&id,
|
||
&view,
|
||
&sorting,
|
||
&columns[*col],
|
||
widths[*col],
|
||
narrowest,
|
||
cx,
|
||
)
|
||
}))
|
||
};
|
||
let figures = |cols: &[usize], lead: bool, cx: &App| {
|
||
columns
|
||
.iter()
|
||
.any(|column| column.aggregate.is_some())
|
||
.then(|| {
|
||
let theme = cx.theme();
|
||
div()
|
||
.flex()
|
||
.items_center()
|
||
.h(height)
|
||
.text_size(theme.text_size(TextSize::Sm))
|
||
.font_weight(FontWeight::MEDIUM)
|
||
.when(lead, |row| {
|
||
row.child(div().flex_none().w(body.lead_width()))
|
||
})
|
||
.children(cols.iter().map(|col| {
|
||
let column = &columns[*col];
|
||
let shown = column.aggregate.map(|how| {
|
||
let shares = rows.first().is_some_and(|row| {
|
||
matches!(row.cells[*col], Cell::Progress(_))
|
||
});
|
||
let value = figure(&rows, &order, *col, how).map_or(
|
||
"—".to_string(),
|
||
|value| match how {
|
||
Aggregate::Count => format!("{}", value as usize),
|
||
_ if shares => format::percent(value, 0, false),
|
||
_ => column.reads(value),
|
||
},
|
||
);
|
||
div()
|
||
.flex()
|
||
.items_baseline()
|
||
.gap_1()
|
||
.child(
|
||
div()
|
||
.text_size(theme.text_size(TextSize::Xs))
|
||
.text_color(theme.colors.fg_subtle)
|
||
.child(how.label()),
|
||
)
|
||
.child(value)
|
||
});
|
||
let cell = div()
|
||
.px_3()
|
||
.border_x_1()
|
||
.border_color(transparent_black())
|
||
.flex()
|
||
.items_center();
|
||
match widths[*col] {
|
||
Some(width) => cell.w(width).flex_none(),
|
||
None => sized(cell, column, narrowest),
|
||
}
|
||
.children(shown)
|
||
}))
|
||
})
|
||
};
|
||
let part = |cols: &[usize], lead: bool, window: &mut Window, cx: &mut App| -> Div {
|
||
let lines: Vec<AnyElement> = (0..body.lines.len())
|
||
.map(|at| body.line(at, cols, lead, window, cx))
|
||
.collect();
|
||
div()
|
||
.flex()
|
||
.flex_col()
|
||
.child(heads(cols, lead, cx))
|
||
.children(lines)
|
||
.children(figures(cols, lead, cx))
|
||
};
|
||
let table: AnyElement =
|
||
if total == 0 {
|
||
let theme = cx.theme();
|
||
let least = body.least(&shown_columns, true, rem);
|
||
sideways(&id)
|
||
.child(
|
||
div()
|
||
.flex()
|
||
.flex_col()
|
||
.min_w(least)
|
||
.child(heads(&shown_columns, true, cx))
|
||
.child(
|
||
div()
|
||
.flex()
|
||
.justify_center()
|
||
.py_6()
|
||
.text_size(theme.text_size(TextSize::Sm))
|
||
.text_color(theme.colors.fg_subtle)
|
||
.child("Nothing matches."),
|
||
),
|
||
)
|
||
.into_any_element()
|
||
} else if self.virtualized {
|
||
let (scroll, count, cols, drawn) = (
|
||
view.read(cx).scroll.clone(),
|
||
body.lines.len(),
|
||
shown_columns.clone(),
|
||
body.clone(),
|
||
);
|
||
div()
|
||
.flex()
|
||
.flex_col()
|
||
.size_full()
|
||
.child(heads(&shown_columns, true, cx))
|
||
.child(
|
||
uniform_list((id.clone(), "rows"), count, move |range, window, cx| {
|
||
range
|
||
.map(|at| drawn.line(at, &cols, true, window, cx))
|
||
.collect()
|
||
})
|
||
.track_scroll(scroll)
|
||
.flex_1()
|
||
.min_h_0(),
|
||
)
|
||
.children(figures(&shown_columns, true, cx))
|
||
.into_any_element()
|
||
} else if pinned_any {
|
||
let (held, moving): (Vec<usize>, Vec<usize>) =
|
||
shown_columns.iter().partition(|col| columns[**col].pinned);
|
||
div()
|
||
.flex()
|
||
.child(part(&held, true, window, cx).flex_none())
|
||
.child(sideways(&id).flex_1().min_w_0().child(
|
||
part(&moving, false, window, cx).min_w(body.least(&moving, false, rem)),
|
||
))
|
||
.into_any_element()
|
||
} else {
|
||
let least = body.least(&shown_columns, true, rem);
|
||
sideways(&id)
|
||
.child(part(&shown_columns, true, window, cx).min_w(least))
|
||
.into_any_element()
|
||
};
|
||
let pager = pages.filter(|pages| *pages > 1).map(|pages| {
|
||
let size = self.page_size.expect("pages come from a page size");
|
||
let (first, last) = (page_at * size + 1, ((page_at + 1) * size).min(total));
|
||
let (view, id) = (view.clone(), id.clone());
|
||
div()
|
||
.flex()
|
||
.items_center()
|
||
.justify_between()
|
||
.pt_3()
|
||
.child(Caption::new(format!("{first}–{last} of {total}")))
|
||
.child(
|
||
Pagination::new((id.clone(), "pages"), page_at + 1, pages).on_change(
|
||
move |page, _, cx| {
|
||
view.update(cx, |view, cx| {
|
||
view.page = page - 1;
|
||
log::info!("data table {id:?}: page {page}");
|
||
cx.notify();
|
||
})
|
||
},
|
||
),
|
||
)
|
||
});
|
||
self.base.flex().flex_col().child(table).children(pager)
|
||
}
|
||
}
|