T14b Tables: resize, reorder, visibility, pinning, expansion, grouping, inline edit, toolbar, filter and sort builders (review pending: codex 401)
This commit is contained in:
17 files changed
+2292
-420
No files matched your search
+273
-62
@@ -1,24 +1,29 @@
|
||||
use std::rc::Rc;
|
||||
use std::{collections::HashSet, rc::Rc};
|
||||
|
||||
use gpui::{
|
||||
AnyElement, App, ElementId, InteractiveElement, IntoElement, MouseButton, ParentElement, Rems,
|
||||
SharedString, StatefulInteractiveElement, Styled, div, prelude::*,
|
||||
AnyElement, App, ElementId, Entity, InteractiveElement, IntoElement, MouseButton,
|
||||
ParentElement, Pixels, Rems, SharedString, StatefulInteractiveElement, Styled, Window, div,
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
use super::{Align, Column, Row, cell::draw, model::tint};
|
||||
use super::{Align, Column, Row, cell::draw, model::tint, table::View};
|
||||
use crate::{
|
||||
forms::{CheckState, check_mark},
|
||||
forms::{CheckState, Editing, Input, check_mark},
|
||||
lists::Pick,
|
||||
theme::{ActiveTheme, TextSize},
|
||||
primitives::Disclosure,
|
||||
theme::{ActiveTheme, ControlSize, TextSize},
|
||||
};
|
||||
|
||||
pub(super) type Select = Rc<dyn Fn(usize, Pick, &mut gpui::Window, &mut App)>;
|
||||
pub(super) type Select = Rc<dyn Fn(usize, Pick, &mut Window, &mut App)>;
|
||||
pub(super) type Detail = Rc<dyn Fn(&SharedString, &mut Window, &mut App) -> AnyElement>;
|
||||
pub(super) type OnEdit =
|
||||
Rc<dyn Fn(&SharedString, &SharedString, &SharedString, &mut Window, &mut App)>;
|
||||
|
||||
/// Gives a cell its column's width: fixed, or a share of what is left.
|
||||
pub(super) fn sized<E: Styled>(cell: E, column: &Column) -> E {
|
||||
pub(super) fn sized<E: Styled>(cell: E, column: &Column, narrowest: Rems) -> E {
|
||||
let cell = match column.width {
|
||||
Some(width) => cell.w(width).flex_none(),
|
||||
None => cell.flex_1().min_w_0(),
|
||||
None => cell.flex_1().min_w(narrowest),
|
||||
};
|
||||
if column.align == Align::End {
|
||||
cell.justify_end()
|
||||
@@ -27,35 +32,154 @@ pub(super) fn sized<E: Styled>(cell: E, column: &Column) -> E {
|
||||
}
|
||||
}
|
||||
|
||||
/// What drawing a row needs, shared by pages and by a long table's list.
|
||||
/// What the body draws, one after another.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(super) enum Line {
|
||||
Group {
|
||||
name: SharedString,
|
||||
count: usize,
|
||||
folded: bool,
|
||||
},
|
||||
Row {
|
||||
ix: usize,
|
||||
position: usize,
|
||||
},
|
||||
Detail {
|
||||
ix: usize,
|
||||
},
|
||||
}
|
||||
|
||||
/// What drawing lines needs, shared by pages and by a long table's list.
|
||||
pub(super) struct Body {
|
||||
pub id: ElementId,
|
||||
pub view: Entity<View>,
|
||||
pub columns: Rc<Vec<Column>>,
|
||||
pub widths: Rc<Vec<Option<Pixels>>>,
|
||||
pub rows: Rc<Vec<Row>>,
|
||||
pub keys: Rc<Vec<SharedString>>,
|
||||
pub shown: Rc<Vec<usize>>,
|
||||
pub offset: usize,
|
||||
pub lines: Rc<Vec<Line>>,
|
||||
pub selected: Option<Rc<Vec<SharedString>>>,
|
||||
pub select: Option<Select>,
|
||||
pub detail: Option<Detail>,
|
||||
pub open: Rc<HashSet<SharedString>>,
|
||||
pub editing: Option<(SharedString, SharedString)>,
|
||||
pub editor: Entity<Editing>,
|
||||
pub on_edit: Option<OnEdit>,
|
||||
pub ranges: Rc<Vec<Option<(f64, f64)>>>,
|
||||
pub height: Rems,
|
||||
pub check: Rems,
|
||||
pub lead: Rems,
|
||||
pub narrowest: Rems,
|
||||
}
|
||||
|
||||
impl Body {
|
||||
pub(super) fn row(&self, at: usize, cx: &App) -> AnyElement {
|
||||
let ix = self.shown[at];
|
||||
let key = self.keys[ix].clone();
|
||||
/// How wide the leading cells stand: a box to select, a disclosure to expand.
|
||||
pub(super) fn lead_width(&self) -> Rems {
|
||||
self.lead * (usize::from(self.select.is_some()) + usize::from(self.detail.is_some())) as f32
|
||||
}
|
||||
|
||||
/// Line `at` over columns `cols`; `lead` draws the leading cells.
|
||||
pub(super) fn line(
|
||||
&self,
|
||||
at: usize,
|
||||
cols: &[usize],
|
||||
lead: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
match self.lines[at].clone() {
|
||||
Line::Group {
|
||||
name,
|
||||
count,
|
||||
folded,
|
||||
} => self.group(name, count, folded, lead, cx),
|
||||
Line::Row { ix, position } => self.row(ix, position, cols, lead, cx),
|
||||
Line::Detail { ix } => {
|
||||
let key = self.rows[ix].key.clone();
|
||||
let detail = self
|
||||
.detail
|
||||
.clone()
|
||||
.expect("a detail line comes from a detail");
|
||||
let content = detail(&key, window, cx);
|
||||
let colors = &cx.theme().colors;
|
||||
div()
|
||||
.w_full()
|
||||
.pl(self.lead_width())
|
||||
.pr_3()
|
||||
.py_3()
|
||||
.bg(colors.sunken)
|
||||
.border_b_1()
|
||||
.border_color(colors.border)
|
||||
.child(content)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn group(
|
||||
&self,
|
||||
name: SharedString,
|
||||
count: usize,
|
||||
folded: bool,
|
||||
lead: bool,
|
||||
cx: &App,
|
||||
) -> AnyElement {
|
||||
let theme = cx.theme();
|
||||
let colors = &theme.colors;
|
||||
let row = div()
|
||||
.id((self.id.clone(), format!("group-{name}-{lead}")))
|
||||
.w_full()
|
||||
.h(self.height)
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.px_3()
|
||||
.bg(colors.sunken)
|
||||
.border_b_1()
|
||||
.border_color(colors.border)
|
||||
.text_size(theme.text_size(TextSize::Sm));
|
||||
if !lead {
|
||||
return row.into_any_element();
|
||||
}
|
||||
let view = self.view.clone();
|
||||
let title = if name.is_empty() {
|
||||
SharedString::from("—")
|
||||
} else {
|
||||
name.clone()
|
||||
};
|
||||
row.cursor_pointer()
|
||||
.on_click(move |_, _, cx| {
|
||||
view.update(cx, |view, cx| {
|
||||
if !view.folded.remove(&name) {
|
||||
view.folded.insert(name.clone());
|
||||
}
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.child(Disclosure::new(
|
||||
(self.id.clone(), format!("fold-{title}")),
|
||||
!folded,
|
||||
))
|
||||
.child(
|
||||
div()
|
||||
.font_weight(gpui::FontWeight::MEDIUM)
|
||||
.text_color(colors.fg)
|
||||
.child(title),
|
||||
)
|
||||
.child(div().text_color(colors.fg_subtle).child(count.to_string()))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn row(&self, ix: usize, position: usize, cols: &[usize], lead: bool, cx: &App) -> AnyElement {
|
||||
let key = self.rows[ix].key.clone();
|
||||
let on = self
|
||||
.selected
|
||||
.as_ref()
|
||||
.is_some_and(|selected| selected.contains(&key));
|
||||
let theme = cx.theme();
|
||||
let colors = &theme.colors;
|
||||
let position = self.offset + at;
|
||||
let press = self.select.clone();
|
||||
let open = self.open.contains(&key);
|
||||
div()
|
||||
.id((self.id.clone(), format!("row-{key}")))
|
||||
.id((self.id.clone(), format!("row-{key}-{lead}")))
|
||||
.flex()
|
||||
.items_center()
|
||||
.w_full()
|
||||
@@ -66,52 +190,139 @@ impl Body {
|
||||
.text_color(colors.fg)
|
||||
.when(on, |row| row.bg(colors.active))
|
||||
.when(!on, |row| row.hover(|style| style.bg(colors.hover)))
|
||||
.when_some(self.select.clone(), |row, select| {
|
||||
row.cursor_pointer()
|
||||
.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.w(self.check)
|
||||
.flex()
|
||||
.justify_center()
|
||||
.on_mouse_down(MouseButton::Left, move |_, window, cx| {
|
||||
cx.stop_propagation();
|
||||
select(position, Pick::Toggle, window, cx);
|
||||
.when_some(self.select.clone().filter(|_| lead), |row, select| {
|
||||
row.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.w(self.lead)
|
||||
.flex()
|
||||
.justify_center()
|
||||
.cursor_pointer()
|
||||
.on_mouse_down(MouseButton::Left, move |_, window, cx| {
|
||||
cx.stop_propagation();
|
||||
select(position, Pick::Toggle, window, cx);
|
||||
})
|
||||
.child(check_mark(
|
||||
if on { CheckState::On } else { CheckState::Off },
|
||||
false,
|
||||
false,
|
||||
0,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
})
|
||||
.when(self.detail.is_some() && lead, |row| {
|
||||
let (view, key, name) = (self.view.clone(), key.clone(), key.clone());
|
||||
row.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.w(self.lead)
|
||||
.flex()
|
||||
.justify_center()
|
||||
.cursor_pointer()
|
||||
.on_mouse_down(MouseButton::Left, move |_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
view.update(cx, |view, cx| {
|
||||
if !view.open.remove(&key) {
|
||||
view.open.insert(key.clone());
|
||||
}
|
||||
cx.notify();
|
||||
})
|
||||
.child(check_mark(
|
||||
if on { CheckState::On } else { CheckState::Off },
|
||||
false,
|
||||
false,
|
||||
0,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.on_click(move |event, window, cx| {
|
||||
let held = event.modifiers();
|
||||
let how = match (held.shift, held.platform) {
|
||||
(true, _) => Pick::Range,
|
||||
(false, true) => Pick::Toggle,
|
||||
_ => Pick::One,
|
||||
};
|
||||
if let Some(press) = &press {
|
||||
press(position, how, window, cx);
|
||||
})
|
||||
.child(Disclosure::new(
|
||||
(self.id.clone(), format!("open-{name}")),
|
||||
open,
|
||||
)),
|
||||
)
|
||||
})
|
||||
.when_some(press, |row, press| {
|
||||
row.cursor_pointer().on_click(move |event, window, cx| {
|
||||
let held = event.modifiers();
|
||||
let how = match (held.shift, held.platform) {
|
||||
(true, _) => Pick::Range,
|
||||
(false, true) => Pick::Toggle,
|
||||
_ => Pick::One,
|
||||
};
|
||||
press(position, how, window, cx);
|
||||
})
|
||||
})
|
||||
.children(cols.iter().map(|col| self.cell(ix, *col, cx)))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn cell(&self, ix: usize, col: usize, cx: &App) -> AnyElement {
|
||||
let (row, column) = (&self.rows[ix], &self.columns[col]);
|
||||
let cell = &row.cells[col];
|
||||
let colors = &cx.theme().colors;
|
||||
let editing = self
|
||||
.editing
|
||||
.as_ref()
|
||||
.is_some_and(|(key, at)| *key == row.key && *at == column.key);
|
||||
let tinted = self.ranges[col]
|
||||
.zip(cell.number())
|
||||
.map(|(range, value)| tint(value, range, colors));
|
||||
let frame = div()
|
||||
.id((self.id.clone(), format!("cell-{}-{col}", row.key)))
|
||||
.h_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.px_3();
|
||||
let frame = match self.widths[col] {
|
||||
Some(width) => frame
|
||||
.w(width)
|
||||
.flex_none()
|
||||
.when(column.align == Align::End, |cell| cell.justify_end()),
|
||||
None => sized(frame, column, self.narrowest),
|
||||
};
|
||||
let content = match editing.then(|| self.editor.read(cx).field()).flatten() {
|
||||
Some(field) => {
|
||||
let editor = self.editor.clone();
|
||||
div()
|
||||
.w_full()
|
||||
.on_key_down(move |event, window, cx| {
|
||||
if event.keystroke.key == "escape" {
|
||||
cx.stop_propagation();
|
||||
editor.update(cx, |editing, cx| editing.finish(true, window, cx));
|
||||
}
|
||||
})
|
||||
})
|
||||
.children(self.columns.iter().enumerate().map(|(col, column)| {
|
||||
let cell = &self.rows[ix].cells[col];
|
||||
let tinted = self.ranges[col]
|
||||
.zip(cell.number())
|
||||
.map(|(range, value)| tint(value, range, colors));
|
||||
sized(div().h_full().flex().items_center().px_3(), column)
|
||||
.when_some(tinted, |cell, bg| cell.bg(bg))
|
||||
.child(draw(
|
||||
cell,
|
||||
column,
|
||||
(self.id.clone(), format!("cell-{key}-{col}")).into(),
|
||||
cx,
|
||||
))
|
||||
}))
|
||||
.child(Input::new(&field).size(ControlSize::Sm))
|
||||
.into_any_element()
|
||||
}
|
||||
None => draw(
|
||||
cell,
|
||||
column,
|
||||
(self.id.clone(), format!("draw-{}-{col}", row.key)).into(),
|
||||
cx,
|
||||
),
|
||||
};
|
||||
frame
|
||||
.when_some(tinted, |cell, bg| cell.bg(bg))
|
||||
.when(
|
||||
column.editable && !editing && self.on_edit.is_some(),
|
||||
|frame| {
|
||||
let (view, editor, on_edit) = (
|
||||
self.view.clone(),
|
||||
self.editor.clone(),
|
||||
self.on_edit.clone().expect("checked"),
|
||||
);
|
||||
let (key, at, words) = (row.key.clone(), column.key.clone(), cell.words());
|
||||
frame.cursor_text().on_click(move |event, window, cx| {
|
||||
if event.click_count() != 2 {
|
||||
return;
|
||||
}
|
||||
cx.stop_propagation();
|
||||
let (key, at, on_edit) = (key.clone(), at.clone(), on_edit.clone());
|
||||
view.update(cx, |view, _| view.editing = Some((key.clone(), at.clone())));
|
||||
editor.update(cx, |editing, _| {
|
||||
editing.on_commit = Some(Rc::new(move |text, window, cx| {
|
||||
on_edit(&key, &at, text, window, cx)
|
||||
}))
|
||||
});
|
||||
Editing::begin(&editor, words.to_string(), window, cx);
|
||||
})
|
||||
},
|
||||
)
|
||||
.child(content)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user