1use std::sync::Arc;
31
32use gpui_kit::base::TestSupportExt as _;
33use gpui_kit::component::{
34 ActiveTheme as _, Icon, IconName, Sizable as _, Size as ComponentSize, ThemeStyled as _,
35 button::{Button, ButtonVariants as _},
36 menu::{PopupMenu, PopupMenuItem},
37 tag::Tag,
38};
39use gpui_kit::{
40 AnyElement, AppContext as _, Bounds, ClickEvent, Context, CursorStyle, Div, DragMoveEvent,
41 ElementId, EntityId, FocusHandle, InteractiveElement, IntoElement, MouseButton, MouseDownEvent,
42 ParentElement as _, Pixels, Point, Render, Role, SharedString, StatefulInteractiveElement as _,
43 Styled as _, WeakEntity, Window, div, point, prelude::FluentBuilder as _, px, size,
44};
45use rust_i18n::t;
46
47use super::{
48 CELL_PADDING, ColumnOf, GridMenuContext, GridTarget, LiveGridDelegate, LiveGridState,
49 columns::column_of, events::GroupOrigin, ghost::GhostTextStyle, header::DragSource,
50};
51use crate::{
52 actions::{
53 ClearGrouping, CollapseAll, ExpandAll, GROUP_PANEL_KEY_CONTEXT, GroupByColumn,
54 ShowCaptionsOnly, ShowContextMenu, UngroupColumn,
55 },
56 engine::{ExpandDepth, GroupBy, SortDirection},
57 model::{ColumnId, ColumnIx, GridColumns, GridRow},
58 policy::GridArea,
59};
60
61const PANEL_ID: &str = "live-grid-group-panel";
63const CHIP_ID: &str = "live-grid-group-chip";
65const CHIP_REMOVE_ID: &str = "live-grid-group-chip-remove";
67const HINT_ID: &str = "live-grid-group-panel-hint";
69const GAP_ID: &str = "live-grid-group-panel-gap";
71const CHIP_GAP: Pixels = px(4.);
73const GAP_LINE: Pixels = px(2.);
75
76#[derive(Default)]
78pub(super) struct GroupPanelState {
79 shown: bool,
81 size: ComponentSize,
83 chips: Vec<(ColumnId, FocusHandle)>,
86 chip_bounds: Vec<Bounds<Pixels>>,
89 target: Option<PanelTarget>,
91 dragged: Option<LevelDrag>,
93 menu_press: bool,
96}
97
98impl GroupPanelState {
99 pub(super) fn is_shown(&self) -> bool {
101 self.shown
102 }
103
104 fn chip_handle(&self, level: &ColumnId) -> Option<&FocusHandle> {
105 self.chips
106 .iter()
107 .find(|(id, _)| id == level)
108 .map(|(_, handle)| handle)
109 }
110}
111
112#[derive(Clone, Copy, Debug, PartialEq)]
114pub(super) enum PanelTarget {
115 Gap { gap: usize, x: Pixels },
118 Here,
120 Refused,
123}
124
125impl PanelTarget {
126 pub(super) fn is_accepted(self) -> bool {
128 self != Self::Refused
129 }
130}
131
132struct LevelDrag {
134 cursor: Option<CursorStyle>,
136}
137
138#[derive(Clone)]
141pub(crate) struct DraggedGroupLevel {
142 grid: EntityId,
143 level: ColumnId,
144 icon: Option<SharedString>,
146 title: SharedString,
147 direction: SortDirection,
148 size: ComponentSize,
149 text: GhostTextStyle,
150}
151
152impl Render for DraggedGroupLevel {
153 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
154 let theme = cx.theme();
155 self.text.root().child(
156 Tag::new()
157 .outline()
158 .with_size(self.size)
159 .gap_1()
160 .bg(theme.table_head)
161 .text_color(theme.table_head_foreground)
162 .shadow_md()
163 .opacity(0.9)
164 .children(self.icon.clone().map(level_icon))
165 .child(self.title.clone())
166 .child(Icon::new(direction_icon(self.direction)).size_3()),
167 )
168 }
169}
170
171#[derive(Clone, Copy, Debug, Default)]
173pub(super) struct MenuGrouping {
174 column: Option<ColumnGrouping>,
176 grouped: bool,
178}
179
180#[derive(Clone, Copy, Debug)]
181struct ColumnGrouping {
182 groupable: bool,
184 level: bool,
186}
187
188impl<'a, R: GridRow> GridMenuContext<'a, R> {
189 pub fn grouping_items(&self, menu: PopupMenu) -> PopupMenu {
198 grouping_items(self.grouping, menu)
199 }
200}
201
202pub(super) fn grouping_items(grouping: MenuGrouping, menu: PopupMenu) -> PopupMenu {
204 menu.when_some(grouping.column, |menu, column| {
205 menu.menu_with_disabled(
206 label("live_grid.menu.group_by"),
207 Box::new(GroupByColumn),
208 !column.groupable || column.level,
209 )
210 .menu_with_disabled(
211 label("live_grid.menu.ungroup"),
212 Box::new(UngroupColumn),
213 !column.level,
214 )
215 })
216 .menu_with_disabled(
217 label("live_grid.menu.clear_grouping"),
218 Box::new(ClearGrouping),
219 !grouping.grouped,
220 )
221}
222
223fn label(key: &str) -> SharedString {
225 SharedString::from(t!(key).into_owned())
226}
227
228fn direction_icon(direction: SortDirection) -> IconName {
230 match direction {
231 SortDirection::Ascending => IconName::SortAscending,
232 SortDirection::Descending => IconName::SortDescending,
233 }
234}
235
236fn reversed(direction: SortDirection) -> SortDirection {
237 match direction {
238 SortDirection::Ascending => SortDirection::Descending,
239 SortDirection::Descending => SortDirection::Ascending,
240 }
241}
242
243fn level_icon(path: SharedString) -> Icon {
245 Icon::empty().path(path).size_3()
246}
247
248fn chip_label(title: &str, direction: SortDirection) -> SharedString {
250 let direction = match direction {
251 SortDirection::Ascending => t!("live_grid.sort.ascending"),
252 SortDirection::Descending => t!("live_grid.sort.descending"),
253 };
254 let text = t!(
255 "live_grid.group_panel.chip",
256 title = title,
257 direction = direction
258 );
259 SharedString::from(text.into_owned())
260}
261
262fn remove_label(title: &str) -> SharedString {
264 SharedString::from(t!("live_grid.group_panel.remove", title = title).into_owned())
265}
266
267fn chip_part_id(part: &'static str, level: &ColumnId) -> ElementId {
270 ElementId::NamedChild(
271 Arc::new(ElementId::Name(part.into())),
272 SharedString::from(level.as_str().to_owned()),
273 )
274}
275
276fn remove_button_size(size: ComponentSize) -> ComponentSize {
279 match size {
280 ComponentSize::Large | ComponentSize::Medium => ComponentSize::Small,
281 _ => ComponentSize::XSmall,
282 }
283}
284
285impl<D: LiveGridDelegate> LiveGridState<D> {
286 pub(crate) fn set_group_panel_shown(
289 &mut self,
290 shown: bool,
291 size: ComponentSize,
292 cx: &mut Context<Self>,
293 ) {
294 let panel = &mut self.group_panel;
295 if panel.shown == shown && panel.size == size {
296 return;
297 }
298 panel.shown = shown;
299 panel.size = size;
300 panel.target = None;
301 cx.notify();
303 }
304
305 #[cfg(any(test, feature = "test-support"))]
308 pub fn is_group_panel_shown(&self) -> bool {
309 self.group_panel.shown
310 }
311
312 pub(super) fn group_panel_height(&self) -> Pixels {
314 if self.group_panel.shown {
315 self.row_height
316 } else {
317 px(0.)
318 }
319 }
320
321 fn group_panel_bounds(&self) -> Option<Bounds<Pixels>> {
324 let height = self.group_panel_height();
325 (height > px(0.)).then(|| {
326 let body = self.body_bounds;
327 Bounds::new(
328 point(body.origin.x, body.origin.y - self.header_height() - height),
329 size(body.size.width, height),
330 )
331 })
332 }
333
334 pub(super) fn is_over_group_panel(&self, position: Point<Pixels>) -> bool {
336 self.group_panel_bounds()
337 .is_some_and(|bounds| bounds.contains(&position))
338 }
339
340 fn accepts_user_grouping(&self, column: ColumnIx) -> bool {
343 self.tree().is_none()
344 && self
345 .columns
346 .get(column)
347 .is_some_and(|column| column.groupable)
348 }
349
350 fn level_ix(&self, level: &ColumnId) -> Option<usize> {
352 self.view
353 .group_by()
354 .iter()
355 .position(|kept| kept.id() == *level)
356 }
357
358 pub(super) fn menu_grouping(&self, column: Option<ColumnOf<D>>) -> MenuGrouping {
360 MenuGrouping {
361 column: column.map(|column| ColumnGrouping {
362 groupable: self.accepts_user_grouping(column.ix()),
363 level: self.level_ix(&column.column_id()).is_some(),
364 }),
365 grouped: !self.view.group_by().is_empty(),
366 }
367 }
368
369 fn edit_levels(
373 &mut self,
374 edit: impl FnOnce(&mut Vec<GroupBy<D::Row>>) -> bool,
375 cx: &mut Context<Self>,
376 ) -> bool {
377 let mut levels = self.view.group_by().to_vec();
378 edit(&mut levels) && self.apply_group_by(levels, GroupOrigin::User, cx)
379 }
380
381 fn insert_column_level(&mut self, column: ColumnOf<D>, gap: usize, cx: &mut Context<Self>) {
384 let id = column.column_id();
385 self.edit_levels(
386 |levels| {
387 let (level, gap) = match levels.iter().position(|level| level.id() == id) {
388 Some(ix) => (levels.remove(ix), if gap > ix { gap - 1 } else { gap }),
389 None => (GroupBy::column(column), gap),
390 };
391 levels.insert(gap.min(levels.len()), level);
392 true
393 },
394 cx,
395 );
396 }
397
398 fn move_level_to(&mut self, level: &ColumnId, gap: usize, cx: &mut Context<Self>) {
400 let Some(ix) = self.level_ix(level) else {
401 return;
402 };
403 self.edit_levels(
404 |levels| {
405 let moved = levels.remove(ix);
406 let gap = if gap > ix { gap - 1 } else { gap };
407 levels.insert(gap.min(levels.len()), moved);
408 true
409 },
410 cx,
411 );
412 }
413
414 fn move_level_by(&mut self, level: &ColumnId, step: isize, cx: &mut Context<Self>) {
416 let Some(ix) = self.level_ix(level) else {
417 return;
418 };
419 let Some(other) = ix.checked_add_signed(step) else {
420 return;
421 };
422 self.edit_levels(
423 |levels| {
424 let swapped = other < levels.len();
425 if swapped {
426 levels.swap(ix, other);
427 }
428 swapped
429 },
430 cx,
431 );
432 }
433
434 fn set_level_direction(
436 &mut self,
437 level: &ColumnId,
438 direction: Option<SortDirection>,
439 cx: &mut Context<Self>,
440 ) {
441 let Some(ix) = self.level_ix(level) else {
442 return;
443 };
444 self.edit_levels(
445 |levels| {
446 let current = levels[ix].direction();
447 let direction = direction.unwrap_or_else(|| reversed(current));
448 levels[ix] = levels[ix].clone().with_direction(direction);
449 direction != current
450 },
451 cx,
452 );
453 }
454
455 fn remove_level(&mut self, level: &ColumnId, cx: &mut Context<Self>) -> bool {
457 self.edit_levels(
458 |levels| {
459 let count = levels.len();
460 levels.retain(|kept| kept.id() != *level);
461 levels.len() != count
462 },
463 cx,
464 )
465 }
466
467 fn ungroup_level(
471 &mut self,
472 level: &ColumnId,
473 refocus: bool,
474 window: &mut Window,
475 cx: &mut Context<Self>,
476 ) {
477 let Some(ix) = self.level_ix(level) else {
478 return;
479 };
480 let levels = self.view.group_by();
481 let neighbour = levels
482 .get(ix + 1)
483 .or_else(|| ix.checked_sub(1).and_then(|left| levels.get(left)))
484 .map(GroupBy::id);
485 let removed = self.remove_level(level, cx);
486 if removed && refocus {
487 let handle = neighbour
488 .and_then(|id| self.group_panel.chip_handle(&id))
489 .unwrap_or(&self.focus_handle)
490 .clone();
491 window.focus(&handle, cx);
492 }
493 }
494
495 fn on_group_by_column(&mut self, _: &GroupByColumn, _: &mut Window, cx: &mut Context<Self>) {
499 let Some(column) = self.current_column() else {
500 return;
501 };
502 if !self.accepts_user_grouping(column.ix()) {
503 return;
504 }
505 let id = column.column_id();
506 self.edit_levels(
507 |levels| {
508 let added = levels.iter().all(|level| level.id() != id);
509 if added {
510 levels.push(GroupBy::column(column));
511 }
512 added
513 },
514 cx,
515 );
516 }
517
518 fn on_ungroup_column(&mut self, _: &UngroupColumn, _: &mut Window, cx: &mut Context<Self>) {
519 let Some(column) = self.current_column() else {
520 return;
521 };
522 self.remove_level(&column.column_id(), cx);
523 }
524
525 fn on_clear_grouping(&mut self, _: &ClearGrouping, _: &mut Window, cx: &mut Context<Self>) {
526 self.edit_levels(
527 |levels| {
528 let grouped = !levels.is_empty();
529 levels.clear();
530 grouped
531 },
532 cx,
533 );
534 }
535
536 pub(super) fn bind_group_panel<E: InteractiveElement>(root: E, cx: &Context<Self>) -> E {
539 root.on_action(cx.listener(Self::on_group_by_column))
540 .on_action(cx.listener(Self::on_ungroup_column))
541 .on_action(cx.listener(Self::on_clear_grouping))
542 .on_drag_move(cx.listener(Self::on_level_drag_move))
543 .on_drop(cx.listener(Self::on_level_drop))
544 }
545
546 fn panel_gap_at(&self, x: Pixels) -> (usize, Pixels) {
552 let count = self.view.group_by().len();
553 let chips = &self.group_panel.chip_bounds;
554 let left = self.body_bounds.origin.x;
555 if chips.len() != count || count == 0 {
556 let x = chips
557 .last()
558 .map_or(CELL_PADDING, |chip| chip.right() + CHIP_GAP / 2. - left);
559 return (count, x);
560 }
561 let gap = chips.iter().take_while(|chip| x >= chip.center().x).count();
562 let line = match gap {
563 0 => chips[0].left() - CHIP_GAP / 2.,
564 gap if gap == count => chips[count - 1].right() + CHIP_GAP / 2.,
565 gap => (chips[gap - 1].right() + chips[gap].left()) / 2.,
566 };
567 (gap, line - left)
568 }
569
570 fn panel_column_target(&self, source: DragSource, position: Point<Pixels>) -> PanelTarget {
574 let DragSource::Column(ix) = source else {
575 return PanelTarget::Refused;
576 };
577 if !self.accepts_user_grouping(ix) {
578 return PanelTarget::Refused;
579 }
580 let (gap, x) = self.panel_gap_at(position.x);
581 match self.level_ix(&column_of::<ColumnOf<D>>(ix).column_id()) {
582 Some(level) if gap == level || gap == level + 1 => PanelTarget::Here,
583 _ => PanelTarget::Gap { gap, x },
584 }
585 }
586
587 fn panel_level_target(&self, level: &ColumnId, position: Point<Pixels>) -> PanelTarget {
590 let Some(ix) = self.level_ix(level) else {
591 return PanelTarget::Refused;
592 };
593 let (gap, x) = self.panel_gap_at(position.x);
594 if gap == ix || gap == ix + 1 {
595 PanelTarget::Here
596 } else {
597 PanelTarget::Gap { gap, x }
598 }
599 }
600
601 fn set_panel_target(&mut self, target: Option<PanelTarget>, cx: &mut Context<Self>) {
602 if self.group_panel.target != target {
603 self.group_panel.target = target;
604 cx.notify();
605 }
606 }
607
608 pub(super) fn refresh_panel_column_target(
612 &mut self,
613 source: DragSource,
614 position: Option<Point<Pixels>>,
615 cx: &mut Context<Self>,
616 ) -> Option<PanelTarget> {
617 let target = position
618 .filter(|position| self.is_over_group_panel(*position))
619 .map(|position| self.panel_column_target(source, position));
620 self.set_panel_target(target, cx);
621 target
622 }
623
624 pub(super) fn drop_column_on_group_panel(
627 &mut self,
628 source: DragSource,
629 position: Point<Pixels>,
630 cx: &mut Context<Self>,
631 ) {
632 if let (PanelTarget::Gap { gap, .. }, DragSource::Column(ix)) =
633 (self.panel_column_target(source, position), source)
634 {
635 self.insert_column_level(column_of(ix), gap, cx);
636 }
637 }
638
639 pub(super) fn clear_panel_target(&mut self) {
641 self.group_panel.target = None;
642 }
643
644 fn begin_level_drag(&mut self, cx: &mut Context<Self>) {
649 self.group_panel.dragged = Some(LevelDrag { cursor: None });
650 self.column_gesture_started(cx);
651 cx.notify();
652 }
653
654 fn end_level_drag(&mut self) {
655 self.group_panel.dragged = None;
656 self.group_panel.target = None;
657 }
658
659 pub(super) fn is_level_dragged(&self) -> bool {
661 self.group_panel.dragged.is_some()
662 }
663
664 fn on_level_drag_move(
665 &mut self,
666 event: &DragMoveEvent<DraggedGroupLevel>,
667 window: &mut Window,
668 cx: &mut Context<Self>,
669 ) {
670 let drag = event.drag(cx);
671 if drag.grid != cx.entity_id() {
672 return;
673 }
674 let level = drag.level.clone();
675 if self.group_panel.dragged.is_none() {
676 self.begin_level_drag(cx);
677 }
678 let position = event.event.position;
679 let inside = event.bounds.contains(&position);
680 let target = (inside && self.is_over_group_panel(position))
681 .then(|| self.panel_level_target(&level, position));
682 self.set_panel_target(target, cx);
683 let cursor = if inside {
685 CursorStyle::ClosedHand
686 } else {
687 CursorStyle::OperationNotAllowed
688 };
689 if let Some(dragged) = &mut self.group_panel.dragged
690 && dragged.cursor != Some(cursor)
691 {
692 dragged.cursor = Some(cursor);
693 cx.set_active_drag_cursor_style(cursor, window);
694 }
695 }
696
697 fn on_level_drop(
701 &mut self,
702 drag: &DraggedGroupLevel,
703 window: &mut Window,
704 cx: &mut Context<Self>,
705 ) {
706 if drag.grid != cx.entity_id() {
707 return;
708 }
709 let position = window.mouse_position();
710 self.end_level_drag();
711 if self.is_over_group_panel(position) {
712 if let PanelTarget::Gap { gap, .. } = self.panel_level_target(&drag.level, position) {
713 self.move_level_to(&drag.level, gap, cx);
714 }
715 } else {
716 self.ungroup_level(&drag.level, false, window, cx);
717 }
718 cx.notify();
719 }
720
721 pub(super) fn on_level_drag_mouse_up(&mut self, cx: &mut Context<Self>) {
724 if self.group_panel.dragged.is_none() {
725 return;
726 }
727 let this = cx.weak_entity();
728 cx.defer(move |cx| {
729 let _ = this.update(cx, |state, cx| {
730 if state.group_panel.dragged.is_some() {
731 state.end_level_drag();
732 cx.notify();
733 }
734 });
735 });
736 }
737
738 fn on_chip_click(&mut self, level: &ColumnId, event: &ClickEvent, cx: &mut Context<Self>) {
742 if !event.standard_click() || (self.group_panel.menu_press && !event.is_keyboard()) {
743 return;
744 }
745 self.set_level_direction(level, None, cx);
746 }
747
748 fn on_chip_remove(
750 &mut self,
751 level: &ColumnId,
752 event: &ClickEvent,
753 window: &mut Window,
754 cx: &mut Context<Self>,
755 ) {
756 if self.group_panel.menu_press && !event.is_keyboard() {
757 return;
758 }
759 self.ungroup_level(level, event.is_keyboard(), window, cx);
760 }
761
762 fn on_group_panel_mouse_down(
769 &mut self,
770 event: &MouseDownEvent,
771 window: &mut Window,
772 cx: &mut Context<Self>,
773 ) {
774 let primary = event.button == MouseButton::Left;
775 if primary {
776 self.group_panel.menu_press = self.is_menu_open();
777 if self.group_panel.menu_press {
778 return;
779 }
780 }
781 if self.is_context_press(
782 event.button,
783 event.modifiers,
784 event.click_count,
785 GridArea::Header,
786 ) {
787 self.group_panel.menu_press = primary;
788 window.focus(&self.focus_handle, cx);
789 let level = self.chip_at(event.position);
790 self.open_group_panel_menu(level, event.position, window, cx);
791 return;
792 }
793 if primary && !self.is_menu_open() {
794 window.focus(&self.focus_handle, cx);
795 }
796 }
797
798 fn chip_at(&self, position: Point<Pixels>) -> Option<ColumnId> {
800 let levels = self.view.group_by();
801 let chips = &self.group_panel.chip_bounds;
802 if chips.len() != levels.len() {
803 return None;
804 }
805 chips
806 .iter()
807 .position(|chip| chip.contains(&position))
808 .map(|ix| levels[ix].id())
809 }
810
811 fn show_chip_menu(&mut self, level: &ColumnId, window: &mut Window, cx: &mut Context<Self>) {
813 let position = self
814 .level_ix(level)
815 .and_then(|ix| self.group_panel.chip_bounds.get(ix))
816 .map(Bounds::bottom_left)
817 .or_else(|| self.group_panel_bounds().map(|panel| panel.bottom_left()))
818 .unwrap_or_default();
819 self.open_group_panel_menu(Some(level.clone()), position, window, cx);
820 }
821
822 fn open_group_panel_menu(
825 &mut self,
826 level: Option<ColumnId>,
827 position: Point<Pixels>,
828 window: &mut Window,
829 cx: &mut Context<Self>,
830 ) {
831 let this = cx.weak_entity();
832 window.defer(cx, move |window, cx| {
833 let _ = this.update(cx, |state, cx| {
834 state.build_group_panel_menu(level, position, window, cx)
835 });
836 });
837 }
838
839 fn build_group_panel_menu(
844 &mut self,
845 level: Option<ColumnId>,
846 position: Point<Pixels>,
847 window: &mut Window,
848 cx: &mut Context<Self>,
849 ) {
850 let replaced = self.menu.take().is_some();
851 let levels = self.view.group_by();
852 let chip = match level {
853 Some(level) => match self.level_ix(&level) {
854 Some(ix) => Some((ix, level, levels[ix].direction())),
855 None => {
856 if replaced {
857 self.reconsider_held(cx);
858 }
859 cx.notify();
860 return;
861 }
862 },
863 None => None,
864 };
865 let count = levels.len();
866 let captions_only = self.expand_depth() == ExpandDepth::Captions;
867 let focus = self.focus_handle.clone();
868 let this = cx.weak_entity();
869 let menu = PopupMenu::build(window, cx, move |menu, _, _| {
870 panel_menu_items(
871 menu.action_context(focus),
872 &this,
873 chip,
874 count,
875 captions_only,
876 )
877 });
878 self.install_menu(menu, GridTarget::Empty, position, window, cx);
879 cx.notify();
880 }
881
882 fn sync_chip_handles(&mut self, cx: &mut Context<Self>) {
887 let levels = self.view.group_by();
888 let chips = &mut self.group_panel.chips;
889 chips.retain(|(id, _)| levels.iter().any(|level| level.id() == *id));
890 for level in levels {
891 let id = level.id();
892 if chips.iter().all(|(kept, _)| *kept != id) {
893 chips.push((id, cx.focus_handle().tab_stop(true)));
894 }
895 }
896 }
897
898 fn level_icon(&self, level: &GroupBy<D::Row>) -> Option<SharedString> {
900 let column = level.as_column()?;
901 self.columns.get(column.ix())?.icon().cloned()
902 }
903
904 fn level_title(&self, level: &GroupBy<D::Row>) -> SharedString {
906 match (level.as_column(), level.as_key()) {
907 (Some(column), _) => self.columns.column(column.ix()).title.clone(),
908 (None, Some(key)) => SharedString::from(key.title().to_owned()),
909 (None, None) => SharedString::default(),
910 }
911 }
912
913 pub(super) fn render_group_panel(
915 &mut self,
916 window: &mut Window,
917 cx: &mut Context<Self>,
918 ) -> Option<impl IntoElement + use<D>> {
919 if !self.group_panel.shown {
920 return None;
921 }
922 self.sync_chip_handles(cx);
923 let theme = cx.theme();
924 let levels = self.view.group_by();
925 let count = levels.len();
926 let height = self.group_panel_height();
927 let hint = (count == 0).then(|| {
928 if self.tree().is_some() {
929 label("live_grid.group_panel.tree")
930 } else {
931 label("live_grid.group_panel.hint")
932 }
933 });
934 let mut chips: Vec<AnyElement> = Vec::with_capacity(count * 2);
935 for (ix, level) in levels.iter().enumerate() {
936 if ix > 0 {
937 chips.push(
939 Icon::new(IconName::ChevronRight)
940 .size_3()
941 .text_color(theme.muted_foreground)
942 .into_any_element(),
943 );
944 }
945 chips.push(
946 self.render_chip(ix, level, count, window, cx)
947 .into_any_element(),
948 );
949 }
950 let this = cx.weak_entity();
951 let row = div()
952 .flex()
953 .flex_none()
954 .items_center()
955 .gap(CHIP_GAP)
956 .on_children_prepainted(move |bounds, _, cx| {
959 let chips: Vec<Bounds<Pixels>> = bounds.into_iter().step_by(2).collect();
960 let _ = this.update(cx, |state, _| state.group_panel.chip_bounds = chips);
961 })
962 .children(chips);
963 let gap_line = self.panel_gap_line(cx);
964 Some(
965 div()
966 .id(PANEL_ID)
967 .test_support()
968 .key_context(GROUP_PANEL_KEY_CONTEXT)
969 .role(Role::Toolbar)
970 .aria_label(label("live_grid.group_panel.label"))
971 .when_some(hint.clone(), |this, hint| this.aria_description(hint))
972 .relative()
973 .flex_none()
974 .w_full()
975 .h(height)
976 .overflow_hidden()
977 .flex()
978 .items_center()
979 .pl(CELL_PADDING)
980 .bg(theme.table_head)
981 .text_color(theme.table_head_foreground)
982 .border_b_1()
985 .border_color(theme.border)
986 .on_any_mouse_down(cx.listener(Self::on_group_panel_mouse_down))
987 .child(row)
988 .when_some(hint, |this, hint| {
989 this.child(
990 div()
991 .id(HINT_ID)
992 .test_support()
993 .aria_label(hint.clone())
994 .min_w(px(0.))
995 .truncate()
996 .text_color(theme.muted_foreground)
997 .child(hint),
998 )
999 })
1000 .when_some(gap_line, |this, (x, top, line_height)| {
1001 this.child(
1002 div()
1003 .id(GAP_ID)
1004 .test_support()
1005 .absolute()
1006 .left(x - GAP_LINE / 2.)
1007 .top(top)
1008 .w(GAP_LINE)
1009 .h(line_height)
1010 .bg(theme.drag_border),
1011 )
1012 }),
1013 )
1014 }
1015
1016 fn panel_gap_line(&self, cx: &gpui_kit::App) -> Option<(Pixels, Pixels, Pixels)> {
1019 let Some(PanelTarget::Gap { x, .. }) = self.group_panel.target else {
1020 return None;
1021 };
1022 if !cx.has_active_drag() {
1023 return None;
1024 }
1025 let panel = self.group_panel_bounds()?;
1026 let inset = px(4.);
1027 let (top, height) = self
1028 .group_panel
1029 .chip_bounds
1030 .first()
1031 .map(|chip| (chip.top() - panel.top(), chip.size.height))
1032 .unwrap_or((inset, panel.size.height - inset * 2.));
1033 Some((x, top, height))
1034 }
1035
1036 fn render_chip(
1041 &self,
1042 ix: usize,
1043 level: &GroupBy<D::Row>,
1044 count: usize,
1045 window: &Window,
1046 cx: &Context<Self>,
1047 ) -> Div {
1048 let theme = cx.theme();
1049 let id = level.id();
1050 let title = self.level_title(level);
1051 let icon = self.level_icon(level);
1052 let direction = level.direction();
1053 let size = self.group_panel.size.smaller();
1054 let handle = self.group_panel.chip_handle(&id).cloned();
1055 let ring = window.last_input_was_keyboard()
1056 && handle
1057 .as_ref()
1058 .is_some_and(|handle| handle.is_focused(window));
1059 let drag = DraggedGroupLevel {
1060 grid: cx.entity_id(),
1061 level: id.clone(),
1062 icon: icon.clone(),
1063 title: title.clone(),
1064 direction,
1065 size,
1066 text: GhostTextStyle::capture(window),
1067 };
1068 let this = cx.weak_entity();
1069 let chip = div()
1070 .id(chip_part_id(CHIP_ID, &id))
1071 .test_support()
1072 .when_some(handle, |this, handle| this.track_focus(&handle))
1073 .role(Role::Button)
1074 .aria_label(chip_label(&title, direction))
1075 .aria_position_in_set(ix + 1)
1076 .aria_size_of_set(count)
1077 .flex()
1078 .items_center()
1079 .gap_1()
1080 .on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
1082 .on_click(cx.listener({
1083 let id = id.clone();
1084 move |state, event: &ClickEvent, _, cx| state.on_chip_click(&id, event, cx)
1085 }))
1086 .on_drag(drag, move |drag: &DraggedGroupLevel, _, _, cx| {
1087 let _ = this.update(cx, |state, cx| state.begin_level_drag(cx));
1088 cx.new(|_| drag.clone())
1089 })
1090 .children(icon.map(level_icon))
1091 .child(title.clone())
1092 .child(Icon::new(direction_icon(direction)).size_3());
1093 let remove = Button::new(chip_part_id(CHIP_REMOVE_ID, &id))
1094 .ghost()
1095 .icon(IconName::Close)
1096 .with_size(remove_button_size(size))
1097 .accessibility_label(remove_label(&title))
1098 .tooltip(remove_label(&title))
1099 .on_click(cx.listener({
1100 let id = id.clone();
1101 move |state, event: &ClickEvent, window, cx| {
1102 state.on_chip_remove(&id, event, window, cx)
1103 }
1104 }));
1105 div()
1106 .flex_none()
1107 .on_action(cx.listener({
1108 let id = id.clone();
1109 move |state, _: &UngroupColumn, window, cx| {
1110 state.ungroup_level(&id, true, window, cx)
1111 }
1112 }))
1113 .on_action(cx.listener(move |state, _: &ShowContextMenu, window, cx| {
1114 state.show_chip_menu(&id, window, cx)
1115 }))
1116 .child(
1117 Tag::new()
1118 .outline()
1119 .with_size(size)
1120 .py_0()
1121 .gap_0p5()
1122 .text_color(theme.table_head_foreground)
1123 .when(ring, |this| this.focus_ring_style(window, cx))
1124 .child(chip)
1125 .child(remove),
1126 )
1127 }
1128}
1129
1130fn command<D: LiveGridDelegate>(
1132 label: SharedString,
1133 this: &WeakEntity<LiveGridState<D>>,
1134 command: impl Fn(&mut LiveGridState<D>, &mut Context<LiveGridState<D>>) + 'static,
1135) -> PopupMenuItem {
1136 let this = this.clone();
1137 PopupMenuItem::new(label).on_click(move |_, _, cx| {
1138 let _ = this.update(cx, |state, cx| command(state, cx));
1139 })
1140}
1141
1142fn panel_menu_items<D: LiveGridDelegate>(
1151 menu: PopupMenu,
1152 this: &WeakEntity<LiveGridState<D>>,
1153 chip: Option<(usize, ColumnId, SortDirection)>,
1154 count: usize,
1155 captions_only: bool,
1156) -> PopupMenu {
1157 menu.when_some(chip, |menu, (ix, level, direction)| {
1158 let directed = |key: &str, to: SortDirection| {
1159 let level = level.clone();
1160 command(label(key), this, move |state, cx| {
1161 state.set_level_direction(&level, Some(to), cx)
1162 })
1163 .checked(direction == to)
1164 };
1165 let moved = |key: &str, step: isize| {
1166 let level = level.clone();
1167 command(label(key), this, move |state, cx| {
1168 state.move_level_by(&level, step, cx)
1169 })
1170 };
1171 let ungroup = {
1172 let level = level.clone();
1173 command(label("live_grid.menu.ungroup"), this, move |state, cx| {
1174 state.remove_level(&level, cx);
1175 })
1176 };
1177 menu.item(directed(
1178 "live_grid.menu.sort_ascending",
1179 SortDirection::Ascending,
1180 ))
1181 .item(directed(
1182 "live_grid.menu.sort_descending",
1183 SortDirection::Descending,
1184 ))
1185 .separator()
1186 .item(moved("live_grid.menu.move_left", -1).disabled(ix == 0))
1187 .item(moved("live_grid.menu.move_right", 1).disabled(ix + 1 >= count))
1188 .separator()
1189 .item(ungroup)
1190 })
1191 .menu_with_disabled(
1192 label("live_grid.menu.clear_grouping"),
1193 Box::new(ClearGrouping),
1194 count == 0,
1195 )
1196 .separator()
1197 .menu_with_disabled(
1198 label("live_grid.menu.expand_all"),
1199 Box::new(ExpandAll),
1200 count == 0,
1201 )
1202 .menu_with_disabled(
1203 label("live_grid.menu.collapse_all"),
1204 Box::new(CollapseAll),
1205 count == 0,
1206 )
1207 .menu_with_check_and_disabled(
1208 label("live_grid.menu.captions_only"),
1209 captions_only,
1210 Box::new(ShowCaptionsOnly),
1211 count == 0,
1212 )
1213}