redistributable_columns.rs

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