1use fluent_zero::t;
4
5use super::{
6 error::TableError,
7 filter::{Filter, highlight::HighlightFilter, search::SearchBar},
8};
9
10#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
12pub enum HeaderMenuAnchor {
13 Cell,
15 #[default]
17 Cursor,
18}
19
20#[derive(Clone, Debug, Default)]
22pub struct ColumnState {
23 pub response: ColResponse,
24 pub sort_up: Option<bool>,
25}
26
27#[derive(Clone, Debug, Default)]
29pub struct ColResponse {
30 pub to_sort: bool,
31 pub hovered: bool,
32 pub filtering: Filter,
33 pub secondary_clicked: bool,
34}
35
36pub const fn set_menu_style(style: &mut egui::Style) {
38 style.wrap_mode = Some(egui::TextWrapMode::Extend);
39
40 style.spacing.button_padding = egui::vec2(2.0, 0.0);
41 style.visuals.widgets.active.bg_stroke = egui::Stroke::NONE;
42 style.visuals.widgets.hovered.bg_stroke = egui::Stroke::NONE;
43 style.visuals.widgets.inactive.weak_bg_fill = egui::Color32::TRANSPARENT;
44 style.visuals.widgets.inactive.bg_stroke = egui::Stroke::NONE;
45
46 style.visuals.selection.bg_fill = egui::Color32::from_gray(50);
47 style.visuals.selection.stroke.color = egui::Color32::from_rgb(86, 92, 128);
48}
49
50pub fn show_header_cell_contents(
52 ui: &mut egui::Ui,
53 text: &str,
54 sort_up: &Option<bool>,
55 previous_response: &ColResponse,
56 org_colors: &[[u8; 3]],
57 user_colors: &[[u8; 3]],
58 anchor_mode: HeaderMenuAnchor,
59) -> Result<ColResponse, TableError> {
60 let mut response = ColResponse {
61 filtering: previous_response.filtering.clone(),
62 ..Default::default()
63 };
64 let mut halt_error = None;
65 let is_hovered = ui.rect_contains_pointer(ui.max_rect());
66
67 let item_spacing = ui.spacing().item_spacing;
68 let gapless_rect = ui.max_rect().expand2(0.5 * item_spacing);
69
70 let bg_color = ui.visuals().widgets.noninteractive.weak_bg_fill;
72 ui.painter()
73 .rect_filled(gapless_rect, egui::CornerRadius::ZERO, bg_color);
74
75 let unique_id = ui.id();
78
79 let cell_interact = ui.interact(
81 gapless_rect,
82 unique_id.with("header_interact"),
83 egui::Sense::hover(),
84 );
85
86 let popup_id = ui.make_persistent_id(unique_id.with("filter"));
87 let anchor_pos_id = unique_id.with("popup_anchor_pos");
88
89 let mut click_anchor = ui.data_mut(|d| d.get_temp::<egui::Pos2>(anchor_pos_id));
91
92 let is_cell_hovered = ui.rect_contains_pointer(gapless_rect);
94 let right_clicked = is_cell_hovered && ui.input(|i| i.pointer.secondary_clicked());
95 if right_clicked {
96 egui::Popup::toggle_id(ui.ctx(), popup_id);
97 }
98
99 let popup_open = egui::Popup::is_id_open(ui.ctx(), popup_id);
100 let mut ellipsis_clicked = false;
101
102 let has_filter = !response.filtering.is_empty();
104 let is_sorted = sort_up.is_some();
105 let show_ellipsis = has_filter || is_hovered || popup_open;
106 let show_sort = is_sorted || is_hovered;
107
108 let right_padding = 8.0f32;
109 let mut solid_width = 0.0f32;
110 let mut ellipsis_center_x = None;
111
112 let cell_width = gapless_rect.width();
113 let right_x = gapless_rect.max.x;
114
115 if !ui.is_sizing_pass() && cell_width > 20.0 && show_ellipsis {
117 let current_offset = right_padding;
118 ellipsis_center_x = Some(right_x - current_offset - 6.0); solid_width = current_offset + 12.0;
120 }
121
122 ui.horizontal(|ui| {
123 ui.add_space(8.0);
124
125 if ui.is_sizing_pass() {
126 ui.add(
127 egui::Label::new(egui::RichText::new(text).strong())
128 .selectable(false)
129 .wrap_mode(ui.wrap_mode()),
130 );
131 if show_sort {
132 let sort_text = match sort_up {
133 Some(false) => "🔺",
134 Some(true) | None => "🔻",
135 };
136 let font_id = egui::FontId::proportional(11.0);
137 ui.add(
138 egui::Label::new(egui::RichText::new(sort_text).font(font_id))
139 .selectable(false),
140 );
141 }
142 ui.add_space(8.0);
143 } else {
144 let max_container_width = (cell_width - solid_width - 12.0).max(0.0);
147 ui.allocate_ui(
148 egui::vec2(max_container_width, ui.available_height()),
149 |ui| {
150 ui.horizontal(|ui| {
151 ui.spacing_mut().item_spacing.x = 6.0;
152
153 let sort_width = if show_sort { 14.0 } else { 0.0 };
154 let max_text_width = (max_container_width - sort_width).max(0.0);
155
156 ui.allocate_ui(egui::vec2(max_text_width, ui.available_height()), |ui| {
157 ui.add(
158 egui::Label::new(egui::RichText::new(text).strong())
159 .selectable(false)
160 .wrap_mode(egui::TextWrapMode::Truncate),
161 );
162 });
163
164 if show_sort {
165 let sort_text = match sort_up {
166 Some(false) => "🔺",
167 Some(true) | None => "🔻",
168 };
169 let sort_color = if sort_up.is_some() {
170 ui.visuals().widgets.active.text_color()
171 } else {
172 ui.visuals()
173 .widgets
174 .inactive
175 .text_color()
176 .linear_multiply(0.4)
177 };
178
179 let font_id = egui::FontId::proportional(11.0);
180 let sort_response = ui.add(
181 egui::Label::new(
182 egui::RichText::new(sort_text)
183 .font(font_id)
184 .color(sort_color),
185 )
186 .selectable(false),
187 );
188
189 let sort_rect = sort_response.rect;
191 let sort_interact_response = ui.interact(
192 sort_rect,
193 unique_id.with("sort_interact"),
194 egui::Sense::click(),
195 );
196 if sort_interact_response.clicked() {
197 response.to_sort = true;
198 }
199 }
200 });
201 },
202 );
203 }
204 });
205
206 if !ui.is_sizing_pass() && show_ellipsis && cell_width > 20.0 {
207 let center_y = gapless_rect.center().y;
208
209 if let Some(cx) = ellipsis_center_x {
211 let ellipsis_rect =
212 egui::Rect::from_center_size(egui::pos2(cx, center_y), egui::vec2(12.0, 18.0));
213
214 let ellipsis_response = ui.interact(
215 ellipsis_rect,
216 unique_id.with("ellipsis_interact"),
217 egui::Sense::click(),
218 );
219
220 if ellipsis_response.clicked() {
221 ellipsis_clicked = true;
222 }
223
224 let center = ellipsis_rect.center();
226 let dot_radius = 1.3;
227 let spacing = 3.5;
228 let dot_color = if ellipsis_response.hovered() {
229 ui.visuals().widgets.hovered.text_color()
230 } else {
231 ui.visuals()
232 .widgets
233 .inactive
234 .text_color()
235 .linear_multiply(0.7)
236 };
237 for i in -1..=1 {
238 #[allow(clippy::cast_precision_loss)]
239 let dot_center = center + egui::vec2(0.0, (i as f32) * spacing);
240 ui.painter()
241 .circle_filled(dot_center, dot_radius, dot_color);
242 }
243 }
244 }
245
246 if ellipsis_clicked {
248 egui::Popup::toggle_id(ui.ctx(), popup_id);
249 }
250
251 if (right_clicked || ellipsis_clicked)
253 && click_anchor.is_none()
254 && let Some(pointer_pos) = ui.ctx().pointer_latest_pos()
255 {
256 ui.data_mut(|d| d.insert_temp(anchor_pos_id, pointer_pos));
257 click_anchor = Some(pointer_pos);
258 }
259
260 if is_cell_hovered && ui.input(|i| i.pointer.primary_clicked()) && !ellipsis_clicked {
262 response.to_sort = true;
263 }
264
265 let menu_anchor_response = match anchor_mode {
267 HeaderMenuAnchor::Cursor if let Some(pos) = click_anchor => {
268 let anchor_rect = egui::Rect::from_center_size(pos, egui::Vec2::splat(1.0));
269 ui.interact(
270 anchor_rect,
271 unique_id.with("dummy_popup_anchor"),
272 egui::Sense::hover(),
273 )
274 }
275 _ => cell_interact,
276 };
277
278 egui::Popup::menu(&menu_anchor_response)
280 .id(popup_id)
281 .close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside)
282 .show(|ui| {
283 ui.set_width(150.0);
284 set_menu_style(ui.style_mut());
285
286 ui.strong(t!("column-options"));
287 ui.separator();
288
289 SearchBar::new(&t!("filter-text")).ui(ui, &mut response.filtering.search);
290
291 if (!org_colors.is_empty() || !user_colors.is_empty())
292 && HighlightFilter::new(&t!("new-highlight-filter"))
293 .ui(
294 ui,
295 &mut response.filtering.highlight,
296 org_colors,
297 user_colors,
298 )
299 .is_err()
300 {
301 halt_error = Some(TableError::CorruptedState);
302 }
303
304 ui.separator();
305
306 let sort_desc = match sort_up {
307 Some(true) => format!(" {}", t!("current-ascending")),
308 Some(false) => format!(" {}", t!("current-descending")),
309 None => String::new(),
310 };
311 if ui
312 .button(format!("{} {sort_desc}", t!("toggle-sort")))
313 .clicked()
314 {
315 response.to_sort = true;
316 ui.close_kind(egui::UiKind::Menu);
317 }
318 });
319
320 if !popup_open {
322 ui.data_mut(|d| {
323 d.remove::<egui::Pos2>(anchor_pos_id);
324 });
325 }
326
327 if let Some(err) = halt_error {
328 return Err(err);
329 }
330
331 if is_hovered {
332 response.hovered = true;
333 }
334 if ellipsis_clicked {
335 response.secondary_clicked = true;
336 }
337
338 Ok(response)
339}