Detailed changes
@@ -42,8 +42,10 @@ use theme_settings::ThemeSettings;
use time::{OffsetDateTime, UtcOffset, format_description::BorrowedFormatItem};
use ui::{
ButtonLike, Chip, ColumnWidthConfig, CommonAnimationExt as _, ContextMenu, DiffStat, Divider,
- HighlightedLabel, RedistributableColumnsState, ScrollableHandle, Table, TableInteractionState,
- TableResizeBehavior, Tooltip, WithScrollbar, prelude::*,
+ HeaderResizeInfo, HighlightedLabel, RedistributableColumnsState, ScrollableHandle, Table,
+ TableInteractionState, TableRenderContext, TableResizeBehavior, Tooltip, WithScrollbar,
+ bind_redistributable_columns, prelude::*, render_redistributable_columns_resize_handles,
+ render_table_header, table_row::TableRow,
};
use workspace::{
Workspace,
@@ -901,9 +903,8 @@ pub struct GitGraph {
context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
row_height: Pixels,
table_interaction_state: Entity<TableInteractionState>,
- table_column_widths: Entity<RedistributableColumnsState>,
+ column_widths: Entity<RedistributableColumnsState>,
horizontal_scroll_offset: Pixels,
- graph_viewport_width: Pixels,
selected_entry_idx: Option<usize>,
hovered_entry_idx: Option<usize>,
graph_canvas_bounds: Rc<Cell<Option<Bounds<Pixels>>>>,
@@ -933,8 +934,60 @@ impl GitGraph {
font_size + px(12.0)
}
- fn graph_content_width(&self) -> Pixels {
- (LANE_WIDTH * self.graph_data.max_lanes.min(8) as f32) + LEFT_PADDING * 2.0
+ fn graph_canvas_content_width(&self) -> Pixels {
+ (LANE_WIDTH * self.graph_data.max_lanes.max(6) as f32) + LEFT_PADDING * 2.0
+ }
+
+ fn preview_column_fractions(&self, window: &Window, cx: &App) -> [f32; 5] {
+ let fractions = self
+ .column_widths
+ .read(cx)
+ .preview_fractions(window.rem_size());
+ [
+ fractions[0],
+ fractions[1],
+ fractions[2],
+ fractions[3],
+ fractions[4],
+ ]
+ }
+
+ fn table_column_width_config(&self, window: &Window, cx: &App) -> ColumnWidthConfig {
+ let [_, description, date, author, commit] = self.preview_column_fractions(window, cx);
+ let table_total = description + date + author + commit;
+
+ let widths = if table_total > 0.0 {
+ vec![
+ DefiniteLength::Fraction(description / table_total),
+ DefiniteLength::Fraction(date / table_total),
+ DefiniteLength::Fraction(author / table_total),
+ DefiniteLength::Fraction(commit / table_total),
+ ]
+ } else {
+ vec![
+ DefiniteLength::Fraction(0.25),
+ DefiniteLength::Fraction(0.25),
+ DefiniteLength::Fraction(0.25),
+ DefiniteLength::Fraction(0.25),
+ ]
+ };
+
+ ColumnWidthConfig::explicit(widths)
+ }
+
+ fn graph_viewport_width(&self, window: &Window, cx: &App) -> Pixels {
+ self.column_widths
+ .read(cx)
+ .preview_column_width(0, window)
+ .unwrap_or_else(|| self.graph_canvas_content_width())
+ }
+
+ fn clamp_horizontal_scroll_offset(&mut self, graph_viewport_width: Pixels) {
+ let max_horizontal_scroll =
+ (self.graph_canvas_content_width() - graph_viewport_width).max(px(0.));
+ self.horizontal_scroll_offset = self
+ .horizontal_scroll_offset
+ .clamp(px(0.), max_horizontal_scroll);
}
pub fn new(
@@ -972,20 +1025,22 @@ impl GitGraph {
});
let table_interaction_state = cx.new(|cx| TableInteractionState::new(cx));
- let table_column_widths = cx.new(|_cx| {
+ let column_widths = cx.new(|_cx| {
RedistributableColumnsState::new(
- 4,
+ 5,
vec![
- DefiniteLength::Fraction(0.72),
- DefiniteLength::Fraction(0.12),
- DefiniteLength::Fraction(0.10),
- DefiniteLength::Fraction(0.06),
+ DefiniteLength::Fraction(0.14),
+ DefiniteLength::Fraction(0.6192),
+ DefiniteLength::Fraction(0.1032),
+ DefiniteLength::Fraction(0.086),
+ DefiniteLength::Fraction(0.0516),
],
vec![
TableResizeBehavior::Resizable,
TableResizeBehavior::Resizable,
TableResizeBehavior::Resizable,
TableResizeBehavior::Resizable,
+ TableResizeBehavior::Resizable,
],
)
});
@@ -1020,9 +1075,8 @@ impl GitGraph {
context_menu: None,
row_height,
table_interaction_state,
- table_column_widths,
+ column_widths,
horizontal_scroll_offset: px(0.),
- graph_viewport_width: px(88.),
selected_entry_idx: None,
hovered_entry_idx: None,
graph_canvas_bounds: Rc::new(Cell::new(None)),
@@ -2089,8 +2143,12 @@ impl GitGraph {
let vertical_scroll_offset = scroll_offset_y - (first_visible_row as f32 * row_height);
let horizontal_scroll_offset = self.horizontal_scroll_offset;
- let max_lanes = self.graph_data.max_lanes.max(6);
- let graph_width = LANE_WIDTH * max_lanes as f32 + LEFT_PADDING * 2.0;
+ let graph_viewport_width = self.graph_viewport_width(window, cx);
+ let graph_width = if self.graph_canvas_content_width() > graph_viewport_width {
+ self.graph_canvas_content_width()
+ } else {
+ graph_viewport_width
+ };
let last_visible_row =
first_visible_row + (viewport_height / row_height).ceil() as usize + 1;
@@ -2414,9 +2472,9 @@ impl GitGraph {
let new_y = (current_offset.y + delta.y).clamp(max_vertical_scroll, px(0.));
let new_offset = Point::new(current_offset.x, new_y);
- let max_lanes = self.graph_data.max_lanes.max(1);
- let graph_content_width = LANE_WIDTH * max_lanes as f32 + LEFT_PADDING * 2.0;
- let max_horizontal_scroll = (graph_content_width - self.graph_viewport_width).max(px(0.));
+ let graph_viewport_width = self.graph_viewport_width(window, cx);
+ let max_horizontal_scroll =
+ (self.graph_canvas_content_width() - graph_viewport_width).max(px(0.));
let new_horizontal_offset =
(self.horizontal_scroll_offset - delta.x).clamp(px(0.), max_horizontal_scroll);
@@ -2497,6 +2555,8 @@ impl Render for GitGraph {
cx,
);
self.graph_data.add_commits(&commits);
+ let graph_viewport_width = self.graph_viewport_width(window, cx);
+ self.clamp_horizontal_scroll_offset(graph_viewport_width);
(commits.len(), is_loading)
})
} else {
@@ -2527,118 +2587,202 @@ impl Render for GitGraph {
this.child(self.render_loading_spinner(cx))
})
} else {
- div()
+ let header_resize_info = HeaderResizeInfo::from_state(&self.column_widths, cx);
+ let header_context = TableRenderContext::for_column_widths(
+ Some(self.column_widths.read(cx).widths_to_render()),
+ true,
+ );
+ let [
+ graph_fraction,
+ description_fraction,
+ date_fraction,
+ author_fraction,
+ commit_fraction,
+ ] = self.preview_column_fractions(window, cx);
+ let table_fraction =
+ description_fraction + date_fraction + author_fraction + commit_fraction;
+ let table_width_config = self.table_column_width_config(window, cx);
+ let graph_viewport_width = self.graph_viewport_width(window, cx);
+ self.clamp_horizontal_scroll_offset(graph_viewport_width);
+
+ h_flex()
.size_full()
- .flex()
- .flex_row()
.child(
div()
- .w(self.graph_content_width())
- .h_full()
+ .flex_1()
+ .min_w_0()
+ .size_full()
.flex()
.flex_col()
- .child(
- div()
- .flex()
- .items_center()
- .px_1()
- .py_0p5()
- .border_b_1()
- .whitespace_nowrap()
- .border_color(cx.theme().colors().border)
- .child(Label::new("Graph").color(Color::Muted)),
- )
- .child(
- div()
- .id("graph-canvas")
- .flex_1()
- .overflow_hidden()
- .child(self.render_graph(window, cx))
- .on_scroll_wheel(cx.listener(Self::handle_graph_scroll))
- .on_mouse_move(cx.listener(Self::handle_graph_mouse_move))
- .on_click(cx.listener(Self::handle_graph_click))
- .on_hover(cx.listener(|this, &is_hovered: &bool, _, cx| {
- if !is_hovered && this.hovered_entry_idx.is_some() {
- this.hovered_entry_idx = None;
- cx.notify();
- }
- })),
- ),
- )
- .child({
- let row_height = self.row_height;
- let selected_entry_idx = self.selected_entry_idx;
- let hovered_entry_idx = self.hovered_entry_idx;
- let weak_self = cx.weak_entity();
- let focus_handle = self.focus_handle.clone();
- div().flex_1().size_full().child(
- Table::new(4)
- .interactable(&self.table_interaction_state)
- .hide_row_borders()
- .hide_row_hover()
- .header(vec![
- Label::new("Description")
- .color(Color::Muted)
- .into_any_element(),
- Label::new("Date").color(Color::Muted).into_any_element(),
- Label::new("Author").color(Color::Muted).into_any_element(),
- Label::new("Commit").color(Color::Muted).into_any_element(),
- ])
- .width_config(ColumnWidthConfig::redistributable(
- self.table_column_widths.clone(),
- ))
- .map_row(move |(index, row), window, cx| {
- let is_selected = selected_entry_idx == Some(index);
- let is_hovered = hovered_entry_idx == Some(index);
- let is_focused = focus_handle.is_focused(window);
- let weak = weak_self.clone();
- let weak_for_hover = weak.clone();
-
- let hover_bg = cx.theme().colors().element_hover.opacity(0.6);
- let selected_bg = if is_focused {
- cx.theme().colors().element_selected
- } else {
- cx.theme().colors().element_hover
- };
-
- row.h(row_height)
- .when(is_selected, |row| row.bg(selected_bg))
- .when(is_hovered && !is_selected, |row| row.bg(hover_bg))
- .on_hover(move |&is_hovered, _, cx| {
- weak_for_hover
- .update(cx, |this, cx| {
- if is_hovered {
- if this.hovered_entry_idx != Some(index) {
- this.hovered_entry_idx = Some(index);
- cx.notify();
- }
- } else if this.hovered_entry_idx == Some(index) {
- // Only clear if this row was the hovered one
- this.hovered_entry_idx = None;
- cx.notify();
- }
- })
- .ok();
- })
- .on_click(move |event, window, cx| {
- let click_count = event.click_count();
- weak.update(cx, |this, cx| {
- this.select_entry(index, ScrollStrategy::Center, cx);
- if click_count >= 2 {
- this.open_commit_view(index, window, cx);
- }
- })
- .ok();
- })
- .into_any_element()
- })
- .uniform_list(
- "git-graph-commits",
- commit_count,
- cx.processor(Self::render_table_rows),
+ .child(render_table_header(
+ TableRow::from_vec(
+ vec![
+ Label::new("Graph")
+ .color(Color::Muted)
+ .truncate()
+ .into_any_element(),
+ Label::new("Description")
+ .color(Color::Muted)
+ .into_any_element(),
+ Label::new("Date").color(Color::Muted).into_any_element(),
+ Label::new("Author").color(Color::Muted).into_any_element(),
+ Label::new("Commit").color(Color::Muted).into_any_element(),
+ ],
+ 5,
),
- )
- })
+ header_context,
+ Some(header_resize_info),
+ Some(self.column_widths.entity_id()),
+ cx,
+ ))
+ .child({
+ let row_height = self.row_height;
+ let selected_entry_idx = self.selected_entry_idx;
+ let hovered_entry_idx = self.hovered_entry_idx;
+ let weak_self = cx.weak_entity();
+ let focus_handle = self.focus_handle.clone();
+
+ bind_redistributable_columns(
+ div()
+ .relative()
+ .flex_1()
+ .w_full()
+ .overflow_hidden()
+ .child(
+ h_flex()
+ .size_full()
+ .child(
+ div()
+ .w(DefiniteLength::Fraction(graph_fraction))
+ .h_full()
+ .min_w_0()
+ .overflow_hidden()
+ .child(
+ div()
+ .id("graph-canvas")
+ .size_full()
+ .overflow_hidden()
+ .child(
+ div()
+ .size_full()
+ .child(self.render_graph(window, cx)),
+ )
+ .on_scroll_wheel(
+ cx.listener(Self::handle_graph_scroll),
+ )
+ .on_mouse_move(
+ cx.listener(Self::handle_graph_mouse_move),
+ )
+ .on_click(cx.listener(Self::handle_graph_click))
+ .on_hover(cx.listener(
+ |this, &is_hovered: &bool, _, cx| {
+ if !is_hovered
+ && this.hovered_entry_idx.is_some()
+ {
+ this.hovered_entry_idx = None;
+ cx.notify();
+ }
+ },
+ )),
+ ),
+ )
+ .child(
+ div()
+ .w(DefiniteLength::Fraction(table_fraction))
+ .h_full()
+ .min_w_0()
+ .child(
+ Table::new(4)
+ .interactable(&self.table_interaction_state)
+ .hide_row_borders()
+ .hide_row_hover()
+ .width_config(table_width_config)
+ .map_row(move |(index, row), window, cx| {
+ let is_selected =
+ selected_entry_idx == Some(index);
+ let is_hovered =
+ hovered_entry_idx == Some(index);
+ let is_focused =
+ focus_handle.is_focused(window);
+ let weak = weak_self.clone();
+ let weak_for_hover = weak.clone();
+
+ let hover_bg = cx
+ .theme()
+ .colors()
+ .element_hover
+ .opacity(0.6);
+ let selected_bg = if is_focused {
+ cx.theme().colors().element_selected
+ } else {
+ cx.theme().colors().element_hover
+ };
+
+ row.h(row_height)
+ .when(is_selected, |row| row.bg(selected_bg))
+ .when(
+ is_hovered && !is_selected,
+ |row| row.bg(hover_bg),
+ )
+ .on_hover(move |&is_hovered, _, cx| {
+ weak_for_hover
+ .update(cx, |this, cx| {
+ if is_hovered {
+ if this.hovered_entry_idx
+ != Some(index)
+ {
+ this.hovered_entry_idx =
+ Some(index);
+ cx.notify();
+ }
+ } else if this
+ .hovered_entry_idx
+ == Some(index)
+ {
+ this.hovered_entry_idx =
+ None;
+ cx.notify();
+ }
+ })
+ .ok();
+ })
+ .on_click(move |event, window, cx| {
+ let click_count = event.click_count();
+ weak.update(cx, |this, cx| {
+ this.select_entry(
+ index,
+ ScrollStrategy::Center,
+ cx,
+ );
+ if click_count >= 2 {
+ this.open_commit_view(
+ index,
+ window,
+ cx,
+ );
+ }
+ })
+ .ok();
+ })
+ .into_any_element()
+ })
+ .uniform_list(
+ "git-graph-commits",
+ commit_count,
+ cx.processor(Self::render_table_rows),
+ ),
+ ),
+ ),
+ )
+ .child(render_redistributable_columns_resize_handles(
+ &self.column_widths,
+ window,
+ cx,
+ )),
+ self.column_widths.clone(),
+ )
+ }),
+ )
.on_drag_move::<DraggedSplitHandle>(cx.listener(|this, event, window, cx| {
this.commit_details_split_state.update(cx, |state, cx| {
state.on_drag_move(event, window, cx);
@@ -3734,9 +3878,11 @@ mod tests {
});
cx.run_until_parked();
- git_graph.update_in(&mut *cx, |this, window, cx| {
- this.render(window, cx);
- });
+ cx.draw(
+ point(px(0.), px(0.)),
+ gpui::size(px(1200.), px(800.)),
+ |_, _| git_graph.clone().into_any_element(),
+ );
cx.run_until_parked();
let commit_count_after_switch_back =
@@ -29,6 +29,7 @@ mod notification;
mod popover;
mod popover_menu;
mod progress;
+mod redistributable_columns;
mod right_click_menu;
mod scrollbar;
mod stack;
@@ -73,6 +74,7 @@ pub use notification::*;
pub use popover::*;
pub use popover_menu::*;
pub use progress::*;
+pub use redistributable_columns::*;
pub use right_click_menu::*;
pub use scrollbar::*;
pub use stack::*;
@@ -1,19 +1,19 @@
use std::{ops::Range, rc::Rc};
use gpui::{
- AbsoluteLength, AppContext as _, DefiniteLength, DragMoveEvent, Entity, EntityId, FocusHandle,
- Length, ListHorizontalSizingBehavior, ListSizingBehavior, ListState, Point, Stateful,
- UniformListScrollHandle, WeakEntity, list, transparent_black, uniform_list,
+ DefiniteLength, Entity, EntityId, FocusHandle, Length, ListHorizontalSizingBehavior,
+ ListSizingBehavior, ListState, Point, Stateful, UniformListScrollHandle, WeakEntity, list,
+ transparent_black, uniform_list,
};
-use itertools::intersperse_with;
use crate::{
ActiveTheme as _, AnyElement, App, Button, ButtonCommon as _, ButtonStyle, Color, Component,
- ComponentScope, Context, Div, ElementId, FixedWidth as _, FluentBuilder as _, Indicator,
- InteractiveElement, IntoElement, ParentElement, Pixels, RegisterComponent, RenderOnce,
- ScrollAxes, ScrollableHandle, Scrollbars, SharedString, StatefulInteractiveElement, Styled,
- StyledExt as _, StyledTypography, Window, WithScrollbar, div, example_group_with_title, h_flex,
- px, single_example,
+ ComponentScope, Context, Div, ElementId, FixedWidth as _, FluentBuilder as _, HeaderResizeInfo,
+ Indicator, InteractiveElement, IntoElement, ParentElement, Pixels, RedistributableColumnsState,
+ RegisterComponent, RenderOnce, ScrollAxes, ScrollableHandle, Scrollbars, SharedString,
+ StatefulInteractiveElement, Styled, StyledExt as _, StyledTypography, Window, WithScrollbar,
+ bind_redistributable_columns, div, example_group_with_title, h_flex, px,
+ render_redistributable_columns_resize_handles, single_example,
table_row::{IntoTableRow as _, TableRow},
v_flex,
};
@@ -22,16 +22,10 @@ pub mod table_row;
#[cfg(test)]
mod tests;
-const RESIZE_COLUMN_WIDTH: f32 = 8.0;
-const RESIZE_DIVIDER_WIDTH: f32 = 1.0;
-
/// Represents an unchecked table row, which is a vector of elements.
/// Will be converted into `TableRow<T>` internally
pub type UncheckedTableRow<T> = Vec<T>;
-#[derive(Debug)]
-pub(crate) struct DraggedColumn(pub(crate) usize);
-
struct UniformListData {
render_list_of_rows_fn:
Box<dyn Fn(Range<usize>, &mut Window, &mut App) -> Vec<UncheckedTableRow<AnyElement>>>,
@@ -113,124 +107,6 @@ impl TableInteractionState {
}
}
-/// Renders invisible resize handles overlaid on top of table content.
-///
-/// - Spacer: invisible element that matches the width of table column content
-/// - Divider: contains the actual resize handle that users can drag to resize columns
-///
-/// Structure: [spacer] [divider] [spacer] [divider] [spacer]
-///
-/// Business logic:
-/// 1. Creates spacers matching each column width
-/// 2. Intersperses (inserts) resize handles between spacers (interactive only for resizable columns)
-/// 3. Each handle supports hover highlighting, double-click to reset, and drag to resize
-/// 4. Returns an absolute-positioned overlay that sits on top of table content
-fn render_resize_handles(
- column_widths: &TableRow<Length>,
- resizable_columns: &TableRow<TableResizeBehavior>,
- initial_sizes: &TableRow<DefiniteLength>,
- columns: Option<Entity<RedistributableColumnsState>>,
- window: &mut Window,
- cx: &mut App,
-) -> AnyElement {
- let spacers = column_widths
- .as_slice()
- .iter()
- .map(|width| base_cell_style(Some(*width)).into_any_element());
-
- let mut column_ix = 0;
- let resizable_columns_shared = Rc::new(resizable_columns.clone());
- let initial_sizes_shared = Rc::new(initial_sizes.clone());
- let mut resizable_columns_iter = resizable_columns.as_slice().iter();
-
- let dividers = intersperse_with(spacers, || {
- let resizable_columns = Rc::clone(&resizable_columns_shared);
- let initial_sizes = Rc::clone(&initial_sizes_shared);
- window.with_id(column_ix, |window| {
- let mut resize_divider = div()
- .id(column_ix)
- .relative()
- .top_0()
- .w(px(RESIZE_DIVIDER_WIDTH))
- .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_iter
- .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,
- 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 enum ColumnWidthConfig {
/// Static column widths (no resize handles).
Static {
@@ -278,6 +154,21 @@ impl ColumnWidthConfig {
}
}
+ /// Explicit column widths with no fixed table width.
+ pub fn explicit<T: Into<DefiniteLength>>(widths: Vec<T>) -> Self {
+ let cols = widths.len();
+ ColumnWidthConfig::Static {
+ widths: StaticColumnWidths::Explicit(
+ widths
+ .into_iter()
+ .map(Into::into)
+ .collect::<Vec<_>>()
+ .into_table_row(cols),
+ ),
+ table_width: None,
+ }
+ }
+
/// Column widths for rendering.
pub fn widths_to_render(&self, cx: &App) -> Option<TableRow<Length>> {
match self {
@@ -292,10 +183,7 @@ impl ColumnWidthConfig {
ColumnWidthConfig::Redistributable {
columns_state: entity,
..
- } => {
- let state = entity.read(cx);
- Some(state.preview_widths.map_cloned(Length::Definite))
- }
+ } => Some(entity.read(cx).widths_to_render()),
}
}
@@ -316,296 +204,6 @@ impl ColumnWidthConfig {
None => ListHorizontalSizingBehavior::FitList,
}
}
-
- /// Render resize handles overlay if applicable.
- pub fn render_resize_handles(&self, window: &mut Window, cx: &mut App) -> Option<AnyElement> {
- match self {
- ColumnWidthConfig::Redistributable {
- columns_state: entity,
- ..
- } => {
- let (column_widths, resize_behavior, initial_widths) = {
- let state = entity.read(cx);
- (
- state.preview_widths.map_cloned(Length::Definite),
- state.resize_behavior.clone(),
- state.initial_widths.clone(),
- )
- };
- Some(render_resize_handles(
- &column_widths,
- &resize_behavior,
- &initial_widths,
- Some(entity.clone()),
- window,
- cx,
- ))
- }
- _ => None,
- }
- }
-
- /// Returns info needed for header double-click-to-reset, if applicable.
- pub fn header_resize_info(&self, cx: &App) -> Option<HeaderResizeInfo> {
- match self {
- ColumnWidthConfig::Redistributable { columns_state, .. } => {
- let state = columns_state.read(cx);
- Some(HeaderResizeInfo {
- columns_state: columns_state.downgrade(),
- resize_behavior: state.resize_behavior.clone(),
- initial_widths: state.initial_widths.clone(),
- })
- }
- _ => None,
- }
- }
-}
-
-#[derive(Clone)]
-pub struct HeaderResizeInfo {
- pub columns_state: WeakEntity<RedistributableColumnsState>,
- pub resize_behavior: TableRow<TableResizeBehavior>,
- pub initial_widths: TableRow<DefiniteLength>,
-}
-
-pub struct RedistributableColumnsState {
- pub(crate) initial_widths: TableRow<DefiniteLength>,
- pub(crate) committed_widths: TableRow<DefiniteLength>,
- pub(crate) preview_widths: TableRow<DefiniteLength>,
- pub(crate) resize_behavior: TableRow<TableResizeBehavior>,
- pub(crate) cached_table_width: Pixels,
-}
-
-impl RedistributableColumnsState {
- pub fn new(
- cols: usize,
- initial_widths: UncheckedTableRow<impl Into<DefiniteLength>>,
- resize_behavior: UncheckedTableRow<TableResizeBehavior>,
- ) -> Self {
- let widths: TableRow<DefiniteLength> = initial_widths
- .into_iter()
- .map(Into::into)
- .collect::<Vec<_>>()
- .into_table_row(cols);
- Self {
- initial_widths: widths.clone(),
- committed_widths: widths.clone(),
- preview_widths: widths,
- resize_behavior: resize_behavior.into_table_row(cols),
- cached_table_width: Default::default(),
- }
- }
-
- pub fn cols(&self) -> usize {
- self.committed_widths.cols()
- }
-
- pub fn initial_widths(&self) -> &TableRow<DefiniteLength> {
- &self.initial_widths
- }
-
- pub fn resize_behavior(&self) -> &TableRow<TableResizeBehavior> {
- &self.resize_behavior
- }
-
- 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,
- }
- }
-
- pub(crate) fn on_double_click(
- &mut self,
- double_click_position: usize,
- initial_sizes: &TableRow<DefiniteLength>,
- resize_behavior: &TableRow<TableResizeBehavior>,
- window: &mut Window,
- ) {
- let bounds_width = self.cached_table_width;
- let rem_size = window.rem_size();
- let initial_sizes =
- initial_sizes.map_ref(|length| Self::get_fraction(length, bounds_width, rem_size));
- let widths = self
- .committed_widths
- .map_ref(|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.committed_widths = updated_widths.map(DefiniteLength::Fraction);
- self.preview_widths = self.committed_widths.clone();
- }
-
- pub(crate) fn reset_to_initial_size(
- col_idx: usize,
- mut widths: TableRow<f32>,
- initial_sizes: TableRow<f32>,
- resize_behavior: &TableRow<TableResizeBehavior>,
- ) -> TableRow<f32> {
- 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
- }
-
- pub(crate) fn on_drag_move(
- &mut self,
- drag_event: &DragMoveEvent<DraggedColumn>,
- 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 divider_width = Self::get_fraction(
- &DefiniteLength::Absolute(AbsoluteLength::Pixels(px(RESIZE_DIVIDER_WIDTH))),
- bounds_width,
- rem_size,
- );
-
- let mut widths = self
- .committed_widths
- .map_ref(|length| Self::get_fraction(length, bounds_width, rem_size));
-
- for length in widths[0..=col_idx].iter() {
- col_position += length + divider_width;
- }
-
- let mut total_length_ratio = col_position;
- for length in widths[col_idx + 1..].iter() {
- total_length_ratio += length;
- }
- let cols = self.resize_behavior.cols();
- total_length_ratio += (cols - 1 - col_idx) as f32 * divider_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 - divider_width / 2.0;
-
- Self::drag_column_handle(diff, col_idx, &mut widths, &self.resize_behavior);
-
- self.preview_widths = widths.map(DefiniteLength::Fraction);
- }
-
- pub(crate) fn drag_column_handle(
- diff: f32,
- col_idx: usize,
- widths: &mut TableRow<f32>,
- resize_behavior: &TableRow<TableResizeBehavior>,
- ) {
- 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);
- }
- }
-
- pub(crate) fn propagate_resize_diff(
- diff: f32,
- col_idx: usize,
- widths: &mut TableRow<f32>,
- resize_behavior: &TableRow<TableResizeBehavior>,
- 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 < widths.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
- }
}
/// A table component
@@ -919,11 +517,8 @@ pub fn render_table_header(
if event.click_count() > 1 {
info.columns_state
.update(cx, |column, _| {
- column.on_double_click(
- header_idx,
- &info.initial_widths,
- &info.resize_behavior,
- window,
+ column.reset_column_to_initial_width(
+ header_idx, window,
);
})
.ok();
@@ -962,6 +557,19 @@ impl TableRenderContext {
disable_base_cell_style: table.disable_base_cell_style,
}
}
+
+ pub fn for_column_widths(column_widths: Option<TableRow<Length>>, use_ui_font: bool) -> Self {
+ Self {
+ striped: false,
+ show_row_borders: true,
+ show_row_hover: true,
+ total_row_count: 0,
+ column_widths,
+ map_row: None,
+ use_ui_font,
+ disable_base_cell_style: false,
+ }
+ }
}
impl RenderOnce for Table {
@@ -969,9 +577,15 @@ impl RenderOnce for Table {
let table_context = TableRenderContext::new(&self, cx);
let interaction_state = self.interaction_state.and_then(|state| state.upgrade());
- let header_resize_info = interaction_state
- .as_ref()
- .and_then(|_| self.column_width_config.header_resize_info(cx));
+ let header_resize_info =
+ interaction_state
+ .as_ref()
+ .and_then(|_| match &self.column_width_config {
+ ColumnWidthConfig::Redistributable { columns_state, .. } => {
+ Some(HeaderResizeInfo::from_state(columns_state, cx))
+ }
+ _ => None,
+ });
let table_width = self.column_width_config.table_width();
let horizontal_sizing = self.column_width_config.list_horizontal_sizing();
@@ -985,13 +599,19 @@ impl RenderOnce for Table {
ColumnWidthConfig::Redistributable {
columns_state: entity,
..
- } => Some(entity.downgrade()),
+ } => Some(entity.clone()),
_ => None,
});
- let resize_handles = interaction_state
- .as_ref()
- .and_then(|_| self.column_width_config.render_resize_handles(window, cx));
+ let resize_handles =
+ interaction_state
+ .as_ref()
+ .and_then(|_| match &self.column_width_config {
+ ColumnWidthConfig::Redistributable { columns_state, .. } => Some(
+ render_redistributable_columns_resize_handles(columns_state, window, cx),
+ ),
+ _ => None,
+ });
let table = div()
.when_some(table_width, |this, width| this.w(width))
@@ -1006,38 +626,8 @@ impl RenderOnce for Table {
cx,
))
})
- .when_some(redistributable_entity, {
- |this, widths| {
- this.on_drag_move::<DraggedColumn>({
- let widths = widths.clone();
- move |e, window, cx| {
- widths
- .update(cx, |widths, cx| {
- widths.on_drag_move(e, 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_table_width =
- bounds[0].right() - bounds[0].left();
- })
- .ok();
- }
- })
- .on_drop::<DraggedColumn>(move |_, _, cx| {
- widths
- .update(cx, |widths, _| {
- widths.committed_widths = widths.preview_widths.clone();
- })
- .ok();
- })
- }
+ .when_some(redistributable_entity, |this, widths| {
+ bind_redistributable_columns(this, widths)
})
.child({
let content = div()
@@ -1,4 +1,5 @@
-use super::*;
+use super::table_row::TableRow;
+use crate::{RedistributableColumnsState, TableResizeBehavior};
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)
@@ -0,0 +1,485 @@
+use std::rc::Rc;
+
+use gpui::{
+ AbsoluteLength, AppContext as _, Bounds, DefiniteLength, DragMoveEvent, Empty, Entity, Length,
+ WeakEntity,
+};
+use itertools::intersperse_with;
+
+use super::data_table::table_row::{IntoTableRow as _, TableRow};
+use crate::{
+ ActiveTheme as _, AnyElement, App, Context, Div, FluentBuilder as _, InteractiveElement,
+ IntoElement, ParentElement, Pixels, StatefulInteractiveElement, Styled, Window, div, h_flex,
+ px,
+};
+
+const RESIZE_COLUMN_WIDTH: f32 = 8.0;
+const RESIZE_DIVIDER_WIDTH: f32 = 1.0;
+
+#[derive(Debug)]
+struct DraggedColumn(usize);
+
+#[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),
+ }
+ }
+}
+
+#[derive(Clone)]
+pub struct HeaderResizeInfo {
+ pub columns_state: WeakEntity<RedistributableColumnsState>,
+ pub resize_behavior: TableRow<TableResizeBehavior>,
+}
+
+impl HeaderResizeInfo {
+ pub fn from_state(columns_state: &Entity<RedistributableColumnsState>, cx: &App) -> Self {
+ let resize_behavior = columns_state.read(cx).resize_behavior().clone();
+ Self {
+ columns_state: columns_state.downgrade(),
+ resize_behavior,
+ }
+ }
+}
+
+pub struct RedistributableColumnsState {
+ pub(crate) initial_widths: TableRow<DefiniteLength>,
+ pub(crate) committed_widths: TableRow<DefiniteLength>,
+ pub(crate) preview_widths: TableRow<DefiniteLength>,
+ pub(crate) resize_behavior: TableRow<TableResizeBehavior>,
+ pub(crate) cached_container_width: Pixels,
+}
+
+impl RedistributableColumnsState {
+ pub fn new(
+ cols: usize,
+ initial_widths: Vec<impl Into<DefiniteLength>>,
+ resize_behavior: Vec<TableResizeBehavior>,
+ ) -> Self {
+ let widths: TableRow<DefiniteLength> = initial_widths
+ .into_iter()
+ .map(Into::into)
+ .collect::<Vec<_>>()
+ .into_table_row(cols);
+ Self {
+ initial_widths: widths.clone(),
+ committed_widths: widths.clone(),
+ preview_widths: widths,
+ resize_behavior: resize_behavior.into_table_row(cols),
+ cached_container_width: Default::default(),
+ }
+ }
+
+ pub fn cols(&self) -> usize {
+ self.committed_widths.cols()
+ }
+
+ pub fn initial_widths(&self) -> &TableRow<DefiniteLength> {
+ &self.initial_widths
+ }
+
+ pub fn preview_widths(&self) -> &TableRow<DefiniteLength> {
+ &self.preview_widths
+ }
+
+ pub fn resize_behavior(&self) -> &TableRow<TableResizeBehavior> {
+ &self.resize_behavior
+ }
+
+ pub fn widths_to_render(&self) -> TableRow<Length> {
+ self.preview_widths.map_cloned(Length::Definite)
+ }
+
+ pub fn preview_fractions(&self, rem_size: Pixels) -> TableRow<f32> {
+ if self.cached_container_width > px(0.) {
+ self.preview_widths
+ .map_ref(|length| Self::get_fraction(length, self.cached_container_width, rem_size))
+ } else {
+ self.preview_widths.map_ref(|length| match length {
+ DefiniteLength::Fraction(fraction) => *fraction,
+ DefiniteLength::Absolute(_) => 0.0,
+ })
+ }
+ }
+
+ pub fn preview_column_width(&self, column_index: usize, window: &Window) -> Option<Pixels> {
+ let width = self.preview_widths().as_slice().get(column_index)?;
+ match width {
+ DefiniteLength::Fraction(fraction) if self.cached_container_width > px(0.) => {
+ Some(self.cached_container_width * *fraction)
+ }
+ DefiniteLength::Fraction(_) => None,
+ DefiniteLength::Absolute(AbsoluteLength::Pixels(pixels)) => Some(*pixels),
+ DefiniteLength::Absolute(AbsoluteLength::Rems(rems_width)) => {
+ Some(rems_width.to_pixels(window.rem_size()))
+ }
+ }
+ }
+
+ pub fn cached_container_width(&self) -> Pixels {
+ self.cached_container_width
+ }
+
+ pub fn set_cached_container_width(&mut self, width: Pixels) {
+ self.cached_container_width = width;
+ }
+
+ pub fn commit_preview(&mut self) {
+ self.committed_widths = self.preview_widths.clone();
+ }
+
+ pub fn reset_column_to_initial_width(&mut self, column_index: usize, window: &Window) {
+ let bounds_width = self.cached_container_width;
+ if bounds_width <= px(0.) {
+ return;
+ }
+
+ let rem_size = window.rem_size();
+ let initial_sizes = self
+ .initial_widths
+ .map_ref(|length| Self::get_fraction(length, bounds_width, rem_size));
+ let widths = self
+ .committed_widths
+ .map_ref(|length| Self::get_fraction(length, bounds_width, rem_size));
+
+ let updated_widths =
+ Self::reset_to_initial_size(column_index, widths, initial_sizes, &self.resize_behavior);
+ self.committed_widths = updated_widths.map(DefiniteLength::Fraction);
+ self.preview_widths = self.committed_widths.clone();
+ }
+
+ 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,
+ }
+ }
+
+ pub(crate) fn reset_to_initial_size(
+ col_idx: usize,
+ mut widths: TableRow<f32>,
+ initial_sizes: TableRow<f32>,
+ resize_behavior: &TableRow<TableResizeBehavior>,
+ ) -> TableRow<f32> {
+ 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>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let drag_position = drag_event.event.position;
+ let bounds = drag_event.bounds;
+ let bounds_width = bounds.right() - bounds.left();
+ if bounds_width <= px(0.) {
+ return;
+ }
+
+ let mut col_position = 0.0;
+ let rem_size = window.rem_size();
+ let col_idx = drag_event.drag(cx).0;
+
+ let divider_width = Self::get_fraction(
+ &DefiniteLength::Absolute(AbsoluteLength::Pixels(px(RESIZE_DIVIDER_WIDTH))),
+ bounds_width,
+ rem_size,
+ );
+
+ let mut widths = self
+ .committed_widths
+ .map_ref(|length| Self::get_fraction(length, bounds_width, rem_size));
+
+ for length in widths[0..=col_idx].iter() {
+ col_position += length + divider_width;
+ }
+
+ let mut total_length_ratio = col_position;
+ for length in widths[col_idx + 1..].iter() {
+ total_length_ratio += length;
+ }
+ let cols = self.resize_behavior.cols();
+ total_length_ratio += (cols - 1 - col_idx) as f32 * divider_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 - divider_width / 2.0;
+
+ Self::drag_column_handle(diff, col_idx, &mut widths, &self.resize_behavior);
+
+ self.preview_widths = widths.map(DefiniteLength::Fraction);
+ }
+
+ pub(crate) fn drag_column_handle(
+ diff: f32,
+ col_idx: usize,
+ widths: &mut TableRow<f32>,
+ resize_behavior: &TableRow<TableResizeBehavior>,
+ ) {
+ 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);
+ }
+ }
+
+ pub(crate) fn propagate_resize_diff(
+ diff: f32,
+ col_idx: usize,
+ widths: &mut TableRow<f32>,
+ resize_behavior: &TableRow<TableResizeBehavior>,
+ 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 < widths.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 fn bind_redistributable_columns(
+ container: Div,
+ columns_state: Entity<RedistributableColumnsState>,
+) -> Div {
+ container
+ .on_drag_move::<DraggedColumn>({
+ let columns_state = columns_state.clone();
+ move |event, window, cx| {
+ columns_state.update(cx, |columns, cx| {
+ columns.on_drag_move(event, window, cx);
+ });
+ }
+ })
+ .on_children_prepainted({
+ let columns_state = columns_state.clone();
+ move |bounds, _, cx| {
+ if let Some(width) = child_bounds_width(&bounds) {
+ columns_state.update(cx, |columns, _| {
+ columns.set_cached_container_width(width);
+ });
+ }
+ }
+ })
+ .on_drop::<DraggedColumn>(move |_, _, cx| {
+ columns_state.update(cx, |columns, _| {
+ columns.commit_preview();
+ });
+ })
+}
+
+pub fn render_redistributable_columns_resize_handles(
+ columns_state: &Entity<RedistributableColumnsState>,
+ window: &mut Window,
+ cx: &mut App,
+) -> AnyElement {
+ let (column_widths, resize_behavior) = {
+ let state = columns_state.read(cx);
+ (state.widths_to_render(), state.resize_behavior().clone())
+ };
+
+ let mut column_ix = 0;
+ let resize_behavior = Rc::new(resize_behavior);
+ let dividers = intersperse_with(
+ column_widths
+ .as_slice()
+ .iter()
+ .copied()
+ .map(|width| resize_spacer(width).into_any_element()),
+ || {
+ let current_column_ix = column_ix;
+ let resize_behavior = Rc::clone(&resize_behavior);
+ let columns_state = columns_state.clone();
+ column_ix += 1;
+
+ window.with_id(current_column_ix, |window| {
+ let mut resize_divider = div()
+ .id(current_column_ix)
+ .relative()
+ .top_0()
+ .w(px(RESIZE_DIVIDER_WIDTH))
+ .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 resize_behavior[current_column_ix].is_resizable() {
+ let is_highlighted = window.use_state(cx, |_window, _cx| false);
+
+ resize_divider = resize_divider.when(*is_highlighted.read(cx), |div| {
+ div.bg(cx.theme().colors().border_focused)
+ });
+
+ resize_handle = resize_handle
+ .on_hover({
+ let is_highlighted = is_highlighted.clone();
+ move |&was_hovered, _, cx| is_highlighted.write(cx, was_hovered)
+ })
+ .cursor_col_resize()
+ .on_click({
+ let columns_state = columns_state.clone();
+ move |event, window, cx| {
+ if event.click_count() >= 2 {
+ columns_state.update(cx, |columns, _| {
+ columns.reset_column_to_initial_width(
+ current_column_ix,
+ window,
+ );
+ });
+ }
+
+ cx.stop_propagation();
+ }
+ })
+ .on_drag(DraggedColumn(current_column_ix), {
+ let is_highlighted = is_highlighted.clone();
+ move |_, _offset, _window, cx| {
+ is_highlighted.write(cx, true);
+ cx.new(|_cx| Empty)
+ }
+ })
+ .on_drop::<DraggedColumn>(move |_, _, cx| {
+ is_highlighted.write(cx, false);
+ columns_state.update(cx, |state, _| {
+ state.commit_preview();
+ });
+ });
+ }
+
+ resize_divider.child(resize_handle).into_any_element()
+ })
+ },
+ );
+
+ h_flex()
+ .id("resize-handles")
+ .absolute()
+ .inset_0()
+ .w_full()
+ .children(dividers)
+ .into_any_element()
+}
+
+fn resize_spacer(width: Length) -> Div {
+ div().w(width).h_full()
+}
+
+fn child_bounds_width(bounds: &[Bounds<Pixels>]) -> Option<Pixels> {
+ let first_bounds = bounds.first()?;
+ let mut left = first_bounds.left();
+ let mut right = first_bounds.right();
+
+ for bound in bounds.iter().skip(1) {
+ left = left.min(bound.left());
+ right = right.max(bound.right());
+ }
+
+ Some(right - left)
+}