diff --git a/crates/egui/src/atomics/atom_widget.rs b/crates/egui/src/atomics/atom_widget.rs new file mode 100644 index 000000000000..4a0a7c151271 --- /dev/null +++ b/crates/egui/src/atomics/atom_widget.rs @@ -0,0 +1,288 @@ +use crate::{Atom, AtomExt, AtomKind, AtomLayout, Atoms, Button, Color32, Context, Id, InnerResponse, IntoAtoms, Layout, Response, Sense, Spacing, Style, Ui, UiBuilder, Visuals, Widget, WidgetRect}; +use emath::{Align, Pos2, Rect, Vec2}; +use epaint::Direction; + +pub fn atom() -> Atom<'static> { + Atom::default() +} + +pub trait AtomWidget<'a> { + fn atom_ui(self, ui: &mut AtomWidgetContext, response: &mut Response) -> AtomLayout<'a>; + + fn show_for(self, ui: &mut AtomWidgetContext) -> (AtomLayout<'a>, Response) + where + Self: Sized, + { + let id = ui.make_auto_id(); + let mut response = ui.read_response(id); + + let mut layout = self.atom_ui(ui, &mut response); + layout = layout.id(id); + + (layout, response) + } +} + +impl<'a> AtomWidget<'a> for AtomLayout<'a> { + fn atom_ui(self, ui: &mut AtomWidgetContext, response: &mut Response) -> AtomLayout<'a> { + self + } +} + +impl<'a> AtomWidget<'a> for Atom<'a> { + fn atom_ui(self, ui: &mut AtomWidgetContext, response: &mut Response) -> AtomLayout<'a> { + AtomLayout::new(self) + } +} +impl<'a> AtomWidget<'a> for AtomKind<'a> { + fn atom_ui(self, ui: &mut AtomWidgetContext, response: &mut Response) -> AtomLayout<'a> { + AtomLayout::new(self) + } +} +impl<'a> AtomWidget<'a> for Atoms<'a> { + fn atom_ui(self, ui: &mut AtomWidgetContext, response: &mut Response) -> AtomLayout<'a> { + AtomLayout::new(self) + } +} + +#[macro_export] +macro_rules! impl_widget_for_atom_widget { + ($widget:ty) => { + impl $crate::Widget for $widget { + fn ui(self, ui: &mut $crate::Ui) -> $crate::Response { + let layout = self.show_for(ui).0; + ui.add(layout) + } + } + }; +} + +pub trait IsAtomWidgetContext { + fn ctx(&self) -> &crate::Context; + fn make_auto_id(&mut self) -> Id; + + fn is_enabled(&self) -> bool; + + fn style(&self) -> &Style; + fn style_mut(&mut self) -> &mut Style; + + fn spacing(&self) -> &Spacing { + &self.style().spacing + } + fn spacing_mut(&mut self) -> &mut Spacing { + &mut self.style_mut().spacing + } + + fn visuals(&self) -> &Visuals { + &self.style().visuals + } + fn visuals_mut(&mut self) -> &mut Visuals { + &mut self.style_mut().visuals + } + + fn read_response(&self, id: Id) -> Response; + + fn child_ui(&mut self, builder: UiBuilder) -> Ui; +} + +pub type AtomWidgetContext = dyn IsAtomWidgetContext; + +impl IsAtomWidgetContext for Ui { + fn ctx(&self) -> &Context { + self.ctx() + } + + fn make_auto_id(&mut self) -> Id { + let id = self.next_auto_id(); + self.skip_ahead_auto_ids(1); + id + } + + fn is_enabled(&self) -> bool { + self.is_enabled() + } + + fn style(&self) -> &Style { + self.style() + } + + fn style_mut(&mut self) -> &mut Style { + self.style_mut() + } + + fn read_response(&self, id: Id) -> Response { + read_or_default_response(&self, id, Sense::hover()) + } + + fn child_ui(&mut self, builder: UiBuilder) -> Ui { + Ui::new_child(self, builder) + } +} + +pub struct AtomUi<'ui, 'layout> { + ctx: &'ui mut AtomWidgetContext, + layout: AtomLayout<'layout>, +} + +impl<'ui, 'layout> AtomUi<'ui, 'layout> { + pub fn new(ctx: &'ui mut AtomWidgetContext, builder: AtomLayout<'layout>) -> Self { + let layout = builder.id(ctx.make_auto_id()); + Self { ctx, layout } + } + + pub fn response(&self) -> Response { + self.ctx + .read_response(self.layout.id.expect("set in constructor")) + } + + pub fn add(&mut self, mut config: Atom<'layout>, widget: impl AtomWidget<'layout>) -> Response { + let (layout, response) = widget.show_for(self.ctx); + + config.kind = AtomKind::Layout(Box::new(layout)); + self.layout.push_right(config); + + response + } + + pub fn scope_builder( + &mut self, + builder: AtomLayout<'layout>, + mut atom: Atom<'layout>, + add_content: impl FnOnce(&mut AtomUi) -> R, + ) -> InnerResponse { + let mut child = AtomUi::new(self.ctx, builder); + let inner = add_content(&mut child); + let response = InnerResponse { + inner, + response: child.response(), + }; + atom.kind = AtomKind::Layout(Box::new(child.layout)); + self.layout.push_right(atom); + response + } + + pub fn vertical( + &mut self, + atom: Atom<'layout>, + add_content: impl FnOnce(&mut AtomUi) -> R, + ) -> InnerResponse { + self.scope_builder( + AtomLayout::default().direction(Direction::TopDown), + atom, + add_content, + ) + } + + pub fn show(self) -> (AtomLayout<'layout>, Response) { + let response = self.response(); + (self.layout, response) + } + + pub fn immediate_scope( + &mut self, + mut ui_builder: UiBuilder, + mut atom: Atom<'layout>, + add_content: impl FnOnce(&mut Ui) -> R, + ) -> InnerResponse { + let sizing_id = self.ctx.make_auto_id(); + let mut sizing_response = self.ctx.read_response(sizing_id); + + let mut size = Vec2::ZERO; + if sizing_response.rect.is_finite() && sizing_response.rect.is_positive() { + size = sizing_response + .intrinsic_size() + .unwrap_or(sizing_response.rect.size()); + } + + let placement_response = self.add(atom.clone(), AtomLayout::new(atom.atom_size(size))); + + if placement_response.rect.is_finite() && placement_response.rect.is_positive() { + ui_builder = ui_builder.max_rect(Rect::from_min_size( + placement_response.rect.min, + Vec2::INFINITY, + )); + } else { + ui_builder = ui_builder + .max_rect(Rect::from_min_size(Pos2::ZERO, Vec2::INFINITY)) + .invisible(); + } + ui_builder = ui_builder + .id(sizing_id) + .layout(Layout::left_to_right(Align::Min)); + + let mut immediate_ui = self.ctx.child_ui(ui_builder); + let inner = add_content(&mut immediate_ui); + + InnerResponse { + inner, + response: sizing_response, + } + } + + /// Show a label which can be selected or not. + /// + /// See also [`Button::selectable`] and [`Self::toggle_value`]. + #[must_use = "You should check if the user clicked this with `if ui.selectable_label(…).clicked() { … } "] + pub fn selectable_label(&mut self, checked: bool, text: impl IntoAtoms<'layout>) -> Response { + self.add(atom().atom_grow(true), Button::selectable(checked, text)) + } + + /// Show selectable text. It is selected if `*current_value == selected_value`. + /// If clicked, `selected_value` is assigned to `*current_value`. + /// + /// Example: `ui.selectable_value(&mut my_enum, Enum::Alternative, "Alternative")`. + /// + /// See also [`Button::selectable`] and [`Self::toggle_value`]. + pub fn selectable_value( + &mut self, + current_value: &mut Value, + selected_value: Value, + text: impl IntoAtoms<'layout>, + ) -> Response { + let mut response = self.selectable_label(*current_value == selected_value, text); + if response.clicked() && *current_value != selected_value { + *current_value = selected_value; + response.mark_changed(); + } + response + } +} + +impl Ui { + pub fn atom_builder( + &mut self, + builder: AtomLayout, + add_contents: impl FnOnce(&mut AtomUi) -> T, + ) -> InnerResponse { + let mut ui = AtomUi::new(self, builder); + let inner = add_contents(&mut ui); + let AtomUi { ctx, 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 7c8922c97ed4..aa89197027c4 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 a1526c50e5b0..c712e138eed8 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -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, AtomWidgetContext}; /// 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 AtomWidgetContext, response: &mut Response) -> AtomLayout<'a> { let Button { mut layout, fill, @@ -299,7 +295,9 @@ impl<'a> Button<'a> { if limit_image_size { layout.map_atoms(|atom| { if matches!(&atom.kind, AtomKind::Image(_)) { - atom.atom_max_height_font_size(ui) + // TODO + // atom.atom_max_height_font_size(ui) + atom } else { atom } @@ -310,9 +308,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); @@ -348,6 +344,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 +355,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 +374,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 {