Closes #ISSUE Move the data table component created for the Keymap Editor to the UI crate. Additionally includes simplifications to the scrollbar component in UI necessary for the table component to support scrollbar configurations, and a fix for an issue with the table component where when used with the `.row` API instead of `uniform_list` the rows would render on top of each other. Release Notes: - N/A *or* Added/Fixed/Improved ...
1378 lines
50 KiB
Rust
1378 lines
50 KiB
Rust
use std::{ops::Range, rc::Rc};
|
|
|
|
use gpui::{
|
|
AbsoluteLength, AppContext, Context, DefiniteLength, DragMoveEvent, Entity, EntityId,
|
|
FocusHandle, Length, ListHorizontalSizingBehavior, ListSizingBehavior, Point, Stateful,
|
|
UniformListScrollHandle, WeakEntity, transparent_black, uniform_list,
|
|
};
|
|
|
|
use crate::{
|
|
ActiveTheme as _, AnyElement, App, Button, ButtonCommon as _, ButtonStyle, Color, Component,
|
|
ComponentScope, Div, ElementId, FixedWidth as _, FluentBuilder as _, Indicator,
|
|
InteractiveElement, IntoElement, ParentElement, Pixels, RegisterComponent, RenderOnce,
|
|
ScrollableHandle, Scrollbars, SharedString, StatefulInteractiveElement, Styled, StyledExt as _,
|
|
StyledTypography, Window, WithScrollbar, div, example_group_with_title, h_flex, px,
|
|
single_example, v_flex,
|
|
};
|
|
use itertools::intersperse_with;
|
|
|
|
const RESIZE_COLUMN_WIDTH: f32 = 8.0;
|
|
|
|
#[derive(Debug)]
|
|
struct DraggedColumn(usize);
|
|
|
|
struct UniformListData<const COLS: usize> {
|
|
render_item_fn: Box<dyn Fn(Range<usize>, &mut Window, &mut App) -> Vec<[AnyElement; COLS]>>,
|
|
element_id: ElementId,
|
|
row_count: usize,
|
|
}
|
|
|
|
enum TableContents<const COLS: usize> {
|
|
Vec(Vec<[AnyElement; COLS]>),
|
|
UniformList(UniformListData<COLS>),
|
|
}
|
|
|
|
impl<const COLS: usize> TableContents<COLS> {
|
|
fn rows_mut(&mut self) -> Option<&mut Vec<[AnyElement; COLS]>> {
|
|
match self {
|
|
TableContents::Vec(rows) => Some(rows),
|
|
TableContents::UniformList(_) => None,
|
|
}
|
|
}
|
|
|
|
fn len(&self) -> usize {
|
|
match self {
|
|
TableContents::Vec(rows) => rows.len(),
|
|
TableContents::UniformList(data) => data.row_count,
|
|
}
|
|
}
|
|
|
|
fn is_empty(&self) -> bool {
|
|
self.len() == 0
|
|
}
|
|
}
|
|
|
|
pub struct TableInteractionState {
|
|
pub focus_handle: FocusHandle,
|
|
pub scroll_handle: UniformListScrollHandle,
|
|
pub custom_scrollbar: Option<Scrollbars>,
|
|
}
|
|
|
|
impl TableInteractionState {
|
|
pub fn new(cx: &mut App) -> Self {
|
|
Self {
|
|
focus_handle: cx.focus_handle(),
|
|
scroll_handle: UniformListScrollHandle::new(),
|
|
custom_scrollbar: None,
|
|
}
|
|
}
|
|
|
|
pub fn with_custom_scrollbar(mut self, custom_scrollbar: Scrollbars) -> Self {
|
|
self.custom_scrollbar = Some(custom_scrollbar);
|
|
self
|
|
}
|
|
|
|
pub fn scroll_offset(&self) -> Point<Pixels> {
|
|
self.scroll_handle.offset()
|
|
}
|
|
|
|
pub fn set_scroll_offset(&self, offset: Point<Pixels>) {
|
|
self.scroll_handle.set_offset(offset);
|
|
}
|
|
|
|
pub fn listener<E: ?Sized>(
|
|
this: &Entity<Self>,
|
|
f: impl Fn(&mut Self, &E, &mut Window, &mut Context<Self>) + 'static,
|
|
) -> impl Fn(&E, &mut Window, &mut App) + 'static {
|
|
let view = this.downgrade();
|
|
move |e: &E, window: &mut Window, cx: &mut App| {
|
|
view.update(cx, |view, cx| f(view, e, window, cx)).ok();
|
|
}
|
|
}
|
|
|
|
fn render_resize_handles<const COLS: usize>(
|
|
&self,
|
|
column_widths: &[Length; COLS],
|
|
resizable_columns: &[TableResizeBehavior; COLS],
|
|
initial_sizes: [DefiniteLength; COLS],
|
|
columns: Option<Entity<TableColumnWidths<COLS>>>,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) -> AnyElement {
|
|
let spacers = column_widths
|
|
.iter()
|
|
.map(|width| base_cell_style(Some(*width)).into_any_element());
|
|
|
|
let mut column_ix = 0;
|
|
let resizable_columns_slice = *resizable_columns;
|
|
let mut resizable_columns = resizable_columns.iter();
|
|
|
|
let dividers = intersperse_with(spacers, || {
|
|
window.with_id(column_ix, |window| {
|
|
let mut resize_divider = div()
|
|
// This is required because this is evaluated at a different time than the use_state call above
|
|
.id(column_ix)
|
|
.relative()
|
|
.top_0()
|
|
.w_px()
|
|
.h_full()
|
|
.bg(cx.theme().colors().border.opacity(0.8));
|
|
|
|
let mut resize_handle = div()
|
|
.id("column-resize-handle")
|
|
.absolute()
|
|
.left_neg_0p5()
|
|
.w(px(RESIZE_COLUMN_WIDTH))
|
|
.h_full();
|
|
|
|
if resizable_columns
|
|
.next()
|
|
.is_some_and(TableResizeBehavior::is_resizable)
|
|
{
|
|
let hovered = window.use_state(cx, |_window, _cx| false);
|
|
|
|
resize_divider = resize_divider.when(*hovered.read(cx), |div| {
|
|
div.bg(cx.theme().colors().border_focused)
|
|
});
|
|
|
|
resize_handle = resize_handle
|
|
.on_hover(move |&was_hovered, _, cx| hovered.write(cx, was_hovered))
|
|
.cursor_col_resize()
|
|
.when_some(columns.clone(), |this, columns| {
|
|
this.on_click(move |event, window, cx| {
|
|
if event.click_count() >= 2 {
|
|
columns.update(cx, |columns, _| {
|
|
columns.on_double_click(
|
|
column_ix,
|
|
&initial_sizes,
|
|
&resizable_columns_slice,
|
|
window,
|
|
);
|
|
})
|
|
}
|
|
|
|
cx.stop_propagation();
|
|
})
|
|
})
|
|
.on_drag(DraggedColumn(column_ix), |_, _offset, _window, cx| {
|
|
cx.new(|_cx| gpui::Empty)
|
|
})
|
|
}
|
|
|
|
column_ix += 1;
|
|
resize_divider.child(resize_handle).into_any_element()
|
|
})
|
|
});
|
|
|
|
h_flex()
|
|
.id("resize-handles")
|
|
.absolute()
|
|
.inset_0()
|
|
.w_full()
|
|
.children(dividers)
|
|
.into_any_element()
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
|
pub enum TableResizeBehavior {
|
|
None,
|
|
Resizable,
|
|
MinSize(f32),
|
|
}
|
|
|
|
impl TableResizeBehavior {
|
|
pub fn is_resizable(&self) -> bool {
|
|
*self != TableResizeBehavior::None
|
|
}
|
|
|
|
pub fn min_size(&self) -> Option<f32> {
|
|
match self {
|
|
TableResizeBehavior::None => None,
|
|
TableResizeBehavior::Resizable => Some(0.05),
|
|
TableResizeBehavior::MinSize(min_size) => Some(*min_size),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct TableColumnWidths<const COLS: usize> {
|
|
widths: [DefiniteLength; COLS],
|
|
visible_widths: [DefiniteLength; COLS],
|
|
cached_bounds_width: Pixels,
|
|
initialized: bool,
|
|
}
|
|
|
|
impl<const COLS: usize> TableColumnWidths<COLS> {
|
|
pub fn new(_: &mut App) -> Self {
|
|
Self {
|
|
widths: [DefiniteLength::default(); COLS],
|
|
visible_widths: [DefiniteLength::default(); COLS],
|
|
cached_bounds_width: Default::default(),
|
|
initialized: false,
|
|
}
|
|
}
|
|
|
|
fn get_fraction(length: &DefiniteLength, bounds_width: Pixels, rem_size: Pixels) -> f32 {
|
|
match length {
|
|
DefiniteLength::Absolute(AbsoluteLength::Pixels(pixels)) => *pixels / bounds_width,
|
|
DefiniteLength::Absolute(AbsoluteLength::Rems(rems_width)) => {
|
|
rems_width.to_pixels(rem_size) / bounds_width
|
|
}
|
|
DefiniteLength::Fraction(fraction) => *fraction,
|
|
}
|
|
}
|
|
|
|
fn on_double_click(
|
|
&mut self,
|
|
double_click_position: usize,
|
|
initial_sizes: &[DefiniteLength; COLS],
|
|
resize_behavior: &[TableResizeBehavior; COLS],
|
|
window: &mut Window,
|
|
) {
|
|
let bounds_width = self.cached_bounds_width;
|
|
let rem_size = window.rem_size();
|
|
let initial_sizes =
|
|
initial_sizes.map(|length| Self::get_fraction(&length, bounds_width, rem_size));
|
|
let widths = self
|
|
.widths
|
|
.map(|length| Self::get_fraction(&length, bounds_width, rem_size));
|
|
|
|
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;
|
|
}
|
|
|
|
fn reset_to_initial_size(
|
|
col_idx: usize,
|
|
mut widths: [f32; COLS],
|
|
initial_sizes: [f32; COLS],
|
|
resize_behavior: &[TableResizeBehavior; 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::<f32>() - widths[..col_idx].iter().sum::<f32>();
|
|
let right_diff = initial_sizes[col_idx + 1..].iter().sum::<f32>()
|
|
- widths[col_idx + 1..].iter().sum::<f32>();
|
|
|
|
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 && col_idx > 0 {
|
|
Self::propagate_resize_diff(
|
|
diff_remaining,
|
|
col_idx,
|
|
&mut widths,
|
|
resize_behavior,
|
|
-1,
|
|
);
|
|
}
|
|
} 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(
|
|
diff_remaining,
|
|
col_idx,
|
|
&mut widths,
|
|
resize_behavior,
|
|
1,
|
|
);
|
|
}
|
|
}
|
|
|
|
widths
|
|
}
|
|
|
|
fn on_drag_move(
|
|
&mut self,
|
|
drag_event: &DragMoveEvent<DraggedColumn>,
|
|
resize_behavior: &[TableResizeBehavior; COLS],
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let drag_position = drag_event.event.position;
|
|
let bounds = drag_event.bounds;
|
|
|
|
let mut col_position = 0.0;
|
|
let rem_size = window.rem_size();
|
|
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 + 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 - column_handle_width / 2.0;
|
|
|
|
Self::drag_column_handle(diff, col_idx, &mut widths, resize_behavior);
|
|
|
|
self.visible_widths = widths.map(DefiniteLength::Fraction);
|
|
}
|
|
|
|
fn drag_column_handle(
|
|
diff: f32,
|
|
col_idx: usize,
|
|
widths: &mut [f32; COLS],
|
|
resize_behavior: &[TableResizeBehavior; COLS],
|
|
) {
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
fn propagate_resize_diff(
|
|
diff: f32,
|
|
col_idx: usize,
|
|
widths: &mut [f32; COLS],
|
|
resize_behavior: &[TableResizeBehavior; COLS],
|
|
direction: i8,
|
|
) -> f32 {
|
|
let mut diff_remaining = diff;
|
|
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 -= step_left;
|
|
curr_column += step_right;
|
|
continue;
|
|
};
|
|
|
|
let curr_width = widths[curr_column] - diff_remaining;
|
|
widths[curr_column] = curr_width;
|
|
|
|
if min_size > curr_width {
|
|
diff_remaining = min_size - curr_width;
|
|
widths[curr_column] = min_size;
|
|
} else {
|
|
diff_remaining = 0.0;
|
|
break;
|
|
}
|
|
if curr_column == 0 {
|
|
break;
|
|
}
|
|
curr_column -= step_left;
|
|
curr_column += step_right;
|
|
}
|
|
widths[col_idx] = widths[col_idx] + (diff - diff_remaining);
|
|
|
|
diff_remaining
|
|
}
|
|
}
|
|
|
|
pub struct TableWidths<const COLS: usize> {
|
|
initial: [DefiniteLength; COLS],
|
|
current: Option<Entity<TableColumnWidths<COLS>>>,
|
|
resizable: [TableResizeBehavior; COLS],
|
|
}
|
|
|
|
impl<const COLS: usize> TableWidths<COLS> {
|
|
pub fn new(widths: [impl Into<DefiniteLength>; COLS]) -> Self {
|
|
let widths = widths.map(Into::into);
|
|
|
|
TableWidths {
|
|
initial: widths,
|
|
current: None,
|
|
resizable: [TableResizeBehavior::None; COLS],
|
|
}
|
|
}
|
|
|
|
fn lengths(&self, cx: &App) -> [Length; COLS] {
|
|
self.current
|
|
.as_ref()
|
|
.map(|entity| entity.read(cx).visible_widths.map(Length::Definite))
|
|
.unwrap_or(self.initial.map(Length::Definite))
|
|
}
|
|
}
|
|
|
|
/// A table component
|
|
#[derive(RegisterComponent, IntoElement)]
|
|
pub struct Table<const COLS: usize = 3> {
|
|
striped: bool,
|
|
width: Option<Length>,
|
|
headers: Option<[AnyElement; COLS]>,
|
|
rows: TableContents<COLS>,
|
|
interaction_state: Option<WeakEntity<TableInteractionState>>,
|
|
col_widths: Option<TableWidths<COLS>>,
|
|
map_row: Option<Rc<dyn Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement>>,
|
|
empty_table_callback: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>>,
|
|
}
|
|
|
|
impl<const COLS: usize> Table<COLS> {
|
|
/// number of headers provided.
|
|
pub fn new() -> Self {
|
|
Self {
|
|
striped: false,
|
|
width: None,
|
|
headers: None,
|
|
rows: TableContents::Vec(Vec::new()),
|
|
interaction_state: None,
|
|
map_row: None,
|
|
empty_table_callback: None,
|
|
col_widths: None,
|
|
}
|
|
}
|
|
|
|
/// Enables uniform list rendering.
|
|
/// The provided function will be passed directly to the `uniform_list` element.
|
|
/// Therefore, if this method is called, any calls to [`Table::row`] before or after
|
|
/// this method is called will be ignored.
|
|
pub fn uniform_list(
|
|
mut self,
|
|
id: impl Into<ElementId>,
|
|
row_count: usize,
|
|
render_item_fn: impl Fn(Range<usize>, &mut Window, &mut App) -> Vec<[AnyElement; COLS]>
|
|
+ 'static,
|
|
) -> Self {
|
|
self.rows = TableContents::UniformList(UniformListData {
|
|
element_id: id.into(),
|
|
row_count,
|
|
render_item_fn: Box::new(render_item_fn),
|
|
});
|
|
self
|
|
}
|
|
|
|
/// Enables row striping.
|
|
pub fn striped(mut self) -> Self {
|
|
self.striped = true;
|
|
self
|
|
}
|
|
|
|
/// Sets the width of the table.
|
|
/// Will enable horizontal scrolling if [`Self::interactable`] is also called.
|
|
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
|
self.width = Some(width.into());
|
|
self
|
|
}
|
|
|
|
/// Enables interaction (primarily scrolling) with the table.
|
|
///
|
|
/// Vertical scrolling will be enabled by default if the table is taller than its container.
|
|
///
|
|
/// Horizontal scrolling will only be enabled if [`Self::width`] is also called, otherwise
|
|
/// the list will always shrink the table columns to fit their contents I.e. If [`Self::uniform_list`]
|
|
/// is used without a width and with [`Self::interactable`], the [`ListHorizontalSizingBehavior`] will
|
|
/// be set to [`ListHorizontalSizingBehavior::FitList`].
|
|
pub fn interactable(mut self, interaction_state: &Entity<TableInteractionState>) -> Self {
|
|
self.interaction_state = Some(interaction_state.downgrade());
|
|
self
|
|
}
|
|
|
|
pub fn header(mut self, headers: [impl IntoElement; COLS]) -> Self {
|
|
self.headers = Some(headers.map(IntoElement::into_any_element));
|
|
self
|
|
}
|
|
|
|
pub fn row(mut self, items: [impl IntoElement; COLS]) -> Self {
|
|
if let Some(rows) = self.rows.rows_mut() {
|
|
rows.push(items.map(IntoElement::into_any_element));
|
|
}
|
|
self
|
|
}
|
|
|
|
pub fn column_widths(mut self, widths: [impl Into<DefiniteLength>; COLS]) -> Self {
|
|
if self.col_widths.is_none() {
|
|
self.col_widths = Some(TableWidths::new(widths));
|
|
}
|
|
self
|
|
}
|
|
|
|
pub fn resizable_columns(
|
|
mut self,
|
|
resizable: [TableResizeBehavior; COLS],
|
|
column_widths: &Entity<TableColumnWidths<COLS>>,
|
|
cx: &mut App,
|
|
) -> Self {
|
|
if let Some(table_widths) = self.col_widths.as_mut() {
|
|
table_widths.resizable = resizable;
|
|
let column_widths = table_widths
|
|
.current
|
|
.get_or_insert_with(|| column_widths.clone());
|
|
|
|
column_widths.update(cx, |widths, _| {
|
|
if !widths.initialized {
|
|
widths.initialized = true;
|
|
widths.widths = table_widths.initial;
|
|
widths.visible_widths = widths.widths;
|
|
}
|
|
})
|
|
}
|
|
self
|
|
}
|
|
|
|
pub fn map_row(
|
|
mut self,
|
|
callback: impl Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement + 'static,
|
|
) -> Self {
|
|
self.map_row = Some(Rc::new(callback));
|
|
self
|
|
}
|
|
|
|
/// Provide a callback that is invoked when the table is rendered without any rows
|
|
pub fn empty_table_callback(
|
|
mut self,
|
|
callback: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
|
|
) -> Self {
|
|
self.empty_table_callback = Some(Rc::new(callback));
|
|
self
|
|
}
|
|
}
|
|
|
|
fn base_cell_style(width: Option<Length>) -> Div {
|
|
div()
|
|
.px_1p5()
|
|
.when_some(width, |this, width| this.w(width))
|
|
.when(width.is_none(), |this| this.flex_1())
|
|
.whitespace_nowrap()
|
|
.text_ellipsis()
|
|
.overflow_hidden()
|
|
}
|
|
|
|
fn base_cell_style_text(width: Option<Length>, cx: &App) -> Div {
|
|
base_cell_style(width).text_ui(cx)
|
|
}
|
|
|
|
pub fn render_table_row<const COLS: usize>(
|
|
row_index: usize,
|
|
items: [impl IntoElement; COLS],
|
|
table_context: TableRenderContext<COLS>,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) -> AnyElement {
|
|
let is_striped = table_context.striped;
|
|
let is_last = row_index == table_context.total_row_count - 1;
|
|
let bg = if row_index % 2 == 1 && is_striped {
|
|
Some(cx.theme().colors().text.opacity(0.05))
|
|
} else {
|
|
None
|
|
};
|
|
let column_widths = table_context
|
|
.column_widths
|
|
.map_or([None; COLS], |widths| widths.map(Some));
|
|
|
|
let mut row = h_flex()
|
|
.h_full()
|
|
.id(("table_row", row_index))
|
|
.w_full()
|
|
.justify_between()
|
|
.when_some(bg, |row, bg| row.bg(bg))
|
|
.when(!is_striped, |row| {
|
|
row.border_b_1()
|
|
.border_color(transparent_black())
|
|
.when(!is_last, |row| row.border_color(cx.theme().colors().border))
|
|
});
|
|
|
|
row = row.children(
|
|
items
|
|
.map(IntoElement::into_any_element)
|
|
.into_iter()
|
|
.zip(column_widths)
|
|
.map(|(cell, width)| base_cell_style_text(width, cx).px_1().py_0p5().child(cell)),
|
|
);
|
|
|
|
let row = if let Some(map_row) = table_context.map_row {
|
|
map_row((row_index, row), window, cx)
|
|
} else {
|
|
row.into_any_element()
|
|
};
|
|
|
|
div().size_full().child(row).into_any_element()
|
|
}
|
|
|
|
pub fn render_table_header<const COLS: usize>(
|
|
headers: [impl IntoElement; COLS],
|
|
table_context: TableRenderContext<COLS>,
|
|
columns_widths: Option<(
|
|
WeakEntity<TableColumnWidths<COLS>>,
|
|
[TableResizeBehavior; COLS],
|
|
[DefiniteLength; COLS],
|
|
)>,
|
|
entity_id: Option<EntityId>,
|
|
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()
|
|
.items_center()
|
|
.justify_between()
|
|
.w_full()
|
|
.p_2()
|
|
.border_b_1()
|
|
.border_color(cx.theme().colors().border)
|
|
.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.click_count() > 1 {
|
|
column_widths
|
|
.update(cx, |column, _| {
|
|
column.on_double_click(
|
|
header_idx,
|
|
&initial_sizes,
|
|
&resizables,
|
|
window,
|
|
);
|
|
})
|
|
.ok();
|
|
}
|
|
})
|
|
} else {
|
|
this
|
|
}
|
|
},
|
|
)
|
|
},
|
|
))
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct TableRenderContext<const COLS: usize> {
|
|
pub striped: bool,
|
|
pub total_row_count: usize,
|
|
pub column_widths: Option<[Length; COLS]>,
|
|
pub map_row: Option<Rc<dyn Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement>>,
|
|
}
|
|
|
|
impl<const COLS: usize> TableRenderContext<COLS> {
|
|
fn new(table: &Table<COLS>, cx: &App) -> Self {
|
|
Self {
|
|
striped: table.striped,
|
|
total_row_count: table.rows.len(),
|
|
column_widths: table.col_widths.as_ref().map(|widths| widths.lengths(cx)),
|
|
map_row: table.map_row.clone(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<const COLS: usize> RenderOnce for Table<COLS> {
|
|
fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
let table_context = TableRenderContext::new(&self, cx);
|
|
let interaction_state = self.interaction_state.and_then(|state| state.upgrade());
|
|
let current_widths = self
|
|
.col_widths
|
|
.as_ref()
|
|
.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 width = self.width;
|
|
let no_rows_rendered = self.rows.is_empty();
|
|
|
|
let table = div()
|
|
.when_some(width, |this, width| this.w(width))
|
|
.h_full()
|
|
.v_flex()
|
|
.when_some(self.headers.take(), |this, headers| {
|
|
this.child(render_table_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)| {
|
|
this.on_drag_move::<DraggedColumn>({
|
|
let widths = widths.clone();
|
|
move |e, window, cx| {
|
|
widths
|
|
.update(cx, |widths, cx| {
|
|
widths.on_drag_move(e, &resize_behavior, window, cx);
|
|
})
|
|
.ok();
|
|
}
|
|
})
|
|
.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::<DraggedColumn>(move |_, _, cx| {
|
|
widths
|
|
.update(cx, |widths, _| {
|
|
widths.widths = widths.visible_widths;
|
|
})
|
|
.ok();
|
|
// Finish the resize operation
|
|
})
|
|
}
|
|
})
|
|
.child({
|
|
let content = div()
|
|
.flex_grow()
|
|
.w_full()
|
|
.relative()
|
|
.overflow_hidden()
|
|
.map(|parent| match self.rows {
|
|
TableContents::Vec(items) => {
|
|
parent.children(items.into_iter().enumerate().map(|(index, row)| {
|
|
div().child(render_table_row(
|
|
index,
|
|
row,
|
|
table_context.clone(),
|
|
window,
|
|
cx,
|
|
))
|
|
}))
|
|
}
|
|
TableContents::UniformList(uniform_list_data) => parent.child(
|
|
uniform_list(
|
|
uniform_list_data.element_id,
|
|
uniform_list_data.row_count,
|
|
{
|
|
let render_item_fn = uniform_list_data.render_item_fn;
|
|
move |range: Range<usize>, window, cx| {
|
|
let elements = render_item_fn(range.clone(), window, cx);
|
|
elements
|
|
.into_iter()
|
|
.zip(range)
|
|
.map(|(row, row_index)| {
|
|
render_table_row(
|
|
row_index,
|
|
row,
|
|
table_context.clone(),
|
|
window,
|
|
cx,
|
|
)
|
|
})
|
|
.collect()
|
|
}
|
|
},
|
|
)
|
|
.size_full()
|
|
.flex_grow()
|
|
.with_sizing_behavior(ListSizingBehavior::Auto)
|
|
.with_horizontal_sizing_behavior(if width.is_some() {
|
|
ListHorizontalSizingBehavior::Unconstrained
|
|
} else {
|
|
ListHorizontalSizingBehavior::FitList
|
|
})
|
|
.when_some(
|
|
interaction_state.as_ref(),
|
|
|this, state| {
|
|
this.track_scroll(
|
|
state.read_with(cx, |s, _| s.scroll_handle.clone()),
|
|
)
|
|
},
|
|
),
|
|
),
|
|
})
|
|
.when_some(
|
|
self.col_widths.as_ref().zip(interaction_state.as_ref()),
|
|
|parent, (table_widths, state)| {
|
|
parent.child(state.update(cx, |state, cx| {
|
|
let resizable_columns = table_widths.resizable;
|
|
let column_widths = table_widths.lengths(cx);
|
|
let columns = table_widths.current.clone();
|
|
let initial_sizes = table_widths.initial;
|
|
state.render_resize_handles(
|
|
&column_widths,
|
|
&resizable_columns,
|
|
initial_sizes,
|
|
columns,
|
|
window,
|
|
cx,
|
|
)
|
|
}))
|
|
},
|
|
);
|
|
|
|
if let Some(state) = interaction_state.as_ref() {
|
|
let scrollbars = state
|
|
.read(cx)
|
|
.custom_scrollbar
|
|
.clone()
|
|
.unwrap_or_else(|| Scrollbars::new(super::ScrollAxes::Both));
|
|
content
|
|
.custom_scrollbars(
|
|
scrollbars.tracked_scroll_handle(state.read(cx).scroll_handle.clone()),
|
|
window,
|
|
cx,
|
|
)
|
|
.into_any_element()
|
|
} else {
|
|
content.into_any_element()
|
|
}
|
|
})
|
|
.when_some(
|
|
no_rows_rendered
|
|
.then_some(self.empty_table_callback)
|
|
.flatten(),
|
|
|this, callback| {
|
|
this.child(
|
|
h_flex()
|
|
.size_full()
|
|
.p_3()
|
|
.items_start()
|
|
.justify_center()
|
|
.child(callback(window, cx)),
|
|
)
|
|
},
|
|
);
|
|
|
|
if let Some(interaction_state) = interaction_state.as_ref() {
|
|
table
|
|
.track_focus(&interaction_state.read(cx).focus_handle)
|
|
.id(("table", interaction_state.entity_id()))
|
|
.into_any_element()
|
|
} else {
|
|
table.into_any_element()
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Component for Table<3> {
|
|
fn scope() -> ComponentScope {
|
|
ComponentScope::Layout
|
|
}
|
|
|
|
fn description() -> Option<&'static str> {
|
|
Some("A table component for displaying data in rows and columns with optional styling.")
|
|
}
|
|
|
|
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
|
Some(
|
|
v_flex()
|
|
.gap_6()
|
|
.children(vec![
|
|
example_group_with_title(
|
|
"Basic Tables",
|
|
vec![
|
|
single_example(
|
|
"Simple Table",
|
|
Table::new()
|
|
.width(px(400.))
|
|
.header(["Name", "Age", "City"])
|
|
.row(["Alice", "28", "New York"])
|
|
.row(["Bob", "32", "San Francisco"])
|
|
.row(["Charlie", "25", "London"])
|
|
.into_any_element(),
|
|
),
|
|
single_example(
|
|
"Two Column Table",
|
|
Table::new()
|
|
.header(["Category", "Value"])
|
|
.width(px(300.))
|
|
.row(["Revenue", "$100,000"])
|
|
.row(["Expenses", "$75,000"])
|
|
.row(["Profit", "$25,000"])
|
|
.into_any_element(),
|
|
),
|
|
],
|
|
),
|
|
example_group_with_title(
|
|
"Styled Tables",
|
|
vec![
|
|
single_example(
|
|
"Default",
|
|
Table::new()
|
|
.width(px(400.))
|
|
.header(["Product", "Price", "Stock"])
|
|
.row(["Laptop", "$999", "In Stock"])
|
|
.row(["Phone", "$599", "Low Stock"])
|
|
.row(["Tablet", "$399", "Out of Stock"])
|
|
.into_any_element(),
|
|
),
|
|
single_example(
|
|
"Striped",
|
|
Table::new()
|
|
.width(px(400.))
|
|
.striped()
|
|
.header(["Product", "Price", "Stock"])
|
|
.row(["Laptop", "$999", "In Stock"])
|
|
.row(["Phone", "$599", "Low Stock"])
|
|
.row(["Tablet", "$399", "Out of Stock"])
|
|
.row(["Headphones", "$199", "In Stock"])
|
|
.into_any_element(),
|
|
),
|
|
],
|
|
),
|
|
example_group_with_title(
|
|
"Mixed Content Table",
|
|
vec![single_example(
|
|
"Table with Elements",
|
|
Table::new()
|
|
.width(px(840.))
|
|
.header(["Status", "Name", "Priority", "Deadline", "Action"])
|
|
.row([
|
|
Indicator::dot().color(Color::Success).into_any_element(),
|
|
"Project A".into_any_element(),
|
|
"High".into_any_element(),
|
|
"2023-12-31".into_any_element(),
|
|
Button::new("view_a", "View")
|
|
.style(ButtonStyle::Filled)
|
|
.full_width()
|
|
.into_any_element(),
|
|
])
|
|
.row([
|
|
Indicator::dot().color(Color::Warning).into_any_element(),
|
|
"Project B".into_any_element(),
|
|
"Medium".into_any_element(),
|
|
"2024-03-15".into_any_element(),
|
|
Button::new("view_b", "View")
|
|
.style(ButtonStyle::Filled)
|
|
.full_width()
|
|
.into_any_element(),
|
|
])
|
|
.row([
|
|
Indicator::dot().color(Color::Error).into_any_element(),
|
|
"Project C".into_any_element(),
|
|
"Low".into_any_element(),
|
|
"2024-06-30".into_any_element(),
|
|
Button::new("view_c", "View")
|
|
.style(ButtonStyle::Filled)
|
|
.full_width()
|
|
.into_any_element(),
|
|
])
|
|
.into_any_element(),
|
|
)],
|
|
),
|
|
])
|
|
.into_any_element(),
|
|
)
|
|
}
|
|
}
|
|
|
|
#[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<const COLS: usize>(cols: &[f32; COLS], total_size: f32) -> String {
|
|
cols.map(|f| "*".repeat(f32::round(f * total_size) as usize))
|
|
.join("|")
|
|
}
|
|
|
|
fn parse_resize_behavior<const COLS: usize>(
|
|
input: &str,
|
|
total_size: f32,
|
|
) -> [TableResizeBehavior; COLS] {
|
|
let mut resize_behavior = [TableResizeBehavior::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] = TableResizeBehavior::None;
|
|
} else if col.starts_with('*') {
|
|
resize_behavior[index] =
|
|
TableResizeBehavior::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<const COLS: usize>(input: &str) -> ([f32; COLS], f32, Option<usize>) {
|
|
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::<f32>();
|
|
for width in &mut widths {
|
|
*width /= total;
|
|
}
|
|
(widths, total, column_index)
|
|
}
|
|
|
|
#[track_caller]
|
|
fn check_reset_size<const COLS: usize>(
|
|
initial_sizes: &str,
|
|
widths: &str,
|
|
expected: &str,
|
|
resize_behavior: &str,
|
|
) {
|
|
let (initial_sizes, total_1, None) = parse::<COLS>(initial_sizes) else {
|
|
panic!("invalid test input: initial sizes should not be marked");
|
|
};
|
|
let (widths, total_2, Some(column_index)) = parse::<COLS>(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::<COLS>(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::<COLS>(resize_behavior, total_1);
|
|
let result = TableColumnWidths::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<const COLS: usize>(input: &str) -> ([f32; COLS], f32, Option<usize>) {
|
|
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::<f32>();
|
|
for width in &mut widths {
|
|
*width /= total;
|
|
}
|
|
(widths, total, column_index)
|
|
}
|
|
|
|
#[track_caller]
|
|
fn check<const COLS: usize>(
|
|
distance: i32,
|
|
widths: &str,
|
|
expected: &str,
|
|
resize_behavior: &str,
|
|
) {
|
|
let (mut widths, total_1, Some(column_index)) = parse::<COLS>(widths) else {
|
|
panic!("invalid test input: widths should be marked");
|
|
};
|
|
let (expected, total_2, None) = parse::<COLS>(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::<COLS>(resize_behavior, total_1);
|
|
|
|
let distance = distance as f32 / total_1;
|
|
|
|
let result = TableColumnWidths::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|*|*|*|*",
|
|
);
|
|
}
|
|
}
|