egui_macview/controls/combo_box/
popover.rs1use super::MacComboBox;
4use crate::theme::MacTheme;
5use egui::{
6 Area, CornerRadius, FontId, Id, Key, Modifiers, Order, Pos2, Rect, Response, ScrollArea, Sense,
7 Stroke, StrokeKind, Ui, Vec2,
8};
9
10#[derive(Debug, Clone, Copy, PartialEq)]
12pub struct ComboBoxPopoverGeometry {
13 pub popover_rect: Rect,
15 pub popover_pos: Pos2,
17 pub item_height: f32,
19 pub popover_height: f32,
21 pub popover_width: f32,
23}
24
25pub(super) fn compute_popover_geometry<T: AsRef<str>>(
26 combo: &MacComboBox<'_, T>,
27 ui: &Ui,
28 header_rect: Rect,
29 item_count: usize,
30) -> ComboBoxPopoverGeometry {
31 let theme_cow = MacTheme::resolve(combo.theme, ui.ctx());
32 let metrics = theme_cow.combo_box();
33 let item_h = metrics.item_height.points();
34 let visible_count = (metrics.visible_items.points() as usize).min(item_count);
35 let pop_h = item_h * (visible_count as f32);
36 let pop_w = header_rect.width();
37
38 let screen_rect = ui
39 .ctx()
40 .input(|i| i.raw.screen_rect)
41 .unwrap_or_else(|| ui.clip_rect());
42
43 let gap = metrics.intercell_spacing_y.points();
44 let pop_y = if header_rect.max.y + gap + pop_h > screen_rect.bottom()
45 && header_rect.min.y - gap - pop_h >= screen_rect.top()
46 {
47 header_rect.min.y - gap - pop_h
48 } else {
49 header_rect.max.y + gap
50 };
51
52 let pop_pos = Pos2::new(header_rect.min.x, pop_y);
53 let pop_rect = Rect::from_min_size(pop_pos, Vec2::new(pop_w, pop_h));
54
55 ComboBoxPopoverGeometry {
56 popover_rect: pop_rect,
57 popover_pos: pop_pos,
58 item_height: item_h,
59 popover_height: pop_h,
60 popover_width: pop_w,
61 }
62}
63
64#[allow(clippy::too_many_arguments)]
65pub(super) fn handle_combo_popover<T: AsRef<str>>(
66 combo: &mut MacComboBox<'_, T>,
67 ui: &mut Ui,
68 theme: &MacTheme,
69 rect: Rect,
70 corner_radius: f32,
71 is_open: &mut bool,
72 stored_sel_idx: Option<usize>,
73 open_id: Id,
74 selected_item_id: Id,
75 pointer_pos: Option<Pos2>,
76 response: &mut Response,
77) {
78 let metrics = theme.combo_box();
79 let mut final_sel_idx = stored_sel_idx.unwrap_or(0);
80
81 if *is_open {
82 let current_text_lower = combo.text.to_lowercase();
83 let is_exact_match = combo
84 .items
85 .iter()
86 .any(|it| it.as_ref().eq_ignore_ascii_case(combo.text.as_str()));
87 let filtered: Vec<(usize, &str)> = combo
88 .items
89 .iter()
90 .enumerate()
91 .map(|(i, item)| (i, item.as_ref()))
92 .filter(|(_, s)| {
93 current_text_lower.is_empty()
94 || is_exact_match
95 || s.to_lowercase().contains(¤t_text_lower)
96 })
97 .collect();
98
99 let matching_idx = filtered
100 .iter()
101 .position(|(_, s)| s.eq_ignore_ascii_case(combo.text.as_str()))
102 .unwrap_or(0);
103 let mut sel_idx = stored_sel_idx
104 .unwrap_or(matching_idx)
105 .min(filtered.len().saturating_sub(1));
106
107 let mut nav_changed = false;
108 if ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::Escape)) {
109 *is_open = false;
110 } else if ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::ArrowDown)) {
111 if !filtered.is_empty() {
112 sel_idx = (sel_idx + 1).min(filtered.len() - 1);
113 nav_changed = true;
114 }
115 } else if ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::ArrowUp)) {
116 sel_idx = sel_idx.saturating_sub(1);
117 nav_changed = true;
118 } else if ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::Enter)) {
119 if let Some((_, selected_str)) = filtered.get(sel_idx) {
120 *combo.text = (*selected_str).to_string();
121 response.mark_changed();
122 *is_open = false;
123 }
124 }
125
126 if *is_open && !filtered.is_empty() {
127 let pop_geom = compute_popover_geometry(combo, ui, rect, filtered.len());
128 let area = Area::new(combo.id_salt.with("popover_area"))
129 .order(Order::Foreground)
130 .fixed_pos(pop_geom.popover_pos);
131
132 area.show(ui.ctx(), |pop_ui| {
133 pop_ui.set_clip_rect(pop_geom.popover_rect);
134 let pop_painter = pop_ui.painter_at(pop_geom.popover_rect);
135 pop_painter.rect(
136 pop_geom.popover_rect,
137 CornerRadius::same(corner_radius as u8),
138 theme.control_background(),
139 Stroke::new(1.0, theme.separator()),
140 StrokeKind::Inside,
141 );
142
143 ScrollArea::vertical()
144 .max_height(pop_geom.popover_height)
145 .show(pop_ui, |scroll_ui| {
146 let body_font = scroll_ui
147 .style()
148 .text_styles
149 .get(&egui::TextStyle::Body)
150 .cloned()
151 .unwrap_or_else(|| FontId::proportional(theme.text_field().regular.font_size.points()));
152 for (idx, (_, item_str)) in filtered.iter().enumerate() {
153 let (row_rect, row_resp) = scroll_ui.allocate_exact_size(
154 Vec2::new(pop_geom.popover_width, pop_geom.item_height),
155 Sense::click(),
156 );
157
158 let is_selected = idx == sel_idx;
159 if is_selected && nav_changed {
160 row_resp.scroll_to_me(Some(egui::Align::Center));
161 }
162
163 let row_hovered = row_resp.hovered();
164 if row_hovered || is_selected {
165 scroll_ui.painter().rect_filled(
166 row_rect,
167 CornerRadius::same(0),
168 theme.selected_control(),
169 );
170 }
171
172 let text_color = if row_hovered || is_selected {
173 theme.on_accent_text()
174 } else {
175 theme.label()
176 };
177
178 let galley = theme.layout_tracked_galley(
179 scroll_ui,
180 item_str,
181 body_font.clone(),
182 text_color,
183 f32::INFINITY,
184 );
185 scroll_ui.painter().galley(
186 Pos2::new(
187 row_rect.min.x + metrics.intercell_spacing_x.points(),
188 row_rect.center().y - galley.size().y * 0.5,
189 ),
190 galley,
191 text_color,
192 );
193
194 if row_resp.clicked() {
195 *combo.text = (*item_str).to_string();
196 response.mark_changed();
197 *is_open = false;
198 }
199 }
200 });
201 });
202
203 if ui.input(|i| i.pointer.primary_clicked()) {
204 if let Some(pos) = pointer_pos {
205 if !rect.contains(pos) && !pop_geom.popover_rect.contains(pos) {
206 *is_open = false;
207 }
208 }
209 }
210 final_sel_idx = sel_idx;
211 }
212 }
213
214 ui.data_mut(|d| {
215 d.insert_temp(open_id, *is_open);
216 if *is_open {
217 d.insert_temp(selected_item_id, final_sel_idx);
218 } else {
219 d.remove_temp::<usize>(selected_item_id);
220 }
221 });
222}