Skip to repository content326 lines · 11.8 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T03:33:45.701Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
table_header.rs
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