Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
835db5c
style by widget
Oct 12, 2025
a99adc6
Finish button : retrocompatibility + comments
Oct 13, 2025
ad4b0de
fix some logic
Oct 15, 2025
f66b5b5
apply style to label
Oct 15, 2025
e39d8c4
Finish label
Oct 16, 2025
1e346da
CheckboxStyle
Oct 16, 2025
b18070c
checkbox & cleanup
Oct 21, 2025
51c6c42
Button resize fix + Margin i8 -> i16
Oct 21, 2025
f97ade3
check passed
Nov 7, 2025
e99ce8d
style by widget
Oct 12, 2025
c2e62f8
Finish button : retrocompatibility + comments
Oct 13, 2025
ce5c9cd
fix some logic
Oct 15, 2025
5265d4e
apply style to label
Oct 15, 2025
a40959b
Finish label
Oct 16, 2025
785e19f
CheckboxStyle
Oct 16, 2025
509d17d
checkbox & cleanup
Oct 21, 2025
2df6154
Button resize fix + Margin i8 -> i16
Oct 21, 2025
daccad6
check passed
Nov 7, 2025
b938a27
temp
Nov 7, 2025
a748515
fix snapshot
Nov 7, 2025
94f8ad6
fix check
Nov 7, 2025
33db68f
Fix pixel diff
Nov 10, 2025
abeb7a2
Merge branch 'widget_style' of github.com:AdrienZianne/egui into widg…
Nov 10, 2025
e1af887
more pixel diff fix
Nov 10, 2025
4bd97ce
revert gitignore
Nov 10, 2025
ec558b8
Merge branch 'main' into widget_style
AdrienZianne Nov 10, 2025
db4c159
Merge branch 'widget_style' of github.com:AdrienZianne/egui into widg…
Nov 10, 2025
a17aa67
cleanup
Nov 10, 2025
c3ce6bf
cleanup snapshot
Nov 10, 2025
34bf318
fix features
Nov 12, 2025
2f917bc
fix text color override
Nov 12, 2025
0b9f70d
remove unused import
Nov 12, 2025
58992a6
rename style_trait.rs -> widget_style.rs and font/color fallback for …
Nov 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/egui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,7 @@ mod ui_stack;
pub mod util;
pub mod viewport;
mod widget_rect;
pub mod widget_style;
pub mod widget_text;
pub mod widgets;

Expand Down
201 changes: 201 additions & 0 deletions crates/egui/src/widget_style.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
use emath::Vec2;
use epaint::{Color32, FontId, Shadow, Stroke, text::TextWrapMode};

use crate::{
Frame, Response, Style, TextStyle,
style::{WidgetVisuals, Widgets},
};

/// General text style
pub struct TextVisuals {
/// Font used
pub font_id: FontId,

/// Font color
pub color: Color32,

/// Text decoration
pub underline: Stroke,
pub strikethrough: Stroke,
}

/// General widget style
pub struct WidgetStyle {
pub frame: Frame,

pub text: TextVisuals,

pub stroke: Stroke,
}

pub struct ButtonStyle {
pub frame: Frame,
pub text_style: TextVisuals,
}

pub struct CheckboxStyle {
/// Frame around
pub frame: Frame,

/// Text next to it
pub text_style: TextVisuals,

/// Checkbox size
pub checkbox_size: f32,

/// Checkmark size
pub check_size: f32,

/// Frame of the checkbox itself
pub checkbox_frame: Frame,

/// Checkmark stroke
pub check_stroke: Stroke,
}

pub struct LabelStyle {
/// Frame around
pub frame: Frame,

/// Text style
pub text: TextVisuals,

/// Wrap mode used
pub wrap_mode: TextWrapMode,
}

pub struct SeparatorStyle {
/// How much space is allocated in the layout direction
pub spacing: f32,

/// How to paint it
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,
inner_margin: self.spacing.button_padding.into(),
..Default::default()
},
stroke: visuals.fg_stroke,
text: TextVisuals {
color: self
.visuals
.override_text_color
.unwrap_or(visuals.text_color()),
font_id: font_id.unwrap_or(TextStyle::Body.resolve(self)),
strikethrough: Stroke::NONE,
underline: Stroke::NONE,
},
}
}

pub fn button_style(&self, state: WidgetState, selected: bool) -> ButtonStyle {
let mut visuals = *self.visuals.widgets.state(state);
let mut ws = self.widget_style(state);

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;
ws.text.color = self.visuals.selection.stroke.color;
}

