1use std::sync::Arc;
14
15use gpui_kit::base::actions::Confirm;
16use gpui_kit::base::{POPUP_PRIORITY, StyledExt as _, TestSupportExt as _, h_flex};
17use gpui_kit::component::kbd::Kbd;
18use gpui_kit::component::menu::{PopupMenu, PopupMenuItem};
19use gpui_kit::{
20 AnyElement, App, Context, DismissEvent, Entity, FocusHandle, Focusable as _,
21 InteractiveElement as _, IntoElement, ParentElement as _, Pixels, Point, Role, SharedString,
22 StatefulInteractiveElement as _, Styled as _, Subscription, Window, anchored, deferred, div,
23 point, prelude::FluentBuilder as _, px, transparent_white,
24};
25use rust_i18n::t;
26
27use super::{
28 CELL_PADDING, ColumnOf, ColumnZone, KeyOf, LiveGridDelegate, LiveGridState, ScopedSelection,
29 SortOrigin, columns::column_of, selection::LineTarget,
30};
31use super::group_panel::{MenuGrouping, grouping_items};
33use crate::{
34 actions,
35 engine::{ExpandDepth, GroupInfo, GroupPath, Projection, SortDirection},
36 model::{GridColumns, GridRow},
37 store::{PinnedEdge, StoreSnapshot},
38};
39
40#[derive(Clone, Debug, PartialEq, Eq)]
45#[non_exhaustive]
46pub enum GridTarget<K, C> {
47 Cell {
49 key: K,
51 column: C,
53 },
54 Pinned {
57 edge: PinnedEdge,
59 key: K,
61 column: C,
63 },
64 Header {
67 column: C,
69 },
70 Empty,
73 GroupCaption {
75 group: GroupPath,
77 column: Option<C>,
80 },
81 GroupFooter {
83 group: GroupPath,
85 column: C,
87 },
88 Totals {
90 edge: PinnedEdge,
92 column: C,
94 },
95}
96
97impl<K, C: Copy> GridTarget<K, C> {
98 pub fn key(&self) -> Option<&K> {
102 match self {
103 Self::Cell { key, .. } => Some(key),
104 Self::Pinned { .. }
105 | Self::Header { .. }
106 | Self::Empty
107 | Self::GroupCaption { .. }
108 | Self::GroupFooter { .. }
109 | Self::Totals { .. } => None,
110 }
111 }
112
113 pub fn column(&self) -> Option<C> {
116 match self {
117 Self::Cell { column, .. }
118 | Self::Pinned { column, .. }
119 | Self::Header { column }
120 | Self::GroupFooter { column, .. }
121 | Self::Totals { column, .. } => Some(*column),
122 Self::GroupCaption { column, .. } => *column,
123 Self::Empty => None,
124 }
125 }
126
127 pub fn group(&self) -> Option<&GroupPath> {
129 match self {
130 Self::GroupCaption { group, .. } | Self::GroupFooter { group, .. } => Some(group),
131 Self::Cell { .. }
132 | Self::Pinned { .. }
133 | Self::Header { .. }
134 | Self::Empty
135 | Self::Totals { .. } => None,
136 }
137 }
138
139 pub(crate) fn line(&self) -> Option<LineTarget<K>>
143 where
144 K: Clone,
145 {
146 match self {
147 Self::Cell { key, .. } => Some(LineTarget::Row(key.clone())),
148 Self::GroupCaption { group, .. } => Some(LineTarget::Group(group.clone())),
149 Self::Pinned { .. }
150 | Self::Header { .. }
151 | Self::Empty
152 | Self::GroupFooter { .. }
153 | Self::Totals { .. } => None,
154 }
155 }
156}
157
158#[derive(Clone, Copy, Debug)]
161struct MenuColumn {
162 sortable: bool,
163 direction: Option<SortDirection>,
165 zone: ColumnZone,
166 last_visible: bool,
168 hideable: bool,
170}
171
172pub struct GridMenuContext<'a, R: GridRow> {
179 target: GridTarget<R::Key, R::Column>,
180 selection: ScopedSelection<R::Key>,
181 snapshot: &'a StoreSnapshot<R>,
182 projection: &'a Projection,
183 expanded: Option<bool>,
185 expand_depth: ExpandDepth,
187 column: Option<MenuColumn>,
188 any_hidden: bool,
189 sorted: bool,
191 pub(super) grouping: MenuGrouping,
193 focus_handle: FocusHandle,
195}
196
197impl<'a, R: GridRow> GridMenuContext<'a, R> {
198 pub fn target(&self) -> &GridTarget<R::Key, R::Column> {
200 &self.target
201 }
202
203 pub fn selection(&self) -> &ScopedSelection<R::Key> {
208 &self.selection
209 }
210
211 pub fn group(&self) -> Option<GroupInfo<'a>> {
214 self.projection.groups().find(self.target.group()?)
215 }
216
217 pub fn row(&self, key: &R::Key) -> Option<&'a Arc<R>> {
219 self.snapshot.get(key)
220 }
221
222 pub fn target_row(&self) -> Option<&'a Arc<R>> {
227 match &self.target {
228 GridTarget::Cell { key, .. } => self.snapshot.get(key),
229 GridTarget::Pinned { edge, key, .. } => self
230 .snapshot
231 .pinned_rows(*edge)
232 .iter()
233 .find(|row| row.key() == key),
234 GridTarget::Header { .. }
235 | GridTarget::Empty
236 | GridTarget::GroupCaption { .. }
237 | GridTarget::GroupFooter { .. }
238 | GridTarget::Totals { .. } => None,
239 }
240 }
241
242 pub fn standard_items(
256 &self,
257 menu: PopupMenu,
258 window: &mut Window,
259 cx: &mut Context<PopupMenu>,
260 ) -> PopupMenu {
261 match &self.target {
262 GridTarget::Cell { .. } => {
263 let (column, any_hidden, sorted) = (self.column, self.any_hidden, self.sorted);
267 let grouping = self.grouping;
268 let sort = PopupMenu::build(window, cx, {
269 let focus = self.focus_handle.clone();
270 move |menu, _, _| sort_items(column, sorted, menu.action_context(focus))
271 });
272 let columns = PopupMenu::build(window, cx, {
273 let focus = self.focus_handle.clone();
274 move |menu, _, _| {
275 column_items(column, any_hidden, grouping, menu.action_context(focus))
276 }
277 });
278 let sortable = column.is_some_and(|column| column.sortable);
281 let menu = menu.when(self.expanded.is_some(), |menu| {
283 self.group_items(menu).separator()
284 });
285 self.copy_items(menu)
286 .separator()
287 .item(
288 PopupMenuItem::submenu(label("live_grid.menu.sort"), sort)
289 .disabled(!sortable && !sorted),
290 )
291 .item(PopupMenuItem::submenu(
292 label("live_grid.menu.column"),
293 columns,
294 ))
295 }
296 GridTarget::Header { .. } => {
297 let menu = self.sort_items(menu).separator();
298 self.copy_items(self.column_items(menu).separator())
299 }
300 GridTarget::GroupCaption { .. } => self.copy_items(self.group_items(menu).separator()),
301 GridTarget::Pinned { .. }
302 | GridTarget::GroupFooter { .. }
303 | GridTarget::Totals { .. }
304 | GridTarget::Empty => self.copy_items(menu),
305 }
306 }
307
308 pub fn group_items(&self, menu: PopupMenu) -> PopupMenu {
314 menu.when_some(self.expanded, |menu, expanded| {
315 if expanded {
316 menu.menu(
317 label("live_grid.menu.collapse"),
318 Box::new(actions::CollapseRow),
319 )
320 } else {
321 menu.menu(label("live_grid.menu.expand"), Box::new(actions::ExpandRow))
322 }
323 })
324 .menu(
325 label("live_grid.menu.expand_all"),
326 Box::new(actions::ExpandAll),
327 )
328 .menu(
329 label("live_grid.menu.collapse_all"),
330 Box::new(actions::CollapseAll),
331 )
332 .when(!self.projection.is_tree(), |menu| {
334 menu.menu_with_check(
335 label("live_grid.menu.captions_only"),
336 self.expand_depth == ExpandDepth::Captions,
337 Box::new(actions::ShowCaptionsOnly),
338 )
339 })
340 }
341
342 pub fn copy_items(&self, menu: PopupMenu) -> PopupMenu {
345 menu.menu_with_disabled(
346 label("live_grid.menu.copy"),
347 Box::new(actions::Copy),
348 self.selection.is_empty(),
349 )
350 .menu(label("live_grid.menu.copy_all"), Box::new(actions::CopyAll))
351 }
352
353 pub fn sort_items(&self, menu: PopupMenu) -> PopupMenu {
360 sort_items(self.column, self.sorted, menu)
361 }
362
363 pub fn column_items(&self, menu: PopupMenu) -> PopupMenu {
369 column_items(self.column, self.any_hidden, self.grouping, menu)
370 }
371}
372
373fn label(key: &str) -> SharedString {
375 SharedString::from(t!(key).into_owned())
376}
377
378fn column_items(
380 column: Option<MenuColumn>,
381 any_hidden: bool,
382 grouping: MenuGrouping,
383 menu: PopupMenu,
384) -> PopupMenu {
385 menu.when(column.is_some(), |menu| {
386 menu.menu(
387 label("live_grid.menu.autofit_column"),
388 Box::new(actions::AutofitColumn),
389 )
390 })
391 .menu(
392 label("live_grid.menu.autofit_columns"),
393 Box::new(actions::AutofitColumns),
394 )
395 .separator()
396 .when_some(column, |menu, column| {
397 menu.menu_with_disabled(
398 label("live_grid.menu.hide_column"),
399 Box::new(actions::HideColumn),
400 column.last_visible || !column.hideable,
401 )
402 })
403 .menu_with_disabled(
404 label("live_grid.menu.show_all_columns"),
405 Box::new(actions::ShowAllColumns),
406 !any_hidden,
407 )
408 .menu(
410 label("live_grid.menu.choose_columns"),
411 Box::new(actions::ShowColumnChooser),
412 )
413 .separator()
414 .when_some(column, |menu, column| {
416 menu.when(column.zone != ColumnZone::FrozenLeft, |menu| {
417 menu.menu(
418 label("live_grid.menu.freeze_left"),
419 Box::new(actions::FreezeColumnLeft),
420 )
421 })
422 .when(column.zone != ColumnZone::FrozenRight, |menu| {
423 menu.menu(
424 label("live_grid.menu.freeze_right"),
425 Box::new(actions::FreezeColumnRight),
426 )
427 })
428 .when(column.zone != ColumnZone::Scrolling, |menu| {
429 menu.menu(
430 label("live_grid.menu.unfreeze"),
431 Box::new(actions::UnfreezeColumn),
432 )
433 })
434 .separator()
435 })
436 .menu(
437 label("live_grid.menu.reset_columns"),
438 Box::new(actions::ResetLayout),
439 )
440 .map(|menu| grouping_items(grouping, menu.separator()))
442}
443
444fn sort_items(column: Option<MenuColumn>, sorted: bool, menu: PopupMenu) -> PopupMenu {
446 let Some(column) = column else {
447 return menu;
448 };
449 let disabled = !column.sortable;
450 menu.menu_with_check_and_disabled(
451 label("live_grid.menu.sort_ascending"),
452 column.direction == Some(SortDirection::Ascending),
453 Box::new(actions::SortAscending),
454 disabled,
455 )
456 .menu_with_check_and_disabled(
457 label("live_grid.menu.sort_descending"),
458 column.direction == Some(SortDirection::Descending),
459 Box::new(actions::SortDescending),
460 disabled,
461 )
462 .menu_with_disabled(
463 label("live_grid.menu.clear_sort"),
464 Box::new(actions::ClearSort),
465 disabled || column.direction.is_none(),
466 )
467 .menu_with_disabled(
468 label("live_grid.menu.clear_all_sorting"),
469 Box::new(actions::ClearAllSorting),
470 !sorted,
471 )
472}
473
474const PRIMARY_LABEL_ID: &str = "live-grid-primary-command";
476
477fn primary_item(
484 label: SharedString,
485 focus: FocusHandle,
486) -> impl Fn(&mut Window, &mut App) -> AnyElement + 'static {
487 move |window, _| {
488 let key = Kbd::binding_for_action_in(&Confirm { secondary: false }, &focus, window);
489 h_flex()
490 .w_full()
491 .gap_3()
492 .items_center()
493 .justify_between()
494 .child(
495 div()
496 .id(PRIMARY_LABEL_ID)
497 .test_support()
498 .role(Role::Label)
499 .aria_value(label.clone())
500 .font_semibold()
501 .child(label.clone()),
502 )
503 .children(key.map(|key| key.p_0().flex_nowrap().border_0().bg(transparent_white())))
504 .into_any_element()
505 }
506}
507
508pub(super) struct OpenMenu<K, C> {
510 entity: Entity<PopupMenu>,
511 target: GridTarget<K, C>,
512 position: Point<Pixels>,
514 _dismiss: Subscription,
515}
516
517impl<D: LiveGridDelegate> LiveGridState<D> {
518 pub fn is_menu_open(&self) -> bool {
520 self.menu.is_some()
521 }
522
523 pub(super) fn menu_position(&self) -> Option<Point<Pixels>> {
526 self.menu.as_ref().map(|menu| menu.position)
527 }
528
529 pub fn show_context_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
533 self.reveal_focus();
534 let target = self
535 .selection
536 .focus()
537 .and_then(|cell| {
538 if let Some(group) = cell.group() {
539 return Some(GridTarget::GroupCaption {
540 group: group.clone(),
541 column: None,
542 });
543 }
544 let key = cell.row()?;
545 self.snapshot.get(key)?;
546 Some(GridTarget::Cell {
547 key: key.clone(),
548 column: cell.column(),
549 })
550 })
551 .unwrap_or(GridTarget::Empty);
552 let position = self
553 .cell_menu_position(&target)
554 .unwrap_or_else(|| self.rows_bounds().origin + point(CELL_PADDING, CELL_PADDING));
555 self.open_menu(target, position, window, cx);
556 cx.notify();
557 }
558
559 fn cell_menu_position(
562 &self,
563 target: &GridTarget<KeyOf<D>, ColumnOf<D>>,
564 ) -> Option<Point<Pixels>> {
565 if let GridTarget::GroupCaption { group, .. } = target {
566 let found = self.projection.groups().find(group)?;
567 let display_ix = self.projection.group_line(found.id())?;
568 let line = display_ix.checked_sub(self.scroll_row)?;
569 let body = self.rows_bounds();
570 let bottom = body.origin.y + self.row_height * (line + 1) as f32 - self.scroll_offset;
571 return Some(point(
572 self.caption_lane_x(group.depth()),
573 bottom.clamp(body.origin.y, body.bottom()),
574 ));
575 }
576 let GridTarget::Cell { key, column } = target else {
577 return None;
578 };
579 let display_ix = self
580 .snapshot
581 .row_id(key)
582 .and_then(|id| self.projection.display_ix(id))?;
583 let line = display_ix.checked_sub(self.scroll_row)?;
584 let body = self.rows_bounds();
585 let bottom = body.origin.y + self.row_height * (line + 1) as f32 - self.scroll_offset;
586 let geometry = self.columns.geometry(body.size.width, self.scroll_x);
587 let (zone, i) = self.columns.zone_position(column.ix())?;
588 let frame = geometry.zone(zone);
589 let x = frame
590 .column_x(self.columns.zone_columns(zone), i)
591 .clamp(frame.x, (frame.x + frame.width).max(frame.x));
592 Some(point(
593 body.origin.x + x,
594 bottom.clamp(body.origin.y, body.bottom()),
595 ))
596 }
597
598 pub(super) fn open_body_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
602 let Some(pointer) = self.pointer_target() else {
603 return;
604 };
605 let position = pointer.position();
606 if let Some(target) = self.pressed_target() {
607 self.open_menu(target, position, window, cx);
608 }
609 }
610
611 pub(super) fn pressed_target(&self) -> Option<GridTarget<KeyOf<D>, ColumnOf<D>>> {
617 let pointer = self.pointer_target()?;
618 if let Some(group) = pointer.group() {
619 return Some(GridTarget::GroupCaption {
620 group: group.clone(),
621 column: pointer.column().map(column_of),
622 });
623 }
624 if pointer.row().is_none()
625 && let Some(target) = self.summary_target(pointer.position())
626 {
627 return Some(target);
628 }
629 Some(match (pointer.pinned(), pointer.row(), pointer.column()) {
630 (Some(edge), Some(key), Some(column)) => GridTarget::Pinned {
631 edge,
632 key: key.clone(),
633 column: column_of(column),
634 },
635 (None, Some(key), Some(column)) => GridTarget::Cell {
636 key: key.clone(),
637 column: column_of(column),
638 },
639 (_, Some(_), None) => GridTarget::Empty,
640 (_, None, _) if self.is_removed_row_at(pointer.position()) => return None,
641 (_, None, _) => GridTarget::Empty,
642 })
643 }
644
645 fn is_removed_row_at(&self, position: Point<Pixels>) -> bool {
648 self.line_at(position)
649 .and_then(|line| self.painted.get(line)?.row())
650 .is_some_and(|id| self.snapshot.row(id).is_none())
651 }
652
653 pub(super) fn open_menu(
658 &mut self,
659 target: GridTarget<KeyOf<D>, ColumnOf<D>>,
660 position: Point<Pixels>,
661 window: &mut Window,
662 cx: &mut Context<Self>,
663 ) {
664 if let Some(column) = target.column() {
665 self.set_current_column(column.ix());
666 }
667 let this = cx.weak_entity();
668 window.defer(cx, move |window, cx| {
669 let _ = this.update(cx, |state, cx| {
670 state.build_menu(target, position, window, cx)
671 });
672 });
673 }
674
675 fn build_menu(
679 &mut self,
680 target: GridTarget<KeyOf<D>, ColumnOf<D>>,
681 position: Point<Pixels>,
682 window: &mut Window,
683 cx: &mut Context<Self>,
684 ) {
685 let replaced = self.menu.take().is_some();
688 self.drop_column_chooser();
689 let removed = self.is_target_gone(&target);
690 if !removed {
691 self.show_menu(target, position, window, cx);
692 }
693 if replaced && self.menu.is_none() {
694 self.reconsider_held(cx);
695 }
696 cx.notify();
697 }
698
699 fn show_menu(
700 &mut self,
701 target: GridTarget<KeyOf<D>, ColumnOf<D>>,
702 position: Point<Pixels>,
703 window: &mut Window,
704 cx: &mut Context<Self>,
705 ) {
706 let selection = self.command_scope(target.line().as_ref());
709 let expanded = self.target_expansion(&target);
711 let column = target.column().map(|column| {
712 let ix = column.ix();
713 MenuColumn {
714 sortable: self.columns.column(ix).sortable,
715 direction: self
716 .view
717 .sort()
718 .iter()
719 .find(|(sorted, _)| *sorted == column)
720 .map(|(_, direction)| *direction),
721 zone: self.columns.zone_of(ix),
722 last_visible: self.columns.is_visible(ix) && self.columns.visible_set().len() <= 1,
723 hideable: self.columns.column(ix).hideable,
724 }
725 });
726 let any_hidden = self.columns.any_hidden();
728 let sorted = !self.view.sort().is_empty();
729 let grouping = self.menu_grouping(target.column());
730 let snapshot = self.snapshot.clone();
731 let projection = self.projection.clone();
732 let focus = self.focus_handle.clone();
733 let context = GridMenuContext {
734 target,
735 selection,
736 snapshot: &snapshot,
737 projection: &projection,
738 expanded,
739 expand_depth: self.expand_depth(),
740 column,
741 any_hidden,
742 sorted,
743 grouping,
744 focus_handle: focus.clone(),
745 };
746 let primary = match &context.target {
747 GridTarget::Cell { .. } | GridTarget::GroupCaption { .. }
748 if !context.selection.is_empty() =>
749 {
750 self.delegate.primary_command(&context, cx)
751 }
752 GridTarget::Cell { .. }
753 | GridTarget::GroupCaption { .. }
754 | GridTarget::Pinned { .. }
755 | GridTarget::Header { .. }
756 | GridTarget::GroupFooter { .. }
757 | GridTarget::Totals { .. }
758 | GridTarget::Empty => None,
759 };
760 let delegate = &self.delegate;
761 let menu = PopupMenu::build(window, cx, |menu, window, cx| {
762 let menu = menu
763 .action_context(focus.clone())
764 .when_some(primary, |menu, label| {
765 menu.menu_element(
766 Box::new(Confirm { secondary: false }),
767 primary_item(label, focus.clone()),
768 )
769 .separator()
770 });
771 delegate.context_menu(&context, menu, window, cx)
772 });
773 if menu.read(cx).is_empty() {
774 return;
775 }
776 self.install_menu(menu, context.target, position, window, cx);
777 }
778
779 pub(super) fn install_menu(
783 &mut self,
784 menu: Entity<PopupMenu>,
785 target: GridTarget<KeyOf<D>, ColumnOf<D>>,
786 position: Point<Pixels>,
787 window: &mut Window,
788 cx: &mut Context<Self>,
789 ) {
790 let menu_focus = menu.read(cx).focus_handle(cx);
791 window.focus(&menu_focus, cx);
792 let dismiss = cx.subscribe_in(&menu, window, Self::on_menu_dismiss);
793 self.menu = Some(OpenMenu {
794 entity: menu,
795 target,
796 position,
797 _dismiss: dismiss,
798 });
799 }
800
801 fn on_menu_dismiss(
805 &mut self,
806 menu: &Entity<PopupMenu>,
807 _: &DismissEvent,
808 _: &mut Window,
809 cx: &mut Context<Self>,
810 ) {
811 if self.menu.as_ref().is_some_and(|open| open.entity == *menu) {
812 self.menu = None;
813 self.reconsider_held(cx);
814 cx.notify();
815 }
816 }
817
818 pub(super) fn is_target_gone(&self, target: &GridTarget<KeyOf<D>, ColumnOf<D>>) -> bool {
823 match target {
824 GridTarget::Cell { key, .. } => self.snapshot.get(key).is_none(),
825 GridTarget::Pinned { edge, key, .. } => !self.is_pinned_key(*edge, key),
826 GridTarget::GroupCaption { group, .. } => {
827 self.projection.groups().find(group).is_none()
828 }
829 GridTarget::GroupFooter { group, column } => {
830 !self.columns.is_visible(column.ix()) || !self.is_footer_shown(group)
831 }
832 GridTarget::Totals { edge, column } => {
833 !self.columns.is_visible(column.ix()) || self.totals() != Some(*edge)
834 }
835 GridTarget::Header { .. } | GridTarget::Empty => false,
836 }
837 }
838
839 pub(super) fn close_stale_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
846 let stale = self
847 .menu
848 .as_ref()
849 .is_some_and(|open| self.is_target_gone(&open.target));
850 if !stale {
851 return;
852 }
853 if let Some(open) = self.menu.take()
854 && open
855 .entity
856 .read(cx)
857 .focus_handle(cx)
858 .contains_focused(window, cx)
859 {
860 window.focus(&self.focus_handle, cx);
861 }
862 cx.defer_in(window, |state, _, cx| state.reconsider_held(cx));
864 }
865
866 pub(super) fn render_menu(&self, window: &Window) -> Option<impl IntoElement + use<D>> {
870 let open = self.menu.as_ref()?;
871 let viewport = window.viewport_size();
872 Some(
873 deferred(
874 anchored().position(point(px(0.), px(0.))).child(
875 div()
876 .w(viewport.width)
877 .h(viewport.height)
878 .on_scroll_wheel(|_, _, cx| cx.stop_propagation())
879 .child(
880 anchored()
881 .position(open.position)
882 .snap_to_window_with_margin(px(8.))
883 .child(open.entity.clone()),
884 ),
885 ),
886 )
887 .with_priority(POPUP_PRIORITY),
888 )
889 }
890
891 pub(super) fn on_show_context_menu(
893 &mut self,
894 _: &actions::ShowContextMenu,
895 window: &mut Window,
896 cx: &mut Context<Self>,
897 ) {
898 self.show_context_menu(window, cx);
899 }
900
901 pub(super) fn on_sort_ascending(
903 &mut self,
904 _: &actions::SortAscending,
905 _: &mut Window,
906 cx: &mut Context<Self>,
907 ) {
908 self.sort_current_column(Some(SortDirection::Ascending), cx);
909 }
910
911 pub(super) fn on_sort_descending(
913 &mut self,
914 _: &actions::SortDescending,
915 _: &mut Window,
916 cx: &mut Context<Self>,
917 ) {
918 self.sort_current_column(Some(SortDirection::Descending), cx);
919 }
920
921 pub(super) fn on_clear_sort(
923 &mut self,
924 _: &actions::ClearSort,
925 _: &mut Window,
926 cx: &mut Context<Self>,
927 ) {
928 self.sort_current_column(None, cx);
929 }
930
931 pub(super) fn on_clear_all_sorting(
933 &mut self,
934 _: &actions::ClearAllSorting,
935 _: &mut Window,
936 cx: &mut Context<Self>,
937 ) {
938 self.apply_sort([], SortOrigin::User, cx);
939 }
940
941 fn sort_current_column(&mut self, direction: Option<SortDirection>, cx: &mut Context<Self>) {
944 let Some(column) = self.current_column() else {
945 return;
946 };
947 if !self.columns.column(column.ix()).sortable {
948 return;
949 }
950 let sort: Vec<(ColumnOf<D>, SortDirection)> = match direction {
951 Some(direction) => vec![(column, direction)],
952 None => self
953 .view
954 .sort()
955 .iter()
956 .copied()
957 .filter(|(sorted, _)| *sorted != column)
958 .collect(),
959 };
960 self.apply_sort(sort, SortOrigin::User, cx);
961 }
962}