From 1ad451eb289ebfbd2aafe73997ffc63095cec0db Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Tue, 2 Jun 2026 13:08:27 +0200 Subject: [PATCH] Add AtomWidget --- crates/egui/src/atomics/atom.rs | 5 +- crates/egui/src/atomics/atom_widget.rs | 99 ++++++++++++++++++++++++++ crates/egui/src/atomics/mod.rs | 2 + crates/egui/src/widgets/button.rs | 46 +++++------- 4 files changed, 120 insertions(+), 32 deletions(-) create mode 100644 crates/egui/src/atomics/atom_widget.rs diff --git a/crates/egui/src/atomics/atom.rs b/crates/egui/src/atomics/atom.rs index d465d84ab..d424b9283 100644 --- a/crates/egui/src/atomics/atom.rs +++ b/crates/egui/src/atomics/atom.rs @@ -82,11 +82,12 @@ impl<'a> Atom<'a> { /// /// Example: /// ``` - /// # use egui::{AtomExt, AtomKind, Atom, Button, Id, __run_test_ui}; + /// # use egui::{AtomExt, AtomKind, Atom, AtomWidget, Button, Id, __run_test_ui}; /// # use emath::Vec2; /// # __run_test_ui(|ui| { /// let id = Id::new("my_button"); - /// let response = Button::new(("Hi!", Atom::custom(id, Vec2::splat(18.0)))).atom_ui(ui); + /// let button = Button::new(("Hi!", Atom::custom(id, Vec2::splat(18.0)))); + /// let response = button.show_for(ui).0.show(ui); /// /// let rect = response.rect(id); /// if let Some(rect) = rect { diff --git a/crates/egui/src/atomics/atom_widget.rs b/crates/egui/src/atomics/atom_widget.rs new file mode 100644 index 000000000..880e069fa --- /dev/null +++ b/crates/egui/src/atomics/atom_widget.rs @@ -0,0 +1,99 @@ +use crate::{ + Atom, AtomKind, AtomLayout, Button, Id, InnerResponse, Response, Sense, Ui, WidgetRect, +}; +use emath::Rect; + +pub fn atom() -> Atom<'static> { + Atom::default() +} + +pub trait AtomWidget<'a> { + fn atom_ui(self, ui: &mut Ui, response: &mut Response) -> AtomLayout<'a>; + + fn show_for(self, ui: &mut Ui) -> (AtomLayout<'a>, Response) + where + Self: Sized, + { + let id = ui.next_auto_id(); + ui.skip_ahead_auto_ids(1); + + let mut response = read_or_default_response(ui, id, Sense::hover()); + let mut layout = self.atom_ui(ui, &mut response); + layout = layout.id(id); + + (layout, response) + } +} + +#[macro_export] +macro_rules! impl_widget_for_atom_widget { + ($widget:ty) => { + impl Widget for $widget { + fn ui(self, ui: &mut Ui) -> Response { + let layout = self.show_for(ui).0; + ui.add(layout) + } + } + }; +} + +pub struct AtomUi<'ui, 'layout> { + ui: &'ui mut Ui, + layout: AtomLayout<'layout>, +} + +impl<'ui, 'layout> AtomUi<'ui, 'layout> { + pub fn new(ui: &'ui mut Ui) -> Self { + let layout = AtomLayout::new(()); + Self { ui, layout } + } + + pub fn add(&mut self, mut config: Atom<'layout>, widget: impl AtomWidget<'layout>) -> Response { + let (layout, response) = widget.show_for(self.ui); + + config.kind = AtomKind::Layout(Box::new(layout)); + + self.layout.push_right(config); + + response + } +} + +impl Ui { + pub fn atom_builder( + &mut self, + layout: AtomLayout, + add_contents: impl FnOnce(&mut AtomUi) -> T, + ) -> InnerResponse { + let mut ui = AtomUi { ui: self, layout }; + let inner = add_contents(&mut ui); + let AtomUi { ui, layout } = ui; + InnerResponse { + inner, + response: self.add(layout), + } + } + + pub fn atom(&mut self, add_contents: impl FnOnce(&mut AtomUi) -> T) -> InnerResponse { + self.atom_builder(AtomLayout::default(), add_contents) + } +} + +/// Read this widget's [`Response`] from a previous frame for state-based styling, or synthesize a +/// default (inactive) one if it hasn't been registered yet (e.g. the first frame). +/// +/// Mirrors the old `read_response(id).map(..).unwrap_or_default()` pattern, but yields a real +/// [`Response`] so it can feed [`Button::into_atom_ui`]. +fn read_or_default_response(ui: &Ui, id: Id, sense: Sense) -> Response { + ui.ctx().read_response(id).unwrap_or_else(|| { + ui.ctx().get_response(WidgetRect { + id, + parent_id: ui.id(), + layer_id: ui.layer_id(), + rect: Rect::NOTHING, + interact_rect: Rect::NOTHING, + sense, + enabled: ui.is_enabled(), + }) + }) +} diff --git a/crates/egui/src/atomics/mod.rs b/crates/egui/src/atomics/mod.rs index 7c8922c97..aa8919702 100644 --- a/crates/egui/src/atomics/mod.rs +++ b/crates/egui/src/atomics/mod.rs @@ -5,6 +5,7 @@ mod atom_layout; mod atoms; mod sized_atom; mod sized_atom_kind; +mod atom_widget; pub use atom::*; pub use atom_ext::*; @@ -13,3 +14,4 @@ pub use atom_layout::*; pub use atoms::*; pub use sized_atom::*; pub use sized_atom_kind::*; +pub use atom_widget::*; diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index a1d2f84ed..98c00be59 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -1,9 +1,9 @@ use epaint::Margin; use crate::{ - Atom, AtomExt as _, AtomKind, AtomLayout, AtomLayoutResponse, Atoms, Color32, CornerRadius, - Frame, Image, IntoAtoms, NumExt as _, Response, Sense, Stroke, TextStyle, TextWrapMode, Ui, - Vec2, Widget, WidgetInfo, WidgetText, WidgetType, + Atom, AtomExt as _, AtomKind, AtomLayout, AtomWidget, Atoms, Color32, CornerRadius, Frame, + Image, IntoAtoms, NumExt as _, Response, Sense, Stroke, TextStyle, TextWrapMode, Ui, Vec2, + Widget, WidgetInfo, WidgetText, WidgetType, impl_widget_for_atom_widget, widget_style::{ButtonStyle, Classes, HasClasses, SELECTED_CLASS, WidgetState}, }; @@ -285,9 +285,10 @@ impl<'a> Button<'a> { pub fn atoms(&self) -> &Atoms<'a> { &self.layout.atoms } +} - /// Show the button and return a [`AtomLayoutResponse`] for painting custom contents. - pub fn atom_ui(self, ui: &mut Ui) -> AtomLayoutResponse { +impl<'a> AtomWidget<'a> for Button<'a> { + fn atom_ui(self, ui: &mut Ui, response: &mut Response) -> AtomLayout<'a> { let Button { mut layout, fill, @@ -322,9 +323,7 @@ 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 state = response.widget_state(); classes.add_class_if(SELECTED_CLASS, selected.unwrap_or(false)); @@ -360,6 +359,10 @@ impl<'a> Button<'a> { .fallback_font(text_style.font_id.clone()) .fallback_text_color(text_style.color); + if image_tint_follows_text_color { + layout.map_images(|image| image.tint(text_style.color)); + } + // Retrocompatibility with button settings layout = if has_frame_margin && (state != WidgetState::Inactive || frame_when_inactive) { layout.frame(frame) @@ -367,28 +370,15 @@ impl<'a> Button<'a> { 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(text_style.color)); - } - - prepared.fallback_text_color = text_style.color; - - prepared.paint(ui) - } else { - AtomLayoutResponse::empty(prepared.response) - }; + layout = layout.min_size(min_size); if let Some(cursor) = ui.visuals().interact_cursor - && response.response.hovered() + && response.hovered() { ui.ctx().set_cursor_icon(cursor); } - response.response.widget_info(|| match (selected, &text) { + response.widget_info(|| match (selected, &text) { (Some(selected), Some(text)) => { WidgetInfo::selected(WidgetType::Button, ui.is_enabled(), selected, text) } @@ -402,15 +392,11 @@ impl<'a> Button<'a> { (None, None) => WidgetInfo::new(WidgetType::Button), }); - response + layout } } -impl Widget for Button<'_> { - fn ui(self, ui: &mut Ui) -> Response { - self.atom_ui(ui).response - } -} +impl_widget_for_atom_widget!(Button<'_>); impl HasClasses for Button<'_> { fn classes(&self) -> &Classes {