diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 2e44e25fb..dbe8be16d 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -424,6 +424,7 @@ mod ui_stack; pub mod util; pub mod viewport; mod widget_rect; +pub mod widget_style; pub mod widget_text; pub mod widgets; diff --git a/crates/egui/src/widget_style.rs b/crates/egui/src/widget_style.rs new file mode 100644 index 000000000..c459cc81a --- /dev/null +++ b/crates/egui/src/widget_style.rs @@ -0,0 +1,203 @@ +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_else(|| visuals.text_color()), + font_id: font_id.unwrap_or_else(|| 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, + // Use the inner_margin for the expansion + inner_margin: visuals.expansion.into(), + ..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, + } + } +} diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index ccb1db69f..4b4c2fe32 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -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. @@ -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); } @@ -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 = 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); + } + frame = frame.inner_margin(button_padding); + + // 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) { - let visuals = ui.style().interact_selectable(&prepared.response, selected); - - 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()) - }; - if image_tint_follows_text_color { - prepared.map_images(|image| image.tint(visuals.text_color())); + prepared.map_images(|image| image.tint(text_style.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 = text_style.color; prepared.paint(ui) } else { diff --git a/crates/egui/src/widgets/checkbox.rs b/crates/egui/src/widgets/checkbox.rs index c90cca292..3d0dbc367 100644 --- a/crates/egui/src/widgets/checkbox.rs +++ b/crates/egui/src/widgets/checkbox.rs @@ -1,6 +1,8 @@ +use emath::Rect; + use crate::{ Atom, AtomLayout, Atoms, Id, IntoAtoms, NumExt as _, Response, Sense, Shape, Ui, Vec2, Widget, - WidgetInfo, WidgetType, epaint, pos2, + WidgetInfo, WidgetType, epaint, pos2, widget_style::CheckboxStyle, }; // TODO(emilk): allow checkbox without a text label @@ -55,14 +57,25 @@ impl Widget for Checkbox<'_> { indeterminate, } = self; - let spacing = &ui.spacing(); - let icon_width = spacing.icon_width; + // Get the widget style by reading the response from the previous pass + 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 mut min_size = Vec2::splat(spacing.interact_size.y); - min_size.y = min_size.y.at_least(icon_width); + let CheckboxStyle { + check_size, + checkbox_frame, + checkbox_size, + frame, + check_stroke, + text_style, + } = ui.style().checkbox_style(state); + + let mut min_size = Vec2::splat(ui.spacing().interact_size.y); + min_size.y = min_size.y.at_least(checkbox_size); // In order to center the checkbox based on min_size we set the icon height to at least min_size.y - let mut icon_size = Vec2::splat(icon_width); + let mut icon_size = Vec2::splat(checkbox_size); icon_size.y = icon_size.y.at_least(min_size.y); let rect_id = Id::new("egui::checkbox"); atoms.push_left(Atom::custom(rect_id, icon_size)); @@ -72,6 +85,7 @@ impl Widget for Checkbox<'_> { let mut prepared = AtomLayout::new(atoms) .sense(Sense::click()) .min_size(min_size) + .frame(frame) .allocate(ui); if prepared.response.clicked() { @@ -96,18 +110,21 @@ impl Widget for Checkbox<'_> { }); if ui.is_rect_visible(prepared.response.rect) { - // let visuals = ui.style().interact_selectable(&response, *checked); // too colorful - let visuals = *ui.style().interact(&prepared.response); - prepared.fallback_text_color = visuals.text_color(); + prepared.fallback_text_color = text_style.color; let response = prepared.paint(ui); if let Some(rect) = response.rect(rect_id) { - let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect); + let big_icon_rect = Rect::from_center_size( + pos2(rect.left() + checkbox_size / 2.0, rect.center().y), + Vec2::splat(checkbox_size), + ); + let small_icon_rect = + Rect::from_center_size(big_icon_rect.center(), Vec2::splat(check_size)); ui.painter().add(epaint::RectShape::new( - big_icon_rect.expand(visuals.expansion), - visuals.corner_radius, - visuals.bg_fill, - visuals.bg_stroke, + big_icon_rect.expand(checkbox_frame.inner_margin.left.into()), + checkbox_frame.corner_radius, + checkbox_frame.fill, + checkbox_frame.stroke, epaint::StrokeKind::Inside, )); @@ -116,7 +133,7 @@ impl Widget for Checkbox<'_> { ui.painter().add(Shape::hline( small_icon_rect.x_range(), small_icon_rect.center().y, - visuals.fg_stroke, + check_stroke, )); } else if *checked { // Check mark: @@ -126,7 +143,7 @@ impl Widget for Checkbox<'_> { pos2(small_icon_rect.center().x, small_icon_rect.bottom()), pos2(small_icon_rect.right(), small_icon_rect.top()), ], - visuals.fg_stroke, + check_stroke, )); } } diff --git a/crates/egui/src/widgets/separator.rs b/crates/egui/src/widgets/separator.rs index 6fdd03b96..ab211773e 100644 --- a/crates/egui/src/widgets/separator.rs +++ b/crates/egui/src/widgets/separator.rs @@ -1,4 +1,4 @@ -use crate::{Response, Sense, Ui, Vec2, Widget, vec2}; +use crate::{Response, Sense, Ui, Vec2, Widget, vec2, widget_style::SeparatorStyle}; /// A visual separator. A horizontal or vertical line (depending on [`crate::Layout`]). /// @@ -13,7 +13,7 @@ use crate::{Response, Sense, Ui, Vec2, Widget, vec2}; /// ``` #[must_use = "You should put this widget in a ui with `ui.add(widget);`"] pub struct Separator { - spacing: f32, + spacing: Option, grow: f32, is_horizontal_line: Option, } @@ -21,7 +21,7 @@ pub struct Separator { impl Default for Separator { fn default() -> Self { Self { - spacing: 6.0, + spacing: None, grow: 0.0, is_horizontal_line: None, } @@ -38,7 +38,7 @@ impl Separator { /// this is the width of the separator widget. #[inline] pub fn spacing(mut self, spacing: f32) -> Self { - self.spacing = spacing; + self.spacing = Some(spacing); self } @@ -93,6 +93,18 @@ impl Widget for Separator { is_horizontal_line, } = self; + // Get the widget style by reading the response from the previous pass + 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 SeparatorStyle { + spacing: spacing_style, + stroke, + } = ui.style().separator_style(state); + + // override the spacing if not set + let spacing = spacing.unwrap_or(spacing_style); + let is_horizontal_line = is_horizontal_line .unwrap_or_else(|| ui.is_grid() || !ui.layout().main_dir().is_horizontal()); @@ -111,7 +123,6 @@ impl Widget for Separator { let (rect, response) = ui.allocate_at_least(size, Sense::hover()); if ui.is_rect_visible(response.rect) { - let stroke = ui.visuals().widgets.noninteractive.bg_stroke; let painter = ui.painter(); if is_horizontal_line { painter.hline( diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 7d2c4c848..8865bcb6b 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -721,39 +721,42 @@ impl TextEdit<'_> { paint_text_selection(&mut galley, ui.visuals(), &cursor_range, None); } - if !clip_text { - // Allocate additional space if edits were made this frame that changed the size. This is important so that, - // if there's a ScrollArea, it can properly scroll to the cursor. - // Condition `!clip_text` is important to avoid breaking layout for `TextEdit::singleline` (PR #5640) - let extra_size = galley.size() - rect.size(); - if extra_size.x > 0.0 || extra_size.y > 0.0 { - match ui.layout().main_dir() { - crate::Direction::LeftToRight | crate::Direction::TopDown => { - ui.allocate_rect( - Rect::from_min_size(outer_rect.max, extra_size), - Sense::hover(), - ); - } - crate::Direction::RightToLeft => { - ui.allocate_rect( - Rect::from_min_size( - emath::pos2(outer_rect.min.x - extra_size.x, outer_rect.max.y), - extra_size, - ), - Sense::hover(), - ); - } - crate::Direction::BottomUp => { - ui.allocate_rect( - Rect::from_min_size( - emath::pos2(outer_rect.min.x, outer_rect.max.y - extra_size.y), - extra_size, - ), - Sense::hover(), - ); - } + // Allocate additional space if edits were made this frame that changed the size. This is important so that, + // if there's a ScrollArea, it can properly scroll to the cursor. + // Condition `!clip_text` is important to avoid breaking layout for `TextEdit::singleline` (PR #5640) + if !clip_text + && let extra_size = galley.size() - rect.size() + && (extra_size.x > 0.0 || extra_size.y > 0.0) + { + match ui.layout().main_dir() { + crate::Direction::LeftToRight | crate::Direction::TopDown => { + ui.allocate_rect( + Rect::from_min_size(outer_rect.max, extra_size), + Sense::hover(), + ); + } + crate::Direction::RightToLeft => { + ui.allocate_rect( + Rect::from_min_size( + emath::pos2(outer_rect.min.x - extra_size.x, outer_rect.max.y), + extra_size, + ), + Sense::hover(), + ); + } + crate::Direction::BottomUp => { + ui.allocate_rect( + Rect::from_min_size( + emath::pos2(outer_rect.min.x, outer_rect.max.y - extra_size.y), + extra_size, + ), + Sense::hover(), + ); } } + } else { + // Avoid an ID shift during this pass if the textedit grow + ui.skip_ahead_auto_ids(1); } painter.galley(galley_pos, galley.clone(), text_color); diff --git a/scripts/check.sh b/scripts/check.sh index ffee218f6..9c08f3fc6 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -33,21 +33,25 @@ cargo check --quiet -p egui_demo_app --lib --target wasm32-unknown-unknown --al cargo test --quiet --all-targets --all-features cargo test --quiet --doc # slow - checks all doc-tests -cargo check --quiet -p eframe --no-default-features --features "glow" if [[ "$OSTYPE" == "linux-gnu"* ]]; then + cargo check --quiet -p eframe --no-default-features --features "glow","x11" + cargo check --quiet -p eframe --no-default-features --features "glow","wayland" cargo check --quiet -p eframe --no-default-features --features "wgpu","x11" cargo check --quiet -p eframe --no-default-features --features "wgpu","wayland" else + cargo check --quiet -p eframe --no-default-features --features "glow" cargo check --quiet -p eframe --no-default-features --features "wgpu" fi cargo check --quiet -p egui --no-default-features --features "serde" -cargo check --quiet -p egui_demo_app --no-default-features --features "glow" if [[ "$OSTYPE" == "linux-gnu"* ]]; then + cargo check --quiet -p egui_demo_app --no-default-features --features "glow","x11" + cargo check --quiet -p egui_demo_app --no-default-features --features "glow","wayland" cargo check --quiet -p egui_demo_app --no-default-features --features "wgpu","x11" cargo check --quiet -p egui_demo_app --no-default-features --features "wgpu","wayland" else + cargo check --quiet -p egui_demo_app --no-default-features --features "glow" cargo check --quiet -p egui_demo_app --no-default-features --features "wgpu" fi diff --git a/tests/egui_tests/tests/snapshots/visuals/drag_value.png b/tests/egui_tests/tests/snapshots/visuals/drag_value.png index de70c841c..87030fe29 100644 --- a/tests/egui_tests/tests/snapshots/visuals/drag_value.png +++ b/tests/egui_tests/tests/snapshots/visuals/drag_value.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5ca946ae1875730db15a7e525d2edfab4b55d9a07ad72998c565ce0c7c9bea90 -size 8400 +oid sha256:540388365970accbc4c03aa34809b1f07d4e31c8d56bccb8f73da9c2292cb36a +size 8426 diff --git a/tests/egui_tests/tests/test_widgets.rs b/tests/egui_tests/tests/test_widgets.rs index 440b1939b..e810f1744 100644 --- a/tests/egui_tests/tests/test_widgets.rs +++ b/tests/egui_tests/tests/test_widgets.rs @@ -339,7 +339,7 @@ impl<'a> VisualTests<'a> { f(&mut harness); - harness.step(); + harness.run(); let image = harness.render().expect("Failed to render harness");