1use core::ops::RangeInclusive;
4use emath::Align;
5use epaint::{
6 CornerRadius, FontColorTransferFunction, Shadow, Stroke, TextOptions,
7 text::{FontTweak, FontVariationAxis, HintingTarget, SmoothHinting},
8};
9use std::{collections::BTreeMap, sync::Arc};
10
11use crate::{
12 ComboBox, CursorIcon, FontFamily, FontId, Grid, Margin, Response, RichText, TextWrapMode,
13 WidgetText,
14 ecolor::Color32,
15 emath::{Rangef, Rect, Vec2, pos2, vec2},
16 reset_button_with,
17};
18
19#[derive(Clone)]
21pub struct NumberFormatter(
22 Arc<dyn 'static + Sync + Send + Fn(f64, RangeInclusive<usize>) -> String>,
23);
24
25impl NumberFormatter {
26 #[inline]
31 pub fn new(
32 formatter: impl 'static + Sync + Send + Fn(f64, RangeInclusive<usize>) -> String,
33 ) -> Self {
34 Self(Arc::new(formatter))
35 }
36
37 #[inline]
46 pub fn format(&self, value: f64, decimals: RangeInclusive<usize>) -> String {
47 (self.0)(value, decimals)
48 }
49}
50
51impl core::fmt::Debug for NumberFormatter {
52 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
53 f.write_str("NumberFormatter")
54 }
55}
56
57impl PartialEq for NumberFormatter {
58 #[inline]
59 fn eq(&self, other: &Self) -> bool {
60 Arc::ptr_eq(&self.0, &other.0)
61 }
62}
63
64#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
71#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
72pub enum TextStyle {
73 Small,
75
76 Body,
78
79 Monospace,
81
82 Button,
86
87 Heading,
89
90 Name(std::sync::Arc<str>),
95}
96
97impl core::fmt::Display for TextStyle {
98 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
99 match self {
100 Self::Small => "Small".fmt(f),
101 Self::Body => "Body".fmt(f),
102 Self::Monospace => "Monospace".fmt(f),
103 Self::Button => "Button".fmt(f),
104 Self::Heading => "Heading".fmt(f),
105 Self::Name(name) => (*name).fmt(f),
106 }
107 }
108}
109
110impl TextStyle {
111 pub fn resolve(&self, style: &Style) -> FontId {
113 style.text_styles.get(self).cloned().unwrap_or_else(|| {
114 panic!(
115 "Failed to find {:?} in Style::text_styles. Available styles:\n{:#?}",
116 self,
117 style.text_styles()
118 )
119 })
120 }
121}
122
123#[derive(Debug, Clone)]
127pub enum FontSelection {
128 Default,
131
132 FontId(FontId),
134
135 Style(TextStyle),
137}
138
139impl Default for FontSelection {
140 #[inline]
141 fn default() -> Self {
142 Self::Default
143 }
144}
145
146impl FontSelection {
147 pub fn resolve(self, style: &Style) -> FontId {
152 self.resolve_with_fallback(style, TextStyle::Body.into())
153 }
154
155 pub fn resolve_with_fallback(self, style: &Style, fallback: Self) -> FontId {
159 match self {
160 Self::Default => {
161 if let Some(override_font_id) = &style.override_font_id {
162 override_font_id.clone()
163 } else if let Some(text_style) = &style.override_text_style {
164 text_style.resolve(style)
165 } else {
166 fallback.resolve(style)
167 }
168 }
169 Self::FontId(font_id) => font_id,
170 Self::Style(text_style) => text_style.resolve(style),
171 }
172 }
173}
174
175impl From<FontId> for FontSelection {
176 #[inline(always)]
177 fn from(font_id: FontId) -> Self {
178 Self::FontId(font_id)
179 }
180}
181
182impl From<TextStyle> for FontSelection {
183 #[inline(always)]
184 fn from(text_style: TextStyle) -> Self {
185 Self::Style(text_style)
186 }
187}
188
189#[derive(Clone, Default)]
194pub struct StyleModifier(Option<Arc<dyn Fn(&mut Style) + Send + Sync>>);
195
196impl core::fmt::Debug for StyleModifier {
197 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
198 f.write_str("StyleModifier")
199 }
200}
201
202impl<T> From<T> for StyleModifier
203where
204 T: Fn(&mut Style) + Send + Sync + 'static,
205{
206 fn from(f: T) -> Self {
207 Self(Some(Arc::new(f)))
208 }
209}
210
211impl From<Style> for StyleModifier {
212 fn from(style: Style) -> Self {
213 Self(Some(Arc::new(move |s| *s = style.clone())))
214 }
215}
216
217impl StyleModifier {
218 pub fn new(f: impl Fn(&mut Style) + Send + Sync + 'static) -> Self {
220 Self::from(f)
221 }
222
223 pub fn apply(&self, style: &mut Style) {
226 if let Some(f) = &self.0 {
227 f(style);
228 }
229 }
230}
231
232#[derive(Clone, Debug, PartialEq)]
242#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
243#[cfg_attr(feature = "serde", serde(default))]
244pub struct Style {
245 pub override_text_style: Option<TextStyle>,
250
251 pub override_font_id: Option<FontId>,
256
257 pub override_text_valign: Option<Align>,
261
262 pub text_styles: BTreeMap<TextStyle, FontId>,
290
291 pub drag_value_text_style: TextStyle,
293
294 #[cfg_attr(feature = "serde", serde(skip))]
298 pub number_formatter: NumberFormatter,
299
300 pub wrap_mode: Option<crate::TextWrapMode>,
307
308 pub spacing: Spacing,
310
311 pub interaction: Interaction,
313
314 pub visuals: Visuals,
316
317 pub animation_time: f32,
319
320 #[cfg(debug_assertions)]
324 pub debug: DebugOptions,
325
326 pub explanation_tooltips: bool,
330
331 pub url_in_tooltip: bool,
333
334 pub always_scroll_the_only_direction: bool,
336
337 pub scroll_animation: ScrollAnimation,
339
340 pub compact_menu_style: bool,
342}
343
344#[test]
345fn style_impl_send_sync() {
346 fn assert_send_sync<T: Send + Sync>() {}
347 assert_send_sync::<Style>();
348}
349
350impl Style {
351 pub fn interact(&self, response: &Response) -> &WidgetVisuals {
356 self.visuals.widgets.style(response)
357 }
358
359 pub fn interact_selectable(&self, response: &Response, selected: bool) -> WidgetVisuals {
360 let mut visuals = *self.visuals.widgets.style(response);
361 if selected {
362 visuals.weak_bg_fill = self.visuals.selection.bg_fill;
363 visuals.bg_fill = self.visuals.selection.bg_fill;
364 visuals.fg_stroke = self.visuals.selection.stroke;
366 }
367 visuals
368 }
369
370 pub fn noninteractive(&self) -> &WidgetVisuals {
372 &self.visuals.widgets.noninteractive
373 }
374
375 pub fn text_styles(&self) -> Vec<TextStyle> {
377 self.text_styles.keys().cloned().collect()
378 }
379}
380
381#[derive(Clone, Debug, PartialEq)]
383#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
384#[cfg_attr(feature = "serde", serde(default))]
385pub struct Spacing {
386 pub item_spacing: Vec2,
393
394 pub window_margin: Margin,
396
397 pub button_padding: Vec2,
399
400 pub menu_margin: Margin,
402
403 pub indent: f32,
405
406 pub interact_size: Vec2, pub slider_width: f32,
413
414 pub slider_rail_height: f32,
416
417 pub combo_width: f32,
419
420 pub text_edit_width: f32,
422
423 pub extra_text_line_spacing: f32,
425
426 pub icon_width: f32,
429
430 pub icon_width_inner: f32,
433
434 pub icon_spacing: f32,
437
438 pub default_area_size: Vec2,
446
447 pub tooltip_width: f32,
449
450 pub menu_width: f32,
454
455 pub menu_spacing: f32,
457
458 pub indent_ends_with_horizontal_line: bool,
460
461 pub combo_height: f32,
463
464 pub scroll: ScrollStyle,
466}
467
468impl Spacing {
469 pub fn icon_rectangles(&self, rect: Rect) -> (Rect, Rect) {
471 let icon_width = self.icon_width;
472 let big_icon_rect = Rect::from_center_size(
473 pos2(rect.left() + icon_width / 2.0, rect.center().y),
474 vec2(icon_width, icon_width),
475 );
476
477 let small_icon_rect =
478 Rect::from_center_size(big_icon_rect.center(), Vec2::splat(self.icon_width_inner));
479
480 (small_icon_rect, big_icon_rect)
481 }
482}
483
484#[derive(Clone, Copy, Debug, PartialEq)]
493#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
494#[cfg_attr(feature = "serde", serde(default))]
495pub struct ScrollStyle {
496 pub floating: bool,
504
505 pub content_margin: Margin,
510
511 pub bar_width: f32,
513
514 pub handle_min_length: f32,
516
517 pub bar_inner_margin: f32,
519
520 pub bar_outer_margin: f32,
523
524 pub floating_width: f32,
528
529 pub floating_allocated_width: f32,
536
537 pub foreground_color: bool,
539
540 pub dormant_background_opacity: f32,
546
547 pub active_background_opacity: f32,
553
554 pub interact_background_opacity: f32,
560
561 pub dormant_handle_opacity: f32,
567
568 pub active_handle_opacity: f32,
574
575 pub interact_handle_opacity: f32,
581
582 pub fade: ScrollFadeStyle,
583}
584
585impl Default for ScrollStyle {
586 fn default() -> Self {
587 Self::floating()
588 }
589}
590
591impl ScrollStyle {
592 pub fn solid() -> Self {
594 Self {
595 floating: false,
596 content_margin: Margin::ZERO,
597 bar_width: 6.0,
598 handle_min_length: 12.0,
599 bar_inner_margin: 4.0,
600 bar_outer_margin: 0.0,
601 floating_width: 2.0,
602 floating_allocated_width: 0.0,
603
604 foreground_color: false,
605
606 dormant_background_opacity: 0.0,
607 active_background_opacity: 0.4,
608 interact_background_opacity: 0.7,
609
610 dormant_handle_opacity: 0.0,
611 active_handle_opacity: 0.6,
612 interact_handle_opacity: 1.0,
613
614 fade: Default::default(),
615 }
616 }
617
618 pub fn thin() -> Self {
620 Self {
621 floating: true,
622 bar_width: 10.0,
623 floating_allocated_width: 6.0,
624 foreground_color: false,
625
626 dormant_background_opacity: 1.0,
627 dormant_handle_opacity: 1.0,
628
629 active_background_opacity: 1.0,
630 active_handle_opacity: 1.0,
631
632 interact_background_opacity: 0.6,
634 interact_handle_opacity: 0.6,
635
636 ..Self::solid()
637 }
638 }
639
640 pub fn floating() -> Self {
644 Self {
645 floating: true,
646 bar_width: 10.0,
647 foreground_color: true,
648 floating_allocated_width: 0.0,
649 dormant_background_opacity: 0.0,
650 dormant_handle_opacity: 0.0,
651 ..Self::solid()
652 }
653 }
654
655 pub fn allocated_width(&self) -> f32 {
657 if self.floating {
658 self.floating_allocated_width
659 } else {
660 self.bar_inner_margin + self.bar_width + self.bar_outer_margin
661 }
662 }
663
664 pub fn ui(&mut self, ui: &mut Ui) {
665 ui.horizontal(|ui| {
666 ui.label("Presets:");
667 ui.selectable_value(self, Self::solid(), "Solid");
668 ui.selectable_value(self, Self::thin(), "Thin");
669 ui.selectable_value(self, Self::floating(), "Floating");
670 });
671
672 ui.collapsing("Details", |ui| {
673 self.details_ui(ui);
674 });
675 }
676
677 pub fn details_ui(&mut self, ui: &mut Ui) {
678 let Self {
679 floating,
680
681 content_margin,
682
683 bar_width,
684 handle_min_length,
685 bar_inner_margin,
686 bar_outer_margin,
687 floating_width,
688 floating_allocated_width,
689
690 foreground_color,
691
692 dormant_background_opacity,
693 active_background_opacity,
694 interact_background_opacity,
695 dormant_handle_opacity,
696 active_handle_opacity,
697 interact_handle_opacity,
698
699 fade,
700 } = self;
701
702 ui.horizontal(|ui| {
703 ui.label("Type:");
704 ui.selectable_value(floating, false, "Solid");
705 ui.selectable_value(floating, true, "Floating");
706 });
707
708 ui.horizontal(|ui| {
709 ui.label("Content margin:");
710 content_margin.ui(ui);
711 });
712
713 ui.horizontal(|ui| {
714 ui.add(DragValue::new(bar_width).range(0.0..=32.0));
715 ui.label("Full bar width");
716 });
717 if *floating {
718 ui.horizontal(|ui| {
719 ui.add(DragValue::new(floating_width).range(0.0..=32.0));
720 ui.label("Thin bar width");
721 });
722 ui.horizontal(|ui| {
723 ui.add(DragValue::new(floating_allocated_width).range(0.0..=32.0));
724 ui.label("Allocated width");
725 });
726 }
727
728 ui.horizontal(|ui| {
729 ui.add(DragValue::new(handle_min_length).range(0.0..=32.0));
730 ui.label("Minimum handle length");
731 });
732 ui.horizontal(|ui| {
733 ui.add(DragValue::new(bar_outer_margin).range(0.0..=32.0));
734 ui.label("Outer margin");
735 });
736
737 ui.horizontal(|ui| {
738 ui.label("Color:");
739 ui.selectable_value(foreground_color, false, "Background");
740 ui.selectable_value(foreground_color, true, "Foreground");
741 });
742
743 if *floating {
744 crate::Grid::new("opacity").show(ui, |ui| {
745 fn opacity_ui(ui: &mut Ui, opacity: &mut f32) {
746 ui.add(DragValue::new(opacity).speed(0.01).range(0.0..=1.0));
747 }
748
749 ui.label("Opacity");
750 ui.label("Dormant");
751 ui.label("Active");
752 ui.label("Interacting");
753 ui.end_row();
754
755 ui.label("Background:");
756 opacity_ui(ui, dormant_background_opacity);
757 opacity_ui(ui, active_background_opacity);
758 opacity_ui(ui, interact_background_opacity);
759 ui.end_row();
760
761 ui.label("Handle:");
762 opacity_ui(ui, dormant_handle_opacity);
763 opacity_ui(ui, active_handle_opacity);
764 opacity_ui(ui, interact_handle_opacity);
765 ui.end_row();
766 });
767 } else {
768 ui.horizontal(|ui| {
769 ui.add(DragValue::new(bar_inner_margin).range(0.0..=32.0));
770 ui.label("Inner margin");
771 });
772 }
773
774 ui.separator();
775 fade.ui(ui);
776 }
777}
778
779#[derive(Clone, Copy, Debug, PartialEq)]
782#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
783#[cfg_attr(feature = "serde", serde(default))]
784pub struct ScrollFadeStyle {
785 pub strength: f32,
789
790 pub size: f32,
793}
794
795impl Default for ScrollFadeStyle {
796 fn default() -> Self {
797 Self {
798 strength: 0.5,
799 size: 20.0,
800 }
801 }
802}
803
804impl ScrollFadeStyle {
805 pub fn ui(&mut self, ui: &mut Ui) {
806 let Self { strength, size } = self;
807
808 ui.horizontal(|ui| {
809 ui.add(DragValue::new(strength).speed(0.01).range(0.0..=1.0));
810 ui.label("Fade strength");
811 });
812
813 if 0.0 < *strength {
814 ui.horizontal(|ui| {
815 ui.add(DragValue::new(size).range(0.0..=64.0));
816 ui.label("Fade size");
817 });
818 }
819 }
820}
821
822#[derive(Copy, Clone, Debug, PartialEq)]
829#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
830#[cfg_attr(feature = "serde", serde(default))]
831pub struct ScrollAnimation {
832 pub points_per_second: f32,
834
835 pub duration: Rangef,
837}
838
839impl Default for ScrollAnimation {
840 fn default() -> Self {
841 Self {
842 points_per_second: 1000.0,
843 duration: Rangef::new(0.1, 0.3),
844 }
845 }
846}
847
848impl ScrollAnimation {
849 pub fn new(points_per_second: f32, duration: Rangef) -> Self {
851 Self {
852 points_per_second,
853 duration,
854 }
855 }
856
857 pub fn none() -> Self {
859 Self {
860 points_per_second: f32::INFINITY,
861 duration: Rangef::new(0.0, 0.0),
862 }
863 }
864
865 pub fn duration(t: f32) -> Self {
867 Self {
868 points_per_second: f32::INFINITY,
869 duration: Rangef::new(t, t),
870 }
871 }
872
873 pub fn ui(&mut self, ui: &mut crate::Ui) {
874 crate::Grid::new("scroll_animation").show(ui, |ui| {
875 ui.label("Scroll animation:");
876 ui.add(
877 DragValue::new(&mut self.points_per_second)
878 .speed(100.0)
879 .range(0.0..=5000.0),
880 );
881 ui.label("points/second");
882 ui.end_row();
883
884 ui.label("Min duration:");
885 ui.add(
886 DragValue::new(&mut self.duration.min)
887 .speed(0.01)
888 .range(0.0..=self.duration.max),
889 );
890 ui.label("seconds");
891 ui.end_row();
892
893 ui.label("Max duration:");
894 ui.add(
895 DragValue::new(&mut self.duration.max)
896 .speed(0.01)
897 .range(0.0..=1.0),
898 );
899 ui.label("seconds");
900 ui.end_row();
901 });
902 }
903}
904
905#[derive(Clone, Debug, PartialEq)]
909#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
910#[cfg_attr(feature = "serde", serde(default))]
911pub struct Interaction {
912 pub interact_radius: f32,
917
918 pub resize_grab_radius_side: f32,
920
921 pub resize_grab_radius_corner: f32,
923
924 pub show_tooltips_only_when_still: bool,
926
927 pub tooltip_delay: f32,
929
930 pub tooltip_grace_time: f32,
936
937 pub selectable_labels: bool,
939
940 pub multi_widget_text_select: bool,
945}
946
947#[derive(Clone, Debug, PartialEq)]
949#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
950#[cfg_attr(feature = "serde", serde(default))]
951pub struct TextCursorStyle {
952 pub stroke: Stroke,
954
955 pub preview: bool,
957
958 pub blink: bool,
960
961 pub on_duration: f32,
963
964 pub off_duration: f32,
966}
967
968impl Default for TextCursorStyle {
969 fn default() -> Self {
970 Self {
971 stroke: Stroke::new(2.0, Color32::from_rgb(192, 222, 255)), preview: false,
973 blink: true,
974 on_duration: 0.5,
975 off_duration: 0.5,
976 }
977 }
978}
979
980#[derive(Clone, Debug, PartialEq)]
987#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
988#[cfg_attr(feature = "serde", serde(default))]
989pub struct Visuals {
990 pub dark_mode: bool,
996
997 pub text_options: TextOptions,
1002
1003 pub override_text_color: Option<Color32>,
1017
1018 pub weak_text_alpha: f32,
1022
1023 pub weak_text_color: Option<Color32>,
1028
1029 pub widgets: Widgets,
1031
1032 pub selection: Selection,
1033 pub ime_composition: ImeComposition,
1034
1035 pub hyperlink_color: Color32,
1037
1038 pub faint_bg_color: Color32,
1041
1042 pub extreme_bg_color: Color32,
1046
1047 pub text_edit_bg_color: Option<Color32>,
1051
1052 pub code_bg_color: Color32,
1054
1055 pub warn_fg_color: Color32,
1057
1058 pub error_fg_color: Color32,
1060
1061 pub window_corner_radius: CornerRadius,
1062 pub window_shadow: Shadow,
1063 pub window_fill: Color32,
1064 pub window_stroke: Stroke,
1065
1066 pub window_highlight_topmost: bool,
1068
1069 pub menu_corner_radius: CornerRadius,
1070
1071 pub panel_fill: Color32,
1073
1074 pub popup_shadow: Shadow,
1075
1076 pub resize_corner_size: f32,
1077
1078 pub text_cursor: TextCursorStyle,
1080
1081 #[deprecated(note = "This is now unused and has no effect")]
1087 pub clip_rect_margin: f32,
1088
1089 pub button_frame: bool,
1091
1092 pub collapsing_header_frame: bool,
1094
1095 pub indent_has_left_vline: bool,
1097
1098 pub striped: bool,
1101
1102 pub slider_trailing_fill: bool,
1106
1107 pub handle_shape: HandleShape,
1111
1112 pub interact_cursor: Option<CursorIcon>,
1118
1119 pub image_loading_spinners: bool,
1121
1122 pub numeric_color_space: NumericColorSpace,
1124
1125 pub disabled_alpha: f32,
1127}
1128
1129impl Visuals {
1130 #[inline(always)]
1131 pub fn noninteractive(&self) -> &WidgetVisuals {
1132 &self.widgets.noninteractive
1133 }
1134
1135 pub fn text_color(&self) -> Color32 {
1137 self.override_text_color
1138 .unwrap_or_else(|| self.widgets.noninteractive.text_color())
1139 }
1140
1141 pub fn weak_text_color(&self) -> Color32 {
1142 self.weak_text_color
1143 .unwrap_or_else(|| self.text_color().gamma_multiply(self.weak_text_alpha))
1144 }
1145
1146 #[inline(always)]
1147 pub fn strong_text_color(&self) -> Color32 {
1148 self.widgets.active.text_color()
1149 }
1150
1151 pub fn text_edit_bg_color(&self) -> Color32 {
1153 self.text_edit_bg_color.unwrap_or(self.extreme_bg_color)
1154 }
1155
1156 #[inline(always)]
1158 pub fn window_fill(&self) -> Color32 {
1159 self.window_fill
1160 }
1161
1162 #[inline(always)]
1163 pub fn window_stroke(&self) -> Stroke {
1164 self.window_stroke
1165 }
1166
1167 #[inline(always)]
1169 pub fn disabled_alpha(&self) -> f32 {
1170 self.disabled_alpha
1171 }
1172
1173 #[inline(always)]
1178 pub fn disable(&self, color: Color32) -> Color32 {
1179 color.gamma_multiply(self.disabled_alpha())
1180 }
1181
1182 #[doc(alias = "grey_out")]
1184 #[inline(always)]
1185 pub fn gray_out(&self, color: Color32) -> Color32 {
1186 crate::ecolor::tint_color_towards(color, self.widgets.noninteractive.weak_bg_fill)
1187 }
1188}
1189
1190#[derive(Clone, Copy, Debug, PartialEq)]
1192#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
1193#[cfg_attr(feature = "serde", serde(default))]
1194pub struct Selection {
1195 pub bg_fill: Color32,
1197
1198 pub stroke: Stroke,
1200}
1201
1202#[derive(Clone, Copy, Debug, PartialEq)]
1204#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
1205#[cfg_attr(feature = "serde", serde(default))]
1206pub struct ImeComposition {
1207 pub active_underline_stroke: Stroke,
1209
1210 pub inactive_underline_stroke: Stroke,
1212
1213 pub legacy_visuals: bool,
1230}
1231
1232#[derive(Clone, Copy, Debug, PartialEq)]
1234#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
1235pub enum HandleShape {
1236 Circle,
1238
1239 Rect {
1241 aspect_ratio: f32,
1243 },
1244}
1245
1246#[derive(Clone, Debug, PartialEq)]
1248#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
1249#[cfg_attr(feature = "serde", serde(default))]
1250pub struct Widgets {
1251 pub noninteractive: WidgetVisuals,
1256
1257 pub inactive: WidgetVisuals,
1259
1260 pub hovered: WidgetVisuals,
1264
1265 pub active: WidgetVisuals,
1267
1268 pub open: WidgetVisuals,
1270}
1271
1272impl Widgets {
1273 pub fn style(&self, response: &Response) -> &WidgetVisuals {
1274 if !response.sense.interactive() {
1275 &self.noninteractive
1276 } else if response.is_pointer_button_down_on() || response.has_focus() || response.clicked()
1277 {
1278 &self.active
1279 } else if response.hovered() || response.highlighted() {
1280 &self.hovered
1281 } else {
1282 &self.inactive
1283 }
1284 }
1285}
1286
1287#[derive(Clone, Copy, Debug, PartialEq)]
1289#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
1290pub struct WidgetVisuals {
1291 pub bg_fill: Color32,
1296
1297 pub weak_bg_fill: Color32,
1301
1302 pub bg_stroke: Stroke,
1306
1307 pub corner_radius: CornerRadius,
1309
1310 pub fg_stroke: Stroke,
1312
1313 pub expansion: f32,
1320}
1321
1322impl WidgetVisuals {
1323 #[inline(always)]
1324 pub fn text_color(&self) -> Color32 {
1325 self.fg_stroke.color
1326 }
1327}
1328
1329#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1331#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
1332#[cfg(debug_assertions)]
1333pub struct DebugOptions {
1334 #[cfg(debug_assertions)]
1342 pub debug_on_hover: bool,
1343
1344 #[cfg(debug_assertions)]
1354 pub debug_on_hover_with_all_modifiers: bool,
1355
1356 #[cfg(debug_assertions)]
1358 pub hover_shows_next: bool,
1359
1360 pub show_expand_width: bool,
1362
1363 pub show_expand_height: bool,
1365
1366 pub show_resize: bool,
1367
1368 pub show_interactive_widgets: bool,
1370
1371 pub show_widget_hits: bool,
1373
1374 pub warn_if_rect_changes_id: bool,
1377
1378 pub show_unaligned: bool,
1382
1383 pub show_focused_widget: bool,
1389}
1390
1391#[cfg(debug_assertions)]
1392impl Default for DebugOptions {
1393 fn default() -> Self {
1394 Self {
1395 debug_on_hover: false,
1396 debug_on_hover_with_all_modifiers: cfg!(feature = "callstack")
1397 && !cfg!(target_arch = "wasm32"),
1398 hover_shows_next: false,
1399 show_expand_width: false,
1400 show_expand_height: false,
1401 show_resize: false,
1402 show_interactive_widgets: false,
1403 show_widget_hits: false,
1404 warn_if_rect_changes_id: false,
1405 show_unaligned: cfg!(debug_assertions),
1406 show_focused_widget: false,
1407 }
1408 }
1409}
1410
1411pub fn default_text_styles() -> BTreeMap<TextStyle, FontId> {
1415 use FontFamily::{Monospace, Proportional};
1416
1417 [
1418 (TextStyle::Small, FontId::new(9.0, Proportional)),
1419 (TextStyle::Body, FontId::new(13.0, Proportional)),
1420 (TextStyle::Button, FontId::new(13.0, Proportional)),
1421 (TextStyle::Heading, FontId::new(18.0, Proportional)),
1422 (TextStyle::Monospace, FontId::new(13.0, Monospace)),
1423 ]
1424 .into()
1425}
1426
1427impl Default for Style {
1428 fn default() -> Self {
1429 Self {
1430 override_font_id: None,
1431 override_text_style: None,
1432 override_text_valign: Some(Align::Center),
1433 text_styles: default_text_styles(),
1434 drag_value_text_style: TextStyle::Button,
1435 number_formatter: NumberFormatter(Arc::new(emath::format_with_decimals_in_range)),
1436 wrap_mode: None,
1437 spacing: Spacing::default(),
1438 interaction: Interaction::default(),
1439 visuals: Visuals::default(),
1440 animation_time: 0.2,
1441 #[cfg(debug_assertions)]
1442 debug: Default::default(),
1443 explanation_tooltips: false,
1444 url_in_tooltip: false,
1445 always_scroll_the_only_direction: false,
1446 scroll_animation: ScrollAnimation::default(),
1447 compact_menu_style: true,
1448 }
1449 }
1450}
1451
1452impl Default for Spacing {
1453 fn default() -> Self {
1454 Self {
1455 item_spacing: vec2(8.0, 3.0),
1456 window_margin: Margin::same(6),
1457 menu_margin: Margin::same(6),
1458 button_padding: vec2(4.0, 1.0),
1459 indent: 18.0, interact_size: vec2(40.0, 18.0),
1461 slider_width: 100.0,
1462 slider_rail_height: 8.0,
1463 combo_width: 100.0,
1464 text_edit_width: 280.0,
1465 extra_text_line_spacing: 0.0,
1466 icon_width: 14.0,
1467 icon_width_inner: 8.0,
1468 icon_spacing: 4.0,
1469 default_area_size: vec2(600.0, 400.0),
1470 tooltip_width: 500.0,
1471 menu_width: 400.0,
1472 menu_spacing: 2.0,
1473 combo_height: 200.0,
1474 scroll: Default::default(),
1475 indent_ends_with_horizontal_line: false,
1476 }
1477 }
1478}
1479
1480impl Default for Interaction {
1481 fn default() -> Self {
1482 Self {
1483 interact_radius: 5.0,
1484 resize_grab_radius_side: 3.0,
1485 resize_grab_radius_corner: 10.0,
1486 show_tooltips_only_when_still: true,
1487 tooltip_delay: 0.5,
1488 tooltip_grace_time: 0.2,
1489 selectable_labels: true,
1490 multi_widget_text_select: true,
1491 }
1492 }
1493}
1494
1495impl Visuals {
1496 #[expect(deprecated)]
1498 pub fn dark() -> Self {
1499 Self {
1500 dark_mode: true,
1501 text_options: TextOptions {
1502 color_transfer_function: FontColorTransferFunction::DARK_MODE_DEFAULT,
1503 ..Default::default()
1504 },
1505 override_text_color: None,
1506 weak_text_alpha: 0.6,
1507 weak_text_color: None,
1508 widgets: Widgets::default(),
1509 selection: Selection::default(),
1510 ime_composition: ImeComposition::default(),
1511 hyperlink_color: Color32::from_rgb(90, 170, 255),
1512 faint_bg_color: Color32::from_additive_luminance(5), extreme_bg_color: Color32::from_gray(10), text_edit_bg_color: None, code_bg_color: Color32::from_gray(64),
1516 warn_fg_color: Color32::from_rgb(255, 143, 0), error_fg_color: Color32::from_rgb(255, 0, 0), window_corner_radius: CornerRadius::same(6),
1520 window_shadow: Shadow {
1521 offset: [10, 20],
1522 blur: 15,
1523 spread: 0,
1524 color: Color32::from_black_alpha(96),
1525 },
1526 window_fill: Color32::from_gray(27),
1527 window_stroke: Stroke::new(1.0, Color32::from_gray(60)),
1528 window_highlight_topmost: true,
1529
1530 menu_corner_radius: CornerRadius::same(6),
1531
1532 panel_fill: Color32::from_gray(27),
1533
1534 popup_shadow: Shadow {
1535 offset: [6, 10],
1536 blur: 8,
1537 spread: 0,
1538 color: Color32::from_black_alpha(96),
1539 },
1540
1541 resize_corner_size: 12.0,
1542
1543 text_cursor: Default::default(),
1544
1545 clip_rect_margin: 0.0,
1546 button_frame: true,
1547 collapsing_header_frame: false,
1548 indent_has_left_vline: true,
1549
1550 striped: false,
1551
1552 slider_trailing_fill: false,
1553 handle_shape: HandleShape::Rect { aspect_ratio: 0.75 },
1554
1555 interact_cursor: None,
1556
1557 image_loading_spinners: true,
1558
1559 numeric_color_space: NumericColorSpace::GammaByte,
1560 disabled_alpha: 0.5,
1561 }
1562 }
1563
1564 pub fn light() -> Self {
1566 Self {
1567 dark_mode: false,
1568 text_options: TextOptions {
1569 color_transfer_function: FontColorTransferFunction::LIGHT_MODE_DEFAULT,
1570 ..Default::default()
1571 },
1572 widgets: Widgets::light(),
1573 selection: Selection::light(),
1574 ime_composition: ImeComposition::light(),
1575 hyperlink_color: Color32::from_rgb(0, 155, 255),
1576 faint_bg_color: Color32::from_additive_luminance(5), extreme_bg_color: Color32::from_gray(255), code_bg_color: Color32::from_gray(230),
1579 warn_fg_color: Color32::from_rgb(255, 100, 0), error_fg_color: Color32::from_rgb(255, 0, 0), window_shadow: Shadow {
1583 offset: [10, 20],
1584 blur: 15,
1585 spread: 0,
1586 color: Color32::from_black_alpha(25),
1587 },
1588 window_fill: Color32::from_gray(248),
1589 window_stroke: Stroke::new(1.0, Color32::from_gray(190)),
1590
1591 panel_fill: Color32::from_gray(248),
1592
1593 popup_shadow: Shadow {
1594 offset: [6, 10],
1595 blur: 8,
1596 spread: 0,
1597 color: Color32::from_black_alpha(25),
1598 },
1599
1600 text_cursor: TextCursorStyle {
1601 stroke: Stroke::new(2.0, Color32::from_rgb(0, 83, 125)),
1602 ..Default::default()
1603 },
1604
1605 ..Self::dark()
1606 }
1607 }
1608}
1609
1610impl Default for Visuals {
1611 fn default() -> Self {
1612 Self::dark()
1613 }
1614}
1615
1616impl Selection {
1617 fn dark() -> Self {
1618 Self {
1619 bg_fill: Color32::from_rgb(0, 92, 128),
1620 stroke: Stroke::new(1.0, Color32::from_rgb(192, 222, 255)),
1621 }
1622 }
1623
1624 fn light() -> Self {
1625 Self {
1626 bg_fill: Color32::from_rgb(144, 209, 255),
1627 stroke: Stroke::new(1.0, Color32::from_rgb(0, 83, 125)),
1628 }
1629 }
1630}
1631
1632impl Default for Selection {
1633 fn default() -> Self {
1634 Self::dark()
1635 }
1636}
1637
1638impl ImeComposition {
1639 fn dark() -> Self {
1640 let active_underline_stroke = Stroke::new(2.0, Color32::from_rgb(192, 222, 255));
1642 let inactive_underline_stroke = Stroke {
1643 width: active_underline_stroke.width,
1644 color: active_underline_stroke.color.linear_multiply(0.5),
1645 };
1646 Self {
1647 active_underline_stroke,
1648 inactive_underline_stroke,
1649 legacy_visuals: Self::default_legacy_visuals(),
1650 }
1651 }
1652
1653 fn light() -> Self {
1654 let active_underline_stroke = Stroke::new(2.0, Color32::from_rgb(0, 83, 125));
1656 let inactive_underline_stroke = Stroke {
1657 width: active_underline_stroke.width,
1658 color: active_underline_stroke.color.linear_multiply(0.5),
1659 };
1660 Self {
1661 active_underline_stroke,
1662 inactive_underline_stroke,
1663 legacy_visuals: Self::default_legacy_visuals(),
1664 }
1665 }
1666
1667 const fn default_legacy_visuals() -> bool {
1670 cfg!(windows)
1671 }
1672}
1673
1674impl Default for ImeComposition {
1675 fn default() -> Self {
1676 Self::dark()
1677 }
1678}
1679
1680impl Widgets {
1681 pub fn dark() -> Self {
1682 Self {
1683 noninteractive: WidgetVisuals {
1684 weak_bg_fill: Color32::from_gray(27),
1685 bg_fill: Color32::from_gray(27),
1686 bg_stroke: Stroke::new(1.0, Color32::from_gray(60)), fg_stroke: Stroke::new(1.0, Color32::from_gray(140)), corner_radius: CornerRadius::same(2),
1689 expansion: 0.0,
1690 },
1691 inactive: WidgetVisuals {
1692 weak_bg_fill: Color32::from_gray(60), bg_fill: Color32::from_gray(60), bg_stroke: Default::default(),
1695 fg_stroke: Stroke::new(1.0, Color32::from_gray(180)), corner_radius: CornerRadius::same(2),
1697 expansion: 0.0,
1698 },
1699 hovered: WidgetVisuals {
1700 weak_bg_fill: Color32::from_gray(70),
1701 bg_fill: Color32::from_gray(70),
1702 bg_stroke: Stroke::new(1.0, Color32::from_gray(150)), fg_stroke: Stroke::new(1.5, Color32::from_gray(240)),
1704 corner_radius: CornerRadius::same(3),
1705 expansion: 0.0,
1706 },
1707 active: WidgetVisuals {
1708 weak_bg_fill: Color32::from_gray(55),
1709 bg_fill: Color32::from_gray(55),
1710 bg_stroke: Stroke::new(1.0, Color32::WHITE),
1711 fg_stroke: Stroke::new(2.0, Color32::WHITE),
1712 corner_radius: CornerRadius::same(2),
1713 expansion: 0.0,
1714 },
1715 open: WidgetVisuals {
1716 weak_bg_fill: Color32::from_gray(45),
1717 bg_fill: Color32::from_gray(27),
1718 bg_stroke: Stroke::new(1.0, Color32::from_gray(60)),
1719 fg_stroke: Stroke::new(1.0, Color32::from_gray(210)),
1720 corner_radius: CornerRadius::same(2),
1721 expansion: 0.0,
1722 },
1723 }
1724 }
1725
1726 pub fn light() -> Self {
1727 Self {
1728 noninteractive: WidgetVisuals {
1729 weak_bg_fill: Color32::from_gray(248),
1730 bg_fill: Color32::from_gray(248),
1731 bg_stroke: Stroke::new(1.0, Color32::from_gray(190)), fg_stroke: Stroke::new(1.0, Color32::from_gray(80)), corner_radius: CornerRadius::same(2),
1734 expansion: 0.0,
1735 },
1736 inactive: WidgetVisuals {
1737 weak_bg_fill: Color32::from_gray(230), bg_fill: Color32::from_gray(230), bg_stroke: Default::default(),
1740 fg_stroke: Stroke::new(1.0, Color32::from_gray(60)), corner_radius: CornerRadius::same(2),
1742 expansion: 0.0,
1743 },
1744 hovered: WidgetVisuals {
1745 weak_bg_fill: Color32::from_gray(220),
1746 bg_fill: Color32::from_gray(220),
1747 bg_stroke: Stroke::new(1.0, Color32::from_gray(105)), fg_stroke: Stroke::new(1.5, Color32::BLACK),
1749 corner_radius: CornerRadius::same(3),
1750 expansion: 0.0,
1751 },
1752 active: WidgetVisuals {
1753 weak_bg_fill: Color32::from_gray(165),
1754 bg_fill: Color32::from_gray(165),
1755 bg_stroke: Stroke::new(1.0, Color32::BLACK),
1756 fg_stroke: Stroke::new(2.0, Color32::BLACK),
1757 corner_radius: CornerRadius::same(2),
1758 expansion: 0.0,
1759 },
1760 open: WidgetVisuals {
1761 weak_bg_fill: Color32::from_gray(220),
1762 bg_fill: Color32::from_gray(220),
1763 bg_stroke: Stroke::new(1.0, Color32::from_gray(160)),
1764 fg_stroke: Stroke::new(1.0, Color32::BLACK),
1765 corner_radius: CornerRadius::same(2),
1766 expansion: 0.0,
1767 },
1768 }
1769 }
1770}
1771
1772impl Default for Widgets {
1773 fn default() -> Self {
1774 Self::dark()
1775 }
1776}
1777
1778use crate::{
1781 Ui,
1782 widgets::{DragValue, Slider, Widget, reset_button},
1783};
1784
1785impl Style {
1786 pub fn ui(&mut self, ui: &mut crate::Ui) {
1787 let Self {
1788 override_font_id,
1789 override_text_style,
1790 override_text_valign,
1791 text_styles,
1792 drag_value_text_style,
1793 number_formatter: _, wrap_mode,
1795 spacing,
1796 interaction,
1797 visuals,
1798 animation_time,
1799 #[cfg(debug_assertions)]
1800 debug,
1801 explanation_tooltips,
1802 url_in_tooltip,
1803 always_scroll_the_only_direction,
1804 scroll_animation,
1805 compact_menu_style,
1806 } = self;
1807
1808 crate::Grid::new("_options").show(ui, |ui| {
1809 ui.label("Override font id");
1810 ui.vertical(|ui| {
1811 ui.horizontal(|ui| {
1812 ui.radio_value(override_font_id, None, "None");
1813 if ui.radio(override_font_id.is_some(), "override").clicked() {
1814 *override_font_id = Some(FontId::default());
1815 }
1816 });
1817 if let Some(override_font_id) = override_font_id {
1818 crate::introspection::font_id_ui(ui, override_font_id);
1819 }
1820 });
1821 ui.end_row();
1822
1823 ui.label("Override text style");
1824 crate::ComboBox::from_id_salt("override_text_style")
1825 .selected_text(match override_text_style {
1826 None => "None".to_owned(),
1827 Some(override_text_style) => override_text_style.to_string(),
1828 })
1829 .show_ui(ui, |ui| {
1830 ui.selectable_value(override_text_style, None, "None");
1831 let all_text_styles = ui.style().text_styles();
1832 for style in all_text_styles {
1833 let text =
1834 crate::RichText::new(style.to_string()).text_style(style.clone());
1835 ui.selectable_value(override_text_style, Some(style), text);
1836 }
1837 });
1838 ui.end_row();
1839
1840 fn valign_name(valign: Align) -> &'static str {
1841 match valign {
1842 Align::TOP => "Top",
1843 Align::Center => "Center",
1844 Align::BOTTOM => "Bottom",
1845 }
1846 }
1847
1848 ui.label("Override text valign");
1849 crate::ComboBox::from_id_salt("override_text_valign")
1850 .selected_text(match override_text_valign {
1851 None => "None",
1852 Some(override_text_valign) => valign_name(*override_text_valign),
1853 })
1854 .show_ui(ui, |ui| {
1855 ui.selectable_value(override_text_valign, None, "None");
1856 for align in [Align::TOP, Align::Center, Align::BOTTOM] {
1857 ui.selectable_value(override_text_valign, Some(align), valign_name(align));
1858 }
1859 });
1860 ui.end_row();
1861
1862 ui.label("Text style of DragValue");
1863 crate::ComboBox::from_id_salt("drag_value_text_style")
1864 .selected_text(drag_value_text_style.to_string())
1865 .show_ui(ui, |ui| {
1866 let all_text_styles = ui.style().text_styles();
1867 for style in all_text_styles {
1868 let text =
1869 crate::RichText::new(style.to_string()).text_style(style.clone());
1870 ui.selectable_value(drag_value_text_style, style, text);
1871 }
1872 });
1873 ui.end_row();
1874
1875 ui.label("Text Wrap Mode");
1876 crate::ComboBox::from_id_salt("text_wrap_mode")
1877 .selected_text(format!("{wrap_mode:?}"))
1878 .show_ui(ui, |ui| {
1879 let all_wrap_mode: Vec<Option<TextWrapMode>> = vec![
1880 None,
1881 Some(TextWrapMode::Extend),
1882 Some(TextWrapMode::Wrap),
1883 Some(TextWrapMode::Truncate),
1884 ];
1885 for style in all_wrap_mode {
1886 let text = crate::RichText::new(format!("{style:?}"));
1887 ui.selectable_value(wrap_mode, style, text);
1888 }
1889 });
1890 ui.end_row();
1891
1892 ui.label("Animation duration");
1893 ui.add(
1894 DragValue::new(animation_time)
1895 .range(0.0..=1.0)
1896 .speed(0.02)
1897 .suffix(" s"),
1898 );
1899 ui.end_row();
1900 });
1901
1902 ui.collapsing("🔠 Text styles", |ui| text_styles_ui(ui, text_styles));
1903 ui.collapsing("📏 Spacing", |ui| spacing.ui(ui));
1904 ui.collapsing("☝ Interaction", |ui| interaction.ui(ui));
1905 ui.collapsing("🎨 Visuals", |ui| visuals.ui(ui));
1906 ui.collapsing("🔄 Scroll animation", |ui| scroll_animation.ui(ui));
1907
1908 #[cfg(debug_assertions)]
1909 ui.collapsing("🐛 Debug", |ui| debug.ui(ui));
1910
1911 ui.checkbox(compact_menu_style, "Compact menu style");
1912
1913 ui.checkbox(explanation_tooltips, "Explanation tooltips")
1914 .on_hover_text(
1915 "Show explanatory text when hovering DragValue:s and other egui widgets",
1916 );
1917
1918 ui.checkbox(url_in_tooltip, "Show url when hovering links");
1919
1920 ui.checkbox(always_scroll_the_only_direction, "Always scroll the only enabled direction")
1921 .on_hover_text(
1922 "If scrolling is enabled for only one direction, allow horizontal scrolling without pressing shift",
1923 );
1924
1925 ui.vertical_centered(|ui| reset_button(ui, self, "Reset style"));
1926 }
1927}
1928
1929fn text_styles_ui(ui: &mut Ui, text_styles: &mut BTreeMap<TextStyle, FontId>) -> Response {
1930 ui.vertical(|ui| {
1931 crate::Grid::new("text_styles").show(ui, |ui| {
1932 for (text_style, font_id) in &mut *text_styles {
1933 ui.label(RichText::new(text_style.to_string()).font(font_id.clone()));
1934 crate::introspection::font_id_ui(ui, font_id);
1935 ui.end_row();
1936 }
1937 });
1938 crate::reset_button_with(ui, text_styles, "Reset text styles", default_text_styles());
1939 })
1940 .response
1941}
1942
1943impl Spacing {
1944 pub fn ui(&mut self, ui: &mut crate::Ui) {
1945 let Self {
1946 item_spacing,
1947 window_margin,
1948 menu_margin,
1949 button_padding,
1950 indent,
1951 interact_size,
1952 slider_width,
1953 slider_rail_height,
1954 combo_width,
1955 text_edit_width,
1956 extra_text_line_spacing,
1957 icon_width,
1958 icon_width_inner,
1959 icon_spacing,
1960 default_area_size,
1961 tooltip_width,
1962 menu_width,
1963 menu_spacing,
1964 indent_ends_with_horizontal_line,
1965 combo_height,
1966 scroll,
1967 } = self;
1968
1969 Grid::new("spacing")
1970 .num_columns(2)
1971 .spacing([12.0, 8.0])
1972 .striped(true)
1973 .show(ui, |ui| {
1974 ui.label("Item spacing");
1975 ui.add(two_drag_values(item_spacing, 0.0..=20.0));
1976 ui.end_row();
1977
1978 ui.label("Window margin");
1979 ui.add(window_margin);
1980 ui.end_row();
1981
1982 ui.label("ScrollArea margin");
1983 scroll.content_margin.ui(ui);
1984 ui.end_row();
1985
1986 ui.label("Menu margin");
1987 ui.add(menu_margin);
1988 ui.end_row();
1989
1990 ui.label("Button padding");
1991 ui.add(two_drag_values(button_padding, 0.0..=20.0));
1992 ui.end_row();
1993
1994 ui.label("Interact size")
1995 .on_hover_text("Minimum size of an interactive widget");
1996 ui.add(two_drag_values(interact_size, 4.0..=60.0));
1997 ui.end_row();
1998
1999 ui.label("Indent");
2000 ui.add(DragValue::new(indent).range(0.0..=100.0));
2001 ui.end_row();
2002
2003 ui.label("Slider width");
2004 ui.add(DragValue::new(slider_width).range(0.0..=1000.0));
2005 ui.end_row();
2006
2007 ui.label("Slider rail height");
2008 ui.add(DragValue::new(slider_rail_height).range(0.0..=50.0));
2009 ui.end_row();
2010
2011 ui.label("ComboBox width");
2012 ui.add(DragValue::new(combo_width).range(0.0..=1000.0));
2013 ui.end_row();
2014
2015 ui.label("Default area size");
2016 ui.add(two_drag_values(default_area_size, 0.0..=1000.0));
2017 ui.end_row();
2018
2019 ui.label("TextEdit width");
2020 ui.add(DragValue::new(text_edit_width).range(0.0..=1000.0));
2021 ui.end_row();
2022
2023 ui.label("Extra text line spacing");
2024 ui.add(DragValue::new(extra_text_line_spacing).range(0.0..=20.0));
2025 ui.end_row();
2026
2027 ui.label("Tooltip wrap width");
2028 ui.add(DragValue::new(tooltip_width).range(0.0..=1000.0));
2029 ui.end_row();
2030
2031 ui.label("Default menu width");
2032 ui.add(DragValue::new(menu_width).range(0.0..=1000.0));
2033 ui.end_row();
2034
2035 ui.label("Menu spacing")
2036 .on_hover_text("Horizontal spacing between menus");
2037 ui.add(DragValue::new(menu_spacing).range(0.0..=10.0));
2038 ui.end_row();
2039
2040 ui.label("Checkboxes etc");
2041 ui.vertical(|ui| {
2042 ui.add(
2043 DragValue::new(icon_width)
2044 .prefix("outer icon width:")
2045 .range(0.0..=60.0),
2046 );
2047 ui.add(
2048 DragValue::new(icon_width_inner)
2049 .prefix("inner icon width:")
2050 .range(0.0..=60.0),
2051 );
2052 ui.add(
2053 DragValue::new(icon_spacing)
2054 .prefix("spacing:")
2055 .range(0.0..=10.0),
2056 );
2057 });
2058 ui.end_row();
2059 });
2060
2061 ui.checkbox(
2062 indent_ends_with_horizontal_line,
2063 "End indented regions with a horizontal separator",
2064 );
2065
2066 ui.horizontal(|ui| {
2067 ui.label("Max height of a combo box");
2068 ui.add(DragValue::new(combo_height).range(0.0..=1000.0));
2069 });
2070
2071 ui.collapsing("Scroll Area", |ui| {
2072 scroll.ui(ui);
2073 });
2074
2075 ui.vertical_centered(|ui| reset_button(ui, self, "Reset spacing"));
2076 }
2077}
2078
2079impl Interaction {
2080 pub fn ui(&mut self, ui: &mut crate::Ui) {
2081 let Self {
2082 interact_radius,
2083 resize_grab_radius_side,
2084 resize_grab_radius_corner,
2085 show_tooltips_only_when_still,
2086 tooltip_delay,
2087 tooltip_grace_time,
2088 selectable_labels,
2089 multi_widget_text_select,
2090 } = self;
2091
2092 ui.spacing_mut().item_spacing = vec2(12.0, 8.0);
2093
2094 Grid::new("interaction")
2095 .num_columns(2)
2096 .striped(true)
2097 .show(ui, |ui| {
2098 ui.label("interact_radius")
2099 .on_hover_text("Interact with the closest widget within this radius.");
2100 ui.add(DragValue::new(interact_radius).range(0.0..=20.0));
2101 ui.end_row();
2102
2103 ui.label("resize_grab_radius_side").on_hover_text("Radius of the interactive area of the side of a window during drag-to-resize");
2104 ui.add(DragValue::new(resize_grab_radius_side).range(0.0..=20.0));
2105 ui.end_row();
2106
2107 ui.label("resize_grab_radius_corner").on_hover_text("Radius of the interactive area of the corner of a window during drag-to-resize.");
2108 ui.add(DragValue::new(resize_grab_radius_corner).range(0.0..=20.0));
2109 ui.end_row();
2110
2111 ui.label("Tooltip delay").on_hover_text(
2112 "Delay in seconds before showing tooltips after the mouse stops moving",
2113 );
2114 ui.add(
2115 DragValue::new(tooltip_delay)
2116 .range(0.0..=1.0)
2117 .speed(0.05)
2118 .suffix(" s"),
2119 );
2120 ui.end_row();
2121
2122 ui.label("Tooltip grace time").on_hover_text(
2123 "If a tooltip is open and you hover another widget within this grace period, show the next tooltip right away",
2124 );
2125 ui.add(
2126 DragValue::new(tooltip_grace_time)
2127 .range(0.0..=1.0)
2128 .speed(0.05)
2129 .suffix(" s"),
2130 );
2131 ui.end_row();
2132 });
2133
2134 ui.checkbox(
2135 show_tooltips_only_when_still,
2136 "Only show tooltips if mouse is still",
2137 );
2138
2139 ui.horizontal(|ui| {
2140 ui.checkbox(selectable_labels, "Selectable text in labels");
2141 if *selectable_labels {
2142 ui.checkbox(multi_widget_text_select, "Across multiple labels");
2143 }
2144 });
2145
2146 ui.vertical_centered(|ui| reset_button(ui, self, "Reset interaction settings"));
2147 }
2148}
2149
2150impl Widgets {
2151 pub fn ui(&mut self, ui: &mut crate::Ui) {
2152 let Self {
2153 active,
2154 hovered,
2155 inactive,
2156 noninteractive,
2157 open,
2158 } = self;
2159
2160 ui.collapsing("Noninteractive", |ui| {
2161 ui.label(
2162 "The style of a widget that you cannot interact with, e.g. labels and separators.",
2163 );
2164 noninteractive.ui(ui);
2165 });
2166 ui.collapsing("Interactive but inactive", |ui| {
2167 ui.label("The style of an interactive widget, such as a button, at rest.");
2168 inactive.ui(ui);
2169 });
2170 ui.collapsing("Interactive and hovered", |ui| {
2171 ui.label("The style of an interactive widget while you hover it.");
2172 hovered.ui(ui);
2173 });
2174 ui.collapsing("Interactive and active", |ui| {
2175 ui.label("The style of an interactive widget as you are clicking or dragging it.");
2176 active.ui(ui);
2177 });
2178 ui.collapsing("Open menu", |ui| {
2179 ui.label("The style of an open combo-box or menu button");
2180 open.ui(ui);
2181 });
2182
2183 }
2185}
2186
2187impl Selection {
2188 pub fn ui(&mut self, ui: &mut crate::Ui) {
2189 let Self { bg_fill, stroke } = self;
2190 ui.label("Selectable labels");
2191
2192 Grid::new("selectiom").num_columns(2).show(ui, |ui| {
2193 ui.label("Background fill");
2194 ui.color_edit_button_srgba(bg_fill);
2195 ui.end_row();
2196
2197 ui.label("Stroke");
2198 ui.add(stroke);
2199 ui.end_row();
2200 });
2201 }
2202}
2203
2204impl ImeComposition {
2205 pub fn ui(&mut self, ui: &mut crate::Ui) {
2206 let Self {
2207 active_underline_stroke,
2208 inactive_underline_stroke,
2209 legacy_visuals,
2210 } = self;
2211
2212 ui.label("IME composition");
2213
2214 ui.checkbox(legacy_visuals, "Legacy visuals").on_hover_text(
2215 "If enabled, IME composition (preedit) text looks like a text selection \
2216 with the cursor at the end. If disabled, the cursor position and active \
2217 conversion segment are shown.",
2218 );
2219
2220 Grid::new("ime_composition").num_columns(2).show(ui, |ui| {
2221 ui.label("Active underline stroke");
2222 ui.add(active_underline_stroke);
2223 ui.end_row();
2224
2225 ui.label("Inactive underline stroke");
2226 ui.add(inactive_underline_stroke);
2227 ui.end_row();
2228 });
2229 }
2230}
2231
2232impl WidgetVisuals {
2233 pub fn ui(&mut self, ui: &mut crate::Ui) {
2234 let Self {
2235 weak_bg_fill,
2236 bg_fill: mandatory_bg_fill,
2237 bg_stroke,
2238 corner_radius,
2239 fg_stroke,
2240 expansion,
2241 } = self;
2242
2243 Grid::new("widget")
2244 .num_columns(2)
2245 .spacing([12.0, 8.0])
2246 .striped(true)
2247 .show(ui, |ui| {
2248 ui.label("Optional background fill")
2249 .on_hover_text("For buttons, combo-boxes, etc");
2250 ui.color_edit_button_srgba(weak_bg_fill);
2251 ui.end_row();
2252
2253 ui.label("Mandatory background fill")
2254 .on_hover_text("For checkboxes, sliders, etc");
2255 ui.color_edit_button_srgba(mandatory_bg_fill);
2256 ui.end_row();
2257
2258 ui.label("Background stroke");
2259 ui.add(bg_stroke);
2260 ui.end_row();
2261
2262 ui.label("Corner radius");
2263 ui.add(corner_radius);
2264 ui.end_row();
2265
2266 ui.label("Foreground stroke (text)");
2267 ui.add(fg_stroke);
2268 ui.end_row();
2269
2270 ui.label("Expansion")
2271 .on_hover_text("make shapes this much larger");
2272 ui.add(DragValue::new(expansion).speed(0.1));
2273 ui.end_row();
2274 });
2275 }
2276}
2277
2278impl Visuals {
2279 #[expect(deprecated)]
2280 pub fn ui(&mut self, ui: &mut crate::Ui) {
2281 let Self {
2282 dark_mode,
2283 text_options,
2284 override_text_color: _,
2285 weak_text_alpha,
2286 weak_text_color,
2287 widgets,
2288 selection,
2289 ime_composition,
2290 hyperlink_color,
2291 faint_bg_color,
2292 extreme_bg_color,
2293 text_edit_bg_color,
2294 code_bg_color,
2295 warn_fg_color,
2296 error_fg_color,
2297
2298 window_corner_radius,
2299 window_shadow,
2300 window_fill,
2301 window_stroke,
2302 window_highlight_topmost,
2303
2304 menu_corner_radius,
2305
2306 panel_fill,
2307
2308 popup_shadow,
2309
2310 resize_corner_size,
2311
2312 text_cursor,
2313
2314 clip_rect_margin: _,
2315 button_frame,
2316 collapsing_header_frame,
2317 indent_has_left_vline,
2318
2319 striped,
2320
2321 slider_trailing_fill,
2322 handle_shape,
2323 interact_cursor,
2324
2325 image_loading_spinners,
2326
2327 numeric_color_space,
2328 disabled_alpha,
2329 } = self;
2330
2331 fn ui_optional_color(
2332 ui: &mut Ui,
2333 color: &mut Option<Color32>,
2334 default_value: Color32,
2335 label: impl Into<WidgetText>,
2336 ) -> Response {
2337 let label_response = ui.label(label);
2338
2339 ui.horizontal(|ui| {
2340 let mut set = color.is_some();
2341 ui.checkbox(&mut set, "");
2342 if set {
2343 let color = color.get_or_insert(default_value);
2344 ui.color_edit_button_srgba(color);
2345 } else {
2346 *color = None;
2347 }
2348 });
2349
2350 ui.end_row();
2351
2352 label_response
2353 }
2354
2355 ui.collapsing("Background colors", |ui| {
2356 Grid::new("background_colors")
2357 .num_columns(2)
2358 .show(ui, |ui| {
2359 fn ui_color(
2360 ui: &mut Ui,
2361 color: &mut Color32,
2362 label: impl Into<WidgetText>,
2363 ) -> Response {
2364 let label_response = ui.label(label);
2365 ui.color_edit_button_srgba(color);
2366 ui.end_row();
2367 label_response
2368 }
2369
2370 ui_color(ui, &mut widgets.inactive.weak_bg_fill, "Buttons");
2371 ui_color(ui, window_fill, "Windows");
2372 ui_color(ui, panel_fill, "Panels");
2373 ui_color(ui, faint_bg_color, "Faint accent").on_hover_text(
2374 "Used for faint accentuation of interactive things, like striped grids.",
2375 );
2376 ui_color(ui, extreme_bg_color, "Extreme")
2377 .on_hover_text("Background of plots and paintings");
2378
2379 ui_optional_color(ui, text_edit_bg_color, *extreme_bg_color, "TextEdit")
2380 .on_hover_text("Background of TextEdit");
2381 });
2382 });
2383
2384 ui.collapsing("Text rendering", |ui| {
2385 fn ui_text_color(ui: &mut Ui, color: &mut Color32, label: impl Into<RichText>) {
2386 ui.label(label.into().color(*color));
2387 ui.color_edit_button_srgba(color);
2388 ui.end_row();
2389 }
2390
2391 Grid::new("text_color").num_columns(2).show(ui, |ui| {
2392 ui_text_color(ui, &mut widgets.noninteractive.fg_stroke.color, "Label");
2393
2394 ui_text_color(
2395 ui,
2396 &mut widgets.inactive.fg_stroke.color,
2397 "Unhovered button",
2398 );
2399 ui_text_color(ui, &mut widgets.hovered.fg_stroke.color, "Hovered button");
2400 ui_text_color(ui, &mut widgets.active.fg_stroke.color, "Clicked button");
2401
2402 ui_text_color(ui, warn_fg_color, RichText::new("Warnings"));
2403 ui_text_color(ui, error_fg_color, RichText::new("Errors"));
2404
2405 ui_text_color(ui, hyperlink_color, "hyperlink_color");
2406
2407 ui.label(RichText::new("Code background").code())
2408 .on_hover_ui(|ui| {
2409 ui.horizontal(|ui| {
2410 ui.spacing_mut().item_spacing.x = 0.0;
2411 ui.label("For monospaced inlined text ");
2412 ui.code("like this");
2413 ui.label(".");
2414 });
2415 });
2416 ui.color_edit_button_srgba(code_bg_color);
2417 ui.end_row();
2418
2419 ui.label("Weak text alpha");
2420 ui.add_enabled(
2421 weak_text_color.is_none(),
2422 DragValue::new(weak_text_alpha).speed(0.01).range(0.0..=1.0),
2423 );
2424 ui.end_row();
2425
2426 ui_optional_color(
2427 ui,
2428 weak_text_color,
2429 widgets.noninteractive.text_color(),
2430 "Weak text color",
2431 );
2432 });
2433
2434 ui.add_space(4.0);
2435
2436 let TextOptions {
2437 max_texture_side: _,
2438 color_transfer_function,
2439 font_hinting,
2440 subpixel_binning,
2441 } = text_options;
2442
2443 color_transfer_function_ui(ui, color_transfer_function);
2444
2445 ui.checkbox(font_hinting, "Font hinting (sharper text)");
2446 ui.checkbox(subpixel_binning, "Sub-pixel binning (more even kerning)");
2447 });
2448
2449 ui.collapsing("Text cursor", |ui| {
2450 text_cursor.ui(ui);
2451 });
2452
2453 ui.collapsing("Window", |ui| {
2454 Grid::new("window")
2455 .num_columns(2)
2456 .spacing([12.0, 8.0])
2457 .striped(true)
2458 .show(ui, |ui| {
2459 ui.label("Fill");
2460 ui.color_edit_button_srgba(window_fill);
2461 ui.end_row();
2462
2463 ui.label("Stroke");
2464 ui.add(window_stroke);
2465 ui.end_row();
2466
2467 ui.label("Corner radius");
2468 ui.add(window_corner_radius);
2469 ui.end_row();
2470
2471 ui.label("Shadow");
2472 ui.add(window_shadow);
2473 ui.end_row();
2474 });
2475
2476 ui.checkbox(window_highlight_topmost, "Highlight topmost Window");
2477 });
2478
2479 ui.collapsing("Menus and popups", |ui| {
2480 Grid::new("menus_and_popups")
2481 .num_columns(2)
2482 .spacing([12.0, 8.0])
2483 .striped(true)
2484 .show(ui, |ui| {
2485 ui.label("Corner radius");
2486 ui.add(menu_corner_radius);
2487 ui.end_row();
2488
2489 ui.label("Shadow");
2490 ui.add(popup_shadow);
2491 ui.end_row();
2492 });
2493 });
2494
2495 ui.collapsing("Widgets", |ui| widgets.ui(ui));
2496 ui.collapsing("Selection", |ui| selection.ui(ui));
2497 ui.collapsing("IME composition", |ui| ime_composition.ui(ui));
2498
2499 ui.collapsing("Misc", |ui| {
2500 ui.add(Slider::new(resize_corner_size, 0.0..=20.0).text("resize_corner_size"));
2501 ui.checkbox(button_frame, "Button has a frame");
2502 ui.checkbox(collapsing_header_frame, "Collapsing header has a frame");
2503 ui.checkbox(
2504 indent_has_left_vline,
2505 "Paint a vertical line to the left of indented regions",
2506 );
2507
2508 ui.checkbox(striped, "Default stripes on grids and tables");
2509
2510 ui.checkbox(slider_trailing_fill, "Add trailing color to sliders");
2511
2512 handle_shape.ui(ui);
2513
2514 ComboBox::from_label("Interact cursor")
2515 .selected_text(
2516 interact_cursor.map_or_else(|| "-".to_owned(), |cursor| format!("{cursor:?}")),
2517 )
2518 .show_ui(ui, |ui| {
2519 ui.selectable_value(interact_cursor, None, "-");
2520
2521 for cursor in CursorIcon::ALL {
2522 ui.selectable_value(interact_cursor, Some(cursor), format!("{cursor:?}"))
2523 .on_hover_cursor(cursor);
2524 }
2525 })
2526 .response
2527 .on_hover_text("Use this cursor when hovering buttons etc");
2528
2529 ui.checkbox(image_loading_spinners, "Image loading spinners")
2530 .on_hover_text("Show a spinner when an Image is loading");
2531
2532 ui.horizontal(|ui| {
2533 ui.label("Color picker type");
2534 numeric_color_space.toggle_button_ui(ui);
2535 });
2536
2537 ui.add(Slider::new(disabled_alpha, 0.0..=1.0).text("Disabled element alpha"));
2538 });
2539
2540 let dark_mode = *dark_mode;
2541 ui.vertical_centered(|ui| {
2542 reset_button_with(
2543 ui,
2544 self,
2545 "Reset visuals",
2546 if dark_mode {
2547 Self::dark()
2548 } else {
2549 Self::light()
2550 },
2551 );
2552 });
2553 }
2554}
2555
2556fn color_transfer_function_ui(
2557 ui: &mut Ui,
2558 color_transfer_function: &mut FontColorTransferFunction,
2559) {
2560 ui.horizontal(|ui| {
2561 ui.label("Opacity tweaking:");
2562
2563 ui.radio_value(
2564 color_transfer_function,
2565 FontColorTransferFunction::Off,
2566 "Off",
2567 );
2568 ui.radio_value(
2569 color_transfer_function,
2570 FontColorTransferFunction::DARK_MODE_DEFAULT,
2571 "Dark-mode special",
2572 );
2573
2574 let mut use_gamma = matches!(color_transfer_function, FontColorTransferFunction::Gamma(_));
2575 ui.radio_value(&mut use_gamma, true, "Gamma function");
2576
2577 if use_gamma {
2578 let mut gamma = color_transfer_function.to_gamma();
2579
2580 ui.add(
2581 DragValue::new(&mut gamma)
2582 .speed(0.01)
2583 .range(0.1..=4.0)
2584 .prefix("Gamma: "),
2585 );
2586
2587 *color_transfer_function = FontColorTransferFunction::Gamma(gamma);
2588 }
2589 });
2590}
2591
2592impl TextCursorStyle {
2593 fn ui(&mut self, ui: &mut Ui) {
2594 let Self {
2595 stroke,
2596 preview,
2597 blink,
2598 on_duration,
2599 off_duration,
2600 } = self;
2601
2602 ui.horizontal(|ui| {
2603 ui.label("Stroke");
2604 ui.add(stroke);
2605 });
2606
2607 ui.checkbox(preview, "Preview text cursor on hover");
2608
2609 ui.checkbox(blink, "Blink");
2610
2611 if *blink {
2612 Grid::new("cursor_blink").show(ui, |ui| {
2613 ui.label("On time");
2614 ui.add(
2615 DragValue::new(on_duration)
2616 .speed(0.1)
2617 .range(0.0..=2.0)
2618 .suffix(" s"),
2619 );
2620 ui.end_row();
2621
2622 ui.label("Off time");
2623 ui.add(
2624 DragValue::new(off_duration)
2625 .speed(0.1)
2626 .range(0.0..=2.0)
2627 .suffix(" s"),
2628 );
2629 ui.end_row();
2630 });
2631 }
2632 }
2633}
2634
2635#[cfg(debug_assertions)]
2636impl DebugOptions {
2637 pub fn ui(&mut self, ui: &mut crate::Ui) {
2638 let Self {
2639 debug_on_hover,
2640 debug_on_hover_with_all_modifiers,
2641 hover_shows_next,
2642 show_expand_width,
2643 show_expand_height,
2644 show_resize,
2645 show_interactive_widgets,
2646 show_widget_hits,
2647 warn_if_rect_changes_id,
2648 show_unaligned,
2649 show_focused_widget,
2650 } = self;
2651
2652 {
2653 ui.checkbox(debug_on_hover, "Show widget info on hover.");
2654 ui.checkbox(
2655 debug_on_hover_with_all_modifiers,
2656 "Show widget info on hover if holding all modifier keys",
2657 );
2658
2659 ui.checkbox(hover_shows_next, "Show next widget placement on hover");
2660 }
2661
2662 ui.checkbox(
2663 show_expand_width,
2664 "Show which widgets make their parent wider",
2665 );
2666 ui.checkbox(
2667 show_expand_height,
2668 "Show which widgets make their parent higher",
2669 );
2670 ui.checkbox(show_resize, "Debug Resize");
2671
2672 ui.checkbox(
2673 show_interactive_widgets,
2674 "Show an overlay on all interactive widgets",
2675 );
2676
2677 ui.checkbox(show_widget_hits, "Show widgets under mouse pointer");
2678
2679 ui.checkbox(
2680 warn_if_rect_changes_id,
2681 "Warn if a Rect changes Id between frames",
2682 );
2683
2684 ui.checkbox(
2685 show_unaligned,
2686 "Show rectangles not aligned to integer point coordinates",
2687 );
2688
2689 ui.checkbox(
2690 show_focused_widget,
2691 "Highlight which widget has keyboard focus",
2692 );
2693
2694 ui.vertical_centered(|ui| reset_button(ui, self, "Reset debug options"));
2695 }
2696}
2697
2698fn two_drag_values(value: &mut Vec2, range: core::ops::RangeInclusive<f32>) -> impl Widget + '_ {
2700 move |ui: &mut crate::Ui| {
2701 ui.horizontal(|ui| {
2702 ui.add(
2703 DragValue::new(&mut value.x)
2704 .range(range.clone())
2705 .prefix("x: "),
2706 );
2707 ui.add(
2708 DragValue::new(&mut value.y)
2709 .range(range.clone())
2710 .prefix("y: "),
2711 );
2712 })
2713 .response
2714 }
2715}
2716
2717impl HandleShape {
2718 pub fn ui(&mut self, ui: &mut Ui) {
2719 ui.horizontal(|ui| {
2720 ui.label("Slider handle");
2721 ui.radio_value(self, Self::Circle, "Circle");
2722 if ui
2723 .radio(matches!(self, Self::Rect { .. }), "Rectangle")
2724 .clicked()
2725 {
2726 *self = Self::Rect { aspect_ratio: 0.5 };
2727 }
2728 if let Self::Rect { aspect_ratio } = self {
2729 ui.add(Slider::new(aspect_ratio, 0.1..=3.0).text("Aspect ratio"));
2730 }
2731 });
2732 }
2733}
2734
2735#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2737#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
2738pub enum NumericColorSpace {
2739 GammaByte,
2743
2744 Linear,
2746 }
2748
2749impl NumericColorSpace {
2750 pub fn toggle_button_ui(&mut self, ui: &mut Ui) -> crate::Response {
2751 let tooltip = match self {
2752 Self::GammaByte => "Showing color values in 0-255 gamma space",
2753 Self::Linear => "Showing color values in 0-1 linear space",
2754 };
2755
2756 let mut response = ui.button(self.to_string()).on_hover_text(tooltip);
2757 if response.clicked() {
2758 *self = match self {
2759 Self::GammaByte => Self::Linear,
2760 Self::Linear => Self::GammaByte,
2761 };
2762 response.mark_changed();
2763 }
2764 response
2765 }
2766}
2767
2768impl core::fmt::Display for NumericColorSpace {
2769 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
2770 match self {
2771 Self::GammaByte => write!(f, "U8"),
2772 Self::Linear => write!(f, "F"),
2773 }
2774 }
2775}
2776
2777impl Widget for &mut Margin {
2778 fn ui(self, ui: &mut Ui) -> Response {
2779 let mut same = self.is_same();
2780
2781 let response = if same {
2782 ui.horizontal(|ui| {
2783 ui.checkbox(&mut same, "same");
2784
2785 let mut value = self.left;
2786 ui.add(DragValue::new(&mut value).range(0.0..=100.0));
2787 *self = Margin::same(value);
2788 })
2789 .response
2790 } else {
2791 ui.vertical(|ui| {
2792 ui.checkbox(&mut same, "same");
2793
2794 crate::Grid::new("margin").num_columns(2).show(ui, |ui| {
2795 ui.label("Left");
2796 ui.add(DragValue::new(&mut self.left).range(0.0..=100.0));
2797 ui.end_row();
2798
2799 ui.label("Right");
2800 ui.add(DragValue::new(&mut self.right).range(0.0..=100.0));
2801 ui.end_row();
2802
2803 ui.label("Top");
2804 ui.add(DragValue::new(&mut self.top).range(0.0..=100.0));
2805 ui.end_row();
2806
2807 ui.label("Bottom");
2808 ui.add(DragValue::new(&mut self.bottom).range(0.0..=100.0));
2809 ui.end_row();
2810 });
2811 })
2812 .response
2813 };
2814
2815 if same {
2817 *self =
2818 Margin::from((self.leftf() + self.rightf() + self.topf() + self.bottomf()) / 4.0);
2819 } else {
2820 if self.is_same() {
2822 if self.right == i8::MAX {
2823 self.right = i8::MAX - 1;
2824 } else {
2825 self.right += 1;
2826 }
2827 }
2828 }
2829
2830 response
2831 }
2832}
2833
2834impl Widget for &mut CornerRadius {
2835 fn ui(self, ui: &mut Ui) -> Response {
2836 let mut same = self.is_same();
2837
2838 let response = if same {
2839 ui.horizontal(|ui| {
2840 ui.checkbox(&mut same, "same");
2841
2842 let mut cr = self.nw;
2843 ui.add(DragValue::new(&mut cr).range(0.0..=f32::INFINITY));
2844 *self = CornerRadius::same(cr);
2845 })
2846 .response
2847 } else {
2848 ui.vertical(|ui| {
2849 ui.checkbox(&mut same, "same");
2850
2851 crate::Grid::new("Corner radius")
2852 .num_columns(2)
2853 .show(ui, |ui| {
2854 ui.label("NW");
2855 ui.add(DragValue::new(&mut self.nw).range(0.0..=f32::INFINITY));
2856 ui.end_row();
2857
2858 ui.label("NE");
2859 ui.add(DragValue::new(&mut self.ne).range(0.0..=f32::INFINITY));
2860 ui.end_row();
2861
2862 ui.label("SW");
2863 ui.add(DragValue::new(&mut self.sw).range(0.0..=f32::INFINITY));
2864 ui.end_row();
2865
2866 ui.label("SE");
2867 ui.add(DragValue::new(&mut self.se).range(0.0..=f32::INFINITY));
2868 ui.end_row();
2869 });
2870 })
2871 .response
2872 };
2873
2874 if same {
2876 *self = CornerRadius::from(self.average());
2877 } else {
2878 if self.is_same() {
2880 if self.average() == 0.0 {
2881 self.se = 1;
2882 } else {
2883 self.se -= 1;
2884 }
2885 }
2886 }
2887
2888 response
2889 }
2890}
2891
2892impl Widget for &mut Shadow {
2893 fn ui(self, ui: &mut Ui) -> Response {
2894 let epaint::Shadow {
2895 offset,
2896 blur,
2897 spread,
2898 color,
2899 } = self;
2900
2901 ui.vertical(|ui| {
2902 crate::Grid::new("shadow_ui").show(ui, |ui| {
2903 ui.add(
2904 DragValue::new(&mut offset[0])
2905 .speed(1.0)
2906 .range(-100.0..=100.0)
2907 .prefix("x: "),
2908 );
2909 ui.add(
2910 DragValue::new(&mut offset[1])
2911 .speed(1.0)
2912 .range(-100.0..=100.0)
2913 .prefix("y: "),
2914 );
2915 ui.end_row();
2916
2917 ui.add(
2918 DragValue::new(blur)
2919 .speed(1.0)
2920 .range(0.0..=100.0)
2921 .prefix("blur: "),
2922 );
2923
2924 ui.add(
2925 DragValue::new(spread)
2926 .speed(1.0)
2927 .range(0.0..=100.0)
2928 .prefix("spread: "),
2929 );
2930 });
2931 ui.color_edit_button_srgba(color);
2932 })
2933 .response
2934 }
2935}
2936
2937impl Widget for &mut Stroke {
2938 fn ui(self, ui: &mut Ui) -> Response {
2939 let Stroke { width, color } = self;
2940
2941 ui.horizontal(|ui| {
2942 ui.add(DragValue::new(width).speed(0.1).range(0.0..=1e9))
2943 .on_hover_text("Width");
2944 ui.color_edit_button_srgba(color);
2945
2946 let (_id, stroke_rect) = ui.allocate_space(ui.spacing().interact_size);
2948 let left = stroke_rect.left_center();
2949 let right = stroke_rect.right_center();
2950 ui.painter().line_segment([left, right], (*width, *color));
2951 })
2952 .response
2953 }
2954}
2955
2956impl Widget for &mut crate::Frame {
2957 fn ui(self, ui: &mut Ui) -> Response {
2958 let crate::Frame {
2959 inner_margin,
2960 outer_margin,
2961 corner_radius,
2962 shadow,
2963 fill,
2964 stroke,
2965 } = self;
2966
2967 crate::Grid::new("frame")
2968 .num_columns(2)
2969 .spacing([12.0, 8.0])
2970 .striped(true)
2971 .show(ui, |ui| {
2972 ui.label("Inner margin");
2973 ui.add(inner_margin);
2974 ui.end_row();
2975
2976 ui.label("Outer margin");
2977 ui.push_id("outer", |ui| ui.add(outer_margin));
2979 ui.end_row();
2980
2981 ui.label("Corner radius");
2982 ui.add(corner_radius);
2983 ui.end_row();
2984
2985 ui.label("Shadow");
2986 ui.add(shadow);
2987 ui.end_row();
2988
2989 ui.label("Fill");
2990 ui.color_edit_button_srgba(fill);
2991 ui.end_row();
2992
2993 ui.label("Stroke");
2994 ui.add(stroke);
2995 ui.end_row();
2996 })
2997 .response
2998 }
2999}
3000
3001pub fn font_tweak_ui(ui: &mut Ui, tweak: &mut FontTweak, axes: &[FontVariationAxis]) -> Response {
3012 let original: FontTweak = tweak.clone();
3013
3014 let mut response = Grid::new("font_tweak")
3015 .num_columns(2)
3016 .show(ui, |ui| {
3017 let FontTweak {
3018 scale,
3019 y_offset_factor,
3020 y_offset,
3021 hinting,
3022 hinting_target,
3023 coords,
3024 thin_space_width,
3025 tab_size,
3026 subpixel_binning,
3027 } = tweak;
3028
3029 ui.label("Scale");
3030 let speed = *scale * 0.01;
3031 ui.add(DragValue::new(scale).range(0.01..=10.0).speed(speed));
3032 ui.end_row();
3033
3034 ui.label("y_offset_factor");
3035 ui.add(DragValue::new(y_offset_factor).speed(-0.0025));
3036 ui.end_row();
3037
3038 ui.label("y_offset");
3039 ui.add(DragValue::new(y_offset).speed(-0.02));
3040 ui.end_row();
3041
3042 ui.label("hinting");
3043 ui.horizontal(|ui| {
3044 ui.radio_value(hinting, Some(true), "on");
3045 ui.radio_value(hinting, Some(false), "off");
3046 ui.radio_value(hinting, None, "default");
3047 });
3048 ui.end_row();
3049
3050 ui.label("hinting_target")
3051 .on_hover_text("How aggressively to snap glyph outlines to the pixel grid. Only matters when hinting is enabled.");
3052 ui.vertical(|ui| {
3053 ui.horizontal(|ui| {
3054 let is_mono = matches!(hinting_target, HintingTarget::Mono);
3055 if ui
3056 .radio(!is_mono, "Smooth")
3057 .on_hover_text("Hinting tuned for anti-aliased rendering. The normal choice.")
3058 .clicked()
3059 && is_mono
3060 {
3061 *hinting_target = HintingTarget::default();
3062 }
3063 if ui
3064 .radio(is_mono, "Mono")
3065 .on_hover_text(
3066 "Strongest hinting (designed for 1-bit rendering). Sharpest, but \
3067 distorts glyph weight across sizes.",
3068 )
3069 .clicked()
3070 {
3071 *hinting_target = HintingTarget::Mono;
3072 }
3073 if ui
3074 .button("Reset")
3075 .on_hover_text("Reset the hinting target to its default.")
3076 .clicked()
3077 {
3078 *hinting_target = HintingTarget::default();
3079 }
3080 });
3081 if let HintingTarget::Smooth(SmoothHinting {
3082 light,
3083 symmetric_rendering,
3084 preserve_linear_metrics,
3085 }) = hinting_target
3086 {
3087 ui.checkbox(light, "light").on_hover_text(
3088 "Hint only vertically, preserving the font's horizontal proportions \
3089 (softer). Off also fits horizontally.",
3090 );
3091 ui.checkbox(symmetric_rendering, "symmetric_rendering").on_hover_text(
3092 "Render glyphs the same regardless of sub-pixel position (good for \
3093 caching/animation), but can blur stems. Only affects interpreter-hinted fonts.",
3094 );
3095 ui.checkbox(preserve_linear_metrics, "preserve_linear_metrics").on_hover_text(
3096 "Keep spacing independent of hinting. Off lets the hinter snap \
3097 horizontally for crisper vertical stems on low-dpi screens.",
3098 );
3099 }
3100 });
3101 ui.end_row();
3102
3103 ui.label("subpixel_binning");
3104 ui.horizontal(|ui| {
3105 ui.radio_value(subpixel_binning, Some(true), "on");
3106 ui.radio_value(subpixel_binning, Some(false), "off");
3107 ui.radio_value(subpixel_binning, None, "default");
3108 });
3109 ui.end_row();
3110
3111 ui.label("thin_space_width");
3112 ui.horizontal(|ui| {
3113 ui.add(
3114 DragValue::new(thin_space_width)
3115 .range(0.0..=1.0)
3116 .speed(0.01),
3117 );
3118 ui.label("1\u{2009}234\u{2009}567\u{2009}890");
3119 });
3120 ui.end_row();
3121
3122 ui.label("tab_size");
3123 ui.add(DragValue::new(tab_size).range(0.0..=16.0).speed(0.1));
3124 ui.end_row();
3125
3126 for axis in axes.iter().filter(|axis| !axis.hidden) {
3128 match &axis.name {
3129 Some(name) => ui.label(format!("{name} ({})", axis.tag)),
3130 None => ui.label(axis.tag.to_string()),
3131 };
3132
3133 let existing = coords.as_ref().iter().position(|(tag, _)| *tag == axis.tag);
3134 let mut value = existing.map_or(axis.default, |i| coords.as_ref()[i].1);
3135
3136 ui.horizontal(|ui| {
3137 if ui.add(Slider::new(&mut value, axis.range)).changed() {
3138 match existing {
3139 Some(i) => coords.as_mut()[i].1 = value,
3140 None => coords.push(axis.tag, value),
3141 }
3142 }
3143 if existing.is_some()
3145 && ui
3146 .small_button("⟲")
3147 .on_hover_text("Reset to the font's default value")
3148 .clicked()
3149 && let Some(i) =
3150 coords.as_ref().iter().position(|(tag, _)| *tag == axis.tag)
3151 {
3152 coords.remove(i);
3153 }
3154 });
3155 ui.end_row();
3156 }
3157
3158 if ui.button("Reset").clicked() {
3159 *tweak = Default::default();
3160 }
3161 })
3162 .response;
3163
3164 if *tweak != original {
3165 response.mark_changed();
3166 }
3167
3168 response
3169}
3170
3171impl Widget for &mut FontTweak {
3172 fn ui(self, ui: &mut Ui) -> Response {
3173 font_tweak_ui(ui, self, &[])
3174 }
3175}