1use std::{rc::Rc, sync::Arc};
20
21use gpui_kit::base::actions::Cancel;
22use gpui_kit::base::{POPUP_PRIORITY, TestSupportExt as _};
23use gpui_kit::component::{
24 ActiveTheme as _, Disableable as _, Icon, IconName, IndexPath, Sizable as _, ThemeStyled as _,
25 button::{Button, ButtonVariants as _},
26 checkbox::Checkbox,
27 input::{Escape, Input, InputEvent, InputState},
28 list::{List, ListDelegate, ListItem, ListState},
29};
30use gpui_kit::{
31 AnyView, App, AppContext as _, Context, DismissEvent, ElementId, Entity, EventEmitter,
32 FocusHandle, Focusable, InteractiveElement as _, IntoElement, ParentElement as _, Pixels,
33 Point, Render, Role, SharedString, StatefulInteractiveElement as _, Styled as _, Subscription,
34 Window, anchored, deferred, div, point, prelude::FluentBuilder as _, px,
35};
36use rust_i18n::t;
37
38use super::{
39 ColumnInfo, LiveGridDelegate, LiveGridHandle, LiveGridState, WeakLiveGridHandle,
40 columns::{ColumnZone, LayoutDelta},
41 events::{LayoutOrigin, LayoutPart},
42 ghost::GhostTextStyle,
43 handle::GridHandleEvent,
44};
45use crate::{
46 actions::{COLUMN_CHOOSER_KEY_CONTEXT, MoveColumnLeft, MoveColumnRight, ResetLayout},
47 model::{ColumnId, ColumnIx, ColumnSet, GridColumns as _},
48};
49
50pub(crate) const CHOOSER_WIDTH: Pixels = px(280.);
52
53const LIST_MAX_HEIGHT: Pixels = px(360.);
55
56const BAND_INDENT: Pixels = px(16.);
58
59const DEFAULT_SEARCH_THRESHOLD: usize = 10;
61
62#[derive(Clone, Debug, PartialEq, Eq)]
65pub(crate) enum ChooserItem {
66 Column(ColumnId),
67 Band(&'static str),
68}
69
70pub(crate) enum ChooserCommand<'a> {
72 Toggle(&'a ChooserItem),
75 ShowAll,
77 Reset,
79 Move {
83 item: &'a ChooserItem,
84 zone: ColumnZone,
85 before: Option<&'a ColumnId>,
86 dry_run: bool,
87 },
88 Step { item: &'a ChooserItem, step: isize },
90}
91
92#[derive(Clone, Debug)]
94enum ChooserRow {
95 Column {
96 info: ColumnInfo,
97 depth: usize,
99 locked: bool,
101 grip: bool,
103 },
104 Band {
105 id: &'static str,
106 title: SharedString,
107 depth: usize,
108 shown: usize,
109 count: usize,
110 grip: bool,
111 },
112}
113
114impl ChooserRow {
115 fn item(&self) -> ChooserItem {
116 match self {
117 Self::Column { info, .. } => ChooserItem::Column(info.id().clone()),
118 Self::Band { id, .. } => ChooserItem::Band(id),
119 }
120 }
121
122 fn title(&self) -> &SharedString {
123 match self {
124 Self::Column { info, .. } => info.title(),
125 Self::Band { title, .. } => title,
126 }
127 }
128}
129
130#[derive(Clone, Debug)]
133struct ChooserSection {
134 zone: Option<ColumnZone>,
135 rows: Vec<ChooserRow>,
136 first: Option<ColumnId>,
138}
139
140fn chooser_sections(columns: &[ColumnInfo], query: &str) -> Vec<ChooserSection> {
143 let columns: Vec<ColumnInfo> = columns
145 .iter()
146 .filter(|column| !column.is_internal())
147 .cloned()
148 .collect();
149 let columns = columns.as_slice();
150 let visible = columns.iter().filter(|column| column.is_visible()).count();
151 let column_row = |info: &ColumnInfo, depth: usize, grip: bool| ChooserRow::Column {
152 locked: !info.is_hideable() || (info.is_visible() && visible <= 1),
153 grip: grip && info.is_movable(),
154 info: info.clone(),
155 depth,
156 };
157 if !query.is_empty() {
158 let rows = columns
159 .iter()
160 .filter(|column| column.title().to_lowercase().contains(query))
161 .map(|column| column_row(column, 0, false))
162 .collect();
163 return vec![ChooserSection {
164 zone: None,
165 rows,
166 first: None,
167 }];
168 }
169 let sectioned = columns
170 .iter()
171 .any(|column| column.zone() != ColumnZone::Scrolling);
172 let mut sections = Vec::new();
173 for zone in ColumnZone::ALL {
174 let zone_columns: Vec<&ColumnInfo> = columns
175 .iter()
176 .filter(|column| column.zone() == zone)
177 .collect();
178 if zone_columns.is_empty() {
179 continue;
180 }
181 let mut rows = Vec::with_capacity(zone_columns.len());
182 let mut open: Vec<&'static str> = Vec::new();
183 for (position, column) in zone_columns.iter().enumerate() {
184 let chain: Vec<&'static str> = column.bands().iter().map(|band| band.id()).collect();
185 let common = open
186 .iter()
187 .zip(&chain)
188 .take_while(|(open, band)| open == band)
189 .count();
190 open.truncate(common);
191 for (level, band) in column.bands().iter().enumerate().skip(common) {
192 let members: Vec<&&ColumnInfo> = zone_columns[position..]
195 .iter()
196 .take_while(|member| {
197 member
198 .bands()
199 .get(level)
200 .is_some_and(|each| each.id() == band.id())
201 })
202 .collect();
203 rows.push(ChooserRow::Band {
204 id: band.id(),
205 title: band.title().clone(),
206 depth: level,
207 shown: members.iter().filter(|member| member.is_visible()).count(),
208 count: members.len(),
209 grip: members.iter().all(|member| member.is_movable()),
210 });
211 open.push(band.id());
212 }
213 rows.push(column_row(column, chain.len(), true));
214 }
215 sections.push(ChooserSection {
216 zone: Some(zone),
217 first: zone_columns.first().map(|column| column.id().clone()),
218 rows,
219 });
220 }
221 if !sectioned {
222 for section in &mut sections {
223 section.zone = None;
224 }
225 }
226 sections
227}
228
229fn zone_label(zone: ColumnZone) -> SharedString {
231 let key = match zone {
232 ColumnZone::FrozenLeft => "live_grid.chooser.frozen_left",
233 ColumnZone::Scrolling => "live_grid.chooser.scrolling",
234 ColumnZone::FrozenRight => "live_grid.chooser.frozen_right",
235 };
236 SharedString::from(t!(key).into_owned())
237}
238
239fn zone_id(zone: ColumnZone) -> &'static str {
241 match zone {
242 ColumnZone::FrozenLeft => "frozen-left",
243 ColumnZone::Scrolling => "scrolling",
244 ColumnZone::FrozenRight => "frozen-right",
245 }
246}
247
248#[derive(Clone)]
250struct ChooserDrag {
251 item: ChooserItem,
252 title: SharedString,
253 text: GhostTextStyle,
254}
255
256impl Render for ChooserDrag {
257 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
258 let theme = cx.theme();
259 self.text.root().child(
260 div()
261 .px_2()
262 .py_1()
263 .popover_style(cx)
264 .text_color(theme.popover_foreground)
265 .opacity(0.9)
266 .child(self.title.clone()),
267 )
268 }
269}
270
271pub(crate) struct ChooserDelegate {
273 grid: WeakLiveGridHandle,
274 sections: Vec<ChooserSection>,
275 query: String,
276 selected: Option<IndexPath>,
277}
278
279impl ChooserDelegate {
280 fn row(&self, ix: IndexPath) -> Option<&ChooserRow> {
281 self.sections.get(ix.section)?.rows.get(ix.row)
282 }
283
284 fn position(&self, item: &ChooserItem) -> Option<IndexPath> {
286 self.sections
287 .iter()
288 .enumerate()
289 .find_map(|(section, rows)| {
290 rows.rows
291 .iter()
292 .position(|row| row.item() == *item)
293 .map(|row| IndexPath::default().section(section).row(row))
294 })
295 }
296
297 fn selected_item(&self) -> Option<ChooserItem> {
299 self.row(self.selected?).map(ChooserRow::item)
300 }
301
302 fn drop_place(&self, ix: IndexPath) -> Option<(ColumnZone, Option<ColumnId>)> {
305 let section = self.sections.get(ix.section)?;
306 let zone = section.zone.unwrap_or(ColumnZone::Scrolling);
307 let before = section.rows[ix.row..].iter().find_map(|row| match row {
308 ChooserRow::Column { info, .. } => Some(info.id().clone()),
309 ChooserRow::Band { .. } => None,
310 });
311 Some((zone, before))
312 }
313
314 fn render_row(
316 &self,
317 ix: IndexPath,
318 row: &ChooserRow,
319 ghost_text: GhostTextStyle,
320 cx: &mut Context<ListState<Self>>,
321 ) -> ListItem {
322 let theme = cx.theme();
323 let (id, depth, grip, checked, disabled, icon, suffix) = match row {
324 ChooserRow::Column {
325 info,
326 depth,
327 locked,
328 grip,
329 } => (
330 ElementId::NamedChild(
331 Arc::new(ElementId::Name("chooser-column".into())),
332 SharedString::from(info.id().as_ref().to_owned()),
333 ),
334 *depth,
335 *grip,
336 info.is_visible(),
337 *locked,
338 info.icon().cloned(),
339 None,
340 ),
341 ChooserRow::Band {
342 id,
343 depth,
344 shown,
345 count,
346 grip,
347 ..
348 } => (
349 ElementId::NamedChild(
350 Arc::new(ElementId::Name("chooser-band".into())),
351 SharedString::new_static(id),
352 ),
353 *depth,
354 *grip,
355 shown == count,
356 false,
357 None,
358 (shown != count).then(|| {
359 SharedString::from(
360 t!("live_grid.chooser.band_shown", shown = shown, count = count)
361 .into_owned(),
362 )
363 }),
364 ),
365 };
366 let title = row.title().clone();
367 let drag = ChooserDrag {
368 item: row.item(),
369 title: title.clone(),
370 text: ghost_text,
371 };
372 let place = self.drop_place(ix);
373 let grid = self.grid.clone();
374 let can_drop_grid = self.grid.clone();
375 let can_drop_place = place.clone();
376 ListItem::new(id)
377 .py_0p5()
378 .px_2()
379 .child(
380 div()
381 .flex()
382 .items_center()
383 .gap_1p5()
384 .pl(BAND_INDENT * depth as f32)
385 .min_w(px(0.))
386 .child(
387 div()
388 .id("chooser-grip")
389 .test_support()
390 .flex_none()
391 .size_4()
392 .when(grip, |this| {
393 this.role(Role::Button)
394 .aria_label(SharedString::from(
395 t!("live_grid.chooser.move", title = title.as_ref())
396 .into_owned(),
397 ))
398 .cursor_grab()
399 .child(
400 Icon::new(IconName::Menu)
401 .size_4()
402 .text_color(theme.muted_foreground),
403 )
404 .on_drag(drag, |drag, _, _, cx| cx.new(|_| drag.clone()))
405 .on_click(|_, _, cx| cx.stop_propagation())
408 }),
409 )
410 .child(
411 Checkbox::new("chooser-check")
412 .checked(checked)
413 .disabled(disabled)
414 .tab_stop(false)
415 .accessibility_label(title.clone())
416 .small(),
417 )
418 .children(icon.map(|icon| {
419 Icon::empty()
420 .path(icon)
421 .size_4()
422 .text_color(theme.muted_foreground)
423 }))
424 .child(div().flex_1().min_w(px(0.)).truncate().child(title)),
425 )
426 .children(suffix.map(|suffix| {
427 div()
428 .flex_none()
429 .text_xs()
430 .text_color(theme.muted_foreground)
431 .child(suffix)
432 }))
433 .when_some(place, |this, (zone, before)| {
434 let drag_border = theme.drag_border;
435 this.drag_over::<ChooserDrag>(move |style, _, _, _| {
436 style.border_t_2().border_color(drag_border)
437 })
438 .can_drop(move |drag, _, cx| {
439 let (Some(drag), Some(grid), Some((zone, before))) = (
440 drag.downcast_ref::<ChooserDrag>(),
441 can_drop_grid.upgrade(),
442 can_drop_place.as_ref(),
443 ) else {
444 return false;
445 };
446 grid.chooser_command_in_app(
447 ChooserCommand::Move {
448 item: &drag.item,
449 zone: *zone,
450 before: before.as_ref(),
451 dry_run: true,
452 },
453 cx,
454 )
455 })
456 .on_drop(move |drag: &ChooserDrag, window, cx| {
457 if let Some(grid) = grid.upgrade() {
458 grid.chooser_command(
459 ChooserCommand::Move {
460 item: &drag.item,
461 zone,
462 before: before.as_ref(),
463 dry_run: false,
464 },
465 window,
466 cx,
467 );
468 }
469 })
470 })
471 }
472}
473
474impl ListDelegate for ChooserDelegate {
475 type Item = ListItem;
476
477 fn sections_count(&self, _: &App) -> usize {
478 self.sections.len().max(1)
479 }
480
481 fn items_count(&self, section: usize, _: &App) -> usize {
482 self.sections
483 .get(section)
484 .map_or(0, |section| section.rows.len())
485 }
486
487 fn render_item(
488 &mut self,
489 ix: IndexPath,
490 window: &mut Window,
491 cx: &mut Context<ListState<Self>>,
492 ) -> Option<ListItem> {
493 let row = self.row(ix)?.clone();
494 Some(self.render_row(ix, &row, GhostTextStyle::capture(window), cx))
495 }
496
497 fn render_section_header(
498 &mut self,
499 section: usize,
500 _: &mut Window,
501 cx: &mut Context<ListState<Self>>,
502 ) -> Option<impl IntoElement> {
503 let section = self.sections.get(section)?;
504 let zone = section.zone?;
505 let theme = cx.theme();
506 let first = section.first.clone();
507 let grid = self.grid.clone();
508 let drag_border = theme.drag_border;
509 Some(
510 div()
511 .id(ElementId::NamedChild(
512 Arc::new(ElementId::Name("chooser-zone".into())),
513 SharedString::new_static(zone_id(zone)),
514 ))
515 .test_support()
516 .px_2()
517 .pt_2()
518 .pb_1()
519 .text_xs()
520 .text_color(theme.muted_foreground)
521 .child(zone_label(zone))
522 .drag_over::<ChooserDrag>(move |style, _, _, _| {
524 style.border_b_2().border_color(drag_border)
525 })
526 .on_drop(move |drag: &ChooserDrag, window, cx| {
527 if let Some(grid) = grid.upgrade() {
528 grid.chooser_command(
529 ChooserCommand::Move {
530 item: &drag.item,
531 zone,
532 before: first.as_ref(),
533 dry_run: false,
534 },
535 window,
536 cx,
537 );
538 }
539 }),
540 )
541 }
542
543 fn render_empty(
544 &mut self,
545 _: &mut Window,
546 cx: &mut Context<ListState<Self>>,
547 ) -> impl IntoElement {
548 div()
549 .id("chooser-no-match")
550 .test_support()
551 .p_3()
552 .text_sm()
553 .text_color(cx.theme().muted_foreground)
554 .child(SharedString::from(
555 t!("live_grid.chooser.no_match").into_owned(),
556 ))
557 }
558
559 fn set_selected_index(
560 &mut self,
561 ix: Option<IndexPath>,
562 _: &mut Window,
563 _: &mut Context<ListState<Self>>,
564 ) {
565 self.selected = ix;
566 }
567
568 fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<ListState<Self>>) {
569 if let (Some(item), Some(grid)) = (self.selected_item(), self.grid.upgrade()) {
570 grid.chooser_command(ChooserCommand::Toggle(&item), window, cx);
571 }
572 }
573}
574
575pub struct ColumnChooserPanel {
593 grid: WeakLiveGridHandle,
594 list: Entity<ListState<ChooserDelegate>>,
595 search: Entity<InputState>,
596 search_threshold: usize,
597 column_count: usize,
599 all_shown: bool,
601 stale: bool,
603 _subscriptions: Vec<Subscription>,
604}
605
606impl ColumnChooserPanel {
607 pub fn new(grid: &LiveGridHandle, window: &mut Window, cx: &mut Context<Self>) -> Self {
609 let columns = grid.columns(cx);
610 let delegate = ChooserDelegate {
611 grid: grid.downgrade(),
612 sections: chooser_sections(&columns, ""),
613 query: String::new(),
614 selected: None,
615 };
616 let list = cx.new(|cx| ListState::new(delegate, window, cx));
617 let search = cx.new(|cx| {
618 InputState::new(window, cx).placeholder(SharedString::from(
619 t!("live_grid.chooser.search").into_owned(),
620 ))
621 });
622 let this = cx.weak_entity();
623 let layout = grid.subscribe(cx, move |event, cx| {
624 if matches!(event, GridHandleEvent::LayoutChange { .. }) {
625 let _ = this.update(cx, |panel: &mut Self, cx| {
626 panel.stale = true;
627 cx.notify();
628 });
629 }
630 });
631 let search_changes = cx.subscribe_in(&search, window, Self::on_search_event);
632 Self {
633 grid: grid.downgrade(),
634 list,
635 search,
636 search_threshold: DEFAULT_SEARCH_THRESHOLD,
637 all_shown: columns.iter().all(ColumnInfo::is_visible),
638 column_count: columns.len(),
639 stale: false,
640 _subscriptions: vec![layout, search_changes],
641 }
642 }
643
644 pub fn with_search_threshold(mut self, columns: usize) -> Self {
647 self.search_threshold = columns;
648 self
649 }
650
651 fn is_searchable(&self) -> bool {
653 self.column_count >= self.search_threshold
654 }
655
656 fn on_search_event(
657 &mut self,
658 search: &Entity<InputState>,
659 event: &InputEvent,
660 window: &mut Window,
661 cx: &mut Context<Self>,
662 ) {
663 if matches!(event, InputEvent::Change) {
664 let query = search.read(cx).value().trim().to_lowercase();
665 let changed = self.list.read(cx).delegate().query != query;
666 if changed {
667 self.list
668 .update(cx, |list, _| list.delegate_mut().query = query);
669 self.refresh(window, cx);
670 }
671 }
672 }
673
674 fn refresh(&mut self, window: &mut Window, cx: &mut Context<Self>) {
677 self.stale = false;
678 let Some(grid) = self.grid.upgrade() else {
679 return;
680 };
681 let columns = grid.columns(cx);
682 self.column_count = columns.len();
683 self.all_shown = columns.iter().all(ColumnInfo::is_visible);
684 self.list.update(cx, |list, cx| {
685 let selected = list.delegate().selected_item();
686 let query = list.delegate().query.clone();
687 list.delegate_mut().sections = chooser_sections(&columns, &query);
688 let ix = selected.and_then(|item| list.delegate().position(&item));
689 list.set_selected_index(ix, window, cx);
690 cx.notify();
691 });
692 cx.notify();
693 }
694
695 fn step(&mut self, step: isize, window: &mut Window, cx: &mut Context<Self>) {
698 let (Some(item), Some(grid)) = (
699 self.list.read(cx).delegate().selected_item(),
700 self.grid.upgrade(),
701 ) else {
702 return;
703 };
704 grid.chooser_command(ChooserCommand::Step { item: &item, step }, window, cx);
705 }
706
707 fn dismiss(&mut self, cx: &mut Context<Self>) {
708 cx.emit(DismissEvent);
709 }
710}
711
712impl EventEmitter<DismissEvent> for ColumnChooserPanel {}
713
714impl Focusable for ColumnChooserPanel {
715 fn focus_handle(&self, cx: &App) -> FocusHandle {
717 if self.is_searchable() {
718 self.search.focus_handle(cx)
719 } else {
720 self.list.focus_handle(cx)
721 }
722 }
723}
724
725impl Render for ColumnChooserPanel {
726 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
727 if self.grid.upgrade().is_none() {
728 cx.defer_in(window, |panel, _, cx| panel.dismiss(cx));
730 } else if self.stale {
731 self.refresh(window, cx);
732 }
733 let theme = cx.theme();
734 let searchable = self.is_searchable();
735 let grid = self.grid.clone();
736 let reset_grid = self.grid.clone();
737 div()
738 .id("live-grid-column-chooser")
739 .test_support()
740 .role(Role::Dialog)
741 .aria_label(SharedString::from(
742 t!("live_grid.chooser.label").into_owned(),
743 ))
744 .flex()
745 .flex_col()
746 .w(CHOOSER_WIDTH)
747 .popover_style(cx)
748 .text_sm()
749 .overflow_hidden()
750 .on_action(cx.listener(|panel, _: &Cancel, _, cx| panel.dismiss(cx)))
751 .on_action(cx.listener(|panel, _: &Escape, _, cx| panel.dismiss(cx)))
752 .when(searchable, |this| {
753 this.child(
754 div().p_2().border_b_1().border_color(theme.border).child(
755 Input::new(&self.search)
756 .small()
757 .prefix(Icon::new(IconName::Search).text_color(theme.muted_foreground))
758 .cleanable(true),
759 ),
760 )
761 })
762 .child(
763 div()
764 .key_context(COLUMN_CHOOSER_KEY_CONTEXT)
765 .on_action(cx.listener(|panel, _: &MoveColumnLeft, window, cx| {
766 panel.step(-1, window, cx)
767 }))
768 .on_action(cx.listener(|panel, _: &MoveColumnRight, window, cx| {
769 panel.step(1, window, cx)
770 }))
771 .py_1()
772 .child(List::new(&self.list).small().max_h(LIST_MAX_HEIGHT)),
773 )
774 .child(
775 div()
776 .flex()
777 .justify_between()
778 .p_1()
779 .border_t_1()
780 .border_color(theme.border)
781 .child(
782 Button::new("chooser-show-all")
783 .ghost()
784 .small()
785 .label(SharedString::from(
786 t!("live_grid.chooser.show_all").into_owned(),
787 ))
788 .disabled(self.all_shown)
789 .on_click(move |_, window, cx| {
790 if let Some(grid) = grid.upgrade() {
791 grid.chooser_command(ChooserCommand::ShowAll, window, cx);
792 }
793 }),
794 )
795 .child(
796 Button::new("chooser-reset")
797 .ghost()
798 .small()
799 .label(SharedString::from(
800 t!("live_grid.menu.reset_columns").into_owned(),
801 ))
802 .on_click(move |_, window, cx| {
803 if let Some(grid) = reset_grid.upgrade() {
804 grid.chooser_command(ChooserCommand::Reset, window, cx);
805 }
806 }),
807 ),
808 )
809 }
810}
811
812pub trait ColumnChooserFactory: 'static {
814 fn open(
820 &self,
821 context: &ColumnChooserContext,
822 window: &mut Window,
823 cx: &mut App,
824 ) -> Option<AnyView>;
825}
826
827pub struct ColumnChooserContext {
829 grid: LiveGridHandle,
830 position: Point<Pixels>,
831}
832
833impl ColumnChooserContext {
834 pub fn grid(&self) -> &LiveGridHandle {
836 &self.grid
837 }
838
839 pub fn position(&self) -> Point<Pixels> {
841 self.position
842 }
843}
844
845#[derive(Clone, Copy, Debug)]
848pub struct PopoverColumnChooser {
849 search_threshold: usize,
850}
851
852impl PopoverColumnChooser {
853 pub const fn new() -> Self {
855 Self {
856 search_threshold: DEFAULT_SEARCH_THRESHOLD,
857 }
858 }
859
860 pub const fn with_search_threshold(mut self, columns: usize) -> Self {
862 self.search_threshold = columns;
863 self
864 }
865
866 pub const fn search_threshold(&self) -> usize {
868 self.search_threshold
869 }
870}
871
872impl Default for PopoverColumnChooser {
873 fn default() -> Self {
874 Self::new()
875 }
876}
877
878impl ColumnChooserFactory for PopoverColumnChooser {
879 fn open(
880 &self,
881 context: &ColumnChooserContext,
882 window: &mut Window,
883 cx: &mut App,
884 ) -> Option<AnyView> {
885 let threshold = self.search_threshold;
886 let panel = cx.new(|cx| {
887 ColumnChooserPanel::new(context.grid(), window, cx).with_search_threshold(threshold)
888 });
889 let focus = panel.focus_handle(cx);
890 window.focus(&focus, cx);
891 Some(panel.into())
892 }
893}
894
895pub(super) struct ChooserSlot {
897 factory: Rc<dyn ColumnChooserFactory>,
898 open: Option<OpenChooser>,
899}
900
901impl Default for ChooserSlot {
902 fn default() -> Self {
903 Self {
904 factory: Rc::new(PopoverColumnChooser::new()),
905 open: None,
906 }
907 }
908}
909
910struct OpenChooser {
912 view: AnyView,
913 position: Point<Pixels>,
914}
915
916impl<D: LiveGridDelegate> LiveGridState<D> {
917 pub fn with_column_chooser(mut self, factory: impl ColumnChooserFactory) -> Self {
919 self.column_chooser.factory = Rc::new(factory);
920 self
921 }
922
923 pub fn show_column_chooser(&mut self, window: &mut Window, cx: &mut Context<Self>) {
927 let body = self.body_bounds;
928 let position = point(
929 (body.origin.x + body.size.width - CHOOSER_WIDTH).max(body.origin.x),
930 body.origin.y,
931 );
932 self.show_column_chooser_at(position, window, cx);
933 }
934
935 pub fn show_column_chooser_at(
938 &mut self,
939 position: Point<Pixels>,
940 window: &mut Window,
941 cx: &mut Context<Self>,
942 ) {
943 if self.menu.take().is_some() {
945 self.reconsider_held(cx);
946 }
947 let factory = self.column_chooser.factory.clone();
948 let grid = self.handle(cx);
949 let this = cx.weak_entity();
950 window.defer(cx, move |window, cx| {
951 let context = ColumnChooserContext { grid, position };
952 let view = factory.open(&context, window, cx);
953 let _ = this.update(cx, |state, cx| {
954 state.column_chooser.open = view.map(|view| OpenChooser { view, position });
955 cx.notify();
956 });
957 });
958 }
959
960 pub fn close_column_chooser(&mut self, window: &mut Window, cx: &mut Context<Self>) {
963 if self.column_chooser.open.take().is_some() {
964 window.focus(&self.focus_handle, cx);
965 cx.notify();
966 }
967 }
968
969 pub fn is_column_chooser_open(&self) -> bool {
971 self.column_chooser.open.is_some()
972 }
973
974 pub(super) fn drop_column_chooser(&mut self) {
976 self.column_chooser.open = None;
977 }
978
979 pub(super) fn on_show_column_chooser(
982 &mut self,
983 _: &crate::actions::ShowColumnChooser,
984 window: &mut Window,
985 cx: &mut Context<Self>,
986 ) {
987 match self.menu_position() {
988 Some(position) => self.show_column_chooser_at(position, window, cx),
989 None => self.show_column_chooser(window, cx),
990 }
991 }
992
993 pub(super) fn render_column_chooser(
997 &self,
998 window: &Window,
999 cx: &mut Context<Self>,
1000 ) -> Option<impl IntoElement + use<D>> {
1001 let open = self.column_chooser.open.as_ref()?;
1002 let viewport = window.viewport_size();
1003 Some(
1004 deferred(
1005 anchored().position(point(px(0.), px(0.))).child(
1006 div()
1007 .id("live-grid-chooser-layer")
1008 .w(viewport.width)
1009 .h(viewport.height)
1010 .occlude()
1011 .on_scroll_wheel(|_, _, cx| cx.stop_propagation())
1012 .on_any_mouse_down(cx.listener(|state, _, window, cx| {
1013 cx.stop_propagation();
1014 state.close_column_chooser(window, cx);
1015 }))
1016 .capture_action(cx.listener(|state, _: &Cancel, window, cx| {
1017 state.close_column_chooser(window, cx);
1018 }))
1019 .capture_action(cx.listener(|state, _: &Escape, window, cx| {
1020 state.close_column_chooser(window, cx);
1021 }))
1022 .child(
1023 anchored()
1024 .position(open.position)
1025 .snap_to_window_with_margin(px(8.))
1026 .child(
1027 div()
1028 .on_any_mouse_down(|_, _, cx| cx.stop_propagation())
1030 .child(open.view.clone()),
1031 ),
1032 ),
1033 ),
1034 )
1035 .with_priority(POPUP_PRIORITY),
1036 )
1037 }
1038
1039 pub(super) fn run_chooser_command(
1042 &mut self,
1043 command: ChooserCommand<'_>,
1044 window: Option<&mut Window>,
1045 cx: &mut Context<Self>,
1046 ) -> bool {
1047 match command {
1048 ChooserCommand::Toggle(item) => self.chooser_toggle(item, cx),
1049 ChooserCommand::ShowAll => {
1050 let all_shown = self.columns.visible_set().len() == self.columns.order().len();
1051 self.show_all_columns_from(LayoutOrigin::User, cx);
1052 !all_shown
1053 }
1054 ChooserCommand::Reset => match window {
1055 Some(window) => {
1056 self.on_reset_layout(&ResetLayout, window, cx);
1057 true
1058 }
1059 None => false,
1060 },
1061 ChooserCommand::Move {
1062 item,
1063 zone,
1064 before,
1065 dry_run,
1066 } => self.chooser_move(item, zone, before, dry_run, cx),
1067 ChooserCommand::Step { item, step } => self.chooser_step(item, step, cx),
1068 }
1069 }
1070
1071 fn chooser_members(&self, item: &ChooserItem) -> Option<(ColumnZone, Vec<ColumnIx>)> {
1073 match item {
1074 ChooserItem::Column(id) => {
1075 let ix = self.column_by_id(id.as_ref())?.ix();
1076 Some((self.columns.zone_of(ix), vec![ix]))
1077 }
1078 ChooserItem::Band(band) => {
1079 let first = self
1080 .columns
1081 .order()
1082 .iter()
1083 .copied()
1084 .find(|ix| self.columns.bands_of(*ix).contains(band))?;
1085 let zone = self.columns.zone_of(first);
1086 Some((zone, self.columns.band_members(zone, band)))
1087 }
1088 }
1089 }
1090
1091 fn chooser_toggle(&mut self, item: &ChooserItem, cx: &mut Context<Self>) -> bool {
1092 let Some((_, members)) = self.chooser_members(item) else {
1093 return false;
1094 };
1095 let show = !members.iter().all(|ix| self.columns.is_visible(*ix));
1098 let hideable: Vec<bool> = members
1099 .iter()
1100 .map(|ix| self.columns.column(*ix).hideable)
1101 .collect();
1102 self.change_layout(LayoutOrigin::User, cx, |layout, _| {
1103 let mut changed = ColumnSet::EMPTY;
1104 for (ix, hideable) in members.iter().zip(&hideable) {
1105 if !show && !hideable {
1106 continue;
1107 }
1108 if layout.set_visible(*ix, show) == Some(true) {
1109 changed.insert(*ix);
1110 }
1111 }
1112 if changed.is_empty() {
1113 LayoutDelta::default()
1114 } else {
1115 LayoutDelta::new(LayoutPart::Visibility, changed.iter())
1116 }
1117 })
1118 }
1119
1120 fn chooser_move(
1121 &mut self,
1122 item: &ChooserItem,
1123 zone: ColumnZone,
1124 before: Option<&ColumnId>,
1125 dry_run: bool,
1126 cx: &mut Context<Self>,
1127 ) -> bool {
1128 let Some((from, members)) = self.chooser_members(item) else {
1129 return false;
1130 };
1131 let before = match before {
1132 Some(id) => match self.column_by_id(id.as_ref()) {
1133 Some(column) => Some(column.ix()),
1134 None => return false,
1135 },
1136 None => None,
1137 };
1138 if members.iter().any(|ix| !self.columns.column(*ix).movable)
1139 || before.is_some_and(|ix| members.contains(&ix))
1140 {
1141 return false;
1142 }
1143 if dry_run {
1144 let mut layout = self.columns.clone();
1145 return chooser_move_in(&mut layout, &*self.column_move, from, members, zone, before)
1146 .is_some();
1147 }
1148 self.change_layout(LayoutOrigin::User, cx, |layout, policy| {
1149 chooser_move_in(layout, policy, from, members, zone, before).unwrap_or_default()
1150 })
1151 }
1152
1153 fn chooser_step(&mut self, item: &ChooserItem, step: isize, cx: &mut Context<Self>) -> bool {
1154 let Some((zone, members)) = self.chooser_members(item) else {
1155 return false;
1156 };
1157 if members.iter().any(|ix| !self.columns.column(*ix).movable) {
1158 return false;
1159 }
1160 let positions: Vec<usize> = members
1161 .iter()
1162 .filter_map(|ix| self.columns.zone_position(*ix).map(|(_, i)| i))
1163 .collect();
1164 let (Some(&first), Some(&last)) = (positions.iter().min(), positions.iter().max()) else {
1165 return false;
1166 };
1167 let boundary = if step < 0 {
1168 match first.checked_sub(1) {
1169 Some(k) => k,
1170 None => return false,
1171 }
1172 } else {
1173 last + 2
1174 };
1175 let super::columns::Boundary::Gap(gap) = self
1176 .columns
1177 .zone_move(zone, members.clone())
1178 .resolve(boundary, &*self.column_move)
1179 else {
1180 return false;
1181 };
1182 self.change_layout(LayoutOrigin::User, cx, |layout, _| {
1183 if layout.move_members(zone, &members, gap) {
1184 LayoutDelta::new(LayoutPart::Order, members.iter().copied()).revealing()
1185 } else {
1186 LayoutDelta::default()
1187 }
1188 })
1189 }
1190}
1191
1192fn chooser_move_in<C: crate::model::GridColumns>(
1197 layout: &mut super::columns::ColumnLayout<C>,
1198 policy: &dyn crate::policy::ColumnMovePolicy,
1199 from: ColumnZone,
1200 members: Vec<ColumnIx>,
1201 zone: ColumnZone,
1202 before: Option<ColumnIx>,
1203) -> Option<LayoutDelta> {
1204 if before.is_some_and(|ix| layout.zone_of(ix) != zone) {
1205 return None;
1206 }
1207 let mut delta = LayoutDelta::default();
1208 let mut members = members;
1209 if from != zone {
1210 let mut moved = Vec::new();
1211 for ix in &members {
1212 if layout.zone_of(*ix) == from {
1213 moved.extend(layout.set_zone(*ix, zone, policy));
1214 }
1215 }
1216 if moved.is_empty() {
1217 return None;
1218 }
1219 delta = LayoutDelta::new(LayoutPart::Zone, moved.iter().copied());
1220 delta.parts = delta.parts.with(LayoutPart::Order);
1221 members = layout
1222 .zone_order(zone)
1223 .iter()
1224 .copied()
1225 .filter(|ix| moved.contains(ix))
1226 .collect();
1227 }
1228 match layout.move_before(zone, members.clone(), before, policy) {
1229 Some(true) => {
1230 delta.union_with(LayoutDelta::new(LayoutPart::Order, members.iter().copied()));
1231 }
1232 Some(false) => {}
1233 None if from == zone => return None,
1234 None => {}
1236 }
1237 (!delta.is_empty()).then(|| delta.revealing())
1238}
1239
1240impl LiveGridHandle {
1241 pub(crate) fn chooser_command(
1243 &self,
1244 command: ChooserCommand<'_>,
1245 window: &mut Window,
1246 cx: &mut App,
1247 ) -> bool {
1248 self.ops().chooser(command, Some(window), cx)
1249 }
1250
1251 pub(crate) fn chooser_command_in_app(&self, command: ChooserCommand<'_>, cx: &mut App) -> bool {
1254 self.ops().chooser(command, None, cx)
1255 }
1256}
1257
1258#[cfg(test)]
1259mod tests {
1260 use super::*;
1261
1262 #[test]
1263 fn test_default_factory() {
1264 let factory = PopoverColumnChooser::default();
1265 assert_eq!(factory.search_threshold(), 10);
1266 assert_eq!(
1267 PopoverColumnChooser::new()
1268 .with_search_threshold(3)
1269 .search_threshold(),
1270 3
1271 );
1272 assert_eq!(ChooserItem::Band("prices"), ChooserItem::Band("prices"));
1273 }
1274}