1use egui::{
4 vec2, Align2, Color32, CornerRadius, CursorIcon, Image, ImageSource, Pos2, Rect, Sense, Stroke,
5 StrokeKind, Ui, Vec2,
6};
7
8use crate::{sizing, theme};
9
10#[derive(Clone)]
12pub struct IconButtonStyle {
13 pub size: Vec2,
15 pub icon_size: Vec2,
17 pub corner_radius: u8,
19 pub bg_color: Color32,
21 pub hover_color: Color32,
23 pub selected_color: Color32,
25 pub icon_tint: Option<Color32>,
27 pub selected_icon_tint: Option<Color32>,
29 pub solid_selected: bool,
31}
32
33impl Default for IconButtonStyle {
34 fn default() -> Self {
35 Self {
36 size: vec2(sizing::MEDIUM, sizing::MEDIUM),
37 icon_size: vec2(18.0, 18.0),
38 corner_radius: sizing::CORNER_RADIUS,
39 bg_color: Color32::TRANSPARENT,
40 hover_color: theme::HOVER_BG,
41 selected_color: theme::ACCENT,
42 icon_tint: Some(Color32::from_gray(80)),
43 selected_icon_tint: Some(Color32::WHITE),
44 solid_selected: true,
45 }
46 }
47}
48
49impl IconButtonStyle {
50 pub fn small() -> Self {
52 Self {
53 size: vec2(24.0, 24.0),
54 icon_size: vec2(16.0, 16.0),
55 corner_radius: sizing::CORNER_RADIUS,
56 bg_color: Color32::TRANSPARENT,
57 hover_color: Color32::TRANSPARENT,
58 selected_color: Color32::TRANSPARENT,
59 icon_tint: Some(Color32::from_gray(100)),
60 selected_icon_tint: Some(theme::ACCENT),
61 solid_selected: false,
62 }
63 }
64
65 pub fn tool() -> Self {
67 Self {
68 size: vec2(32.0, 32.0),
69 icon_size: vec2(18.0, 18.0),
70 corner_radius: 6,
71 bg_color: Color32::TRANSPARENT,
72 hover_color: Color32::from_gray(235),
73 selected_color: theme::ACCENT,
74 icon_tint: Some(Color32::from_gray(80)),
75 selected_icon_tint: Some(Color32::WHITE),
76 solid_selected: true,
77 }
78 }
79
80 pub fn large() -> Self {
82 Self {
83 size: vec2(sizing::LARGE, sizing::LARGE),
84 icon_size: vec2(24.0, 24.0),
85 ..Default::default()
86 }
87 }
88}
89
90pub struct IconButton<'a> {
92 icon: ImageSource<'a>,
93 tooltip: &'a str,
94 shortcut: Option<&'a str>,
95 selected: bool,
96 style: IconButtonStyle,
97}
98
99impl<'a> IconButton<'a> {
100 pub fn new(icon: ImageSource<'a>, tooltip: &'a str) -> Self {
102 Self {
103 icon,
104 tooltip,
105 shortcut: None,
106 selected: false,
107 style: IconButtonStyle::default(),
108 }
109 }
110
111 pub fn selected(mut self, selected: bool) -> Self {
113 self.selected = selected;
114 self
115 }
116
117 pub fn style(mut self, style: IconButtonStyle) -> Self {
119 self.style = style;
120 self
121 }
122
123 pub fn small(mut self) -> Self {
125 self.style = IconButtonStyle::small();
126 self
127 }
128
129 pub fn tool(mut self) -> Self {
131 self.style = IconButtonStyle::tool();
132 self
133 }
134
135 pub fn shortcut(mut self, shortcut: &'a str) -> Self {
137 self.shortcut = Some(shortcut);
138 self
139 }
140
141 pub fn show(self, ui: &mut Ui) -> bool {
143 let (rect, response) = ui.allocate_exact_size(self.style.size, Sense::click());
144
145 if ui.is_rect_visible(rect) {
146 let bg_color = if self.selected && self.style.solid_selected {
147 self.style.selected_color
148 } else if response.hovered() {
149 self.style.hover_color
150 } else {
151 self.style.bg_color
152 };
153
154 ui.painter().rect_filled(
156 rect,
157 CornerRadius::same(self.style.corner_radius),
158 bg_color,
159 );
160
161 let icon_tint = if self.selected {
163 self.style.selected_icon_tint
164 } else if response.hovered() {
165 Some(Color32::from_gray(40))
166 } else {
167 self.style.icon_tint
168 };
169
170 let icon_rect = Rect::from_center_size(rect.center(), self.style.icon_size);
172 let mut image = Image::new(self.icon).fit_to_exact_size(self.style.icon_size);
173 if let Some(tint) = icon_tint {
174 image = image.tint(tint);
175 }
176 image.paint_at(ui, icon_rect);
177 }
178
179 let clicked = response.clicked();
180 if let Some(shortcut) = self.shortcut {
182 response.clone().on_hover_ui(|ui| {
183 ui.horizontal(|ui| {
184 ui.label(self.tooltip);
185 ui.label(
186 egui::RichText::new(format!("({})", shortcut))
187 .color(Color32::from_gray(128))
188 .small(),
189 );
190 });
191 });
192 } else {
193 response.clone().on_hover_text(self.tooltip);
194 }
195 response.on_hover_cursor(CursorIcon::PointingHand);
196 clicked
197 }
198}
199
200pub struct ToggleButton<'a> {
203 label: &'a str,
204 selected: bool,
205 min_width: Option<f32>,
206 height: f32,
207 font_size: f32,
208}
209
210impl<'a> ToggleButton<'a> {
211 pub fn new(label: &'a str, selected: bool) -> Self {
213 Self {
214 label,
215 selected,
216 min_width: None,
217 height: 24.0,
218 font_size: 11.0,
219 }
220 }
221
222 pub fn min_width(mut self, width: f32) -> Self {
224 self.min_width = Some(width);
225 self
226 }
227
228 pub fn height(mut self, height: f32) -> Self {
230 self.height = height;
231 self
232 }
233
234 pub fn font_size(mut self, size: f32) -> Self {
236 self.font_size = size;
237 self
238 }
239
240 pub fn show(self, ui: &mut Ui) -> bool {
242 let font_id = egui::FontId::proportional(self.font_size);
244 let galley = ui.painter().layout_no_wrap(
245 self.label.to_string(),
246 font_id.clone(),
247 Color32::PLACEHOLDER, );
249 let text_width = galley.size().x;
250 let width = self.min_width.unwrap_or(text_width + 16.0).max(text_width + 16.0);
251 let size = vec2(width, self.height);
252
253 let (rect, response) = ui.allocate_exact_size(size, Sense::click());
254
255 if ui.is_rect_visible(rect) {
256 let bg_color = if self.selected {
257 theme::ACCENT
258 } else if response.hovered() {
259 Color32::from_gray(235)
260 } else {
261 Color32::from_gray(245)
262 };
263
264 let text_color = if self.selected {
265 Color32::WHITE
266 } else {
267 Color32::from_gray(80)
268 };
269
270 ui.painter()
271 .rect_filled(rect, CornerRadius::same(sizing::CORNER_RADIUS), bg_color);
272
273 ui.painter().text(
275 rect.center(),
276 Align2::CENTER_CENTER,
277 self.label,
278 font_id,
279 text_color,
280 );
281 }
282
283 let clicked = response.clicked();
284 response.on_hover_cursor(CursorIcon::PointingHand);
285 clicked
286 }
287}
288
289pub struct MultiToggleState<'a, T: Clone + PartialEq> {
291 pub value: T,
293 pub icon: ImageSource<'a>,
295 pub tooltip: &'a str,
297}
298
299impl<'a, T: Clone + PartialEq> MultiToggleState<'a, T> {
300 pub fn new(value: T, icon: ImageSource<'a>, tooltip: &'a str) -> Self {
302 Self {
303 value,
304 icon,
305 tooltip,
306 }
307 }
308}
309
310pub struct MultiToggle<'a, T: Clone + PartialEq> {
313 states: &'a [MultiToggleState<'a, T>],
314 current: &'a T,
315 style: IconButtonStyle,
316}
317
318impl<'a, T: Clone + PartialEq> MultiToggle<'a, T> {
319 pub fn new(states: &'a [MultiToggleState<'a, T>], current: &'a T) -> Self {
321 Self {
322 states,
323 current,
324 style: IconButtonStyle::default(),
325 }
326 }
327
328 pub fn style(mut self, style: IconButtonStyle) -> Self {
330 self.style = style;
331 self
332 }
333
334 pub fn small(mut self) -> Self {
336 self.style = IconButtonStyle::small();
337 self
338 }
339
340 pub fn show(self, ui: &mut Ui) -> Option<T> {
342 let current_idx = self
344 .states
345 .iter()
346 .position(|s| &s.value == self.current)
347 .unwrap_or(0);
348
349 let state = &self.states[current_idx];
350 let (rect, response) = ui.allocate_exact_size(self.style.size, Sense::click());
351
352 if ui.is_rect_visible(rect) {
353 let bg_color = if response.hovered() {
354 self.style.hover_color
355 } else {
356 self.style.bg_color
357 };
358
359 ui.painter().rect_filled(
360 rect,
361 CornerRadius::same(self.style.corner_radius),
362 bg_color,
363 );
364
365 let icon_rect = Rect::from_center_size(rect.center(), self.style.icon_size);
367 Image::new(state.icon.clone()).paint_at(ui, icon_rect);
368 }
369
370 let clicked = response.clicked();
371 response.on_hover_text(state.tooltip).on_hover_cursor(CursorIcon::PointingHand);
372
373 if clicked {
374 let next_idx = (current_idx + 1) % self.states.len();
376 Some(self.states[next_idx].value.clone())
377 } else {
378 None
379 }
380 }
381}
382
383pub struct TextButton<'a> {
385 label: &'a str,
386 shortcut: Option<&'a str>,
387}
388
389impl<'a> TextButton<'a> {
390 pub fn new(label: &'a str) -> Self {
392 Self {
393 label,
394 shortcut: None,
395 }
396 }
397
398 pub fn shortcut(mut self, shortcut: &'a str) -> Self {
400 self.shortcut = Some(shortcut);
401 self
402 }
403
404 pub fn show(self, ui: &mut Ui) -> bool {
406 let size = vec2(0.0, 24.0);
407 let (rect, response) = ui.allocate_at_least(size, Sense::click());
408
409 if ui.is_rect_visible(rect) {
410 let bg_color = if response.hovered() {
411 theme::HOVER_BG
412 } else {
413 Color32::TRANSPARENT
414 };
415
416 ui.painter()
417 .rect_filled(rect, CornerRadius::same(sizing::CORNER_RADIUS), bg_color);
418
419 ui.painter().text(
421 Pos2::new(rect.left() + 8.0, rect.center().y),
422 egui::Align2::LEFT_CENTER,
423 self.label,
424 egui::FontId::proportional(12.0),
425 theme::TEXT,
426 );
427
428 if let Some(shortcut) = self.shortcut {
430 ui.painter().text(
431 Pos2::new(rect.right() - 8.0, rect.center().y),
432 egui::Align2::RIGHT_CENTER,
433 shortcut,
434 egui::FontId::proportional(11.0),
435 theme::TEXT_MUTED,
436 );
437 }
438 }
439
440 let clicked = response.clicked();
441 response.on_hover_cursor(CursorIcon::PointingHand);
442 clicked
443 }
444}
445
446pub struct StrokeWidthButton<'a> {
448 width: f32,
449 tooltip: &'a str,
450 selected: bool,
451}
452
453impl<'a> StrokeWidthButton<'a> {
454 pub fn new(width: f32, tooltip: &'a str, selected: bool) -> Self {
456 Self {
457 width,
458 tooltip,
459 selected,
460 }
461 }
462
463 pub fn show(self, ui: &mut Ui) -> bool {
465 let size = vec2(28.0, 20.0);
466 let (rect, response) = ui.allocate_exact_size(size, Sense::click());
467
468 if ui.is_rect_visible(rect) {
469 let bg_color = if self.selected {
470 theme::ACCENT
471 } else if response.hovered() {
472 Color32::from_gray(235)
473 } else {
474 Color32::from_gray(250)
475 };
476
477 let line_color = if self.selected {
478 Color32::WHITE
479 } else {
480 Color32::from_gray(60)
481 };
482
483 ui.painter().rect_filled(rect, CornerRadius::same(sizing::CORNER_RADIUS), bg_color);
485
486 if !self.selected {
488 ui.painter().rect_stroke(
489 rect,
490 CornerRadius::same(sizing::CORNER_RADIUS),
491 Stroke::new(1.0, Color32::from_gray(200)),
492 StrokeKind::Inside,
493 );
494 }
495
496 let line_y = rect.center().y;
498 let line_start = Pos2::new(rect.left() + 6.0, line_y);
499 let line_end = Pos2::new(rect.right() - 6.0, line_y);
500 ui.painter().line_segment(
501 [line_start, line_end],
502 Stroke::new(self.width.min(4.0), line_color),
503 );
504 }
505
506 let clicked = response.clicked();
507 response.on_hover_text(self.tooltip).on_hover_cursor(CursorIcon::PointingHand);
508 clicked
509 }
510}
511
512pub struct FontSizeButton<'a> {
514 label: &'a str,
515 size_px: f32,
516 selected: bool,
517}
518
519impl<'a> FontSizeButton<'a> {
520 pub fn new(label: &'a str, size_px: f32, selected: bool) -> Self {
522 Self {
523 label,
524 size_px,
525 selected,
526 }
527 }
528
529 pub fn show(self, ui: &mut Ui) -> bool {
531 let width = if self.label == "XL" { 36.0 } else { 28.0 };
533 let size = vec2(width, 24.0);
534 let (rect, response) = ui.allocate_exact_size(size, Sense::click());
535
536 if ui.is_rect_visible(rect) {
537 let bg_color = if self.selected {
538 theme::ACCENT
539 } else if response.hovered() {
540 Color32::from_gray(230)
541 } else {
542 Color32::from_gray(245)
543 };
544
545 let text_color = if self.selected {
546 Color32::WHITE
547 } else {
548 Color32::from_gray(60)
549 };
550
551 ui.painter().rect_filled(rect, CornerRadius::same(sizing::CORNER_RADIUS), bg_color);
552
553 let display_size = match self.label {
555 "S" => 10.0,
556 "M" => 12.0,
557 "L" => 14.0,
558 "XL" => 14.0,
559 _ => 12.0,
560 };
561
562 ui.painter().text(
563 rect.center(),
564 Align2::CENTER_CENTER,
565 self.label,
566 egui::FontId::proportional(display_size),
567 text_color,
568 );
569 }
570
571 let clicked = response.clicked();
572 response.on_hover_text(format!("{} px", self.size_px as i32)).on_hover_cursor(CursorIcon::PointingHand);
573 clicked
574 }
575}