1use crate::localization as l10n;
21use egui::{Color32, Rect, Response, RichText, Sense, TextEdit, Ui, Vec2, Widget};
22
23#[cfg(feature = "icons")]
24use crate::icons::Icon;
25#[cfg(feature = "icons")]
26use crate::styles::icons;
27use crate::theme::ThemeExt;
28use crate::tokens::DESIGN_TOKENS;
29
30pub trait UiExt {
32 #[cfg(feature = "icons")]
38 fn small_icon(&mut self, icon: &Icon) -> Response;
39
40 #[cfg(feature = "icons")]
42 fn medium_icon(&mut self, icon: &Icon) -> Response;
43
44 #[cfg(feature = "icons")]
46 fn large_icon(&mut self, icon: &Icon) -> Response;
47
48 #[cfg(feature = "icons")]
50 fn icon_with_size(&mut self, icon: &Icon, size: f32) -> Response;
51
52 fn error_label(&mut self, text: impl Into<String>) -> Response;
58
59 fn warning_label(&mut self, text: impl Into<String>) -> Response;
61
62 fn success_label(&mut self, text: impl Into<String>) -> Response;
64
65 fn info_label(&mut self, text: impl Into<String>) -> Response;
67
68 fn muted_label(&mut self, text: impl Into<String>) -> Response;
70
71 fn subdued_label(&mut self, text: impl Into<String>) -> Response;
73
74 fn hint_label(&mut self, text: impl Into<String>) -> Response;
76
77 fn strong_label(&mut self, text: impl Into<String>) -> Response;
79
80 fn monospace_label(&mut self, text: impl Into<String>) -> Response;
82
83 fn code_label(&mut self, text: impl Into<String>) -> Response;
85
86 #[cfg(feature = "icons")]
88 fn label_with_icon(&mut self, icon: &Icon, text: impl Into<String>) -> Response;
89
90 fn clickable_label(&mut self, text: impl Into<String>) -> Response;
92
93 fn primary_button(&mut self, text: impl Into<String>) -> Response;
99
100 fn secondary_button(&mut self, text: impl Into<String>) -> Response;
102
103 fn danger_button(&mut self, text: impl Into<String>) -> Response;
105
106 #[cfg(feature = "icons")]
108 fn icon_button(&mut self, icon: &Icon) -> Response;
109
110 #[cfg(feature = "icons")]
112 fn icon_button_with_tooltip(&mut self, icon: &Icon, tooltip: impl Into<String>) -> Response;
113
114 fn toggle_button(&mut self, selected: &mut bool, text: impl Into<String>) -> Response;
116
117 #[cfg(feature = "icons")]
123 fn selectable_label_with_icon(
124 &mut self,
125 selected: bool,
126 icon: &Icon,
127 text: impl Into<String>,
128 ) -> Response;
129
130 fn selectable_value_ui<T: PartialEq + Clone>(
132 &mut self,
133 current: &mut T,
134 value: T,
135 text: impl Into<String>,
136 ) -> Response;
137
138 fn radio_value_ui<T: PartialEq + Clone>(
140 &mut self,
141 current: &mut T,
142 value: T,
143 text: impl Into<String>,
144 ) -> Response;
145
146 fn combo_select<T: PartialEq + Clone>(
160 &mut self,
161 id_salt: impl std::hash::Hash + std::fmt::Debug,
162 value: &mut T,
163 options: impl IntoIterator<Item = T>,
164 display: impl Fn(&T) -> String,
165 ) -> bool;
166
167 fn combo_select_width<T: PartialEq + Clone>(
169 &mut self,
170 id_salt: impl std::hash::Hash + std::fmt::Debug,
171 value: &mut T,
172 options: impl IntoIterator<Item = T>,
173 display: impl Fn(&T) -> String,
174 width: f32,
175 ) -> bool;
176
177 fn combo_str_select(
187 &mut self,
188 id_salt: impl std::hash::Hash + std::fmt::Debug,
189 value: &mut String,
190 options: &[&str],
191 ) -> bool;
192
193 fn combo_str_select_width(
195 &mut self,
196 id_salt: impl std::hash::Hash + std::fmt::Debug,
197 value: &mut String,
198 options: &[&str],
199 width: f32,
200 ) -> bool;
201
202 fn panel_content(&mut self, add_contents: impl FnOnce(&mut Ui));
208
209 fn section_header(&mut self, text: impl Into<String>);
211
212 fn section_label(&mut self, text: impl Into<String>) -> Response;
214
215 fn subsection(&mut self, text: impl Into<String>, add_contents: impl FnOnce(&mut Ui));
217
218 fn list(&mut self, add_contents: impl FnOnce(&mut Ui));
220
221 fn right_aligned(&mut self, add_contents: impl FnOnce(&mut Ui));
225
226 fn scroll_vertical(&mut self, add_contents: impl FnOnce(&mut Ui));
230
231 fn separator_with_margin(&mut self, margin: f32);
233
234 fn spaced_separator(&mut self);
236
237 fn themed_separator(&mut self);
239
240 fn toolbar_separator(&mut self);
243
244 fn full_width(&self) -> f32;
246
247 fn space_xs(&mut self);
253
254 fn space_sm(&mut self);
256
257 fn space_md(&mut self);
259
260 fn space_lg(&mut self);
262
263 fn space_xl(&mut self);
265
266 fn space_xxl(&mut self);
268
269 fn small_space(&mut self);
272
273 fn medium_space(&mut self);
275
276 fn large_space(&mut self);
278
279 fn text_edit_singleline_ui(&mut self, text: &mut String) -> Response;
285
286 fn text_edit_multiline_ui(&mut self, text: &mut String) -> Response;
288
289 fn number_edit(&mut self, value: &mut f64) -> Response;
291
292 fn search_input(&mut self, query: &mut String) -> Response;
294
295 fn drag_value_f32(&mut self, value: &mut f32) -> Response;
301
302 fn drag_value_with_range(
304 &mut self,
305 value: &mut f32,
306 range: std::ops::RangeInclusive<f32>,
307 ) -> Response;
308
309 fn drag_angle(&mut self, radians: &mut f32) -> Response;
311
312 fn property_row(&mut self, label: impl Into<String>, add_value: impl FnOnce(&mut Ui));
318
319 fn visible_rect(&self) -> Rect;
325
326 fn is_visible(&self) -> bool;
328
329 fn current_theme(&self) -> egui::Theme;
335
336 fn is_dark_mode(&self) -> bool;
338
339 fn is_light_mode(&self) -> bool;
341
342 fn is_touch_mode(&self) -> bool;
348
349 fn min_touch_target(&self) -> f32;
351
352 fn pnl_color(&self, value: f64) -> Color32;
358}
359
360impl UiExt for Ui {
361 #[cfg(feature = "icons")]
366 fn small_icon(&mut self, icon: &Icon) -> Response {
367 self.icon_with_size(icon, icons::SMALL)
368 }
369
370 #[cfg(feature = "icons")]
371 fn medium_icon(&mut self, icon: &Icon) -> Response {
372 self.icon_with_size(icon, icons::MEDIUM)
373 }
374
375 #[cfg(feature = "icons")]
376 fn large_icon(&mut self, icon: &Icon) -> Response {
377 self.icon_with_size(icon, icons::LARGE)
378 }
379
380 #[cfg(feature = "icons")]
381 fn icon_with_size(&mut self, icon: &Icon, size: f32) -> Response {
382 let tint = self.style().visuals.widgets.noninteractive.fg_stroke.color;
383 let image = icon.as_image_tinted(Vec2::splat(size), tint);
384 self.add(image)
385 }
386
387 fn error_label(&mut self, text: impl Into<String>) -> Response {
392 let color = self.active_theme().ui.error;
393 self.colored_label(color, l10n::text(self.ctx(), Into::<String>::into(text)))
394 }
395
396 fn warning_label(&mut self, text: impl Into<String>) -> Response {
397 let color = self.active_theme().ui.warning;
398 self.colored_label(color, l10n::text(self.ctx(), Into::<String>::into(text)))
399 }
400
401 fn success_label(&mut self, text: impl Into<String>) -> Response {
402 let color = self.active_theme().ui.success;
403 self.colored_label(color, l10n::text(self.ctx(), Into::<String>::into(text)))
404 }
405
406 fn info_label(&mut self, text: impl Into<String>) -> Response {
407 let color = self.active_theme().ui.info;
408 self.colored_label(color, l10n::text(self.ctx(), Into::<String>::into(text)))
409 }
410
411 fn muted_label(&mut self, text: impl Into<String>) -> Response {
412 let color = self.style().visuals.widgets.noninteractive.fg_stroke.color;
413 self.colored_label(color, l10n::text(self.ctx(), Into::<String>::into(text)))
414 }
415
416 fn subdued_label(&mut self, text: impl Into<String>) -> Response {
417 self.muted_label(text)
418 }
419
420 fn hint_label(&mut self, text: impl Into<String>) -> Response {
421 self.label(
422 RichText::new(l10n::text(self.ctx(), Into::<String>::into(text)))
423 .small()
424 .weak(),
425 )
426 }
427
428 fn strong_label(&mut self, text: impl Into<String>) -> Response {
429 self.label(RichText::new(l10n::text(self.ctx(), Into::<String>::into(text))).strong())
430 }
431
432 fn monospace_label(&mut self, text: impl Into<String>) -> Response {
433 self.label(RichText::new(l10n::text(self.ctx(), Into::<String>::into(text))).monospace())
434 }
435
436 fn code_label(&mut self, text: impl Into<String>) -> Response {
437 self.code(text.into())
438 }
439
440 #[cfg(feature = "icons")]
441 fn label_with_icon(&mut self, icon: &Icon, text: impl Into<String>) -> Response {
442 let text = l10n::text(self.ctx(), Into::<String>::into(text));
443 let icon_size = icons::SMALL;
444
445 self.horizontal(|ui| {
446 let tint = ui.style().visuals.widgets.noninteractive.fg_stroke.color;
447 ui.add(icon.as_image_tinted(Vec2::splat(icon_size), tint));
448 ui.label(text);
449 })
450 .response
451 }
452
453 fn clickable_label(&mut self, text: impl Into<String>) -> Response {
454 let text = l10n::text(self.ctx(), Into::<String>::into(text));
455 let color = self.visuals().hyperlink_color;
456 self.add(egui::Label::new(RichText::new(text).color(color)).sense(Sense::click()))
457 }
458
459 fn primary_button(&mut self, text: impl Into<String>) -> Response {
464 self.add(crate::ui_kit::buttons::Button::new(text).primary())
465 }
466
467 fn secondary_button(&mut self, text: impl Into<String>) -> Response {
468 self.add(crate::ui_kit::buttons::Button::new(text).secondary())
469 }
470
471 fn danger_button(&mut self, text: impl Into<String>) -> Response {
472 self.add(crate::ui_kit::buttons::Button::new(text).danger())
473 }
474
475 #[cfg(feature = "icons")]
476 fn icon_button(&mut self, icon: &Icon) -> Response {
477 let size = DESIGN_TOKENS.sizing.button_md;
478 let icon_size = icons::MEDIUM;
479 let (rect, response) = self.allocate_exact_size(Vec2::splat(size), Sense::click());
480 crate::styles::focus::draw_focus_ring(self, &response, self.active_theme().radius.sm);
481
482 if self.is_rect_visible(rect) {
483 let visuals = &self.style().visuals;
484 let bg_color = if response.is_pointer_button_down_on() {
485 visuals.widgets.active.bg_fill
486 } else if response.hovered() {
487 visuals.widgets.hovered.bg_fill
488 } else {
489 Color32::TRANSPARENT
490 };
491
492 self.painter()
493 .rect_filled(rect, self.active_theme().radius.sm, bg_color);
494
495 let icon_rect = Rect::from_center_size(rect.center(), Vec2::splat(icon_size));
496 let icon_color = if response.hovered() {
497 visuals.widgets.hovered.fg_stroke.color
498 } else {
499 visuals.widgets.noninteractive.fg_stroke.color
500 };
501 icon.as_image_tinted(Vec2::splat(icon_size), icon_color)
502 .paint_at(self, icon_rect);
503 }
504
505 response
506 }
507
508 #[cfg(feature = "icons")]
509 fn icon_button_with_tooltip(&mut self, icon: &Icon, tooltip: impl Into<String>) -> Response {
510 let response = self.icon_button(icon);
511 response.on_hover_text(l10n::text(self.ctx(), Into::<String>::into(tooltip)))
512 }
513
514 fn toggle_button(&mut self, selected: &mut bool, text: impl Into<String>) -> Response {
515 let text = l10n::text(self.ctx(), Into::<String>::into(text));
516 let visuals = &self.style().visuals;
517
518 let (bg, fg) = if *selected {
519 (visuals.selection.bg_fill, visuals.selection.stroke.color)
520 } else {
521 (
522 Color32::TRANSPARENT,
523 visuals.widgets.inactive.fg_stroke.color,
524 )
525 };
526
527 let button = egui::Button::new(RichText::new(text).color(fg))
528 .fill(bg)
529 .corner_radius(self.active_theme().radius.sm);
530
531 let response = self.add(button);
532 if response.clicked() {
533 *selected = !*selected;
534 }
535 response
536 }
537
538 #[cfg(feature = "icons")]
543 fn selectable_label_with_icon(
544 &mut self,
545 selected: bool,
546 icon: &Icon,
547 text: impl Into<String>,
548 ) -> Response {
549 let text = l10n::text(self.ctx(), Into::<String>::into(text));
550 let icon_size = icons::SMALL;
551 let total_width = self.available_width();
552
553 let (rect, response) = self.allocate_exact_size(
554 Vec2::new(total_width, DESIGN_TOKENS.sizing.button_md),
555 Sense::click(),
556 );
557 crate::styles::focus::draw_focus_ring(self, &response, self.active_theme().radius.sm);
558
559 if self.is_rect_visible(rect) {
560 let visuals = self.style().visuals.clone();
561
562 let bg_color = if selected {
564 visuals.selection.bg_fill
565 } else if response.hovered() {
566 visuals.widgets.hovered.bg_fill
567 } else {
568 Color32::TRANSPARENT
569 };
570
571 self.painter()
572 .rect_filled(rect, self.active_theme().radius.sm, bg_color);
573
574 let icon_rect = Rect::from_min_size(
576 rect.min
577 + Vec2::new(
578 self.active_theme().spacing.sm,
579 (rect.height() - icon_size) / 2.0,
580 ),
581 Vec2::splat(icon_size),
582 );
583 let icon_color = if selected {
584 visuals.selection.stroke.color
585 } else {
586 visuals.widgets.noninteractive.fg_stroke.color
587 };
588 icon.as_image_tinted(Vec2::splat(icon_size), icon_color)
589 .paint_at(self, icon_rect);
590
591 let text_pos = egui::pos2(
593 icon_rect.right() + self.active_theme().spacing.sm,
594 rect.center().y - self.text_style_height(&egui::TextStyle::Body) / 2.0,
595 );
596 let text_color = if selected {
597 visuals.selection.stroke.color
598 } else {
599 visuals.widgets.noninteractive.fg_stroke.color
600 };
601 self.painter().text(
602 text_pos,
603 egui::Align2::LEFT_TOP,
604 text,
605 egui::TextStyle::Body.resolve(self.style()),
606 text_color,
607 );
608 }
609
610 response
611 }
612
613 fn selectable_value_ui<T: PartialEq + Clone>(
614 &mut self,
615 current: &mut T,
616 value: T,
617 text: impl Into<String>,
618 ) -> Response {
619 let selected = *current == value;
620 let response =
621 self.selectable_label(selected, l10n::text(self.ctx(), Into::<String>::into(text)));
622 if response.clicked() {
623 *current = value;
624 }
625 response
626 }
627
628 fn radio_value_ui<T: PartialEq + Clone>(
629 &mut self,
630 current: &mut T,
631 value: T,
632 text: impl Into<String>,
633 ) -> Response {
634 let selected = *current == value;
635 let response = self.radio(selected, l10n::text(self.ctx(), Into::<String>::into(text)));
636 if response.clicked() {
637 *current = value;
638 }
639 response
640 }
641
642 fn combo_select<T: PartialEq + Clone>(
643 &mut self,
644 id_salt: impl std::hash::Hash + std::fmt::Debug,
645 value: &mut T,
646 options: impl IntoIterator<Item = T>,
647 display: impl Fn(&T) -> String,
648 ) -> bool {
649 let selected_text = l10n::text(self.ctx(), display(value));
650 let mut changed = false;
651 egui::ComboBox::from_id_salt(id_salt)
652 .icon(crate::ui_kit::buttons::combo_chevron)
653 .selected_text(selected_text)
654 .show_ui(self, |ui| {
655 for option in options {
656 let label = l10n::text(ui.ctx(), display(&option));
657 if ui.selectable_value(value, option, label).changed() {
658 changed = true;
659 }
660 }
661 });
662 changed
663 }
664
665 fn combo_select_width<T: PartialEq + Clone>(
666 &mut self,
667 id_salt: impl std::hash::Hash + std::fmt::Debug,
668 value: &mut T,
669 options: impl IntoIterator<Item = T>,
670 display: impl Fn(&T) -> String,
671 width: f32,
672 ) -> bool {
673 let selected_text = l10n::text(self.ctx(), display(value));
674 let mut changed = false;
675 egui::ComboBox::from_id_salt(id_salt)
676 .icon(crate::ui_kit::buttons::combo_chevron)
677 .selected_text(selected_text)
678 .width(width)
679 .show_ui(self, |ui| {
680 for option in options {
681 let label = l10n::text(ui.ctx(), display(&option));
682 if ui.selectable_value(value, option, label).changed() {
683 changed = true;
684 }
685 }
686 });
687 changed
688 }
689
690 fn combo_str_select(
691 &mut self,
692 id_salt: impl std::hash::Hash + std::fmt::Debug,
693 value: &mut String,
694 options: &[&str],
695 ) -> bool {
696 let mut changed = false;
697 egui::ComboBox::from_id_salt(id_salt)
698 .icon(crate::ui_kit::buttons::combo_chevron)
699 .selected_text(l10n::text(self.ctx(), value.as_str()))
700 .show_ui(self, |ui| {
701 for &option in options {
702 if ui
703 .selectable_value(value, option.to_string(), l10n::text(ui.ctx(), option))
704 .changed()
705 {
706 changed = true;
707 }
708 }
709 });
710 changed
711 }
712
713 fn combo_str_select_width(
714 &mut self,
715 id_salt: impl std::hash::Hash + std::fmt::Debug,
716 value: &mut String,
717 options: &[&str],
718 width: f32,
719 ) -> bool {
720 let mut changed = false;
721 egui::ComboBox::from_id_salt(id_salt)
722 .icon(crate::ui_kit::buttons::combo_chevron)
723 .selected_text(l10n::text(self.ctx(), value.as_str()))
724 .width(width)
725 .show_ui(self, |ui| {
726 for &option in options {
727 if ui
728 .selectable_value(value, option.to_string(), l10n::text(ui.ctx(), option))
729 .changed()
730 {
731 changed = true;
732 }
733 }
734 });
735 changed
736 }
737
738 fn panel_content(&mut self, add_contents: impl FnOnce(&mut Ui)) {
743 egui::Frame::new()
744 .inner_margin(egui::Margin::same(self.active_theme().spacing.md as i8))
745 .show(self, |ui| {
746 add_contents(ui);
747 });
748 }
749
750 fn section_header(&mut self, text: impl Into<String>) {
751 let text = l10n::text(self.ctx(), Into::<String>::into(text));
752 self.add_space(self.active_theme().spacing.lg);
753 self.label(
754 RichText::new(text)
755 .strong()
756 .size(self.active_theme().text.body),
757 );
758 self.add_space(self.active_theme().spacing.sm);
759 }
760
761 fn section_label(&mut self, text: impl Into<String>) -> Response {
762 let color = self.visuals().widgets.noninteractive.fg_stroke.color;
763 self.label(
764 RichText::new(l10n::text(self.ctx(), Into::<String>::into(text)))
765 .size(self.active_theme().text.marker)
766 .strong()
767 .color(color),
768 )
769 }
770
771 fn subsection(&mut self, text: impl Into<String>, add_contents: impl FnOnce(&mut Ui)) {
772 let text = text.into();
773 egui::CollapsingHeader::new(l10n::text(self.ctx(), &text))
774 .id_salt(&text)
775 .default_open(true)
776 .show(self, |ui| {
777 add_contents(ui);
778 });
779 }
780
781 fn right_aligned(&mut self, add_contents: impl FnOnce(&mut Ui)) {
782 self.with_layout(
783 egui::Layout::right_to_left(egui::Align::Center),
784 add_contents,
785 );
786 }
787
788 fn scroll_vertical(&mut self, add_contents: impl FnOnce(&mut Ui)) {
789 egui::ScrollArea::vertical()
790 .auto_shrink([false, false])
791 .show(self, |ui| {
792 add_contents(ui);
793 });
794 }
795
796 fn list(&mut self, add_contents: impl FnOnce(&mut Ui)) {
797 self.vertical(|ui| {
798 ui.spacing_mut().item_spacing.y = ui.active_theme().spacing.xs;
799 add_contents(ui);
800 });
801 }
802
803 fn separator_with_margin(&mut self, margin: f32) {
804 self.add_space(margin);
805 self.separator();
806 self.add_space(margin);
807 }
808
809 fn spaced_separator(&mut self) {
810 self.add_space(self.active_theme().spacing.sm);
811 self.separator();
812 self.add_space(self.active_theme().spacing.sm);
813 }
814
815 fn themed_separator(&mut self) {
816 let stroke_color = self.style().visuals.widgets.noninteractive.bg_stroke.color;
817 let rect = self.available_rect_before_wrap();
818 let y = rect.top();
819 self.painter().hline(
820 rect.x_range(),
821 y,
822 egui::Stroke::new(DESIGN_TOKENS.stroke.hairline, stroke_color),
823 );
824 self.add_space(DESIGN_TOKENS.stroke.hairline);
825 }
826
827 fn toolbar_separator(&mut self) {
828 let padding = self.active_theme().spacing.xl; let sep_width = DESIGN_TOKENS.sizing.toolbar.separator_width;
830 let sep_height = DESIGN_TOKENS.sizing.toolbar.separator_height;
831 self.add_space(padding);
832 let (rect, _) = self.allocate_exact_size(Vec2::new(sep_width, sep_height), Sense::hover());
833 if self.is_rect_visible(rect) {
834 self.painter().rect_filled(
835 rect,
836 0.0,
837 self.visuals().widgets.noninteractive.bg_stroke.color,
838 );
839 }
840 self.add_space(padding);
841 }
842
843 fn full_width(&self) -> f32 {
844 self.available_width()
845 }
846
847 fn space_xs(&mut self) {
852 self.add_space(self.active_theme().spacing.xs);
853 }
854
855 fn space_sm(&mut self) {
856 self.add_space(self.active_theme().spacing.sm);
857 }
858
859 fn space_md(&mut self) {
860 self.add_space(self.active_theme().spacing.md);
861 }
862
863 fn space_lg(&mut self) {
864 self.add_space(self.active_theme().spacing.lg);
865 }
866
867 fn space_xl(&mut self) {
868 self.add_space(self.active_theme().spacing.xl);
869 }
870
871 fn space_xxl(&mut self) {
872 self.add_space(self.active_theme().spacing.xxl);
873 }
874
875 fn small_space(&mut self) {
877 self.space_sm();
878 }
879
880 fn medium_space(&mut self) {
881 self.space_lg();
882 }
883
884 fn large_space(&mut self) {
885 self.space_xl();
886 }
887
888 fn text_edit_singleline_ui(&mut self, text: &mut String) -> Response {
893 TextEdit::singleline(text)
894 .desired_width(f32::INFINITY)
895 .ui(self)
896 }
897
898 fn text_edit_multiline_ui(&mut self, text: &mut String) -> Response {
899 TextEdit::multiline(text)
900 .desired_width(f32::INFINITY)
901 .ui(self)
902 }
903
904 fn number_edit(&mut self, value: &mut f64) -> Response {
905 self.add(egui::DragValue::new(value).speed(0.1))
906 }
907
908 fn search_input(&mut self, query: &mut String) -> Response {
909 let placeholder = l10n::text(self.ctx(), "Search...");
910 let response = TextEdit::singleline(query)
911 .hint_text(&placeholder)
912 .desired_width(f32::INFINITY)
913 .ui(self)
914 .accessible_name(placeholder);
915
916 if !query.is_empty()
918 && response.lost_focus()
919 && self.input(|i| i.key_pressed(egui::Key::Escape))
920 {
921 query.clear();
922 }
923
924 response
925 }
926
927 fn drag_value_f32(&mut self, value: &mut f32) -> Response {
932 self.add(egui::DragValue::new(value).speed(0.1))
933 }
934
935 fn drag_value_with_range(
936 &mut self,
937 value: &mut f32,
938 range: std::ops::RangeInclusive<f32>,
939 ) -> Response {
940 self.add(egui::DragValue::new(value).speed(0.1).range(range))
941 }
942
943 fn drag_angle(&mut self, radians: &mut f32) -> Response {
944 self.add(
945 egui::DragValue::new(radians)
946 .speed(0.01)
947 .suffix("°")
948 .custom_formatter(|n, _| format!("{:.1}", n.to_degrees()))
949 .custom_parser(|s| s.parse::<f64>().ok().map(|d| d.to_radians())),
950 )
951 }
952
953 fn property_row(&mut self, label: impl Into<String>, add_value: impl FnOnce(&mut Ui)) {
958 self.horizontal(|ui| {
959 ui.label(l10n::text(ui.ctx(), Into::<String>::into(label)));
960 ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
961 add_value(ui);
962 });
963 });
964 }
965
966 fn visible_rect(&self) -> Rect {
971 self.clip_rect()
972 }
973
974 fn is_visible(&self) -> bool {
975 let rect = self.clip_rect();
976 rect.width() > 0.0 && rect.height() > 0.0
977 }
978
979 fn current_theme(&self) -> egui::Theme {
984 self.ctx().theme()
985 }
986
987 fn is_dark_mode(&self) -> bool {
988 self.ctx().theme() == egui::Theme::Dark
989 }
990
991 fn is_light_mode(&self) -> bool {
992 self.ctx().theme() == egui::Theme::Light
993 }
994
995 fn is_touch_mode(&self) -> bool {
1000 self.ctx().input(|i| i.any_touches())
1001 }
1002
1003 fn min_touch_target(&self) -> f32 {
1004 DESIGN_TOKENS.sizing.target_min
1005 }
1006
1007 fn pnl_color(&self, value: f64) -> Color32 {
1012 if value > 0.0 {
1013 self.active_theme().chart.candle_up
1014 } else if value < 0.0 {
1015 self.active_theme().chart.candle_down
1016 } else {
1017 self.style().visuals.text_color()
1018 }
1019 }
1020}