//! egui theme (spacing, colors, etc). #![allow(clippy::if_same_then_else)] use crate::{color::*, emath::*, FontFamily, FontId, Response, RichText, WidgetText}; use epaint::{Rounding, Shadow, Stroke}; use std::collections::BTreeMap; // ---------------------------------------------------------------------------- /// Alias for a [`FontId`] (font of a certain size). /// /// The font is found via look-up in [`Style::text_styles`]. /// You can use [`TextStyle::resolve`] to do this lookup. #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub enum TextStyle { /// Used when small text is needed. Small, /// Normal labels. Easily readable, doesn't take up too much space. Body, /// Same size as [`Self::Body`], but used when monospace is important (for code snippets, aligning numbers, etc). Monospace, /// Buttons. Maybe slightly bigger than [`Self::Body`]. /// /// Signifies that he item can be interacted with. Button, /// Heading. Probably larger than [`Self::Body`]. Heading, /// A user-chosen style, found in [`Style::text_styles`]. /// ``` /// egui::TextStyle::Name("footing".into()); /// ```` Name(std::sync::Arc), } impl std::fmt::Display for TextStyle { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Small => "Small".fmt(f), Self::Body => "Body".fmt(f), Self::Monospace => "Monospace".fmt(f), Self::Button => "Button".fmt(f), Self::Heading => "Heading".fmt(f), Self::Name(name) => (*name).fmt(f), } } } impl TextStyle { /// Look up this [`TextStyle`] in [`Style::text_styles`]. pub fn resolve(&self, style: &Style) -> FontId { style.text_styles.get(self).cloned().unwrap_or_else(|| { panic!( "Failed to find {:?} in Style::text_styles. Available styles:\n{:#?}", self, style.text_styles() ) }) } } // ---------------------------------------------------------------------------- /// A way to select [`FontId`], either by picking one directly or by using a [`TextStyle`]. pub enum FontSelection { /// Default text style - will use [`TextStyle::Body`], unless /// [`Style::override_font_id`] or [`Style::override_text_style`] is set. Default, /// Directly select size and font family FontId(FontId), /// Use a [`TextStyle`] to look up the [`FontId`] in [`Style::text_styles`]. Style(TextStyle), } impl Default for FontSelection { #[inline] fn default() -> Self { Self::Default } } impl FontSelection { pub fn resolve(self, style: &Style) -> FontId { match self { Self::Default => { if let Some(override_font_id) = &style.override_font_id { override_font_id.clone() } else if let Some(text_style) = &style.override_text_style { text_style.resolve(style) } else { TextStyle::Body.resolve(style) } } Self::FontId(font_id) => font_id, Self::Style(text_style) => text_style.resolve(style), } } } impl From for FontSelection { #[inline(always)] fn from(font_id: FontId) -> Self { Self::FontId(font_id) } } impl From for FontSelection { #[inline(always)] fn from(text_style: TextStyle) -> Self { Self::Style(text_style) } } // ---------------------------------------------------------------------------- /// Specifies the look and feel of egui. /// /// You can change the visuals of a [`Ui`] with [`Ui::style_mut`] /// and of everything with [`crate::Context::set_style`]. /// /// If you want to change fonts, use [`crate::Context::set_fonts`] instead. #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] pub struct Style { /// If set this will change the default [`TextStyle`] for all widgets. /// /// On most widgets you can also set an explicit text style, /// which will take precedence over this. pub override_text_style: Option, /// If set this will change the font family and size for all widgets. /// /// On most widgets you can also set an explicit text style, /// which will take precedence over this. pub override_font_id: Option, /// The [`FontFamily`] and size you want to use for a specific [`TextStyle`]. /// /// The most convenient way to look something up in this is to use [`TextStyle::resolve`]. /// /// If you would like to overwrite app text_styles /// /// ``` /// # let mut ctx = egui::Context::default(); /// use egui::FontFamily::Proportional; /// use egui::FontId; /// use egui::TextStyle::*; /// /// // Get current context style /// let mut style = (*ctx.style()).clone(); /// /// // Redefine text_styles /// style.text_styles = [ /// (Heading, FontId::new(30.0, Proportional)), /// (Name("Heading2".into()), FontId::new(25.0, Proportional)), /// (Name("Context".into()), FontId::new(23.0, Proportional)), /// (Body, FontId::new(18.0, Proportional)), /// (Monospace, FontId::new(14.0, Proportional)), /// (Button, FontId::new(14.0, Proportional)), /// (Small, FontId::new(10.0, Proportional)), /// ].into(); /// /// // Mutate global style with above changes /// ctx.set_style(style); /// ``` pub text_styles: BTreeMap, /// If set, labels buttons wtc will use this to determine whether or not /// to wrap the text at the right edge of the [`Ui`] they are in. /// By default this is `None`. /// /// * `None`: follow layout /// * `Some(true)`: default on /// * `Some(false)`: default off pub wrap: Option, /// Sizes and distances between widgets pub spacing: Spacing, /// How and when interaction happens. pub interaction: Interaction, /// Colors etc. pub visuals: Visuals, /// How many seconds a typical animation should last. pub animation_time: f32, /// Options to help debug why egui behaves strangely. pub debug: DebugOptions, /// Show tooltips explaining [`DragValue`]:s etc when hovered. /// /// This only affects a few egui widgets. pub explanation_tooltips: bool, } impl Style { // TODO(emilk): rename style.interact() to maybe... `style.interactive` ? /// Use this style for interactive things. /// Note that you must already have a response, /// i.e. you must allocate space and interact BEFORE painting the widget! pub fn interact(&self, response: &Response) -> &WidgetVisuals { self.visuals.widgets.style(response) } pub fn interact_selectable(&self, response: &Response, selected: bool) -> WidgetVisuals { let mut visuals = *self.visuals.widgets.style(response); if selected { visuals.bg_fill = self.visuals.selection.bg_fill; // visuals.bg_stroke = self.visuals.selection.stroke; visuals.fg_stroke = self.visuals.selection.stroke; } visuals } /// Style to use for non-interactive widgets. pub fn noninteractive(&self) -> &WidgetVisuals { &self.visuals.widgets.noninteractive } /// All known text styles. pub fn text_styles(&self) -> Vec { self.text_styles.keys().cloned().collect() } } /// Controls the sizes and distances between widgets. #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] pub struct Spacing { /// Horizontal and vertical spacing between widgets. /// /// To add extra space between widgets, use [`Ui::add_space`]. /// /// `item_spacing` is inserted _after_ adding a widget, so to increase the spacing between /// widgets `A` and `B` you need to change `item_spacing` before adding `A`. pub item_spacing: Vec2, /// Horizontal and vertical margins within a window frame. pub window_margin: Margin, /// Button size is text size plus this on each side pub button_padding: Vec2, /// Horizontal and vertical margins within a menu frame. pub menu_margin: Margin, /// Indent collapsing regions etc by this much. pub indent: f32, /// Minimum size of a [`DragValue`], color picker button, and other small widgets. /// `interact_size.y` is the default height of button, slider, etc. /// Anything clickable should be (at least) this size. pub interact_size: Vec2, // TODO(emilk): rename min_interact_size ? /// Default width of a [`Slider`] and [`ComboBox`](crate::ComboBox). pub slider_width: f32, // TODO(emilk): rename big_interact_size ? /// Default width of a [`TextEdit`]. pub text_edit_width: f32, /// Checkboxes, radio button and collapsing headers have an icon at the start. /// This is the width/height of the outer part of this icon (e.g. the BOX of the checkbox). pub icon_width: f32, /// Checkboxes, radio button and collapsing headers have an icon at the start. /// This is the width/height of the inner part of this icon (e.g. the check of the checkbox). pub icon_width_inner: f32, /// Checkboxes, radio button and collapsing headers have an icon at the start. /// This is the spacing between the icon and the text pub icon_spacing: f32, /// Width of a tooltip (`on_hover_ui`, `on_hover_text` etc). pub tooltip_width: f32, /// End indented regions with a horizontal line pub indent_ends_with_horizontal_line: bool, /// Height of a combo-box before showing scroll bars. pub combo_height: f32, pub scroll_bar_width: f32, } impl Spacing { /// Returns small icon rectangle and big icon rectangle pub fn icon_rectangles(&self, rect: Rect) -> (Rect, Rect) { let icon_width = self.icon_width; let big_icon_rect = Rect::from_center_size( pos2(rect.left() + icon_width / 2.0, rect.center().y), vec2(icon_width, icon_width), ); let small_icon_rect = Rect::from_center_size(big_icon_rect.center(), Vec2::splat(self.icon_width_inner)); (small_icon_rect, big_icon_rect) } } // ---------------------------------------------------------------------------- #[derive(Clone, Copy, Debug, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct Margin { pub left: f32, pub right: f32, pub top: f32, pub bottom: f32, } impl Margin { #[inline] pub fn same(margin: f32) -> Self { Self { left: margin, right: margin, top: margin, bottom: margin, } } /// Margins with the same size on opposing sides #[inline] pub fn symmetric(x: f32, y: f32) -> Self { Self { left: x, right: x, top: y, bottom: y, } } /// Total margins on both sides pub fn sum(&self) -> Vec2 { vec2(self.left + self.right, self.top + self.bottom) } pub fn left_top(&self) -> Vec2 { vec2(self.left, self.top) } pub fn right_bottom(&self) -> Vec2 { vec2(self.right, self.bottom) } } impl From for Margin { fn from(v: f32) -> Self { Self::same(v) } } impl From for Margin { fn from(v: Vec2) -> Self { Self::symmetric(v.x, v.y) } } impl std::ops::Add for Margin { type Output = Self; fn add(self, other: Self) -> Self { Self { left: self.left + other.left, right: self.right + other.right, top: self.top + other.top, bottom: self.bottom + other.bottom, } } } // ---------------------------------------------------------------------------- /// How and when interaction happens. #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] pub struct Interaction { /// Mouse must be this close to the side of a window to resize pub resize_grab_radius_side: f32, /// Mouse must be this close to the corner of a window to resize pub resize_grab_radius_corner: f32, /// If `false`, tooltips will show up anytime you hover anything, even is mouse is still moving pub show_tooltips_only_when_still: bool, } /// Controls the visual style (colors etc) of egui. /// /// You can change the visuals of a [`Ui`] with [`Ui::visuals_mut`] /// and of everything with [`crate::Context::set_visuals`]. /// /// If you want to change fonts, use [`crate::Context::set_fonts`] instead. #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] pub struct Visuals { /// If true, the visuals are overall dark with light text. /// If false, the visuals are overall light with dark text. /// /// NOTE: setting this does very little by itself, /// this is more to provide a convenient summary of the rest of the settings. pub dark_mode: bool, /// Override default text color for all text. /// /// This is great for setting the color of text for any widget. /// /// If `text_color` is `None` (default), then the text color will be the same as the /// foreground stroke color (`WidgetVisuals::fg_stroke`) /// and will depend on whether or not the widget is being interacted with. /// /// In the future we may instead modulate /// the `text_color` based on whether or not it is interacted with /// so that `visuals.text_color` is always used, /// but its alpha may be different based on whether or not /// it is disabled, non-interactive, hovered etc. pub override_text_color: Option, /// Visual styles of widgets pub widgets: Widgets, pub selection: Selection, /// The color used for [`Hyperlink`], pub hyperlink_color: Color32, /// Something just barely different from the background color. /// Used for [`crate::Grid::striped`]. pub faint_bg_color: Color32, /// Very dark or light color (for corresponding theme). /// Used as the background of text edits, scroll bars and others things /// that needs to look different from other interactive stuff. pub extreme_bg_color: Color32, /// Background color behind code-styled monospaced labels. pub code_bg_color: Color32, /// A good color for warning text (e.g. orange). pub warn_fg_color: Color32, /// A good color for error text (e.g. red). pub error_fg_color: Color32, pub window_rounding: Rounding, pub window_shadow: Shadow, pub popup_shadow: Shadow, pub resize_corner_size: f32, pub text_cursor_width: f32, /// show where the text cursor would be if you clicked pub text_cursor_preview: bool, /// Allow child widgets to be just on the border and still have a stroke with some thickness pub clip_rect_margin: f32, /// Show a background behind buttons. pub button_frame: bool, /// Show a background behind collapsing headers. pub collapsing_header_frame: bool, } impl Visuals { #[inline(always)] pub fn noninteractive(&self) -> &WidgetVisuals { &self.widgets.noninteractive } pub fn text_color(&self) -> Color32 { self.override_text_color .unwrap_or_else(|| self.widgets.noninteractive.text_color()) } pub fn weak_text_color(&self) -> Color32 { crate::color::tint_color_towards(self.text_color(), self.window_fill()) } #[inline(always)] pub fn strong_text_color(&self) -> Color32 { self.widgets.active.text_color() } /// Window background color. #[inline(always)] pub fn window_fill(&self) -> Color32 { self.widgets.noninteractive.bg_fill } #[inline(always)] pub fn window_stroke(&self) -> Stroke { self.widgets.noninteractive.bg_stroke } } /// Selected text, selected elements etc #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] pub struct Selection { pub bg_fill: Color32, pub stroke: Stroke, } /// The visuals of widgets for different states of interaction. #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] pub struct Widgets { /// The style of a widget that you cannot interact with. /// * `noninteractive.bg_stroke` is the outline of windows. /// * `noninteractive.bg_fill` is the background color of windows. /// * `noninteractive.fg_stroke` is the normal text color. pub noninteractive: WidgetVisuals, /// The style of an interactive widget, such as a button, at rest. pub inactive: WidgetVisuals, /// The style of an interactive widget while you hover it. pub hovered: WidgetVisuals, /// The style of an interactive widget as you are clicking or dragging it. pub active: WidgetVisuals, /// The style of a button that has an open menu beneath it (e.g. a combo-box) pub open: WidgetVisuals, } impl Widgets { pub fn style(&self, response: &Response) -> &WidgetVisuals { if !response.sense.interactive() { &self.noninteractive } else if response.is_pointer_button_down_on() || response.has_focus() { &self.active } else if response.hovered() { &self.hovered } else { &self.inactive } } } /// bg = background, fg = foreground. #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct WidgetVisuals { /// Background color of widget. pub bg_fill: Color32, /// For surrounding rectangle of things that need it, /// like buttons, the box of the checkbox, etc. /// Should maybe be called `frame_stroke`. pub bg_stroke: Stroke, /// Button frames etc. pub rounding: Rounding, /// Stroke and text color of the interactive part of a component (button text, slider grab, check-mark, …). pub fg_stroke: Stroke, /// Make the frame this much larger. pub expansion: f32, } impl WidgetVisuals { #[inline(always)] pub fn text_color(&self) -> Color32 { self.fg_stroke.color } } /// Options for help debug egui by adding extra visualization #[derive(Clone, Copy, Debug, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct DebugOptions { /// However over widgets to see their rectangles pub debug_on_hover: bool, /// Show which widgets make their parent wider pub show_expand_width: bool, /// Show which widgets make their parent higher pub show_expand_height: bool, pub show_resize: bool, } // ---------------------------------------------------------------------------- /// The default text styles of the default egui theme. pub fn default_text_styles() -> BTreeMap { use FontFamily::{Monospace, Proportional}; [ (TextStyle::Small, FontId::new(9.0, Proportional)), (TextStyle::Body, FontId::new(12.5, Proportional)), (TextStyle::Button, FontId::new(12.5, Proportional)), (TextStyle::Heading, FontId::new(18.0, Proportional)), (TextStyle::Monospace, FontId::new(12.0, Monospace)), ] .into() } impl Default for Style { fn default() -> Self { Self { override_font_id: None, override_text_style: None, text_styles: default_text_styles(), wrap: None, spacing: Spacing::default(), interaction: Interaction::default(), visuals: Visuals::default(), animation_time: 1.0 / 12.0, debug: Default::default(), explanation_tooltips: false, } } } impl Default for Spacing { fn default() -> Self { Self { item_spacing: vec2(8.0, 3.0), window_margin: Margin::same(6.0), menu_margin: Margin::same(1.0), button_padding: vec2(4.0, 1.0), indent: 18.0, // match checkbox/radio-button with `button_padding.x + icon_width + icon_spacing` interact_size: vec2(40.0, 18.0), slider_width: 100.0, text_edit_width: 280.0, icon_width: 14.0, icon_width_inner: 8.0, icon_spacing: 4.0, tooltip_width: 600.0, combo_height: 200.0, scroll_bar_width: 8.0, indent_ends_with_horizontal_line: false, } } } impl Default for Interaction { fn default() -> Self { Self { resize_grab_radius_side: 5.0, resize_grab_radius_corner: 10.0, show_tooltips_only_when_still: false, } } } impl Visuals { /// Default dark theme. pub fn dark() -> Self { Self { dark_mode: true, override_text_color: None, widgets: Widgets::default(), selection: Selection::default(), hyperlink_color: Color32::from_rgb(90, 170, 255), faint_bg_color: Color32::from_gray(35), extreme_bg_color: Color32::from_gray(10), // e.g. TextEdit background code_bg_color: Color32::from_gray(64), warn_fg_color: Color32::from_rgb(255, 143, 0), // orange error_fg_color: Color32::from_rgb(255, 0, 0), // red window_rounding: Rounding::same(6.0), window_shadow: Shadow::big_dark(), popup_shadow: Shadow::small_dark(), resize_corner_size: 12.0, text_cursor_width: 2.0, text_cursor_preview: false, clip_rect_margin: 3.0, // should be at least half the size of the widest frame stroke + max WidgetVisuals::expansion button_frame: true, collapsing_header_frame: false, } } /// Default light theme. pub fn light() -> Self { Self { dark_mode: false, widgets: Widgets::light(), selection: Selection::light(), hyperlink_color: Color32::from_rgb(0, 155, 255), faint_bg_color: Color32::from_gray(242), extreme_bg_color: Color32::from_gray(255), // e.g. TextEdit background code_bg_color: Color32::from_gray(230), warn_fg_color: Color32::from_rgb(255, 100, 0), // slightly orange red. it's difficult to find a warning color that pops on bright background. error_fg_color: Color32::from_rgb(255, 0, 0), // red window_shadow: Shadow::big_light(), popup_shadow: Shadow::small_light(), ..Self::dark() } } } impl Default for Visuals { fn default() -> Self { Self::dark() } } impl Selection { fn dark() -> Self { Self { bg_fill: Color32::from_rgb(0, 92, 128), stroke: Stroke::new(1.0, Color32::from_rgb(192, 222, 255)), } } fn light() -> Self { Self { bg_fill: Color32::from_rgb(144, 209, 255), stroke: Stroke::new(1.0, Color32::from_rgb(0, 83, 125)), } } } impl Default for Selection { fn default() -> Self { Self::dark() } } impl Widgets { pub fn dark() -> Self { Self { noninteractive: WidgetVisuals { bg_fill: Color32::from_gray(27), // window background bg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // separators, indentation lines, windows outlines fg_stroke: Stroke::new(1.0, Color32::from_gray(140)), // normal text color rounding: Rounding::same(2.0), expansion: 0.0, }, inactive: WidgetVisuals { bg_fill: Color32::from_gray(60), // button background bg_stroke: Default::default(), fg_stroke: Stroke::new(1.0, Color32::from_gray(180)), // button text rounding: Rounding::same(2.0), expansion: 0.0, }, hovered: WidgetVisuals { bg_fill: Color32::from_gray(70), bg_stroke: Stroke::new(1.0, Color32::from_gray(150)), // e.g. hover over window edge or button fg_stroke: Stroke::new(1.5, Color32::from_gray(240)), rounding: Rounding::same(3.0), expansion: 1.0, }, active: WidgetVisuals { bg_fill: Color32::from_gray(55), bg_stroke: Stroke::new(1.0, Color32::WHITE), fg_stroke: Stroke::new(2.0, Color32::WHITE), rounding: Rounding::same(2.0), expansion: 1.0, }, open: WidgetVisuals { bg_fill: Color32::from_gray(27), bg_stroke: Stroke::new(1.0, Color32::from_gray(60)), fg_stroke: Stroke::new(1.0, Color32::from_gray(210)), rounding: Rounding::same(2.0), expansion: 0.0, }, } } pub fn light() -> Self { Self { noninteractive: WidgetVisuals { bg_fill: Color32::from_gray(248), // window background - should be distinct from TextEdit background bg_stroke: Stroke::new(1.0, Color32::from_gray(190)), // separators, indentation lines, windows outlines fg_stroke: Stroke::new(1.0, Color32::from_gray(80)), // normal text color rounding: Rounding::same(2.0), expansion: 0.0, }, inactive: WidgetVisuals { bg_fill: Color32::from_gray(230), // button background bg_stroke: Default::default(), fg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // button text rounding: Rounding::same(2.0), expansion: 0.0, }, hovered: WidgetVisuals { bg_fill: Color32::from_gray(220), bg_stroke: Stroke::new(1.0, Color32::from_gray(105)), // e.g. hover over window edge or button fg_stroke: Stroke::new(1.5, Color32::BLACK), rounding: Rounding::same(3.0), expansion: 1.0, }, active: WidgetVisuals { bg_fill: Color32::from_gray(165), bg_stroke: Stroke::new(1.0, Color32::BLACK), fg_stroke: Stroke::new(2.0, Color32::BLACK), rounding: Rounding::same(2.0), expansion: 1.0, }, open: WidgetVisuals { bg_fill: Color32::from_gray(220), bg_stroke: Stroke::new(1.0, Color32::from_gray(160)), fg_stroke: Stroke::new(1.0, Color32::BLACK), rounding: Rounding::same(2.0), expansion: 0.0, }, } } } impl Default for Widgets { fn default() -> Self { Self::dark() } } // ---------------------------------------------------------------------------- use crate::{widgets::*, Ui}; impl Style { pub fn ui(&mut self, ui: &mut crate::Ui) { let Self { override_font_id, override_text_style, text_styles, wrap: _, spacing, interaction, visuals, animation_time, debug, explanation_tooltips, } = self; visuals.light_dark_radio_buttons(ui); crate::Grid::new("_options").show(ui, |ui| { ui.label("Override font id:"); ui.horizontal(|ui| { ui.radio_value(override_font_id, None, "None"); if ui.radio(override_font_id.is_some(), "override").clicked() { *override_font_id = Some(FontId::default()); } if let Some(override_font_id) = override_font_id { crate::introspection::font_id_ui(ui, override_font_id); } }); ui.end_row(); ui.label("Override text style:"); crate::ComboBox::from_id_source("Override text style") .selected_text(match override_text_style { None => "None".to_owned(), Some(override_text_style) => override_text_style.to_string(), }) .show_ui(ui, |ui| { ui.selectable_value(override_text_style, None, "None"); let all_text_styles = ui.style().text_styles(); for style in all_text_styles { let text = crate::RichText::new(style.to_string()).text_style(style.clone()); ui.selectable_value(override_text_style, Some(style), text); } }); ui.end_row(); ui.label("Animation duration:"); ui.add( Slider::new(animation_time, 0.0..=1.0) .clamp_to_range(true) .suffix(" s"), ); ui.end_row(); }); ui.collapsing("🔠 Text Styles", |ui| text_styles_ui(ui, text_styles)); ui.collapsing("📏 Spacing", |ui| spacing.ui(ui)); ui.collapsing("☝ Interaction", |ui| interaction.ui(ui)); ui.collapsing("🎨 Visuals", |ui| visuals.ui(ui)); ui.collapsing("🐛 Debug", |ui| debug.ui(ui)); ui.checkbox(explanation_tooltips, "Explanation tooltips") .on_hover_text( "Show explanatory text when hovering DragValue:s and other egui widgets", ); ui.vertical_centered(|ui| reset_button(ui, self)); } } fn text_styles_ui(ui: &mut Ui, text_styles: &mut BTreeMap) -> Response { ui.vertical(|ui| { crate::Grid::new("text_styles").show(ui, |ui| { for (text_style, font_id) in text_styles.iter_mut() { ui.label(RichText::new(text_style.to_string()).font(font_id.clone())); crate::introspection::font_id_ui(ui, font_id); ui.end_row(); } }); crate::reset_button_with(ui, text_styles, default_text_styles()); }) .response } impl Spacing { pub fn ui(&mut self, ui: &mut crate::Ui) { let Self { item_spacing, window_margin, menu_margin, button_padding, indent, interact_size, slider_width, text_edit_width, icon_width, icon_width_inner, icon_spacing, tooltip_width, indent_ends_with_horizontal_line, combo_height, scroll_bar_width, } = self; ui.add(slider_vec2(item_spacing, 0.0..=20.0, "Item spacing")); let margin_range = 0.0..=20.0; ui.horizontal(|ui| { ui.add( DragValue::new(&mut window_margin.left) .clamp_range(margin_range.clone()) .prefix("left: "), ); ui.add( DragValue::new(&mut window_margin.right) .clamp_range(margin_range.clone()) .prefix("right: "), ); ui.label("Window margins x"); }); ui.horizontal(|ui| { ui.add( DragValue::new(&mut window_margin.top) .clamp_range(margin_range.clone()) .prefix("top: "), ); ui.add( DragValue::new(&mut window_margin.bottom) .clamp_range(margin_range.clone()) .prefix("bottom: "), ); ui.label("Window margins y"); }); ui.horizontal(|ui| { ui.add( DragValue::new(&mut menu_margin.left) .clamp_range(margin_range.clone()) .prefix("left: "), ); ui.add( DragValue::new(&mut menu_margin.right) .clamp_range(margin_range.clone()) .prefix("right: "), ); ui.label("Menu margins x"); }); ui.horizontal(|ui| { ui.add( DragValue::new(&mut menu_margin.top) .clamp_range(margin_range.clone()) .prefix("top: "), ); ui.add( DragValue::new(&mut menu_margin.bottom) .clamp_range(margin_range) .prefix("bottom: "), ); ui.label("Menu margins y"); }); ui.add(slider_vec2(button_padding, 0.0..=20.0, "Button padding")); ui.add(slider_vec2(interact_size, 4.0..=60.0, "Interact size")) .on_hover_text("Minimum size of an interactive widget"); ui.horizontal(|ui| { ui.add(DragValue::new(indent).clamp_range(0.0..=100.0)); ui.label("Indent"); }); ui.horizontal(|ui| { ui.add(DragValue::new(slider_width).clamp_range(0.0..=1000.0)); ui.label("Slider width"); }); ui.horizontal(|ui| { ui.add(DragValue::new(text_edit_width).clamp_range(0.0..=1000.0)); ui.label("TextEdit width"); }); ui.horizontal(|ui| { ui.add(DragValue::new(scroll_bar_width).clamp_range(0.0..=32.0)); ui.label("Scroll-bar width width"); }); ui.horizontal(|ui| { ui.label("Checkboxes etc:"); ui.add( DragValue::new(icon_width) .prefix("outer icon width:") .clamp_range(0.0..=60.0), ); ui.add( DragValue::new(icon_width_inner) .prefix("inner icon width:") .clamp_range(0.0..=60.0), ); ui.add( DragValue::new(icon_spacing) .prefix("spacing:") .clamp_range(0.0..=10.0), ); }); ui.horizontal(|ui| { ui.add(DragValue::new(tooltip_width).clamp_range(0.0..=1000.0)); ui.label("Tooltip wrap width"); }); ui.checkbox( indent_ends_with_horizontal_line, "End indented regions with a horizontal separator", ); ui.horizontal(|ui| { ui.label("Max height of a combo box"); ui.add(DragValue::new(combo_height).clamp_range(0.0..=1000.0)); }); ui.vertical_centered(|ui| reset_button(ui, self)); } } impl Interaction { pub fn ui(&mut self, ui: &mut crate::Ui) { let Self { resize_grab_radius_side, resize_grab_radius_corner, show_tooltips_only_when_still, } = self; ui.add(Slider::new(resize_grab_radius_side, 0.0..=20.0).text("resize_grab_radius_side")); ui.add( Slider::new(resize_grab_radius_corner, 0.0..=20.0).text("resize_grab_radius_corner"), ); ui.checkbox( show_tooltips_only_when_still, "Only show tooltips if mouse is still", ); ui.vertical_centered(|ui| reset_button(ui, self)); } } impl Widgets { pub fn ui(&mut self, ui: &mut crate::Ui) { let Self { active, hovered, inactive, noninteractive, open, } = self; ui.collapsing("Noninteractive", |ui| { ui.label( "The style of a widget that you cannot interact with, e.g. labels and separators.", ); noninteractive.ui(ui); }); ui.collapsing("Interactive but inactive", |ui| { ui.label("The style of an interactive widget, such as a button, at rest."); inactive.ui(ui); }); ui.collapsing("Interactive and hovered", |ui| { ui.label("The style of an interactive widget while you hover it."); hovered.ui(ui); }); ui.collapsing("Interactive and active", |ui| { ui.label("The style of an interactive widget as you are clicking or dragging it."); active.ui(ui); }); ui.collapsing("Open menu", |ui| { ui.label("The style of an open combo-box or menu button"); open.ui(ui); }); // ui.vertical_centered(|ui| reset_button(ui, self)); } } impl Selection { pub fn ui(&mut self, ui: &mut crate::Ui) { let Self { bg_fill, stroke } = self; ui.label("Selectable labels"); ui_color(ui, bg_fill, "background fill"); stroke_ui(ui, stroke, "stroke"); } } impl WidgetVisuals { pub fn ui(&mut self, ui: &mut crate::Ui) { let Self { bg_fill, bg_stroke, rounding, fg_stroke, expansion, } = self; ui_color(ui, bg_fill, "background fill"); stroke_ui(ui, bg_stroke, "background stroke"); rounding_ui(ui, rounding); stroke_ui(ui, fg_stroke, "foreground stroke (text)"); ui.add(Slider::new(expansion, -5.0..=5.0).text("expansion")) .on_hover_text("make shapes this much larger"); } } impl Visuals { /// Show radio-buttons to switch between light and dark mode. pub fn light_dark_radio_buttons(&mut self, ui: &mut crate::Ui) { ui.horizontal(|ui| { ui.selectable_value(self, Self::light(), "☀ Light"); ui.selectable_value(self, Self::dark(), "🌙 Dark"); }); } /// Show small toggle-button for light and dark mode. #[must_use] pub fn light_dark_small_toggle_button(&self, ui: &mut crate::Ui) -> Option { #![allow(clippy::collapsible_else_if)] if self.dark_mode { if ui .add(Button::new("☀").frame(false)) .on_hover_text("Switch to light mode") .clicked() { return Some(Self::light()); } } else { if ui .add(Button::new("🌙").frame(false)) .on_hover_text("Switch to dark mode") .clicked() { return Some(Self::dark()); } } None } pub fn ui(&mut self, ui: &mut crate::Ui) { let Self { dark_mode: _, override_text_color: _, widgets, selection, hyperlink_color, faint_bg_color, extreme_bg_color, code_bg_color, warn_fg_color, error_fg_color, window_rounding, window_shadow, popup_shadow, resize_corner_size, text_cursor_width, text_cursor_preview, clip_rect_margin, button_frame, collapsing_header_frame, } = self; ui.collapsing("Background Colors", |ui| { ui_color(ui, &mut widgets.inactive.bg_fill, "Buttons"); ui_color(ui, &mut widgets.noninteractive.bg_fill, "Windows"); ui_color(ui, faint_bg_color, "Faint accent").on_hover_text( "Used for faint accentuation of interactive things, like striped grids.", ); ui_color(ui, extreme_bg_color, "Extreme") .on_hover_text("Background of plots and paintings"); }); ui.collapsing("Window", |ui| { // Common shortcuts ui_color(ui, &mut widgets.noninteractive.bg_fill, "Fill"); stroke_ui(ui, &mut widgets.noninteractive.bg_stroke, "Outline"); rounding_ui(ui, window_rounding); shadow_ui(ui, window_shadow, "Shadow"); shadow_ui(ui, popup_shadow, "Shadow (small menus and popups)"); }); ui.collapsing("Widgets", |ui| widgets.ui(ui)); ui.collapsing("Selection", |ui| selection.ui(ui)); ui.horizontal(|ui| { ui_color( ui, &mut widgets.noninteractive.fg_stroke.color, "Text color", ); ui_color(ui, warn_fg_color, RichText::new("Warnings")); ui_color(ui, error_fg_color, RichText::new("Errors")); }); ui_color(ui, code_bg_color, RichText::new("Code background").code()).on_hover_ui(|ui| { ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = 0.0; ui.label("For monospaced inlined text "); ui.code("like this"); ui.label("."); }); }); ui_color(ui, hyperlink_color, "hyperlink_color"); ui.add(Slider::new(resize_corner_size, 0.0..=20.0).text("resize_corner_size")); ui.add(Slider::new(text_cursor_width, 0.0..=4.0).text("text_cursor_width")); ui.checkbox(text_cursor_preview, "Preview text cursor on hover"); ui.add(Slider::new(clip_rect_margin, 0.0..=20.0).text("clip_rect_margin")); ui.checkbox(button_frame, "Button has a frame"); ui.checkbox(collapsing_header_frame, "Collapsing header has a frame"); ui.vertical_centered(|ui| reset_button(ui, self)); } } impl DebugOptions { pub fn ui(&mut self, ui: &mut crate::Ui) { let Self { debug_on_hover, show_expand_width: debug_expand_width, show_expand_height: debug_expand_height, show_resize: debug_resize, } = self; ui.checkbox(debug_on_hover, "Show debug info on hover"); ui.checkbox( debug_expand_width, "Show which widgets make their parent wider", ); ui.checkbox( debug_expand_height, "Show which widgets make their parent higher", ); ui.checkbox(debug_resize, "Debug Resize"); ui.vertical_centered(|ui| reset_button(ui, self)); } } // TODO(emilk): improve and standardize `slider_vec2` fn slider_vec2<'a>( value: &'a mut Vec2, range: std::ops::RangeInclusive, text: &'a str, ) -> impl Widget + 'a { move |ui: &mut crate::Ui| { ui.horizontal(|ui| { ui.add( DragValue::new(&mut value.x) .clamp_range(range.clone()) .prefix("x: "), ); ui.add( DragValue::new(&mut value.y) .clamp_range(range.clone()) .prefix("y: "), ); ui.label(text); }) .response } } fn ui_color(ui: &mut Ui, srgba: &mut Color32, label: impl Into) -> Response { ui.horizontal(|ui| { ui.color_edit_button_srgba(srgba); ui.label(label); }) .response } fn rounding_ui(ui: &mut Ui, rounding: &mut Rounding) { const MAX: f32 = 20.0; let mut same = rounding.is_same(); ui.group(|ui| { ui.horizontal(|ui| { ui.label("Rounding: "); ui.radio_value(&mut same, true, "Same"); ui.radio_value(&mut same, false, "Separate"); }); if same { let mut cr = rounding.nw; ui.add(Slider::new(&mut cr, 0.0..=MAX)); *rounding = Rounding::same(cr); } else { ui.add(Slider::new(&mut rounding.nw, 0.0..=MAX).text("North-West")); ui.add(Slider::new(&mut rounding.ne, 0.0..=MAX).text("North-East")); ui.add(Slider::new(&mut rounding.sw, 0.0..=MAX).text("South-West")); ui.add(Slider::new(&mut rounding.se, 0.0..=MAX).text("South-East")); if rounding.is_same() { rounding.se *= 1.00001; } } }); }