From 4abe14f94a30c0fc9c4e0e3ef14cae7609f7b429 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Fri, 25 Jul 2025 13:19:05 -0400 Subject: [PATCH] keymap ui: Resize columns on double click improvement (#35095) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR improves the behavior of resetting a column's size by double-clicking on its column handle. We now shrink/grow to the side that has more leftover/additional space. We also improved the below 1. dragging was a couple of pixels off to the left because we didn't take the column handle’s width into account. 2. Column dragging now has memory and will shift things to their exact position when reversing a drag before letting the drag handle go. 3. Improved our test infrastructure. 4. Double clicking on a column's header resizes the column Release Notes: - N/A --------- Co-authored-by: Ben Kunkle --- crates/settings_ui/Cargo.toml | 3 + crates/settings_ui/src/ui_components/table.rs | 635 +++++++++++++++--- 2 files changed, 545 insertions(+), 93 deletions(-) diff --git a/crates/settings_ui/Cargo.toml b/crates/settings_ui/Cargo.toml index 02327045fdb2279597342a5d838d356d7b738c73..25f033469d7b00e1b351c7a0385b2de5bc10d9ad 100644 --- a/crates/settings_ui/Cargo.toml +++ b/crates/settings_ui/Cargo.toml @@ -45,3 +45,6 @@ ui_input.workspace = true util.workspace = true workspace-hack.workspace = true workspace.workspace = true + +[dev-dependencies] +db = {"workspace"= true, "features" = ["test-support"]} diff --git a/crates/settings_ui/src/ui_components/table.rs b/crates/settings_ui/src/ui_components/table.rs index 69207f559b89b83b6709bd41ab861e3a71be6616..65778c20eb4bc60f22dd15b634de433dc781cd97 100644 --- a/crates/settings_ui/src/ui_components/table.rs +++ b/crates/settings_ui/src/ui_components/table.rs @@ -2,9 +2,9 @@ use std::{ops::Range, rc::Rc, time::Duration}; use editor::{EditorSettings, ShowScrollbar, scroll::ScrollbarAutoHide}; use gpui::{ - AbsoluteLength, AppContext, Axis, Context, DefiniteLength, DragMoveEvent, Entity, FocusHandle, - Length, ListHorizontalSizingBehavior, ListSizingBehavior, MouseButton, Point, Stateful, Task, - UniformListScrollHandle, WeakEntity, transparent_black, uniform_list, + AbsoluteLength, AppContext, Axis, Context, DefiniteLength, DragMoveEvent, Entity, EntityId, + FocusHandle, Length, ListHorizontalSizingBehavior, ListSizingBehavior, MouseButton, Point, + Stateful, Task, UniformListScrollHandle, WeakEntity, transparent_black, uniform_list, }; use itertools::intersperse_with; @@ -13,10 +13,12 @@ use ui::{ ActiveTheme as _, AnyElement, App, Button, ButtonCommon as _, ButtonStyle, Color, Component, ComponentScope, Div, ElementId, FixedWidth as _, FluentBuilder as _, Indicator, InteractiveElement, IntoElement, ParentElement, Pixels, RegisterComponent, RenderOnce, - Scrollbar, ScrollbarState, StatefulInteractiveElement, Styled, StyledExt as _, + Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, StyledExt as _, StyledTypography, Window, div, example_group_with_title, h_flex, px, single_example, v_flex, }; +const RESIZE_COLUMN_WIDTH: f32 = 5.0; + #[derive(Debug)] struct DraggedColumn(usize); @@ -227,7 +229,7 @@ impl TableInteractionState { .id("column-resize-handle") .absolute() .left_neg_0p5() - .w(px(5.0)) + .w(px(RESIZE_COLUMN_WIDTH)) .h_full(); if resizable_columns @@ -478,6 +480,7 @@ impl ResizeBehavior { pub struct ColumnWidths { widths: [DefiniteLength; COLS], + visible_widths: [DefiniteLength; COLS], cached_bounds_width: Pixels, initialized: bool, } @@ -486,6 +489,7 @@ impl ColumnWidths { pub fn new(_: &mut App) -> Self { Self { widths: [DefiniteLength::default(); COLS], + visible_widths: [DefiniteLength::default(); COLS], cached_bounds_width: Default::default(), initialized: false, } @@ -512,46 +516,105 @@ impl ColumnWidths { let rem_size = window.rem_size(); let initial_sizes = initial_sizes.map(|length| Self::get_fraction(&length, bounds_width, rem_size)); - let mut widths = self + let widths = self .widths .map(|length| Self::get_fraction(&length, bounds_width, rem_size)); - let diff = initial_sizes[double_click_position] - widths[double_click_position]; + let updated_widths = Self::reset_to_initial_size( + double_click_position, + widths, + initial_sizes, + resize_behavior, + ); + self.widths = updated_widths.map(DefiniteLength::Fraction); + self.visible_widths = self.widths; + } - if diff > 0.0 { - let diff_remaining = self.propagate_resize_diff_right( - diff, - double_click_position, - &mut widths, - resize_behavior, - ); + fn reset_to_initial_size( + col_idx: usize, + mut widths: [f32; COLS], + initial_sizes: [f32; COLS], + resize_behavior: &[ResizeBehavior; COLS], + ) -> [f32; COLS] { + // RESET: + // Part 1: + // Figure out if we should shrink/grow the selected column + // Get diff which represents the change in column we want to make initial size delta curr_size = diff + // + // Part 2: We need to decide which side column we should move and where + // + // If we want to grow our column we should check the left/right columns diff to see what side + // has a greater delta than their initial size. Likewise, if we shrink our column we should check + // the left/right column diffs to see what side has the smallest delta. + // + // Part 3: resize + // + // col_idx represents the column handle to the right of an active column + // + // If growing and right has the greater delta { + // shift col_idx to the right + // } else if growing and left has the greater delta { + // shift col_idx - 1 to the left + // } else if shrinking and the right has the greater delta { + // shift + // } { + // + // } + // } + // + // if we need to shrink, then if the right + // + + // DRAGGING + // we get diff which represents the change in the _drag handle_ position + // -diff => dragging left -> + // grow the column to the right of the handle as much as we can shrink columns to the left of the handle + // +diff => dragging right -> growing handles column + // grow the column to the left of the handle as much as we can shrink columns to the right of the handle + // + + let diff = initial_sizes[col_idx] - widths[col_idx]; + + let left_diff = + initial_sizes[..col_idx].iter().sum::() - widths[..col_idx].iter().sum::(); + let right_diff = initial_sizes[col_idx + 1..].iter().sum::() + - widths[col_idx + 1..].iter().sum::(); + + let go_left_first = if diff < 0.0 { + left_diff > right_diff + } else { + left_diff < right_diff + }; + + if !go_left_first { + let diff_remaining = + Self::propagate_resize_diff(diff, col_idx, &mut widths, resize_behavior, 1); - if diff_remaining > 0.0 && double_click_position > 0 { - self.propagate_resize_diff_left( - -diff_remaining, - double_click_position - 1, + if diff_remaining != 0.0 && col_idx > 0 { + Self::propagate_resize_diff( + diff_remaining, + col_idx, &mut widths, resize_behavior, + -1, ); } - } else if double_click_position > 0 { - let diff_remaining = self.propagate_resize_diff_left( - diff, - double_click_position, - &mut widths, - resize_behavior, - ); + } else { + let diff_remaining = + Self::propagate_resize_diff(diff, col_idx, &mut widths, resize_behavior, -1); - if diff_remaining < 0.0 { - self.propagate_resize_diff_right( - -diff_remaining, - double_click_position, + if diff_remaining != 0.0 { + Self::propagate_resize_diff( + diff_remaining, + col_idx, &mut widths, resize_behavior, + 1, ); } } - self.widths = widths.map(DefiniteLength::Fraction); + + widths } fn on_drag_move( @@ -569,98 +632,102 @@ impl ColumnWidths { let bounds_width = bounds.right() - bounds.left(); let col_idx = drag_event.drag(cx).0; + let column_handle_width = Self::get_fraction( + &DefiniteLength::Absolute(AbsoluteLength::Pixels(px(RESIZE_COLUMN_WIDTH))), + bounds_width, + rem_size, + ); + let mut widths = self .widths .map(|length| Self::get_fraction(&length, bounds_width, rem_size)); for length in widths[0..=col_idx].iter() { - col_position += length; + col_position += length + column_handle_width; } let mut total_length_ratio = col_position; for length in widths[col_idx + 1..].iter() { total_length_ratio += length; } + total_length_ratio += (COLS - 1 - col_idx) as f32 * column_handle_width; let drag_fraction = (drag_position.x - bounds.left()) / bounds_width; let drag_fraction = drag_fraction * total_length_ratio; - let diff = drag_fraction - col_position; + let diff = drag_fraction - col_position - column_handle_width / 2.0; - let is_dragging_right = diff > 0.0; + Self::drag_column_handle(diff, col_idx, &mut widths, resize_behavior); - if is_dragging_right { - self.propagate_resize_diff_right(diff, col_idx, &mut widths, resize_behavior); - } else { - // Resize behavior should be improved in the future by also seeking to the right column when there's not enough space - self.propagate_resize_diff_left(diff, col_idx, &mut widths, resize_behavior); - } - self.widths = widths.map(DefiniteLength::Fraction); + self.visible_widths = widths.map(DefiniteLength::Fraction); } - fn propagate_resize_diff_right( - &self, + fn drag_column_handle( diff: f32, col_idx: usize, widths: &mut [f32; COLS], resize_behavior: &[ResizeBehavior; COLS], - ) -> f32 { - let mut diff_remaining = diff; - let mut curr_column = col_idx + 1; - - while diff_remaining > 0.0 && curr_column < COLS { - let Some(min_size) = resize_behavior[curr_column - 1].min_size() else { - curr_column += 1; - continue; - }; - - let mut curr_width = widths[curr_column] - diff_remaining; - - diff_remaining = 0.0; - if min_size > curr_width { - diff_remaining += min_size - curr_width; - curr_width = min_size; - } - widths[curr_column] = curr_width; - curr_column += 1; + ) { + // if diff > 0.0 then go right + if diff > 0.0 { + Self::propagate_resize_diff(diff, col_idx, widths, resize_behavior, 1); + } else { + Self::propagate_resize_diff(-diff, col_idx + 1, widths, resize_behavior, -1); } - - widths[col_idx] = widths[col_idx] + (diff - diff_remaining); - return diff_remaining; } - fn propagate_resize_diff_left( - &mut self, + fn propagate_resize_diff( diff: f32, - mut curr_column: usize, + col_idx: usize, widths: &mut [f32; COLS], resize_behavior: &[ResizeBehavior; COLS], + direction: i8, ) -> f32 { let mut diff_remaining = diff; - let col_idx = curr_column; - while diff_remaining < 0.0 { + if resize_behavior[col_idx].min_size().is_none() { + return diff; + } + + let step_right; + let step_left; + if direction < 0 { + step_right = 0; + step_left = 1; + } else { + step_right = 1; + step_left = 0; + } + if col_idx == 0 && direction < 0 { + return diff; + } + let mut curr_column = col_idx + step_right - step_left; + + while diff_remaining != 0.0 && curr_column < COLS { let Some(min_size) = resize_behavior[curr_column].min_size() else { if curr_column == 0 { break; } - curr_column -= 1; + curr_column -= step_left; + curr_column += step_right; continue; }; - let mut curr_width = widths[curr_column] + diff_remaining; + let curr_width = widths[curr_column] - diff_remaining; + widths[curr_column] = curr_width; - diff_remaining = 0.0; - if curr_width < min_size { - diff_remaining = curr_width - min_size; - curr_width = min_size + if min_size > curr_width { + diff_remaining = min_size - curr_width; + widths[curr_column] = min_size; + } else { + diff_remaining = 0.0; + break; } - - widths[curr_column] = curr_width; if curr_column == 0 { break; } - curr_column -= 1; + curr_column -= step_left; + curr_column += step_right; } - widths[col_idx + 1] = widths[col_idx + 1] - (diff - diff_remaining); + widths[col_idx] = widths[col_idx] + (diff - diff_remaining); return diff_remaining; } @@ -686,7 +753,7 @@ impl TableWidths { fn lengths(&self, cx: &App) -> [Length; COLS] { self.current .as_ref() - .map(|entity| entity.read(cx).widths.map(Length::Definite)) + .map(|entity| entity.read(cx).visible_widths.map(Length::Definite)) .unwrap_or(self.initial.map(Length::Definite)) } } @@ -799,6 +866,7 @@ impl Table { if !widths.initialized { widths.initialized = true; widths.widths = table_widths.initial; + widths.visible_widths = widths.widths; } }) } @@ -888,11 +956,24 @@ pub fn render_row( pub fn render_header( headers: [impl IntoElement; COLS], table_context: TableRenderContext, + columns_widths: Option<( + WeakEntity>, + [ResizeBehavior; COLS], + [DefiniteLength; COLS], + )>, + entity_id: Option, cx: &mut App, ) -> impl IntoElement { let column_widths = table_context .column_widths .map_or([None; COLS], |widths| widths.map(Some)); + + let element_id = entity_id + .map(|entity| entity.to_string()) + .unwrap_or_default(); + + let shared_element_id: SharedString = format!("table-{}", element_id).into(); + div() .flex() .flex_row() @@ -902,12 +983,39 @@ pub fn render_header( .p_2() .border_b_1() .border_color(cx.theme().colors().border) - .children( - headers - .into_iter() - .zip(column_widths) - .map(|(h, width)| base_cell_style_text(width, cx).child(h)), - ) + .children(headers.into_iter().enumerate().zip(column_widths).map( + |((header_idx, h), width)| { + base_cell_style_text(width, cx) + .child(h) + .id(ElementId::NamedInteger( + shared_element_id.clone(), + header_idx as u64, + )) + .when_some( + columns_widths.as_ref().cloned(), + |this, (column_widths, resizables, initial_sizes)| { + if resizables[header_idx].is_resizable() { + this.on_click(move |event, window, cx| { + if event.down.click_count > 1 { + column_widths + .update(cx, |column, _| { + column.on_double_click( + header_idx, + &initial_sizes, + &resizables, + window, + ); + }) + .ok(); + } + }) + } else { + this + } + }, + ) + }, + )) } #[derive(Clone)] @@ -939,6 +1047,12 @@ impl RenderOnce for Table { .and_then(|widths| Some((widths.current.as_ref()?, widths.resizable))) .map(|(curr, resize_behavior)| (curr.downgrade(), resize_behavior)); + let current_widths_with_initial_sizes = self + .col_widths + .as_ref() + .and_then(|widths| Some((widths.current.as_ref()?, widths.resizable, widths.initial))) + .map(|(curr, resize_behavior, initial)| (curr.downgrade(), resize_behavior, initial)); + let scroll_track_size = px(16.); let h_scroll_offset = if interaction_state .as_ref() @@ -958,7 +1072,13 @@ impl RenderOnce for Table { .h_full() .v_flex() .when_some(self.headers.take(), |this, headers| { - this.child(render_header(headers, table_context.clone(), cx)) + this.child(render_header( + headers, + table_context.clone(), + current_widths_with_initial_sizes, + interaction_state.as_ref().map(Entity::entity_id), + cx, + )) }) .when_some(current_widths, { |this, (widths, resize_behavior)| { @@ -972,19 +1092,28 @@ impl RenderOnce for Table { .ok(); } }) - .on_children_prepainted(move |bounds, _, cx| { + .on_children_prepainted({ + let widths = widths.clone(); + move |bounds, _, cx| { + widths + .update(cx, |widths, _| { + // This works because all children x axis bounds are the same + widths.cached_bounds_width = + bounds[0].right() - bounds[0].left(); + }) + .ok(); + } + }) + .on_drop::(move |_, _, cx| { widths .update(cx, |widths, _| { - // This works because all children x axis bounds are the same - widths.cached_bounds_width = bounds[0].right() - bounds[0].left(); + widths.widths = widths.visible_widths; }) .ok(); + // Finish the resize operation }) } }) - .on_drop::(|_, _, _| { - // Finish the resize operation - }) .child( div() .flex_grow() @@ -1313,3 +1442,323 @@ impl Component for Table<3> { ) } } + +#[cfg(test)] +mod test { + use super::*; + + fn is_almost_eq(a: &[f32], b: &[f32]) -> bool { + a.len() == b.len() && a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-6) + } + + fn cols_to_str(cols: &[f32; COLS], total_size: f32) -> String { + cols.map(|f| "*".repeat(f32::round(f * total_size) as usize)) + .join("|") + } + + fn parse_resize_behavior( + input: &str, + total_size: f32, + ) -> [ResizeBehavior; COLS] { + let mut resize_behavior = [ResizeBehavior::None; COLS]; + let mut max_index = 0; + for (index, col) in input.split('|').enumerate() { + if col.starts_with('X') || col.is_empty() { + resize_behavior[index] = ResizeBehavior::None; + } else if col.starts_with('*') { + resize_behavior[index] = ResizeBehavior::MinSize(col.len() as f32 / total_size); + } else { + panic!("invalid test input: unrecognized resize behavior: {}", col); + } + max_index = index; + } + + if max_index + 1 != COLS { + panic!("invalid test input: too many columns"); + } + resize_behavior + } + + mod reset_column_size { + use super::*; + + fn parse(input: &str) -> ([f32; COLS], f32, Option) { + let mut widths = [f32::NAN; COLS]; + let mut column_index = None; + for (index, col) in input.split('|').enumerate() { + widths[index] = col.len() as f32; + if col.starts_with('X') { + column_index = Some(index); + } + } + + for w in widths { + assert!(w.is_finite(), "incorrect number of columns"); + } + let total = widths.iter().sum::(); + for width in &mut widths { + *width /= total; + } + (widths, total, column_index) + } + + #[track_caller] + fn check_reset_size( + initial_sizes: &str, + widths: &str, + expected: &str, + resize_behavior: &str, + ) { + let (initial_sizes, total_1, None) = parse::(initial_sizes) else { + panic!("invalid test input: initial sizes should not be marked"); + }; + let (widths, total_2, Some(column_index)) = parse::(widths) else { + panic!("invalid test input: widths should be marked"); + }; + assert_eq!( + total_1, total_2, + "invalid test input: total width not the same {total_1}, {total_2}" + ); + let (expected, total_3, None) = parse::(expected) else { + panic!("invalid test input: expected should not be marked: {expected:?}"); + }; + assert_eq!( + total_2, total_3, + "invalid test input: total width not the same" + ); + let resize_behavior = parse_resize_behavior::(resize_behavior, total_1); + let result = ColumnWidths::reset_to_initial_size( + column_index, + widths, + initial_sizes, + &resize_behavior, + ); + let is_eq = is_almost_eq(&result, &expected); + if !is_eq { + let result_str = cols_to_str(&result, total_1); + let expected_str = cols_to_str(&expected, total_1); + panic!( + "resize failed\ncomputed: {result_str}\nexpected: {expected_str}\n\ncomputed values: {result:?}\nexpected values: {expected:?}\n:minimum widths: {resize_behavior:?}" + ); + } + } + + macro_rules! check_reset_size { + (columns: $cols:expr, starting: $initial:expr, snapshot: $current:expr, expected: $expected:expr, resizing: $resizing:expr $(,)?) => { + check_reset_size::<$cols>($initial, $current, $expected, $resizing); + }; + ($name:ident, columns: $cols:expr, starting: $initial:expr, snapshot: $current:expr, expected: $expected:expr, minimums: $resizing:expr $(,)?) => { + #[test] + fn $name() { + check_reset_size::<$cols>($initial, $current, $expected, $resizing); + } + }; + } + + check_reset_size!( + basic_right, + columns: 5, + starting: "**|**|**|**|**", + snapshot: "**|**|X|***|**", + expected: "**|**|**|**|**", + minimums: "X|*|*|*|*", + ); + + check_reset_size!( + basic_left, + columns: 5, + starting: "**|**|**|**|**", + snapshot: "**|**|***|X|**", + expected: "**|**|**|**|**", + minimums: "X|*|*|*|**", + ); + + check_reset_size!( + squashed_left_reset_col2, + columns: 6, + starting: "*|***|**|**|****|*", + snapshot: "*|*|X|*|*|********", + expected: "*|*|**|*|*|*******", + minimums: "X|*|*|*|*|*", + ); + + check_reset_size!( + grow_cascading_right, + columns: 6, + starting: "*|***|****|**|***|*", + snapshot: "*|***|X|**|**|*****", + expected: "*|***|****|*|*|****", + minimums: "X|*|*|*|*|*", + ); + + check_reset_size!( + squashed_right_reset_col4, + columns: 6, + starting: "*|***|**|**|****|*", + snapshot: "*|********|*|*|X|*", + expected: "*|*****|*|*|****|*", + minimums: "X|*|*|*|*|*", + ); + + check_reset_size!( + reset_col6_right, + columns: 6, + starting: "*|***|**|***|***|**", + snapshot: "*|***|**|***|**|XXX", + expected: "*|***|**|***|***|**", + minimums: "X|*|*|*|*|*", + ); + + check_reset_size!( + reset_col6_left, + columns: 6, + starting: "*|***|**|***|***|**", + snapshot: "*|***|**|***|****|X", + expected: "*|***|**|***|***|**", + minimums: "X|*|*|*|*|*", + ); + + check_reset_size!( + last_column_grow_cascading, + columns: 6, + starting: "*|***|**|**|**|***", + snapshot: "*|*******|*|**|*|X", + expected: "*|******|*|*|*|***", + minimums: "X|*|*|*|*|*", + ); + + check_reset_size!( + goes_left_when_left_has_extreme_diff, + columns: 6, + starting: "*|***|****|**|**|***", + snapshot: "*|********|X|*|**|**", + expected: "*|*****|****|*|**|**", + minimums: "X|*|*|*|*|*", + ); + + check_reset_size!( + basic_shrink_right, + columns: 6, + starting: "**|**|**|**|**|**", + snapshot: "**|**|XXX|*|**|**", + expected: "**|**|**|**|**|**", + minimums: "X|*|*|*|*|*", + ); + + check_reset_size!( + shrink_should_go_left, + columns: 6, + starting: "*|***|**|*|*|*", + snapshot: "*|*|XXX|**|*|*", + expected: "*|**|**|**|*|*", + minimums: "X|*|*|*|*|*", + ); + + check_reset_size!( + shrink_should_go_right, + columns: 6, + starting: "*|***|**|**|**|*", + snapshot: "*|****|XXX|*|*|*", + expected: "*|****|**|**|*|*", + minimums: "X|*|*|*|*|*", + ); + } + + mod drag_handle { + use super::*; + + fn parse(input: &str) -> ([f32; COLS], f32, Option) { + let mut widths = [f32::NAN; COLS]; + let column_index = input.replace("*", "").find("I"); + for (index, col) in input.replace("I", "|").split('|').enumerate() { + widths[index] = col.len() as f32; + } + + for w in widths { + assert!(w.is_finite(), "incorrect number of columns"); + } + let total = widths.iter().sum::(); + for width in &mut widths { + *width /= total; + } + (widths, total, column_index) + } + + #[track_caller] + fn check( + distance: i32, + widths: &str, + expected: &str, + resize_behavior: &str, + ) { + let (mut widths, total_1, Some(column_index)) = parse::(widths) else { + panic!("invalid test input: widths should be marked"); + }; + let (expected, total_2, None) = parse::(expected) else { + panic!("invalid test input: expected should not be marked: {expected:?}"); + }; + assert_eq!( + total_1, total_2, + "invalid test input: total width not the same" + ); + let resize_behavior = parse_resize_behavior::(resize_behavior, total_1); + + let distance = distance as f32 / total_1; + + let result = ColumnWidths::drag_column_handle( + distance, + column_index, + &mut widths, + &resize_behavior, + ); + + let is_eq = is_almost_eq(&widths, &expected); + if !is_eq { + let result_str = cols_to_str(&widths, total_1); + let expected_str = cols_to_str(&expected, total_1); + panic!( + "resize failed\ncomputed: {result_str}\nexpected: {expected_str}\n\ncomputed values: {result:?}\nexpected values: {expected:?}\n:minimum widths: {resize_behavior:?}" + ); + } + } + + macro_rules! check { + (columns: $cols:expr, distance: $dist:expr, snapshot: $current:expr, expected: $expected:expr, resizing: $resizing:expr $(,)?) => { + check!($cols, $dist, $snapshot, $expected, $resizing); + }; + ($name:ident, columns: $cols:expr, distance: $dist:expr, snapshot: $current:expr, expected: $expected:expr, minimums: $resizing:expr $(,)?) => { + #[test] + fn $name() { + check::<$cols>($dist, $current, $expected, $resizing); + } + }; + } + + check!( + basic_right_drag, + columns: 3, + distance: 1, + snapshot: "**|**I**", + expected: "**|***|*", + minimums: "X|*|*", + ); + + check!( + drag_left_against_mins, + columns: 5, + distance: -1, + snapshot: "*|*|*|*I*******", + expected: "*|*|*|*|*******", + minimums: "X|*|*|*|*", + ); + + check!( + drag_left, + columns: 5, + distance: -2, + snapshot: "*|*|*|*****I***", + expected: "*|*|*|***|*****", + minimums: "X|*|*|*|*", + ); + } +}