#![allow(clippy::if_same_then_else)]
use crate::{color::*, emath::*, Response};
use epaint::{Shadow, Stroke, TextStyle};
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "persistence", serde(default))]
pub struct Style {
pub body_text_style: TextStyle,
pub wrap: Option<bool>,
pub spacing: Spacing,
pub interaction: Interaction,
pub visuals: Visuals,
pub animation_time: f32,
}
impl Style {
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.fg_stroke = self.visuals.selection.stroke;
}
visuals
}
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 {
pub item_spacing: Vec2,
pub window_padding: Vec2,
pub button_padding: Vec2,
pub indent: f32,
pub interact_size: Vec2,
pub slider_width: f32,
pub text_edit_width: f32,
pub icon_width: f32,
pub icon_spacing: f32,
pub tooltip_width: f32,
}
impl Spacing {
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;
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 {
pub resize_grab_radius_side: f32,
pub resize_grab_radius_corner: f32,
pub show_tooltips_only_when_still: bool,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "persistence", serde(default))]
pub struct Visuals {
pub dark_mode: bool,
pub override_text_color: Option<Color32>,
pub widgets: Widgets,
pub selection: Selection,
pub extreme_bg_color: Color32,
pub hyperlink_color: Color32,
pub code_bg_color: Color32,
pub window_corner_radius: f32,
pub window_shadow: Shadow,
pub resize_corner_size: f32,
pub text_cursor_width: f32,
pub text_cursor_preview: bool,
pub clip_rect_margin: f32,
pub debug_expand_width: bool,
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 {
crate::color::tint_color_towards(self.text_color(), self.window_fill())
}
pub fn strong_text_color(&self) -> Color32 {
self.widgets.active.text_color()
}
pub fn window_fill(&self) -> Color32 {
self.widgets.noninteractive.bg_fill
}
pub fn window_stroke(&self) -> Stroke {
self.widgets.noninteractive.bg_stroke
}
}
#[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 {
pub noninteractive: WidgetVisuals,
pub inactive: WidgetVisuals,
pub hovered: WidgetVisuals,
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.hovered() {
&self.hovered
} else {
&self.inactive
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
pub struct WidgetVisuals {
pub bg_fill: Color32,
pub bg_stroke: Stroke,
pub corner_radius: f32,
pub fg_stroke: Stroke,
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,
wrap: None,
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: 600.0,
}
}
}
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: true,
}
}
}
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,
text_cursor_preview: false,
clip_rect_margin: 3.0,
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),
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: 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(30),
bg_stroke: Stroke::new(1.0, Color32::from_gray(65)),
fg_stroke: Stroke::new(1.0, Color32::from_gray(160)),
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)),
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)),
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_fill: Color32::from_gray(220),
bg_stroke: Stroke::new(1.0, Color32::from_gray(180)),
fg_stroke: Stroke::new(1.0, Color32::from_gray(70)),
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)),
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)),
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,
wrap: _,
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.add(slider_vec2(item_spacing, 0.0..=10.0, "item_spacing"));
ui.add(slider_vec2(window_padding, 0.0..=10.0, "window_padding"));
ui.add(slider_vec2(button_padding, 0.0..=10.0, "button_padding"));
ui.add(slider_vec2(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..=1000.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,
show_tooltips_only_when_still,
} = 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.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,
} = self;
ui.collapsing("noninteractive", |ui| {
ui.label("The style of a widget that you cannot interact with.");
noninteractive.ui(ui)
});
ui.collapsing("interactive & inactive", |ui| {
ui.label("The style of an interactive widget, such as a button, at rest.");
inactive.ui(ui)
});
ui.collapsing("interactive & hovered", |ui| {
ui.label("The style of an interactive widget while you hover it.");
hovered.ui(ui)
});
ui.collapsing("interactive & active", |ui| {
ui.label("The style of an interactive 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 {
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");
});
});
}
#[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,
text_cursor_preview,
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");
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.checkbox(text_cursor_preview, "text_cursor_preview");
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));
}
}
fn slider_vec2<'a>(
value: &'a mut Vec2,
range: std::ops::RangeInclusive<f32>,
text: &'a str,
) -> impl Widget + 'a {
move |ui: &mut crate::Ui| {
ui.horizontal(|ui| {
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);
})
.response
}
}
fn ui_color(ui: &mut Ui, srgba: &mut Color32, text: &str) {
ui.horizontal(|ui| {
ui.color_edit_button_srgba(srgba);
ui.label(text);
});
}