Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 36 additions & 2 deletions crates/moon-ui-components/src/moon/data_table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,28 +231,62 @@ impl MoonDataCell {
}
}

/// One row handed to a [`MoonDataTable`] row builder.
///
/// CONSTRUCT IT THROUGH [`MoonDataRow::new`] AND THE BUILDERS, never a struct literal. `cells` and
/// `selected` are `pub` for the table's own internals to read back, which reads like an invitation
/// to build one field-by-field; it is not, and `banner` is deliberately private to say so. A
/// literal would break on the next field added here whatever that field's visibility is, so the
/// builder path is the only construction contract this type has ever been able to keep.
pub struct MoonDataRow {
pub cells: Vec<MoonDataCell>,
pub selected: bool,
banner: Option<AnyElement>,
}

impl MoonDataRow {
pub fn new(cells: impl IntoIterator<Item = MoonDataCell>) -> Self {
Self {
cells: cells.into_iter().collect(),
selected: false,
banner: None,
}
}

/// Lay one element across the whole row, above the cells and outside their clipping.
///
/// Forwards to [`MoonTableRow::banner`], which carries the contract: a section heading or any
/// other row-wide statement, on a row whose cells are decorative. The cells are still emitted
/// — they keep the column geometry and any background the row paints — the banner simply
/// covers them.
///
/// Args:
/// banner: The element to lay across the row.
///
/// Returns:
/// The row, carrying the banner.
pub fn banner(mut self, banner: impl IntoElement) -> Self {
self.banner = Some(banner.into_any_element());
self
}

pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}

fn as_table_row(self) -> MoonTableRow {
MoonTableRow::new()
// A plain `match`, NOT `FluentBuilder::when_some`: that combinator is blanket-implemented
// only for `IntoElement`, and `MoonTableRow` is a plain builder struct that is not an
// element. Reaching for it here fails to compile with E0599 "trait bounds were not
// satisfied" -- measured, after a review suggested exactly that swap.
let row = MoonTableRow::new()
.selected(self.selected)
.cells(self.cells.into_iter().map(|cell| cell.cell))
.cells(self.cells.into_iter().map(|cell| cell.cell));
match self.banner {
Some(banner) => row.banner(banner),
None => row,
}
}
}

Expand Down
23 changes: 20 additions & 3 deletions crates/moon-ui-components/src/moon/data_table/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
// Do not use `super::*`: the glob would import the `gpui::test` macro, causing `#[test]` to
// recursively expand into itself.
use super::{
MIN_COLUMN_WIDTH, MoonDataTable, MoonDataTableColumn, MoonDataTableWidthPolicy,
is_select_all_shortcut,
MIN_COLUMN_WIDTH, MoonDataCell, MoonDataRow, MoonDataTable, MoonDataTableColumn,
MoonDataTableWidthPolicy, is_select_all_shortcut,
};
use gpui::Modifiers;
use gpui::{Modifiers, div};