ButtonStyle {
frame: Frame {
fill: visuals.weak_bg_fill,
stroke: visuals.bg_stroke,
corner_radius: visuals.corner_radius,
outer_margin: (-Vec2::splat(visuals.expansion)).into(),
inner_margin: (self.spacing.button_padding + Vec2::splat(visuals.expansion)
- Vec2::splat(visuals.bg_stroke.width))
.into(),
..Default::default()
},
text_style: ws.text,
}
}

pub fn checkbox_style(&self, state: WidgetState) -> CheckboxStyle {
let visuals = self.visuals.widgets.state(state);
let ws = self.widget_style(state);
CheckboxStyle {
frame: Frame::new(),
checkbox_size: self.spacing.icon_width,
check_size: self.spacing.icon_width_inner,
checkbox_frame: Frame {
fill: visuals.bg_fill,
corner_radius: visuals.corner_radius,
stroke: visuals.bg_stroke,
..Default::default()
},
text_style: ws.text,
check_stroke: ws.stroke,
}
}

pub fn label_style(&self, state: WidgetState) -> LabelStyle {
let ws = self.widget_style(state);
LabelStyle {
frame: Frame {
fill: ws.frame.fill,
inner_margin: 0.0.into(),
outer_margin: 0.0.into(),
stroke: Stroke::NONE,
shadow: Shadow::NONE,
corner_radius: 0.into(),
},
text: ws.text,
wrap_mode: TextWrapMode::Wrap,
}
}

pub fn separator_style(&self, _state: WidgetState) -> SeparatorStyle {
let visuals = self.visuals.noninteractive();
SeparatorStyle {
spacing: 6.0,
stroke: visuals.bg_stroke,
}
}
}
77 changes: 44 additions & 33 deletions crates/egui/src/widgets/button.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use epaint::Margin;

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,
widget_style::{ButtonStyle, WidgetState},
};

/// Clickable button with text.
Expand Down Expand Up @@ -272,6 +275,7 @@ impl<'a> Button<'a> {
limit_image_size,
} = self;

// Min size height always equal or greater than interact size if not small
if !small {
min_size.y = min_size.y.at_least(ui.spacing().interact_size.y);
}
Expand All @@ -290,51 +294,58 @@ impl<'a> Button<'a> {

let has_frame_margin = frame.unwrap_or_else(|| ui.visuals().button_frame);

let id = ui.next_auto_id();
let response: Option<Response> = ui.ctx().read_response(id);
let state = response.map(|r| r.widget_state()).unwrap_or_default();

let ButtonStyle { frame, text_style } = ui.style().button_style(state, selected);

let mut button_padding = if has_frame_margin {
ui.spacing().button_padding
frame.inner_margin
} else {
Vec2::ZERO
Margin::ZERO
};

if small {
button_padding.y = 0.0;
button_padding.bottom = 0;
button_padding.top = 0;
}

let mut prepared = layout
.frame(Frame::new().inner_margin(button_padding))
.min_size(min_size)
.allocate(ui);
// Override global style by local style
let mut frame = frame;
if let Some(fill) = fill {
frame = frame.fill(fill);
}
if let Some(corner_radius) = corner_radius {
frame = frame.corner_radius(corner_radius);
}
if let Some(stroke) = stroke {
frame = frame.stroke(stroke);
}

let response = if ui.is_rect_visible(prepared.response.rect) {
let visuals = ui.style().interact_selectable(&prepared.response, selected);
frame = frame.inner_margin(button_padding);

let visible_frame = if frame_when_inactive {
has_frame_margin
} else {
has_frame_margin
&& (prepared.response.hovered()
|| prepared.response.is_pointer_button_down_on()
|| prepared.response.has_focus())
};
// Apply the style font and color as fallback
layout = layout
.fallback_font(text_style.font_id.clone())
.fallback_text_color(text_style.color);

// Retrocompatibility with button settings
layout = if has_frame_margin && (state != WidgetState::Inactive || frame_when_inactive) {
layout.frame(frame)
} else {
layout.frame(Frame::new().inner_margin(frame.inner_margin))
};

let mut prepared = layout.min_size(min_size).allocate(ui);

// Get AtomLayoutResponse, empty if not visible
let response = if ui.is_rect_visible(prepared.response.rect) {
if image_tint_follows_text_color {
prepared.map_images(|image| image.tint(visuals.text_color()));
prepared.map_images(|image| image.tint(ui.visuals().text_color()));
}

prepared.fallback_text_color = visuals.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.fallback_text_color = ui.visuals().text_color();

prepared.paint(ui)
} else {
Expand Down
Loading
Loading