redistributable_columns.rs

  1use std::rc::Rc;
  2
  3use gpui::{
  4    AbsoluteLength, AppContext as _, Bounds, DefiniteLength, DragMoveEvent, Empty, Entity,
  5    EntityId, Length, Stateful, WeakEntity,
  6};
  7use itertools::intersperse_with;
  8
  9use super::data_table::{
 10    ResizableColumnsState,
 11    table_row::{IntoTableRow as _, TableRow},
 12};
 13use crate::{
 14    ActiveTheme as _, AnyElement, App, Context, Div, FluentBuilder as _, InteractiveElement,
 15    IntoElement, ParentElement, Pixels, StatefulInteractiveElement, Styled, Window, div, h_flex,
 16    px,
 17};
 18
 19pub(crate) const RESIZE_COLUMN_WIDTH: f32 = 8.0;
 20pub(crate) const RESIZE_DIVIDER_WIDTH: f32 = 1.0;
 21
 22/// Drag payload for column resize handles.
 23/// Includes the `EntityId` of the owning column state so that
 24/// `on_drag_move` handlers on unrelated tables ignore the event.
 25#[derive(Debug)]
 26pub(crate) struct DraggedColumn {
 27    pub(crate) col_idx: usize,
 28    pub(crate) state_id: EntityId,
 29}
 30
 31#[derive(Debug, Copy, Clone, PartialEq)]
 32pub enum TableResizeBehavior {
 33    None,
 34    Resizable,
 35    MinSize(f32),
 36}
 37
 38impl TableResizeBehavior {
 39    pub fn is_resizable(&self) -> bool {
 40        *self != TableResizeBehavior::None
 41    }
 42
 43    pub fn min_size(&self) -> Option<f32> {
 44        match self {
 45            TableResizeBehavior::None => None,
 46            TableResizeBehavior::Resizable => Some(0.05),
 47            TableResizeBehavior::MinSize(min_size) => Some(*min_size),
 48        }
 49    }
 50}
 51
 52#[derive(Clone)]
 53pub(crate) enum ColumnsStateRef {
 54    Redistributable(WeakEntity<RedistributableColumnsState>),
 55    Resizable(WeakEntity<ResizableColumnsState>),
 56}
 57
 58#[derive(Clone)]
 59pub struct HeaderResizeInfo {
 60    pub(crate) columns_state: ColumnsStateRef,
 61    pub resize_behavior: TableRow<TableResizeBehavior>,
 62}
 63
 64impl HeaderResizeInfo {
 65    pub fn from_redistributable(
 66        columns_state: &Entity<RedistributableColumnsState>,
 67        cx: &App,
 68    ) -> Self {
 69        let resize_behavior = columns_state.read(cx).resize_behavior().clone();
 70        Self {
 71            columns_state: ColumnsStateRef::Redistributable(columns_state.downgrade()),
 72            resize_behavior,
 73        }
 74    }
 75
 76    pub fn from_resizable(columns_state: &Entity<ResizableColumnsState>, cx: &App) -> Self {
 77        let resize_behavior = columns_state.read(cx).resize_behavior().clone();
 78        Self {
 79            columns_state: ColumnsStateRef::Resizable(columns_state.downgrade()),
 80            resize_behavior,
 81        }
 82    }
 83
 84    pub fn reset_column(&self, col_idx: usize, window: &mut Window, cx: &mut App) {
 85        match &self.columns_state {
 86            ColumnsStateRef::Redistributable(weak) => {
 87                weak.update(cx, |state, cx| {
 88                    state.reset_column_to_initial_width(col_idx, window);
 89                    cx.notify();
 90                })
 91                .ok();
 92            }
 93            ColumnsStateRef::Resizable(weak) => {
 94                weak.update(cx, |state, cx| {
 95                    state.reset_column_to_initial_width(col_idx);
 96                    cx.notify();
 97                })
 98                .ok();
 99            }
100        }
101    }
102}
103
104pub struct RedistributableColumnsState {
105    pub(crate) initial_widths: TableRow<DefiniteLength>,
106    pub(crate) committed_widths: TableRow<DefiniteLength>,
107    pub(crate) preview_widths: TableRow<DefiniteLength>,
108    pub(crate) resize_behavior: TableRow<TableResizeBehavior>,
109    pub(crate) cached_container_width: Pixels,
110}
111
112impl RedistributableColumnsState {
113    pub fn new(
114        cols: usize,
115        initial_widths: Vec<impl Into<DefiniteLength>>,
116        resize_behavior: Vec<TableResizeBehavior>,
117    ) -> Self {
118        let widths: TableRow<DefiniteLength> = initial_widths
119            .into_iter()
120            .map(Into::into)
121            .collect::<Vec<_>>()
122            .into_table_row(cols);
123        Self {
124            initial_widths: widths.clone(),
125            committed_widths: widths.clone(),
126            preview_widths: widths,
127            resize_behavior: resize_behavior.into_table_row(cols),
128            cached_container_width: Default::default(),
129        }
130    }
131
132    pub fn cols(&self) -> usize {
133        self.committed_widths.cols()
134    }
135
136    pub fn initial_widths(&self) -> &TableRow<DefiniteLength> {
137        &self.initial_widths
138    }
139
140    pub fn preview_widths(&self) -> &TableRow<DefiniteLength> {
141        &self.preview_widths
142    }
143
144    pub fn resize_behavior(&self) -> &TableRow<TableResizeBehavior> {
145        &self.resize_behavior
146    }
147
148    pub fn widths_to_render(&self) -> TableRow<Length> {
149        self.preview_widths.map_cloned(Length::Definite)
150    }
151
152    pub fn preview_fractions(&self, rem_size: Pixels) -> TableRow<f32> {
153        if self.cached_container_width > px(0.) {
154            self.preview_widths
155                .map_ref(|length| Self::get_fraction(length, self.cached_container_width, rem_size))
156        } else {
157            self.preview_widths.map_ref(|length| match length {
158                DefiniteLength::Fraction(fraction) => *fraction,
159                DefiniteLength::Absolute(_) => 0.0,
160            })
161        }
162    }
163
164    pub fn preview_column_width(&self, column_index: usize, window: &Window) -> Option<Pixels> {
165        let width = self.preview_widths().as_slice().get(column_index)?;
166        match width {
167            DefiniteLength::Fraction(fraction) if self.cached_container_width > px(0.) => {
168                Some(self.cached_container_width * *fraction)
169            }
170            DefiniteLength::Fraction(_) => None,
171            DefiniteLength::Absolute(AbsoluteLength::Pixels(pixels)) => Some(*pixels),
172            DefiniteLength::Absolute(AbsoluteLength::Rems(rems_width)) => {
173                Some(rems_width.to_pixels(window.rem_size()))
174            }
175        }
176    }
177
178    pub fn cached_container_width(&self) -> Pixels {
179        self.cached_container_width
180    }
181
182    pub fn set_cached_container_width(&mut self, width: Pixels) {
183        self.cached_container_width = width;
184    }
185
186    pub fn commit_preview(&mut self) {
187        self.committed_widths = self.preview_widths.clone();
188    }
189
190    pub fn reset_column_to_initial_width(&mut self, column_index: usize, window: &Window) {
191        let bounds_width = self.cached_container_width;
192        if bounds_width <= px(0.) {
193            return;
194        }
195
196        let rem_size = window.rem_size();
197        let initial_sizes = self
198            .initial_widths
199            .map_ref(|length| Self::get_fraction(length, bounds_width, rem_size));
200        let widths = self
201            .committed_widths
202            .map_ref(|length| Self::get_fraction(length, bounds_width, rem_size));
203
204        let updated_widths =
205            Self::reset_to_initial_size(column_index, widths, initial_sizes, &self.resize_behavior);
206        self.committed_widths = updated_widths.map(DefiniteLength::Fraction);
207        self.preview_widths = self.committed_widths.clone();
208    }
209
210    fn get_fraction(length: &DefiniteLength, bounds_width: Pixels, rem_size: Pixels) -> f32 {
211        match length {
212            DefiniteLength::Absolute(AbsoluteLength::Pixels(pixels)) => *pixels / bounds_width,
213            DefiniteLength::Absolute(AbsoluteLength::Rems(rems_width)) => {
214                rems_width.to_pixels(rem_size) / bounds_width
215            }
216            DefiniteLength::Fraction(fraction) => *fraction,
217        }
218    }
219
220    pub(crate) fn reset_to_initial_size(
221        col_idx: usize,
222        mut widths: TableRow<f32>,
223        initial_sizes: TableRow<f32>,
224        resize_behavior: &TableRow<TableResizeBehavior>,
225    ) -> TableRow<f32> {
226        let diff = initial_sizes[col_idx] - widths[col_idx];
227
228        let left_diff =
229            initial_sizes[..col_idx].iter().sum::<f32>() - widths[..col_idx].iter().sum::<f32>();
230        let right_diff = initial_sizes[col_idx + 1..].iter().sum::<f32>()
231            - widths[col_idx + 1..].iter().sum::<f32>();
232
233        let go_left_first = if diff < 0.0 {
234            left_diff > right_diff
235        } else {
236            left_diff < right_diff
237        };
238
239        if !go_left_first {
240            let diff_remaining =
241                Self::propagate_resize_diff(diff, col_idx, &mut widths, resize_behavior, 1);
242
243            if diff_remaining != 0.0 && col_idx > 0 {
244                Self::propagate_resize_diff(
245                    diff_remaining,
246                    col_idx,
247                    &mut widths,
248                    resize_behavior,
249                    -1,
250                );
251            }
252        } else {
253            let diff_remaining =
254                Self::propagate_resize_diff(diff, col_idx, &mut widths, resize_behavior, -1);
255
256            if diff_remaining != 0.0 {
257                Self::propagate_resize_diff(
258                    diff_remaining,
259                    col_idx,
260                    &mut widths,
261                    resize_behavior,
262                    1,
263                );
264            }
265        }
266
267        widths
268    }
269
270    fn on_drag_move(
271        &mut self,
272        drag_event: &DragMoveEvent<DraggedColumn>,
273        window: &mut Window,
274        cx: &mut Context<Self>,
275    ) {
276        let drag_position = drag_event.event.position;
277        let bounds = drag_event.bounds;
278        let bounds_width = bounds.right() - bounds.left();
279        if bounds_width <= px(0.) {
280            return;
281        }
282
283        let mut col_position = 0.0;
284        let rem_size = window.rem_size();
285        let col_idx = drag_event.drag(cx).col_idx;
286
287        let divider_width = Self::get_fraction(
288            &DefiniteLength::Absolute(AbsoluteLength::Pixels(px(RESIZE_DIVIDER_WIDTH))),
289            bounds_width,
290            rem_size,
291        );
292
293        let mut widths = self
294            .committed_widths
295            .map_ref(|length| Self::get_fraction(length, bounds_width, rem_size));
296
297        for length in widths[0..=col_idx].iter() {
298            col_position += length + divider_width;
299        }
300
301        let mut total_length_ratio = col_position;
302        for length in widths[col_idx + 1..].iter() {
303            total_length_ratio += length;
304        }
305        let cols = self.resize_behavior.cols();
306        total_length_ratio += (cols - 1 - col_idx) as f32 * divider_width;
307
308        let drag_fraction = (drag_position.x - bounds.left()) / bounds_width;
309        let drag_fraction = drag_fraction * total_length_ratio;
310        let diff = drag_fraction - col_position - divider_width / 2.0;
311
312        Self::drag_column_handle(diff, col_idx, &mut widths, &self.resize_behavior);
313
314        self.preview_widths = widths.map(DefiniteLength::Fraction);
315    }
316
317    pub(crate) fn drag_column_handle(
318        diff: f32,
319        col_idx: usize,
320        widths: &mut TableRow<f32>,
321        resize_behavior: &TableRow<TableResizeBehavior>,
322    ) {
323        if diff > 0.0 {
324            Self::propagate_resize_diff(diff, col_idx, widths, resize_behavior, 1);
325        } else {
326            Self::propagate_resize_diff(-diff, col_idx + 1, widths, resize_behavior, -1);
327        }
328    }
329
330    pub(crate) fn propagate_resize_diff(
331        diff: f32,
332        col_idx: usize,
333        widths: &mut TableRow<f32>,
334        resize_behavior: &TableRow<TableResizeBehavior>,
335        direction: i8,
336    ) -> f32 {
337        let mut diff_remaining = diff;
338        if resize_behavior[col_idx].min_size().is_none() {
339            return diff;
340        }
341
342        let step_right;
343        let step_left;
344        if direction < 0 {
345            step_right = 0;
346            step_left = 1;
347        } else {
348            step_right = 1;
349            step_left = 0;
350        }
351        if col_idx == 0 && direction < 0 {
352            return diff;
353        }
354        let mut curr_column = col_idx + step_right - step_left;
355
356        while diff_remaining != 0.0 && curr_column < widths.cols() {
357            let Some(min_size) = resize_behavior[curr_column].min_size() else {
358                if curr_column == 0 {
359                    break;
360                }
361                curr_column -= step_left;
362                curr_column += step_right;
363                continue;
364            };
365
366            let curr_width = widths[curr_column] - diff_remaining;
367            widths[curr_column] = curr_width;
368
369            if min_size > curr_width {
370                diff_remaining = min_size - curr_width;
371                widths[curr_column] = min_size;
372            } else {
373                diff_remaining = 0.0;
374                break;
375            }
376            if curr_column == 0 {
377                break;
378            }
379            curr_column -= step_left;
380            curr_column += step_right;
381        }
382        widths[col_idx] = widths[col_idx] + (diff - diff_remaining);
383
384        diff_remaining
385    }
386}
387
388pub fn bind_redistributable_columns(
389    container: Div,
390    columns_state: Entity<RedistributableColumnsState>,
391) -> Div {
392    container
393        .on_drag_move::<DraggedColumn>({
394            let columns_state = columns_state.clone();
395            move |event, window, cx| {
396                if event.drag(cx).state_id != columns_state.entity_id() {
397                    return;
398                }
399                columns_state.update(cx, |columns, cx| {
400                    columns.on_drag_move(event, window, cx);
401                });
402            }
403        })
404        .on_children_prepainted({
405            let columns_state = columns_state.clone();
406            move |bounds, _, cx| {
407                if let Some(width) = child_bounds_width(&bounds) {
408                    columns_state.update(cx, |columns, _| {
409                        columns.set_cached_container_width(width);
410                    });
411                }
412            }
413        })
414        .on_drop::<DraggedColumn>(move |_, _, cx| {
415            columns_state.update(cx, |columns, _| {
416                columns.commit_preview();
417            });
418        })
419}
420
421pub fn render_redistributable_columns_resize_handles(
422    columns_state: &Entity<RedistributableColumnsState>,
423    window: &mut Window,
424    cx: &mut App,
425) -> AnyElement {
426    let (column_widths, resize_behavior) = {
427        let state = columns_state.read(cx);
428        (state.widths_to_render(), state.resize_behavior().clone())
429    };
430
431    let mut column_ix = 0;
432    let resize_behavior = Rc::new(resize_behavior);
433    let dividers = intersperse_with(
434        column_widths
435            .as_slice()
436            .iter()
437            .copied()
438            .map(|width| resize_spacer(width).into_any_element()),
439        || {
440            let current_column_ix = column_ix;
441            let resize_behavior = Rc::clone(&resize_behavior);
442            let columns_state = columns_state.clone();
443            column_ix += 1;
444
445            {
446                let divider = div().id(current_column_ix).relative().top_0();
447                let entity_id = columns_state.entity_id();
448                let on_reset: Rc<dyn Fn(&mut Window, &mut App)> = {
449                    let columns_state = columns_state.clone();
450                    Rc::new(move |window, cx| {
451                        columns_state.update(cx, |columns, cx| {
452                            columns.reset_column_to_initial_width(current_column_ix, window);
453                            cx.notify();
454                        });
455                    })
456                };
457                let on_drag_end: Option<Rc<dyn Fn(&mut App)>> = {
458                    Some(Rc::new(move |cx| {
459                        columns_state.update(cx, |state, _| state.commit_preview());
460                    }))
461                };
462                render_column_resize_divider(
463                    divider,
464                    current_column_ix,
465                    resize_behavior[current_column_ix].is_resizable(),
466                    entity_id,
467                    on_reset,
468                    on_drag_end,
469                    window,
470                    cx,
471                )
472            }
473        },
474    );
475
476    h_flex()
477        .id("resize-handles")
478        .absolute()
479        .inset_0()
480        .w_full()
481        .children(dividers)
482        .into_any_element()
483}
484
485/// Builds a single column resize divider with an interactive drag handle.
486///
487/// The caller provides:
488/// - `divider`: a pre-positioned divider element (with absolute or relative positioning)
489/// - `col_idx`: which column this divider is for
490/// - `is_resizable`: whether the column supports resizing
491/// - `entity_id`: the `EntityId` of the owning column state (for the drag payload)
492/// - `on_reset`: called on double-click to reset the column to its initial width
493/// - `on_drag_end`: called when the drag ends (e.g. to commit preview widths)
494pub(crate) fn render_column_resize_divider(
495    divider: Stateful<Div>,
496    col_idx: usize,
497    is_resizable: bool,
498    entity_id: EntityId,
499    on_reset: Rc<dyn Fn(&mut Window, &mut App)>,
500    on_drag_end: Option<Rc<dyn Fn(&mut App)>>,
501    window: &mut Window,
502    cx: &mut App,
503) -> AnyElement {
504    window.with_id(col_idx, |window| {
505        let mut resize_divider = divider.w(px(RESIZE_DIVIDER_WIDTH)).h_full().bg(cx
506            .theme()
507            .colors()
508            .border
509            .opacity(0.8));
510
511        let mut resize_handle = div()
512            .id("column-resize-handle")
513            .absolute()
514            .left_neg_0p5()
515            .w(px(RESIZE_COLUMN_WIDTH))
516            .h_full();
517
518        if is_resizable {
519            let is_highlighted = window.use_state(cx, |_window, _cx| false);
520
521            resize_divider = resize_divider.when(*is_highlighted.read(cx), |div| {
522                div.bg(cx.theme().colors().border_focused)
523            });
524
525            resize_handle = resize_handle
526                .on_hover({
527                    let is_highlighted = is_highlighted.clone();
528                    move |&was_hovered, _, cx| is_highlighted.write(cx, was_hovered)
529                })
530                .cursor_col_resize()
531                .on_click(move |event, window, cx| {
532                    if event.click_count() >= 2 {
533                        on_reset(window, cx);
534                    }
535                    cx.stop_propagation();
536                })
537                .on_drag(
538                    DraggedColumn {
539                        col_idx,
540                        state_id: entity_id,
541                    },
542                    {
543                        let is_highlighted = is_highlighted.clone();
544                        move |_, _offset, _window, cx| {
545                            is_highlighted.write(cx, true);
546                            cx.new(|_cx| Empty)
547                        }
548                    },
549                )
550                .on_drop::<DraggedColumn>(move |_, _, cx| {
551                    is_highlighted.write(cx, false);
552                    if let Some(on_drag_end) = &on_drag_end {
553                        on_drag_end(cx);
554                    }
555                });
556        }
557
558        resize_divider.child(resize_handle).into_any_element()
559    })
560}
561
562fn resize_spacer(width: Length) -> Div {
563    div().w(width).h_full()
564}
565
566fn child_bounds_width(bounds: &[Bounds<Pixels>]) -> Option<Pixels> {
567    let first_bounds = bounds.first()?;
568    let mut left = first_bounds.left();
569    let mut right = first_bounds.right();
570
571    for bound in bounds.iter().skip(1) {
572        left = left.min(bound.left());
573        right = right.max(bound.right());
574    }
575
576    Some(right - left)
577}