1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-26 14:49:06 -04:00

Add AtomWidget

This commit is contained in:
lucasmerlin
2026-06-02 13:08:27 +02:00
parent cd9e351221
commit 4167dbf48c
3 changed files with 115 additions and 30 deletions

View File

@@ -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<T>(
&mut self,
layout: AtomLayout,
add_contents: impl FnOnce(&mut AtomUi) -> T,
) -> InnerResponse<T> {
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<T>(&mut self, add_contents: impl FnOnce(&mut AtomUi) -> T) -> InnerResponse<T> {
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(),
})
})
}

View File

@@ -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::*;

View File

@@ -1,11 +1,6 @@
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, Classes, HasClasses, SELECTED_CLASS, WidgetState},
};
use crate::{Atom, AtomExt as _, AtomKind, AtomLayout, AtomLayoutResponse, Color32, CornerRadius, Frame, Id, Image, IntoAtoms, NumExt as _, Rect, Response, Sense, Stroke, TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetRect, WidgetText, WidgetType, widget_style::{ButtonStyle, Classes, HasClasses, SELECTED_CLASS, WidgetState}, AtomWidget, impl_widget_for_atom_widget};
/// Clickable button with text.
///
@@ -273,9 +268,10 @@ impl<'a> Button<'a> {
self.layout = self.layout.gap(gap);
self
}
}
/// 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,
@@ -310,9 +306,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<Response> = 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);
@@ -348,6 +342,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)
@@ -355,28 +353,18 @@ impl<'a> Button<'a> {
layout.frame(Frame::new().inner_margin(frame.inner_margin))
};
let mut prepared = layout.min_size(min_size).allocate(ui);
layout = layout.min_size(min_size);
// 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)
};
if let Some(cursor) = ui.visuals().interact_cursor
&& response.response.hovered()
&& response.hovered()
{
ui.ctx().set_cursor_icon(cursor);
}
response.response.widget_info(|| {
response.widget_info(|| {
if let Some(text) = &text {
WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), text)
} else {
@@ -384,15 +372,11 @@ impl<'a> Button<'a> {
}
});
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 {