From 835db5cba6b7d62f3af3bcc17c08aaf29a96e655 Mon Sep 17 00:00:00 2001 From: adrien <221212@umons.ac.be> Date: Mon, 13 Oct 2025 00:01:34 +0200 Subject: [PATCH] style by widget --- crates/egui/src/lib.rs | 1 + crates/egui/src/style_trait.rs | 188 ++++++++++++++++++++++++++ crates/egui/src/widgets/button.rs | 55 ++++---- crates/egui/src/widgets/drag_value.rs | 13 +- 4 files changed, 224 insertions(+), 33 deletions(-) create mode 100644 crates/egui/src/style_trait.rs diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 960480b23..c0c68884a 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -434,6 +434,7 @@ mod plugin; pub mod response; mod sense; pub mod style; +pub mod style_trait; pub mod text_selection; mod ui; mod ui_builder; diff --git a/crates/egui/src/style_trait.rs b/crates/egui/src/style_trait.rs new file mode 100644 index 000000000..079013649 --- /dev/null +++ b/crates/egui/src/style_trait.rs @@ -0,0 +1,188 @@ +use emath::Vec2; +use epaint::{Color32, FontId, Stroke}; + +use crate::{ + Frame, Response, Style, + style::{WidgetVisuals, Widgets}, +}; + +/// General text style +pub struct TextVisuals { + /// Font used + pub font_id: FontId, + /// Font color + pub color: Color32, +} + +/// General widget style +pub struct WidgetStyle { + pub frame: Frame, + + pub text: TextVisuals, + + pub stroke: Stroke, +} + +pub struct ButtonStyle { + pub frame: Frame, + pub text: TextVisuals, +} + +pub struct CheckboxStyle { + /// Frame around + pub frame: Frame, + /// Text next to it + pub text: TextVisuals, + /// Size + pub size: Vec2, + /// Frame of the checkbox itself + pub checkbox_frame: Frame, + /// Checkmark stroke + pub stroke: Stroke, +} + +pub struct DragValueStyle { + /// Frame around + pub frame: Frame, + /// Text of the value + pub text: TextVisuals, + pub min_size: Vec2, +} + +pub struct HyperlinkStyle { + pub frame: Frame, + pub text: TextVisuals, + pub size: Vec2, + pub checkbox_frame: Frame, + pub stroke: Stroke, +} + +pub struct ImageStyle { + pub frame: Frame, + pub text: TextVisuals, + pub size: Vec2, + pub checkbox_frame: Frame, + pub stroke: Stroke, +} + +pub struct LabelStyle { + pub frame: Frame, + pub text: TextVisuals, + pub size: Vec2, + pub checkbox_frame: Frame, + pub stroke: Stroke, +} + +pub struct RadioButtonStyle { + pub frame: Frame, + pub text: TextVisuals, + pub size: Vec2, + pub checkbox_frame: Frame, + pub stroke: Stroke, +} + +pub struct SeparatorStyle { + pub size: f32, + pub stroke: Stroke, +} + +pub struct SliderStyle { + pub frame: Frame, + pub text: TextVisuals, + pub size: Vec2, + pub checkbox_frame: Frame, + pub stroke: Stroke, +} + +pub struct SpinnerStyle { + pub frame: Frame, + pub text: TextVisuals, + pub size: Vec2, + pub checkbox_frame: Frame, + pub stroke: Stroke, +} + +#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)] +pub enum WidgetState { + Noninteractive, + #[default] + Inactive, + Hovered, + Active, +} + +impl Widgets { + pub fn state(&self, state: WidgetState) -> &WidgetVisuals { + match state { + WidgetState::Noninteractive => &self.noninteractive, + WidgetState::Inactive => &self.inactive, + WidgetState::Hovered => &self.hovered, + WidgetState::Active => &self.active, + } + } +} + +impl Response { + pub fn widget_state(&self) -> WidgetState { + if !self.sense.interactive() { + WidgetState::Noninteractive + } else if self.is_pointer_button_down_on() || self.has_focus() || self.clicked() { + WidgetState::Active + } else if self.hovered() || self.highlighted() { + WidgetState::Hovered + } else { + WidgetState::Inactive + } + } +} + +impl Style { + pub fn widget_style(&self, state: WidgetState) -> WidgetStyle { + let visuals = self.visuals.widgets.state(state); + let font_id = self.override_font_id.clone(); + WidgetStyle { + frame: Frame { + fill: visuals.bg_fill, + stroke: visuals.bg_stroke, + corner_radius: visuals.corner_radius, + ..Default::default() + }, + stroke: visuals.fg_stroke, + text: TextVisuals { + color: visuals.fg_stroke.color, + font_id: font_id.unwrap_or(FontId::new(13.0, epaint::FontFamily::Proportional)), + }, + } + } + + pub fn button_style(&self, state: WidgetState) -> ButtonStyle { + let ws = self.widget_style(state); + ButtonStyle { + frame: ws.frame.inner_margin(self.spacing.button_padding), + text: ws.text, + } + } + + // pub fn checkbox_style(&self, state: WidgetState) -> CheckboxStylee {} + + // pub fn label_style(&self, state: WidgetState) -> LabelStyle {} + + pub fn drag_value_style(&self, state: WidgetState) -> DragValueStyle { + let ws = self.widget_style(state); + DragValueStyle { + frame: ws.frame.inner_margin(self.spacing.button_padding), + min_size: self.spacing.interact_size, + text: ws.text, + } + } + + // pub fn hyperlink_style(&self, state: WidgetState) -> HyperlinkStyle {} + + // pub fn image_style(&self, state: WidgetState) -> ImageStyle {} + + // pub fn slider_style(&self, state: WidgetState) -> SliderStyle {} + + // pub fn separator_style(&self, state: WidgetState) -> SeparatorStyle {} + + // pub fn spinner_style(&self, state: WidgetState) -> SpinnerStyle {} +} diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index af31b40af..00d438959 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -1,7 +1,9 @@ +use std::{mem, sync::Arc}; + use crate::{ Atom, AtomExt as _, AtomKind, AtomLayout, AtomLayoutResponse, Color32, CornerRadius, Frame, - Image, IntoAtoms, NumExt as _, Response, Sense, Stroke, TextStyle, TextWrapMode, Ui, Vec2, - Widget, WidgetInfo, WidgetText, WidgetType, + Image, IntoAtoms, NumExt as _, Response, RichText, Sense, Stroke, TextStyle, TextWrapMode, Ui, + Vec2, Widget, WidgetInfo, WidgetText, WidgetType, }; /// Clickable button with text. @@ -281,25 +283,30 @@ impl<'a> Button<'a> { let text = layout.text().map(String::from); + let id = ui.next_auto_id(); + let response: Option = ui.ctx().read_response(id); + let state = response.map(|r| r.widget_state()).unwrap_or_default(); + + let style = ui.style().button_style(state); + let has_frame_margin = frame.unwrap_or_else(|| ui.visuals().button_frame); - let mut button_padding = if has_frame_margin { - ui.spacing().button_padding - } else { - Vec2::ZERO - }; - if small { - button_padding.y = 0.0; - } + layout.map_texts(|t| match t { + WidgetText::RichText(mut text) => { + let text_mut = Arc::make_mut(&mut text); + *text_mut = mem::take(text_mut).font(style.text.font_id.clone()); + WidgetText::RichText(text) + } + WidgetText::Text(text) => { + let rich_text = RichText::new(text.clone()).font(style.text.font_id.clone()); + WidgetText::RichText(Arc::new(rich_text)) + } + w => w, + }); - let mut prepared = layout - .frame(Frame::new().inner_margin(button_padding)) - .min_size(min_size) - .allocate(ui); + let mut prepared = layout.frame(style.frame).min_size(min_size).allocate(ui); let response = if ui.is_rect_visible(prepared.response.rect) { - let visuals = ui.style().interact_selectable(&prepared.response, selected); - let visible_frame = if frame_when_inactive { has_frame_margin } else { @@ -310,23 +317,13 @@ impl<'a> Button<'a> { }; if image_tint_follows_text_color { - prepared.map_images(|image| image.tint(visuals.text_color())); + prepared.map_images(|image| image.tint(style.text.color)); } - prepared.fallback_text_color = visuals.text_color(); + prepared.fallback_text_color = style.text.color; if visible_frame { - let stroke = stroke.unwrap_or(visuals.bg_stroke); - let fill = fill.unwrap_or(visuals.weak_bg_fill); - prepared.frame = prepared - .frame - .inner_margin( - button_padding + Vec2::splat(visuals.expansion) - Vec2::splat(stroke.width), - ) - .outer_margin(-Vec2::splat(visuals.expansion)) - .fill(fill) - .stroke(stroke) - .corner_radius(corner_radius.unwrap_or(visuals.corner_radius)); + prepared.frame = style.frame; } prepared.paint(ui) diff --git a/crates/egui/src/widgets/drag_value.rs b/crates/egui/src/widgets/drag_value.rs index 9515726c2..1c4caf995 100644 --- a/crates/egui/src/widgets/drag_value.rs +++ b/crates/egui/src/widgets/drag_value.rs @@ -4,7 +4,7 @@ use std::{cmp::Ordering, ops::RangeInclusive}; use crate::{ Button, CursorIcon, Id, Key, MINUS_CHAR_STR, Modifiers, NumExt as _, Response, RichText, Sense, - TextEdit, TextWrapMode, Ui, Widget, WidgetInfo, emath, text, + TextEdit, TextWrapMode, Ui, Widget, WidgetInfo, emath, grid::State, text, }; // ---------------------------------------------------------------------------- @@ -447,6 +447,10 @@ impl Widget for DragValue<'_> { let id = ui.next_auto_id(); let is_slow_speed = shift && ui.ctx().is_being_dragged(id); + let response = ui.ctx().read_response(id); + let state = response.map(|r| r.widget_state()).unwrap_or_default(); + let style = ui.style().drag_value_style(state); + // The following ensures that when a `DragValue` receives focus, // it is immediately rendered in edit mode, rather than being rendered // in button mode for just one frame. This is important for @@ -560,13 +564,14 @@ impl Widget for DragValue<'_> { .clip_text(false) .horizontal_align(ui.layout().horizontal_align()) .vertical_align(ui.layout().vertical_align()) - .margin(ui.spacing().button_padding) - .min_size(ui.spacing().interact_size) + .margin(style.frame.inner_margin) + .min_size(style.min_size) .id(id) .desired_width( ui.spacing().interact_size.x - 2.0 * ui.spacing().button_padding.x, ) - .font(text_style), + .text_color(style.text.color) + .font(style.text.font_id), ); // Select all text when the edit gains focus.