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}