launcher/widgets/
nav_group_button.rs1use eframe::egui;
2
3use crate::{state::Page, widgets::nav_button::nav_button};
4
5pub struct NavGroupItem {
7 pub page: Page,
8 pub icon: &'static str,
9 pub label: &'static str,
10}
11
12fn popup_option(
19 ui: &mut egui::Ui,
20 id: egui::Id,
21 label: impl Into<egui::WidgetText>,
22 is_active: bool,
23) -> egui::Response {
24 let text: egui::WidgetText = label.into();
25
26 let galley = text.into_galley(
27 ui,
28 Some(egui::TextWrapMode::Extend),
29 f32::INFINITY,
30 egui::TextStyle::Button,
31 );
32
33 let padding = egui::vec2(12.0, 7.0);
34 let desired_size = galley.size() + padding * 2.0;
35
36 let (_auto_id, rect) = ui.allocate_space(desired_size);
37 let response = ui.interact(rect, id, egui::Sense::click());
38
39 if ui.is_rect_visible(rect) {
40 let visuals = ui.visuals();
41 let (bg, text_color) = if is_active {
42 (
44 visuals.selection.bg_fill,
45 visuals.widgets.hovered.fg_stroke.color,
46 )
47 } else if response.hovered() {
48 (
49 visuals.widgets.hovered.weak_bg_fill,
50 visuals.widgets.hovered.fg_stroke.color,
51 )
52 } else {
53 (visuals.widgets.inactive.weak_bg_fill, visuals.text_color())
54 };
55 let corner_radius = visuals.widgets.inactive.corner_radius;
56
57 ui.painter().rect_filled(rect, corner_radius, bg);
58
59 let text_pos = egui::Align2::CENTER_CENTER
60 .align_size_within_rect(galley.size(), rect)
61 .min;
62 ui.painter().galley(text_pos, galley, text_color);
63 }
64
65 response
66}
67
68pub fn nav_group_button(
74 ui: &mut egui::Ui,
75 group_icon: &'static str,
76 group_label: &'static str,
77 items: &[NavGroupItem],
78 current_page: &mut Page,
79) -> egui::Response {
80 let active_item = items.iter().find(|item| item.page == *current_page);
81 let main_icon = active_item.map(|i| i.icon).unwrap_or(group_icon);
82 let main_label = active_item.map(|i| i.label).unwrap_or(group_label);
83 let is_selected = active_item.is_some();
84
85 let response = nav_button(ui, main_icon, main_label, is_selected, false);
86
87 let popup_id = ui.id().with(group_label).with("group_popup");
88
89 let was_open = ui.memory(|m| m.data.get_temp::<bool>(popup_id).unwrap_or(false));
91 let main_hovered = response.contains_pointer();
92 let main_focused = response.has_focus();
93
94 if was_open || main_hovered || main_focused {
95 let mut popup_hovered = false;
96
97 let area_response = egui::Area::new(popup_id)
98 .order(egui::Order::Foreground)
99 .fixed_pos(egui::pos2(response.rect.right() - 5.0, response.rect.top()))
100 .constrain(true)
101 .show(ui.ctx(), |ui| {
102 egui::Frame::popup(ui.style()).show(ui, |ui| {
103 ui.horizontal(|ui| {
104 for (idx, item) in items.iter().enumerate() {
105 let chip_id = popup_id.with(idx);
106 let chip_response = popup_option(
107 ui,
108 chip_id,
109 format!("{} {}", item.icon, item.label),
110 *current_page == item.page,
111 );
112
113 popup_hovered |= chip_response.hovered();
114
115 if chip_response.clicked() {
116 *current_page = item.page;
117 }
118 }
119 });
120 });
121 })
122 .response;
123
124 popup_hovered |= area_response.contains_pointer();
126
127 const SCROLL_THRESHOLD: f32 = 20.0;
139 let scroll_accum_id = popup_id.with("scroll_accum");
140
141 if main_hovered || popup_hovered {
142 let scroll_delta = ui.input(|i| i.smooth_scroll_delta.y);
143 if scroll_delta != 0.0 {
144 let new_accum = ui.memory(|m| {
146 m.data.get_temp::<f32>(scroll_accum_id).unwrap_or(0.0) + scroll_delta
147 });
148
149 if new_accum.abs() >= SCROLL_THRESHOLD {
150 let current_idx = items
151 .iter()
152 .position(|item| item.page == *current_page)
153 .unwrap_or(0);
154 let n = items.len();
155 let next_idx = if new_accum > 0.0 {
158 current_idx.saturating_sub(1)
159 } else {
160 (current_idx + 1).min(n - 1)
161 };
162 *current_page = items[next_idx].page;
163 ui.memory_mut(|m| m.data.insert_temp(scroll_accum_id, 0.0f32));
165 } else {
166 ui.memory_mut(|m| m.data.insert_temp(scroll_accum_id, new_accum));
167 }
168
169 ui.input_mut(|i| i.smooth_scroll_delta.y = 0.0);
171 }
172 } else {
173 ui.memory_mut(|m| m.data.remove::<f32>(scroll_accum_id));
176 }
177
178 let stay_open = main_hovered || main_focused || popup_hovered;
180 ui.memory_mut(|m| m.data.insert_temp(popup_id, stay_open));
181 }
182
183 if response.clicked() {
185 let current_idx = items.iter().position(|item| item.page == *current_page);
186 let next_idx = match current_idx {
187 Some(idx) => (idx + 1) % items.len(),
188 None => 0,
189 };
190 *current_page = items[next_idx].page;
191 }
192
193 response
194}