1use crate::icons::Icon;
6use crate::localization as l10n;
7use crate::theme::ThemeExt;
8use egui::{Color32, Pos2, Rect, Sense, Stroke, Ui, Vec2};
9
10use super::svg_helpers::{rail_icon_color, rail_icon_size, render_svg_at_rect_themed};
11use crate::tokens::DESIGN_TOKENS;
12
13#[derive(Debug, Clone)]
15pub struct PairButtonResponse {
16 pub icon_clicked: bool,
18 pub arrow_clicked: bool,
20 pub icon_hovered: bool,
22 pub arrow_hovered: bool,
24 pub icon_rect: Rect,
26 pub arrow_rect: Rect,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum ArrowDirection {
33 Up,
35 Down,
37 Left,
39 Right,
41}
42
43pub struct PairButton<'a> {
74 pub icon: &'a Icon,
76 pub tooltip: &'a str,
78 pub arrow_tooltip: &'a str,
80 pub is_icon_active: bool,
82 pub is_expanded: bool,
84}
85
86impl<'a> PairButton<'a> {
87 pub fn show(
89 &self,
90 ui: &mut Ui,
91 width: f32,
92 height: f32,
93 icon_size: f32,
94 padding: f32,
95 ) -> PairButtonResponse {
96 let cursor_pos = ui.cursor().min;
97
98 let pair_rect = Rect::from_min_size(cursor_pos, Vec2::new(width, height));
100
101 let arrow_width = padding;
103 let _left_padding = arrow_width;
104
105 let icon_rect = Rect::from_center_size(pair_rect.center(), Vec2::splat(icon_size));
107 let icon_res = ui.allocate_rect(icon_rect, Sense::click());
108 let translated_label = l10n::text(ui.ctx(), self.tooltip);
109 icon_res.widget_info(|| {
110 egui::WidgetInfo::selected(
111 egui::Role::Button,
112 icon_res.enabled(),
113 self.is_icon_active,
114 &translated_label,
115 )
116 });
117 crate::styles::focus::draw_focus_ring(ui, &icon_res, ui.active_theme().radius.sm);
118
119 let arrow_rect = Rect::from_min_size(
121 Pos2::new(pair_rect.max.x - arrow_width, pair_rect.min.y),
122 Vec2::new(arrow_width, height),
123 );
124 let arrow_res = ui.allocate_rect(arrow_rect, Sense::click());
125 let translated_label = l10n::text(ui.ctx(), self.arrow_tooltip);
126 arrow_res.widget_info(|| {
127 egui::WidgetInfo::labeled(egui::Role::Button, arrow_res.enabled(), &translated_label)
128 });
129 ui.ctx()
130 .accesskit_node_builder(arrow_res.id, |node| node.set_expanded(self.is_expanded));
131 crate::styles::focus::draw_focus_ring(ui, &arrow_res, ui.active_theme().radius.sm);
132
133 if ui.is_rect_visible(icon_rect) {
135 let icon_bg = if self.is_icon_active && icon_res.hovered() {
136 super::active_hover_color(ui)
137 } else if self.is_icon_active {
138 ui.visuals().widgets.active.bg_fill
139 } else if icon_res.hovered() {
140 ui.visuals().widgets.hovered.bg_fill
141 } else {
142 Color32::TRANSPARENT };
144 ui.painter()
145 .rect_filled(icon_rect, ui.active_theme().radius.sm, icon_bg);
146 }
147
148 let interactive_width = width - padding - arrow_width;
150 let icon_center_x = pair_rect.min.x + padding + interactive_width / 2.0;
151 let icon_rect_center = Rect::from_center_size(
152 Pos2::new(icon_center_x, pair_rect.center().y),
153 Vec2::splat(icon_size),
154 );
155 if ui.is_rect_visible(icon_rect_center) {
156 render_svg_at_rect_themed(
157 ui,
158 self.icon,
159 icon_rect_center,
160 icon_res.hovered(),
161 self.is_icon_active,
162 );
163 }
164
165 if ui.is_rect_visible(arrow_rect) {
167 let arrow_stroke = Stroke::new(
168 1.2_f32,
169 if arrow_res.hovered() {
170 ui.visuals().widgets.active.fg_stroke.color
171 } else {
172 ui.visuals().widgets.noninteractive.fg_stroke.color
173 },
174 );
175
176 let arrow_center = arrow_rect.center();
177 draw_arrow(
178 ui,
179 arrow_center,
180 if self.is_expanded {
181 ArrowDirection::Up
182 } else {
183 ArrowDirection::Down
184 },
185 arrow_stroke,
186 );
187 }
188
189 let icon_clicked = icon_res.clicked();
191 let arrow_clicked = arrow_res.clicked();
192 let icon_hovered = icon_res.hovered();
193 let arrow_hovered = arrow_res.hovered();
194
195 icon_res.on_hover_text(l10n::text(ui.ctx(), self.tooltip));
197 arrow_res.on_hover_text(l10n::text(ui.ctx(), self.arrow_tooltip));
198
199 PairButtonResponse {
200 icon_clicked,
201 arrow_clicked,
202 icon_hovered,
203 arrow_hovered,
204 icon_rect,
205 arrow_rect,
206 }
207 }
208
209 pub fn show_styled(&self, ui: &mut Ui) -> PairButtonResponse {
217 let tokens = &DESIGN_TOKENS.sizing.toolbar;
218 let btn_width = tokens.button_width;
219 let btn_height = tokens.button_height;
220 let margin_h = tokens.hover_margin_h;
221 let margin_v = tokens.hover_margin_v;
222 let inner_rounding = ui.active_theme().radius.md;
223
224 let cursor_pos = ui.cursor().min;
225
226 let outer_rect = Rect::from_min_size(cursor_pos, Vec2::new(btn_width, btn_height));
228
229 let inner_rect = Rect::from_min_max(
231 Pos2::new(outer_rect.min.x + margin_h, outer_rect.min.y + margin_v),
232 Pos2::new(outer_rect.max.x - margin_h, outer_rect.max.y - margin_v),
233 );
234
235 let arrow_width = margin_h + 2.0;
237 let arrow_rect = Rect::from_min_max(
238 Pos2::new(outer_rect.max.x - arrow_width, outer_rect.min.y),
239 outer_rect.max,
240 );
241
242 let icon_zone_rect = Rect::from_min_max(
244 outer_rect.min,
245 Pos2::new(outer_rect.max.x - arrow_width, outer_rect.max.y),
246 );
247
248 let icon_res = ui.allocate_rect(icon_zone_rect, Sense::click());
250 let translated_label = l10n::text(ui.ctx(), self.tooltip);
251 icon_res.widget_info(|| {
252 egui::WidgetInfo::selected(
253 egui::Role::Button,
254 icon_res.enabled(),
255 self.is_icon_active,
256 &translated_label,
257 )
258 });
259 crate::styles::focus::draw_focus_ring(ui, &icon_res, inner_rounding);
260 let arrow_res = ui.allocate_rect(arrow_rect, Sense::click());
261 let translated_label = l10n::text(ui.ctx(), self.arrow_tooltip);
262 arrow_res.widget_info(|| {
263 egui::WidgetInfo::labeled(egui::Role::Button, arrow_res.enabled(), &translated_label)
264 });
265 ui.ctx()
266 .accesskit_node_builder(arrow_res.id, |node| node.set_expanded(self.is_expanded));
267 crate::styles::focus::draw_focus_ring(ui, &arrow_res, inner_rounding);
268
269 let icon_hovered = icon_res.hovered();
270 let arrow_hovered = arrow_res.hovered();
271 let icon_clicked = icon_res.clicked();
272 let arrow_clicked = arrow_res.clicked();
273
274 let any_hovered = icon_hovered || arrow_hovered;
276
277 let show_bg = any_hovered || self.is_icon_active;
279 if show_bg && ui.is_rect_visible(inner_rect) {
280 let bg = if self.is_icon_active && icon_hovered {
281 super::active_hover_color(ui)
282 } else if self.is_icon_active {
283 ui.visuals().widgets.active.bg_fill
284 } else if any_hovered {
285 ui.visuals().widgets.hovered.bg_fill
286 } else {
287 Color32::TRANSPARENT
288 };
289 ui.painter().rect_filled(inner_rect, inner_rounding, bg);
290 }
291
292 let icon_rect = Rect::from_center_size(inner_rect.center(), rail_icon_size(self.icon));
294 if ui.is_rect_visible(icon_rect) {
295 self.icon
296 .as_image_tinted(icon_rect.size(), rail_icon_color(ui))
297 .paint_at(ui, icon_rect);
298 }
299
300 if ui.is_rect_visible(arrow_rect) {
302 let arrow_stroke = Stroke::new(
303 1.2_f32,
304 if arrow_hovered {
305 ui.visuals().widgets.active.fg_stroke.color
306 } else {
307 ui.visuals().widgets.noninteractive.fg_stroke.color
308 },
309 );
310
311 let arrow_center = Pos2::new(outer_rect.max.x - margin_h / 2.0, outer_rect.center().y);
312 draw_arrow(
313 ui,
314 arrow_center,
315 if self.is_expanded {
316 ArrowDirection::Up
317 } else {
318 ArrowDirection::Down
319 },
320 arrow_stroke,
321 );
322 }
323
324 icon_res.on_hover_text(l10n::text(ui.ctx(), self.tooltip));
326 arrow_res.on_hover_text(l10n::text(ui.ctx(), self.arrow_tooltip));
327
328 PairButtonResponse {
329 icon_clicked,
330 arrow_clicked,
331 icon_hovered,
332 arrow_hovered,
333 icon_rect,
334 arrow_rect,
335 }
336 }
337}
338
339pub fn draw_arrow(ui: &mut Ui, center: Pos2, direction: ArrowDirection, stroke: Stroke) {
341 let arrow_x = center.x;
342 let arrow_y = center.y;
343
344 match direction {
345 ArrowDirection::Down => {
346 ui.painter().line_segment(
348 [
349 Pos2::new(arrow_x - 2.0, arrow_y - 1.0),
350 Pos2::new(arrow_x, arrow_y + 1.0),
351 ],
352 stroke,
353 );
354 ui.painter().line_segment(
355 [
356 Pos2::new(arrow_x, arrow_y + 1.0),
357 Pos2::new(arrow_x + 2.0, arrow_y - 1.0),
358 ],
359 stroke,
360 );
361 }
362 ArrowDirection::Up => {
363 ui.painter().line_segment(
365 [
366 Pos2::new(arrow_x - 2.0, arrow_y + 1.0),
367 Pos2::new(arrow_x, arrow_y - 1.0),
368 ],
369 stroke,
370 );
371 ui.painter().line_segment(
372 [
373 Pos2::new(arrow_x, arrow_y - 1.0),
374 Pos2::new(arrow_x + 2.0, arrow_y + 1.0),
375 ],
376 stroke,
377 );
378 }
379 ArrowDirection::Right => {
380 ui.painter().line_segment(
382 [
383 Pos2::new(arrow_x - 1.0, arrow_y - 2.0),
384 Pos2::new(arrow_x + 1.0, arrow_y),
385 ],
386 stroke,
387 );
388 ui.painter().line_segment(
389 [
390 Pos2::new(arrow_x + 1.0, arrow_y),
391 Pos2::new(arrow_x - 1.0, arrow_y + 2.0),
392 ],
393 stroke,
394 );
395 }
396 ArrowDirection::Left => {
397 ui.painter().line_segment(
399 [
400 Pos2::new(arrow_x + 1.0, arrow_y - 2.0),
401 Pos2::new(arrow_x - 1.0, arrow_y),
402 ],
403 stroke,
404 );
405 ui.painter().line_segment(
406 [
407 Pos2::new(arrow_x - 1.0, arrow_y),
408 Pos2::new(arrow_x + 1.0, arrow_y + 2.0),
409 ],
410 stroke,
411 );
412 }
413 }
414}