From 84ba1578e77b59bdcccc9a1a638afea04db584f0 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 18 Aug 2026 11:24:25 +0200 Subject: [PATCH] Impl `AtomWidget` for `Checkbox` --- crates/egui/src/atomics/atom.rs | 25 ++++- crates/egui/src/atomics/atom_kind.rs | 28 ++++- crates/egui/src/atomics/atom_layout.rs | 3 + crates/egui/src/atomics/atom_widget.rs | 17 +-- crates/egui/src/atomics/sized_atom_kind.rs | 21 +++- crates/egui/src/widgets/checkbox.rs | 119 ++++++++++----------- 6 files changed, 136 insertions(+), 77 deletions(-) diff --git a/crates/egui/src/atomics/atom.rs b/crates/egui/src/atomics/atom.rs index d424b9283..a35ee8938 100644 --- a/crates/egui/src/atomics/atom.rs +++ b/crates/egui/src/atomics/atom.rs @@ -1,7 +1,7 @@ use crate::{ AtomKind, AtomLayout, FontSelection, Id, IntoSizedArgs, IntoSizedResult, SizedAtom, Ui, }; -use emath::{Align2, NumExt as _, Vec2}; +use emath::{Align2, NumExt as _, Rect, Vec2}; use epaint::text::TextWrapMode; /// A low-level ui building block. @@ -104,6 +104,29 @@ impl<'a> Atom<'a> { } } + /// Create an [`AtomKind::Paint`] with a specific size. + /// + /// The closure paints the atom at the [`Rect`] the layout gives it. + /// + /// Example: + /// ``` + /// # use egui::{Atom, AtomWidget, Button, Color32, CornerRadius, __run_test_ui}; + /// # use emath::Vec2; + /// # __run_test_ui(|ui| { + /// let dot = Atom::paint(Vec2::splat(8.0), |ui, rect| { + /// ui.painter().rect_filled(rect, CornerRadius::same(4), Color32::RED); + /// }); + /// ui.add(Button::new((dot, "Recording"))); + /// # }); + /// ``` + pub fn paint(size: impl Into, func: impl Fn(&Ui, Rect) + 'a) -> Self { + Atom { + size: Some(size.into()), + kind: AtomKind::paint(func), + ..Default::default() + } + } + /// Nest an [`AtomLayout`] (e.g. an atom-based widget) as a single atom. /// /// The nested layout is sized when the parent is sized and painted (and interacted with) diff --git a/crates/egui/src/atomics/atom_kind.rs b/crates/egui/src/atomics/atom_kind.rs index 0dfe56069..4106cb3da 100644 --- a/crates/egui/src/atomics/atom_kind.rs +++ b/crates/egui/src/atomics/atom_kind.rs @@ -1,7 +1,8 @@ use crate::{AtomLayout, FontSelection, Image, ImageSource, SizedAtomKind, Ui, WidgetText}; use core::fmt::Debug; -use emath::Vec2; +use emath::{Rect, Vec2}; use epaint::text::TextWrapMode; +use std::sync::Arc; /// Args passed when sizing an [`super::Atom`] pub struct IntoSizedArgs { @@ -21,6 +22,11 @@ pub struct IntoSizedResult<'a> { // Otherwise, a single 'static Atom would force the closure to be 'static. pub type AtomClosure<'a> = Box IntoSizedResult<'static> + 'a>; +/// See [`AtomKind::Paint`] +/// +/// It is an [`Arc`] so the atom stays cloneable. +pub type AtomPaint<'a> = Arc; + /// The different kinds of [`crate::Atom`]s. #[derive(Default)] pub enum AtomKind<'a> { @@ -66,6 +72,15 @@ pub enum AtomKind<'a> { /// When cloning, this will be cloned as [`AtomKind::Empty`]. Closure(AtomClosure<'a>), + /// A closure that paints the atom at the [`Rect`] the layout gives it. + /// + /// It has no size of its own, so set one with [`crate::AtomExt::atom_size`], or use + /// [`crate::Atom::paint`], which does that for you. + /// + /// Use this for widgets that draw their own shapes, like the check mark of + /// [`crate::Checkbox`]. + Paint(AtomPaint<'a>), + /// A nested [`AtomLayout`], letting you embed an atom-based widget as a single atom /// inside another [`AtomLayout`]. /// @@ -84,6 +99,7 @@ impl Clone for AtomKind<'_> { log::warn!("Cannot clone atom closures"); AtomKind::Empty } + AtomKind::Paint(paint) => AtomKind::Paint(Arc::clone(paint)), AtomKind::Layout(layout) => AtomKind::Layout(layout.clone()), } } @@ -96,6 +112,7 @@ impl Debug for AtomKind<'_> { AtomKind::Text(text) => write!(f, "AtomKind::Text({text:?})"), AtomKind::Image(image) => write!(f, "AtomKind::Image({image:?})"), AtomKind::Closure(_) => write!(f, "AtomKind::Closure()"), + AtomKind::Paint(_) => write!(f, "AtomKind::Paint()"), AtomKind::Layout(_) => write!(f, "AtomKind::Layout()"), } } @@ -112,6 +129,11 @@ impl<'a> AtomKind<'a> { AtomKind::Image(image.into()) } + /// See [`Self::Paint`] + pub fn paint(func: impl Fn(&Ui, Rect) + 'a) -> Self { + AtomKind::Paint(Arc::new(func)) + } + /// See [`Self::Closure`] pub fn closure(func: impl FnOnce(&Ui, IntoSizedArgs) -> IntoSizedResult<'static> + 'a) -> Self { AtomKind::Closure(Box::new(func)) @@ -158,6 +180,10 @@ impl<'a> AtomKind<'a> { fallback_font, }, ), + AtomKind::Paint(paint) => IntoSizedResult { + intrinsic_size: Vec2::ZERO, + sized: SizedAtomKind::Paint(paint), + }, AtomKind::Layout(layout) => { let sized = layout.measure(ui, available_size); IntoSizedResult { diff --git a/crates/egui/src/atomics/atom_layout.rs b/crates/egui/src/atomics/atom_layout.rs index 5c7da5cc7..e6e34e128 100644 --- a/crates/egui/src/atomics/atom_layout.rs +++ b/crates/egui/src/atomics/atom_layout.rs @@ -673,6 +673,9 @@ impl<'atom> SizedAtomLayout<'atom> { image.paint_at(ui, item_rect); } SizedAtomKind::Empty { .. } => {} + SizedAtomKind::Paint(paint) => { + paint(ui, item_rect); + } SizedAtomKind::Layout(layout) => { // TODO(lucasmerlin): Add some kind of justify flag, right now nested atoms are always // shown fully stretched. diff --git a/crates/egui/src/atomics/atom_widget.rs b/crates/egui/src/atomics/atom_widget.rs index e303dc95c..7a234b825 100644 --- a/crates/egui/src/atomics/atom_widget.rs +++ b/crates/egui/src/atomics/atom_widget.rs @@ -151,7 +151,7 @@ impl<'ui, 'layout> AtomUi<'ui, 'layout> { &mut self, builder: AtomLayout<'layout>, mut atom: Atom<'layout>, - add_content: impl FnOnce(&mut AtomUi<'_, '_>) -> R, + add_content: impl FnOnce(&mut AtomUi<'_, 'layout>) -> R, ) -> InnerResponse { let mut child = AtomUi::new(self.ctx, builder); let inner = add_content(&mut child); @@ -167,7 +167,7 @@ impl<'ui, 'layout> AtomUi<'ui, 'layout> { pub fn vertical( &mut self, atom: Atom<'layout>, - add_content: impl FnOnce(&mut AtomUi<'_, '_>) -> R, + add_content: impl FnOnce(&mut AtomUi<'_, 'layout>) -> R, ) -> InnerResponse { self.scope_builder( AtomLayout::default().direction(Direction::TopDown), @@ -253,10 +253,10 @@ impl<'ui, 'layout> AtomUi<'ui, 'layout> { } impl Ui { - pub fn atom_builder( + pub fn atom_builder<'a, T>( &mut self, - builder: AtomLayout<'_>, - add_contents: impl FnOnce(&mut AtomUi<'_, '_>) -> T, + builder: AtomLayout<'a>, + add_contents: impl FnOnce(&mut AtomUi<'_, 'a>) -> T, ) -> InnerResponse { let mut ui = AtomUi::new(self, builder); let inner = add_contents(&mut ui); @@ -267,11 +267,12 @@ impl Ui { } } - pub fn atom( + pub fn atom<'a, T>( &mut self, - add_contents: impl FnOnce(&mut AtomUi<'_, '_>) -> T, + add_contents: impl FnOnce(&mut AtomUi<'_, 'a>) -> T, ) -> InnerResponse { - self.atom_builder(AtomLayout::default(), add_contents) + let direction = self.layout().main_dir(); + self.atom_builder(AtomLayout::default().direction(direction), add_contents) } } diff --git a/crates/egui/src/atomics/sized_atom_kind.rs b/crates/egui/src/atomics/sized_atom_kind.rs index 7dc0b7c40..691fb586d 100644 --- a/crates/egui/src/atomics/sized_atom_kind.rs +++ b/crates/egui/src/atomics/sized_atom_kind.rs @@ -1,17 +1,33 @@ -use crate::{Image, SizedAtomLayout}; +use crate::{AtomPaint, Image, SizedAtomLayout}; +use core::fmt::Debug; use emath::Vec2; use epaint::Galley; use std::sync::Arc; /// A sized [`crate::AtomKind`]. -#[derive(Clone, Debug)] +#[derive(Clone)] pub enum SizedAtomKind<'a> { Empty { size: Option }, Text(Arc), Image { image: Image<'a>, size: Vec2 }, + Paint(AtomPaint<'a>), Layout(Box>), } +impl Debug for SizedAtomKind<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + SizedAtomKind::Empty { size } => write!(f, "SizedAtomKind::Empty({size:?})"), + SizedAtomKind::Text(galley) => write!(f, "SizedAtomKind::Text({galley:?})"), + SizedAtomKind::Image { image, size } => { + write!(f, "SizedAtomKind::Image({image:?}, {size:?})") + } + SizedAtomKind::Paint(_) => write!(f, "SizedAtomKind::Paint()"), + SizedAtomKind::Layout(layout) => write!(f, "SizedAtomKind::Layout({layout:?})"), + } + } +} + impl Default for SizedAtomKind<'_> { fn default() -> Self { Self::Empty { size: None } @@ -25,6 +41,7 @@ impl SizedAtomKind<'_> { SizedAtomKind::Text(galley) => galley.size(), SizedAtomKind::Image { image: _, size } => *size, SizedAtomKind::Empty { size } => size.unwrap_or_default(), + SizedAtomKind::Paint(_) => Vec2::ZERO, SizedAtomKind::Layout(layout) => layout.outer_size, } } diff --git a/crates/egui/src/widgets/checkbox.rs b/crates/egui/src/widgets/checkbox.rs index 0aaedec76..5907c9d93 100644 --- a/crates/egui/src/widgets/checkbox.rs +++ b/crates/egui/src/widgets/checkbox.rs @@ -1,8 +1,8 @@ use emath::Rect; use crate::{ - Atom, AtomLayout, Atoms, Id, IntoAtoms, NumExt as _, Response, Sense, Shape, Ui, Vec2, Widget, - WidgetInfo, WidgetType, epaint, pos2, + Atom, AtomLayout, AtomWidget, AtomWidgetContext, Atoms, IntoAtoms, NumExt as _, Response, + Sense, Shape, Vec2, WidgetInfo, WidgetType, epaint, impl_widget_for_atom_widget, pos2, widget_style::{CheckboxStyle, Classes, HasClasses}, }; @@ -59,8 +59,8 @@ impl<'a> Checkbox<'a> { } } -impl Widget for Checkbox<'_> { - fn ui(self, ui: &mut Ui) -> Response { +impl<'a> AtomWidget<'a> for Checkbox<'a> { + fn atom_ui(self, ui: &mut AtomWidgetContext, response: &mut Response) -> AtomLayout<'a> { let Checkbox { checked, mut atoms, @@ -68,10 +68,11 @@ impl Widget for Checkbox<'_> { classes, } = 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(); + if response.clicked() { + *checked = !*checked; + response.mark_changed(); + } + let checked = *checked; let CheckboxStyle { check_size, @@ -80,7 +81,7 @@ impl Widget for Checkbox<'_> { frame, check_stroke, text_style, - } = ui.style().checkbox_style(&classes, state); + } = ui.style().checkbox_style(&classes, response.widget_state()); let mut min_size = Vec2::splat(ui.spacing().interact_size.y); min_size.y = min_size.y.at_least(checkbox_size); @@ -88,22 +89,45 @@ impl Widget for Checkbox<'_> { // 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(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)); + atoms.push_left(Atom::paint(icon_size, move |ui, 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(checkbox_frame.inner_margin.left.into()), + checkbox_frame.corner_radius, + checkbox_frame.fill, + checkbox_frame.stroke, + epaint::StrokeKind::Inside, + )); + + if indeterminate { + // Horizontal line: + ui.painter().add(Shape::hline( + small_icon_rect.x_range(), + small_icon_rect.center().y, + check_stroke, + )); + } else if checked { + // Check mark: + ui.painter().add(Shape::line( + vec![ + pos2(small_icon_rect.left(), small_icon_rect.center().y), + pos2(small_icon_rect.center().x, small_icon_rect.bottom()), + pos2(small_icon_rect.right(), small_icon_rect.top()), + ], + check_stroke, + )); + } + })); let text = atoms.text().map(String::from); - let mut prepared = AtomLayout::new(atoms) - .sense(Sense::click()) - .min_size(min_size) - .frame(frame) - .allocate(ui); - - if prepared.response.clicked() { - *checked = !*checked; - prepared.response.mark_changed(); - } - prepared.response.widget_info(|| { + response.widget_info(|| { if indeterminate { WidgetInfo::labeled( WidgetType::Checkbox, @@ -114,57 +138,22 @@ impl Widget for Checkbox<'_> { WidgetInfo::selected( WidgetType::Checkbox, ui.is_enabled(), - *checked, + checked, text.as_deref().unwrap_or(""), ) } }); - if ui.is_rect_visible(prepared.response.rect) { - prepared.fallback_text_color = text_style.color; - let response = prepared.paint(ui); - - if let Some(rect) = response.rect(rect_id) { - 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(checkbox_frame.inner_margin.left.into()), - checkbox_frame.corner_radius, - checkbox_frame.fill, - checkbox_frame.stroke, - epaint::StrokeKind::Inside, - )); - - if indeterminate { - // Horizontal line: - ui.painter().add(Shape::hline( - small_icon_rect.x_range(), - small_icon_rect.center().y, - check_stroke, - )); - } else if *checked { - // Check mark: - ui.painter().add(Shape::line( - vec![ - pos2(small_icon_rect.left(), small_icon_rect.center().y), - pos2(small_icon_rect.center().x, small_icon_rect.bottom()), - pos2(small_icon_rect.right(), small_icon_rect.top()), - ], - check_stroke, - )); - } - } - response.response - } else { - prepared.response - } + AtomLayout::new(atoms) + .sense(Sense::click()) + .min_size(min_size) + .frame(frame) + .fallback_text_color(text_style.color) } } +impl_widget_for_atom_widget!(Checkbox<'_>); + impl HasClasses for Checkbox<'_> { fn classes(&self) -> &Classes { &self.classes