/// Build a test column with matching key and label.
fn col(key: &str, width: f32) -> MoonDataTableColumn {
Expand Down Expand Up @@ -246,3 +246,20 @@ fn select_all_shortcut_requires_the_exact_platform_secondary_modifier() {
assert!(!is_select_all_shortcut("a", with_shift));
assert!(!is_select_all_shortcut("a", with_alt));
}

/// Catches dropping the banner forward in `data_table.rs:MoonDataRow::as_table_row`, which would
/// render an exchange section heading as an empty grey stripe without its logo, name, or count.
#[test]
fn data_row_conversion_preserves_banner_presence() {
let row_with_banner = MoonDataRow::new([MoonDataCell::text("placeholder")]).banner(div());
let row_without_banner = MoonDataRow::new([MoonDataCell::text("placeholder")]);

assert!(
row_with_banner.as_table_row().has_banner(),
"a banner supplied through MoonDataRow must reach MoonTableRow"
);
assert!(
!row_without_banner.as_table_row().has_banner(),
"a MoonDataRow without a banner must not create one during conversion"
);
}
65 changes: 65 additions & 0 deletions crates/moon-ui-components/src/moon/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ pub struct MoonTableRow {
cells: Vec<MoonTableCell>,
selected: bool,
text_alpha: f32,
banner: Option<AnyElement>,
}

impl Default for MoonTableRow {
Expand All @@ -151,9 +152,39 @@ impl MoonTableRow {
cells: Vec::new(),
selected: false,
text_alpha: 1.0,
banner: None,
}
}

/// Lay one element across the WHOLE row, above the cells and outside their clipping.
///
/// Every cell is `overflow_hidden`, which is what keeps a long value from spilling into its
/// neighbour — and is also why a row that wants to say ONE thing across its full width cannot
/// say it through a cell: a section heading put in the leftmost cell is cut at that column's
/// edge, however much empty room the rest of the row has. The banner is the escape hatch, and
/// it is deliberately the only one: widening a cell's clipping would let ordinary values
/// overlap.
///
/// It is painted AFTER the cells, so it sits ON TOP of them VISUALLY. It does NOT take their
/// pointer events: GPUI stops hit-testing at a hitbox only when that hitbox is
/// `HitboxBehavior::BlockMouse` (`window.rs::hit_test` breaks on exactly that), and only
/// `occlude()` / `occlude_mouse()` set it. This wrapper calls neither, so a cell underneath
/// stays clickable and a banner is safe on a row whose cells are interactive.
///
/// The consequence to design around is the OPPOSITE of occlusion: put a click handler on the
/// banner AND on a cell beneath it and BOTH fire for one click. A caller that wants the banner
/// to win calls `cx.stop_propagation()` in its own handler, or `occlude()`s its own element.
///
/// Args:
/// banner: The element to lay across the row.
///
/// Returns:
/// The row, carrying the banner.
pub fn banner(mut self, banner: impl IntoElement) -> Self {
self.banner = Some(banner.into_any_element());
self
}

pub fn cell(mut self, cell: MoonTableCell) -> Self {
self.cells.push(cell);
self
Expand All @@ -173,6 +204,23 @@ impl MoonTableRow {
self.text_alpha = text_alpha;
self
}

/// Whether a banner was laid across this row.
///
/// `pub(crate)` and a PREDICATE rather than a field: `MoonDataRow::as_table_row` forwards the
/// banner across a module boundary, and nothing downstream of that forward is observable
/// without a rendering harness — so the forward itself is untestable unless the receiving side
/// can be asked. Exposing the field instead would let any module in the crate MOVE the element
/// out of a row it does not own; a bool answers the only question a caller has.
///
/// `allow(dead_code)` OUTSIDE a test build, and only there: the crate's own callers are all in
/// `#[cfg(test)] mod tests`, which is stripped before dead-code analysis runs, so an ordinary
/// `cargo build` would warn about a method that is doing its job. The attribute is conditional
/// rather than blanket so that a REAL orphaning — the tests going away — still warns.
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn has_banner(&self) -> bool {
self.banner.is_some()
}
}

#[derive(Clone, Copy, Debug)]
Expand Down Expand Up @@ -290,6 +338,23 @@ impl MoonTable {
row_el = row_el.child(decorate_cell(column_ix, cell));
}

// LAST, so it paints over the cells rather than under them, and absolute so it spans the
// row instead of joining the cell flex line. The row itself is `relative` and carries no
// `overflow_hidden`, which is the whole reason a banner can reach past a column edge.
//
// Deliberately NOT `occlude()`d — see the builder's doc. Painting last decides what the
// eye sees, never what the mouse reaches.
if let Some(banner) = row.banner {
row_el = row_el.child(
div()
.absolute()
.left(px(0.0))
.top(px(0.0))
.size_full()
.child(banner),
);
}

row_el
}

Expand Down
8 changes: 8 additions & 0 deletions docs/component-api-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,10 @@
"file": "crates/moon-ui-components/src/moon/data_table.rs",
"signature": "pub fn background_policy(mut self, policy: MoonBackgroundPolicy) -> Self"
},
{
"file": "crates/moon-ui-components/src/moon/data_table.rs",
"signature": "pub fn banner(mut self, banner: impl IntoElement) -> Self"
},
{
"file": "crates/moon-ui-components/src/moon/data_table.rs",
"signature": "pub fn bounds(mut self, bounds: MoonRect) -> Self"
Expand Down Expand Up @@ -3909,6 +3913,10 @@
"file": "crates/moon-ui-components/src/moon/table.rs",
"signature": "pub fn align(mut self, align: MoonTableAlign) -> Self"
},
{
"file": "crates/moon-ui-components/src/moon/table.rs",
"signature": "pub fn banner(mut self, banner: impl IntoElement) -> Self"
},
{
"file": "crates/moon-ui-components/src/moon/table.rs",
"signature": "pub fn cell(mut self, cell: MoonTableCell) -> Self"
Expand Down
Loading