Project · a focused card scrolls into view
The board scrolled sideways without a handle, so a card that took focus past the fold stayed out of sight, ring and all, and Option-Right could send it into a column nobody saw. The board now tracks its scroll, and a card that takes focus sets the offset through layout::bring_into_view from its row's painted box, ring included, once per focus and again when it lands in another column. The stepping rules gain tests for left and right keeping a card's place and for the end of a shorter column. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
62382f7e68
commit
fc0c3e564d
4 files changed
+82
-6
No files matched your search
@@ -364,3 +364,4 @@ Per-component lines live in `tasks/`. Tags there:
|
||||
| T30d | 1 | FAIL | a chosen day outlived the owner's days, and the next render panicked |
|
||||
| T30d | 2 | PASS | |
|
||||
| T31a | 1 | PASS | |
|
||||
| T31b | 1 | FAIL | a focused card past the board's fold stayed out of view; left and right steps that keep a card's place went untested |
|
||||
+45
-4
@@ -1,14 +1,14 @@
|
||||
use std::{collections::HashMap, rc::Rc};
|
||||
|
||||
use gpui::{
|
||||
AnyElement, App, Bounds, ElementId, EmptyView, EntityId, FocusHandle, InteractiveElement,
|
||||
IntoElement, ParentElement, Pixels, Point, RenderOnce, SharedString,
|
||||
StatefulInteractiveElement, Styled, Window, anchored, canvas, div, prelude::*,
|
||||
AnyElement, App, Axis, Bounds, ElementId, EmptyView, Entity, EntityId, FocusHandle,
|
||||
InteractiveElement, IntoElement, ParentElement, Pixels, Point, RenderOnce, ScrollHandle,
|
||||
SharedString, StatefulInteractiveElement, Styled, Window, anchored, canvas, div, prelude::*,
|
||||
};
|
||||
|
||||
use super::moves::{landing, stepped};
|
||||
use crate::{
|
||||
layout::on_axis,
|
||||
layout::{bring_into_view, on_axis},
|
||||
motion::Flip,
|
||||
primitives::{FocusRing, raise, tab_stop},
|
||||
theme::{ActiveTheme, Elevation, Radius, TextSize},
|
||||
@@ -106,6 +106,39 @@ fn measure(
|
||||
.size_full()
|
||||
}
|
||||
|
||||
/// The card shown after focus, by key and column.
|
||||
type Revealed = Option<(SharedString, SharedString)>;
|
||||
|
||||
/// Scrolls the board so a card that takes focus shows, once per focus and again in a new column.
|
||||
fn reveal(
|
||||
scroll: &ScrollHandle,
|
||||
revealed: &Entity<Revealed>,
|
||||
focus: &FocusHandle,
|
||||
place: (SharedString, SharedString),
|
||||
) -> impl IntoElement {
|
||||
let (scroll, revealed, focus) = (scroll.clone(), revealed.clone(), focus.clone());
|
||||
canvas(
|
||||
move |bounds, window, cx| {
|
||||
let focused = focus.is_focused(window);
|
||||
let held = revealed.read(cx).clone();
|
||||
if focused && held.as_ref() != Some(&place) {
|
||||
revealed.update(cx, |revealed, _| *revealed = Some(place.clone()));
|
||||
if bring_into_view(&scroll, bounds, Axis::Horizontal) {
|
||||
log::info!("board: {} scrolled into view", place.0);
|
||||
window.request_animation_frame();
|
||||
}
|
||||
} else if !focused && held.is_some_and(|(key, _)| key == place.0) {
|
||||
revealed.update(cx, |revealed, _| *revealed = None);
|
||||
}
|
||||
},
|
||||
|_, _, _, _| {},
|
||||
)
|
||||
.absolute()
|
||||
.top_0()
|
||||
.left_0()
|
||||
.size_full()
|
||||
}
|
||||
|
||||
/// Columns of cards: a card drags to a place in any column, the others gliding aside, and Option with an arrow moves the focused one. Each move asks the owner through `on_move(card, column, index)`, the index counted without the card.
|
||||
#[derive(IntoElement)]
|
||||
pub struct KanbanBoard {
|
||||
@@ -166,6 +199,11 @@ impl RenderOnce for KanbanBoard {
|
||||
let hold = window.use_keyed_state((id.clone(), "hold"), cx, |_, _| None::<Hold>);
|
||||
let measures =
|
||||
window.use_keyed_state((id.clone(), "measures"), cx, |_, _| Measures::default());
|
||||
let scroll = window
|
||||
.use_keyed_state((id.clone(), "scroll"), cx, |_, _| ScrollHandle::new())
|
||||
.read(cx)
|
||||
.clone();
|
||||
let revealed = window.use_keyed_state((id.clone(), "revealed"), cx, |_, _| None);
|
||||
let owner = hold.entity_id();
|
||||
let stale = hold
|
||||
.read(cx)
|
||||
@@ -307,10 +345,12 @@ impl RenderOnce for KanbanBoard {
|
||||
measures.cards.insert(key.clone(), bounds) != Some(bounds)
|
||||
}
|
||||
}));
|
||||
let place = (key.clone(), column.key.clone());
|
||||
let row = div()
|
||||
.relative()
|
||||
.pb_2()
|
||||
.child(face)
|
||||
.child(reveal(&scroll, &revealed, &handle, place))
|
||||
.child(measure(&row_measures, {
|
||||
let key = key.clone();
|
||||
move |measures, bounds| {
|
||||
@@ -385,6 +425,7 @@ impl RenderOnce for KanbanBoard {
|
||||
div()
|
||||
.id(id.clone())
|
||||
.debug_selector(|| "kanban-board".into())
|
||||
.track_scroll(&scroll)
|
||||
.overflow_x_scroll(),
|
||||
)
|
||||
.flex()
|
||||
|
||||
@@ -54,5 +54,20 @@ mod tests {
|
||||
assert_eq!(stepped(&lens, 2, 1, "left"), Some((1, 0)));
|
||||
assert_eq!(stepped(&lens, 2, 1, "right"), None);
|
||||
assert_eq!(stepped(&lens, 1, 0, "home"), None);
|
||||
assert_eq!(
|
||||
stepped(&[3, 2], 0, 1, "right"),
|
||||
Some((1, 1)),
|
||||
"the same place"
|
||||
);
|
||||
assert_eq!(
|
||||
stepped(&[3, 2], 1, 1, "left"),
|
||||
Some((0, 1)),
|
||||
"the same place"
|
||||
);
|
||||
assert_eq!(
|
||||
stepped(&[3, 2], 0, 2, "right"),
|
||||
Some((1, 2)),
|
||||
"the end of a shorter column"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use gpui::{
|
||||
AnyElement, Entity, IntoElement, Modifiers, MouseButton, SharedString, TestAppContext, point,
|
||||
px,
|
||||
AnyElement, Entity, IntoElement, Modifiers, MouseButton, ParentElement, SharedString, Styled,
|
||||
TestAppContext, point, px,
|
||||
};
|
||||
use jiff::civil::date;
|
||||
|
||||
@@ -92,6 +92,25 @@ fn option_arrows_move_the_focused_card_and_enter_opens_it(cx: &mut TestAppContex
|
||||
);
|
||||
}
|
||||
|
||||
fn narrow(owner: Entity<Desk>) -> AnyElement {
|
||||
gpui::div()
|
||||
.w(px(300.0))
|
||||
.child(board(owner))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_focused_card_past_the_fold_scrolls_into_view(cx: &mut TestAppContext) {
|
||||
let (_, cx) = desk(narrow, cx);
|
||||
tab_to(3, cx);
|
||||
let board = cx.debug_bounds("kanban-board").expect("the board draws");
|
||||
let card = cx.debug_bounds("card c").expect("card c draws");
|
||||
assert!(
|
||||
board.left() <= card.left() && card.right() <= board.right(),
|
||||
"card c at {card:?} shows inside the board at {board:?}"
|
||||
);
|
||||
}
|
||||
|
||||
fn labels(owner: Entity<Desk>) -> AnyElement {
|
||||
let (renamed, made) = (owner.clone(), owner);
|
||||
LabelManager::new(
|
||||
|
||||
Reference in new issue
Block a user