egui/egui/src/style.rs
2021-02-03 19:30:58 +01:00

720 lines
23 KiB
Rust

//! egui theme (spacing, colors, etc).
#![allow(clippy::if_same_then_else)]
use crate::{
color::*,
math::*,
paint::{Shadow, Stroke, TextStyle},
Response,
};
/// Specifies the look and feel of a [`Ui`].
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "persistence", serde(default))]
pub struct Style {
/// Default `TextStyle` for normal text (i.e. for `Label` and `TextEdit`).
pub body_text_style: TextStyle,
pub spacing: Spacing,
pub interaction: Interaction,
pub visuals: Visuals,
/// How many seconds a typical animation should last
pub animation_time: f32,
}
impl Style {
// TODO: 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)
}
/// Style to use for non-interactive widgets.
pub fn noninteractive(&self) -> &WidgetVisuals {
&self.visuals.widgets.noninteractive
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "persistence", serde(default))]
pub struct Spacing {
/// Horizontal and vertical spacing between widgets
pub item_spacing: Vec2,
/// Horizontal and vertical padding within a window frame.
pub window_padding: Vec2,
/// Button size is text size plus this on each side
pub button_padding: Vec2,
/// Indent collapsing regions etc by this much.
pub indent: f32,
/// Minimum size of e.g. a button (including padding).
/// `interact_size.y` is the default height of button, slider, etc.
/// Anything clickable should be (at least) this size.
pub interact_size: Vec2, // TODO: rename min_interact_size ?
/// Default width of a `Slider`.
pub slider_width: f32, // TODO: 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 this icon.
pub icon_width: 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,
}
impl Spacing {
/// Returns small icon rectangle and big icon rectangle
pub fn icon_rectangles(&self, rect: Rect) -> (Rect, Rect) {
let box_side = self.icon_width;
let big_icon_rect = Rect::from_center_size(
pos2(rect.left() + box_side / 2.0, rect.center().y),
vec2(box_side, box_side),
);
let small_rect_side = 8.0; // TODO: make a parameter
let small_icon_rect =
Rect::from_center_size(big_icon_rect.center(), Vec2::splat(small_rect_side));
(small_icon_rect, big_icon_rect)
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "persistence", serde(default))]
pub struct Interaction {
/// Mouse must be the close to the side of a window to resize
pub resize_grab_radius_side: f32,
/// Mouse must be the close to the corner of a window to resize
pub resize_grab_radius_corner: f32,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "persistence", 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.
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 wether or not the widget is being interacted with.
///
/// In the future we may instead modulate
/// the `text_color` based on wether 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<Color32>,
/// Visual styles of widgets
pub widgets: Widgets,
pub selection: Selection,
/// 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,
/// The color used for `Hyperlink`,
pub hyperlink_color: Color32,
/// Background color behind code-styled monospaced labels.
pub code_bg_color: Color32,
pub window_corner_radius: f32,
pub window_shadow: Shadow,
pub resize_corner_size: f32,
pub text_cursor_width: f32,
/// Allow child widgets to be just on the border and still have a stroke with some thickness
pub clip_rect_margin: f32,
// -----------------------------------------------
// Debug rendering:
/// Show which widgets make their parent wider
pub debug_expand_width: bool,
/// Show which widgets make their parent higher
pub debug_expand_height: bool,
pub debug_resize: bool,
}
impl Visuals {
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 {
self.widgets.disabled.text_color()
}
pub fn strong_text_color(&self) -> Color32 {
self.widgets.active.text_color()
}
}
/// Selected text, selected elements etc
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "persistence", serde(default))]
pub struct Selection {
pub bg_fill: Color32,
pub stroke: Stroke,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "persistence", serde(default))]
pub struct Widgets {
/// For a non-interactive widget
pub noninteractive: WidgetVisuals,
/// For an otherwise interactive widget that has been disabled
pub disabled: WidgetVisuals,
/// For an interactive widget that is "resting"
pub inactive: WidgetVisuals,
/// For an interactive widget that is being hovered
pub hovered: WidgetVisuals,
/// For an interactive widget that is being interacted with
pub active: WidgetVisuals,
}
impl Widgets {
pub fn style(&self, response: &Response) -> &WidgetVisuals {
if response.is_pointer_button_down_on() || response.has_kb_focus {
&self.active
} else if response.sense == crate::Sense::hover() {
&self.disabled
} else if response.hovered() {
&self.hovered
} else {
&self.inactive
}
}
}
/// bg = background, fg = foreground.
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "persistence", 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 corner_radius: f32,
/// 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 {
pub fn text_color(&self) -> Color32 {
self.fg_stroke.color
}
}
// ----------------------------------------------------------------------------
impl Default for Style {
fn default() -> Self {
Self {
body_text_style: TextStyle::Body,
spacing: Spacing::default(),
interaction: Interaction::default(),
visuals: Visuals::default(),
animation_time: 1.0 / 12.0,
}
}
}
impl Default for Spacing {
fn default() -> Self {
Self {
item_spacing: vec2(8.0, 3.0),
window_padding: Vec2::splat(6.0),
button_padding: vec2(4.0, 1.0),
indent: 25.0,
interact_size: vec2(40.0, 20.0),
slider_width: 100.0,
text_edit_width: 280.0,
icon_width: 16.0,
icon_spacing: 0.0,
tooltip_width: 400.0,
}
}
}
impl Default for Interaction {
fn default() -> Self {
Self {
resize_grab_radius_side: 5.0,
resize_grab_radius_corner: 10.0,
}
}
}
impl Visuals {
pub fn dark() -> Self {
Self {
dark_mode: true,
override_text_color: None,
widgets: Widgets::default(),
selection: Selection::default(),
extreme_bg_color: Color32::from_gray(10),
hyperlink_color: Color32::from_rgb(90, 170, 255),
code_bg_color: Color32::from_gray(64),
window_corner_radius: 10.0,
window_shadow: Shadow::big_dark(),
resize_corner_size: 12.0,
text_cursor_width: 2.0,
clip_rect_margin: 3.0, // should be at least half the size of the widest frame stroke + max WidgetVisuals::expansion
debug_expand_width: false,
debug_expand_height: false,
debug_resize: false,
}
}
pub fn light() -> Self {
Self {
dark_mode: false,
widgets: Widgets::light(),
selection: Selection::light(),
extreme_bg_color: Color32::from_gray(235), // TODO: rename
hyperlink_color: Color32::from_rgb(0, 133, 218),
code_bg_color: Color32::from_gray(200),
window_shadow: Shadow::big_light(),
..Self::dark()
}
}
}
impl Default for Visuals {
fn default() -> Self {
Self::dark()
}
}
impl Selection {
fn dark() -> Self {
Self {
bg_fill: Rgba::from_rgb(0.0, 0.5, 1.0)
.additive()
.multiply(0.10)
.into(),
stroke: Stroke::new(1.0, Rgba::from_rgb(0.3, 0.6, 1.0)),
}
}
fn light() -> Self {
Self {
bg_fill: Rgba::from_rgb(0.0, 0.5, 1.0).multiply(0.5).into(),
stroke: Stroke::new(1.0, Rgba::from_rgb(0.3, 0.6, 1.0)),
}
}
}
impl Default for Selection {
fn default() -> Self {
Self::dark()
}
}
impl Widgets {
pub fn dark() -> Self {
Self {
noninteractive: WidgetVisuals {
bg_stroke: Stroke::new(1.0, Color32::from_gray(65)), // window outline
bg_fill: Color32::from_gray(30), // window background
fg_stroke: Stroke::new(1.0, Color32::from_gray(160)), // text color
corner_radius: 4.0,
expansion: 0.0,
},
disabled: WidgetVisuals {
bg_fill: Color32::from_gray(40), // Should look grayed out
bg_stroke: Stroke::new(1.0, Color32::from_gray(70)),
fg_stroke: Stroke::new(1.0, Color32::from_gray(140)), // Should look grayed out
corner_radius: 4.0,
expansion: 0.0,
},
inactive: WidgetVisuals {
bg_fill: Color32::from_gray(70),
bg_stroke: Default::default(),
fg_stroke: Stroke::new(1.0, Color32::from_gray(200)), // Should NOT look grayed out!
corner_radius: 4.0,
expansion: 0.0,
},
hovered: WidgetVisuals {
bg_fill: Color32::from_gray(80),
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)),
corner_radius: 4.0,
expansion: 1.0,
},
active: WidgetVisuals {
bg_fill: Color32::from_gray(90),
bg_stroke: Stroke::new(1.0, Color32::WHITE),
fg_stroke: Stroke::new(2.0, Color32::WHITE),
corner_radius: 4.0,
expansion: 2.0,
},
}
}
pub fn light() -> Self {
Self {
noninteractive: WidgetVisuals {
bg_stroke: Stroke::new(1.0, Color32::from_gray(180)), // window outline
bg_fill: Color32::from_gray(220), // window background
fg_stroke: Stroke::new(1.0, Color32::from_gray(70)), // text color
corner_radius: 4.0,
expansion: 0.0,
},
disabled: WidgetVisuals {
bg_fill: Color32::from_gray(215), // Should look grayed out
bg_stroke: Stroke::new(1.0, Color32::from_gray(185)),
fg_stroke: Stroke::new(1.0, Color32::from_gray(115)), // Should look grayed out
corner_radius: 4.0,
expansion: 0.0,
},
inactive: WidgetVisuals {
bg_fill: Color32::from_gray(195),
bg_stroke: Default::default(),
fg_stroke: Stroke::new(1.0, Color32::from_gray(55)), // Should NOT look grayed out!
corner_radius: 4.0,
expansion: 0.0,
},
hovered: WidgetVisuals {
bg_fill: Color32::from_gray(175),
bg_stroke: Stroke::new(1.0, Color32::from_gray(105)), // e.g. hover over window edge or button
fg_stroke: Stroke::new(2.0, Color32::BLACK),
corner_radius: 4.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),
corner_radius: 4.0,
expansion: 2.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 {
body_text_style,
spacing,
interaction,
visuals,
animation_time,
} = self;
visuals.light_dark_radio_buttons(ui);
ui.horizontal(|ui| {
ui.label("Default text style:");
for &value in &[TextStyle::Body, TextStyle::Monospace] {
ui.radio_value(body_text_style, value, format!("{:?}", value));
}
});
ui.collapsing("📏 Spacing", |ui| spacing.ui(ui));
ui.collapsing("☝ Interaction", |ui| interaction.ui(ui));
ui.collapsing("🎨 Visuals", |ui| visuals.ui(ui));
ui.add(Slider::f32(animation_time, 0.0..=1.0).text("animation_time"));
ui.vertical_centered(|ui| reset_button(ui, self));
}
}
impl Spacing {
pub fn ui(&mut self, ui: &mut crate::Ui) {
let Self {
item_spacing,
window_padding,
button_padding,
indent,
interact_size,
slider_width,
text_edit_width,
icon_width,
icon_spacing,
tooltip_width,
} = self;
ui_slider_vec2(ui, item_spacing, 0.0..=10.0, "item_spacing");
ui_slider_vec2(ui, window_padding, 0.0..=10.0, "window_padding");
ui_slider_vec2(ui, button_padding, 0.0..=10.0, "button_padding");
ui_slider_vec2(ui, interact_size, 0.0..=60.0, "interact_size")
.on_hover_text("Minimum size of an interactive widget");
ui.add(Slider::f32(indent, 0.0..=100.0).text("indent"));
ui.add(Slider::f32(slider_width, 0.0..=1000.0).text("slider_width"));
ui.add(Slider::f32(text_edit_width, 0.0..=1000.0).text("text_edit_width"));
ui.add(Slider::f32(icon_width, 0.0..=60.0).text("icon_width"));
ui.add(Slider::f32(icon_spacing, 0.0..=10.0).text("icon_spacing"));
ui.add(Slider::f32(tooltip_width, 0.0..=10.0).text("tooltip_width"));
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,
} = self;
ui.add(Slider::f32(resize_grab_radius_side, 0.0..=20.0).text("resize_grab_radius_side"));
ui.add(
Slider::f32(resize_grab_radius_corner, 0.0..=20.0).text("resize_grab_radius_corner"),
);
ui.vertical_centered(|ui| reset_button(ui, self));
}
}
impl Widgets {
pub fn ui(&mut self, ui: &mut crate::Ui) {
let Self {
active,
hovered,
inactive,
disabled,
noninteractive,
} = self;
ui.collapsing("noninteractive", |ui| {
ui.label("The style of something that you cannot interact with.");
noninteractive.ui(ui)
});
ui.collapsing("interactive & disabled", |ui| {
ui.label("The style of a disabled button.");
disabled.ui(ui)
});
ui.collapsing("interactive & inactive", |ui| {
ui.label("The style of a widget, such as a button, at rest.");
inactive.ui(ui)
});
ui.collapsing("interactive & hovered", |ui| {
ui.label("The style of a widget while you hover it.");
hovered.ui(ui)
});
ui.collapsing("interactive & active", |ui| {
ui.label("The style of a widget as you are clicking or dragging it.");
active.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_color(ui, bg_fill, "bg_fill");
stroke_ui(ui, stroke, "stroke");
}
}
impl WidgetVisuals {
pub fn ui(&mut self, ui: &mut crate::Ui) {
let Self {
bg_fill,
bg_stroke,
corner_radius,
fg_stroke,
expansion,
} = self;
ui_color(ui, bg_fill, "bg_fill");
stroke_ui(ui, bg_stroke, "bg_stroke");
ui.add(Slider::f32(corner_radius, 0.0..=10.0).text("corner_radius"));
stroke_ui(ui, fg_stroke, "fg_stroke (text)");
ui.add(Slider::f32(expansion, -5.0..=5.0).text("expansion"));
}
}
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.group(|ui| {
ui.horizontal(|ui| {
ui.radio_value(self, Self::light(), "☀ Light");
ui.radio_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<Self> {
#![allow(clippy::collapsible_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,
extreme_bg_color,
hyperlink_color,
code_bg_color,
window_corner_radius,
window_shadow,
resize_corner_size,
text_cursor_width,
clip_rect_margin,
debug_expand_width,
debug_expand_height,
debug_resize,
} = self;
ui.collapsing("widgets", |ui| widgets.ui(ui));
ui.collapsing("selection", |ui| selection.ui(ui));
ui.group(|ui| {
ui.label("Window");
// Common shortcuts
ui_color(ui, &mut widgets.noninteractive.bg_fill, "Fill");
stroke_ui(ui, &mut widgets.noninteractive.bg_stroke, "Outline");
ui.add(Slider::f32(window_corner_radius, 0.0..=20.0).text("Corner Radius"));
shadow_ui(ui, window_shadow, "Shadow");
});
ui_color(
ui,
&mut widgets.noninteractive.fg_stroke.color,
"Text color",
);
ui_color(ui, extreme_bg_color, "extreme_bg_color");
ui_color(ui, hyperlink_color, "hyperlink_color");
ui_color(ui, code_bg_color, "code_bg_color");
ui.add(Slider::f32(resize_corner_size, 0.0..=20.0).text("resize_corner_size"));
ui.add(Slider::f32(text_cursor_width, 0.0..=2.0).text("text_cursor_width"));
ui.add(Slider::f32(clip_rect_margin, 0.0..=20.0).text("clip_rect_margin"));
ui.group(|ui| {
ui.label("DEBUG:");
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: improve and standardize ui_slider_vec2
fn ui_slider_vec2(
ui: &mut Ui,
value: &mut Vec2,
range: std::ops::RangeInclusive<f32>,
text: &str,
) -> Response {
ui.horizontal(|ui| {
/*
let fsw = full slider_width
let ssw = small slider_width
let space = item_spacing.x
let value = interact_size.x;
fsw + space + value = ssw + space + value + space + ssw + space + value
fsw + space + value = 2 * ssw + 3 * space + 2 * value
fsw + space - value = 2 * ssw + 3 * space
fsw - 2 * space - value = 2 * ssw
ssw = fsw / 2 - space - value / 2
*/
// let spacing = &ui.spacing();
// let space = spacing.item_spacing.x;
// let value_w = spacing.interact_size.x;
// let full_slider_width = spacing.slider_width;
// let small_slider_width = full_slider_width / 2.0 - space - value_w / 2.0;
// ui.spacing_mut().slider_width = small_slider_width;
ui.add(Slider::f32(&mut value.x, range.clone()).text("w"));
ui.add(Slider::f32(&mut value.y, range.clone()).text("h"));
ui.label(text);
})
.1
}
fn ui_color(ui: &mut Ui, srgba: &mut Color32, text: &str) {
ui.horizontal(|ui| {
ui.color_edit_button_srgba(srgba);
ui.label(text);
});
}