Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:42:17.847Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

table_header.rs

326 lines · 11.8 KB · rust
1use gpui::ElementId;
2use ui::{
3    ContextMenu, GradientFade, IconButton, IconName, IconSize, PopoverMenu, Tooltip, prelude::*,
4};
5
6use crate::{
7    CsvPreviewView,
8    settings::FilterSortOrder,
9    table_data_engine::{
10        filtering_by_column::{FilterEntry, FilterEntryState},
11        sorting_by_column::{AppliedSorting, SortDirection},
12    },
13    types::AnyColumn,
14};
15
16impl CsvPreviewView {
17    /// Create header for data, which is orderable with text on the left and sort button on the right
18    pub(crate) fn create_header_element_with_sort_button(
19        &self,
20        header_text: SharedString,
21        cx: &mut Context<'_, CsvPreviewView>,
22        col_idx: AnyColumn,
23    ) -> AnyElement {
24        let has_active_filter = self.engine.has_active_filters(col_idx);
25        let has_active_sort = self
26            .engine
27            .applied_sorting
28            .is_some_and(|o| o.col_idx == col_idx);
29        let always_show_buttons = has_active_filter || has_active_sort;
30        let group_name = SharedString::from(format!("csv-col-header-{}", col_idx.get()));
31
32        let colors = cx.theme().colors();
33        let base_bg = colors.editor_background;
34        let grad_width_hovered = px(100.);
35        let grad_width = if always_show_buttons {
36            grad_width_hovered
37        } else {
38            px(20.)
39        };
40        h_flex()
41            .group(group_name.clone())
42            .relative()
43            .overflow_hidden()
44            .w_full()
45            .items_center()
46            .font_buffer(cx)
47            .text_buffer(cx)
48            .child(
49                div()
50                    .flex_1()
51                    .min_w_0()
52                    .overflow_hidden()
53                    .whitespace_nowrap()
54                    .child(header_text),
55            )
56            .child(
57                GradientFade::new(base_bg, base_bg, base_bg)
58                    .width(grad_width)
59                    .width_hovered(grad_width_hovered)
60                    .right(px(0.))
61                    .gradient_stop(0.8)
62                    .group_name(group_name.clone()),
63            )
64            .child(
65                h_flex()
66                    .absolute()
67                    .right_0()
68                    .top_0()
69                    .h_full()
70                    .items_center()
71                    .gap_1()
72                    .when(!always_show_buttons, |this| {
73                        this.visible_on_hover(group_name)
74                    })
75                    .child(self.create_filter_button(cx, col_idx))
76                    .child(self.create_sort_button(cx, col_idx)),
77            )
78            .into_any_element()
79    }
80
81    fn create_sort_button(
82        &self,
83        cx: &mut Context<'_, CsvPreviewView>,
84        col_idx: AnyColumn,
85    ) -> Button {
86        let sort_btn = Button::new(
87            ElementId::NamedInteger("sort-button".into(), col_idx.get() as u64),
88            match self.engine.applied_sorting {
89                Some(ordering) if ordering.col_idx == col_idx => match ordering.direction {
90                    SortDirection::Asc => "↓",
91                    SortDirection::Desc => "↑",
92                },
93                _ => "↕", // Unsorted/available for sorting
94            },
95        )
96        .size(ButtonSize::Compact)
97        .style(
98            if self
99                .engine
100                .applied_sorting
101                .is_some_and(|o| o.col_idx == col_idx)
102            {
103                ButtonStyle::Filled
104            } else {
105                ButtonStyle::Subtle
106            },
107        )
108        .tooltip(Tooltip::text(match self.engine.applied_sorting {
109            Some(ordering) if ordering.col_idx == col_idx => match ordering.direction {
110                SortDirection::Asc => "Sorted A-Z. Click to sort Z-A",
111                SortDirection::Desc => "Sorted Z-A. Click to disable sorting",
112            },
113            _ => "Not sorted. Click to sort A-Z",
114        }))
115        .on_click(cx.listener(move |this, _event, _window, cx| {
116            let new_sorting = match this.engine.applied_sorting {
117                Some(ordering) if ordering.col_idx == col_idx => {
118                    // Same column clicked - cycle through states
119                    match ordering.direction {
120                        SortDirection::Asc => Some(AppliedSorting {
121                            col_idx,
122                            direction: SortDirection::Desc,
123                        }),
124                        SortDirection::Desc => None, // Clear sorting
125                    }
126                }
127                _ => {
128                    // Different column or no sorting - start with ascending
129                    Some(AppliedSorting {
130                        col_idx,
131                        direction: SortDirection::Asc,
132                    })
133                }
134            };
135
136            this.engine.applied_sorting = new_sorting;
137            this.apply_sort(cx);
138            cx.notify();
139        }));
140        sort_btn
141    }
142
143    fn create_filter_button(
144        &self,
145        cx: &mut Context<'_, CsvPreviewView>,
146        col: AnyColumn,
147    ) -> PopoverMenu<ContextMenu> {
148        let has_active_filters = self.engine.has_active_filters(col);
149
150        PopoverMenu::new(ElementId::NamedInteger(
151            "filter-menu".into(),
152            col.get() as u64,
153        ))
154        .trigger_with_tooltip(
155            IconButton::new(
156                ElementId::NamedInteger("filter-button".into(), col.get() as u64),
157                IconName::Filter,
158            )
159            .icon_size(IconSize::Small)
160            .style(if has_active_filters {
161                ButtonStyle::Filled
162            } else {
163                ButtonStyle::Subtle
164            })
165            .toggle_state(has_active_filters),
166            Tooltip::text(if has_active_filters {
167                "Column has active filters. Click to manage"
168            } else {
169                "No filters applied. Click to add filters"
170            }),
171        )
172        .menu({
173            let view_entity = cx.entity();
174            move |window, cx| {
175                let view = view_entity.read(cx);
176                let column_filters = match view.engine.get_filters_for_column(col) {
177                    Ok(filters) => filters,
178                    Err(err) => {
179                        log::error!("Failed to get filters for column: {err}");
180                        return None;
181                    }
182                };
183                let filter_sort_order = view.settings.filter_sort_order;
184                let filter_menu = Self::create_filter_menu(
185                    window,
186                    cx,
187                    view_entity.clone(),
188                    col,
189                    &column_filters,
190                    has_active_filters,
191                    filter_sort_order,
192                );
193                Some(filter_menu)
194            }
195        })
196    }
197
198    fn create_filter_menu(
199        window: &mut ui::Window,
200        cx: &mut ui::App,
201        view_entity: gpui::Entity<CsvPreviewView>,
202        col: AnyColumn,
203        column_filters: &[(FilterEntry, FilterEntryState)],
204        has_active_filters: bool,
205        sort_order: FilterSortOrder,
206    ) -> gpui::Entity<ContextMenu> {
207        let mut available: Vec<&FilterEntry> = column_filters
208            .iter()
209            .filter_map(|(entry, state)| {
210                matches!(state, FilterEntryState::Available { .. }).then_some(entry)
211            })
212            .collect();
213
214        match sort_order {
215            FilterSortOrder::AlphaThenCount => available.sort_by(|a, b| {
216                a.content
217                    .cmp(&b.content)
218                    .then_with(|| b.occurred_times().cmp(&a.occurred_times()))
219            }),
220            FilterSortOrder::CountThenAlpha => available.sort_by(|a, b| {
221                b.occurred_times()
222                    .cmp(&a.occurred_times())
223                    .then_with(|| a.content.cmp(&b.content))
224            }),
225        }
226
227        let unavailable: Vec<(&FilterEntry, AnyColumn)> = column_filters
228            .iter()
229            .filter_map(|(entry, state)| {
230                if let FilterEntryState::Unavailable { blocked_by } = state {
231                    Some((entry, *blocked_by))
232                } else {
233                    None
234                }
235            })
236            .collect();
237
238        // Pre-build applied-state lookup before moving into the closure
239        let applied_states: Vec<(FilterEntry, bool)> = column_filters
240            .iter()
241            .filter_map(|(entry, state)| {
242                if let FilterEntryState::Available { is_applied } = state {
243                    Some((entry.clone(), *is_applied))
244                } else {
245                    None
246                }
247            })
248            .collect();
249
250        let available_cloned: Vec<FilterEntry> = available.iter().map(|e| (*e).clone()).collect();
251        let unavailable_cloned: Vec<(FilterEntry, AnyColumn)> = unavailable
252            .into_iter()
253            .map(|(e, col)| (e.clone(), col))
254            .collect();
255
256        ContextMenu::build(window, cx, move |menu, _, _| {
257            let mut menu = menu;
258
259            if has_active_filters {
260                menu = menu
261                    .toggleable_entry("Clear all", false, ui::IconPosition::Start, None, {
262                        let view_entity = view_entity.clone();
263                        move |_window, cx| {
264                            view_entity.update(cx, |view, cx| {
265                                view.clear_filters(col, cx);
266                                cx.notify();
267                            });
268                        }
269                    })
270                    .separator();
271            }
272
273            for entry in &available_cloned {
274                let is_applied = applied_states
275                    .iter()
276                    .find(|(e, _)| e.content == entry.content)
277                    .map_or(false, |(_, applied)| *applied);
278
279                let label: SharedString =
280                    format_filter_label(entry.content.as_ref(), entry.occurred_times()).into();
281                let entry_value = entry.content.clone();
282
283                menu = menu.toggleable_entry(&label, is_applied, ui::IconPosition::Start, None, {
284                    let view_entity = view_entity.clone();
285                    move |_window, cx| {
286                        view_entity.update(cx, |view, cx| {
287                            view.toggle_filter(col, entry_value.clone(), cx);
288                            cx.notify();
289                        });
290                    }
291                });
292            }
293
294            if !unavailable_cloned.is_empty() {
295                menu = menu.separator().header("Hidden by other filters");
296                for (entry, _blocked_by) in &unavailable_cloned {
297                    let label: SharedString =
298                        format_filter_label(entry.content.as_ref(), entry.occurred_times()).into();
299                    menu = menu.custom_entry(
300                        {
301                            let label = label.clone();
302                            move |_window, cx| {
303                                div()
304                                    .px_2()
305                                    .text_color(cx.theme().colors().text_muted)
306                                    .child(label.clone())
307                                    .into_any_element()
308                            }
309                        },
310                        |_, _| {},
311                    );
312                }
313            }
314
315            menu
316        })
317    }
318}
319
320fn format_filter_label(content: Option<&SharedString>, count: usize) -> String {
321    match content {
322        Some(s) => format!("{s} ({count})"),
323        None => format!("<null> ({count})"),
324    }
325}
326
Served at tenant.openagents/omega Member data and write actions are omitted.