Panics · P34 Dashboard: handlers in new; gone tiles move nothing, unmatched tiles and cards stay out

This commit is contained in:
ZacharyZhang-NY committed 2026-10-07 05:27:56 -04:00
1 parent 05e846b62a
commit 45cecebea5
6 files changed
+248 -241

No files matched your search

+16 -50
View File
@@ -34,8 +34,8 @@ pub struct RefreshIntervalSelector {
every: Option<u32>,
last: Timestamp,
now: Timestamp,
on_change: Option<OnInterval>,
on_refresh: Option<Run>,
on_change: OnInterval,
on_refresh: Run,
}
impl RefreshIntervalSelector {
@@ -45,6 +45,8 @@ impl RefreshIntervalSelector {
every: Option<u32>,
last: Timestamp,
now: Timestamp,
on_change: impl Fn(Option<u32>, &mut Window, &mut App) + 'static,
on_refresh: impl Fn(&mut Window, &mut App) + 'static,
) -> Self {
if let Some(every) = every {
assert!(
@@ -59,23 +61,10 @@ impl RefreshIntervalSelector {
every,
last,
now,
on_change: None,
on_refresh: None,
on_change: Rc::new(on_change),
on_refresh: Rc::new(on_refresh),
}
}
pub fn on_change(
mut self,
handler: impl Fn(Option<u32>, &mut Window, &mut App) + 'static,
) -> Self {
self.on_change = Some(Rc::new(handler));
self
}
pub fn on_refresh(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_refresh = Some(Rc::new(handler));
self
}
}
/// The seconds a choice of the selector names; its 0 is Off.
@@ -88,12 +77,8 @@ impl RenderOnce for RefreshIntervalSelector {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let id = self.id;
let on_change = self
.on_change
.unwrap_or_else(|| panic!("refresh interval selector {id:?} has no on_change"));
let on_refresh = self
.on_refresh
.unwrap_or_else(|| panic!("refresh interval selector {id:?} has no on_refresh"));
let on_change = self.on_change;
let on_refresh = self.on_refresh;
div()
.flex()
.flex_wrap()
@@ -195,8 +180,8 @@ pub struct DashboardFilterBar {
id: ElementId,
window: TimeWindow,
filters: Vec<DashboardFilter>,
on_window: Option<OnWindow>,
on_filter: Option<OnFilter>,
on_window: OnWindow,
on_filter: OnFilter,
}
impl DashboardFilterBar {
@@ -204,44 +189,25 @@ impl DashboardFilterBar {
id: impl Into<ElementId>,
window: TimeWindow,
filters: impl IntoIterator<Item = DashboardFilter>,
on_window: impl Fn(TimeWindow, &mut Window, &mut App) + 'static,
on_filter: impl Fn(&SharedString, Option<SharedString>, &mut Window, &mut App) + 'static,
) -> Self {
Self {
id: id.into(),
window,
filters: filters.into_iter().collect(),
on_window: None,
on_filter: None,
on_window: Rc::new(on_window),
on_filter: Rc::new(on_filter),
}
}
pub fn on_window(
mut self,
handler: impl Fn(TimeWindow, &mut Window, &mut App) + 'static,
) -> Self {
self.on_window = Some(Rc::new(handler));
self
}
/// Gets a filter's key and what is picked in it now; Clear sends none for each one picked.
pub fn on_filter(
mut self,
handler: impl Fn(&SharedString, Option<SharedString>, &mut Window, &mut App) + 'static,
) -> Self {
self.on_filter = Some(Rc::new(handler));
self
}
}
impl RenderOnce for DashboardFilterBar {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let id = self.id;
let theme = cx.theme();
let on_window = self
.on_window
.unwrap_or_else(|| panic!("dashboard filter bar {id:?} has no on_window"));
let on_filter = self
.on_filter
.unwrap_or_else(|| panic!("dashboard filter bar {id:?} has no on_filter"));
let on_window = self.on_window;
let on_filter = self.on_filter;
let picked: Vec<SharedString> = self
.filters
.iter()
+124 -119
View File
@@ -41,7 +41,7 @@ pub struct DashboardGrid {
columns: u32,
tiles: Vec<Tile>,
cards: Vec<(SharedString, AnyElement)>,
on_change: Option<OnTiles>,
on_change: OnTiles,
}
impl DashboardGrid {
@@ -49,6 +49,7 @@ impl DashboardGrid {
id: impl Into<ElementId>,
columns: u32,
tiles: impl IntoIterator<Item = Tile>,
on_change: impl Fn(Vec<Tile>, &mut Window, &mut App) + 'static,
) -> Self {
assert!(columns > 0, "a grid of no columns");
Self {
@@ -56,7 +57,7 @@ impl DashboardGrid {
columns,
tiles: tiles.into_iter().collect(),
cards: Vec::new(),
on_change: None,
on_change: Rc::new(on_change),
}
}
@@ -65,14 +66,6 @@ impl DashboardGrid {
self.cards.push((key.into(), card.into_any_element()));
self
}
pub fn on_change(
mut self,
handler: impl Fn(Vec<Tile>, &mut Window, &mut App) + 'static,
) -> Self {
self.on_change = Some(Rc::new(handler));
self
}
}
/// The tile a drag of `by` pixels makes of `start`, a cell at a time.
@@ -124,24 +117,28 @@ fn stacked(
tiles.sort_by_key(|tile| (tile.y, tile.x));
let column: Vec<_> = tiles
.into_iter()
.map(|tile| {
let at = cards
.iter()
.position(|(key, _)| *key == tile.key)
.unwrap_or_else(|| panic!("dashboard grid {id:?}: no card for tile {}", tile.key));
.filter_map(|tile| {
let Some(at) = cards.iter().position(|(key, _)| *key == tile.key) else {
log::error!(
"dashboard grid {id:?}: no card for tile {}; left out",
tile.key
);
return None;
};
let card = cards.remove(at).1;
let key = tile.key.clone();
div()
.debug_selector(move || format!("stacked-{key}"))
.h((sizes.row + sizes.gap) * tile.h as f32 - sizes.gap)
.child(card)
Some(
div()
.debug_selector(move || format!("stacked-{key}"))
.h((sizes.row + sizes.gap) * tile.h as f32 - sizes.gap)
.child(card),
)
})
.collect();
assert!(
cards.is_empty(),
"dashboard grid: cards with no tile: {:?}",
cards.iter().map(|(key, _)| key).collect::<Vec<_>>()
);
if !cards.is_empty() {
let keys: Vec<_> = cards.iter().map(|(key, _)| key).collect();
log::error!("dashboard grid: cards with no tile {keys:?}; left out");
}
div()
.id(id)
.relative()
@@ -172,26 +169,27 @@ impl RenderOnce for DashboardGrid {
let cell = ((width + gap) / columns as f32, row + gap);
let landing = board.read(cx).landing.clone();
let shown = match &landing {
Some(tile) => arranged(&self.tiles, tile.clone(), columns),
Some(tile) => {
arranged(&self.tiles, tile.clone(), columns).unwrap_or_else(|| self.tiles.clone())
}
None => self.tiles.clone(),
};
let height = shown.iter().map(|tile| tile.y + tile.h).max().unwrap_or(0) as f32 * cell.1;
let tiles = Rc::new(self.tiles);
let on_change = self
.on_change
.unwrap_or_else(|| panic!("dashboard grid {id:?} has no on_change"));
let on_change = self.on_change;
let (colors, radius) = (theme.colors.clone(), theme.radius(Radius::Lg));
let grip = theme.control_height(crate::theme::ControlSize::Sm);
let mut cards = self.cards;
let placed: Vec<AnyElement> = shown
.iter()
.map(|tile| {
let at = cards
.iter()
.position(|(key, _)| *key == tile.key)
.unwrap_or_else(|| {
panic!("dashboard grid {id:?}: no card for tile {}", tile.key)
});
.filter_map(|tile| {
let Some(at) = cards.iter().position(|(key, _)| *key == tile.key) else {
log::error!(
"dashboard grid {id:?}: no card for tile {}; left out",
tile.key
);
return None;
};
let card = cards.remove(at).1;
let focus = tab_stop(
(id.clone(), format!("tile-{}", tile.key)).into(),
@@ -217,92 +215,97 @@ impl RenderOnce for DashboardGrid {
};
let (keys, keyed_tiles, keyed_change, own) =
(key.clone(), tiles.clone(), on_change.clone(), focus.clone());
div()
.id((id.clone(), format!("tile-{}", tile.key)))
.track_focus(&focus)
.absolute()
.left(Pixels::from(tile.x as f32 * cell.0))
.top(Pixels::from(tile.y as f32 * cell.1))
.w(Pixels::from(tile.w as f32 * cell.0 - gap))
.h(Pixels::from(tile.h as f32 * cell.1 - gap))
.rounded(radius)
.border_1()
.border_color(if focused {
colors.focus
} else {
gpui::transparent_black()
})
.on_key_down(move |event, window, cx| {
if !own.is_focused(window) {
return;
}
let grow = event.keystroke.modifiers.shift;
let key = event.keystroke.key.as_str();
let (dx, dy): (i64, i64) = match key {
"left" => (-1, 0),
"right" => (1, 0),
"up" => (0, -1),
"down" => (0, 1),
_ => return,
};
cx.stop_propagation();
let tile = keyed_tiles
.iter()
.find(|tile| tile.key == keys)
.expect("a tile of the grid");
let to = match (grow, dy) {
(false, 0) => pulled(tile, Grip::Move, (dx as f32, 0.0), (1.0, 1.0)),
(false, _) => match stepped(&keyed_tiles, &keys, dy > 0) {
Some(to) => to,
None => return,
},
(true, _) => {
pulled(tile, Grip::Size, (dx as f32, dy as f32), (1.0, 1.0))
Some(
div()
.id((id.clone(), format!("tile-{}", tile.key)))
.track_focus(&focus)
.absolute()
.left(Pixels::from(tile.x as f32 * cell.0))
.top(Pixels::from(tile.y as f32 * cell.1))
.w(Pixels::from(tile.w as f32 * cell.0 - gap))
.h(Pixels::from(tile.h as f32 * cell.1 - gap))
.rounded(radius)
.border_1()
.border_color(if focused {
colors.focus
} else {
gpui::transparent_black()
})
.on_key_down(move |event, window, cx| {
if !own.is_focused(window) {
return;
}
};
log::info!("dashboard grid: {} to {to:?}", keys);
keyed_change(arranged(&keyed_tiles, to, columns), window, cx);
})
.child(div().size_full().child(card))
.child(
div()
.id((id.clone(), format!("move-{}", tile.key)))
.absolute()
.top_0()
.left_0()
.right(grip)
.h(grip)
.cursor_grab()
.on_mouse_down(MouseButton::Left, start(Grip::Move))
.on_drag(Pull { owner }, |_, _, _, cx| cx.new(|_| EmptyView)),
)
.child(
div()
.id((id.clone(), format!("size-{}", tile.key)))
.absolute()
.bottom_0()
.right_0()
.size(grip)
.flex()
.items_end()
.justify_end()
.cursor(gpui::CursorStyle::ResizeUpLeftDownRight)
.on_mouse_down(MouseButton::Left, start(Grip::Size))
.on_drag(Pull { owner }, |_, _, _, cx| cx.new(|_| EmptyView))
.child(
Icon::new(IconName::GripHorizontal)
.size(IconSize::Xs)
.color(colors.fg_subtle),
),
)
.into_any_element()
let grow = event.keystroke.modifiers.shift;
let key = event.keystroke.key.as_str();
let (dx, dy): (i64, i64) = match key {
"left" => (-1, 0),
"right" => (1, 0),
"up" => (0, -1),
"down" => (0, 1),
_ => return,
};
cx.stop_propagation();
let tile = keyed_tiles
.iter()
.find(|tile| tile.key == keys)
.expect("a tile of the grid");
let to = match (grow, dy) {
(false, 0) => {
pulled(tile, Grip::Move, (dx as f32, 0.0), (1.0, 1.0))
}
(false, _) => match stepped(&keyed_tiles, &keys, dy > 0) {
Some(to) => to,
None => return,
},
(true, _) => {
pulled(tile, Grip::Size, (dx as f32, dy as f32), (1.0, 1.0))
}
};
log::info!("dashboard grid: {} to {to:?}", keys);
if let Some(tiles) = arranged(&keyed_tiles, to, columns) {
keyed_change(tiles, window, cx);
}
})
.child(div().size_full().child(card))
.child(
div()
.id((id.clone(), format!("move-{}", tile.key)))
.absolute()
.top_0()
.left_0()
.right(grip)
.h(grip)
.cursor_grab()
.on_mouse_down(MouseButton::Left, start(Grip::Move))
.on_drag(Pull { owner }, |_, _, _, cx| cx.new(|_| EmptyView)),
)
.child(
div()
.id((id.clone(), format!("size-{}", tile.key)))
.absolute()
.bottom_0()
.right_0()
.size(grip)
.flex()
.items_end()
.justify_end()
.cursor(gpui::CursorStyle::ResizeUpLeftDownRight)
.on_mouse_down(MouseButton::Left, start(Grip::Size))
.on_drag(Pull { owner }, |_, _, _, cx| cx.new(|_| EmptyView))
.child(
Icon::new(IconName::GripHorizontal)
.size(IconSize::Xs)
.color(colors.fg_subtle),
),
)
.into_any_element(),
)
})
.collect();
assert!(
cards.is_empty(),
"dashboard grid: cards with no tile: {:?}",
cards.iter().map(|(key, _)| key).collect::<Vec<_>>()
);
if !cards.is_empty() {
let keys: Vec<_> = cards.iter().map(|(key, _)| key).collect();
log::error!("dashboard grid: cards with no tile {keys:?}; left out");
}
let ghost = landing.as_ref().map(|tile| {
div()
.absolute()
@@ -334,7 +337,9 @@ impl RenderOnce for DashboardGrid {
});
if let Some(to) = landing {
log::info!("dashboard grid: {} lands at {to:?}", to.key);
on_change(arranged(tiles, to, columns), window, cx);
if let Some(tiles) = arranged(tiles, to, columns) {
on_change(tiles, window, cx);
}
}
};
let end = Rc::new(end);
+28 -3
View File
@@ -27,7 +27,7 @@ impl Render for Wall {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let owner = cx.entity();
let grid = self.tiles.iter().fold(
DashboardGrid::new("grid", 12, self.tiles.clone()).on_change(move |tiles, _, cx| {
DashboardGrid::new("grid", 12, self.tiles.clone(), move |tiles, _, cx| {
owner.update(cx, |wall, cx| {
wall.tiles = tiles.clone();
wall.heard.push(tiles);
@@ -269,9 +269,9 @@ fn filters(_: &Board, owner: Entity<Board>) -> AnyElement {
"filters",
TimeWindow::ALL[0],
[filter("region"), filter("env")],
|_, _, _| {},
move |key, value, _, cx| say(&owner, format!("{key} {value:?}"), cx),
)
.on_window(|_, _, _| {})
.on_filter(move |key, value, _, cx| say(&owner, format!("{key} {value:?}"), cx))
.into_any_element()
}
@@ -309,3 +309,28 @@ fn a_narrow_grid_stacks_its_tiles_in_reading_order(cx: &mut TestAppContext) {
);
assert_eq!(a.size.width, px(300.0), "each fills the width");
}
/// A grid at `width` whose tile b has no card and whose card c has no tile.
struct Mismatched(f32);
impl Render for Mismatched {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
let tiles = [
Tile::new("a", (0, 0), (6, 2)),
Tile::new("b", (6, 0), (6, 2)),
];
let grid = DashboardGrid::new("grid", 12, tiles, |_, _, _| {})
.card("a", DashboardCard::new("A"))
.card("c", DashboardCard::new("C"));
div().w(px(self.0)).child(grid)
}
}
#[gpui::test]
fn tiles_and_cards_that_do_not_match_are_left_out(cx: &mut TestAppContext) {
cx.update(Theme::init);
for width in [960.0, 320.0] {
let (_, cx) = cx.add_window_view(move |_, _| Mismatched(width));
settle(cx);
}
}
+31 -18
View File
@@ -58,14 +58,16 @@ fn compacted(mut tiles: Vec<Tile>) -> Vec<Tile> {
out
}
/// The tiles once `to` takes the place of the tile of its key: it goes where it was put, the tiles it lands on move down below it, then every tile, it too, rises as far as the ones above allow.
pub fn arranged(tiles: &[Tile], to: Tile, columns: u32) -> Vec<Tile> {
/// The tiles once `to` takes the place of the tile of its key: it goes where it was put, the tiles it lands on move down below it, then every tile, it too, rises as far as the ones above allow. None, logged, when the grid lost that tile.
pub fn arranged(tiles: &[Tile], to: Tile, columns: u32) -> Option<Vec<Tile>> {
assert!(columns > 0, "a grid of no columns");
assert!(
tiles.iter().any(|tile| tile.key == to.key),
"no tile {} on the grid",
to.key
);
if !tiles.iter().any(|tile| tile.key == to.key) {
log::error!(
"dashboard grid: no tile {} on the grid; nothing moves",
to.key
);
return None;
}
let mut placed = vec![to.clone().fitted(columns)];
let mut rest: Vec<Tile> = tiles
.iter()
@@ -79,15 +81,15 @@ pub fn arranged(tiles: &[Tile], to: Tile, columns: u32) -> Vec<Tile> {
}
placed.push(tile);
}
compacted(placed)
Some(compacted(placed))
}
/// Where a tile goes on a step down or up: below the next tile under it, or into the place of the next one above; none at the edge. Both overlap its columns.
pub fn stepped(tiles: &[Tile], key: &str, down: bool) -> Option<Tile> {
let tile = tiles
.iter()
.find(|tile| tile.key == key)
.unwrap_or_else(|| panic!("no tile {key} on the grid"));
let Some(tile) = tiles.iter().find(|tile| tile.key == key) else {
log::error!("dashboard grid: no tile {key} on the grid");
return None;
};
let beside = |other: &&Tile| {
other.key != tile.key && other.x < tile.x + tile.w && tile.x < other.x + other.w
};
@@ -131,9 +133,17 @@ mod tests {
]
}
#[test]
fn a_tile_gone_from_the_grid_moves_nothing() {
let gone = Tile::new("gone", (0, 0), (6, 2));
assert_eq!(arranged(&grid(), gone, 12), None);
assert_eq!(stepped(&grid(), "gone", true), None);
}
#[test]
fn a_tile_dropped_on_another_pushes_it_below() {
let moved = arranged(&grid(), Tile::new("b", (0, 0), (6, 2)), 12);
let moved =
arranged(&grid(), Tile::new("b", (0, 0), (6, 2)), 12).expect("a tile of the grid");
assert_eq!(at(&moved, "b"), (0, 0, 6, 2), "the dropped tile stays");
assert_eq!(at(&moved, "a"), (0, 2, 6, 2), "a goes below b");
assert_eq!(at(&moved, "c"), (0, 4, 12, 3), "and c below a");
@@ -141,20 +151,23 @@ mod tests {
#[test]
fn tiles_rise_into_the_gap_a_tile_leaves() {
let moved = arranged(&grid(), Tile::new("a", (0, 9), (6, 2)), 12);
let moved =
arranged(&grid(), Tile::new("a", (0, 9), (6, 2)), 12).expect("a tile of the grid");
assert_eq!(at(&moved, "c"), (0, 2, 12, 3), "c keeps below b");
assert_eq!(at(&moved, "a"), (0, 5, 6, 2), "a rises to rest on c");
}
#[test]
fn a_tile_stays_inside_the_grid() {
let moved = arranged(&grid(), Tile::new("a", (10, 0), (6, 2)), 12);
let moved =
arranged(&grid(), Tile::new("a", (10, 0), (6, 2)), 12).expect("a tile of the grid");
assert_eq!(
at(&moved, "a"),
(6, 0, 6, 2),
"pulled back from the right edge"
);
let wide = arranged(&grid(), Tile::new("b", (0, 0), (20, 1)), 12);
let wide =
arranged(&grid(), Tile::new("b", (0, 0), (20, 1)), 12).expect("a tile of the grid");
assert_eq!(at(&wide, "b"), (0, 0, 12, 1), "no wider than the grid");
}
@@ -162,12 +175,12 @@ mod tests {
fn a_step_down_passes_the_tile_under_and_a_step_up_takes_its_place() {
let down = stepped(&grid(), "a", true).expect("c lies under a");
assert_eq!((down.x, down.y), (0, 5), "a steps under the bottom of c");
let moved = arranged(&grid(), down, 12);
let moved = arranged(&grid(), down, 12).expect("a tile of the grid");
assert_eq!(at(&moved, "c"), (0, 2, 12, 3), "c rests under b");
assert_eq!(at(&moved, "a"), (0, 5, 6, 2), "a rests under c");
let up = stepped(&moved, "a", false).expect("c lies over a");
assert_eq!(
at(&arranged(&moved, up, 12), "a"),
at(&arranged(&moved, up, 12).expect("a tile of the grid"), "a"),
(0, 0, 6, 2),
"back on top"
);