use emath::Align;
use epaint::{
CornerRadius, FontColorTransferFunction, Shadow, Stroke, TextOptions,
text::{FontTweak, FontVariationAxis, HintingTarget, SmoothHinting},
};
use std::{collections::BTreeMap, ops::RangeInclusive, sync::Arc};
use crate::{
ComboBox, CursorIcon, FontFamily, FontId, Grid, Margin, Response, RichText, TextWrapMode,
WidgetText,
ecolor::Color32,
emath::{Rangef, Rect, Vec2, pos2, vec2},
reset_button_with,
};
#[derive(Clone)]
pub struct NumberFormatter(
Arc<dyn 'static + Sync + Send + Fn(f64, RangeInclusive<usize>) -> String>,
);
impl NumberFormatter {
#[inline]
pub fn new(
formatter: impl 'static + Sync + Send + Fn(f64, RangeInclusive<usize>) -> String,
) -> Self {
Self(Arc::new(formatter))
}
#[inline]
pub fn format(&self, value: f64, decimals: RangeInclusive<usize>) -> String {
(self.0)(value, decimals)
}
}
impl std::fmt::Debug for NumberFormatter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("NumberFormatter")
}
}
impl PartialEq for NumberFormatter {
#[inline]
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.0, &other.0)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum TextStyle {
Small,
Body,
Monospace,
Button,
Heading,
Name(std::sync::Arc<str>),
}
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 {
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()
)
})
}
}
#[derive(Debug, Clone)]
pub enum FontSelection {
Default,
FontId(FontId),
Style(TextStyle),
}
impl Default for FontSelection {
#[inline]
fn default() -> Self {
Self::Default
}
}
impl FontSelection {
pub fn resolve(self, style: &Style) -> FontId {
self.resolve_with_fallback(style, TextStyle::Body.into())
}
pub fn resolve_with_fallback(self, style: &Style, fallback: Self) -> 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 {
fallback.resolve(style)
}
}
Self::FontId(font_id) => font_id,
Self::Style(text_style) => text_style.resolve(style),
}
}
}
impl From<FontId> for FontSelection {
#[inline(always)]
fn from(font_id: FontId) -> Self {
Self::FontId(font_id)
}
}
impl From<TextStyle> for FontSelection {
#[inline(always)]
fn from(text_style: TextStyle) -> Self {
Self::Style(text_style)
}
}
#[derive(Clone, Default)]
pub struct StyleModifier(Option<Arc<dyn Fn(&mut Style) + Send + Sync>>);
impl std::fmt::Debug for StyleModifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("StyleModifier")
}
}
impl<T> From<T> for StyleModifier
where
T: Fn(&mut Style) + Send + Sync + 'static,
{
fn from(f: T) -> Self {
Self(Some(Arc::new(f)))
}
}
impl From<Style> for StyleModifier {
fn from(style: Style) -> Self {
Self(Some(Arc::new(move |s| *s = style.clone())))
}
}
impl StyleModifier {
pub fn new(f: impl Fn(&mut Style) + Send + Sync + 'static) -> Self {
Self::from(f)
}
pub fn apply(&self, style: &mut Style) {
if let Some(f) = &self.0 {
f(style);
}
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct Style {
pub override_text_style: Option<TextStyle>,
pub override_font_id: Option<FontId>,
pub override_text_valign: Option<Align>,
pub text_styles: BTreeMap<TextStyle, FontId>,
pub drag_value_text_style: TextStyle,
#[cfg_attr(feature = "serde", serde(skip))]
pub number_formatter: NumberFormatter,
pub wrap_mode: Option<crate::TextWrapMode>,
pub spacing: Spacing,
pub interaction: Interaction,
pub visuals: Visuals,
pub animation_time: f32,
#[cfg(debug_assertions)]
pub debug: DebugOptions,
pub explanation_tooltips: bool,
pub url_in_tooltip: bool,
pub always_scroll_the_only_direction: bool,
pub scroll_animation: ScrollAnimation,
pub compact_menu_style: bool,
}
#[test]
fn style_impl_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Style>();
}
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.weak_bg_fill = self.visuals.selection.bg_fill;
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
}
pub fn text_styles(&self) -> Vec<TextStyle> {
self.text_styles.keys().cloned().collect()
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct Spacing {
pub item_spacing: Vec2,
pub window_margin: Margin,
pub button_padding: Vec2,
pub menu_margin: Margin,
pub indent: f32,
pub interact_size: Vec2,
pub slider_width: f32,
pub slider_rail_height: f32,
pub combo_width: f32,
pub text_edit_width: f32,
pub icon_width: f32,
pub icon_width_inner: f32,
pub icon_spacing: f32,
pub default_area_size: Vec2,
pub tooltip_width: f32,
pub menu_width: f32,
pub menu_spacing: f32,
pub indent_ends_with_horizontal_line: bool,
pub combo_height: f32,
pub scroll: ScrollStyle,
}
impl Spacing {
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, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct ScrollStyle {
pub floating: bool,
pub content_margin: Margin,
pub bar_width: f32,
pub handle_min_length: f32,
pub bar_inner_margin: f32,
pub bar_outer_margin: f32,
pub floating_width: f32,
pub floating_allocated_width: f32,
pub foreground_color: bool,
pub dormant_background_opacity: f32,
pub active_background_opacity: f32,
pub interact_background_opacity: f32,
pub dormant_handle_opacity: f32,
pub active_handle_opacity: f32,
pub interact_handle_opacity: f32,
pub fade: ScrollFadeStyle,
}
impl Default for ScrollStyle {
fn default() -> Self {
Self::floating()
}
}
impl ScrollStyle {
pub fn solid() -> Self {
Self {
floating: false,
content_margin: Margin::ZERO,
bar_width: 6.0,
handle_min_length: 12.0,
bar_inner_margin: 4.0,
bar_outer_margin: 0.0,
floating_width: 2.0,
floating_allocated_width: 0.0,
foreground_color: false,
dormant_background_opacity: 0.0,
active_background_opacity: 0.4,
interact_background_opacity: 0.7,
dormant_handle_opacity: 0.0,
active_handle_opacity: 0.6,
interact_handle_opacity: 1.0,
fade: Default::default(),
}
}
pub fn thin() -> Self {
Self {
floating: true,
bar_width: 10.0,
floating_allocated_width: 6.0,
foreground_color: false,
dormant_background_opacity: 1.0,
dormant_handle_opacity: 1.0,
active_background_opacity: 1.0,
active_handle_opacity: 1.0,
interact_background_opacity: 0.6,
interact_handle_opacity: 0.6,
..Self::solid()
}
}
pub fn floating() -> Self {
Self {
floating: true,
bar_width: 10.0,
foreground_color: true,
floating_allocated_width: 0.0,
dormant_background_opacity: 0.0,
dormant_handle_opacity: 0.0,
..Self::solid()
}
}
pub fn allocated_width(&self) -> f32 {
if self.floating {
self.floating_allocated_width
} else {
self.bar_inner_margin + self.bar_width + self.bar_outer_margin
}
}
pub fn ui(&mut self, ui: &mut Ui) {
ui.horizontal(|ui| {
ui.label("Presets:");
ui.selectable_value(self, Self::solid(), "Solid");
ui.selectable_value(self, Self::thin(), "Thin");
ui.selectable_value(self, Self::floating(), "Floating");
});
ui.collapsing("Details", |ui| {
self.details_ui(ui);
});
}
pub fn details_ui(&mut self, ui: &mut Ui) {
let Self {
floating,
content_margin,
bar_width,
handle_min_length,
bar_inner_margin,
bar_outer_margin,
floating_width,
floating_allocated_width,
foreground_color,
dormant_background_opacity,
active_background_opacity,
interact_background_opacity,
dormant_handle_opacity,
active_handle_opacity,
interact_handle_opacity,
fade,
} = self;
ui.horizontal(|ui| {
ui.label("Type:");
ui.selectable_value(floating, false, "Solid");
ui.selectable_value(floating, true, "Floating");
});
ui.horizontal(|ui| {
ui.label("Content margin:");
content_margin.ui(ui);
});
ui.horizontal(|ui| {
ui.add(DragValue::new(bar_width).range(0.0..=32.0));
ui.label("Full bar width");
});
if *floating {
ui.horizontal(|ui| {
ui.add(DragValue::new(floating_width).range(0.0..=32.0));
ui.label("Thin bar width");
});
ui.horizontal(|ui| {
ui.add(DragValue::new(floating_allocated_width).range(0.0..=32.0));
ui.label("Allocated width");
});
}
ui.horizontal(|ui| {
ui.add(DragValue::new(handle_min_length).range(0.0..=32.0));
ui.label("Minimum handle length");
});
ui.horizontal(|ui| {
ui.add(DragValue::new(bar_outer_margin).range(0.0..=32.0));
ui.label("Outer margin");
});
ui.horizontal(|ui| {
ui.label("Color:");
ui.selectable_value(foreground_color, false, "Background");
ui.selectable_value(foreground_color, true, "Foreground");
});
if *floating {
crate::Grid::new("opacity").show(ui, |ui| {
fn opacity_ui(ui: &mut Ui, opacity: &mut f32) {
ui.add(DragValue::new(opacity).speed(0.01).range(0.0..=1.0));
}
ui.label("Opacity");
ui.label("Dormant");
ui.label("Active");
ui.label("Interacting");
ui.end_row();
ui.label("Background:");
opacity_ui(ui, dormant_background_opacity);
opacity_ui(ui, active_background_opacity);
opacity_ui(ui, interact_background_opacity);
ui.end_row();
ui.label("Handle:");
opacity_ui(ui, dormant_handle_opacity);
opacity_ui(ui, active_handle_opacity);
opacity_ui(ui, interact_handle_opacity);
ui.end_row();
});
} else {
ui.horizontal(|ui| {
ui.add(DragValue::new(bar_inner_margin).range(0.0..=32.0));
ui.label("Inner margin");
});
}
ui.separator();
fade.ui(ui);
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct ScrollFadeStyle {
pub strength: f32,
pub size: f32,
}
impl Default for ScrollFadeStyle {
fn default() -> Self {
Self {
strength: 0.5,
size: 20.0,
}
}
}
impl ScrollFadeStyle {
pub fn ui(&mut self, ui: &mut Ui) {
let Self { strength, size } = self;
ui.horizontal(|ui| {
ui.add(DragValue::new(strength).speed(0.01).range(0.0..=1.0));
ui.label("Fade strength");
});
if 0.0 < *strength {
ui.horizontal(|ui| {
ui.add(DragValue::new(size).range(0.0..=64.0));
ui.label("Fade size");
});
}
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct ScrollAnimation {
pub points_per_second: f32,
pub duration: Rangef,
}
impl Default for ScrollAnimation {
fn default() -> Self {
Self {
points_per_second: 1000.0,
duration: Rangef::new(0.1, 0.3),
}
}
}
impl ScrollAnimation {
pub fn new(points_per_second: f32, duration: Rangef) -> Self {
Self {
points_per_second,
duration,
}
}
pub fn none() -> Self {
Self {
points_per_second: f32::INFINITY,
duration: Rangef::new(0.0, 0.0),
}
}
pub fn duration(t: f32) -> Self {
Self {
points_per_second: f32::INFINITY,
duration: Rangef::new(t, t),
}
}
pub fn ui(&mut self, ui: &mut crate::Ui) {
crate::Grid::new("scroll_animation").show(ui, |ui| {
ui.label("Scroll animation:");
ui.add(
DragValue::new(&mut self.points_per_second)
.speed(100.0)
.range(0.0..=5000.0),
);
ui.label("points/second");
ui.end_row();
ui.label("Min duration:");
ui.add(
DragValue::new(&mut self.duration.min)
.speed(0.01)
.range(0.0..=self.duration.max),
);
ui.label("seconds");
ui.end_row();
ui.label("Max duration:");
ui.add(
DragValue::new(&mut self.duration.max)
.speed(0.01)
.range(0.0..=1.0),
);
ui.label("seconds");
ui.end_row();
});
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct Interaction {
pub interact_radius: f32,
pub resize_grab_radius_side: f32,
pub resize_grab_radius_corner: f32,
pub show_tooltips_only_when_still: bool,
pub tooltip_delay: f32,
pub tooltip_grace_time: f32,
pub selectable_labels: bool,
pub multi_widget_text_select: bool,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct TextCursorStyle {
pub stroke: Stroke,
pub preview: bool,
pub blink: bool,
pub on_duration: f32,
pub off_duration: f32,
}
impl Default for TextCursorStyle {
fn default() -> Self {
Self {
stroke: Stroke::new(2.0, Color32::from_rgb(192, 222, 255)), preview: false,
blink: true,
on_duration: 0.5,
off_duration: 0.5,
}
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct Visuals {
pub dark_mode: bool,
pub text_options: TextOptions,
pub override_text_color: Option<Color32>,
pub weak_text_alpha: f32,
pub weak_text_color: Option<Color32>,
pub widgets: Widgets,
pub selection: Selection,
pub ime_composition: ImeComposition,
pub hyperlink_color: Color32,
pub faint_bg_color: Color32,
pub extreme_bg_color: Color32,
pub text_edit_bg_color: Option<Color32>,
pub code_bg_color: Color32,
pub warn_fg_color: Color32,
pub error_fg_color: Color32,
pub window_corner_radius: CornerRadius,
pub window_shadow: Shadow,
pub window_fill: Color32,
pub window_stroke: Stroke,
pub window_highlight_topmost: bool,
pub menu_corner_radius: CornerRadius,
pub panel_fill: Color32,
pub popup_shadow: Shadow,
pub resize_corner_size: f32,
pub text_cursor: TextCursorStyle,
pub clip_rect_margin: f32,
pub button_frame: bool,
pub collapsing_header_frame: bool,
pub indent_has_left_vline: bool,
pub striped: bool,
pub slider_trailing_fill: bool,
pub handle_shape: HandleShape,
pub interact_cursor: Option<CursorIcon>,
pub image_loading_spinners: bool,
pub numeric_color_space: NumericColorSpace,
pub disabled_alpha: f32,
}
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 {
self.weak_text_color
.unwrap_or_else(|| self.text_color().gamma_multiply(self.weak_text_alpha))
}
#[inline(always)]
pub fn strong_text_color(&self) -> Color32 {
self.widgets.active.text_color()
}
pub fn text_edit_bg_color(&self) -> Color32 {
self.text_edit_bg_color.unwrap_or(self.extreme_bg_color)
}
#[inline(always)]
pub fn window_fill(&self) -> Color32 {
self.window_fill
}
#[inline(always)]
pub fn window_stroke(&self) -> Stroke {
self.window_stroke
}
#[inline(always)]
pub fn disabled_alpha(&self) -> f32 {
self.disabled_alpha
}
#[inline(always)]
pub fn disable(&self, color: Color32) -> Color32 {
color.gamma_multiply(self.disabled_alpha())
}
#[doc(alias = "grey_out")]
#[inline(always)]
pub fn gray_out(&self, color: Color32) -> Color32 {
crate::ecolor::tint_color_towards(color, self.widgets.noninteractive.weak_bg_fill)
}
}
#[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,
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct ImeComposition {
pub active_underline_stroke: Stroke,
pub inactive_underline_stroke: Stroke,
pub legacy_visuals: bool,
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum HandleShape {
Circle,
Rect {
aspect_ratio: f32,
},
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct Widgets {
pub noninteractive: WidgetVisuals,
pub inactive: WidgetVisuals,
pub hovered: WidgetVisuals,
pub active: WidgetVisuals,
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() || response.clicked()
{
&self.active
} else if response.hovered() || response.highlighted() {
&self.hovered
} else {
&self.inactive
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct WidgetVisuals {
pub bg_fill: Color32,
pub weak_bg_fill: Color32,
pub bg_stroke: Stroke,
pub corner_radius: CornerRadius,
pub fg_stroke: Stroke,
pub expansion: f32,
}
impl WidgetVisuals {
#[inline(always)]
pub fn text_color(&self) -> Color32 {
self.fg_stroke.color
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg(debug_assertions)]
pub struct DebugOptions {
#[cfg(debug_assertions)]
pub debug_on_hover: bool,
#[cfg(debug_assertions)]
pub debug_on_hover_with_all_modifiers: bool,
#[cfg(debug_assertions)]
pub hover_shows_next: bool,
pub show_expand_width: bool,
pub show_expand_height: bool,
pub show_resize: bool,
pub show_interactive_widgets: bool,
pub show_widget_hits: bool,
pub warn_if_rect_changes_id: bool,
pub show_unaligned: bool,
pub show_focused_widget: bool,
}
#[cfg(debug_assertions)]
impl Default for DebugOptions {
fn default() -> Self {
Self {
debug_on_hover: false,
debug_on_hover_with_all_modifiers: cfg!(feature = "callstack")
&& !cfg!(target_arch = "wasm32"),
hover_shows_next: false,
show_expand_width: false,
show_expand_height: false,
show_resize: false,
show_interactive_widgets: false,
show_widget_hits: false,
warn_if_rect_changes_id: cfg!(debug_assertions),
show_unaligned: cfg!(debug_assertions),
show_focused_widget: false,
}
}
}
pub fn default_text_styles() -> BTreeMap<TextStyle, FontId> {
use FontFamily::{Monospace, Proportional};
[
(TextStyle::Small, FontId::new(9.0, Proportional)),
(TextStyle::Body, FontId::new(13.0, Proportional)),
(TextStyle::Button, FontId::new(13.0, Proportional)),
(TextStyle::Heading, FontId::new(18.0, Proportional)),
(TextStyle::Monospace, FontId::new(13.0, Monospace)),
]
.into()
}
impl Default for Style {
fn default() -> Self {
Self {
override_font_id: None,
override_text_style: None,
override_text_valign: Some(Align::Center),
text_styles: default_text_styles(),
drag_value_text_style: TextStyle::Button,
number_formatter: NumberFormatter(Arc::new(emath::format_with_decimals_in_range)),
wrap_mode: None,
spacing: Spacing::default(),
interaction: Interaction::default(),
visuals: Visuals::default(),
animation_time: 0.2,
#[cfg(debug_assertions)]
debug: Default::default(),
explanation_tooltips: false,
url_in_tooltip: false,
always_scroll_the_only_direction: false,
scroll_animation: ScrollAnimation::default(),
compact_menu_style: true,
}
}
}
impl Default for Spacing {
fn default() -> Self {
Self {
item_spacing: vec2(8.0, 3.0),
window_margin: Margin::same(6),
menu_margin: Margin::same(6),
button_padding: vec2(4.0, 1.0),
indent: 18.0, interact_size: vec2(40.0, 18.0),
slider_width: 100.0,
slider_rail_height: 8.0,
combo_width: 100.0,
text_edit_width: 280.0,
icon_width: 14.0,
icon_width_inner: 8.0,
icon_spacing: 4.0,
default_area_size: vec2(600.0, 400.0),
tooltip_width: 500.0,
menu_width: 400.0,
menu_spacing: 2.0,
combo_height: 200.0,
scroll: Default::default(),
indent_ends_with_horizontal_line: false,
}
}
}
impl Default for Interaction {
fn default() -> Self {
Self {
interact_radius: 5.0,
resize_grab_radius_side: 3.0,
resize_grab_radius_corner: 10.0,
show_tooltips_only_when_still: true,
tooltip_delay: 0.5,
tooltip_grace_time: 0.2,
selectable_labels: true,
multi_widget_text_select: true,
}
}
}
impl Visuals {
pub fn dark() -> Self {
Self {
dark_mode: true,
text_options: TextOptions {
color_transfer_function: FontColorTransferFunction::DARK_MODE_DEFAULT,
..Default::default()
},
override_text_color: None,
weak_text_alpha: 0.6,
weak_text_color: None,
widgets: Widgets::default(),
selection: Selection::default(),
ime_composition: ImeComposition::default(),
hyperlink_color: Color32::from_rgb(90, 170, 255),
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),
warn_fg_color: Color32::from_rgb(255, 143, 0), error_fg_color: Color32::from_rgb(255, 0, 0),
window_corner_radius: CornerRadius::same(6),
window_shadow: Shadow {
offset: [10, 20],
blur: 15,
spread: 0,
color: Color32::from_black_alpha(96),
},
window_fill: Color32::from_gray(27),
window_stroke: Stroke::new(1.0, Color32::from_gray(60)),
window_highlight_topmost: true,
menu_corner_radius: CornerRadius::same(6),
panel_fill: Color32::from_gray(27),
popup_shadow: Shadow {
offset: [6, 10],
blur: 8,
spread: 0,
color: Color32::from_black_alpha(96),
},
resize_corner_size: 12.0,
text_cursor: Default::default(),
clip_rect_margin: 0.0,
button_frame: true,
collapsing_header_frame: false,
indent_has_left_vline: true,
striped: false,
slider_trailing_fill: false,
handle_shape: HandleShape::Rect { aspect_ratio: 0.75 },
interact_cursor: None,
image_loading_spinners: true,
numeric_color_space: NumericColorSpace::GammaByte,
disabled_alpha: 0.5,
}
}
pub fn light() -> Self {
Self {
dark_mode: false,
text_options: TextOptions {
color_transfer_function: FontColorTransferFunction::LIGHT_MODE_DEFAULT,
..Default::default()
},
widgets: Widgets::light(),
selection: Selection::light(),
ime_composition: ImeComposition::light(),
hyperlink_color: Color32::from_rgb(0, 155, 255),
faint_bg_color: Color32::from_additive_luminance(5), extreme_bg_color: Color32::from_gray(255), code_bg_color: Color32::from_gray(230),
warn_fg_color: Color32::from_rgb(255, 100, 0), error_fg_color: Color32::from_rgb(255, 0, 0),
window_shadow: Shadow {
offset: [10, 20],
blur: 15,
spread: 0,
color: Color32::from_black_alpha(25),
},
window_fill: Color32::from_gray(248),
window_stroke: Stroke::new(1.0, Color32::from_gray(190)),
panel_fill: Color32::from_gray(248),
popup_shadow: Shadow {
offset: [6, 10],
blur: 8,
spread: 0,
color: Color32::from_black_alpha(25),
},
text_cursor: TextCursorStyle {
stroke: Stroke::new(2.0, Color32::from_rgb(0, 83, 125)),
..Default::default()
},
..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 ImeComposition {
fn dark() -> Self {
let active_underline_stroke = Stroke::new(2.0, Color32::from_rgb(192, 222, 255));
let inactive_underline_stroke = Stroke {
width: active_underline_stroke.width,
color: active_underline_stroke.color.linear_multiply(0.5),
};
Self {
active_underline_stroke,
inactive_underline_stroke,
legacy_visuals: Self::default_legacy_visuals(),
}
}
fn light() -> Self {
let active_underline_stroke = Stroke::new(2.0, Color32::from_rgb(0, 83, 125));
let inactive_underline_stroke = Stroke {
width: active_underline_stroke.width,
color: active_underline_stroke.color.linear_multiply(0.5),
};
Self {
active_underline_stroke,
inactive_underline_stroke,
legacy_visuals: Self::default_legacy_visuals(),
}
}
const fn default_legacy_visuals() -> bool {
cfg!(windows)
}
}
impl Default for ImeComposition {
fn default() -> Self {
Self::dark()
}
}
impl Widgets {
pub fn dark() -> Self {
Self {
noninteractive: WidgetVisuals {
weak_bg_fill: Color32::from_gray(27),
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(140)), corner_radius: CornerRadius::same(2),
expansion: 0.0,
},
inactive: WidgetVisuals {
weak_bg_fill: Color32::from_gray(60), bg_fill: Color32::from_gray(60), bg_stroke: Default::default(),
fg_stroke: Stroke::new(1.0, Color32::from_gray(180)), corner_radius: CornerRadius::same(2),
expansion: 0.0,
},
hovered: WidgetVisuals {
weak_bg_fill: Color32::from_gray(70),
bg_fill: Color32::from_gray(70),
bg_stroke: Stroke::new(1.0, Color32::from_gray(150)), fg_stroke: Stroke::new(1.5, Color32::from_gray(240)),
corner_radius: CornerRadius::same(3),
expansion: 0.0,
},
active: WidgetVisuals {
weak_bg_fill: Color32::from_gray(55),
bg_fill: Color32::from_gray(55),
bg_stroke: Stroke::new(1.0, Color32::WHITE),
fg_stroke: Stroke::new(2.0, Color32::WHITE),
corner_radius: CornerRadius::same(2),
expansion: 0.0,
},
open: WidgetVisuals {
weak_bg_fill: Color32::from_gray(45),
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)),
corner_radius: CornerRadius::same(2),
expansion: 0.0,
},
}
}
pub fn light() -> Self {
Self {
noninteractive: WidgetVisuals {
weak_bg_fill: Color32::from_gray(248),
bg_fill: Color32::from_gray(248),
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),
expansion: 0.0,
},
inactive: WidgetVisuals {
weak_bg_fill: Color32::from_gray(230), bg_fill: Color32::from_gray(230), bg_stroke: Default::default(),
fg_stroke: Stroke::new(1.0, Color32::from_gray(60)), corner_radius: CornerRadius::same(2),
expansion: 0.0,
},
hovered: WidgetVisuals {
weak_bg_fill: Color32::from_gray(220),
bg_fill: Color32::from_gray(220),
bg_stroke: Stroke::new(1.0, Color32::from_gray(105)), fg_stroke: Stroke::new(1.5, Color32::BLACK),
corner_radius: CornerRadius::same(3),
expansion: 0.0,
},
active: WidgetVisuals {
weak_bg_fill: Color32::from_gray(165),
bg_fill: Color32::from_gray(165),
bg_stroke: Stroke::new(1.0, Color32::BLACK),
fg_stroke: Stroke::new(2.0, Color32::BLACK),
corner_radius: CornerRadius::same(2),
expansion: 0.0,
},
open: WidgetVisuals {
weak_bg_fill: Color32::from_gray(220),
bg_fill: Color32::from_gray(220),
bg_stroke: Stroke::new(1.0, Color32::from_gray(160)),
fg_stroke: Stroke::new(1.0, Color32::BLACK),
corner_radius: CornerRadius::same(2),
expansion: 0.0,
},
}
}
}
impl Default for Widgets {
fn default() -> Self {
Self::dark()
}
}
use crate::{
Ui,
widgets::{DragValue, Slider, Widget, reset_button},
};
impl Style {
pub fn ui(&mut self, ui: &mut crate::Ui) {
let Self {
override_font_id,
override_text_style,
override_text_valign,
text_styles,
drag_value_text_style,
number_formatter: _, wrap_mode,
spacing,
interaction,
visuals,
animation_time,
#[cfg(debug_assertions)]
debug,
explanation_tooltips,
url_in_tooltip,
always_scroll_the_only_direction,
scroll_animation,
compact_menu_style,
} = self;
crate::Grid::new("_options").show(ui, |ui| {
ui.label("Override font id");
ui.vertical(|ui| {
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_salt("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();
fn valign_name(valign: Align) -> &'static str {
match valign {
Align::TOP => "Top",
Align::Center => "Center",
Align::BOTTOM => "Bottom",
}
}
ui.label("Override text valign");
crate::ComboBox::from_id_salt("override_text_valign")
.selected_text(match override_text_valign {
None => "None",
Some(override_text_valign) => valign_name(*override_text_valign),
})
.show_ui(ui, |ui| {
ui.selectable_value(override_text_valign, None, "None");
for align in [Align::TOP, Align::Center, Align::BOTTOM] {
ui.selectable_value(override_text_valign, Some(align), valign_name(align));
}
});
ui.end_row();
ui.label("Text style of DragValue");
crate::ComboBox::from_id_salt("drag_value_text_style")
.selected_text(drag_value_text_style.to_string())
.show_ui(ui, |ui| {
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(drag_value_text_style, style, text);
}
});
ui.end_row();
ui.label("Text Wrap Mode");
crate::ComboBox::from_id_salt("text_wrap_mode")
.selected_text(format!("{wrap_mode:?}"))
.show_ui(ui, |ui| {
let all_wrap_mode: Vec<Option<TextWrapMode>> = vec![
None,
Some(TextWrapMode::Extend),
Some(TextWrapMode::Wrap),
Some(TextWrapMode::Truncate),
];
for style in all_wrap_mode {
let text = crate::RichText::new(format!("{style:?}"));
ui.selectable_value(wrap_mode, style, text);
}
});
ui.end_row();
ui.label("Animation duration");
ui.add(
DragValue::new(animation_time)
.range(0.0..=1.0)
.speed(0.02)
.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("🔄 Scroll animation", |ui| scroll_animation.ui(ui));
#[cfg(debug_assertions)]
ui.collapsing("🐛 Debug", |ui| debug.ui(ui));
ui.checkbox(compact_menu_style, "Compact menu style");
ui.checkbox(explanation_tooltips, "Explanation tooltips")
.on_hover_text(
"Show explanatory text when hovering DragValue:s and other egui widgets",
);
ui.checkbox(url_in_tooltip, "Show url when hovering links");
ui.checkbox(always_scroll_the_only_direction, "Always scroll the only enabled direction")
.on_hover_text(
"If scrolling is enabled for only one direction, allow horizontal scrolling without pressing shift",
);
ui.vertical_centered(|ui| reset_button(ui, self, "Reset style"));
}
}
fn text_styles_ui(ui: &mut Ui, text_styles: &mut BTreeMap<TextStyle, FontId>) -> Response {
ui.vertical(|ui| {
crate::Grid::new("text_styles").show(ui, |ui| {
for (text_style, font_id) in &mut *text_styles {
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, "Reset 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,
slider_rail_height,
combo_width,
text_edit_width,
icon_width,
icon_width_inner,
icon_spacing,
default_area_size,
tooltip_width,
menu_width,
menu_spacing,
indent_ends_with_horizontal_line,
combo_height,
scroll,
} = self;
Grid::new("spacing")
.num_columns(2)
.spacing([12.0, 8.0])
.striped(true)
.show(ui, |ui| {
ui.label("Item spacing");
ui.add(two_drag_values(item_spacing, 0.0..=20.0));
ui.end_row();
ui.label("Window margin");
ui.add(window_margin);
ui.end_row();
ui.label("ScrollArea margin");
scroll.content_margin.ui(ui);
ui.end_row();
ui.label("Menu margin");
ui.add(menu_margin);
ui.end_row();
ui.label("Button padding");
ui.add(two_drag_values(button_padding, 0.0..=20.0));
ui.end_row();
ui.label("Interact size")
.on_hover_text("Minimum size of an interactive widget");
ui.add(two_drag_values(interact_size, 4.0..=60.0));
ui.end_row();
ui.label("Indent");
ui.add(DragValue::new(indent).range(0.0..=100.0));
ui.end_row();
ui.label("Slider width");
ui.add(DragValue::new(slider_width).range(0.0..=1000.0));
ui.end_row();
ui.label("Slider rail height");
ui.add(DragValue::new(slider_rail_height).range(0.0..=50.0));
ui.end_row();
ui.label("ComboBox width");
ui.add(DragValue::new(combo_width).range(0.0..=1000.0));
ui.end_row();
ui.label("Default area size");
ui.add(two_drag_values(default_area_size, 0.0..=1000.0));
ui.end_row();
ui.label("TextEdit width");
ui.add(DragValue::new(text_edit_width).range(0.0..=1000.0));
ui.end_row();
ui.label("Tooltip wrap width");
ui.add(DragValue::new(tooltip_width).range(0.0..=1000.0));
ui.end_row();
ui.label("Default menu width");
ui.add(DragValue::new(menu_width).range(0.0..=1000.0));
ui.end_row();
ui.label("Menu spacing")
.on_hover_text("Horizontal spacing between menus");
ui.add(DragValue::new(menu_spacing).range(0.0..=10.0));
ui.end_row();
ui.label("Checkboxes etc");
ui.vertical(|ui| {
ui.add(
DragValue::new(icon_width)
.prefix("outer icon width:")
.range(0.0..=60.0),
);
ui.add(
DragValue::new(icon_width_inner)
.prefix("inner icon width:")
.range(0.0..=60.0),
);
ui.add(
DragValue::new(icon_spacing)
.prefix("spacing:")
.range(0.0..=10.0),
);
});
ui.end_row();
});
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).range(0.0..=1000.0));
});
ui.collapsing("Scroll Area", |ui| {
scroll.ui(ui);
});
ui.vertical_centered(|ui| reset_button(ui, self, "Reset spacing"));
}
}
impl Interaction {
pub fn ui(&mut self, ui: &mut crate::Ui) {
let Self {
interact_radius,
resize_grab_radius_side,
resize_grab_radius_corner,
show_tooltips_only_when_still,
tooltip_delay,
tooltip_grace_time,
selectable_labels,
multi_widget_text_select,
} = self;
ui.spacing_mut().item_spacing = vec2(12.0, 8.0);
Grid::new("interaction")
.num_columns(2)
.striped(true)
.show(ui, |ui| {
ui.label("interact_radius")
.on_hover_text("Interact with the closest widget within this radius.");
ui.add(DragValue::new(interact_radius).range(0.0..=20.0));
ui.end_row();
ui.label("resize_grab_radius_side").on_hover_text("Radius of the interactive area of the side of a window during drag-to-resize");
ui.add(DragValue::new(resize_grab_radius_side).range(0.0..=20.0));
ui.end_row();
ui.label("resize_grab_radius_corner").on_hover_text("Radius of the interactive area of the corner of a window during drag-to-resize.");
ui.add(DragValue::new(resize_grab_radius_corner).range(0.0..=20.0));
ui.end_row();
ui.label("Tooltip delay").on_hover_text(
"Delay in seconds before showing tooltips after the mouse stops moving",
);
ui.add(
DragValue::new(tooltip_delay)
.range(0.0..=1.0)
.speed(0.05)
.suffix(" s"),
);
ui.end_row();
ui.label("Tooltip grace time").on_hover_text(
"If a tooltip is open and you hover another widget within this grace period, show the next tooltip right away",
);
ui.add(
DragValue::new(tooltip_grace_time)
.range(0.0..=1.0)
.speed(0.05)
.suffix(" s"),
);
ui.end_row();
});
ui.checkbox(
show_tooltips_only_when_still,
"Only show tooltips if mouse is still",
);
ui.horizontal(|ui| {
ui.checkbox(selectable_labels, "Selectable text in labels");
if *selectable_labels {
ui.checkbox(multi_widget_text_select, "Across multiple labels");
}
});
ui.vertical_centered(|ui| reset_button(ui, self, "Reset interaction settings"));
}
}
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);
});
}
}
impl Selection {
pub fn ui(&mut self, ui: &mut crate::Ui) {
let Self { bg_fill, stroke } = self;
ui.label("Selectable labels");
Grid::new("selectiom").num_columns(2).show(ui, |ui| {
ui.label("Background fill");
ui.color_edit_button_srgba(bg_fill);
ui.end_row();
ui.label("Stroke");
ui.add(stroke);
ui.end_row();
});
}
}
impl ImeComposition {
pub fn ui(&mut self, ui: &mut crate::Ui) {
let Self {
active_underline_stroke,
inactive_underline_stroke,
legacy_visuals,
} = self;
ui.label("IME composition");
ui.checkbox(legacy_visuals, "Legacy visuals").on_hover_text(
"If enabled, IME composition (preedit) text looks like a text selection \
with the cursor at the end. If disabled, the cursor position and active \
conversion segment are shown.",
);
Grid::new("ime_composition").num_columns(2).show(ui, |ui| {
ui.label("Active underline stroke");
ui.add(active_underline_stroke);
ui.end_row();
ui.label("Inactive underline stroke");
ui.add(inactive_underline_stroke);
ui.end_row();
});
}
}
impl WidgetVisuals {
pub fn ui(&mut self, ui: &mut crate::Ui) {
let Self {
weak_bg_fill,
bg_fill: mandatory_bg_fill,
bg_stroke,
corner_radius,
fg_stroke,
expansion,
} = self;
Grid::new("widget")
.num_columns(2)
.spacing([12.0, 8.0])
.striped(true)
.show(ui, |ui| {
ui.label("Optional background fill")
.on_hover_text("For buttons, combo-boxes, etc");
ui.color_edit_button_srgba(weak_bg_fill);
ui.end_row();
ui.label("Mandatory background fill")
.on_hover_text("For checkboxes, sliders, etc");
ui.color_edit_button_srgba(mandatory_bg_fill);
ui.end_row();
ui.label("Background stroke");
ui.add(bg_stroke);
ui.end_row();
ui.label("Corner radius");
ui.add(corner_radius);
ui.end_row();
ui.label("Foreground stroke (text)");
ui.add(fg_stroke);
ui.end_row();
ui.label("Expansion")
.on_hover_text("make shapes this much larger");
ui.add(DragValue::new(expansion).speed(0.1));
ui.end_row();
});
}
}
impl Visuals {
pub fn ui(&mut self, ui: &mut crate::Ui) {
let Self {
dark_mode,
text_options,
override_text_color: _,
weak_text_alpha,
weak_text_color,
widgets,
selection,
ime_composition,
hyperlink_color,
faint_bg_color,
extreme_bg_color,
text_edit_bg_color,
code_bg_color,
warn_fg_color,
error_fg_color,
window_corner_radius,
window_shadow,
window_fill,
window_stroke,
window_highlight_topmost,
menu_corner_radius,
panel_fill,
popup_shadow,
resize_corner_size,
text_cursor,
clip_rect_margin,
button_frame,
collapsing_header_frame,
indent_has_left_vline,
striped,
slider_trailing_fill,
handle_shape,
interact_cursor,
image_loading_spinners,
numeric_color_space,
disabled_alpha,
} = self;
fn ui_optional_color(
ui: &mut Ui,
color: &mut Option<Color32>,
default_value: Color32,
label: impl Into<WidgetText>,
) -> Response {
let label_response = ui.label(label);
ui.horizontal(|ui| {
let mut set = color.is_some();
ui.checkbox(&mut set, "");
if set {
let color = color.get_or_insert(default_value);
ui.color_edit_button_srgba(color);
} else {
*color = None;
}
});
ui.end_row();
label_response
}
ui.collapsing("Background colors", |ui| {
Grid::new("background_colors")
.num_columns(2)
.show(ui, |ui| {
fn ui_color(
ui: &mut Ui,
color: &mut Color32,
label: impl Into<WidgetText>,
) -> Response {
let label_response = ui.label(label);
ui.color_edit_button_srgba(color);
ui.end_row();
label_response
}
ui_color(ui, &mut widgets.inactive.weak_bg_fill, "Buttons");
ui_color(ui, window_fill, "Windows");
ui_color(ui, panel_fill, "Panels");
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_optional_color(ui, text_edit_bg_color, *extreme_bg_color, "TextEdit")
.on_hover_text("Background of TextEdit");
});
});
ui.collapsing("Text rendering", |ui| {
fn ui_text_color(ui: &mut Ui, color: &mut Color32, label: impl Into<RichText>) {
ui.label(label.into().color(*color));
ui.color_edit_button_srgba(color);
ui.end_row();
}
Grid::new("text_color").num_columns(2).show(ui, |ui| {
ui_text_color(ui, &mut widgets.noninteractive.fg_stroke.color, "Label");
ui_text_color(
ui,
&mut widgets.inactive.fg_stroke.color,
"Unhovered button",
);
ui_text_color(ui, &mut widgets.hovered.fg_stroke.color, "Hovered button");
ui_text_color(ui, &mut widgets.active.fg_stroke.color, "Clicked button");
ui_text_color(ui, warn_fg_color, RichText::new("Warnings"));
ui_text_color(ui, error_fg_color, RichText::new("Errors"));
ui_text_color(ui, hyperlink_color, "hyperlink_color");
ui.label(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_edit_button_srgba(code_bg_color);
ui.end_row();
ui.label("Weak text alpha");
ui.add_enabled(
weak_text_color.is_none(),
DragValue::new(weak_text_alpha).speed(0.01).range(0.0..=1.0),
);
ui.end_row();
ui_optional_color(
ui,
weak_text_color,
widgets.noninteractive.text_color(),
"Weak text color",
);
});
ui.add_space(4.0);
let TextOptions {
max_texture_side: _,
color_transfer_function,
font_hinting,
subpixel_binning,
} = text_options;
color_transfer_function_ui(ui, color_transfer_function);
ui.checkbox(font_hinting, "Font hinting (sharper text)");
ui.checkbox(subpixel_binning, "Sub-pixel binning (more even kerning)");
});
ui.collapsing("Text cursor", |ui| {
text_cursor.ui(ui);
});
ui.collapsing("Window", |ui| {
Grid::new("window")
.num_columns(2)
.spacing([12.0, 8.0])
.striped(true)
.show(ui, |ui| {
ui.label("Fill");
ui.color_edit_button_srgba(window_fill);
ui.end_row();
ui.label("Stroke");
ui.add(window_stroke);
ui.end_row();
ui.label("Corner radius");
ui.add(window_corner_radius);
ui.end_row();
ui.label("Shadow");
ui.add(window_shadow);
ui.end_row();
});
ui.checkbox(window_highlight_topmost, "Highlight topmost Window");
});
ui.collapsing("Menus and popups", |ui| {
Grid::new("menus_and_popups")
.num_columns(2)
.spacing([12.0, 8.0])
.striped(true)
.show(ui, |ui| {
ui.label("Corner radius");
ui.add(menu_corner_radius);
ui.end_row();
ui.label("Shadow");
ui.add(popup_shadow);
ui.end_row();
});
});
ui.collapsing("Widgets", |ui| widgets.ui(ui));
ui.collapsing("Selection", |ui| selection.ui(ui));
ui.collapsing("IME composition", |ui| ime_composition.ui(ui));
ui.collapsing("Misc", |ui| {
ui.add(Slider::new(resize_corner_size, 0.0..=20.0).text("resize_corner_size"));
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.checkbox(
indent_has_left_vline,
"Paint a vertical line to the left of indented regions",
);
ui.checkbox(striped, "Default stripes on grids and tables");
ui.checkbox(slider_trailing_fill, "Add trailing color to sliders");
handle_shape.ui(ui);
ComboBox::from_label("Interact cursor")
.selected_text(
interact_cursor.map_or_else(|| "-".to_owned(), |cursor| format!("{cursor:?}")),
)
.show_ui(ui, |ui| {
ui.selectable_value(interact_cursor, None, "-");
for cursor in CursorIcon::ALL {
ui.selectable_value(interact_cursor, Some(cursor), format!("{cursor:?}"))
.on_hover_cursor(cursor);
}
})
.response
.on_hover_text("Use this cursor when hovering buttons etc");
ui.checkbox(image_loading_spinners, "Image loading spinners")
.on_hover_text("Show a spinner when an Image is loading");
ui.horizontal(|ui| {
ui.label("Color picker type");
numeric_color_space.toggle_button_ui(ui);
});
ui.add(Slider::new(disabled_alpha, 0.0..=1.0).text("Disabled element alpha"));
});
let dark_mode = *dark_mode;
ui.vertical_centered(|ui| {
reset_button_with(
ui,
self,
"Reset visuals",
if dark_mode {
Self::dark()
} else {
Self::light()
},
);
});
}
}
fn color_transfer_function_ui(
ui: &mut Ui,
color_transfer_function: &mut FontColorTransferFunction,
) {
ui.horizontal(|ui| {
ui.label("Opacity tweaking:");
ui.radio_value(
color_transfer_function,
FontColorTransferFunction::Off,
"Off",
);
ui.radio_value(
color_transfer_function,
FontColorTransferFunction::DARK_MODE_DEFAULT,
"Dark-mode special",
);
let mut use_gamma = matches!(color_transfer_function, FontColorTransferFunction::Gamma(_));
ui.radio_value(&mut use_gamma, true, "Gamma function");
if use_gamma {
let mut gamma = color_transfer_function.to_gamma();
ui.add(
DragValue::new(&mut gamma)
.speed(0.01)
.range(0.1..=4.0)
.prefix("Gamma: "),
);
*color_transfer_function = FontColorTransferFunction::Gamma(gamma);
}
});
}
impl TextCursorStyle {
fn ui(&mut self, ui: &mut Ui) {
let Self {
stroke,
preview,
blink,
on_duration,
off_duration,
} = self;
ui.horizontal(|ui| {
ui.label("Stroke");
ui.add(stroke);
});
ui.checkbox(preview, "Preview text cursor on hover");
ui.checkbox(blink, "Blink");
if *blink {
Grid::new("cursor_blink").show(ui, |ui| {
ui.label("On time");
ui.add(
DragValue::new(on_duration)
.speed(0.1)
.range(0.0..=2.0)
.suffix(" s"),
);
ui.end_row();
ui.label("Off time");
ui.add(
DragValue::new(off_duration)
.speed(0.1)
.range(0.0..=2.0)
.suffix(" s"),
);
ui.end_row();
});
}
}
}
#[cfg(debug_assertions)]
impl DebugOptions {
pub fn ui(&mut self, ui: &mut crate::Ui) {
let Self {
debug_on_hover,
debug_on_hover_with_all_modifiers,
hover_shows_next,
show_expand_width,
show_expand_height,
show_resize,
show_interactive_widgets,
show_widget_hits,
warn_if_rect_changes_id,
show_unaligned,
show_focused_widget,
} = self;
{
ui.checkbox(debug_on_hover, "Show widget info on hover.");
ui.checkbox(
debug_on_hover_with_all_modifiers,
"Show widget info on hover if holding all modifier keys",
);
ui.checkbox(hover_shows_next, "Show next widget placement on hover");
}
ui.checkbox(
show_expand_width,
"Show which widgets make their parent wider",
);
ui.checkbox(
show_expand_height,
"Show which widgets make their parent higher",
);
ui.checkbox(show_resize, "Debug Resize");
ui.checkbox(
show_interactive_widgets,
"Show an overlay on all interactive widgets",
);
ui.checkbox(show_widget_hits, "Show widgets under mouse pointer");
ui.checkbox(
warn_if_rect_changes_id,
"Warn if a Rect changes Id between frames",
);
ui.checkbox(
show_unaligned,
"Show rectangles not aligned to integer point coordinates",
);
ui.checkbox(
show_focused_widget,
"Highlight which widget has keyboard focus",
);
ui.vertical_centered(|ui| reset_button(ui, self, "Reset debug options"));
}
}
fn two_drag_values(value: &mut Vec2, range: std::ops::RangeInclusive<f32>) -> impl Widget + '_ {
move |ui: &mut crate::Ui| {
ui.horizontal(|ui| {
ui.add(
DragValue::new(&mut value.x)
.range(range.clone())
.prefix("x: "),
);
ui.add(
DragValue::new(&mut value.y)
.range(range.clone())
.prefix("y: "),
);
})
.response
}
}
impl HandleShape {
pub fn ui(&mut self, ui: &mut Ui) {
ui.horizontal(|ui| {
ui.label("Slider handle");
ui.radio_value(self, Self::Circle, "Circle");
if ui
.radio(matches!(self, Self::Rect { .. }), "Rectangle")
.clicked()
{
*self = Self::Rect { aspect_ratio: 0.5 };
}
if let Self::Rect { aspect_ratio } = self {
ui.add(Slider::new(aspect_ratio, 0.1..=3.0).text("Aspect ratio"));
}
});
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum NumericColorSpace {
GammaByte,
Linear,
}
impl NumericColorSpace {
pub fn toggle_button_ui(&mut self, ui: &mut Ui) -> crate::Response {
let tooltip = match self {
Self::GammaByte => "Showing color values in 0-255 gamma space",
Self::Linear => "Showing color values in 0-1 linear space",
};
let mut response = ui.button(self.to_string()).on_hover_text(tooltip);
if response.clicked() {
*self = match self {
Self::GammaByte => Self::Linear,
Self::Linear => Self::GammaByte,
};
response.mark_changed();
}
response
}
}
impl std::fmt::Display for NumericColorSpace {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::GammaByte => write!(f, "U8"),
Self::Linear => write!(f, "F"),
}
}
}
impl Widget for &mut Margin {
fn ui(self, ui: &mut Ui) -> Response {
let mut same = self.is_same();
let response = if same {
ui.horizontal(|ui| {
ui.checkbox(&mut same, "same");
let mut value = self.left;
ui.add(DragValue::new(&mut value).range(0.0..=100.0));
*self = Margin::same(value);
})
.response
} else {
ui.vertical(|ui| {
ui.checkbox(&mut same, "same");
crate::Grid::new("margin").num_columns(2).show(ui, |ui| {
ui.label("Left");
ui.add(DragValue::new(&mut self.left).range(0.0..=100.0));
ui.end_row();
ui.label("Right");
ui.add(DragValue::new(&mut self.right).range(0.0..=100.0));
ui.end_row();
ui.label("Top");
ui.add(DragValue::new(&mut self.top).range(0.0..=100.0));
ui.end_row();
ui.label("Bottom");
ui.add(DragValue::new(&mut self.bottom).range(0.0..=100.0));
ui.end_row();
});
})
.response
};
if same {
*self =
Margin::from((self.leftf() + self.rightf() + self.topf() + self.bottomf()) / 4.0);
} else {
if self.is_same() {
if self.right == i8::MAX {
self.right = i8::MAX - 1;
} else {
self.right += 1;
}
}
}
response
}
}
impl Widget for &mut CornerRadius {
fn ui(self, ui: &mut Ui) -> Response {
let mut same = self.is_same();
let response = if same {
ui.horizontal(|ui| {
ui.checkbox(&mut same, "same");
let mut cr = self.nw;
ui.add(DragValue::new(&mut cr).range(0.0..=f32::INFINITY));
*self = CornerRadius::same(cr);
})
.response
} else {
ui.vertical(|ui| {
ui.checkbox(&mut same, "same");
crate::Grid::new("Corner radius")
.num_columns(2)
.show(ui, |ui| {
ui.label("NW");
ui.add(DragValue::new(&mut self.nw).range(0.0..=f32::INFINITY));
ui.end_row();
ui.label("NE");
ui.add(DragValue::new(&mut self.ne).range(0.0..=f32::INFINITY));
ui.end_row();
ui.label("SW");
ui.add(DragValue::new(&mut self.sw).range(0.0..=f32::INFINITY));
ui.end_row();
ui.label("SE");
ui.add(DragValue::new(&mut self.se).range(0.0..=f32::INFINITY));
ui.end_row();
});
})
.response
};
if same {
*self = CornerRadius::from(self.average());
} else {
if self.is_same() {
if self.average() == 0.0 {
self.se = 1;
} else {
self.se -= 1;
}
}
}
response
}
}
impl Widget for &mut Shadow {
fn ui(self, ui: &mut Ui) -> Response {
let epaint::Shadow {
offset,
blur,
spread,
color,
} = self;
ui.vertical(|ui| {
crate::Grid::new("shadow_ui").show(ui, |ui| {
ui.add(
DragValue::new(&mut offset[0])
.speed(1.0)
.range(-100.0..=100.0)
.prefix("x: "),
);
ui.add(
DragValue::new(&mut offset[1])
.speed(1.0)
.range(-100.0..=100.0)
.prefix("y: "),
);
ui.end_row();
ui.add(
DragValue::new(blur)
.speed(1.0)
.range(0.0..=100.0)
.prefix("blur: "),
);
ui.add(
DragValue::new(spread)
.speed(1.0)
.range(0.0..=100.0)
.prefix("spread: "),
);
});
ui.color_edit_button_srgba(color);
})
.response
}
}
impl Widget for &mut Stroke {
fn ui(self, ui: &mut Ui) -> Response {
let Stroke { width, color } = self;
ui.horizontal(|ui| {
ui.add(DragValue::new(width).speed(0.1).range(0.0..=1e9))
.on_hover_text("Width");
ui.color_edit_button_srgba(color);
let (_id, stroke_rect) = ui.allocate_space(ui.spacing().interact_size);
let left = stroke_rect.left_center();
let right = stroke_rect.right_center();
ui.painter().line_segment([left, right], (*width, *color));
})
.response
}
}
impl Widget for &mut crate::Frame {
fn ui(self, ui: &mut Ui) -> Response {
let crate::Frame {
inner_margin,
outer_margin,
corner_radius,
shadow,
fill,
stroke,
} = self;
crate::Grid::new("frame")
.num_columns(2)
.spacing([12.0, 8.0])
.striped(true)
.show(ui, |ui| {
ui.label("Inner margin");
ui.add(inner_margin);
ui.end_row();
ui.label("Outer margin");
ui.push_id("outer", |ui| ui.add(outer_margin));
ui.end_row();
ui.label("Corner radius");
ui.add(corner_radius);
ui.end_row();
ui.label("Shadow");
ui.add(shadow);
ui.end_row();
ui.label("Fill");
ui.color_edit_button_srgba(fill);
ui.end_row();
ui.label("Stroke");
ui.add(stroke);
ui.end_row();
})
.response
}
}
pub fn font_tweak_ui(ui: &mut Ui, tweak: &mut FontTweak, axes: &[FontVariationAxis]) -> Response {
let original: FontTweak = tweak.clone();
let mut response = Grid::new("font_tweak")
.num_columns(2)
.show(ui, |ui| {
let FontTweak {
scale,
y_offset_factor,
y_offset,
hinting,
hinting_target,
coords,
thin_space_width,
tab_size,
subpixel_binning,
} = tweak;
ui.label("Scale");
let speed = *scale * 0.01;
ui.add(DragValue::new(scale).range(0.01..=10.0).speed(speed));
ui.end_row();
ui.label("y_offset_factor");
ui.add(DragValue::new(y_offset_factor).speed(-0.0025));
ui.end_row();
ui.label("y_offset");
ui.add(DragValue::new(y_offset).speed(-0.02));
ui.end_row();
ui.label("hinting");
ui.horizontal(|ui| {
ui.radio_value(hinting, Some(true), "on");
ui.radio_value(hinting, Some(false), "off");
ui.radio_value(hinting, None, "default");
});
ui.end_row();
ui.label("hinting_target")
.on_hover_text("How aggressively to snap glyph outlines to the pixel grid. Only matters when hinting is enabled.");
ui.vertical(|ui| {
ui.horizontal(|ui| {
let is_mono = matches!(hinting_target, HintingTarget::Mono);
if ui
.radio(!is_mono, "Smooth")
.on_hover_text("Hinting tuned for anti-aliased rendering. The normal choice.")
.clicked()
&& is_mono
{
*hinting_target = HintingTarget::default();
}
if ui
.radio(is_mono, "Mono")
.on_hover_text(
"Strongest hinting (designed for 1-bit rendering). Sharpest, but \
distorts glyph weight across sizes.",
)
.clicked()
{
*hinting_target = HintingTarget::Mono;
}
if ui
.button("Reset")
.on_hover_text("Reset the hinting target to its default.")
.clicked()
{
*hinting_target = HintingTarget::default();
}
});
if let HintingTarget::Smooth(SmoothHinting {
light,
symmetric_rendering,
preserve_linear_metrics,
}) = hinting_target
{
ui.checkbox(light, "light").on_hover_text(
"Hint only vertically, preserving the font's horizontal proportions \
(softer). Off also fits horizontally.",
);
ui.checkbox(symmetric_rendering, "symmetric_rendering").on_hover_text(
"Render glyphs the same regardless of sub-pixel position (good for \
caching/animation), but can blur stems. Only affects interpreter-hinted fonts.",
);
ui.checkbox(preserve_linear_metrics, "preserve_linear_metrics").on_hover_text(
"Keep spacing independent of hinting. Off lets the hinter snap \
horizontally for crisper vertical stems on low-dpi screens.",
);
}
});
ui.end_row();
ui.label("subpixel_binning");
ui.horizontal(|ui| {
ui.radio_value(subpixel_binning, Some(true), "on");
ui.radio_value(subpixel_binning, Some(false), "off");
ui.radio_value(subpixel_binning, None, "default");
});
ui.end_row();
ui.label("thin_space_width");
ui.horizontal(|ui| {
ui.add(
DragValue::new(thin_space_width)
.range(0.0..=1.0)
.speed(0.01),
);
ui.label("1\u{2009}234\u{2009}567\u{2009}890");
});
ui.end_row();
ui.label("tab_size");
ui.add(DragValue::new(tab_size).range(0.0..=16.0).speed(0.1));
ui.end_row();
for axis in axes.iter().filter(|axis| !axis.hidden) {
match &axis.name {
Some(name) => ui.label(format!("{name} ({})", axis.tag)),
None => ui.label(axis.tag.to_string()),
};
let existing = coords.as_ref().iter().position(|(tag, _)| *tag == axis.tag);
let mut value = existing.map_or(axis.default, |i| coords.as_ref()[i].1);
ui.horizontal(|ui| {
if ui.add(Slider::new(&mut value, axis.range)).changed() {
match existing {
Some(i) => coords.as_mut()[i].1 = value,
None => coords.push(axis.tag, value),
}
}
if existing.is_some()
&& ui
.small_button("⟲")
.on_hover_text("Reset to the font's default value")
.clicked()
&& let Some(i) =
coords.as_ref().iter().position(|(tag, _)| *tag == axis.tag)
{
coords.remove(i);
}
});
ui.end_row();
}
if ui.button("Reset").clicked() {
*tweak = Default::default();
}
})
.response;
if *tweak != original {
response.mark_changed();
}
response
}
impl Widget for &mut FontTweak {
fn ui(self, ui: &mut Ui) -> Response {
font_tweak_ui(ui, self, &[])
}
}