Skip to main content

gpui_base/grid_layout/
state.rs

1use gpui::{
2    App, Axis, Bounds, Context, EntityId, EventEmitter, FocusHandle, Pixels, Point, SharedString,
3    Size, Window, px,
4};
5
6use super::layout::{self, GridArea, LayoutRules};
7
8/// Emitted by [`GridLayoutState`] when the layout changes.
9#[derive(Clone, Debug, PartialEq)]
10#[non_exhaustive]
11pub enum GridLayoutEvent {
12    /// A drag, a resize, a keyboard command, or [`GridLayoutState::move_item`]
13    /// or [`GridLayoutState::resize_item`] produced a new layout, carried here
14    /// so the application can persist it. [`GridLayoutState::set_areas`] does
15    /// not emit this: the application already holds the layout it set.
16    Change(Vec<GridArea>),
17    /// The user selected an item — by pressing it, or with Tab or Shift-Tab —
18    /// carried here so a view that follows the selection, such as a settings
19    /// panel, need not watch every redraw. It is emitted only when the
20    /// selection changes; [`GridLayoutState::set_selected_item`] does not
21    /// emit it, as the application already knows what it selected.
22    Select(Option<SharedString>),
23}
24
25/// What a pointer gesture on an item is doing to it.
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub(crate) enum GridGesture {
28    Move,
29    Resize,
30}
31
32/// The payload GPUI carries while an item is dragged.
33#[derive(Clone)]
34pub(crate) struct DragGridItem {
35    pub(crate) grid: EntityId,
36    pub(crate) item: SharedString,
37    pub(crate) gesture: GridGesture,
38}
39
40/// Pixel geometry of a grid at one container width.
41///
42/// Columns share the width left over by the column gaps between them, so an
43/// item spanning `n` columns is `n` column widths plus the `n - 1` gaps it
44/// covers; rows work the same way with the row gap.
45#[derive(Clone, Copy, Debug, PartialEq)]
46pub(crate) struct GridMetrics {
47    pub(crate) width: Pixels,
48    pub(crate) column_width: Pixels,
49    pub(crate) row_height: Pixels,
50    /// The column gap across, the row gap down.
51    pub(crate) gap: Size<Pixels>,
52}
53
54impl GridMetrics {
55    pub(crate) fn new(
56        width: Pixels,
57        columns: usize,
58        row_height: Pixels,
59        gap: Size<Pixels>,
60    ) -> Self {
61        let columns = columns.max(1) as f32;
62        Self {
63            width,
64            column_width: ((width - gap.width * (columns - 1.)) / columns).max(px(0.)),
65            row_height,
66            gap,
67        }
68    }
69
70    pub(crate) fn left(&self, col: usize) -> Pixels {
71        (self.column_width + self.gap.width) * col as f32
72    }
73
74    pub(crate) fn top(&self, row: usize) -> Pixels {
75        (self.row_height + self.gap.height) * row as f32
76    }
77
78    pub(crate) fn span_width(&self, col_span: usize) -> Pixels {
79        (self.column_width * col_span as f32 + self.gap.width * col_span.saturating_sub(1) as f32)
80            .max(px(0.))
81    }
82
83    pub(crate) fn span_height(&self, row_span: usize) -> Pixels {
84        (self.row_height * row_span as f32 + self.gap.height * row_span.saturating_sub(1) as f32)
85            .max(px(0.))
86    }
87
88    /// The column whose leading edge is nearest `left`.
89    fn column_at(&self, left: Pixels) -> usize {
90        snap(left, self.column_width + self.gap.width)
91    }
92
93    /// The row whose top edge is nearest `top`.
94    fn row_at(&self, top: Pixels) -> usize {
95        snap(top, self.row_height + self.gap.height)
96    }
97
98    /// The number of columns whose span is nearest `width`.
99    fn columns_spanning(&self, width: Pixels) -> usize {
100        snap(width + self.gap.width, self.column_width + self.gap.width).max(1)
101    }
102
103    /// The number of rows whose span is nearest `height`.
104    fn rows_spanning(&self, height: Pixels) -> usize {
105        snap(height + self.gap.height, self.row_height + self.gap.height).max(1)
106    }
107}
108
109fn snap(length: Pixels, step: Pixels) -> usize {
110    if step <= px(0.) {
111        return 0;
112    }
113    (length / step).round().max(0.) as usize
114}
115
116/// An item being dragged or resized by the pointer.
117pub(crate) struct GridDrag {
118    pub(crate) item: SharedString,
119    pub(crate) gesture: GridGesture,
120    /// The pointer where the gesture began, in window coordinates.
121    origin: Point<Pixels>,
122    /// The layout when the gesture began; every step is computed from it.
123    start: Vec<GridArea>,
124    /// Where the items would settle if the pointer were released now.
125    pub(crate) preview: Vec<GridArea>,
126    /// The pointer's travel since `origin`.
127    pub(crate) delta: Point<Pixels>,
128    /// The grid's geometry, known once the pointer first moves.
129    pub(crate) metrics: Option<GridMetrics>,
130}
131
132impl GridDrag {
133    /// The dragged item as it was when the gesture began.
134    pub(crate) fn start_area(&self) -> Option<&GridArea> {
135        self.start.iter().find(|area| area.id == self.item)
136    }
137}
138
139/// The state of a [`GridLayout`](super::GridLayout): the items' areas, the
140/// rules the layout settles under, and any gesture in progress.
141///
142/// The application owns the layout. Create the state with the grid's rules,
143/// then its areas, and subscribe to [`GridLayoutEvent::Change`] to persist
144/// what the user arranges.
145///
146/// ```ignore
147/// let state = cx.new(|cx| {
148///     GridLayoutState::new(cx).cols(12).default_areas([
149///         GridArea::new("revenue").col_span(8).row_span(4),
150///         GridArea::new("orders").col(8).col_span(4).row_span(4),
151///     ])
152/// });
153/// ```
154pub struct GridLayoutState {
155    areas: Vec<GridArea>,
156    columns: usize,
157    compaction: Option<Axis>,
158    prevent_collision: bool,
159    draggable: bool,
160    resizable: bool,
161    focus_handle: FocusHandle,
162    selected: Option<SharedString>,
163    /// Whether the last interaction came from the keyboard, which is when the
164    /// selected item shows its selected style.
165    keyboard_active: bool,
166    /// Where the pointer last pressed an item, so a drag starts from the press
167    /// rather than from wherever GPUI's drag threshold was crossed.
168    press_position: Option<Point<Pixels>>,
169    pub(crate) drag: Option<GridDrag>,
170}
171
172impl EventEmitter<GridLayoutEvent> for GridLayoutState {}
173
174impl GridLayoutState {
175    /// An empty, 12-column grid that compacts vertically.
176    pub fn new(cx: &mut App) -> Self {
177        Self {
178            areas: Vec::new(),
179            columns: 12,
180            compaction: Some(Axis::Vertical),
181            prevent_collision: false,
182            draggable: true,
183            resizable: true,
184            focus_handle: cx.focus_handle().tab_stop(true),
185            selected: None,
186            keyboard_active: false,
187            press_position: None,
188            drag: None,
189        }
190    }
191
192    /// The number of equal columns the width is divided into. Defaults to 12.
193    pub fn cols(mut self, cols: usize) -> Self {
194        self.columns = cols.max(1);
195        self.normalize();
196        self
197    }
198
199    /// The direction items float toward when space opens up, or `None` to
200    /// leave them wherever they are put. Defaults to `Some(Axis::Vertical)`.
201    pub fn compaction(mut self, compaction: Option<Axis>) -> Self {
202        self.compaction = compaction;
203        self.normalize();
204        self
205    }
206
207    /// Refuse a move or resize that would overlap another item, instead of
208    /// pushing that item out of the way. Defaults to `false`.
209    pub fn prevent_collision(mut self, prevent_collision: bool) -> Self {
210        self.prevent_collision = prevent_collision;
211        self
212    }
213
214    /// Whether items can be moved. Defaults to `true`.
215    pub fn draggable(mut self, draggable: bool) -> Self {
216        self.draggable = draggable;
217        self
218    }
219
220    /// Whether items can be resized. Defaults to `true`.
221    pub fn resizable(mut self, resizable: bool) -> Self {
222        self.resizable = resizable;
223        self
224    }
225
226    /// The areas the grid starts with, settled under the rules set so far:
227    /// overlaps are pushed apart and, with compaction on, gaps close. Set the
228    /// rules first.
229    pub fn default_areas(mut self, areas: impl IntoIterator<Item = GridArea>) -> Self {
230        self.areas = areas.into_iter().collect();
231        self.normalize();
232        self
233    }
234
235    /// The committed layout, in the order the areas were given.
236    pub fn areas(&self) -> &[GridArea] {
237        &self.areas
238    }
239
240    /// The area of the item `id`.
241    pub fn area(&self, id: &str) -> Option<&GridArea> {
242        self.areas.iter().find(|area| area.id.as_ref() == id)
243    }
244
245    pub fn column_count(&self) -> usize {
246        self.columns
247    }
248
249    pub fn compaction_axis(&self) -> Option<Axis> {
250        self.compaction
251    }
252
253    pub fn is_collision_prevented(&self) -> bool {
254        self.prevent_collision
255    }
256
257    pub fn is_draggable(&self) -> bool {
258        self.draggable
259    }
260
261    pub fn is_resizable(&self) -> bool {
262        self.resizable
263    }
264
265    /// The item keyboard commands act on.
266    pub fn selected_item(&self) -> Option<&SharedString> {
267        self.selected.as_ref()
268    }
269
270    /// The item being dragged or resized, if a gesture is in progress.
271    pub fn active_item(&self) -> Option<&SharedString> {
272        self.drag.as_ref().map(|drag| &drag.item)
273    }
274
275    pub(crate) fn focus_handle(&self) -> &FocusHandle {
276        &self.focus_handle
277    }
278
279    pub(crate) fn is_keyboard_active(&self) -> bool {
280        self.keyboard_active
281    }
282
283    /// Replace the layout, settling it under the grid's rules. Cancels any
284    /// gesture in progress, and does not emit [`GridLayoutEvent::Change`].
285    pub fn set_areas(&mut self, areas: impl IntoIterator<Item = GridArea>, cx: &mut Context<Self>) {
286        self.areas = areas.into_iter().collect();
287        self.drag = None;
288        self.normalize();
289        if let Some(selected) = &self.selected
290            && self.area(selected).is_none()
291        {
292            self.selected = None;
293        }
294        cx.notify();
295    }
296
297    pub fn set_cols(&mut self, cols: usize, cx: &mut Context<Self>) {
298        self.columns = cols.max(1);
299        self.drag = None;
300        self.normalize();
301        cx.notify();
302    }
303
304    pub fn set_compaction(&mut self, compaction: Option<Axis>, cx: &mut Context<Self>) {
305        self.compaction = compaction;
306        self.drag = None;
307        self.normalize();
308        cx.notify();
309    }
310
311    pub fn set_prevent_collision(&mut self, prevent_collision: bool, cx: &mut Context<Self>) {
312        self.prevent_collision = prevent_collision;
313        cx.notify();
314    }
315
316    pub fn set_draggable(&mut self, draggable: bool, cx: &mut Context<Self>) {
317        self.draggable = draggable;
318        cx.notify();
319    }
320
321    pub fn set_resizable(&mut self, resizable: bool, cx: &mut Context<Self>) {
322        self.resizable = resizable;
323        cx.notify();
324    }
325
326    /// Select the item keyboard commands act on.
327    pub fn set_selected_item(&mut self, id: Option<SharedString>, cx: &mut Context<Self>) {
328        let id = id.filter(|id| self.area(id).is_some());
329        if self.selected != id {
330            self.selected = id;
331            cx.notify();
332        }
333    }
334
335    /// Move the item `id` so its leading column is `col` and its top row
336    /// `row`, pushing whatever it lands on out of the way. Returns whether the
337    /// layout changed; a refused or no-op move returns `false`.
338    pub fn move_item(&mut self, id: &str, col: usize, row: usize, cx: &mut Context<Self>) -> bool {
339        let next = layout::move_item(&self.areas, id, col, row, self.rules());
340        self.commit(next, cx)
341    }
342
343    /// Resize the item `id` to cover `col_span` columns and `row_span` rows,
344    /// clamped to its limits. Returns whether the layout changed.
345    pub fn resize_item(
346        &mut self,
347        id: &str,
348        col_span: usize,
349        row_span: usize,
350        cx: &mut Context<Self>,
351    ) -> bool {
352        let next = layout::resize_item(&self.areas, id, col_span, row_span, self.rules());
353        self.commit(next, cx)
354    }
355
356    fn rules(&self) -> LayoutRules {
357        LayoutRules {
358            columns: self.columns,
359            compaction: self.compaction,
360            prevent_collision: self.prevent_collision,
361        }
362    }
363
364    fn normalize(&mut self) {
365        let rules = self.rules();
366        layout::normalize(&mut self.areas, rules);
367    }
368
369    fn commit(&mut self, next: Option<Vec<GridArea>>, cx: &mut Context<Self>) -> bool {
370        match next {
371            Some(next) if next != self.areas => {
372                self.areas = next;
373                cx.emit(GridLayoutEvent::Change(self.areas.clone()));
374                cx.notify();
375                true
376            }
377            _ => false,
378        }
379    }
380
381    /// The layout to draw: a gesture's preview while one is in progress.
382    pub(crate) fn displayed_areas(&self, cx: &App) -> &[GridArea] {
383        match &self.drag {
384            Some(drag) if cx.has_active_drag() => &drag.preview,
385            _ => &self.areas,
386        }
387    }
388
389    /// The active gesture, unless GPUI has already abandoned the drag.
390    pub(crate) fn active_drag(&self, cx: &App) -> Option<&GridDrag> {
391        self.drag.as_ref().filter(|_| cx.has_active_drag())
392    }
393
394    pub(crate) fn is_gesture_allowed(&self, id: &str, gesture: GridGesture) -> bool {
395        let allowed = match gesture {
396            GridGesture::Move => self.draggable,
397            GridGesture::Resize => self.resizable,
398        };
399        allowed && self.area(id).is_some_and(|area| !area.locked)
400    }
401
402    /// The pointer pressed an item: select it and take focus.
403    pub(crate) fn press(
404        &mut self,
405        id: &SharedString,
406        position: Point<Pixels>,
407        window: &mut Window,
408        cx: &mut Context<Self>,
409    ) {
410        self.press_position = Some(position);
411        self.keyboard_active = false;
412        self.select(Some(id.clone()), cx);
413        window.focus(&self.focus_handle, cx);
414        cx.notify();
415    }
416
417    pub(crate) fn begin_drag(
418        &mut self,
419        id: SharedString,
420        gesture: GridGesture,
421        window: &mut Window,
422        cx: &mut Context<Self>,
423    ) {
424        let origin = self
425            .press_position
426            .take()
427            .unwrap_or_else(|| window.mouse_position());
428        self.select(Some(id.clone()), cx);
429        self.drag = Some(GridDrag {
430            item: id,
431            gesture,
432            origin,
433            start: self.areas.clone(),
434            preview: self.areas.clone(),
435            delta: Point::default(),
436            metrics: None,
437        });
438        cx.notify();
439    }
440
441    /// Follow the pointer to `position` over a grid laid out in `bounds`, with
442    /// rows `row_height` tall and `gap` between cells.
443    pub(crate) fn drag_to(
444        &mut self,
445        position: Point<Pixels>,
446        bounds: Bounds<Pixels>,
447        row_height: Pixels,
448        gap: Size<Pixels>,
449        cx: &mut Context<Self>,
450    ) {
451        let metrics = GridMetrics::new(bounds.size.width, self.columns, row_height, gap);
452        let rules = self.rules();
453        let Some(drag) = self.drag.as_mut() else {
454            return;
455        };
456        let Some(start) = drag.start_area().cloned() else {
457            return;
458        };
459        drag.delta = position - drag.origin;
460        drag.metrics = Some(metrics);
461
462        let next = match drag.gesture {
463            GridGesture::Move => {
464                let left = metrics.left(start.col) + drag.delta.x;
465                let top = metrics.top(start.row) + drag.delta.y;
466                let col = metrics
467                    .column_at(left)
468                    .min(self.columns.saturating_sub(start.col_span));
469                layout::move_item(&drag.start, &start.id, col, metrics.row_at(top), rules)
470            }
471            GridGesture::Resize => {
472                let col_span =
473                    metrics.columns_spanning(metrics.span_width(start.col_span) + drag.delta.x);
474                let row_span =
475                    metrics.rows_spanning(metrics.span_height(start.row_span) + drag.delta.y);
476                layout::resize_item(&drag.start, &start.id, col_span, row_span, rules)
477            }
478        };
479        // A refused step keeps the last accepted preview.
480        if let Some(next) = next {
481            drag.preview = next;
482        }
483        cx.notify();
484    }
485
486    /// The pointer was released: commit the preview.
487    pub(crate) fn end_drag(&mut self, cx: &mut Context<Self>) {
488        self.press_position = None;
489        let Some(drag) = self.drag.take() else {
490            return;
491        };
492        cx.notify();
493        self.commit(Some(drag.preview), cx);
494    }
495
496    /// Abandon the gesture, restoring the layout it started from.
497    pub(crate) fn cancel_drag(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
498        if self.drag.take().is_none() {
499            return false;
500        }
501        cx.stop_active_drag(window);
502        cx.notify();
503        true
504    }
505
506    /// Move the selected item one cell along `cols`, `rows`.
507    pub(crate) fn nudge_selected(
508        &mut self,
509        cols: isize,
510        rows: isize,
511        cx: &mut Context<Self>,
512    ) -> bool {
513        let Some(area) = self.selected_area() else {
514            return false;
515        };
516        if !self.is_gesture_allowed(&area.id, GridGesture::Move) {
517            return false;
518        }
519        let (Some(col), Some(row)) = (
520            area.col.checked_add_signed(cols),
521            area.row.checked_add_signed(rows),
522        ) else {
523            return false;
524        };
525        self.keyboard_active = true;
526        let moved = self.move_item(&area.id, col, row, cx);
527        cx.notify();
528        moved
529    }
530
531    /// Grow or shrink the selected item by one cell along `cols`, `rows`.
532    pub(crate) fn resize_selected(
533        &mut self,
534        cols: isize,
535        rows: isize,
536        cx: &mut Context<Self>,
537    ) -> bool {
538        let Some(area) = self.selected_area() else {
539            return false;
540        };
541        if !self.is_gesture_allowed(&area.id, GridGesture::Resize) {
542            return false;
543        }
544        let (Some(col_span), Some(row_span)) = (
545            area.col_span.checked_add_signed(cols),
546            area.row_span.checked_add_signed(rows),
547        ) else {
548            return false;
549        };
550        self.keyboard_active = true;
551        let resized = self.resize_item(&area.id, col_span, row_span, cx);
552        cx.notify();
553        resized
554    }
555
556    /// Select the next or previous item in reading order.
557    ///
558    /// Returns `false` past the last or before the first item, leaving the
559    /// selection as it is so Tab moves focus out of the grid.
560    pub(crate) fn select_adjacent(&mut self, forward: bool, cx: &mut Context<Self>) -> bool {
561        let mut order = self.areas.iter().collect::<Vec<_>>();
562        order.sort_by_key(|area| (area.row, area.col));
563        if order.is_empty() {
564            return false;
565        }
566        let current = self
567            .selected
568            .as_ref()
569            .and_then(|id| order.iter().position(|area| &area.id == id));
570        let next = match (current, forward) {
571            (None, true) => 0,
572            (None, false) => order.len() - 1,
573            (Some(ix), true) if ix + 1 < order.len() => ix + 1,
574            (Some(ix), false) if ix > 0 => ix - 1,
575            (Some(_), _) => return false,
576        };
577        self.keyboard_active = true;
578        self.select(Some(order[next].id.clone()), cx);
579        cx.notify();
580        true
581    }
582
583    /// Change the selection on the user's behalf, announcing it when it
584    /// actually changes.
585    fn select(&mut self, id: Option<SharedString>, cx: &mut Context<Self>) {
586        if self.selected != id {
587            self.selected = id.clone();
588            cx.emit(GridLayoutEvent::Select(id));
589        }
590    }
591
592    fn selected_area(&self) -> Option<GridArea> {
593        self.selected.as_ref().and_then(|id| self.area(id)).cloned()
594    }
595}
596
597#[cfg(test)]
598mod tests {
599    use super::*;
600    use crate::grid_layout::layout::tests::area;
601    use gpui::{AppContext as _, TestAppContext, size};
602
603    fn state(cx: &mut TestAppContext) -> gpui::Entity<GridLayoutState> {
604        cx.update(|cx| {
605            cx.new(|cx| {
606                GridLayoutState::new(cx)
607                    .default_areas([area("a", 0, 0, 4, 2), area("b", 4, 0, 4, 2)])
608            })
609        })
610    }
611
612    #[test]
613    fn metrics_share_the_width_between_columns_and_gaps() {
614        let metrics = GridMetrics::new(px(1190.), 12, px(40.), size(px(10.), px(20.)));
615        assert_eq!(metrics.column_width, px(90.));
616        assert_eq!(metrics.left(1), px(100.));
617        assert_eq!(metrics.span_width(12), px(1190.));
618        // Rows use the row gap, not the column gap.
619        assert_eq!(metrics.top(1), px(60.));
620        assert_eq!(metrics.span_height(2), px(100.));
621        assert_eq!(metrics.column_at(px(140.)), 1);
622        assert_eq!(metrics.column_at(px(160.)), 2);
623        assert_eq!(metrics.column_at(px(-50.)), 0);
624        assert_eq!(metrics.columns_spanning(metrics.span_width(3) + px(20.)), 3);
625        assert_eq!(metrics.rows_spanning(px(0.)), 1);
626    }
627
628    #[gpui::test]
629    fn test_grid_layout_state_builder(cx: &mut TestAppContext) {
630        let state = cx.update(|cx| {
631            cx.new(|cx| {
632                GridLayoutState::new(cx)
633                    .cols(6)
634                    .compaction(None)
635                    .prevent_collision(true)
636                    .draggable(false)
637                    .resizable(false)
638                    .default_areas([area("a", 4, 5, 4, 1)])
639            })
640        });
641        state.read_with(cx, |state, _| {
642            assert_eq!(state.column_count(), 6);
643            assert_eq!(state.compaction_axis(), None);
644            assert!(state.is_collision_prevented());
645            assert!(!state.is_draggable());
646            assert!(!state.is_resizable());
647            // Fitted to six columns, and left where it is without compaction.
648            let area = state.area("a").unwrap();
649            assert_eq!((area.col, area.row), (2, 5));
650        });
651    }
652
653    #[gpui::test]
654    fn programmatic_changes_emit_the_new_layout(cx: &mut TestAppContext) {
655        let state = state(cx);
656        let events = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
657        cx.update(|cx| {
658            let events = events.clone();
659            cx.subscribe(&state, move |_, event: &GridLayoutEvent, _| {
660                events.borrow_mut().push(event.clone())
661            })
662            .detach();
663        });
664
665        state.update(cx, |state, cx| {
666            assert!(state.move_item("a", 8, 0, cx));
667            // Already there: nothing changes and nothing is emitted.
668            assert!(!state.move_item("a", 8, 0, cx));
669            assert!(state.resize_item("b", 2, 3, cx));
670            state.set_areas([area("c", 0, 0, 1, 1)], cx);
671        });
672
673        let events = events.borrow();
674        assert_eq!(events.len(), 2);
675        let GridLayoutEvent::Change(areas) = &events[1] else {
676            panic!("expected a layout change, got {:?}", events[1]);
677        };
678        assert!(
679            areas
680                .iter()
681                .any(|area| area.id == "b" && area.col_span == 2 && area.row_span == 3)
682        );
683    }
684
685    #[gpui::test]
686    fn keyboard_commands_act_on_the_selected_item(cx: &mut TestAppContext) {
687        let state = state(cx);
688        state.update(cx, |state, cx| {
689            assert!(!state.nudge_selected(1, 0, cx));
690            assert!(state.select_adjacent(true, cx));
691            assert_eq!(state.selected_item().map(|id| id.as_ref()), Some("a"));
692            assert!(state.select_adjacent(true, cx));
693            assert_eq!(state.selected_item().map(|id| id.as_ref()), Some("b"));
694            // Past the last item Tab is not handled, so focus can leave the
695            // grid, and the selection stays where it was.
696            assert!(!state.select_adjacent(true, cx));
697            assert_eq!(state.selected_item().map(|id| id.as_ref()), Some("b"));
698
699            assert!(state.nudge_selected(1, 0, cx));
700            assert_eq!(state.area("b").unwrap().col, 5);
701            // Cannot move past the leading edge or the top.
702            assert!(!state.nudge_selected(0, -1, cx));
703
704            assert!(state.resize_selected(0, 1, cx));
705            assert_eq!(state.area("b").unwrap().row_span, 3);
706            assert!(state.is_keyboard_active());
707
708            state.set_draggable(false, cx);
709            assert!(!state.nudge_selected(-1, 0, cx));
710
711            // Before the first item, Shift-Tab is not handled either.
712            state.set_selected_item(Some("a".into()), cx);
713            assert!(!state.select_adjacent(false, cx));
714        });
715    }
716}