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}