Compare commits
12 commits
main
...
resizable-
Author | SHA1 | Date | |
---|---|---|---|
![]() |
43b6277efd | ||
![]() |
02972bbacb | ||
![]() |
d251c4215c | ||
![]() |
91420c7b94 | ||
![]() |
15d3ee182f | ||
![]() |
97a560bbee | ||
![]() |
587b3eeb06 | ||
![]() |
326be25fe9 | ||
![]() |
34b2158e00 | ||
![]() |
16659636a9 | ||
![]() |
e763002280 | ||
![]() |
89a05abf78 |
5 changed files with 448 additions and 49 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -14756,6 +14756,7 @@ dependencies = [
|
|||
"fs",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"itertools 0.14.0",
|
||||
"language",
|
||||
"log",
|
||||
"menu",
|
||||
|
|
|
@ -23,6 +23,7 @@ feature_flags.workspace = true
|
|||
fs.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
menu.workspace = true
|
||||
|
|
|
@ -13,8 +13,8 @@ use gpui::{
|
|||
Action, Animation, AnimationExt, AppContext as _, AsyncApp, Axis, ClickEvent, Context,
|
||||
DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, Global, IsZero,
|
||||
KeyContext, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, Point, ScrollStrategy,
|
||||
ScrollWheelEvent, StyledText, Subscription, Task, TextStyleRefinement, WeakEntity, actions,
|
||||
anchored, deferred, div,
|
||||
ScrollWheelEvent, Stateful, StyledText, Subscription, Task, TextStyleRefinement, WeakEntity,
|
||||
actions, anchored, deferred, div,
|
||||
};
|
||||
use language::{Language, LanguageConfig, ToOffset as _};
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
|
@ -36,7 +36,7 @@ use workspace::{
|
|||
|
||||
use crate::{
|
||||
keybindings::persistence::KEYBINDING_EDITORS,
|
||||
ui_components::table::{Table, TableInteractionState},
|
||||
ui_components::table::{ColumnWidths, ResizeBehavior, Table, TableInteractionState},
|
||||
};
|
||||
|
||||
const NO_ACTION_ARGUMENTS_TEXT: SharedString = SharedString::new_static("<no arguments>");
|
||||
|
@ -284,6 +284,7 @@ struct KeymapEditor {
|
|||
context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
|
||||
previous_edit: Option<PreviousEdit>,
|
||||
humanized_action_names: HumanizedActionNameCache,
|
||||
current_widths: Entity<ColumnWidths<6>>,
|
||||
show_hover_menus: bool,
|
||||
/// In order for the JSON LSP to run in the actions arguments editor, we
|
||||
/// require a backing file In order to avoid issues (primarily log spam)
|
||||
|
@ -400,6 +401,7 @@ impl KeymapEditor {
|
|||
show_hover_menus: true,
|
||||
action_args_temp_dir: None,
|
||||
action_args_temp_dir_worktree: None,
|
||||
current_widths: cx.new(|cx| ColumnWidths::new(cx)),
|
||||
};
|
||||
|
||||
this.on_keymap_changed(window, cx);
|
||||
|
@ -1433,6 +1435,18 @@ impl Render for KeymapEditor {
|
|||
DefiniteLength::Fraction(0.45),
|
||||
DefiniteLength::Fraction(0.08),
|
||||
])
|
||||
.resizable_columns(
|
||||
[
|
||||
ResizeBehavior::None,
|
||||
ResizeBehavior::Resizable,
|
||||
ResizeBehavior::Resizable,
|
||||
ResizeBehavior::Resizable,
|
||||
ResizeBehavior::Resizable,
|
||||
ResizeBehavior::Resizable, // this column doesn't matter
|
||||
],
|
||||
&self.current_widths,
|
||||
cx,
|
||||
)
|
||||
.header(["", "Action", "Arguments", "Keystrokes", "Context", "Source"])
|
||||
.uniform_list(
|
||||
"keymap-editor-table",
|
||||
|
@ -1594,15 +1608,14 @@ impl Render for KeymapEditor {
|
|||
.collect()
|
||||
}),
|
||||
)
|
||||
.map_row(
|
||||
cx.processor(|this, (row_index, row): (usize, Div), _window, cx| {
|
||||
.map_row(cx.processor(
|
||||
|this, (row_index, row): (usize, Stateful<Div>), _window, cx| {
|
||||
let is_conflict = this.has_conflict(row_index);
|
||||
let is_selected = this.selected_index == Some(row_index);
|
||||
|
||||
let row_id = row_group_id(row_index);
|
||||
|
||||
let row = row
|
||||
.id(row_id.clone())
|
||||
.on_any_mouse_down(cx.listener(
|
||||
move |this,
|
||||
mouse_down_event: &gpui::MouseDownEvent,
|
||||
|
@ -1636,11 +1649,12 @@ impl Render for KeymapEditor {
|
|||
})
|
||||
.when(is_selected, |row| {
|
||||
row.border_color(cx.theme().colors().panel_focused_border)
|
||||
.border_2()
|
||||
});
|
||||
|
||||
row.into_any_element()
|
||||
}),
|
||||
),
|
||||
},
|
||||
)),
|
||||
)
|
||||
.on_scroll_wheel(cx.listener(|this, event: &ScrollWheelEvent, _, cx| {
|
||||
// This ensures that the menu is not dismissed in cases where scroll events
|
||||
|
|
|
@ -2,19 +2,24 @@ use std::{ops::Range, rc::Rc, time::Duration};
|
|||
|
||||
use editor::{EditorSettings, ShowScrollbar, scroll::ScrollbarAutoHide};
|
||||
use gpui::{
|
||||
AppContext, Axis, Context, Entity, FocusHandle, Length, ListHorizontalSizingBehavior,
|
||||
ListSizingBehavior, MouseButton, Point, Task, UniformListScrollHandle, WeakEntity,
|
||||
transparent_black, uniform_list,
|
||||
AbsoluteLength, AppContext, Axis, Context, DefiniteLength, DragMoveEvent, Entity, FocusHandle,
|
||||
Length, ListHorizontalSizingBehavior, ListSizingBehavior, MouseButton, Point, Stateful, Task,
|
||||
UniformListScrollHandle, WeakEntity, transparent_black, uniform_list,
|
||||
};
|
||||
|
||||
use itertools::intersperse_with;
|
||||
use settings::Settings as _;
|
||||
use ui::{
|
||||
ActiveTheme as _, AnyElement, App, Button, ButtonCommon as _, ButtonStyle, Color, Component,
|
||||
ComponentScope, Div, ElementId, FixedWidth as _, FluentBuilder as _, Indicator,
|
||||
InteractiveElement as _, IntoElement, ParentElement, Pixels, RegisterComponent, RenderOnce,
|
||||
Scrollbar, ScrollbarState, StatefulInteractiveElement as _, Styled, StyledExt as _,
|
||||
InteractiveElement, IntoElement, ParentElement, Pixels, RegisterComponent, RenderOnce,
|
||||
Scrollbar, ScrollbarState, StatefulInteractiveElement, Styled, StyledExt as _,
|
||||
StyledTypography, Window, div, example_group_with_title, h_flex, px, single_example, v_flex,
|
||||
};
|
||||
|
||||
#[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,
|
||||
|
@ -191,6 +196,87 @@ impl TableInteractionState {
|
|||
}
|
||||
}
|
||||
|
||||
fn render_resize_handles<const COLS: usize>(
|
||||
&self,
|
||||
column_widths: &[Length; COLS],
|
||||
resizable_columns: &[ResizeBehavior; COLS],
|
||||
initial_sizes: [DefiniteLength; COLS],
|
||||
columns: Option<Entity<ColumnWidths<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.into_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_0p5()
|
||||
.h_full()
|
||||
.bg(cx.theme().colors().border.opacity(0.5));
|
||||
|
||||
let mut resize_handle = div()
|
||||
.id("column-resize-handle")
|
||||
.absolute()
|
||||
.left_neg_0p5()
|
||||
.w(px(5.0))
|
||||
.h_full();
|
||||
|
||||
if resizable_columns
|
||||
.next()
|
||||
.is_some_and(ResizeBehavior::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.down.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()
|
||||
})
|
||||
});
|
||||
|
||||
div()
|
||||
.id("resize-handles")
|
||||
.h_flex()
|
||||
.absolute()
|
||||
.w_full()
|
||||
.inset_0()
|
||||
.children(dividers)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_vertical_scrollbar_track(
|
||||
this: &Entity<Self>,
|
||||
parent: Div,
|
||||
|
@ -369,6 +455,216 @@ impl TableInteractionState {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub enum ResizeBehavior {
|
||||
None,
|
||||
Resizable,
|
||||
MinSize(f32),
|
||||
}
|
||||
|
||||
impl ResizeBehavior {
|
||||
pub fn is_resizable(&self) -> bool {
|
||||
*self != ResizeBehavior::None
|
||||
}
|
||||
|
||||
pub fn min_size(&self) -> Option<f32> {
|
||||
match self {
|
||||
ResizeBehavior::None => None,
|
||||
ResizeBehavior::Resizable => Some(0.05),
|
||||
ResizeBehavior::MinSize(min_size) => Some(*min_size),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ColumnWidths<const COLS: usize> {
|
||||
widths: [DefiniteLength; COLS],
|
||||
cached_bounds_width: Pixels,
|
||||
initialized: bool,
|
||||
}
|
||||
|
||||
impl<const COLS: usize> ColumnWidths<COLS> {
|
||||
pub fn new(_: &mut App) -> Self {
|
||||
Self {
|
||||
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: &[ResizeBehavior; COLS],
|
||||
window: &mut Window,
|
||||
) {
|
||||
let bounds_width = self.cached_bounds_width;
|
||||
let rem_size = window.rem_size();
|
||||
|
||||
let diff =
|
||||
Self::get_fraction(
|
||||
&initial_sizes[double_click_position],
|
||||
bounds_width,
|
||||
rem_size,
|
||||
) - Self::get_fraction(&self.widths[double_click_position], bounds_width, rem_size);
|
||||
|
||||
let mut curr_column = double_click_position + 1;
|
||||
let mut diff_left = diff;
|
||||
|
||||
while diff != 0.0 && curr_column < COLS {
|
||||
let Some(min_size) = resize_behavior[curr_column].min_size() else {
|
||||
curr_column += 1;
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut curr_width =
|
||||
Self::get_fraction(&self.widths[curr_column], bounds_width, rem_size) - diff_left;
|
||||
|
||||
diff_left = 0.0;
|
||||
if min_size > curr_width {
|
||||
diff_left += min_size - curr_width;
|
||||
curr_width = min_size;
|
||||
}
|
||||
self.widths[curr_column] = DefiniteLength::Fraction(curr_width);
|
||||
curr_column += 1;
|
||||
}
|
||||
|
||||
self.widths[double_click_position] = DefiniteLength::Fraction(
|
||||
Self::get_fraction(&self.widths[double_click_position], bounds_width, rem_size)
|
||||
+ (diff - diff_left),
|
||||
);
|
||||
}
|
||||
|
||||
fn on_drag_move(
|
||||
&mut self,
|
||||
drag_event: &DragMoveEvent<DraggedColumn>,
|
||||
resize_behavior: &[ResizeBehavior; COLS],
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
// - [ ] Fix bugs in resize
|
||||
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;
|
||||
|
||||
for length in self.widths[0..=col_idx].iter() {
|
||||
col_position += Self::get_fraction(length, bounds_width, rem_size);
|
||||
}
|
||||
|
||||
let mut total_length_ratio = col_position;
|
||||
for length in self.widths[col_idx + 1..].iter() {
|
||||
total_length_ratio += Self::get_fraction(length, bounds_width, rem_size);
|
||||
}
|
||||
|
||||
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 is_dragging_right = diff > 0.0;
|
||||
|
||||
let mut diff_left = diff;
|
||||
let mut curr_column = col_idx + 1;
|
||||
|
||||
if is_dragging_right {
|
||||
while diff_left > 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 =
|
||||
Self::get_fraction(&self.widths[curr_column], bounds_width, rem_size)
|
||||
- diff_left;
|
||||
|
||||
diff_left = 0.0;
|
||||
if min_size > curr_width {
|
||||
diff_left += min_size - curr_width;
|
||||
curr_width = min_size;
|
||||
}
|
||||
self.widths[curr_column] = DefiniteLength::Fraction(curr_width);
|
||||
curr_column += 1;
|
||||
}
|
||||
|
||||
self.widths[col_idx] = DefiniteLength::Fraction(
|
||||
Self::get_fraction(&self.widths[col_idx], bounds_width, rem_size)
|
||||
+ (diff - diff_left),
|
||||
);
|
||||
} else {
|
||||
curr_column = col_idx;
|
||||
// Resize behavior should be improved in the future by also seeking to the right column when there's not enough space
|
||||
while diff_left < 0.0 {
|
||||
let Some(min_size) = resize_behavior[curr_column].min_size() else {
|
||||
if curr_column == 0 {
|
||||
break;
|
||||
}
|
||||
curr_column -= 1;
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut curr_width =
|
||||
Self::get_fraction(&self.widths[curr_column], bounds_width, rem_size)
|
||||
+ diff_left;
|
||||
|
||||
diff_left = 0.0;
|
||||
if curr_width < min_size {
|
||||
diff_left = curr_width - min_size;
|
||||
curr_width = min_size
|
||||
}
|
||||
|
||||
self.widths[curr_column] = DefiniteLength::Fraction(curr_width);
|
||||
if curr_column == 0 {
|
||||
break;
|
||||
}
|
||||
curr_column -= 1;
|
||||
}
|
||||
|
||||
self.widths[col_idx + 1] = DefiniteLength::Fraction(
|
||||
Self::get_fraction(&self.widths[col_idx + 1], bounds_width, rem_size)
|
||||
- (diff - diff_left),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TableWidths<const COLS: usize> {
|
||||
initial: [DefiniteLength; COLS],
|
||||
current: Option<Entity<ColumnWidths<COLS>>>,
|
||||
resizable: [ResizeBehavior; 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: [ResizeBehavior::None; COLS],
|
||||
}
|
||||
}
|
||||
|
||||
fn lengths(&self, cx: &App) -> [Length; COLS] {
|
||||
self.current
|
||||
.as_ref()
|
||||
.map(|entity| entity.read(cx).widths.map(Length::Definite))
|
||||
.unwrap_or(self.initial.map(Length::Definite))
|
||||
}
|
||||
}
|
||||
|
||||
/// A table component
|
||||
#[derive(RegisterComponent, IntoElement)]
|
||||
pub struct Table<const COLS: usize = 3> {
|
||||
|
@ -377,23 +673,23 @@ pub struct Table<const COLS: usize = 3> {
|
|||
headers: Option<[AnyElement; COLS]>,
|
||||
rows: TableContents<COLS>,
|
||||
interaction_state: Option<WeakEntity<TableInteractionState>>,
|
||||
column_widths: Option<[Length; COLS]>,
|
||||
map_row: Option<Rc<dyn Fn((usize, Div), &mut Window, &mut App) -> AnyElement>>,
|
||||
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 {
|
||||
Table {
|
||||
Self {
|
||||
striped: false,
|
||||
width: None,
|
||||
headers: None,
|
||||
rows: TableContents::Vec(Vec::new()),
|
||||
interaction_state: None,
|
||||
column_widths: None,
|
||||
map_row: None,
|
||||
empty_table_callback: None,
|
||||
col_widths: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -454,14 +750,38 @@ impl<const COLS: usize> Table<COLS> {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn column_widths(mut self, widths: [impl Into<Length>; COLS]) -> Self {
|
||||
self.column_widths = Some(widths.map(Into::into));
|
||||
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: [ResizeBehavior; COLS],
|
||||
column_widths: &Entity<ColumnWidths<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;
|
||||
}
|
||||
})
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn map_row(
|
||||
mut self,
|
||||
callback: impl Fn((usize, Div), &mut Window, &mut App) -> AnyElement + 'static,
|
||||
callback: impl Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement + 'static,
|
||||
) -> Self {
|
||||
self.map_row = Some(Rc::new(callback));
|
||||
self
|
||||
|
@ -477,18 +797,21 @@ impl<const COLS: usize> Table<COLS> {
|
|||
}
|
||||
}
|
||||
|
||||
fn base_cell_style(width: Option<Length>, cx: &App) -> Div {
|
||||
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())
|
||||
.justify_start()
|
||||
.text_ui(cx)
|
||||
.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_row<const COLS: usize>(
|
||||
row_index: usize,
|
||||
items: [impl IntoElement; COLS],
|
||||
|
@ -507,33 +830,33 @@ pub fn render_row<const COLS: usize>(
|
|||
.column_widths
|
||||
.map_or([None; COLS], |widths| widths.map(Some));
|
||||
|
||||
let row = div().w_full().child(
|
||||
h_flex()
|
||||
.id("table_row")
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.px_1p5()
|
||||
.py_1()
|
||||
.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))
|
||||
})
|
||||
.children(
|
||||
items
|
||||
.map(IntoElement::into_any_element)
|
||||
.into_iter()
|
||||
.zip(column_widths)
|
||||
.map(|(cell, width)| base_cell_style(width, cx).child(cell)),
|
||||
),
|
||||
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_1p5().py_1().child(cell)),
|
||||
);
|
||||
|
||||
if let Some(map_row) = table_context.map_row {
|
||||
let row = if let Some(map_row) = table_context.map_row {
|
||||
map_row((row_index, row), window, cx)
|
||||
} else {
|
||||
row.into_any_element()
|
||||
}
|
||||
};
|
||||
|
||||
div().h_full().w_full().child(row).into_any_element()
|
||||
}
|
||||
|
||||
pub fn render_header<const COLS: usize>(
|
||||
|
@ -557,7 +880,7 @@ pub fn render_header<const COLS: usize>(
|
|||
headers
|
||||
.into_iter()
|
||||
.zip(column_widths)
|
||||
.map(|(h, width)| base_cell_style(width, cx).child(h)),
|
||||
.map(|(h, width)| base_cell_style_text(width, cx).child(h)),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -566,15 +889,15 @@ 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, Div), &mut Window, &mut App) -> AnyElement>>,
|
||||
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>) -> Self {
|
||||
fn new(table: &Table<COLS>, cx: &App) -> Self {
|
||||
Self {
|
||||
striped: table.striped,
|
||||
total_row_count: table.rows.len(),
|
||||
column_widths: table.column_widths,
|
||||
column_widths: table.col_widths.as_ref().map(|widths| widths.lengths(cx)),
|
||||
map_row: table.map_row.clone(),
|
||||
}
|
||||
}
|
||||
|
@ -582,8 +905,13 @@ impl<const COLS: usize> TableRenderContext<COLS> {
|
|||
|
||||
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);
|
||||
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 scroll_track_size = px(16.);
|
||||
let h_scroll_offset = if interaction_state
|
||||
|
@ -606,6 +934,31 @@ impl<const COLS: usize> RenderOnce for Table<COLS> {
|
|||
.when_some(self.headers.take(), |this, headers| {
|
||||
this.child(render_header(headers, table_context.clone(), 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(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>(|_, _, _| {
|
||||
// Finish the resize operation
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.flex_grow()
|
||||
|
@ -660,6 +1013,25 @@ impl<const COLS: usize> RenderOnce for Table<COLS> {
|
|||
),
|
||||
),
|
||||
})
|
||||
.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,
|
||||
)
|
||||
}))
|
||||
},
|
||||
)
|
||||
.when_some(interaction_state.as_ref(), |this, interaction_state| {
|
||||
this.map(|this| {
|
||||
TableInteractionState::render_vertical_scrollbar_track(
|
||||
|
|
|
@ -943,6 +943,8 @@ mod element {
|
|||
pub struct PaneAxisElement {
|
||||
axis: Axis,
|
||||
basis: usize,
|
||||
/// Equivalent to ColumnWidths (but in terms of flexes instead of percentages)
|
||||
/// For example, flexes "1.33, 1, 1", instead of "40%, 30%, 30%"
|
||||
flexes: Arc<Mutex<Vec<f32>>>,
|
||||
bounding_boxes: Arc<Mutex<Vec<Option<Bounds<Pixels>>>>>,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
|
@ -998,6 +1000,7 @@ mod element {
|
|||
let mut flexes = flexes.lock();
|
||||
debug_assert!(flex_values_in_bounds(flexes.as_slice()));
|
||||
|
||||
// Math to convert a flex value to a pixel value
|
||||
let size = move |ix, flexes: &[f32]| {
|
||||
container_size.along(axis) * (flexes[ix] / flexes.len() as f32)
|
||||
};
|
||||
|
@ -1007,9 +1010,13 @@ mod element {
|
|||
return;
|
||||
}
|
||||
|
||||
// This is basically a "bucket" of pixel changes that need to be applied in response to this
|
||||
// mouse event. Probably a small, fractional number like 0.5 or 1.5 pixels
|
||||
let mut proposed_current_pixel_change =
|
||||
(e.position - child_start).along(axis) - size(ix, flexes.as_slice());
|
||||
|
||||
// This takes a pixel change, and computes the flex changes that correspond to this pixel change
|
||||
// as well as the next one, for some reason
|
||||
let flex_changes = |pixel_dx, target_ix, next: isize, flexes: &[f32]| {
|
||||
let flex_change = pixel_dx / container_size.along(axis);
|
||||
let current_target_flex = flexes[target_ix] + flex_change;
|
||||
|
@ -1017,6 +1024,9 @@ mod element {
|
|||
(current_target_flex, next_target_flex)
|
||||
};
|
||||
|
||||
// Generate the list of flex successors, from the current index.
|
||||
// If you're dragging column 3 forward, out of 6 columns, then this code will produce [4, 5, 6]
|
||||
// If you're dragging column 3 backward, out of 6 columns, then this code will produce [2, 1, 0]
|
||||
let mut successors = iter::from_fn({
|
||||
let forward = proposed_current_pixel_change > px(0.);
|
||||
let mut ix_offset = 0;
|
||||
|
@ -1034,6 +1044,7 @@ mod element {
|
|||
}
|
||||
});
|
||||
|
||||
// Now actually loop over these, and empty our bucket of pixel changes
|
||||
while proposed_current_pixel_change.abs() > px(0.) {
|
||||
let Some(current_ix) = successors.next() else {
|
||||
break;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue